Main merge work #19
+2
-1
@@ -10,7 +10,8 @@
|
||||
"type-check": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"css:audit": "node scripts/css-audit.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "5.3.0",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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.
|
||||
// Current baseline: ~7795. Set budget slightly above to allow incremental work
|
||||
// while preventing uncontrolled growth.
|
||||
const IMPORTANT_BUDGET = 7820;
|
||||
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}).`,
|
||||
);
|
||||
}
|
||||
+14
-14083
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user