chore(web): add CSS audit script for duplicate selectors (UI audit #7)
Adds web/scripts/audit-css.mjs — surfaces classes that appear in many rules (likely targets of style drift) and per-class !important counts (specificity-fight indicators). Implements the script suggested by docs/ui/UI_AUDIT.md §5 #7 / Pattern G. Run: pnpm --filter @bytelyst/trading-web run audit:css (or: cd web && npm run audit:css) Initial run on src/index.css + src/App.css + src/layout-fixes.css exposes the top hotspots: - .positions-tab 22 rules, 26 !important - .trade-plans-page 20 rules, 25 !important - .history-tab 17 rules, 22 !important - .trading-sidebar-logo 9 rules, 20 !important These are the targets for the future Pattern G consolidation pass. The script supports --threshold N and --json for tooling. 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
a0fcb65f5d
commit
c10de34a11
@ -21,7 +21,8 @@
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:viewport": "playwright test e2e/viewport-matrix.spec.ts",
|
||||
"test:e2e:overflow": "playwright test e2e/horizontal-overflow.spec.ts"
|
||||
"test:e2e:overflow": "playwright test e2e/horizontal-overflow.spec.ts",
|
||||
"audit:css": "node scripts/audit-css.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bytelyst/api-client": "^0.1.6",
|
||||
|
||||
129
web/scripts/audit-css.mjs
Normal file
129
web/scripts/audit-css.mjs
Normal file
@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env node
|
||||
// Audit CSS files in src/ for:
|
||||
// - Class selectors defined more than once (rule duplication / drift)
|
||||
// - Per-class !important counts (specificity-fight indicators)
|
||||
//
|
||||
// Outputs a report to stdout. Exits 0 always; this is informational.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/audit-css.mjs [path-or-glob...]
|
||||
// node scripts/audit-css.mjs --threshold 2 # only show classes with >= N defs
|
||||
// node scripts/audit-css.mjs --json # machine-readable output
|
||||
//
|
||||
// See docs/ui/UI_AUDIT.md §5 #7 and Pattern G.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { argv, exit } from 'node:process';
|
||||
|
||||
const args = argv.slice(2);
|
||||
const opts = { threshold: 2, json: false, paths: [] };
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === '--threshold' || a === '-t') opts.threshold = parseInt(args[++i], 10);
|
||||
else if (a === '--json') opts.json = true;
|
||||
else opts.paths.push(a);
|
||||
}
|
||||
if (opts.paths.length === 0) opts.paths = ['src/index.css', 'src/App.css', 'src/layout-fixes.css'];
|
||||
|
||||
const SELECTOR_RE = /(^|[\s},])\s*\.([a-zA-Z_][\w-]*)\b/g;
|
||||
const RULE_BLOCK_RE = /([^{}]+?)\{([^{}]*)\}/gs;
|
||||
|
||||
function readSafe(p) {
|
||||
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
||||
}
|
||||
|
||||
function lineOf(content, idx) {
|
||||
return content.slice(0, idx).split('\n').length;
|
||||
}
|
||||
|
||||
function audit(filePath) {
|
||||
const content = readSafe(filePath);
|
||||
if (content == null) return null;
|
||||
// For each top-level rule block, collect class selectors and !important counts.
|
||||
const defs = new Map(); // className -> [{file, line, importants}]
|
||||
let m;
|
||||
RULE_BLOCK_RE.lastIndex = 0;
|
||||
while ((m = RULE_BLOCK_RE.exec(content)) !== null) {
|
||||
const selector = m[1];
|
||||
const body = m[2];
|
||||
const ruleStartIdx = m.index;
|
||||
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;
|
||||
// Extract class names from selector list.
|
||||
const classes = new Set();
|
||||
let s;
|
||||
SELECTOR_RE.lastIndex = 0;
|
||||
while ((s = SELECTOR_RE.exec(selector)) !== null) {
|
||||
classes.add(s[2]);
|
||||
}
|
||||
for (const cls of classes) {
|
||||
if (!defs.has(cls)) defs.set(cls, []);
|
||||
defs.get(cls).push({ file: filePath, line: lineOf(content, ruleStartIdx), importants });
|
||||
}
|
||||
}
|
||||
// Total !important
|
||||
const totalImportant = (content.match(/!important/g) || []).length;
|
||||
return { filePath, defs, totalImportant };
|
||||
}
|
||||
|
||||
const reports = [];
|
||||
for (const p of opts.paths) {
|
||||
const abs = path.resolve(p);
|
||||
const r = audit(abs);
|
||||
if (r) reports.push(r);
|
||||
}
|
||||
|
||||
// Merge defs across files
|
||||
const merged = new Map();
|
||||
let totalImportant = 0;
|
||||
for (const r of reports) {
|
||||
totalImportant += r.totalImportant;
|
||||
for (const [cls, locs] of r.defs.entries()) {
|
||||
if (!merged.has(cls)) merged.set(cls, []);
|
||||
merged.get(cls).push(...locs);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to classes with >= threshold definitions
|
||||
const dups = [...merged.entries()]
|
||||
.filter(([, locs]) => locs.length >= opts.threshold)
|
||||
.sort((a, b) => b[1].length - a[1].length);
|
||||
|
||||
const importantPerClass = [...merged.entries()]
|
||||
.map(([cls, locs]) => [cls, locs.reduce((s, l) => s + l.importants, 0)])
|
||||
.filter(([, n]) => n > 0)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify({
|
||||
files: opts.paths,
|
||||
totalImportant,
|
||||
duplicateSelectors: dups.map(([cls, locs]) => ({ class: cls, count: locs.length, locations: locs })),
|
||||
importantByClass: importantPerClass.map(([cls, n]) => ({ class: cls, count: n })),
|
||||
}, null, 2));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
console.log('=== CSS audit ===');
|
||||
console.log(`Files scanned: ${opts.paths.join(', ')}`);
|
||||
console.log(`Total !important declarations: ${totalImportant}`);
|
||||
console.log('');
|
||||
console.log(`Class selectors defined ${opts.threshold}+ times (top 30):`);
|
||||
console.log(' count class');
|
||||
console.log(' ----- ----------------------------------------');
|
||||
for (const [cls, locs] of dups.slice(0, 30)) {
|
||||
const lines = locs.map(l => `${path.basename(l.file)}:${l.line}`).join(', ');
|
||||
console.log(` ${String(locs.length).padStart(5)} .${cls} (${lines})`);
|
||||
}
|
||||
console.log('');
|
||||
console.log('Top 15 classes by !important count (each declaration is a specificity fight):');
|
||||
console.log(' count class');
|
||||
console.log(' ----- ----------------------------------------');
|
||||
for (const [cls, n] of importantPerClass.slice(0, 15)) {
|
||||
console.log(` ${String(n).padStart(5)} .${cls}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log('See docs/ui/UI_AUDIT.md Pattern G for fix guidance.');
|
||||
Loading…
Reference in New Issue
Block a user