Skip to content

Commit e9e4217

Browse files
committed
fix
1 parent d52e9a8 commit e9e4217

5 files changed

Lines changed: 135 additions & 25 deletions

File tree

packages/react-doctor/src/constants.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,20 @@ export const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
6262

6363
export const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
6464

65-
export const IGNORED_DIRECTORIES = new Set(["node_modules", "dist", "build", "coverage"]);
65+
export const IGNORED_DIRECTORIES = new Set([
66+
".git",
67+
".next",
68+
".nuxt",
69+
".output",
70+
".svelte-kit",
71+
".turbo",
72+
"build",
73+
"coverage",
74+
"dist",
75+
"node_modules",
76+
"out",
77+
"storybook-static",
78+
]);
6679

6780
export const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
6881

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

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -518,41 +518,82 @@ const hasReactDependency = (packageJson: PackageJson): boolean => {
518518
);
519519
};
520520

521-
export const discoverReactSubprojects = (rootDirectory: string): WorkspacePackage[] => {
522-
if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
523-
521+
const toReactWorkspacePackages = (directories: string[]): WorkspacePackage[] => {
524522
const packages: WorkspacePackage[] = [];
525523

526-
const rootPackageJsonPath = path.join(rootDirectory, "package.json");
527-
if (isFile(rootPackageJsonPath)) {
528-
const rootPackageJson = readPackageJson(rootPackageJsonPath);
529-
if (hasReactDependency(rootPackageJson)) {
530-
const name = rootPackageJson.name ?? path.basename(rootDirectory);
531-
packages.push({ name, directory: rootDirectory });
532-
}
524+
for (const directory of directories) {
525+
const packageJsonPath = path.join(directory, "package.json");
526+
if (!isFile(packageJsonPath)) continue;
527+
528+
const packageJson = readPackageJson(packageJsonPath);
529+
if (!hasReactDependency(packageJson)) continue;
530+
531+
const name = packageJson.name ?? path.basename(directory);
532+
packages.push({ name, directory });
533533
}
534534

535-
const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
535+
return packages;
536+
};
536537

537-
for (const entry of entries) {
538-
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") {
539-
continue;
538+
const listManifestWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {
539+
const packageJsonPath = path.join(rootDirectory, "package.json");
540+
if (isFile(packageJsonPath)) return listWorkspacePackages(rootDirectory);
541+
542+
const patterns = parsePnpmWorkspacePatterns(rootDirectory);
543+
const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
544+
const directories = (patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) =>
545+
resolveWorkspaceDirectories(rootDirectory, pattern),
546+
);
547+
548+
return toReactWorkspacePackages(directories);
549+
};
550+
551+
const discoverReactSubprojectsByFilesystem = (rootDirectory: string): WorkspacePackage[] => {
552+
const packages: WorkspacePackage[] = [];
553+
const pendingDirectories = [rootDirectory];
554+
555+
while (pendingDirectories.length > 0) {
556+
const currentDirectory = pendingDirectories.shift();
557+
if (!currentDirectory) continue;
558+
559+
const packageJsonPath = path.join(currentDirectory, "package.json");
560+
if (isFile(packageJsonPath)) {
561+
const packageJson = readPackageJson(packageJsonPath);
562+
if (hasReactDependency(packageJson)) {
563+
const name = packageJson.name ?? path.basename(currentDirectory);
564+
packages.push({ name, directory: currentDirectory });
565+
}
540566
}
541567

542-
const subdirectory = path.join(rootDirectory, entry.name);
543-
const packageJsonPath = path.join(subdirectory, "package.json");
544-
if (!isFile(packageJsonPath)) continue;
568+
const entries = fs
569+
.readdirSync(currentDirectory, { withFileTypes: true })
570+
.toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
545571

546-
const packageJson = readPackageJson(packageJsonPath);
547-
if (!hasReactDependency(packageJson)) continue;
572+
for (const entry of entries) {
573+
if (
574+
!entry.isDirectory() ||
575+
entry.name.startsWith(".") ||
576+
IGNORED_DIRECTORIES.has(entry.name)
577+
) {
578+
continue;
579+
}
548580

549-
const name = packageJson.name ?? entry.name;
550-
packages.push({ name, directory: subdirectory });
581+
pendingDirectories.push(path.join(currentDirectory, entry.name));
582+
}
551583
}
552584

553585
return packages;
554586
};
555587

588+
export const discoverReactSubprojects = (rootDirectory: string): WorkspacePackage[] => {
589+
if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
590+
591+
const manifestPackages = listManifestWorkspacePackages(rootDirectory);
592+
if (manifestPackages.length > 0) return manifestPackages;
593+
594+
return discoverReactSubprojectsByFilesystem(rootDirectory);
595+
};
596+
556597
export const listWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {
557598
const packageJsonPath = path.join(rootDirectory, "package.json");
558599
if (!isFile(packageJsonPath)) return [];

packages/react-doctor/src/utils/resolve-diagnose-target.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export const resolveDiagnoseTarget = (directory: string): string | null => {
1010
if (reactSubprojects.length === 0) return null;
1111
if (reactSubprojects.length === 1) return reactSubprojects[0].directory;
1212

13-
const relativeCandidates = reactSubprojects.map((subproject) =>
14-
path.relative(directory, subproject.directory),
15-
);
13+
const relativeCandidates = reactSubprojects
14+
.map((subproject) => path.relative(directory, subproject.directory))
15+
.toSorted();
1616
throw new AmbiguousProjectError(directory, relativeCandidates);
1717
};

packages/react-doctor/tests/diagnose.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ export const Debounced = ({ onChange }: { onChange: (value: string) => void }) =
9191
expect(result.project.reactVersion).toBe("^19.0.0");
9292
});
9393

94+
it("falls back to a deeply nested React subproject when the requested directory has no root package.json", async () => {
95+
const wrapperDir = path.join(tempRoot, "diagnose-no-root-package-deep");
96+
fs.mkdirSync(wrapperDir, { recursive: true });
97+
setupReactProject(wrapperDir, "apps/web");
98+
99+
const result = await diagnose(wrapperDir, { lint: false, deadCode: false });
100+
expect(result.project.rootDirectory).toBe(path.join(wrapperDir, "apps", "web"));
101+
expect(result.project.reactVersion).toBe("^19.0.0");
102+
});
103+
94104
it("throws a clear error when the directory has no root package.json and no nested React project", async () => {
95105
const emptyDir = path.join(tempRoot, "diagnose-no-react-anywhere");
96106
fs.mkdirSync(emptyDir, { recursive: true });

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,52 @@ describe("discoverReactSubprojects", () => {
274274
expect(packages[1]).toEqual({ name: "my-extension", directory: subdirectory });
275275
});
276276

277+
it("includes deeply nested React packages", () => {
278+
const rootDirectory = path.join(tempDirectory, "deep-react-package");
279+
const subdirectory = path.join(rootDirectory, "apps", "web");
280+
fs.mkdirSync(subdirectory, { recursive: true });
281+
fs.writeFileSync(
282+
path.join(subdirectory, "package.json"),
283+
JSON.stringify({ name: "web", dependencies: { react: "^19.0.0" } }),
284+
);
285+
286+
const packages = discoverReactSubprojects(rootDirectory);
287+
expect(packages).toContainEqual({ name: "web", directory: subdirectory });
288+
});
289+
290+
it("prefers pnpm workspace packages over filesystem recursion", () => {
291+
const rootDirectory = path.join(tempDirectory, "pnpm-workspace-preferred");
292+
const workspaceDirectory = path.join(rootDirectory, "apps", "web");
293+
const unlistedDirectory = path.join(rootDirectory, "examples", "preview");
294+
fs.mkdirSync(workspaceDirectory, { recursive: true });
295+
fs.mkdirSync(unlistedDirectory, { recursive: true });
296+
fs.writeFileSync(path.join(rootDirectory, "pnpm-workspace.yaml"), "packages:\n - apps/*\n");
297+
fs.writeFileSync(
298+
path.join(workspaceDirectory, "package.json"),
299+
JSON.stringify({ name: "web", dependencies: { react: "^19.0.0" } }),
300+
);
301+
fs.writeFileSync(
302+
path.join(unlistedDirectory, "package.json"),
303+
JSON.stringify({ name: "preview", dependencies: { react: "^19.0.0" } }),
304+
);
305+
306+
const packages = discoverReactSubprojects(rootDirectory);
307+
expect(packages).toEqual([{ name: "web", directory: workspaceDirectory }]);
308+
});
309+
310+
it("skips ignored generated directories during filesystem recursion", () => {
311+
const rootDirectory = path.join(tempDirectory, "ignored-generated-directories");
312+
const ignoredDirectory = path.join(rootDirectory, "node_modules", "preview");
313+
fs.mkdirSync(ignoredDirectory, { recursive: true });
314+
fs.writeFileSync(
315+
path.join(ignoredDirectory, "package.json"),
316+
JSON.stringify({ name: "preview", dependencies: { react: "^19.0.0" } }),
317+
);
318+
319+
const packages = discoverReactSubprojects(rootDirectory);
320+
expect(packages).toHaveLength(0);
321+
});
322+
277323
it("does not match packages with only @types/react", () => {
278324
const rootDirectory = path.join(tempDirectory, "types-only");
279325
fs.mkdirSync(rootDirectory, { recursive: true });

0 commit comments

Comments
 (0)