Files
skills/skills/app-dev/scripts/sync-skin-tokens.sh
T

157 lines
6.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 从平台仓 apps/web/src/skins/index.ts 生成 app 侧皮肤令牌快照(web/src/skin/tokens.ts)。
# 用法: sync-skin-tokens.sh <平台仓根> [--out <输出文件>]
# 退出码: 0 成功 / 1 平台仓或源文件找不到 / 2 用法错
#
# 为什么要有这个脚本:app 跑在 iframe 里,自己的 document 独立,CSS 变量不跨文档继承;
# 令牌只能由 app 自己写一遍。手抄 3 套皮肤 × 40 个令牌必然漂移,所以快照一律由脚本生成。
set -euo pipefail
usage() { echo "用法: sync-skin-tokens.sh <平台仓根> [--out <文件>]" >&2; exit 2; }
PLATFORM="${1:-}"
[ -n "$PLATFORM" ] || usage
shift
OUT="web/src/skin/tokens.ts"
while [ $# -gt 0 ]; do
case "$1" in
--out) [ -n "${2:-}" ] || usage; OUT="$2"; shift 2 ;;
*) usage ;;
esac
done
SKINS="$PLATFORM/apps/web/src/skins/index.ts"
if [ ! -f "$SKINS" ]; then
echo "DIE 找不到 $SKINS" >&2
echo " 原因: 平台仓根参数不对,或平台把皮肤实现挪了位置。" >&2
echo " 怎么办: 传平台仓根目录(含 apps/web/src/skins/index.ts 的那层);" >&2
echo " 若平台确实改了路径,先更新本脚本第 20 行的 SKINS 变量。" >&2
exit 1
fi
command -v node >/dev/null 2>&1 || { echo "DIE 需要 node(app 仓库本身就跑 node,环境里应有)" >&2; exit 1; }
mkdir -p "$(dirname "$OUT")"
node - "$SKINS" "$OUT" "$PLATFORM" <<'EOF'
const fs = require('fs');
const [, , skinsPath, outPath, platformRoot] = process.argv;
const src = fs.readFileSync(skinsPath, 'utf8');
/** 逐个抓 `tokens: { ... }` 块(手写花括号扫描,避免正则被 rgba()/cubic-bezier() 的括号干扰)。
* `export interface Skin` 里的 `tokens: Record<...>` 声明也会命中 "tokens:",
* 所以用「{ 后第一个键必须是 '--sk-」判定真块;伪匹配只前进一个字符,
* 否则会连带吃掉紧跟其后的第一套皮肤。 */
function tokenBlocks(s) {
const blocks = [];
let from = 0;
for (;;) {
const m = s.indexOf('tokens:', from);
if (m === -1) break;
const start = s.indexOf('{', m);
if (start === -1) break;
if (!s.slice(start + 1).trimStart().startsWith("'--sk-")) {
from = m + 1;
continue;
}
let depth = 0;
let i = start;
let inStr = false;
let quote = '';
for (; i < s.length; i++) {
const c = s[i];
if (inStr) {
if (c === quote && s[i - 1] !== '\\') inStr = false;
continue;
}
if (c === "'" || c === '"') { inStr = true; quote = c; continue; }
if (c === '{') depth++;
else if (c === '}') { depth--; if (depth === 0) break; }
}
blocks.push({ headStart: m, bodyStart: start + 1, end: i, body: s.slice(start + 1, i) });
from = i;
}
return blocks;
}
const blocks = tokenBlocks(src);
if (!blocks.length) {
console.error('DIE 没从 skins/index.ts 里解析出任何 tokens 块');
console.error(' 原因: 平台改写了 SKINS 结构(不再是 tokens: { ... } 字面量)。');
console.error(' 怎么办: 人工核对 skins/index.ts,同步改本脚本的解析逻辑,不要手写令牌。');
process.exit(1);
}
const skins = [];
let prevEnd = 0;
for (const b of blocks) {
const header = src.slice(prevEnd, b.headStart);
prevEnd = b.end;
const pick = (k) => {
const m = new RegExp(k + ":\\s*'([^']*)'").exec(header);
return m ? m[1] : '';
};
const tokens = {};
const re = /'(--sk-[a-z0-9-]+)':\s*'([^']*)'/g;
let m;
while ((m = re.exec(b.body))) tokens[m[1]] = m[2];
const key = pick('key');
if (!key || !Object.keys(tokens).length) continue;
skins.push({ key, label: pick('label'), description: pick('description'), tokens });
}
if (!skins.length) {
console.error('DIE 解析出的皮肤对象为空');
process.exit(1);
}
const keyCount = Object.keys(skins[0].tokens).length;
const drift = skins.filter((s) => Object.keys(s.tokens).length !== keyCount);
if (drift.length) {
console.error('WARN 各皮肤令牌键数不一致:' + skins.map((s) => `${s.key}=${Object.keys(s.tokens).length}`).join(' '));
console.error(' 平台 skins/index.ts 自身可能漏了键(令牌键集合应三处同步)。');
}
const today = new Date().toISOString().slice(0, 10);
const lines = [];
lines.push('// 自动生成,请勿手改——改这个文件等于和平台脱钩。');
lines.push('// 来源:平台仓 apps/web/src/skins/index.ts(唯一令牌源)');
lines.push('// 同步:bash scripts/sync-skin-tokens.sh <平台仓根>');
lines.push(`// 快照日期:${today} 皮肤:${skins.map((s) => `${s.key}(${s.label})`).join(' / ')} 令牌数:${keyCount}`);
lines.push('//');
lines.push('// 为什么 app 要自带一份:micro 应用跑在同源 iframe 里,自己的 document 独立,');
lines.push('// 宿主的 --sk-* 不会跨文档继承;皮肤 key 走 localStorage[\'ts-skin\'] 共享,令牌值只能本地落一份。');
lines.push('');
lines.push('export interface Skin {');
lines.push(' key: string;');
lines.push(' label: string;');
lines.push(' description: string;');
lines.push(' tokens: Record<string, string>;');
lines.push('}');
lines.push('');
lines.push('export const SKINS: Skin[] = [');
for (const s of skins) {
lines.push(' {');
lines.push(` key: '${s.key}',`);
lines.push(` label: '${s.label}',`);
lines.push(` description: '${s.description}',`);
lines.push(' tokens: {');
for (const [k, v] of Object.entries(s.tokens)) {
lines.push(` '${k}': '${v}',`);
}
lines.push(' },');
lines.push(' },');
}
lines.push('];');
lines.push('');
lines.push("export const SKIN_STORAGE_KEY = 'ts-skin';");
lines.push(`export const DEFAULT_SKIN_KEY = '${skins[0].key}';`);
lines.push(`export const SKIN_TOKEN_COUNT = ${keyCount};`);
lines.push(`export const SKIN_SNAPSHOT_DATE = '${today}';`);
lines.push('');
fs.writeFileSync(outPath, lines.join('\n'));
console.log(`ok ${outPath}:${skins.length} 套皮肤 / ${keyCount} 个令牌`);
console.log(` 来源 ${platformRoot}/apps/web/src/skins/index.ts`);
EOF