From e975bec4125995b5265f400e36f0be70c3865a19 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Sat, 15 Aug 2026 01:07:52 +0500 Subject: [PATCH] Harden monorepo design-root recognition and the home-directory stop (#570) Read all four workspace-glob sources context.mjs reads (.impeccable projectRoots, package.json workspaces, lerna packages, pnpm packages), so lerna-glob roots and impeccable projectRoots no longer hit the same abstention. Compare the walk against both the logical and realpath forms of the home directory: on distros that symlink home paths (/home to /var/home) the string comparison never matched, and the post-boundary walk could inherit a workspace-declaring home's DESIGN.md. Written with AI assistance (Cursor); reviewed by maintainer. Co-authored-by: Cursor --- cli/engine/design-system.mjs | 34 +++++++++++-- tests/detect-cli-design-monorepo.test.mjs | 61 +++++++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/cli/engine/design-system.mjs b/cli/engine/design-system.mjs index 2601c0d54..de180d093 100644 --- a/cli/engine/design-system.mjs +++ b/cli/engine/design-system.mjs @@ -597,9 +597,20 @@ function designSystemStartDir(targetPath, cwd = process.cwd()) { // Returns { dir, hasDesign } for the stopping directory, or null when the walk // runs out. This is the fix for cross-project contamination. function readWorkspacePatterns(dir) { + const patterns = []; + // Same four glob sources as context.mjs's readProjectPatterns: Impeccable + // projectRoots, package.json workspaces, lerna packages, pnpm packages. + for (const name of ['config.json', 'config.local.json']) { + const roots = safeReadJson(path.join(dir, '.impeccable', name))?.projectRoots; + if (Array.isArray(roots)) { + patterns.push(...roots.filter(entry => typeof entry === 'string' && entry.trim()).map(entry => entry.trim())); + } + } const workspaces = safeReadJson(path.join(dir, 'package.json'))?.workspaces; - const patterns = Array.isArray(workspaces) ? [...workspaces] - : Array.isArray(workspaces?.packages) ? [...workspaces.packages] : []; + if (Array.isArray(workspaces)) patterns.push(...workspaces); + else if (Array.isArray(workspaces?.packages)) patterns.push(...workspaces.packages); + const lernaPackages = safeReadJson(path.join(dir, 'lerna.json'))?.packages; + if (Array.isArray(lernaPackages)) patterns.push(...lernaPackages); try { let inPackages = false; for (const line of fs.readFileSync(path.join(dir, 'pnpm-workspace.yaml'), 'utf-8').split(/\r?\n/)) { @@ -632,9 +643,22 @@ function isMonorepoRoot(dir) { }); } +// Both forms of the home directory. The walk compares path strings, and a +// symlinked home (e.g. /home -> /var/home) never string-matches the physical +// paths a cwd-resolved target produces, which would let the post-boundary walk +// sail through $HOME and inherit from it. +function homeDirForms() { + const homeDir = path.resolve(os.homedir()); + const forms = new Set([homeDir]); + try { + forms.add(fs.realpathSync(homeDir)); + } catch { /* keep the logical form only */ } + return forms; +} + export function findDesignRoot(startDir) { let dir = path.resolve(startDir); - const homeDir = path.resolve(os.homedir()); + const homeDirs = homeDirForms(); let boundary = null; while (true) { if (!boundary && resolveDesignMdPath(dir)) return { dir, hasDesign: true }; @@ -646,7 +670,7 @@ export function findDesignRoot(startDir) { // the walk with nothing inherited. The home directory is never an // owning root, same as context.mjs's findMonorepoRoot, which stops at // homeDir before its monorepo check. - if (dir !== homeDir && isMonorepoRoot(dir)) return { dir, hasDesign: !!resolveDesignMdPath(dir) }; + if (!homeDirs.has(dir) && isMonorepoRoot(dir)) return { dir, hasDesign: !!resolveDesignMdPath(dir) }; if (fs.existsSync(path.join(dir, '.git'))) return boundary; } else if (PROJECT_ROOT_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)))) { boundary = { dir, hasDesign: false }; @@ -654,7 +678,7 @@ export function findDesignRoot(startDir) { // with its own .git, inherits nothing from above. if (isMonorepoRoot(dir) || fs.existsSync(path.join(dir, '.git'))) return boundary; } - if (dir === homeDir) return boundary; + if (homeDirs.has(dir)) return boundary; const parent = path.dirname(dir); if (parent === dir) return boundary; dir = parent; diff --git a/tests/detect-cli-design-monorepo.test.mjs b/tests/detect-cli-design-monorepo.test.mjs index 1c4fe72c5..db7b8c497 100644 --- a/tests/detect-cli-design-monorepo.test.mjs +++ b/tests/detect-cli-design-monorepo.test.mjs @@ -117,6 +117,41 @@ describe('detect CLI monorepo DESIGN.md inheritance', () => { ); }); + it('lerna packages globs: workspace outside apps/packages inherits root DESIGN.md', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-mono-lerna-')); + tempRoots.push(dir); + fs.writeFileSync(path.join(dir, 'DESIGN.md'), DESIGN_MD); + fs.writeFileSync(path.join(dir, 'lerna.json'), '{"packages":["modules/*"]}'); + fs.mkdirSync(path.join(dir, 'modules/web'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'modules/web/package.json'), '{"name":"web"}'); + const page = path.join(dir, 'modules/web/page.html'); + fs.writeFileSync(page, PAGE_HTML); + + const findings = runDetect(dir, [page]); + assert.ok( + fontFindingsFor(findings, page).some((f) => f.ignoreValue === 'verdana'), + 'lerna packages globs must be read as workspace declarations', + ); + }); + + it('impeccable projectRoots: workspace inherits root DESIGN.md', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-mono-iroots-')); + tempRoots.push(dir); + fs.writeFileSync(path.join(dir, 'DESIGN.md'), DESIGN_MD); + fs.mkdirSync(path.join(dir, '.impeccable'), { recursive: true }); + fs.writeFileSync(path.join(dir, '.impeccable/config.json'), '{"projectRoots":["sites/*"]}'); + fs.mkdirSync(path.join(dir, 'sites/docs'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'sites/docs/package.json'), '{"name":"docs"}'); + const page = path.join(dir, 'sites/docs/page.html'); + fs.writeFileSync(page, PAGE_HTML); + + const findings = runDetect(dir, [page]); + assert.ok( + fontFindingsFor(findings, page).some((f) => f.ignoreValue === 'verdana'), + 'impeccable projectRoots must be read as workspace declarations', + ); + }); + it('pnpm flow list with inline comment and non-standard dirs still detected', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-mono-flow-')); tempRoots.push(dir); @@ -195,4 +230,30 @@ typography: 'a project under a workspace-declaring $HOME must not inherit its DESIGN.md', ); }); + + it('symlinked $HOME still stops the walk', () => { + // Some distros symlink home paths (/home -> /var/home), so $HOME never + // string-matches the physical paths a cwd-resolved target produces. The + // walk must compare against the realpath form too. + const real = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-mono-realhome-')); + tempRoots.push(real); + const link = path.join(os.tmpdir(), `impeccable-detect-mono-linkhome-${path.basename(real).slice(-6)}`); + fs.symlinkSync(real, link); + tempRoots.push(link); + fs.writeFileSync(path.join(real, 'DESIGN.md'), DESIGN_MD); + fs.writeFileSync(path.join(real, 'pnpm-workspace.yaml'), "packages:\n - 'apps/*'\n"); + fs.mkdirSync(path.join(real, 'project'), { recursive: true }); + fs.writeFileSync(path.join(real, 'project/package.json'), '{"name":"p"}'); + const page = path.join(real, 'project/page.html'); + fs.writeFileSync(page, PAGE_HTML); + + // HOME is the symlink; the target is passed via its physical path, so a + // logical-only comparison would walk straight past home and inherit. + const findings = runDetect(real, [fs.realpathSync(page)], { HOME: link, USERPROFILE: link }); + assert.equal( + fontFindingsFor(findings, page).length + fontFindingsFor(findings, fs.realpathSync(page)).length, + 0, + 'a symlinked $HOME must still stop the walk before inheriting', + ); + }); });