mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 00:26:41 +03:00
Fix: harden live overlay detector waivers (#639 follow-up)
Read waiver config from every live root (appRoot, contextRoot, repoRoot), so monorepo projects whose config lives at the repo root reach the overlay; serialize served roots and page identities repo-relative there. Resolve each page URL to its actual serving file via the inject config's resolved page list before applying file-scoped waivers; ambiguous URLs keep the conservative common-ancestor fallback (PR #645 review discussion r3840011436). Honour detector.ignoreFiles: a wholly waived page now scans to zero findings in the overlay, matching the CLI and the edit hook. Guard the resolver call so a throwing resolver degrades to an unfiltered scan instead of breaking the detect toggle. Match design-system-color waivers by color value across hex and rgb() spellings, and stop extracting font values for bounce-easing findings, mirroring extractFindingIgnoreValue. Regenerate the browser bundle. AI-assisted change: reviewed, planned, and implemented with Claude Code under maintainer direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude Fable 5
parent
09506a9bb5
commit
152d6940b0
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Project detector waivers for the live overlay (issue #639, hardened in the
|
||||
* PR #645 follow-up). One place decides what the /live.js prelude serializes
|
||||
* as window.__IMPECCABLE_PROJECT_IGNORES__:
|
||||
*
|
||||
* ignoreRules detector.ignoreRules, unioned across every live root.
|
||||
* ignoreValues detector.ignoreValues entries ({rule, value, files?}),
|
||||
* deduped across roots; createdAt/reason stay local.
|
||||
* ignoreFiles detector.ignoreFiles globs, unioned across roots, so a
|
||||
* wholly waived page scans to zero findings in the overlay
|
||||
* just as it reports nothing through the CLI and the hook.
|
||||
* roots served-root prefixes derived from the inject config's own
|
||||
* `files` globs. Never derived from the ignore globs: one
|
||||
* entry scoped to prototype/library/** would lend
|
||||
* prototype/library/ as a candidate prefix to every page,
|
||||
* and that rule would suppress site-wide (issue #639).
|
||||
* pageFiles the inject config's `files` expanded to real project
|
||||
* files, so the browser can resolve a URL to the one file it
|
||||
* actually serves instead of trying every root (PR #645
|
||||
* review: with src/ and public/ both served, /foo.html must
|
||||
* not borrow src/foo.html's waivers while actually serving
|
||||
* public/foo.html).
|
||||
*
|
||||
* Config is read from every root the live session spans: the appRoot the
|
||||
* server chdir'd onto, plus contextRoot and repoRoot when they differ. The
|
||||
* edit hook keys the same config at the session cwd (the repo root in a
|
||||
* monorepo, via resolveCacheCwd), and `impeccable detect` reads it from its
|
||||
* invocation cwd, so reading only the appRoot silently dropped every waiver
|
||||
* in exactly the monorepo layouts the roots manifest exists for. Reading is
|
||||
* additive across roots, matching readConfig's own union of config.json and
|
||||
* config.local.json.
|
||||
*
|
||||
* In a monorepo, roots and pageFiles are serialized repo-relative (the
|
||||
* appRoot's path inside the repo is prefixed), so waivers spelled from
|
||||
* either root match through the resolver's suffix expansion.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { readConfig } from '../hook-lib.mjs';
|
||||
import { resolveFiles } from '../live-inject.mjs';
|
||||
import { resolveLiveConfigPath } from '../lib/impeccable-paths.mjs';
|
||||
|
||||
// Serializing thousands of page identities into every /live.js response
|
||||
// helps nobody; past this cap pageFiles is omitted and the resolver falls
|
||||
// back to the served-root common ancestor, which is correct, just less
|
||||
// precise about cross-root duplicates.
|
||||
const PAGE_FILES_CAP = 500;
|
||||
|
||||
export function collectProjectDetectorIgnores({ appRoot, contextRoot, repoRoot, scriptsDir } = {}) {
|
||||
const configRoots = [];
|
||||
for (const dir of [appRoot, contextRoot, repoRoot]) {
|
||||
if (typeof dir !== 'string' || !dir) continue;
|
||||
const resolved = path.resolve(dir);
|
||||
if (!configRoots.includes(resolved)) configRoots.push(resolved);
|
||||
}
|
||||
if (configRoots.length === 0) configRoots.push(process.cwd());
|
||||
|
||||
const ignoreRules = new Set();
|
||||
const ignoreFiles = new Set();
|
||||
const valueEntries = new Map();
|
||||
for (const dir of configRoots) {
|
||||
// readConfig merges config.json with the gitignored config.local.json
|
||||
// and type-checks both, exactly as the edit hook reads the same pair.
|
||||
const config = readConfig(dir);
|
||||
for (const rule of Array.isArray(config.ignoreRules) ? config.ignoreRules : []) {
|
||||
if (typeof rule === 'string' && rule.trim()) ignoreRules.add(rule);
|
||||
}
|
||||
for (const glob of Array.isArray(config.ignoreFiles) ? config.ignoreFiles : []) {
|
||||
if (typeof glob === 'string' && glob.trim()) ignoreFiles.add(glob);
|
||||
}
|
||||
for (const entry of Array.isArray(config.ignoreValues) ? config.ignoreValues : []) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
// readConfig already normalized rule/value and folded `file` into
|
||||
// `files`; serve only what the browser matches on.
|
||||
const serialized = {
|
||||
rule: entry.rule,
|
||||
value: entry.value,
|
||||
...(Array.isArray(entry.files) && entry.files.length > 0 ? { files: entry.files } : {}),
|
||||
};
|
||||
const key = JSON.stringify([serialized.rule, serialized.value,
|
||||
Array.isArray(serialized.files) ? [...serialized.files].sort() : []]);
|
||||
if (!valueEntries.has(key)) valueEntries.set(key, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
const served = readLiveServedPages({ appRoot: configRoots[0], repoRoot, scriptsDir });
|
||||
return {
|
||||
ignoreRules: [...ignoreRules],
|
||||
ignoreValues: [...valueEntries.values()],
|
||||
ignoreFiles: [...ignoreFiles],
|
||||
roots: served.roots,
|
||||
pageFiles: served.pageFiles,
|
||||
};
|
||||
}
|
||||
|
||||
function readLiveServedPages({ appRoot, repoRoot, scriptsDir }) {
|
||||
let live = null;
|
||||
try {
|
||||
const configPath = resolveLiveConfigPath({ cwd: appRoot, scriptsDir });
|
||||
live = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
} catch {
|
||||
// No readable inject config: the browser matches URL paths as-is.
|
||||
return { roots: [], pageFiles: [] };
|
||||
}
|
||||
const files = Array.isArray(live?.files)
|
||||
? live.files.filter((glob) => typeof glob === 'string' && glob)
|
||||
: [];
|
||||
|
||||
// A monorepo appRoot serializes identities repo-relative, so waivers
|
||||
// spelled from either root match through the resolver's suffix expansion.
|
||||
let prefix = '';
|
||||
if (typeof repoRoot === 'string' && repoRoot) {
|
||||
const rel = path.relative(path.resolve(repoRoot), path.resolve(appRoot)).split(path.sep).join('/');
|
||||
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) prefix = `${rel}/`;
|
||||
}
|
||||
|
||||
const roots = [...new Set(files.map((glob) => {
|
||||
const wildcardAt = glob.search(/[*?{]/);
|
||||
const head = wildcardAt === -1 ? glob : glob.slice(0, wildcardAt);
|
||||
const cut = head.lastIndexOf('/');
|
||||
return prefix + (cut > -1 ? head.slice(0, cut + 1) : '');
|
||||
}))];
|
||||
|
||||
let pageFiles = [];
|
||||
try {
|
||||
pageFiles = resolveFiles(appRoot, { ...live, files })
|
||||
.filter((rel) => {
|
||||
// resolveFiles passes literal entries through even when they do not
|
||||
// exist; a missing file is nobody's identity.
|
||||
try { return fs.statSync(path.join(appRoot, rel)).isFile(); } catch { return false; }
|
||||
})
|
||||
.map((rel) => prefix + rel);
|
||||
} catch {
|
||||
pageFiles = [];
|
||||
}
|
||||
if (pageFiles.length > PAGE_FILES_CAP) pageFiles = [];
|
||||
|
||||
return { roots, pageFiles };
|
||||
}
|
||||
Reference in New Issue
Block a user