- ui-audit.mjs:路由发现扩展至 33 路由(standalone 构建产物 + erp-upgrade 系列); GA/gtm 请求 mock(gtag/js 空 JS + collect 204);第三方统计噪音与同源 ERR_ABORTED(重调度成功/prefetch)降级 INFO;LCP/CLS 断言按系统负载门控 (load >= 5 跳过并注明,防测量环境假阳性);LCP 超阈值热缓存 reload 复测 取优(lcpCold 留存冷值);clip 探针 sr-only 白名单;onReqFail 防御性修复 - ui-audit-report.mjs(新增):多主题探针聚合 HTML 报告,含截图画廊与 INFO 分级 - hardcoded-color-audit.mjs:bg-black/50 半透明遮罩误报修正(负向前瞻)+ 白名单 - 新增 brand-usage-scan.mjs / brand-ink-migrate.mjs(品牌令牌迁移辅助)
61 lines
2.3 KiB
JavaScript
61 lines
2.3 KiB
JavaScript
/**
|
|
* 品牌色 utility 用量盘点(为「文本令牌 / 底色令牌」拆分提供决策依据)
|
|
*
|
|
* 统计 src 下所有 `(?:变体前缀:)?(属性)-brand(?:-子token)?(/透明度)?` 的出现次数与文件分布。
|
|
* 用法:node scripts/audit/brand-usage-scan.mjs [--files]
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
const EXTS = new Set(['.tsx', '.ts', '.css', '.jsx', '.js', '.mdx']);
|
|
const SKIP = new Set(['node_modules', '.next', '.git', '_archive', 'dist', 'coverage']);
|
|
|
|
function walk(dir, out = []) {
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const p = path.join(dir, e.name);
|
|
if (e.isDirectory()) { if (!SKIP.has(e.name)) walk(p, out); }
|
|
else if (EXTS.has(path.extname(e.name))) out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const VARIANTS = '(?:hover:|focus:|focus-visible:|active:|group-hover:|dark:|sm:|md:|lg:|xl:)*';
|
|
const PROPS = '(bg|text|border|from|to|via|fill|stroke|ring|divide|decoration|outline|shadow|placeholder|caret|accent)';
|
|
const re = new RegExp(`(?:^|[\\s\`"'])${VARIANTS}${PROPS}-(brand(?:-[a-z]+)?)(?:/[0-9]+)?(?=[\\s\`"':/]|$)`, 'g');
|
|
|
|
const byToken = new Map();
|
|
const byFile = new Map();
|
|
const byPropToken = new Map();
|
|
|
|
for (const f of walk(path.join(ROOT, 'src'))) {
|
|
let s;
|
|
try { s = fs.readFileSync(f, 'utf8'); } catch { continue; }
|
|
let m;
|
|
while ((m = re.exec(s))) {
|
|
const key = `${m[1]}-${m[2]}`;
|
|
byToken.set(key, (byToken.get(key) || 0) + 1);
|
|
byPropToken.set(m[1], (byPropToken.get(m[1]) || 0) + 1);
|
|
byFile.set(f, (byFile.get(f) || 0) + 1);
|
|
}
|
|
}
|
|
|
|
const sortDesc = a => [...a.entries()].sort((x, y) => y[1] - x[1]);
|
|
|
|
console.log('=== brand utility 用量(src 全量)===');
|
|
for (const [k, n] of sortDesc(byToken)) console.log(String(n).padStart(5), k);
|
|
const total = [...byToken.values()].reduce((s, n) => s + n, 0);
|
|
console.log('总计'.padStart(5), total);
|
|
|
|
console.log('\n=== 按属性聚合 ===');
|
|
for (const [k, n] of sortDesc(byPropToken)) console.log(String(n).padStart(5), k);
|
|
|
|
if (process.argv.includes('--files')) {
|
|
console.log('\n=== 文件分布 Top 25 ===');
|
|
for (const [f, n] of sortDesc(byFile).slice(0, 25)) {
|
|
console.log(String(n).padStart(5), path.relative(ROOT, f));
|
|
}
|
|
}
|