| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- #!/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]*<style[^>]*>[\s\S]*?<\/style>/m)
- if (!m) throw new Error('未找到 style 块: ' + PAGE)
- return m[0].replace(/^[ \t]*<style[^>]*>/, '').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)
- }
|