diff --git a/.chronus/changes/remove-globby-2026-3-10-15-46-5.md b/.chronus/changes/remove-globby-2026-3-10-15-46-5.md new file mode 100644 index 00000000000..71d3e8a5694 --- /dev/null +++ b/.chronus/changes/remove-globby-2026-3-10-15-46-5.md @@ -0,0 +1,12 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/compiler" + - "@typespec/http-client-js" + - "@typespec/http-server-csharp" + - "@typespec/http-server-js" + - "@typespec/spector" +--- + +Replace globby with node built-ins diff --git a/cspell.yaml b/cspell.yaml index 1ec94512ccd..d260efe8658 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -92,7 +92,6 @@ words: - genproto - getpgid - giacamo - - globby - graalvm - Gson - Hdvcmxk diff --git a/packages/bundle-uploader/package.json b/packages/bundle-uploader/package.json index 72ac4eab4e5..1f9b20e41a9 100644 --- a/packages/bundle-uploader/package.json +++ b/packages/bundle-uploader/package.json @@ -41,7 +41,6 @@ "@azure/storage-blob": "catalog:", "@pnpm/workspace.find-packages": "catalog:", "@typespec/bundler": "workspace:^", - "globby": "catalog:", "picocolors": "catalog:", "semver": "catalog:" }, diff --git a/packages/bundle-uploader/src/index.ts b/packages/bundle-uploader/src/index.ts index abf805d40a9..dadd7dd8ad3 100644 --- a/packages/bundle-uploader/src/index.ts +++ b/packages/bundle-uploader/src/index.ts @@ -1,8 +1,7 @@ import { AzureCliCredential } from "@azure/identity"; import { findWorkspacePackagesNoCheck } from "@pnpm/workspace.find-packages"; import { createTypeSpecBundle } from "@typespec/bundler"; -import { readFile } from "fs/promises"; -import { globby } from "globby"; +import { glob, readFile } from "fs/promises"; import { relative, resolve } from "path"; import { join as joinUnix } from "path/posix"; import pc from "picocolors"; @@ -99,7 +98,10 @@ async function uploadPlaygroundAssets( // Upload static assets (e.g. .whl files) if (config.assets) { for (const asset of config.assets) { - const matchedFiles = await globby(asset.path, { cwd: packagePath, absolute: true }); + const matchedFiles: string[] = []; + for await (const file of glob(asset.path, { cwd: packagePath })) { + matchedFiles.push(resolve(packagePath, file)); + } if (matchedFiles.length === 0) { logInfo(pc.yellow(`⚠ No files matched asset pattern: ${asset.path}`)); continue; diff --git a/packages/compiler/package.json b/packages/compiler/package.json index e13549b93eb..7917102f533 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -110,7 +110,6 @@ "ajv": "catalog:", "change-case": "catalog:", "env-paths": "catalog:", - "globby": "catalog:", "is-unicode-supported": "catalog:", "mustache": "catalog:", "picocolors": "catalog:", diff --git a/packages/compiler/src/core/formatter-fs.ts b/packages/compiler/src/core/formatter-fs.ts index a2570ac9487..35cf3499bd1 100644 --- a/packages/compiler/src/core/formatter-fs.ts +++ b/packages/compiler/src/core/formatter-fs.ts @@ -1,5 +1,5 @@ -import { readFile, writeFile } from "fs/promises"; -import { globby } from "globby"; +import { glob, readFile, stat, writeFile } from "fs/promises"; +import { join } from "path"; import { resolveConfig } from "prettier"; import { PrettierParserError } from "../formatter/parser.js"; import { checkFormat, format, getFormatterFromFilename } from "./formatter.js"; @@ -177,10 +177,49 @@ export async function checkFileFormat(filename: string): Promise { - const patterns = [ - ...include.map(normalizePath), - "!**/node_modules", - ...ignore.map((x) => `!${normalizePath(x)}`), - ]; - return globby(patterns); + const expandedInclude = await expandDirectoryPatterns(include); + const results: string[] = []; + for await (const entry of glob(expandedInclude, { + withFileTypes: true, + exclude: ["**/node_modules", ...ignore], + })) { + if (entry.isFile()) { + results.push(join(entry.parentPath, entry.name)); + } + } + return results; +} + +/** + * Expand bare directory paths to glob patterns. + * A directory "src" becomes both "src" and "src/**\/*" so it matches the + * directory entry itself (for exclude short-circuiting) and its contents. + * Glob patterns ending in "/**" also get the bare directory form added. + */ +async function expandDirectoryPatterns(patterns: string[]): Promise { + const expanded: string[] = []; + for (const pattern of patterns) { + if (/[*?{[]/.test(pattern)) { + expanded.push(normalizePath(pattern)); + // Also match the directory itself so exclude can short-circuit traversal + const normalized = normalizePath(pattern); + if (normalized.endsWith("/**/*")) { + expanded.push(normalized.slice(0, -4)); + } else if (normalized.endsWith("/**")) { + expanded.push(normalized.slice(0, -3)); + } + } else { + try { + if ((await stat(pattern)).isDirectory()) { + expanded.push(normalizePath(pattern)); + expanded.push(normalizePath(`${pattern}/**/*`)); + continue; + } + } catch { + // not a valid path — treat as glob pattern + } + expanded.push(normalizePath(pattern)); + } + } + return expanded; } diff --git a/packages/compiler/src/testing/test-host.ts b/packages/compiler/src/testing/test-host.ts index 0902f1b3960..68ec16f523a 100644 --- a/packages/compiler/src/testing/test-host.ts +++ b/packages/compiler/src/testing/test-host.ts @@ -1,5 +1,5 @@ import assert from "assert"; -import { globby } from "globby"; +import { glob } from "fs/promises"; import { logDiagnostics, logVerboseTestOutput } from "../core/diagnostics.js"; import { createLogger } from "../core/logger/logger.js"; import { CompilerOptions } from "../core/options.js"; @@ -82,9 +82,12 @@ async function createTestHostInternal(): Promise { } export async function findFilesFromPattern(directory: string, pattern: string): Promise { - return globby(pattern, { + const results: string[] = []; + for await (const file of glob(pattern, { cwd: directory, - onlyFiles: true, - ignore: ["**/*.{test,spec}.{ts,tsx,js,jsx}"], - }); + exclude: ["**/*.{test,spec}.{ts,tsx,js,jsx}"], + })) { + results.push(file); + } + return results; } diff --git a/packages/compiler/test/core/formatter-fs.test.ts b/packages/compiler/test/core/formatter-fs.test.ts new file mode 100644 index 00000000000..70fe37c8153 --- /dev/null +++ b/packages/compiler/test/core/formatter-fs.test.ts @@ -0,0 +1,119 @@ +import { mkdir, rm, writeFile } from "fs/promises"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { beforeAll, describe, expect, it } from "vitest"; +import { checkFilesFormat } from "../../src/core/formatter-fs.js"; +import { resolvePath } from "../../src/core/path-utils.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const fixtureRoot = resolvePath(__dirname, "../../temp/test/formatter-fs"); + +const wellFormattedTsp = `model Foo { + name: string; +} +`; +const unformattedTsp = `model + Foo { + name: string; +} +`; + +function fixturePath(...segments: string[]) { + return join(fixtureRoot, ...segments); +} + +async function createFixtureFile(relativePath: string, content = "") { + const fullPath = fixturePath(relativePath); + await mkdir(dirname(fullPath), { recursive: true }); + await writeFile(fullPath, content); +} + +function allFiles(result: Awaited>): string[] { + return [...result.formatted, ...result.needsFormat, ...result.ignored].sort(); +} + +beforeAll(async () => { + await rm(fixtureRoot, { recursive: true, force: true }); + await mkdir(fixtureRoot, { recursive: true }); + + await createFixtureFile("project/main.tsp", wellFormattedTsp); + await createFixtureFile("project/lib.tsp", unformattedTsp); + await createFixtureFile("project/sub/nested.tsp", wellFormattedTsp); + await createFixtureFile("project/readme.md", "# Readme"); + await createFixtureFile("project/node_modules/dep/index.tsp", unformattedTsp); + await createFixtureFile("project/excluded/skip.tsp", unformattedTsp); +}); + +describe("formatter-fs: findFiles", () => { + it("finds .tsp files with explicit glob pattern", async () => { + const result = await checkFilesFormat([fixturePath("project/**/*.tsp")], {}); + expect(allFiles(result)).toEqual([ + fixturePath("project/excluded/skip.tsp"), + fixturePath("project/lib.tsp"), + fixturePath("project/main.tsp"), + fixturePath("project/sub/nested.tsp"), + ]); + }); + + it("expands bare directory path to find files recursively", async () => { + const result = await checkFilesFormat([fixturePath("project")], {}); + expect(allFiles(result)).toEqual([ + fixturePath("project/excluded/skip.tsp"), + fixturePath("project/lib.tsp"), + fixturePath("project/main.tsp"), + fixturePath("project/readme.md"), + fixturePath("project/sub/nested.tsp"), + ]); + }); + + it("excludes node_modules automatically", async () => { + const result = await checkFilesFormat([fixturePath("project/**/*.tsp")], {}); + expect(allFiles(result)).not.toContainEqual(expect.stringContaining("node_modules")); + }); + + it("excludes node_modules when using directory expansion", async () => { + const result = await checkFilesFormat([fixturePath("project")], {}); + expect(allFiles(result)).not.toContainEqual(expect.stringContaining("node_modules")); + }); + + it("respects user-provided exclude patterns", async () => { + const result = await checkFilesFormat([fixturePath("project/**/*.tsp")], { + exclude: [fixturePath("project/excluded/**")], + }); + expect(allFiles(result)).toEqual([ + fixturePath("project/lib.tsp"), + fixturePath("project/main.tsp"), + fixturePath("project/sub/nested.tsp"), + ]); + }); + + it("handles multiple include patterns", async () => { + const result = await checkFilesFormat( + [fixturePath("project/main.tsp"), fixturePath("project/sub/**/*.tsp")], + {}, + ); + expect(allFiles(result)).toEqual([ + fixturePath("project/main.tsp"), + fixturePath("project/sub/nested.tsp"), + ]); + }); + + it("classifies non-tsp files as ignored", async () => { + const result = await checkFilesFormat([fixturePath("project/readme.md")], {}); + expect(result.ignored).toEqual([fixturePath("project/readme.md")]); + }); + + it("returns empty results when nothing matches", async () => { + const result = await checkFilesFormat([fixturePath("project/**/*.py")], {}); + expect(allFiles(result)).toEqual([]); + }); + + it("correctly identifies formatted vs needs-format files", async () => { + const result = await checkFilesFormat( + [fixturePath("project/main.tsp"), fixturePath("project/lib.tsp")], + {}, + ); + expect(result.formatted).toEqual([fixturePath("project/main.tsp")]); + expect(result.needsFormat).toEqual([fixturePath("project/lib.tsp")]); + }); +}); diff --git a/packages/http-client-js/eng/scripts/emit-e2e.js b/packages/http-client-js/eng/scripts/emit-e2e.js index 7861d4ca8a3..15a22015af8 100644 --- a/packages/http-client-js/eng/scripts/emit-e2e.js +++ b/packages/http-client-js/eng/scripts/emit-e2e.js @@ -2,8 +2,7 @@ /* eslint-disable no-console */ import { select } from "@inquirer/prompts"; import { execa } from "execa"; -import { access, copyFile, mkdir, readFile, rm, stat, writeFile } from "fs/promises"; -import { globby } from "globby"; +import { access, copyFile, glob, mkdir, readFile, rm, stat, writeFile } from "fs/promises"; import ora from "ora"; import pLimit from "p-limit"; import { basename, dirname, join, resolve } from "path"; @@ -93,7 +92,10 @@ async function processPaths(paths, ignoreList, mainOnly) { results.push({ fullPath, relativePath }); } else if (stats.isDirectory()) { const patterns = mainOnly ? ["**/main.tsp"] : ["**/client.tsp", "**/main.tsp"]; - const discoveredPaths = await globby(patterns, { cwd: fullPath }); + const discoveredPaths = []; + for await (const p of glob(patterns, { cwd: fullPath })) { + discoveredPaths.push(p); + } const validFiles = discoveredPaths .map((p) => ({ fullPath: join(fullPath, p), diff --git a/packages/http-client-js/package.json b/packages/http-client-js/package.json index dbde3f6c00d..383ee7015cb 100644 --- a/packages/http-client-js/package.json +++ b/packages/http-client-js/package.json @@ -74,7 +74,6 @@ "concurrently": "catalog:", "cross-env": "catalog:", "execa": "catalog:", - "globby": "catalog:", "@inquirer/prompts": "catalog:", "ora": "catalog:", "p-limit": "catalog:", diff --git a/packages/http-server-csharp/eng/scripts/emit-scenarios.ts b/packages/http-server-csharp/eng/scripts/emit-scenarios.ts index 149d19882a2..c5c39aca986 100644 --- a/packages/http-server-csharp/eng/scripts/emit-scenarios.ts +++ b/packages/http-server-csharp/eng/scripts/emit-scenarios.ts @@ -1,8 +1,7 @@ /* eslint-disable no-console */ import { select } from "@inquirer/prompts"; import { run } from "@typespec/internal-build-utils"; -import { access, copyFile, mkdir, readFile, rm, writeFile } from "fs/promises"; -import { globby } from "globby"; +import { access, copyFile, glob, mkdir, readFile, rm, writeFile } from "fs/promises"; import ora from "ora"; import pLimit from "p-limit"; import { basename, dirname, join, resolve } from "pathe"; @@ -80,7 +79,10 @@ async function copySelectiveFiles( sourceDir: string, targetDir: string, ): Promise { - const files = await globby(extension, { cwd: sourceDir }); + const files: string[] = []; + for await (const file of glob(extension, { cwd: sourceDir })) { + files.push(file); + } for (const file of files) { const src = join(sourceDir, file); const dest = join(targetDir, file); @@ -291,7 +293,10 @@ async function main(): Promise { const ignoreList = await getIgnoreList(); const patterns = ["**/main.tsp"]; - const specsList = await globby(patterns, { cwd: specDir }); + const specsList: string[] = []; + for await (const spec of glob(patterns, { cwd: specDir })) { + specsList.push(spec); + } const paths = specsList.filter((item) => !ignoreList.includes(item)); diff --git a/packages/http-server-csharp/package.json b/packages/http-server-csharp/package.json index 9ea51a9f83a..7f75dcb5981 100644 --- a/packages/http-server-csharp/package.json +++ b/packages/http-server-csharp/package.json @@ -87,7 +87,6 @@ "@typespec/versioning": "workspace:^", "@vitest/coverage-v8": "catalog:", "@vitest/ui": "catalog:", - "globby": "catalog:", "@inquirer/prompts": "catalog:", "ora": "catalog:", "p-limit": "catalog:", diff --git a/packages/http-server-csharp/test/scenarios/scenarios.test.e2e.ts b/packages/http-server-csharp/test/scenarios/scenarios.test.e2e.ts index cc04285666e..7d11dfc9e7c 100644 --- a/packages/http-server-csharp/test/scenarios/scenarios.test.e2e.ts +++ b/packages/http-server-csharp/test/scenarios/scenarios.test.e2e.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { readFileSync } from "fs"; -import { globby } from "globby"; +import { glob } from "fs/promises"; import { dirname, join, resolve } from "pathe"; import { describe, expect, it } from "vitest"; import { Server, getIgnoreList } from "./helpers.js"; // Import the custom Server class @@ -11,13 +11,11 @@ const generatedRoot = join(testRoot, "generated"); // Root folder for generated const ignoreList = await getIgnoreList(join(testRoot, ".testignore")); // Get all unique service directories -const allGeneratedServices = Array.from( - new Set( - (await globby("**/ServiceProject.csproj", { cwd: generatedRoot })).map((service) => - dirname(service), - ), - ), -); +const allGeneratedServicesList: string[] = []; +for await (const service of glob("**/ServiceProject.csproj", { cwd: generatedRoot })) { + allGeneratedServicesList.push(dirname(service)); +} +const allGeneratedServices = Array.from(new Set(allGeneratedServicesList)); // Filter out ignored services const services = allGeneratedServices.filter((item) => !ignoreList.includes(`${item}/main.tsp`)); diff --git a/packages/http-server-js/eng/scripts/emit-e2e.js b/packages/http-server-js/eng/scripts/emit-e2e.js index 3ef46e0da3a..08d039b5d68 100644 --- a/packages/http-server-js/eng/scripts/emit-e2e.js +++ b/packages/http-server-js/eng/scripts/emit-e2e.js @@ -2,8 +2,7 @@ /* eslint-disable no-console */ import { select } from "@inquirer/prompts"; import { run } from "@typespec/internal-build-utils"; -import { access, copyFile, mkdir, readFile, rm, stat, writeFile } from "fs/promises"; -import { globby } from "globby"; +import { access, copyFile, glob, mkdir, readFile, rm, stat, writeFile } from "fs/promises"; import ora from "ora"; import pLimit from "p-limit"; import { basename, dirname, join, resolve } from "path"; @@ -88,7 +87,10 @@ async function processPaths(paths, ignoreList) { results.push({ fullPath, relativePath }); } else if (stats.isDirectory()) { const patterns = ["**/main.tsp"]; - const discoveredPaths = await globby(patterns, { cwd: fullPath }); + const discoveredPaths = []; + for await (const p of glob(patterns, { cwd: fullPath })) { + discoveredPaths.push(p); + } const validFiles = discoveredPaths .map((p) => ({ fullPath: join(fullPath, p), diff --git a/packages/http-server-js/generated-defs/package.json.ts b/packages/http-server-js/generated-defs/package.json.ts index db0a75f4d1a..a3dc885d734 100644 --- a/packages/http-server-js/generated-defs/package.json.ts +++ b/packages/http-server-js/generated-defs/package.json.ts @@ -19,7 +19,6 @@ export const hsjsDependencies: Record = { "@vitest/ui": "^4.1.3", "decimal.js": "^10.6.0", "express": "^5.2.1", - "globby": "^16.2.0", "@inquirer/prompts": "^8.4.1", "morgan": "^1.10.1", "ora": "^9.3.0", diff --git a/packages/http-server-js/package.json b/packages/http-server-js/package.json index 02b1759784b..e6cef124f3b 100644 --- a/packages/http-server-js/package.json +++ b/packages/http-server-js/package.json @@ -78,7 +78,6 @@ "@vitest/ui": "catalog:", "decimal.js": "catalog:", "express": "catalog:", - "globby": "catalog:", "@inquirer/prompts": "catalog:", "morgan": "catalog:", "ora": "catalog:", diff --git a/packages/spector/package.json b/packages/spector/package.json index fe3a17200a8..a6d353dc12e 100644 --- a/packages/spector/package.json +++ b/packages/spector/package.json @@ -44,7 +44,6 @@ "@typespec/versioning": "workspace:^", "ajv": "catalog:", "express": "catalog:", - "globby": "catalog:", "micromatch": "catalog:", "morgan": "catalog:", "multer": "catalog:", diff --git a/packages/spector/src/utils/file-utils.ts b/packages/spector/src/utils/file-utils.ts index 63a56232489..695850f7982 100644 --- a/packages/spector/src/utils/file-utils.ts +++ b/packages/spector/src/utils/file-utils.ts @@ -1,8 +1,11 @@ -import { mkdir } from "fs/promises"; -import { globby } from "globby"; +import { glob, mkdir } from "fs/promises"; export async function findFilesFromPattern(pattern: string | string[]): Promise { - return await globby(pattern); + const results: string[] = []; + for await (const file of glob(pattern)) { + results.push(file); + } + return results; } /** diff --git a/packages/tsp-integration/package.json b/packages/tsp-integration/package.json index 71b92870e19..30819f95fa4 100644 --- a/packages/tsp-integration/package.json +++ b/packages/tsp-integration/package.json @@ -27,7 +27,6 @@ "dependencies": { "@pnpm/workspace.find-packages": "catalog:", "execa": "catalog:", - "globby": "catalog:", "log-symbols": "catalog:", "ora": "catalog:", "pathe": "catalog:", diff --git a/packages/tsp-integration/src/validate.ts b/packages/tsp-integration/src/validate.ts index e83b5a5485b..5e1f64b777a 100644 --- a/packages/tsp-integration/src/validate.ts +++ b/packages/tsp-integration/src/validate.ts @@ -1,8 +1,7 @@ import { execa } from "execa"; -import { readdir } from "fs/promises"; -import { globby } from "globby"; +import { glob, readdir } from "fs/promises"; import { cpus } from "os"; -import { dirname, join, relative } from "pathe"; +import { dirname, join, relative, resolve } from "pathe"; import pc from "picocolors"; import type { Entrypoint, IntegrationTestSuite } from "./config/types.js"; import { registerConsoleShortcuts } from "./keyboard-api.js"; @@ -193,11 +192,11 @@ async function runValidation( } async function findTspProjects(wd: string, pattern: string): Promise { - const result = await globby(pattern, { - cwd: wd, - absolute: true, - }); - return result.map((x) => dirname(x)); + const result: string[] = []; + for await (const file of glob(pattern, { cwd: wd })) { + result.push(dirname(resolve(wd, file))); + } + return result; } /** Find which entrypoints are available */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcdceae8325..87b4cf4e8d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,9 +321,6 @@ catalogs: fast-xml-parser: specifier: ^5.5.9 version: 5.5.10 - globby: - specifier: ^16.2.0 - version: 16.2.0 happy-dom: specifier: ^20.8.9 version: 20.8.9 @@ -719,9 +716,6 @@ importers: '@typespec/bundler': specifier: workspace:^ version: link:../bundler - globby: - specifier: 'catalog:' - version: 16.2.0 picocolors: specifier: 'catalog:' version: 1.1.1 @@ -811,9 +805,6 @@ importers: env-paths: specifier: 'catalog:' version: 4.0.0 - globby: - specifier: 'catalog:' - version: 16.2.0 is-unicode-supported: specifier: 'catalog:' version: 2.1.0 @@ -1264,9 +1255,6 @@ importers: execa: specifier: 'catalog:' version: 9.6.1 - globby: - specifier: 'catalog:' - version: 16.2.0 ora: specifier: 'catalog:' version: 9.3.0 @@ -1361,9 +1349,6 @@ importers: '@vitest/ui': specifier: 'catalog:' version: 4.1.3(vitest@4.1.3) - globby: - specifier: 'catalog:' - version: 16.2.0 ora: specifier: 'catalog:' version: 9.3.0 @@ -1437,9 +1422,6 @@ importers: express: specifier: 'catalog:' version: 5.2.1 - globby: - specifier: 'catalog:' - version: 16.2.0 morgan: specifier: 'catalog:' version: 1.10.1 @@ -2462,9 +2444,6 @@ importers: express: specifier: 'catalog:' version: 5.2.1 - globby: - specifier: 'catalog:' - version: 16.2.0 micromatch: specifier: 'catalog:' version: 4.0.8 @@ -2666,9 +2645,6 @@ importers: execa: specifier: 'catalog:' version: 9.6.1 - globby: - specifier: 'catalog:' - version: 16.2.0 log-symbols: specifier: 'catalog:' version: 7.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e059d6b549a..69bc8b379a1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -112,7 +112,6 @@ catalog: execa: ^9.6.1 express: ^5.2.1 fast-xml-parser: ^5.5.9 - globby: ^16.2.0 happy-dom: ^20.8.9 is-unicode-supported: ^2.1.0 log-symbols: ^7.0.1