equiv-regression.js 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. #!/usr/bin/env node
  2. /* 等价回归验证(UniForge verify 工具)
  3. * 用法:node scripts/equiv-regression.js <页面路径(相对 cfc-frontend)>
  4. * 例:node scripts/equiv-regression.js pages/index-home/index.vue
  5. *
  6. * 原理:uni-app 将 uni.scss prepend 到每个页面 style。
  7. * 取 HEAD 版与工作区版分别拼接 uni.scss 编译,hex 大小写/3-6 位归一后逐字节比对。
  8. * 全部令牌同值映射时,产物应完全相同 → 证明零观感变化。
  9. */
  10. const { execSync } = require('child_process')
  11. const fs = require('fs')
  12. const path = require('path')
  13. const PAGE = process.argv[2]
  14. if (!PAGE) {
  15. console.error('用法: node scripts/equiv-regression.js <页面路径>')
  16. process.exit(2)
  17. }
  18. const ROOT = path.resolve(__dirname, '..')
  19. const REPO = path.resolve(ROOT, '..')
  20. const REL = path.relative(REPO, path.join(ROOT, PAGE)).split(path.sep).join('/')
  21. const TMP = path.join(ROOT, '.tmp_equiv')
  22. fs.mkdirSync(TMP, { recursive: true })
  23. function extractStyle(src) {
  24. const m = src.match(/<style[^>]*>([\s\S]*?)<\/style>/)
  25. if (!m) throw new Error('未找到 style 块: ' + PAGE)
  26. return m[1]
  27. }
  28. const uniScss = fs.readFileSync(path.join(ROOT, 'uni.scss'), 'utf8')
  29. const afterVue = fs.readFileSync(path.join(ROOT, PAGE), 'utf8')
  30. const beforeVue = execSync(`git -C "${REPO}" show HEAD:${REL}`, { encoding: 'utf8', maxBuffer: 1e8 })
  31. fs.writeFileSync(path.join(TMP, 'after.scss'), uniScss + '\n' + extractStyle(afterVue))
  32. fs.writeFileSync(path.join(TMP, 'before.scss'), uniScss + '\n' + extractStyle(beforeVue))
  33. const sass = path.join(ROOT, 'node_modules/.bin/sass.cmd')
  34. execSync(`"${sass}" --no-source-map "${TMP}/after.scss" "${TMP}/after.out.css"`, { stdio: 'pipe' })
  35. execSync(`"${sass}" --no-source-map "${TMP}/before.scss" "${TMP}/before.out.css"`, { stdio: 'pipe' })
  36. function norm(s) {
  37. return s
  38. .replace(/#([0-9a-fA-F]{3,8})\b/g, (m) => {
  39. let h = m.slice(1).toUpperCase()
  40. if (h.length === 3) h = h.split('').map((c) => c + c).join('')
  41. return '#' + h
  42. })
  43. .split('\n').map((l) => l.trim()).filter(Boolean).join('\n')
  44. }
  45. const before = norm(fs.readFileSync(path.join(TMP, 'before.out.css'), 'utf8'))
  46. const after = norm(fs.readFileSync(path.join(TMP, 'after.out.css'), 'utf8'))
  47. if (before === after) {
  48. console.log(`等价回归 PASS(${PAGE}):编译产物归一后逐字节相同,零观感变化`)
  49. } else {
  50. const bl = before.split('\n'), al = after.split('\n')
  51. console.log(`等价回归 DIFF(${PAGE}):before ${bl.length} 行 / after ${al.length} 行`)
  52. let shown = 0
  53. for (let i = 0; i < Math.max(bl.length, al.length) && shown < 40; i++) {
  54. if (bl[i] !== al[i]) {
  55. console.log(`L${i + 1}\n before: ${bl[i]}\n after: ${al[i]}`)
  56. shown++
  57. }
  58. }
  59. if (shown >= 40) console.log('...(仅显示前 40 处差异)')
  60. process.exit(1)
  61. }