Skip to content

Commit 1947859

Browse files
authored
fix (#68)
1 parent 3920190 commit 1947859

File tree

6 files changed

+142
-13
lines changed

6 files changed

+142
-13
lines changed

packages/react-doctor/src/cli.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ const resolveCliScanOptions = (
8585
programInstance.getOptionValueSource(optionName) === "cli";
8686

8787
return {
88-
lint: isCliOverride("lint") ? flags.lint : (userConfig?.lint ?? flags.lint),
89-
deadCode: isCliOverride("deadCode") ? flags.deadCode : (userConfig?.deadCode ?? flags.deadCode),
88+
lint: isCliOverride("lint") ? flags.lint : (userConfig?.lint ?? true),
89+
deadCode: isCliOverride("deadCode") ? flags.deadCode : (userConfig?.deadCode ?? true),
9090
verbose: isCliOverride("verbose") ? Boolean(flags.verbose) : (userConfig?.verbose ?? false),
9191
scoreOnly: flags.score,
9292
offline: flags.offline,
@@ -133,7 +133,9 @@ const program = new Command()
133133
.description("Diagnose React codebase health")
134134
.version(VERSION, "-v, --version", "display the version number")
135135
.argument("[directory]", "project directory to scan", ".")
136+
.option("--lint", "enable linting")
136137
.option("--no-lint", "skip linting")
138+
.option("--dead-code", "enable dead code detection")
137139
.option("--no-dead-code", "skip dead code detection")
138140
.option("--verbose", "show file details per rule")
139141
.option("--score", "output only the score")
@@ -326,7 +328,12 @@ const openAmiToFix = (directory: string): void => {
326328
if (!isInstalled) {
327329
if (process.platform === "darwin") {
328330
installAmi();
329-
logger.success("Ami installed successfully.");
331+
if (isAmiInstalled()) {
332+
logger.success("Ami installed successfully.");
333+
} else {
334+
logger.error("Installation could not be verified.");
335+
logger.dim(`Install manually at ${highlighter.info(AMI_WEBSITE_URL)}`);
336+
}
330337
} else {
331338
logger.error("Ami is not installed.");
332339
logger.dim(`Download at ${highlighter.info(AMI_RELEASES_URL)}`);

packages/react-doctor/src/utils/discover-project.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,19 +247,31 @@ const findReactInWorkspaces = (rootDirectory: string, packageJson: PackageJson):
247247
return result;
248248
};
249249

250+
const REACT_DEPENDENCY_NAMES = new Set(["react", "react-native", "next"]);
251+
250252
const hasReactDependency = (packageJson: PackageJson): boolean => {
251253
const allDependencies = collectAllDependencies(packageJson);
252-
return Object.keys(allDependencies).some(
253-
(packageName) => packageName === "next" || packageName.includes("react"),
254+
return Object.keys(allDependencies).some((packageName) =>
255+
REACT_DEPENDENCY_NAMES.has(packageName),
254256
);
255257
};
256258

257259
export const discoverReactSubprojects = (rootDirectory: string): WorkspacePackage[] => {
258260
if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
259261

260-
const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
261262
const packages: WorkspacePackage[] = [];
262263

264+
const rootPackageJsonPath = path.join(rootDirectory, "package.json");
265+
if (fs.existsSync(rootPackageJsonPath)) {
266+
const rootPackageJson = readPackageJson(rootPackageJsonPath);
267+
if (hasReactDependency(rootPackageJson)) {
268+
const name = rootPackageJson.name ?? path.basename(rootDirectory);
269+
packages.push({ name, directory: rootDirectory });
270+
}
271+
}
272+
273+
const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
274+
263275
for (const entry of entries) {
264276
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") {
265277
continue;

packages/react-doctor/src/utils/load-config.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@ export const loadConfig = (rootDirectory: string): ReactDoctorConfig | null => {
1515
try {
1616
const fileContent = fs.readFileSync(configFilePath, "utf-8");
1717
const parsed: unknown = JSON.parse(fileContent);
18-
if (!isPlainObject(parsed)) {
19-
console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);
20-
return null;
18+
if (isPlainObject(parsed)) {
19+
return parsed as ReactDoctorConfig;
2120
}
22-
return parsed as ReactDoctorConfig;
21+
console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);
2322
} catch (error) {
2423
console.warn(
2524
`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,
2625
);
27-
return null;
2826
}
2927
}
3028

packages/react-doctor/src/utils/run-knip.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ const extractFailedPluginName = (error: unknown): string | null => {
8181
return match?.[1] ?? null;
8282
};
8383

84+
const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
85+
8486
const resolveTsConfigFile = (directory: string): string | undefined =>
85-
fs.existsSync(path.join(directory, "tsconfig.base.json")) ? "tsconfig.base.json" : undefined;
87+
TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
8688

8789
const runKnipWithOptions = async (
8890
knipCwd: string,

packages/react-doctor/tests/discover-project.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import fs from "node:fs";
2+
import os from "node:os";
13
import path from "node:path";
2-
import { describe, expect, it } from "vitest";
4+
import { afterAll, describe, expect, it } from "vitest";
35
import {
46
discoverProject,
7+
discoverReactSubprojects,
58
formatFrameworkName,
69
listWorkspacePackages,
710
} from "../src/utils/discover-project.js";
@@ -46,6 +49,69 @@ describe("listWorkspacePackages", () => {
4649
});
4750
});
4851

52+
const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-discover-test-"));
53+
54+
afterAll(() => {
55+
fs.rmSync(tempDirectory, { recursive: true, force: true });
56+
});
57+
58+
describe("discoverReactSubprojects", () => {
59+
it("includes root directory when it has a react dependency", () => {
60+
const rootDirectory = path.join(tempDirectory, "root-with-react");
61+
fs.mkdirSync(rootDirectory, { recursive: true });
62+
fs.writeFileSync(
63+
path.join(rootDirectory, "package.json"),
64+
JSON.stringify({ name: "my-app", dependencies: { react: "^19.0.0" } }),
65+
);
66+
67+
const packages = discoverReactSubprojects(rootDirectory);
68+
expect(packages).toContainEqual({ name: "my-app", directory: rootDirectory });
69+
});
70+
71+
it("includes both root and subdirectory when both have react", () => {
72+
const rootDirectory = path.join(tempDirectory, "root-and-sub");
73+
const subdirectory = path.join(rootDirectory, "extension");
74+
fs.mkdirSync(subdirectory, { recursive: true });
75+
fs.writeFileSync(
76+
path.join(rootDirectory, "package.json"),
77+
JSON.stringify({ name: "my-app", dependencies: { react: "^19.0.0" } }),
78+
);
79+
fs.writeFileSync(
80+
path.join(subdirectory, "package.json"),
81+
JSON.stringify({ name: "my-extension", dependencies: { react: "^18.0.0" } }),
82+
);
83+
84+
const packages = discoverReactSubprojects(rootDirectory);
85+
expect(packages).toHaveLength(2);
86+
expect(packages[0]).toEqual({ name: "my-app", directory: rootDirectory });
87+
expect(packages[1]).toEqual({ name: "my-extension", directory: subdirectory });
88+
});
89+
90+
it("does not match packages with only @types/react", () => {
91+
const rootDirectory = path.join(tempDirectory, "types-only");
92+
fs.mkdirSync(rootDirectory, { recursive: true });
93+
fs.writeFileSync(
94+
path.join(rootDirectory, "package.json"),
95+
JSON.stringify({ name: "types-only", devDependencies: { "@types/react": "^18.0.0" } }),
96+
);
97+
98+
const packages = discoverReactSubprojects(rootDirectory);
99+
expect(packages).toHaveLength(0);
100+
});
101+
102+
it("matches packages with react-native dependency", () => {
103+
const rootDirectory = path.join(tempDirectory, "rn-app");
104+
fs.mkdirSync(rootDirectory, { recursive: true });
105+
fs.writeFileSync(
106+
path.join(rootDirectory, "package.json"),
107+
JSON.stringify({ name: "rn-app", dependencies: { "react-native": "^0.74.0" } }),
108+
);
109+
110+
const packages = discoverReactSubprojects(rootDirectory);
111+
expect(packages).toHaveLength(1);
112+
});
113+
});
114+
49115
describe("formatFrameworkName", () => {
50116
it("formats known frameworks", () => {
51117
expect(formatFrameworkName("nextjs")).toBe("Next.js");

packages/react-doctor/tests/load-config.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,50 @@ describe("loadConfig", () => {
184184
warnSpy.mockRestore();
185185
});
186186

187+
it("falls through to package.json when config file has malformed JSON", () => {
188+
const fallbackDirectory = path.join(tempRootDirectory, "malformed-with-fallback");
189+
fs.mkdirSync(fallbackDirectory, { recursive: true });
190+
fs.writeFileSync(
191+
path.join(fallbackDirectory, "react-doctor.config.json"),
192+
"not valid json{{{",
193+
);
194+
fs.writeFileSync(
195+
path.join(fallbackDirectory, "package.json"),
196+
JSON.stringify({
197+
name: "test",
198+
reactDoctor: { ignore: { rules: ["from-fallback"] } },
199+
}),
200+
);
201+
202+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
203+
const config = loadConfig(fallbackDirectory);
204+
expect(config).toEqual({ ignore: { rules: ["from-fallback"] } });
205+
expect(warnSpy).toHaveBeenCalledOnce();
206+
warnSpy.mockRestore();
207+
});
208+
209+
it("falls through to package.json when config file is not an object", () => {
210+
const nonObjectFallbackDirectory = path.join(tempRootDirectory, "non-object-with-fallback");
211+
fs.mkdirSync(nonObjectFallbackDirectory, { recursive: true });
212+
fs.writeFileSync(
213+
path.join(nonObjectFallbackDirectory, "react-doctor.config.json"),
214+
JSON.stringify([1, 2, 3]),
215+
);
216+
fs.writeFileSync(
217+
path.join(nonObjectFallbackDirectory, "package.json"),
218+
JSON.stringify({
219+
name: "test",
220+
reactDoctor: { ignore: { rules: ["from-non-object-fallback"] } },
221+
}),
222+
);
223+
224+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
225+
const config = loadConfig(nonObjectFallbackDirectory);
226+
expect(config).toEqual({ ignore: { rules: ["from-non-object-fallback"] } });
227+
expect(warnSpy).toHaveBeenCalledOnce();
228+
warnSpy.mockRestore();
229+
});
230+
187231
it("ignores non-object reactDoctor key in package.json", () => {
188232
const arrayConfigDirectory = path.join(tempRootDirectory, "array-pkg-config");
189233
fs.mkdirSync(arrayConfigDirectory, { recursive: true });

0 commit comments

Comments
 (0)