diff --git a/.changeset/nested-targets-inherit.md b/.changeset/nested-targets-inherit.md new file mode 100644 index 000000000..288de29b6 --- /dev/null +++ b/.changeset/nested-targets-inherit.md @@ -0,0 +1,5 @@ +--- +"react-doctor": patch +--- + +Inherit browser and Electron target capabilities when scanning a nested project directory. diff --git a/packages/core/src/project-info/capabilities.ts b/packages/core/src/project-info/capabilities.ts index 4cc547c9d..68ac6aba2 100644 --- a/packages/core/src/project-info/capabilities.ts +++ b/packages/core/src/project-info/capabilities.ts @@ -37,6 +37,8 @@ import { parseTailwindMajorMinor, } from "./version.js"; import { detectTargetBlankOpenerProtection } from "./detect-target-blank-opener-protection.js"; +import { findNearestAncestorPackageJson } from "./find-nearest-ancestor-package-json.js"; +import { isFile } from "./fs-utils.js"; import { readPackageJson } from "./package-json.js"; // SPA / mobile frameworks with no server-side form handler at all — @@ -351,9 +353,13 @@ export const getCapabilities = (project: ProjectInfo): ReadonlySet = const cached = capabilitiesByProject.get(project); if (cached !== undefined) return cached; const capabilities = new Set(buildCapabilities(project)); - const packageJson = readPackageJson(path.join(project.rootDirectory, "package.json")); + const packageJsonPath = path.join(project.rootDirectory, "package.json"); + const capabilityRootDirectory = isFile(packageJsonPath) + ? project.rootDirectory + : (findNearestAncestorPackageJson(project.rootDirectory) ?? project.rootDirectory); + const packageJson = readPackageJson(path.join(capabilityRootDirectory, "package.json")); const targetBlankOpenerProtection = detectTargetBlankOpenerProtection( - project.rootDirectory, + capabilityRootDirectory, packageJson, ); if (targetBlankOpenerProtection !== undefined) { diff --git a/packages/core/tests/detect-target-blank-opener-protection.test.ts b/packages/core/tests/detect-target-blank-opener-protection.test.ts index 04e1a9ad1..bc9477a13 100644 --- a/packages/core/tests/detect-target-blank-opener-protection.test.ts +++ b/packages/core/tests/detect-target-blank-opener-protection.test.ts @@ -189,6 +189,40 @@ describe("detectTargetBlankOpenerProtection", () => { expect(capabilities.has("target-blank-needs-noreferrer")).toBe(true); }); + it.each([ + [ + "nested-browser-target", + { + name: "nested-browser-target", + dependencies: { react: "^18.0.0" }, + browserslist: ["chrome 80"], + }, + false, + ], + [ + "nested-electron-target", + { + name: "nested-electron-target", + dependencies: { react: "^18.0.0" }, + devDependencies: { electron: "^0.36.0" }, + }, + true, + ], + ])( + "inherits target-blank policy from the enclosing package for %s", + (caseName, packageJson, needsNoreferrer) => { + const projectDirectory = setupProject(caseName, packageJson); + const nestedDirectory = path.join(projectDirectory, "src", "components"); + fs.mkdirSync(nestedDirectory, { recursive: true }); + fs.writeFileSync(path.join(nestedDirectory, "button.tsx"), "export const Button = null;\n"); + + const capabilities = getCapabilities(discoverProject(nestedDirectory)); + + expect(capabilities.has("target-blank-needs-explicit-protection")).toBe(true); + expect(capabilities.has("target-blank-needs-noreferrer")).toBe(needsNoreferrer); + }, + ); + it("requires noopener once Electron adopted Chromium 49", () => { const packageJson: PackageJson = { name: "electron-0-37",