// CSS 健康度审计脚本。 // 用法: npm run css:audit // 输出每个 CSS 文件的行数、选择器数、!important 数、@media 数, // 以及 !important 密度(每 100 行的 !important 数)。 // 用于建立基线、跟踪 CSS 瘦身进度、防止 !important 回潮。 import { readFileSync, readdirSync, statSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join, relative } from "node:path"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "styles"); const REPORT = []; function scanCssFile(filePath) { const content = readFileSync(filePath, "utf-8"); const lines = content.split(/\r?\n/).length; const selectors = (content.match(/\{/g) || []).length; const important = (content.match(/!important/g) || []).length; const media = (content.match(/@media/g) || []).length; const density = lines > 0 ? ((important / lines) * 100).toFixed(1) : "0"; return { lines, selectors, important, media, density }; } function walk(dir) { for (const entry of readdirSync(dir)) { const full = join(dir, entry); const st = statSync(full); if (st.isDirectory()) { walk(full); } else if (entry.endsWith(".css")) { const rel = relative(ROOT, full).replace(/\\/g, "/"); REPORT.push({ file: rel, ...scanCssFile(full) }); } } } walk(ROOT); // Sort by !important count descending to surface the worst offenders. REPORT.sort((a, b) => b.important - a.important); const totals = REPORT.reduce( (acc, r) => { acc.lines += r.lines; acc.selectors += r.selectors; acc.important += r.important; acc.media += r.media; return acc; }, { lines: 0, selectors: 0, important: 0, media: 0 }, ); const pad = (s, n) => String(s).padEnd(n); const num = (s, n) => String(s).padStart(n); console.log("\nCSS Audit Report — src/styles/\n"); console.log( `${pad("File", 52)} ${num("Lines", 7)} ${num("Sel", 6)} ${num("!imp", 7)} ${num("@media", 7)} imp/100ln`, ); console.log("-".repeat(92)); for (const r of REPORT) { console.log( `${pad(r.file, 52)} ${num(r.lines, 7)} ${num(r.selectors, 6)} ${num(r.important, 7)} ${num(r.media, 7)} ${r.density}`, ); } console.log("-".repeat(92)); console.log( `${pad("TOTAL", 52)} ${num(totals.lines, 7)} ${num(totals.selectors, 6)} ${num(totals.important, 7)} ${num(totals.media, 7)} ${((totals.important / totals.lines) * 100).toFixed(1)}`, ); console.log(""); // Exit non-zero if total !important exceeds a budget threshold. // Post-@layer refactoring baseline: ~970 (formerly ~7812). // Budget set to 2000 to prevent regression while allowing controlled growth. const IMPORTANT_BUDGET = 2000; if (totals.important > IMPORTANT_BUDGET) { console.error( `FAIL: !important count ${totals.important} exceeds budget ${IMPORTANT_BUDGET}. ` + `Run with --no-important-check to bypass (not recommended).`, ); process.exit(1); } else { console.log( `OK: !important count ${totals.important} within budget ${IMPORTANT_BUDGET} ` + `(headroom ${IMPORTANT_BUDGET - totals.important}).`, ); }