diff --git a/packages/react-doctor/src/utils/discover-project.ts b/packages/react-doctor/src/utils/discover-project.ts index c0c70da..c169585 100644 --- a/packages/react-doctor/src/utils/discover-project.ts +++ b/packages/react-doctor/src/utils/discover-project.ts @@ -227,6 +227,19 @@ const hasReactDependency = (packageJson: PackageJson): boolean => { ); }; +export const discoverRootReactPackage = (rootDirectory: string): WorkspacePackage | null => { + const packageJsonPath = path.join(rootDirectory, "package.json"); + if (!fs.existsSync(packageJsonPath)) return null; + + const packageJson = readPackageJson(packageJsonPath); + if (!hasReactDependency(packageJson)) return null; + + return { + name: packageJson.name ?? path.basename(rootDirectory), + directory: rootDirectory, + }; +}; + export const discoverReactSubprojects = (rootDirectory: string): WorkspacePackage[] => { if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return []; diff --git a/packages/react-doctor/src/utils/select-projects.ts b/packages/react-doctor/src/utils/select-projects.ts index 9bbc303..5476e67 100644 --- a/packages/react-doctor/src/utils/select-projects.ts +++ b/packages/react-doctor/src/utils/select-projects.ts @@ -1,6 +1,10 @@ import path from "node:path"; import type { WorkspacePackage } from "../types.js"; -import { discoverReactSubprojects, listWorkspacePackages } from "./discover-project.js"; +import { + discoverReactSubprojects, + discoverRootReactPackage, + listWorkspacePackages, +} from "./discover-project.js"; import { highlighter } from "./highlighter.js"; import { logger } from "./logger.js"; import { prompts } from "./prompts.js"; @@ -15,6 +19,18 @@ export const selectProjects = async ( packages = discoverReactSubprojects(rootDirectory); } + const rootPackage = discoverRootReactPackage(rootDirectory); + if (rootPackage) { + const hasDuplicateRootPackage = packages.some( + (workspacePackage) => + workspacePackage.name === rootPackage.name || + workspacePackage.directory === rootPackage.directory, + ); + if (!hasDuplicateRootPackage) { + packages.push(rootPackage); + } + } + if (packages.length === 0) return [rootDirectory]; if (packages.length === 1) { logger.log( diff --git a/packages/react-doctor/tests/select-projects.test.ts b/packages/react-doctor/tests/select-projects.test.ts new file mode 100644 index 0000000..232e763 --- /dev/null +++ b/packages/react-doctor/tests/select-projects.test.ts @@ -0,0 +1,105 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { selectProjects } from "../src/utils/select-projects.js"; + +interface WorkspacePackageDefinition { + name: string; + reactDependency: boolean; +} + +interface WorkspaceFixtureDefinition { + rootName: string; + rootReactDependency: boolean; + workspacePackages: WorkspacePackageDefinition[]; +} + +const temporaryDirectories: string[] = []; + +const createWorkspaceFixture = (definition: WorkspaceFixtureDefinition): string => { + const rootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-select-projects-")); + + const rootDependencies = definition.rootReactDependency ? { react: "^19.0.0" } : {}; + fs.writeFileSync( + path.join(rootDirectory, "package.json"), + JSON.stringify( + { + name: definition.rootName, + workspaces: ["packages/*"], + dependencies: rootDependencies, + }, + null, + 2, + ), + ); + + for (const workspacePackage of definition.workspacePackages) { + const workspaceDirectory = path.join(rootDirectory, "packages", workspacePackage.name); + fs.mkdirSync(workspaceDirectory, { recursive: true }); + + const workspaceDependencies = workspacePackage.reactDependency ? { react: "^19.0.0" } : {}; + fs.writeFileSync( + path.join(workspaceDirectory, "package.json"), + JSON.stringify( + { + name: workspacePackage.name, + dependencies: workspaceDependencies, + }, + null, + 2, + ), + ); + } + + temporaryDirectories.push(rootDirectory); + return rootDirectory; +}; + +afterEach(() => { + for (const temporaryDirectory of temporaryDirectories) { + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); + } + temporaryDirectories.length = 0; +}); + +describe("selectProjects", () => { + it("includes root package even when workspace candidates exist", async () => { + const rootDirectory = createWorkspaceFixture({ + rootName: "workspace-root", + rootReactDependency: true, + workspacePackages: [{ name: "workspace-app", reactDependency: true }], + }); + const workspaceDirectory = path.join(rootDirectory, "packages", "workspace-app"); + + const selectedDirectories = await selectProjects(rootDirectory, undefined, true); + + expect(selectedDirectories).toContain(workspaceDirectory); + expect(selectedDirectories).toContain(rootDirectory); + }); + + it("resolves root package with --project", async () => { + const rootDirectory = createWorkspaceFixture({ + rootName: "workspace-root", + rootReactDependency: true, + workspacePackages: [{ name: "workspace-app", reactDependency: true }], + }); + + const selectedDirectories = await selectProjects(rootDirectory, "workspace-root", true); + + expect(selectedDirectories).toEqual([rootDirectory]); + }); + + it("does not include root package when root has no React dependency", async () => { + const rootDirectory = createWorkspaceFixture({ + rootName: "workspace-root", + rootReactDependency: false, + workspacePackages: [{ name: "workspace-app", reactDependency: true }], + }); + const workspaceDirectory = path.join(rootDirectory, "packages", "workspace-app"); + + const selectedDirectories = await selectProjects(rootDirectory, undefined, true); + + expect(selectedDirectories).toEqual([workspaceDirectory]); + }); +});