/** * 构建前扫描 `assets/demos/causal_flow/*.json`,写入 `features/causal_flow/genAttributeBundledDemoManifest.generated.ts`,供 bundle 内联 demo 列表。 * 顺序与 UI 名:`order.json` 数组;项为 slug 字符串或 `{ slug, label?, featured? }`(无 label 则 UI 显示 slug;`featured: "bold"` 等样式见 VALID_ORDER_FEATURED)。 * 目录内存在但尚未列入 order 的 demo 会在构建时自动追加到 order.json 末尾(`slug` 与 `label` 均为文件名 stem,便于人工改 label)。 * 若 order.json 不存在则按 UTF-16 码元序生成完整列表。manifest 生成时若仍有遗漏则按 UTF-16 追加。 * order 中的 slug 必须对应目录内已有 demo JSON;重复 slug 亦会在构建时报错。 */ const path = require('path'); const fs = require('fs'); const REL_DIR = 'assets/demos/causal_flow'; const GENERATED_BASENAME = 'genAttributeBundledDemoManifest.generated.ts'; const ORDER_FILENAME = 'order.json'; const VALID_ORDER_FEATURED = new Set(['bold']); function utf16Sort(slugs) { return [...slugs].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); } function discoverSlugs(srcDir) { if (!fs.existsSync(srcDir)) return []; return fs .readdirSync(srcDir) .filter((f) => f.endsWith('.json') && f !== ORDER_FILENAME) .map((f) => f.replace(/\.json$/i, '')) .filter((s) => s.length > 0); } /** @returns {{ slug: string, label: string | null, featured?: string } | null} */ function parseOrderEntry(entry, index) { const at = `${ORDER_FILENAME}[${index}]`; if (typeof entry === 'string') { const slug = entry.trim(); return slug ? { slug, label: null } : null; } if (entry && typeof entry === 'object' && typeof entry.slug === 'string') { const slug = entry.slug.trim(); if (!slug) return null; const label = typeof entry.label === 'string' && entry.label.trim().length > 0 ? entry.label.trim() : null; let featured; if (entry.featured != null) { if (typeof entry.featured !== 'string' || !VALID_ORDER_FEATURED.has(entry.featured)) { throw new Error( `${at}: unknown featured ${JSON.stringify(entry.featured)} (supported: ${[...VALID_ORDER_FEATURED].join(', ')})`, ); } featured = entry.featured; } return featured ? { slug, label, featured } : { slug, label }; } throw new Error( `${at}: expected a slug string or { "slug": "...", "label"?: "...", "featured"?: "bold" }` ); } function readOrderEntries(srcDir) { const orderPath = path.join(srcDir, ORDER_FILENAME); if (!fs.existsSync(orderPath)) return null; let raw; try { raw = JSON.parse(fs.readFileSync(orderPath, 'utf8')); } catch (e) { throw new Error(`${ORDER_FILENAME}: invalid JSON (${e.message})`); } if (!Array.isArray(raw)) { throw new Error(`${ORDER_FILENAME}: expected a JSON array`); } return raw.map((entry, i) => parseOrderEntry(entry, i)).filter(Boolean); } function resolveLabel(slug, label) { return label ?? slug; } /** @param {{ slug: string, label: string | null, featured?: string }[]} entries */ function serializeOrderFile(entries) { const body = entries.map(({ slug, label, featured }) => { const row = { slug, label: resolveLabel(slug, label) }; if (featured) row.featured = featured; return row; }); return `${JSON.stringify(body, null, 2)}\n`; } /** 将新发现的 demo 追加到 order.json 末尾;无 order 文件时生成完整列表。 */ function syncOrderJson(srcDir) { const discovered = discoverSlugs(srcDir); if (discovered.length === 0) return; const orderPath = path.join(srcDir, ORDER_FILENAME); let orderEntries = readOrderEntries(srcDir); const seen = new Set((orderEntries ?? []).map((e) => e.slug)); const missing = utf16Sort(discovered.filter((s) => !seen.has(s))); if (missing.length === 0) return; if (orderEntries == null) { orderEntries = utf16Sort(discovered).map((slug) => ({ slug, label: slug })); } else { for (const slug of missing) { orderEntries.push({ slug, label: slug }); } } const next = serializeOrderFile(orderEntries); if (fs.existsSync(orderPath) && fs.readFileSync(orderPath, 'utf8') === next) return; fs.writeFileSync(orderPath, next, 'utf8'); } function collectDemoEntries(srcDir) { const discovered = new Set(discoverSlugs(srcDir)); const order = readOrderEntries(srcDir); if (order == null) { return utf16Sort([...discovered]).map((slug) => ({ slug, label: slug, })); } const seen = new Set(); const result = []; for (const { slug, label, featured } of order) { if (seen.has(slug)) { throw new Error(`${ORDER_FILENAME}: duplicate slug ${JSON.stringify(slug)}`); } if (!discovered.has(slug)) { throw new Error( `${ORDER_FILENAME}: unknown slug ${JSON.stringify(slug)} (no ${slug}.json under ${REL_DIR})`, ); } seen.add(slug); const row = { slug, label: resolveLabel(slug, label) }; if (featured) row.featured = featured; result.push(row); } for (const slug of utf16Sort([...discovered].filter((s) => !seen.has(s)))) { result.push({ slug, label: slug }); } return result; } function writeGeneratedModule(srcDir, outPath) { syncOrderJson(srcDir); const entries = collectDemoEntries(srcDir); const content = '/**\n' + ' * Generated by GenAttributeDemoManifestPlugin — do not edit.\n' + ' */\n' + 'export type GenAttributeBundledDemoFeaturedStyle = \'bold\';\n' + 'export type GenAttributeBundledDemoManifestEntry = { readonly slug: string; readonly label: string; readonly featured?: GenAttributeBundledDemoFeaturedStyle };\n' + `export const GEN_ATTRIBUTE_BUNDLED_DEMOS: readonly GenAttributeBundledDemoManifestEntry[] = ${JSON.stringify(entries)};\n`; if (fs.existsSync(outPath) && fs.readFileSync(outPath, 'utf8') === content) return; fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, content, 'utf8'); } class GenAttributeDemoManifestPlugin { apply(compiler) { const srcDir = path.join(__dirname, '..', REL_DIR); const outPath = path.join(__dirname, '..', 'features', 'causal_flow', GENERATED_BASENAME); compiler.hooks.beforeCompile.tapAsync('GenAttributeDemoManifestPlugin', (_params, callback) => { try { writeGeneratedModule(srcDir, outPath); callback(); } catch (e) { callback(e); } }); compiler.hooks.thisCompilation.tap('GenAttributeDemoManifestPlugin', (compilation) => { compilation.contextDependencies.add(srcDir); }); } } module.exports = { GenAttributeDemoManifestPlugin, syncOrderJson, collectDemoEntries, discoverSlugs, ORDER_FILENAME, REL_DIR, };