|
@@ -0,0 +1,84 @@
|
|
|
|
|
+#!/usr/bin/env node
|
|
|
|
|
+/**
|
|
|
|
|
+ * 品牌色/令牌引用完整性审计
|
|
|
|
|
+ *
|
|
|
|
|
+ * 检查美化治理过程中容易出现的四类回归(均为真实踩过的坑):
|
|
|
|
|
+ * 1) 'BrandColors.xxx' 字符串字面量 —— 字符串不是变量,作为颜色值无效
|
|
|
|
|
+ * 2) BrandColors.xxx 使用但未 import —— 运行时 ReferenceError / undefined
|
|
|
|
|
+ * 3) import 落在 </script> 之后 —— 模块作用域外,运行时标识符未定义
|
|
|
|
|
+ * 4) 从 utils/brand-colors.js 具名导入不存在的导出 —— 值为 undefined
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用法: node scripts/audit-brandcolor-integrity.js
|
|
|
|
|
+ * 退出码: 0 通过 / 1 存在问题
|
|
|
|
|
+ */
|
|
|
|
|
+const fs = require('fs')
|
|
|
|
|
+const path = require('path')
|
|
|
|
|
+
|
|
|
|
|
+const ROOT = path.resolve(__dirname, '..')
|
|
|
|
|
+const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'scripts', 'docs', '.tmp_equiv'])
|
|
|
|
|
+const SKIP_FILES = new Set(['components/mp-html/node/node.vue']) // mp-html 源码保持原样
|
|
|
|
|
+
|
|
|
|
|
+function walk(dir, out = []) {
|
|
|
|
|
+ for (const name of fs.readdirSync(dir)) {
|
|
|
|
|
+ if (EXCLUDE_DIRS.has(name)) continue
|
|
|
|
|
+ const p = path.join(dir, name)
|
|
|
|
|
+ const st = fs.statSync(p)
|
|
|
|
|
+ if (st.isDirectory()) walk(p, out)
|
|
|
|
|
+ else if (name.endsWith('.vue')) out.push(p)
|
|
|
|
|
+ }
|
|
|
|
|
+ return out
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const bcSrc = fs.readFileSync(path.join(ROOT, 'utils/brand-colors.js'), 'utf8')
|
|
|
|
|
+const exportsSet = new Set()
|
|
|
|
|
+for (const m of bcSrc.matchAll(/export\s+const\s+(\w+)/g)) exportsSet.add(m[1])
|
|
|
|
|
+for (const m of bcSrc.matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*'/gm)) exportsSet.add(m[1])
|
|
|
|
|
+exportsSet.add('default')
|
|
|
|
|
+
|
|
|
|
|
+const issues = { stringLiteral: [], missingImport: [], strayImport: [], badNamedImport: [] }
|
|
|
|
|
+
|
|
|
|
|
+for (const abs of walk(ROOT)) {
|
|
|
|
|
+ const rel = path.relative(ROOT, abs).split(path.sep).join('/')
|
|
|
|
|
+ if (SKIP_FILES.has(rel)) continue
|
|
|
|
|
+ const src = fs.readFileSync(abs, 'utf8')
|
|
|
|
|
+ const scriptIdx = src.search(/<script/)
|
|
|
|
|
+ const closeIdx = src.indexOf('</script>')
|
|
|
|
|
+ const styleIdx = src.search(/<style/)
|
|
|
|
|
+ const head = scriptIdx === -1 ? src : src.slice(0, scriptIdx)
|
|
|
|
|
+ const scriptBody = scriptIdx === -1 || closeIdx === -1 ? '' : src.slice(scriptIdx, closeIdx)
|
|
|
|
|
+
|
|
|
|
|
+ if (/['"]BrandColors\.\w+['"]/.test(src)) issues.stringLiteral.push(rel)
|
|
|
|
|
+
|
|
|
|
|
+ const usesBrandColors = /\bBrandColors\./.test(scriptBody) || /\bBrandColors\./.test(head)
|
|
|
|
|
+ const importedBrandColors = /import\s*\{[^}]*\bBrandColors\b[^}]*\}\s*from\s*['"][^'"]*brand-colors/.test(src)
|
|
|
|
|
+ if (usesBrandColors && !importedBrandColors) issues.missingImport.push(rel)
|
|
|
|
|
+
|
|
|
|
|
+ if (closeIdx !== -1 && styleIdx > closeIdx) {
|
|
|
|
|
+ const between = src.slice(closeIdx + 9, styleIdx)
|
|
|
|
|
+ if (/^\s*import\s/m.test(between)) issues.strayImport.push(rel)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const m of src.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"][^'"]*brand-colors(?:\.js)?['"]/g)) {
|
|
|
|
|
+ for (const raw of m[1].split(',')) {
|
|
|
|
|
+ const name = raw.trim()
|
|
|
|
|
+ if (!name) continue
|
|
|
|
|
+ if (!exportsSet.has(name)) issues.badNamedImport.push(rel + ' -> ' + name)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const labels = {
|
|
|
|
|
+ stringLiteral: "'BrandColors.xxx' 字符串字面量(颜色值无效)",
|
|
|
|
|
+ missingImport: 'BrandColors 已使用但未 import',
|
|
|
|
|
+ strayImport: 'import 落在 </script> 之后',
|
|
|
|
|
+ badNamedImport: 'brand-colors 具名导入不存在',
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let failed = 0
|
|
|
|
|
+for (const [key, list] of Object.entries(issues)) {
|
|
|
|
|
+ if (list.length) failed += list.length
|
|
|
|
|
+ console.log(labels[key] + ': ' + list.length)
|
|
|
|
|
+ list.slice(0, 20).forEach(x => console.log(' - ' + x))
|
|
|
|
|
+}
|
|
|
|
|
+console.log(failed === 0 ? 'PASS' : 'FAIL(' + failed + ')')
|
|
|
|
|
+process.exit(failed === 0 ? 0 : 1)
|