Files
aurora-admin/tools/precompute.mjs
T
aurora-admin c65a69c34e
Deploy to GitHub Pages / deploy (push) Failing after 16s
Regression / regression (push) Failing after 15m2s
feat(P4): precompute移植+app.js sources双向回填+回归1003/1003+验收体积/文件/data.json
2026-09-12 14:18:41 +08:00

337 lines
13 KiB
JavaScript
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.
// P4:`data.js` 瘦身 —— 分步降低运行时对源码全文的依赖
//
// 背景(实测):
// data.json 991KB,其中 components[].sources 占 888KB(89.6%)
// 各端源码量:html 245KB / vue2 184KB / vue3 181KB / jsx 155KB / css 119KB
//
// 哪些 sources 被【渲染期同步】消费(改造难点):
// extractComponentAPI → vue3 / vue2 / jsx(3 端)
// extractScenarios → html(1 端)
// css → 不参与任何解析(可安全分离)
//
// 策略:
// 阶段 A —— 预计算 scenarios,验证「构建期预计算 + 运行时回退」链路可行
// 阶段 B —— 分离 css 到独立文件(零风险,不参与解析)
// 阶段 C —— 预计算 API 表(props/emits/slots),再分离 html/jsx/vue2/vue3
//
// 设计原则:运行时保留回退路径,预计算缺失时功能不受影响。
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
const ROOT = 'C:/Users/12914/Desktop/组件规范第一套';
const DATA_JSON = `${ROOT}/site/data.json`;
const DATA_JS = `${ROOT}/site/data.js`;
const SRC_DIR = `${ROOT}/site/sources`;
if (!existsSync(DATA_JSON)) {
console.error('[FATAL] 找不到 site/data.json,请先运行 build-site.ps1');
process.exit(1);
}
const data = JSON.parse(readFileSync(DATA_JSON, 'utf8'));
/* 与 app.js 的 extractScenarios 完全一致(保证结果可复现) */
function extractScenarios(c) {
const html = (c.sources && c.sources.html) || '';
const bodyM = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
const body = bodyM ? bodyM[1] : html;
const out = [];
const re = /<h2[^>]*>([\s\S]*?)<\/h2>/gi;
let m;
while ((m = re.exec(body)) !== null) {
let text = m[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
text = text.replace(/^\d+[.、]\s*/, '');
if (text) out.push(text);
}
return out;
}
/* ---------- 阶段 A:预计算 scenarios ---------- */
let withScenarios = 0;
let totalScenarios = 0;
for (const c of data.components) {
const s = extractScenarios(c);
c.scenarios = s;
totalScenarios += s.length;
if (s.length >= 2) withScenarios++; // app.js 的 buildScenarioCard 要求 >= 2
}
console.log(`[precompute] 阶段A · scenarios:${totalScenarios} 条 / ${withScenarios} 个组件有 ≥2 条`);
/* ---------- 与 app.js 的 extractComponentAPI 逐行一致 ----------
唯一差别:desc 存中文源串(不调 T),运行时由 app.js 包 T() 做中英映射。
这样中文模式输出与今天逐字节一致,英文模式仍能查字典翻译。
slots 额外存 custom 标记:自定义插槽的 desc 需按 name + 后缀动态拼接,
不能存拼接后的整串(否则英文模式查不到字典)。 */
function scanObjectAt(src, openIdx) {
let depth = 0, inStr = null, escCh = false, end = -1;
for (let i = openIdx; i < src.length; i++) {
const ch = src.charAt(i);
if (inStr) {
if (escCh) { escCh = false; continue; }
if (ch === '\\') { escCh = true; continue; }
if (ch === inStr) inStr = null;
continue;
}
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
if (ch === '{') depth++;
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
}
return end < 0 ? null : src.slice(openIdx, end + 1);
}
function stripOuterBraces(s) { return s.replace(/^\s*\{/, '').replace(/\}\s*$/, ''); }
function splitTopLevel(body) {
const segs = [];
let cur = '', depth = 0, inStr = null, escCh = false;
for (let i = 0; i < body.length; i++) {
const ch = body.charAt(i);
if (inStr) {
cur += ch;
if (escCh) { escCh = false; continue; }
if (ch === '\\') { escCh = true; continue; }
if (ch === inStr) inStr = null;
continue;
}
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; cur += ch; continue; }
if (ch === '{' || ch === '[' || ch === '(') { depth++; cur += ch; continue; }
if (ch === '}' || ch === ']' || ch === ')') { depth--; cur += ch; continue; }
if (ch === ',' && depth === 0) { segs.push(cur); cur = ''; continue; }
cur += ch;
}
if (cur.trim()) segs.push(cur);
return segs;
}
function scanTopLevelValue(str) {
let depth = 0, inStr = null, escCh = false;
for (let i = 0; i < str.length; i++) {
const ch = str.charAt(i);
if (inStr) {
if (escCh) { escCh = false; continue; }
if (ch === '\\') { escCh = true; continue; }
if (ch === inStr) inStr = null;
continue;
}
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
if (ch === '{' || ch === '[' || ch === '(') depth++;
else if (ch === '}' || ch === ']' || ch === ')') { if (depth === 0) return str.slice(0, i); depth--; }
else if (ch === ',' && depth === 0) return str.slice(0, i);
}
return str;
}
function finalizeProp(p) {
let type = 'any';
let def = '—';
let req = false;
const typeM = p.raw.match(/type\s*:\s*(\[[^\]]+\]|[a-zA-Z0-9_$]+)/);
if (typeM) type = typeM[1].replace(/\[|\]/g, '').replace(/,\s*/g, ' | ');
const defM = /default\s*:/.exec(p.raw);
if (defM) {
let val = scanTopLevelValue(p.raw.slice(defM.index + defM[0].length)).trim();
def = val.replace(/\(\)\s*=>\s*/, '') || '—';
if (def === '()') def = '—';
def = def.replace(/\s*\n\s*/g, ' ').replace(/\s{2,}/g, ' ');
if (def.length > 48) def = def.slice(0, 45) + '…';
}
if (/required\s*:\s*true/.test(p.raw)) req = true;
return { name: p.name, type, def, req, desc: p.comment || '—' };
}
function extractComponentAPI(c) {
const v3 = (c.sources && c.sources.vue3) || '';
const v2 = (c.sources && c.sources.vue2) || '';
const jsx = (c.sources && c.sources.jsx) || '';
const code = v3 || v2;
const props = [];
const emits = [];
const slots = [];
let propsBody = null;
const dpM = /defineProps\s*\(/.exec(code);
if (dpM) {
const openIdx = code.indexOf('{', dpM.index);
if (openIdx >= 0) propsBody = stripOuterBraces(scanObjectAt(code, openIdx) || '');
}
if (!propsBody) {
const prM = /props\s*:\s*\{/.exec(code);
if (prM) {
const openIdx2 = prM.index + prM[0].length - 1;
propsBody = stripOuterBraces(scanObjectAt(code, openIdx2) || '');
}
}
if (propsBody && propsBody.trim()) {
splitTopLevel(propsBody).forEach((seg) => {
const m = seg.match(/^\s*([a-zA-Z0-9_$]+)\s*:\s*([\s\S]*)$/);
if (!m) return;
const cM = m[2].match(/\/\/\s*([^\n]+)\s*$/);
props.push(finalizeProp({ name: m[1], comment: cM ? cM[1].trim() : '', raw: m[2] }));
});
} else if (jsx) {
const fnM = jsx.match(/export default function\s+[A-Za-z0-9_$]+\s*\(\s*\{([^}]+)\}/);
if (fnM) {
fnM[1].split(',').forEach((param) => {
const p = param.trim();
if (!p || p.indexOf('...') === 0) return;
const parts = p.split('=');
const name = parts[0].trim();
const def = parts[1] ? parts[1].trim() : '—';
if (name.indexOf('on') === 0 && /[A-Z]/.test(name.charAt(2))) {
emits.push({ name, descZh: '回调触发事件 (Event callback)', params: '(event: any) => void' });
} else if (name === 'children') {
slots.push({ name: 'default', descZh: '子节点内容 (children)', custom: false });
} else {
props.push({ name, type: 'any', def, req: false, desc: '—' });
}
});
}
}
const mEmits = code.match(/defineEmits\s*\(\s*(\[[^\]]+\])\s*\)/);
if (mEmits) {
try {
const emitArr = (new Function('return ' + mEmits[1]))();
emitArr.forEach((e) => {
const descZh = e.indexOf('update:') === 0 ? '双向绑定更新事件 (v-model)' : '组件交互触发事件';
if (!emits.some((x) => x.name === e)) {
emits.push({ name: e, descZh, params: '(val: any) => void' });
}
});
} catch (err) { /* ignore */ }
}
const emitRegex = /\$emit\(\s*['"]([a-zA-Z0-9_:-]+)['"]/g;
let eMatch;
while ((eMatch = emitRegex.exec(code)) !== null) {
const eName = eMatch[1];
if (!emits.some((x) => x.name === eName)) {
emits.push({ name: eName, descZh: '交互回调触发事件', params: '—' });
}
}
const slotRegex = /<slot(?:\s+name=["']([^"']+)["'])?/g;
let sMatch;
while ((sMatch = slotRegex.exec(code)) !== null) {
const sName = sMatch[1] || 'default';
if (!slots.some((x) => x.name === sName)) {
slots.push(sName === 'default'
? { name: sName, descZh: '默认插槽(组件文本或主体内容)', custom: false }
: { name: sName, descZh: '', custom: true });
}
}
return { props, emits, slots };
}
/* ---------- 阶段 C:预计算 API 表 ---------- */
let apiProps = 0, apiEmits = 0, apiSlots = 0, apiComps = 0;
for (const c of data.components) {
const api = extractComponentAPI(c);
c.api = api;
apiProps += api.props.length;
apiEmits += api.emits.length;
apiSlots += api.slots.length;
if (api.props.length || api.emits.length || api.slots.length) apiComps++;
}
console.log(`[precompute] 阶段C · api:${apiComps} 个组件有 API 表(props ${apiProps} / emits ${apiEmits} / slots ${apiSlots})`);
/* ---------- 阶段 B:分离 css 源码到独立文件 ----------
css 不参与任何解析(extractComponentAPI 只读 vue3/vue2/jsx,extractScenarios 只读 html),
因此可安全移出 data.js,改为按需 fetch。
data.json 保持完整(For Agents 承诺不变)。 */
if (!existsSync(SRC_DIR)) mkdirSync(SRC_DIR, { recursive: true });
/* ---------- 阶段 C-2:分离 html/jsx/vue2/vue3 ----------
渲染期同步消费已全部被预计算替代:
- extractComponentAPI → c.api(本文件阶段 C)
- extractScenarios → c.scenarios(阶段 A)
剩余消费全是异步安全(代码展示/srcOf、Playground),由 app.js 按需 fetch。 */
const SPLIT_KINDS = ['html', 'jsx', 'vue2', 'vue3'];
let splitFiles = 0;
for (const c of data.components) {
if (!c.sources) continue;
for (const kind of SPLIT_KINDS) {
if (!c.sources[kind]) continue;
const dir = `${SRC_DIR}/${c.slug}`;
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(`${dir}/${kind}.txt`, c.sources[kind]);
splitFiles++;
}
}
console.log(`[precompute] 阶段C-2 · 源码已分离:${splitFiles} 个文件 → site/sources/<slug>/{html,jsx,vue2,vue3}.txt`);
/* css 分离(阶段 B 保留) */
let cssSplit = 0;
for (const c of data.components) {
if (!c.sources || !c.sources.css) continue;
const dir = `${SRC_DIR}/${c.slug}`;
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(`${dir}/css.txt`, c.sources.css);
cssSplit++;
}
console.log(`[precompute] 阶段B · css 已分离:${cssSplit} 个文件 → site/sources/<slug>/css.txt`);
/* 在 data.js 里把全部 5 端换成引用(data.json 保持完整) */
const dataForJs = JSON.parse(JSON.stringify(data));
for (const c of dataForJs.components) {
if (!c.sources) continue;
for (const kind of ['html', 'css', 'jsx', 'vue2', 'vue3']) {
if (c.sources[kind] != null) {
c.sourcesRef = c.sourcesRef || {};
c.sourcesRef[kind] = `sources/${c.slug}/${kind}.txt`;
delete c.sources[kind];
}
}
}
/* ---------- 阶段 D:分离 contract/specLines/changelog ----------
data.js 体积 ord:contract 38KB + changelog 11.6KB + specLines 11KB ≈ 60KB。
外壳只留 hasContract 布尔;详情写 site/details/<slug>.json,日志写 site/changelog.json。
data.json 保持全量完整(对外承诺不变)。 */
const DETAILS_DIR = `${ROOT}/site/details`;
if (!existsSync(DETAILS_DIR)) mkdirSync(DETAILS_DIR, { recursive: true });
let detailFiles = 0;
for (const c of dataForJs.components) {
const detail = {
slug: c.slug,
contract: c.contract ?? null,
specLines: c.specLines ?? [],
};
writeFileSync(`${DETAILS_DIR}/${c.slug}.json`, JSON.stringify(detail) + '\n');
detailFiles++;
c.hasContract = c.contract != null;
delete c.contract;
delete c.specLines;
}
console.log(`[precompute] 阶段D · details:${detailFiles} 个文件 → site/details/<slug>.json`);
const changelogData = { changelog: dataForJs.changelog || [] };
writeFileSync(`${ROOT}/site/changelog.json`, JSON.stringify(changelogData) + '\n');
delete dataForJs.changelog;
console.log(`[precompute] 阶段D · changelog:${changelogData.changelog.length} 条 → site/changelog.json`);
/* 清理 details 下的孤儿文件(slug 已不存在时删除) */
import { readdirSync, unlinkSync } from 'fs';
const validSlugs = new Set(dataForJs.components.map((c) => c.slug));
let orphans = 0;
for (const f of readdirSync(DETAILS_DIR)) {
if (!f.endsWith('.json')) continue;
if (!validSlugs.has(f.slice(0, -5))) { unlinkSync(`${DETAILS_DIR}/${f}`); orphans++; }
}
if (orphans) console.log(`[precompute] 阶段D · 清理孤儿 details:${orphans} 个`);
/* ---------- 写回 ---------- */
writeFileSync(DATA_JSON, JSON.stringify(data, null, 2) + '\n');
const js = '/* AUTO-GENERATED by build-site.ps1 + tools/precompute.mjs - do not edit by hand */\r\nwindow.AA_DATA = ' + JSON.stringify(dataForJs) + ';\r\n';
writeFileSync(DATA_JS, js);
const beforeKb = 1058;
const afterKb = (Buffer.byteLength(js) / 1024).toFixed(0);
console.log(`[precompute] data.js: ${beforeKb} KB → ${afterKb} KB(−${beforeKb - afterKb} KB)`);