Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
34 changes: 0 additions & 34 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 18 additions & 10 deletions src/collectFileNames.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +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 = process.cwd();
const res = await collectFileNames(cwd, ["node_modules"]);
expect(res?.fileNames.length).toBeGreaterThan(0);
});
});
52 changes: 28 additions & 24 deletions src/collectFileNames.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,52 @@
import { glob } from "glob";
import * as path from "node:path";
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<readonly string[] | string | undefined> => {
const globsAndNames = await collectFileNamesFromGlobs(cwd, include);
if (!globsAndNames) {
): Promise<CollectFileNamesResult | undefined> => {
if (!include?.length) {
return undefined;
}

const [fileGlobs, fileNames] = globsAndNames;
const fileNames = await collectFileNamesFromGlobs(cwd, include);
const implicitNodeModulesInclude = implicitNodeModulesIncluded(
fileGlobs,
include,
fileNames,
);

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 (
cwd: string,
include: readonly string[] | undefined,
): Promise<[readonly string[], readonly string[]] | undefined> => {
if (include === undefined) {
return undefined;
include: readonly string[],
): Promise<readonly string[]> => {
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,
) => {
return (
!fileGlobs.some((glob) => glob.includes("node_modules")) &&
fileNames?.find((fileName) => fileName.includes("node_modules"))
);
fileNames: readonly string[],
): string | undefined => {
if (fileGlobs.some((glob) => glob.includes("node_modules"))) {
return undefined;
}
return fileNames.find((fileName) => fileName.includes("node_modules"));
};
11 changes: 7 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -116,7 +119,7 @@ export const typeStat = async (
await runMutations({
mutationsProvider: createTypeStatProvider({
...allPendingOptions[i],
fileNames,
fileNames: fileNamesRes.fileNames,
}),
});
} catch (error) {
Expand Down
13 changes: 13 additions & 0 deletions src/initialization/initializeProject/index.test.ts
Original file line number Diff line number Diff line change
@@ -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`);
});
});
18 changes: 14 additions & 4 deletions src/initialization/initializeProject/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -32,7 +33,7 @@ const initializeBuiltInProject = async () => {
...uniquify(
TSConfigLocation.Root,
TSConfigLocation.UnderSrc,
...(await glob(["./tsconfig*json", "./*/tsconfig*json"])),
...(await getTsConfigPaths()),
),
TSConfigLocationSuggestion.Custom,
TSConfigLocationSuggestion.DoesNotExist,
Expand All @@ -47,7 +48,7 @@ const initializeBuiltInProject = async () => {
initial: Math.max(
0,
[TSConfigLocation.Root, TSConfigLocation.UnderSrc].findIndex((choice) =>
fs.existsSync(choice),
existsSync(choice),
),
),
type: "select",
Expand All @@ -57,6 +58,15 @@ const initializeBuiltInProject = async () => {
return project;
};

export const getTsConfigPaths = async (): Promise<string[]> => {
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<ProjectDescription> => {
const { project } = await prompt<{ project: string }>([
{
Expand Down
31 changes: 31 additions & 0 deletions src/mutators/builtIn/fixImportExtensions/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";

import { getFixImportExtensionsMutations } from "./index.js";

describe("getFixImportExtensionsMutations", () => {
it("should create mutation for adding extension", () => {
const mutations = getFixImportExtensionsMutations(
process.cwd() + "/src/mutators/builtIn/fixImportExtensions/index.ts",
"./README",
1,
);
expect(mutations).toMatchInlineSnapshot(`
{
"insertion": ".md",
"range": {
"begin": 0,
},
"type": "text-insert",
}
`);
});

it("should not create mutation for .ts file", () => {
const mutations = getFixImportExtensionsMutations(
process.cwd() + "/src/mutators/builtIn/index.ts",
"./fixImportExtensions",
1,
);
expect(mutations).toMatchInlineSnapshot(`undefined`);
});
});
23 changes: 16 additions & 7 deletions src/mutators/builtIn/fixImportExtensions/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -50,15 +50,24 @@ 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
const possibilities = glob.sync(filePath + ".*");
const possibilities = globSync(filePath + ".*");
if (possibilities.length === 0) {
continue;
}
Expand All @@ -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",
};
Expand Down
Loading