audit-brandcolor-integrity.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. #!/usr/bin/env node
  2. /**
  3. * 品牌色/令牌引用完整性审计
  4. *
  5. * 检查美化治理过程中容易出现的四类回归(均为真实踩过的坑):
  6. * 1) 'BrandColors.xxx' 字符串字面量 —— 字符串不是变量,作为颜色值无效
  7. * 2) BrandColors.xxx 使用但未 import —— 运行时 ReferenceError / undefined
  8. * 3) import 落在 </script> 之后 —— 模块作用域外,运行时标识符未定义
  9. * 4) 从 utils/brand-colors.js 具名导入不存在的导出 —— 值为 undefined
  10. *
  11. * 用法: node scripts/audit-brandcolor-integrity.js
  12. * 退出码: 0 通过 / 1 存在问题
  13. */
  14. const fs = require('fs')
  15. const path = require('path')
  16. const ROOT = path.resolve(__dirname, '..')
  17. const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'scripts', 'docs', '.tmp_equiv'])
  18. const SKIP_FILES = new Set(['components/mp-html/node/node.vue']) // mp-html 源码保持原样
  19. function walk(dir, out = []) {
  20. for (const name of fs.readdirSync(dir)) {
  21. if (EXCLUDE_DIRS.has(name)) continue
  22. const p = path.join(dir, name)
  23. const st = fs.statSync(p)
  24. if (st.isDirectory()) walk(p, out)
  25. else if (name.endsWith('.vue')) out.push(p)
  26. }
  27. return out
  28. }
  29. const bcSrc = fs.readFileSync(path.join(ROOT, 'utils/brand-colors.js'), 'utf8')
  30. const exportsSet = new Set()
  31. for (const m of bcSrc.matchAll(/export\s+const\s+(\w+)/g)) exportsSet.add(m[1])
  32. for (const m of bcSrc.matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*'/gm)) exportsSet.add(m[1])
  33. exportsSet.add('default')
  34. const issues = { stringLiteral: [], missingImport: [], strayImport: [], badNamedImport: [] }
  35. for (const abs of walk(ROOT)) {
  36. const rel = path.relative(ROOT, abs).split(path.sep).join('/')
  37. if (SKIP_FILES.has(rel)) continue
  38. const src = fs.readFileSync(abs, 'utf8')
  39. const scriptIdx = src.search(/<script/)
  40. const closeIdx = src.indexOf('</script>')
  41. const styleIdx = src.search(/<style/)
  42. const head = scriptIdx === -1 ? src : src.slice(0, scriptIdx)
  43. const scriptBody = scriptIdx === -1 || closeIdx === -1 ? '' : src.slice(scriptIdx, closeIdx)
  44. if (/['"]BrandColors\.\w+['"]/.test(src)) issues.stringLiteral.push(rel)
  45. const usesBrandColors = /\bBrandColors\./.test(scriptBody) || /\bBrandColors\./.test(head)
  46. const importedBrandColors = /import\s*\{[^}]*\bBrandColors\b[^}]*\}\s*from\s*['"][^'"]*brand-colors/.test(src)
  47. if (usesBrandColors && !importedBrandColors) issues.missingImport.push(rel)
  48. if (closeIdx !== -1 && styleIdx > closeIdx) {
  49. const between = src.slice(closeIdx + 9, styleIdx)
  50. if (/^\s*import\s/m.test(between)) issues.strayImport.push(rel)
  51. }
  52. for (const m of src.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"][^'"]*brand-colors(?:\.js)?['"]/g)) {
  53. for (const raw of m[1].split(',')) {
  54. const name = raw.trim()
  55. if (!name) continue
  56. if (!exportsSet.has(name)) issues.badNamedImport.push(rel + ' -> ' + name)
  57. }
  58. }
  59. }
  60. const labels = {
  61. stringLiteral: "'BrandColors.xxx' 字符串字面量(颜色值无效)",
  62. missingImport: 'BrandColors 已使用但未 import',
  63. strayImport: 'import 落在 </script> 之后',
  64. badNamedImport: 'brand-colors 具名导入不存在',
  65. }
  66. let failed = 0
  67. for (const [key, list] of Object.entries(issues)) {
  68. if (list.length) failed += list.length
  69. console.log(labels[key] + ': ' + list.length)
  70. list.slice(0, 20).forEach(x => console.log(' - ' + x))
  71. }
  72. console.log(failed === 0 ? 'PASS' : 'FAIL(' + failed + ')')
  73. process.exit(failed === 0 ? 0 : 1)