equiv-regression.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  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. // 行首锚定(m 标志):避开脚本区 JS 字符串里的伪 style 标签
  25. // (如 .replace(/<style[\s\S]*?<\/style>/gi, ' '),其 <style 不在行首)
  26. const m = src.match(/^[ \t]*<style[^>]*>[\s\S]*?<\/style>/m)
  27. if (!m) throw new Error('未找到 style 块: ' + PAGE)
  28. return m[0].replace(/^[ \t]*<style[^>]*>/, '').replace(/<\/style>$/, '')
  29. }
  30. const uniScss = fs.readFileSync(path.join(ROOT, 'uni.scss'), 'utf8')
  31. const afterVue = fs.readFileSync(path.join(ROOT, PAGE), 'utf8')
  32. const beforeVue = execSync(`git -C "${REPO}" show HEAD:${REL}`, { encoding: 'utf8', maxBuffer: 1e8 })
  33. fs.writeFileSync(path.join(TMP, 'after.scss'), uniScss + '\n' + extractStyle(afterVue))
  34. fs.writeFileSync(path.join(TMP, 'before.scss'), uniScss + '\n' + extractStyle(beforeVue))
  35. const sass = path.join(ROOT, 'node_modules/.bin/sass.cmd')
  36. execSync(`"${sass}" --no-source-map "${TMP}/after.scss" "${TMP}/after.out.css"`, { stdio: 'pipe' })
  37. execSync(`"${sass}" --no-source-map "${TMP}/before.scss" "${TMP}/before.out.css"`, { stdio: 'pipe' })
  38. function norm(s) {
  39. return s
  40. .replace(/#([0-9a-fA-F]{3,8})\b/g, (m) => {
  41. let h = m.slice(1).toUpperCase()
  42. if (h.length === 3) h = h.split('').map((c) => c + c).join('')
  43. return '#' + h
  44. })
  45. .split('\n').map((l) => l.trim()).filter(Boolean).join('\n')
  46. }
  47. const before = norm(fs.readFileSync(path.join(TMP, 'before.out.css'), 'utf8'))
  48. const after = norm(fs.readFileSync(path.join(TMP, 'after.out.css'), 'utf8'))
  49. if (before === after) {
  50. console.log(`等价回归 PASS(${PAGE}):编译产物归一后逐字节相同,零观感变化`)
  51. } else {
  52. const bl = before.split('\n'), al = after.split('\n')
  53. console.log(`等价回归 DIFF(${PAGE}):before ${bl.length} 行 / after ${al.length} 行`)
  54. let shown = 0
  55. for (let i = 0; i < Math.max(bl.length, al.length) && shown < 40; i++) {
  56. if (bl[i] !== al[i]) {
  57. console.log(`L${i + 1}\n before: ${bl[i]}\n after: ${al[i]}`)
  58. shown++
  59. }
  60. }
  61. if (shown >= 40) console.log('...(仅显示前 40 处差异)')
  62. process.exit(1)
  63. }