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