mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 09:06:53 +03:00
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user