#!/usr/bin/env node /** * gen-families.mjs - 族层数据生成器(契约层) * * 产物: * 1. .design_library/kole-ui/families.json 族模型快照(build-site.ps1 消费) * 2. .design_library/kole-ui/components/.json 注入 family / familyRole / familyParams * * 注入策略:**定点插入**,不重排既有 JSON —— 只在 "slug" 行之后插入三个字段, * 其余字节保持不变,保证 git diff 最小且不覆盖他人未提交改动。 * * 用法: * node tools/gen-families.mjs # 生成(原子写) * node tools/gen-families.mjs --dry-run # 只报将要发生的改动 * node tools/gen-families.mjs --check # 校验磁盘状态与模型一致,不一致则退出码 1 */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { FAMILIES, IMPLEMENTED_FAMILIES, memberIndex } from './lib/family-model.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const LIB = path.join(ROOT, '.design_library', 'kole-ui'); const COMPONENTS = path.join(LIB, 'components'); const INDEX_FILE = path.join(COMPONENTS, 'index.json'); const FAMILIES_FILE = path.join(LIB, 'families.json'); const MODE = process.argv.includes('--dry-run') ? 'dry' : process.argv.includes('--check') ? 'check' : 'write'; const problems = []; let changed = 0; let unchanged = 0; function writeAtomic(file, content) { const tmp = file + '.tmp'; fs.writeFileSync(tmp, content, 'utf8'); fs.renameSync(tmp, file); } /** 渲染要插入的三行族字段(2 空格缩进,与既有契约格式一致) */ function familyLines(entry) { const params = JSON.stringify(entry.params || {}); return ( ` "family": ${JSON.stringify(entry.family)},\n` + ` "familyRole": ${JSON.stringify(entry.role)},\n` + ` "familyParams": ${params},\n` ); } /** 移除既有族字段(幂等重跑),返回 { text, had } */ function stripFamily(text) { const re = /^ {2}"family(?:Role|Params)?":.*\n?/gm; const had = re.test(text); return { text: text.replace(/^ {2}"family(?:Role|Params)?":.*\n?/gm, ''), had }; } function main() { if (!fs.existsSync(INDEX_FILE)) { console.error('FAIL: 找不到 ' + path.relative(ROOT, INDEX_FILE)); process.exit(2); } const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8')); const bySlug = new Map(index.components.map((c) => [c.slug, c])); /* ---- 校验模型自身 ---- */ const entries = memberIndex(); // 同一 slug 属于多族会在此抛错 const baseSlugs = new Set(FAMILIES.map((f) => f.base)); for (const f of FAMILIES) { if (!bySlug.has(f.base)) problems.push(`族 ${f.id} 的基座 ${f.base} 不在 index.json 中`); if (!f.members.some((m) => m.slug === f.base)) problems.push(`族 ${f.id} 的基座 ${f.base} 未登记为成员`); const bases = f.members.filter((m) => m.role === 'base'); if (bases.length !== 1) problems.push(`族 ${f.id} 应恰好 1 个 base,实际 ${bases.length}`); for (const m of f.members) { if (!bySlug.has(m.slug)) problems.push(`族 ${f.id} 成员 ${m.slug} 不在 index.json 中`); const dims = (f.paramSurface || []).map((p) => p.name); for (const k of Object.keys(m.params || {})) { if (!dims.includes(k)) problems.push(`族 ${f.id} 成员 ${m.slug} 的参数 ${k} 未在 paramSurface 声明`); } } for (const p of f.paramSurface || []) { if (p.type === 'enum') { if (!Array.isArray(p.values) || p.values.length < 2) problems.push(`族 ${f.id} 参数 ${p.name} 为 enum 但取值少于 2`); for (const m of f.members) { const v = (m.params || {})[p.name]; if (v !== undefined && !p.values.map(String).includes(String(v))) { problems.push(`族 ${f.id} 成员 ${m.slug} 的 ${p.name}=${v} 不在 enum 取值内`); } } } } } for (const id of IMPLEMENTED_FAMILIES) { if (!FAMILIES.some((f) => f.id === id)) problems.push(`IMPLEMENTED_FAMILIES 含未定义族 ${id}`); } if (problems.length) { console.error('FAIL: 族模型自检未通过'); for (const p of problems) console.error(' - ' + p); process.exit(2); } /* ---- 产物 1:families.json ---- */ const payload = { schemaVersion: 1, sourceKind: 'family-model', generatedBy: 'tools/gen-families.mjs', note: '由 tools/lib/family-model.mjs 生成,勿手改。族把「同源组件」表达为「1 个基座 + N 个参数取值」;组件身份(slug)保持不变,data.json 仍完整输出 79 个组件。', totalFamilies: FAMILIES.length, totalMembers: Object.keys(entries).length, implementedFamilies: IMPLEMENTED_FAMILIES, families: FAMILIES.map((f) => ({ ...f, basePrefix: bySlug.get(f.base) ? bySlug.get(f.base).frameworksPrefix : null, memberCount: f.members.length, members: f.members.map((m) => ({ ...m, frameworksPrefix: bySlug.get(m.slug) ? bySlug.get(m.slug).frameworksPrefix : null, })), })), }; const json = JSON.stringify(payload, null, 2) + '\n'; const prev = fs.existsSync(FAMILIES_FILE) ? fs.readFileSync(FAMILIES_FILE, 'utf8') : null; if (prev === json) { console.log('= families.json 无变化'); } else if (MODE === 'check') { problems.push('families.json 与模型不一致(需重跑 gen-families.mjs)'); } else if (MODE === 'dry') { console.log('~ 将写入 families.json(' + json.length + ' bytes)'); } else { writeAtomic(FAMILIES_FILE, json); console.log('+ families.json 写入 ' + json.length + ' bytes / ' + FAMILIES.length + ' 族 / ' + payload.totalMembers + ' 成员'); } /* ---- 产物 2:79 份契约注入 ---- */ const touched = []; for (const slug of Object.keys(entries).sort()) { const file = path.join(COMPONENTS, slug + '.json'); if (!fs.existsSync(file)) { problems.push('找不到契约 ' + slug + '.json'); continue; } const raw = fs.readFileSync(file, 'utf8'); const { text: stripped, had } = stripFamily(raw); const anchor = ` "slug": ${JSON.stringify(slug)},\n`; if (!stripped.includes(anchor)) { problems.push(slug + '.json 缺少锚点行 ' + JSON.stringify(anchor.trim()) + ',跳过注入'); continue; } const next = stripped.replace(anchor, anchor + familyLines(entries[slug])); let parsed; try { parsed = JSON.parse(next); } catch (e) { problems.push(slug + '.json 注入后 JSON 解析失败: ' + e.message); continue; } if (parsed.family !== entries[slug].family) { problems.push(slug + '.json 注入结果字段不符'); continue; } if (next === raw && had) { unchanged++; continue; } if (MODE === 'check') { problems.push(slug + '.json 族字段缺失或过期'); continue; } if (MODE === 'dry') { touched.push(slug); continue; } writeAtomic(file, next); touched.push(slug); } if (touched.length) changed = touched.length; console.log( `\n${MODE === 'check' ? '[check]' : MODE === 'dry' ? '[dry-run]' : '[write]'} ` + `族 ${FAMILIES.length} / 登记成员 ${payload.totalMembers} / 契约改动 ${changed} / 已最新 ${unchanged}` ); if (touched.length) console.log(' 改动契约:' + touched.join(' ')); const rest = index.components.filter((c) => !entries[c.slug]); console.log(` 独立组件(不属任何族):${rest.length} 个 — ${rest.map((c) => c.slug).slice(0, 12).join(' ')}${rest.length > 12 ? ' …' : ''}`); if (problems.length) { console.error('\nFAIL:'); for (const p of problems) console.error(' - ' + p); process.exit(1); } console.log('\nOK: 族层数据一致'); } main();