From e12c502e56386dd0a54d6dc308c38e0c1ba68ef4 Mon Sep 17 00:00:00 2001 From: rubiesonthesky <2591240+rubiesonthesky@users.noreply.github.com> Date: Sun, 3 May 2026 21:21:33 +0300 Subject: [PATCH 1/3] fix(deps): replace glob with node native glob --- package.json | 1 - pnpm-lock.yaml | 34 ------------------- src/collectFileNames.test.ts | 6 ++++ src/collectFileNames.ts | 32 ++++++++--------- .../initializeProject/index.spec.ts | 13 +++++++ src/initialization/initializeProject/index.ts | 18 +++++++--- .../builtIn/fixImportExtensions/index.ts | 6 ++-- 7 files changed, 50 insertions(+), 60 deletions(-) create mode 100644 src/initialization/initializeProject/index.spec.ts diff --git a/package.json b/package.json index 26baed73bb..99410964d7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "chalk": "5.6.2", "commander": "14.0.3", "enquirer": "2.4.1", - "glob": "13.0.6", "ts-api-utils": "2.5.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1a051e4ef..81788ec628 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: enquirer: specifier: 2.4.1 version: 2.4.1 - glob: - specifier: 13.0.6 - version: 13.0.6 ts-api-utils: specifier: 2.5.0 version: 2.5.0(typescript@5.9.3) @@ -2269,10 +2266,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} - global-directory@5.0.0: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} @@ -2633,10 +2626,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.5: - resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} - engines: {node: 20 || >=22} - lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -2837,10 +2826,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -2984,10 +2969,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -5593,12 +5574,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@13.0.6: - dependencies: - minimatch: 10.2.5 - minipass: 7.1.3 - path-scurry: 2.0.2 - global-directory@5.0.0: dependencies: ini: 6.0.0 @@ -5912,8 +5887,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.3.5: {} - lru-cache@7.18.3: {} macos-release@3.4.0: {} @@ -6341,8 +6314,6 @@ snapshots: minimist@1.2.8: {} - minipass@7.1.3: {} - mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -6553,11 +6524,6 @@ snapshots: path-key@3.1.1: {} - path-scurry@2.0.2: - dependencies: - lru-cache: 11.3.5 - minipass: 7.1.3 - pathe@2.0.3: {} perfect-debounce@2.1.0: {} diff --git a/src/collectFileNames.test.ts b/src/collectFileNames.test.ts index 22e98b1a64..ad772c14a3 100644 --- a/src/collectFileNames.test.ts +++ b/src/collectFileNames.test.ts @@ -20,4 +20,10 @@ describe("collectFileNames", () => { `At least one path including node_modules was included implicitly: '${cwd}/node_modules'. Either adjust TypeStat's included files to not include node_modules (recommended) or explicitly include node_modules/ (not recommended).`, ); }); + + it("should NOT return error if node_modules are explicitly included", async () => { + const cwd = path.resolve(import.meta.dirname, ".."); + const fileNames = await collectFileNames(cwd, ["node_modules"]); + expect(fileNames?.length).toBeGreaterThan(0); + }); }); diff --git a/src/collectFileNames.ts b/src/collectFileNames.ts index 3dff3157b0..9c691a4526 100644 --- a/src/collectFileNames.ts +++ b/src/collectFileNames.ts @@ -1,18 +1,17 @@ -import { glob } from "glob"; -import * as path from "node:path"; +import { glob } from "node:fs/promises"; +import path from "node:path"; export const collectFileNames = async ( cwd: string, include: readonly string[] | undefined, ): Promise => { - const globsAndNames = await collectFileNamesFromGlobs(cwd, include); - if (!globsAndNames) { + if (include === undefined) { return undefined; } - const [fileGlobs, fileNames] = globsAndNames; + const fileNames = await collectFileNamesFromGlobs(cwd, include); const implicitNodeModulesInclude = implicitNodeModulesIncluded( - fileGlobs, + include, fileNames, ); @@ -25,24 +24,21 @@ export const collectFileNames = async ( const collectFileNamesFromGlobs = async ( cwd: string, - include: readonly string[] | undefined, -): Promise<[readonly string[], readonly string[]] | undefined> => { - if (include === undefined) { - return undefined; + include: readonly string[], +): Promise => { + const fileNames: string[] = []; + for await (const entry of glob(include, { cwd, withFileTypes: true })) { + fileNames.push(path.join(entry.parentPath, entry.name)); } - - return [ - include, - await glob(include.map((subInclude) => path.join(cwd, subInclude))), - ]; + return fileNames; }; const implicitNodeModulesIncluded = ( fileGlobs: readonly string[], - fileNames: readonly string[] | undefined, -) => { + fileNames: readonly string[], +): boolean => { return ( !fileGlobs.some((glob) => glob.includes("node_modules")) && - fileNames?.find((fileName) => fileName.includes("node_modules")) + fileNames.some((fileName) => fileName.includes("node_modules")) ); }; diff --git a/src/initialization/initializeProject/index.spec.ts b/src/initialization/initializeProject/index.spec.ts new file mode 100644 index 0000000000..0718110b7f --- /dev/null +++ b/src/initialization/initializeProject/index.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { getTsConfigPaths } from "./index.js"; + +describe("getTsConfigPaths", () => { + it("should collect list of tsconfig files", async () => { + const cwd = process.cwd(); + const fileNames = await getTsConfigPaths(); + expect(fileNames).toHaveLength(2); + expect(fileNames).toContain(`${cwd}/tsconfig.eslint.json`); + expect(fileNames).toContain(`${cwd}/tsconfig.json`); + }); +}); diff --git a/src/initialization/initializeProject/index.ts b/src/initialization/initializeProject/index.ts index e2771f03db..2a0c35cbca 100644 --- a/src/initialization/initializeProject/index.ts +++ b/src/initialization/initializeProject/index.ts @@ -1,6 +1,7 @@ import enquirer from "enquirer"; -import * as fs from "fs"; -import { glob } from "glob"; +import { existsSync } from "node:fs"; +import { glob } from "node:fs/promises"; +import path from "node:path"; import { uniquify } from "../../shared/arrays.js"; import { initializeNewProject } from "./initializeNewProject.js"; @@ -32,7 +33,7 @@ const initializeBuiltInProject = async () => { ...uniquify( TSConfigLocation.Root, TSConfigLocation.UnderSrc, - ...(await glob(["./tsconfig*json", "./*/tsconfig*json"])), + ...(await getTsConfigPaths()), ), TSConfigLocationSuggestion.Custom, TSConfigLocationSuggestion.DoesNotExist, @@ -47,7 +48,7 @@ const initializeBuiltInProject = async () => { initial: Math.max( 0, [TSConfigLocation.Root, TSConfigLocation.UnderSrc].findIndex((choice) => - fs.existsSync(choice), + existsSync(choice), ), ), type: "select", @@ -57,6 +58,15 @@ const initializeBuiltInProject = async () => { return project; }; +export const getTsConfigPaths = async (): Promise => { + const cwd = process.cwd(); + const fileNames: string[] = []; + for await (const entry of glob(["./tsconfig*json", "./*/tsconfig*json"])) { + fileNames.push(path.join(cwd, entry)); + } + return fileNames; +}; + const initializeCustomProject = async (): Promise => { const { project } = await prompt<{ project: string }>([ { diff --git a/src/mutators/builtIn/fixImportExtensions/index.ts b/src/mutators/builtIn/fixImportExtensions/index.ts index 77fa5cc704..4d4c466766 100644 --- a/src/mutators/builtIn/fixImportExtensions/index.ts +++ b/src/mutators/builtIn/fixImportExtensions/index.ts @@ -1,6 +1,6 @@ import { TextInsertMutation } from "automutate"; -import { glob } from "glob"; -import * as path from "node:path"; +import { globSync } from "node:fs"; +import path from "node:path"; import ts from "typescript"; import { @@ -58,7 +58,7 @@ const visitExportOrImportDeclaration = ( for (const filePath of [basePath, path.join(basePath, "index")]) { // If no files exist under that path, ignore this possibility - const possibilities = glob.sync(filePath + ".*"); + const possibilities = globSync(filePath + ".*"); if (possibilities.length === 0) { continue; } From 5f057eb414dfa826ecbff483dced6b13648f4fa6 Mon Sep 17 00:00:00 2001 From: rubiesonthesky <2591240+rubiesonthesky@users.noreply.github.com> Date: Mon, 4 May 2026 21:58:01 +0300 Subject: [PATCH 2/3] add testing and make code more easily testable --- src/collectFileNames.test.ts | 28 ++++++++++--------- src/collectFileNames.ts | 26 +++++++++++------ src/index.ts | 11 +++++--- .../{index.spec.ts => index.test.ts} | 0 .../builtIn/fixImportExtensions/index.test.ts | 22 +++++++++++++++ .../builtIn/fixImportExtensions/index.ts | 17 ++++++++--- 6 files changed, 74 insertions(+), 30 deletions(-) rename src/initialization/initializeProject/{index.spec.ts => index.test.ts} (100%) create mode 100644 src/mutators/builtIn/fixImportExtensions/index.test.ts diff --git a/src/collectFileNames.test.ts b/src/collectFileNames.test.ts index ad772c14a3..12951407b9 100644 --- a/src/collectFileNames.test.ts +++ b/src/collectFileNames.test.ts @@ -1,29 +1,31 @@ -import path from "node:path"; import { describe, expect, it } from "vitest"; import { collectFileNames } from "./collectFileNames.js"; describe("collectFileNames", () => { it("should collect files with wildcard when collection succeeds", async () => { - const cwd = path.resolve(import.meta.dirname, ".."); - const fileNames = await collectFileNames( - path.resolve(import.meta.dirname), - ["*"], - ); - expect(fileNames).toContain(`${cwd}/src/collectFileNames.test.ts`); + const cwd = process.cwd(); + const res = await collectFileNames(cwd, ["src/*"]); + expect(res?.fileNames).toContain(`${cwd}/src/collectFileNames.test.ts`); + }); + + it("should return undefined if includes is empty array", async () => { + const cwd = process.cwd(); + const res = await collectFileNames(cwd, []); + expect(res).toBeUndefined(); }); it("should return error if node_modules are implicitly included", async () => { - const cwd = path.resolve(import.meta.dirname, ".."); - const fileNames = await collectFileNames(cwd, ["*"]); - expect(fileNames).toEqual( + const cwd = process.cwd(); + const res = await collectFileNames(cwd, ["*"]); + expect(res?.error).toEqual( `At least one path including node_modules was included implicitly: '${cwd}/node_modules'. Either adjust TypeStat's included files to not include node_modules (recommended) or explicitly include node_modules/ (not recommended).`, ); }); it("should NOT return error if node_modules are explicitly included", async () => { - const cwd = path.resolve(import.meta.dirname, ".."); - const fileNames = await collectFileNames(cwd, ["node_modules"]); - expect(fileNames?.length).toBeGreaterThan(0); + const cwd = process.cwd(); + const res = await collectFileNames(cwd, ["node_modules"]); + expect(res?.fileNames.length).toBeGreaterThan(0); }); }); diff --git a/src/collectFileNames.ts b/src/collectFileNames.ts index 9c691a4526..1a74fb7c0b 100644 --- a/src/collectFileNames.ts +++ b/src/collectFileNames.ts @@ -1,11 +1,16 @@ import { glob } from "node:fs/promises"; import path from "node:path"; +export interface CollectFileNamesResult { + error?: string; + fileNames: readonly string[]; +} + export const collectFileNames = async ( cwd: string, include: readonly string[] | undefined, -): Promise => { - if (include === undefined) { +): Promise => { + if (!include?.length) { return undefined; } @@ -16,10 +21,13 @@ export const collectFileNames = async ( ); if (implicitNodeModulesInclude) { - return `At least one path including node_modules was included implicitly: '${implicitNodeModulesInclude}'. Either adjust TypeStat's included files to not include node_modules (recommended) or explicitly include node_modules/ (not recommended).`; + return { + error: `At least one path including node_modules was included implicitly: '${implicitNodeModulesInclude}'. Either adjust TypeStat's included files to not include node_modules (recommended) or explicitly include node_modules/ (not recommended).`, + fileNames: [], + }; } - return fileNames; + return { fileNames }; }; const collectFileNamesFromGlobs = async ( @@ -36,9 +44,9 @@ const collectFileNamesFromGlobs = async ( const implicitNodeModulesIncluded = ( fileGlobs: readonly string[], fileNames: readonly string[], -): boolean => { - return ( - !fileGlobs.some((glob) => glob.includes("node_modules")) && - fileNames.some((fileName) => fileName.includes("node_modules")) - ); +): string | undefined => { + if (fileGlobs.some((glob) => glob.includes("node_modules"))) { + return undefined; + } + return fileNames.find((fileName) => fileName.includes("node_modules")); }; diff --git a/src/index.ts b/src/index.ts index 5c481a429e..b0d8ea735f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,11 +91,14 @@ export const typeStat = async ( for (let i = 0; i < allPendingOptions.length; i += 1) { // Collect all files to be run on this option iteration from the include glob(s) - const fileNames = await collectFileNames(cwd, allPendingOptions[i].include); - if (typeof fileNames !== "object") { + const fileNamesRes = await collectFileNames( + cwd, + allPendingOptions[i].include, + ); + if (!fileNamesRes || fileNamesRes.error) { return { error: new Error( - `Could not run options object ${i + 1}: ${fileNames ?? `No files included by the 'include' setting were found.`}`, + `Could not run options object ${i + 1}: ${fileNamesRes?.error ?? `No files included by the 'include' setting were found.`}`, ), status: ResultStatus.Failed, }; @@ -116,7 +119,7 @@ export const typeStat = async ( await runMutations({ mutationsProvider: createTypeStatProvider({ ...allPendingOptions[i], - fileNames, + fileNames: fileNamesRes.fileNames, }), }); } catch (error) { diff --git a/src/initialization/initializeProject/index.spec.ts b/src/initialization/initializeProject/index.test.ts similarity index 100% rename from src/initialization/initializeProject/index.spec.ts rename to src/initialization/initializeProject/index.test.ts diff --git a/src/mutators/builtIn/fixImportExtensions/index.test.ts b/src/mutators/builtIn/fixImportExtensions/index.test.ts new file mode 100644 index 0000000000..f7b04d1851 --- /dev/null +++ b/src/mutators/builtIn/fixImportExtensions/index.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { getFixImportExtensionsMutations } from "./index.js"; + +describe("getFixImportExtensionsMutations", () => { + it("should collect list of tsconfig files", () => { + const mutations = getFixImportExtensionsMutations( + process.cwd() + "/src/mutators/builtIn/fixImportExtensions/index.ts", + "./README", + 1, + ); + expect(mutations).toMatchInlineSnapshot(` + { + "insertion": ".md", + "range": { + "begin": 0, + }, + "type": "text-insert", + } + `); + }); +}); diff --git a/src/mutators/builtIn/fixImportExtensions/index.ts b/src/mutators/builtIn/fixImportExtensions/index.ts index 4d4c466766..d8ff47b17d 100644 --- a/src/mutators/builtIn/fixImportExtensions/index.ts +++ b/src/mutators/builtIn/fixImportExtensions/index.ts @@ -50,11 +50,20 @@ const visitExportOrImportDeclaration = ( return undefined; } - // Try each path that the import could resolve to - const basePath = path.join( - path.dirname(request.sourceFile.fileName), + return getFixImportExtensionsMutations( + request.sourceFile.fileName, node.moduleSpecifier.text, + node.moduleSpecifier.end, ); +}; + +export const getFixImportExtensionsMutations = ( + sourceFileName: string, + moduleSpecifier: string, + end: number, +): TextInsertMutation | undefined => { + // Try each path that the import could resolve to + const basePath = path.join(path.dirname(sourceFileName), moduleSpecifier); for (const filePath of [basePath, path.join(basePath, "index")]) { // If no files exist under that path, ignore this possibility @@ -75,7 +84,7 @@ const visitExportOrImportDeclaration = ( return { insertion: "." + path.basename(mostLikely).split(".").slice(1).join("."), range: { - begin: node.moduleSpecifier.end - 1, + begin: end - 1, }, type: "text-insert", }; From 6a7cabaee3cc99ac99be41452944ce47c1d9ad08 Mon Sep 17 00:00:00 2001 From: rubiesonthesky <2591240+rubiesonthesky@users.noreply.github.com> Date: Mon, 4 May 2026 22:09:06 +0300 Subject: [PATCH 3/3] additional test case --- .../builtIn/fixImportExtensions/index.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/mutators/builtIn/fixImportExtensions/index.test.ts b/src/mutators/builtIn/fixImportExtensions/index.test.ts index f7b04d1851..8d587f0369 100644 --- a/src/mutators/builtIn/fixImportExtensions/index.test.ts +++ b/src/mutators/builtIn/fixImportExtensions/index.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { getFixImportExtensionsMutations } from "./index.js"; describe("getFixImportExtensionsMutations", () => { - it("should collect list of tsconfig files", () => { + it("should create mutation for adding extension", () => { const mutations = getFixImportExtensionsMutations( process.cwd() + "/src/mutators/builtIn/fixImportExtensions/index.ts", "./README", @@ -19,4 +19,13 @@ describe("getFixImportExtensionsMutations", () => { } `); }); + + it("should not create mutation for .ts file", () => { + const mutations = getFixImportExtensionsMutations( + process.cwd() + "/src/mutators/builtIn/index.ts", + "./fixImportExtensions", + 1, + ); + expect(mutations).toMatchInlineSnapshot(`undefined`); + }); });