#!/usr/bin/env node
/**
* 批量替换硬编码色值为 SCSS 令牌(CSS style 块内,零视觉风险)
* 用法:
* node scripts/replace-hex-to-token.js --batch gray # 仅灰阶
* node scripts/replace-hex-to-token.js --batch brand # 仅品牌/状态色
* node scripts/replace-hex-to-token.js # 全部
*/
const fs = require('fs')
const path = require('path')
const ROOT = path.resolve(__dirname, '..')
const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'docs'])
const ALL_RULES = [
// ---- 灰阶(neutral)----
['#FFFFFF', '$app-surface'],
['#ffffff', '$app-surface'],
['#FFF', '$app-surface'],
['#fff', '$app-surface'],
['#333333', '$facade-text-strong'],
['#333', '$facade-text-strong'],
['#666666', '$facade-text-body'],
['#666', '$facade-text-body'],
['#999999', '$facade-text-muted'],
['#999', '$facade-text-muted'],
['#F5F5F5', '$facade-fill-2'],
['#f5f5f5', '$facade-fill-2'],
['#F0F0F0', '$facade-fill'],
['#f0f0f0', '$facade-fill'],
['#FAFAFA', '$facade-fill-ghost'],
['#fafafa', '$facade-fill-ghost'],
['#F9FAFB', '$facade-fill-gray-50'],
['#f9fafb', '$facade-fill-gray-50'],
['#BBBBBB', '$facade-text-ghost-light'],
['#bbbbbb', '$facade-text-ghost-light'],
['#BBB', '$facade-text-ghost-light'],
['#bbb', '$facade-text-ghost-light'],
['#CCCCCC', '$facade-text-ghost'],
['#cccccc', '$facade-text-ghost'],
['#CCC', '$facade-text-ghost'],
['#ccc', '$facade-text-ghost'],
['#DDDDDD', '$facade-border-soft'],
['#dddddd', '$facade-border-soft'],
['#DDD', '$facade-border-soft'],
['#ddd', '$facade-border-soft'],
['#EEEEEE', '$facade-hairline-2'],
['#eeeeee', '$facade-hairline-2'],
['#EEE', '$facade-hairline-2'],
['#eee', '$facade-hairline-2'],
// ---- 品牌/状态色(brand/status)----
['#F97316', '$brand-primary'],
['#f97316', '$brand-primary'],
['#FF8C42', '$dim-body'],
['#ff8c42', '$dim-body'],
['#FFF7ED', '$uni-primary-pale'],
['#fff7ed', '$uni-primary-pale'],
['#FB923C', '$brand-primary-light'],
['#fb923c', '$brand-primary-light'],
['#10B981', '$status-success'],
['#10b981', '$status-success'],
['#EF4444', '$status-danger'],
['#ef4444', '$status-danger'],
['#6366F1', '$dim-wisdom'],
['#6366f1', '$dim-wisdom'],
['#C62828', '$status-danger-strong-2'],
['#c62828', '$status-danger-strong-2'],
].sort((a, b) => b[0].length - a[0].length)
const GRAY_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i < 30 })
const BRAND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 })
const batch = process.argv.find(a => a.startsWith('--batch='))?.split('=')[1]
const rules = batch === 'brand' ? BRAND_RULES : batch === 'gray' ? GRAY_RULES : ALL_RULES
const label = batch === 'brand' ? '品牌/状态色' : batch === 'gray' ? '灰阶' : '全部'
console.log(`\n批次: ${label} 规则数: ${rules.length}\n`)
function walk(dir, out = []) {
for (const name of fs.readdirSync(dir)) {
if (EXCLUDE_DIRS.has(name)) continue
const p = path.join(dir, name)
const st = fs.statSync(p)
if (st.isDirectory()) walk(p, out)
else if (name.endsWith('.vue')) out.push(p)
}
return out
}
function countReplacements(text, rules) {
let n = 0
for (const [hex] of rules) {
const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
n += (text.match(new RegExp(escaped + '(?![0-9a-fA-F])', 'g')) || []).length
}
return n
}
function applyRules(text, rules) {
let result = text
for (const [hex, token] of rules) {
const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
result = result.replace(new RegExp(escaped + '(?![0-9a-fA-F])', 'g'), token)
}
return result
}
function processFile(filePath) {
const src = fs.readFileSync(filePath, 'utf8')
// 按 "
const replCount = countReplacements(body, rules)
if (replCount === 0) continue
const newBody = applyRules(body, rules)
totalRepl += replCount
// 若