| |
| |
| |
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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`; |
| } |
|
|
| |
| 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, |
| }; |
|
|