InfoLens / client /src /scripts /genAttributeDemoManifestPlugin.js
dqy08's picture
反向传播动画改进
ca74a9e
Raw
History Blame Contribute Delete
7.23 kB
/**
* 构建前扫描 `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,
};