89 lines
3.2 KiB
JavaScript
89 lines
3.2 KiB
JavaScript
/**
|
||
* 产物结构校验:把 dist 里所有 WXML 的 class 抽出来,回查是否都有对应 WXSS 规则。
|
||
* 用途:Tailwind 改造后确认「模板写的类」与「编译出的样式」没有错配(尤其任意值类会被
|
||
* weapp-tailwindcss 重写成安全类名,肉眼看不出来)。
|
||
* 用法:node scripts/verify-build.mjs [distDir]
|
||
*/
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
|
||
const ROOT = path.resolve(process.argv[2] || "dist/build/mp-weixin");
|
||
|
||
if (!fs.existsSync(ROOT)) {
|
||
console.error(`找不到构建产物目录:${ROOT}\n请先执行 npm run build:mp-weixin`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const cssFiles = [];
|
||
const wxmlFiles = [];
|
||
(function walk(dir) {
|
||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||
const full = path.join(dir, entry.name);
|
||
if (entry.isDirectory()) walk(full);
|
||
else if (entry.name.endsWith(".wxss")) cssFiles.push(full);
|
||
else if (entry.name.endsWith(".wxml")) wxmlFiles.push(full);
|
||
}
|
||
})(ROOT);
|
||
|
||
/* ---- 1. 收集 WXSS 中定义过的类名 ---- */
|
||
const defined = new Set();
|
||
let cssBytes = 0;
|
||
for (const file of cssFiles) {
|
||
const css = fs.readFileSync(file, "utf8");
|
||
cssBytes += Buffer.byteLength(css);
|
||
// 选择器里的 .class,可能带反斜杠转义(如 .w-\[10px\])
|
||
for (const m of css.matchAll(/\.((?:[A-Za-z0-9_-]|\\.)+)/g)) {
|
||
defined.add(m[1].replace(/\\/g, ""));
|
||
}
|
||
}
|
||
|
||
/* ---- 2. 收集 WXML 里 class 属性用到的 token ---- */
|
||
const used = new Map();
|
||
let staticCount = 0;
|
||
let dynamicCount = 0;
|
||
|
||
for (const file of wxmlFiles) {
|
||
const src = fs.readFileSync(file, "utf8");
|
||
for (const m of src.matchAll(/class="([^"]*)"/g)) {
|
||
let value = m[1];
|
||
if (value.includes("{{")) {
|
||
// 动态绑定:把三元/对象里的字符串字面量也捞出来一起校验
|
||
dynamicCount += 1;
|
||
for (const lit of value.match(/'[^']*'|"[^"]*"/g) || []) value += ` ${lit.slice(1, -1)}`;
|
||
value = value.replace(/\{\{[^}]*\}\}/g, " ");
|
||
} else {
|
||
staticCount += 1;
|
||
}
|
||
for (const token of value.split(/\s+/)) {
|
||
const cls = token.trim();
|
||
if (!cls) continue;
|
||
if (!used.has(cls)) used.set(cls, new Set());
|
||
used.get(cls).add(path.relative(ROOT, file));
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- 3. 校验 ---- */
|
||
const missing = [];
|
||
for (const [cls, files] of used) {
|
||
if (!defined.has(cls)) missing.push([cls, [...files].slice(0, 3)]);
|
||
}
|
||
|
||
console.log("产物目录 :", path.relative(process.cwd(), ROOT));
|
||
console.log("wxss 文件 :", cssFiles.length, `(合计 ${(cssBytes / 1024).toFixed(1)} KB)`);
|
||
console.log("app.wxss :", `${(fs.statSync(path.join(ROOT, "app.wxss")).size / 1024).toFixed(1)} KB`);
|
||
console.log("wxml 文件 :", wxmlFiles.length);
|
||
console.log("class 静态处数 :", staticCount, " 动态处数 :", dynamicCount);
|
||
console.log("涉及 class :", used.size);
|
||
console.log("");
|
||
|
||
if (missing.length === 0) {
|
||
console.log("✅ 结构校验通过:所有 class 均有对应样式规则,0 错配");
|
||
} else {
|
||
console.log(`❌ 有 ${missing.length} 个 class 找不到样式规则:`);
|
||
for (const [cls, files] of missing.slice(0, 80)) {
|
||
console.log(` .${cls} <- ${files.join(", ")}`);
|
||
}
|
||
process.exitCode = 1;
|
||
}
|