#!/usr/bin/env node /* C-2 视觉变更验证(旧蓝→品牌橙/信息蓝分流批次专用) * 用法:node scripts/tmp_c2_verify.js <页面路径> <替换规则.json> * 替换规则.json 支持两种规则形态(按序施加): * 1. 全局规则:["#5B9BD5", "#F97316"] 字面替换;["/^color: #5B9BD5/m", "color: #0EA5E9"] 正则(flags 直通,缺省补 g) * 2. 块级规则:{"block": ".channel-tip-text", "pairs": [["color: #5B9BD5", "color: #0EA5E9"]]} * —— 仅在该选择器规则块(至第一个 } 行)内应用 pairs;块内未命中即报错 * 原理:before(HEAD) 与 after(工作区) 分别拼接 uni.scss 编译并归一; * 对 before 产物按序施加预期替换后,必须与 after 产物逐字节相同 * → 证明除预期颜色替换外零其他变化。 */ const { execSync } = require('child_process') const fs = require('fs') const path = require('path') const PAGE = process.argv[2] const SUBST_FILE = process.argv[3] if (!PAGE || !SUBST_FILE) { console.error('用法: node scripts/tmp_c2_verify.js <页面路径> <替换规则.json>') 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(/^[ \t]*]*>[\s\S]*?<\/style>/m) if (!m) throw new Error('未找到 style 块: ' + PAGE) return m[0].replace(/^[ \t]*]*>/, '').replace(/<\/style>$/, '') } 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(/rgba\(([^)]+)\)/g, (m, inner) => 'rgba(' + inner.split(',').map((x) => x.trim()).join(',') + ')') // canonical rgba spacing .replace(/\/\*[\s\S]*?\*\//g, '') // strip block comments .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') } function toRegExp(pat) { const last = pat.lastIndexOf('/') const flags = pat.slice(last + 1) return new RegExp(pat.slice(1, last), flags.includes('g') ? flags : flags + 'g') } function applyBlockRules(text, sel, pairs) { const lines = text.split('\n') let idx = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].trim() === sel + ' {') { idx = i; break } } if (idx === -1) throw new Error('块选择器未找到: ' + sel) let end = -1 for (let j = idx + 1; j < lines.length; j++) { if (lines[j].trim() === '}') { end = j; break } } if (end === -1) throw new Error('块未闭合: ' + sel) const body = lines.slice(idx + 1, end) for (const [pat, rep] of pairs) { const before = body.join('\n') const after = pat.startsWith('/') && pat.lastIndexOf('/') > 0 ? before.replace(toRegExp(pat), rep) : before.split(pat).join(rep) if (after === before) throw new Error('块内替换未生效: ' + sel + ' :: ' + pat) body.length = 0 body.push(...after.split('\n')) } lines.splice(idx + 1, end - idx - 1, ...body) return lines.join('\n') } const substs = JSON.parse(fs.readFileSync(SUBST_FILE, 'utf8')) let expected = norm(fs.readFileSync(path.join(TMP, 'before.out.css'), 'utf8')) for (const rule of substs) { if (Array.isArray(rule)) { const [pat, rep] = rule expected = pat.startsWith('/') && pat.lastIndexOf('/') > 0 ? expected.replace(toRegExp(pat), rep) : expected.split(pat).join(rep) } else if (rule && rule.block && Array.isArray(rule.pairs)) { expected = applyBlockRules(expected, rule.block, rule.pairs) } else { throw new Error('未知规则形态: ' + JSON.stringify(rule)) } } const after = norm(fs.readFileSync(path.join(TMP, 'after.out.css'), 'utf8')) if (expected === after) { console.log(`C-2 验证 PASS(${PAGE}):before 施加预期替换后与 after 逐字节相同,仅含预期颜色变更`) } else { const el = expected.split('\n'), al = after.split('\n') console.log(`C-2 验证 DIFF(${PAGE}):expected ${el.length} 行 / after ${al.length} 行`) let shown = 0 for (let i = 0; i < Math.max(el.length, al.length) && shown < 40; i++) { if (el[i] !== al[i]) { console.log(`L${i + 1}\n expected: ${el[i]}\n after: ${al[i]}`) shown++ } } if (shown >= 40) console.log('...(仅显示前 40 处差异)') process.exit(1) }