chore(web): audit-css.mjs — detect true (selector+context+body) duplicates
Extends the script with a strict-duplicate detector that distinguishes
real Pattern G dead code (same selector + same @media context + same
body, where everything but the LAST occurrence in cascade order has no
effect) from descendant-selector counts (which were mostly noise in the
previous report).
The new detector surfaced 29 dead rules in the prior commit (14398af);
it now reports 0 — the cleanup is complete and future regressions will
show up immediately.
Also wires the duplicate-rule data into the --json output for tooling.
Updates docs/ui/UI_AUDIT.md §5 #7 with the consolidation metrics.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
14398af792
commit
391b3a9fd3
@ -253,7 +253,11 @@ Quick Actions block fixed in commit `343ffb4`. 28 inline-style blocks remain —
|
|||||||
- `bytelyst-trading/grid-needs-minmax` — fires on bare `Nfr` in gridTemplateColumns. Caught + fixed 2 additional violations on first run.
|
- `bytelyst-trading/grid-needs-minmax` — fires on bare `Nfr` in gridTemplateColumns. Caught + fixed 2 additional violations on first run.
|
||||||
- `bytelyst-trading/no-button-with-stacked-children` — fires on `<Button>` with 2+ block children.
|
- `bytelyst-trading/no-button-with-stacked-children` — fires on `<Button>` with 2+ block children.
|
||||||
- All wired as `'warn'`. `npm run lint` reports zero `bytelyst-trading/*` warnings.
|
- All wired as `'warn'`. `npm run lint` reports zero `bytelyst-trading/*` warnings.
|
||||||
7. ✅ **CSS audit script.** `web/scripts/audit-css.mjs` shipped in `c10de34` (extended for hex audit in `063afdb`). Surfaces duplicate selectors + per-class !important counts + hardcoded hex outside token defs. Run via `npm run audit:css`. Top hotspots: `.positions-tab` (22 rules / 26 !imp), `.trade-plans-page` (20 / 25), `.history-tab` (17 / 22), `.trading-sidebar-logo` (9 / 20) — these are the Pattern G consolidation targets for a future pass.
|
7. ✅ **CSS audit script.** `web/scripts/audit-css.mjs` shipped in `c10de34` (extended for hex audit in `063afdb`, true-duplicate detection in the script-update commit after `14398af`). Surfaces duplicate selectors, per-class !important counts, hardcoded hex outside token defs, and **true duplicates** (same selector + same @media context + same body — pure copy-paste dead code). Run via `npm run audit:css`. Pattern G consolidation pass in commit `14398af` removed 29 dead/duplicate rules and 28 unnecessary `!important` declarations:
|
||||||
|
- `index.css`: 3082 → 2896 lines (-6%)
|
||||||
|
- Total `!important`: 183 → 149 (-19%)
|
||||||
|
- True-duplicate count: 29 → 0
|
||||||
|
- The 5-times-defined `.trading-sidebar-logo` consolidated into a single canonical version in `layout-fixes.css §22`.
|
||||||
|
|
||||||
### Medium-term (2–3 weeks)
|
### Medium-term (2–3 weeks)
|
||||||
|
|
||||||
|
|||||||
@ -65,11 +65,35 @@ function findHexViolations(content) {
|
|||||||
return violations;
|
return violations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the @media (or @supports etc) context for a given byte offset in CSS.
|
||||||
|
// Walks backward through the source counting unmatched braces.
|
||||||
|
function findEnclosingAtRule(content, idx) {
|
||||||
|
let depth = 0;
|
||||||
|
for (let i = idx - 1; i >= 0; i--) {
|
||||||
|
const c = content[i];
|
||||||
|
if (c === '}') depth++;
|
||||||
|
else if (c === '{') {
|
||||||
|
if (depth === 0) {
|
||||||
|
// Find selector-like text before this `{`
|
||||||
|
let j = i - 1;
|
||||||
|
while (j > 0 && content[j] !== '{' && content[j] !== '}') j--;
|
||||||
|
const snippet = content.slice(j + 1, i).trim().replace(/\s+/g, ' ');
|
||||||
|
if (snippet.startsWith('@')) return snippet;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function audit(filePath) {
|
function audit(filePath) {
|
||||||
const content = readSafe(filePath);
|
const content = readSafe(filePath);
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
// For each top-level rule block, collect class selectors and !important counts.
|
// For each top-level rule block, collect class selectors and !important counts.
|
||||||
const defs = new Map(); // className -> [{file, line, importants}]
|
const defs = new Map(); // className -> [{file, line, importants}]
|
||||||
|
// Same-selector + same-context + same-body groups (true Pattern G dead code).
|
||||||
|
const trueDups = new Map(); // key -> [{file, line, selector, context}]
|
||||||
let m;
|
let m;
|
||||||
RULE_BLOCK_RE.lastIndex = 0;
|
RULE_BLOCK_RE.lastIndex = 0;
|
||||||
while ((m = RULE_BLOCK_RE.exec(content)) !== null) {
|
while ((m = RULE_BLOCK_RE.exec(content)) !== null) {
|
||||||
@ -77,10 +101,21 @@ function audit(filePath) {
|
|||||||
const body = m[2];
|
const body = m[2];
|
||||||
const ruleStartIdx = m.index;
|
const ruleStartIdx = m.index;
|
||||||
const importants = (body.match(/!important/g) || []).length;
|
const importants = (body.match(/!important/g) || []).length;
|
||||||
// Skip @media / @supports / @keyframes blocks for "duplicate" detection — they're
|
|
||||||
// legitimately repeated. We track them separately below.
|
|
||||||
if (/^\s*@/.test(selector)) continue;
|
if (/^\s*@/.test(selector)) continue;
|
||||||
// Extract class names from selector list.
|
const normSel = selector.trim().replace(/\s+/g, ' ');
|
||||||
|
const normBody = body.trim().replace(/\s+/g, ' ');
|
||||||
|
const context = findEnclosingAtRule(content, ruleStartIdx);
|
||||||
|
if (normBody) {
|
||||||
|
const key = `${normSel}::${context || 'top'}::${normBody}`;
|
||||||
|
if (!trueDups.has(key)) trueDups.set(key, []);
|
||||||
|
trueDups.get(key).push({
|
||||||
|
file: filePath,
|
||||||
|
line: lineOf(content, ruleStartIdx),
|
||||||
|
selector: normSel,
|
||||||
|
context: context || 'top-level',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Also track loose "class touched by N rules" for the duplicate-class report
|
||||||
const classes = new Set();
|
const classes = new Set();
|
||||||
let s;
|
let s;
|
||||||
SELECTOR_RE.lastIndex = 0;
|
SELECTOR_RE.lastIndex = 0;
|
||||||
@ -92,10 +127,9 @@ function audit(filePath) {
|
|||||||
defs.get(cls).push({ file: filePath, line: lineOf(content, ruleStartIdx), importants });
|
defs.get(cls).push({ file: filePath, line: lineOf(content, ruleStartIdx), importants });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Total !important
|
|
||||||
const totalImportant = (content.match(/!important/g) || []).length;
|
const totalImportant = (content.match(/!important/g) || []).length;
|
||||||
const hexViolations = findHexViolations(content);
|
const hexViolations = findHexViolations(content);
|
||||||
return { filePath, defs, totalImportant, hexViolations };
|
return { filePath, defs, totalImportant, hexViolations, trueDups };
|
||||||
}
|
}
|
||||||
|
|
||||||
const reports = [];
|
const reports = [];
|
||||||
@ -109,6 +143,7 @@ for (const p of opts.paths) {
|
|||||||
const merged = new Map();
|
const merged = new Map();
|
||||||
let totalImportant = 0;
|
let totalImportant = 0;
|
||||||
const allHexViolations = [];
|
const allHexViolations = [];
|
||||||
|
const allTrueDups = new Map(); // key -> occurrences across files
|
||||||
for (const r of reports) {
|
for (const r of reports) {
|
||||||
totalImportant += r.totalImportant;
|
totalImportant += r.totalImportant;
|
||||||
for (const [cls, locs] of r.defs.entries()) {
|
for (const [cls, locs] of r.defs.entries()) {
|
||||||
@ -116,7 +151,12 @@ for (const r of reports) {
|
|||||||
merged.get(cls).push(...locs);
|
merged.get(cls).push(...locs);
|
||||||
}
|
}
|
||||||
for (const v of r.hexViolations) allHexViolations.push({ file: r.filePath, ...v });
|
for (const v of r.hexViolations) allHexViolations.push({ file: r.filePath, ...v });
|
||||||
|
for (const [k, occs] of r.trueDups.entries()) {
|
||||||
|
if (!allTrueDups.has(k)) allTrueDups.set(k, []);
|
||||||
|
allTrueDups.get(k).push(...occs);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const trueDupGroups = [...allTrueDups.values()].filter(g => g.length >= 2);
|
||||||
|
|
||||||
// Filter to classes with >= threshold definitions
|
// Filter to classes with >= threshold definitions
|
||||||
const dups = [...merged.entries()]
|
const dups = [...merged.entries()]
|
||||||
@ -135,6 +175,12 @@ if (opts.json) {
|
|||||||
duplicateSelectors: dups.map(([cls, locs]) => ({ class: cls, count: locs.length, locations: locs })),
|
duplicateSelectors: dups.map(([cls, locs]) => ({ class: cls, count: locs.length, locations: locs })),
|
||||||
importantByClass: importantPerClass.map(([cls, n]) => ({ class: cls, count: n })),
|
importantByClass: importantPerClass.map(([cls, n]) => ({ class: cls, count: n })),
|
||||||
hexViolations: allHexViolations,
|
hexViolations: allHexViolations,
|
||||||
|
trueDuplicates: trueDupGroups.map(g => ({
|
||||||
|
selector: g[0].selector,
|
||||||
|
context: g[0].context,
|
||||||
|
count: g.length,
|
||||||
|
locations: g.map(o => `${path.basename(o.file)}:${o.line}`),
|
||||||
|
})),
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
@ -158,6 +204,17 @@ for (const [cls, n] of importantPerClass.slice(0, 15)) {
|
|||||||
console.log(` ${String(n).padStart(5)} .${cls}`);
|
console.log(` ${String(n).padStart(5)} .${cls}`);
|
||||||
}
|
}
|
||||||
console.log('');
|
console.log('');
|
||||||
|
console.log('');
|
||||||
|
console.log(`True duplicate rules (same selector + same @media + same body — pure dead code): ${trueDupGroups.reduce((s, g) => s + g.length - 1, 0)}`);
|
||||||
|
if (trueDupGroups.length > 0) {
|
||||||
|
console.log(' (these are strict copy-paste duplicates; the LAST one in source order wins, the rest are dead code and can be deleted)');
|
||||||
|
for (const g of trueDupGroups.slice(0, 10)) {
|
||||||
|
console.log(` ${g.length}x ${g[0].selector} [${g[0].context}]`);
|
||||||
|
for (const o of g) console.log(` ${path.basename(o.file)}:${o.line}`);
|
||||||
|
}
|
||||||
|
if (trueDupGroups.length > 10) console.log(` ... ${trueDupGroups.length - 10} more groups`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`Hardcoded hex colors outside token defs / fallbacks: ${allHexViolations.length}`);
|
console.log(`Hardcoded hex colors outside token defs / fallbacks: ${allHexViolations.length}`);
|
||||||
if (allHexViolations.length > 0) {
|
if (allHexViolations.length > 0) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user