Files
WRNexusJS/scripts/generate-ui-component-reference.mjs
T
2026-07-27 12:54:43 +05:30

141 lines
4.6 KiB
JavaScript

import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { format, resolveConfig } from "prettier";
const uiRoot = join(import.meta.dirname, "..", "packages", "ui");
const componentsDir = join(uiRoot, "components");
const catalog = JSON.parse(readFileSync(join(uiRoot, "component-catalog.json"), "utf8"));
const metadata = new Map(catalog.components.map((entry) => [entry.name, entry]));
async function writeFormatted(path, source, parser) {
const config = (await resolveConfig(path)) ?? {};
const formatted = await format(source, {
...config,
filepath: path,
parser,
});
writeFileSync(path, formatted);
}
function block(source, keyword) {
const start = source.indexOf(`${keyword} {`);
if (start < 0) return "";
const open = source.indexOf("{", start);
let depth = 0;
for (let index = open; index < source.length; index++) {
if (source[index] === "{") depth++;
if (source[index] === "}" && --depth === 0) return source.slice(open + 1, index);
}
return "";
}
function propsOf(source) {
const props = [];
for (const line of block(source, "props").split(/\r?\n/)) {
const match = line
.trim()
.match(/^([A-Za-z][A-Za-z0-9_]*)(?:\s*:\s*([^=]+?))?\s*(?:=\s*(.+))?$/);
if (!match) continue;
const [, name, annotation, rawDefault] = match;
const value = rawDefault?.trim();
const type =
annotation?.trim() ||
(value === "true" || value === "false"
? "boolean"
: /^-?\d+(?:\.\d+)?$/.test(value ?? "")
? "number"
: "string");
props.push({ name, type, required: value === undefined, default: value ?? null });
}
return props;
}
function words(name) {
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
}
const components = readdirSync(componentsDir)
.filter((file) => file.endsWith(".wrn"))
.map((file) => {
const source = readFileSync(join(componentsDir, file), "utf8");
const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec(source);
if (!declaration) {
throw new Error(`UI component source does not declare a component: ${file}`);
}
const name = declaration[1];
const entry = metadata.get(name);
const slots = [...source.matchAll(/<slot(?:\s+name="([^"]+)")?/g)].map(
(match) => match[1] ?? "default",
);
const declaredEvents = [
...new Set(
[...source.matchAll(/@event\s+([A-Za-z][A-Za-z0-9_]*)\s*=\s*function/g)].map(
(match) => match[1],
),
),
];
const events =
declaredEvents.length > 0
? declaredEvents
: (entry?.events ?? [
...new Set(
[...source.matchAll(/@([A-Za-z][A-Za-z0-9_-]*)=/g)].map((match) => match[1]),
),
]);
return {
name,
mount: name,
category: entry?.category ?? "core",
purpose: entry?.purpose ?? `Reusable ${words(name)} component.`,
props: propsOf(source),
slots: [...new Set(slots)],
events,
source: `components/${file}`,
};
})
.sort((left, right) => left.name.localeCompare(right.name));
const referencePath = join(uiRoot, "component-reference.json");
await writeFormatted(
referencePath,
JSON.stringify(
{ generatedFrom: "packages/ui/components/*.wrn", count: components.length, components },
null,
2,
) + "\n",
"json",
);
const lines = [
"# WRNexus UI component reference",
"",
`This reference is generated from the ${components.length} packaged \`.wrn\` component sources. Props marked required have no default; all others show their runtime default.`,
"",
];
for (const category of [...new Set(components.map((component) => component.category))].sort()) {
lines.push(`## ${category[0].toUpperCase()}${category.slice(1)}`, "");
for (const component of components.filter((item) => item.category === category)) {
const props = component.props.length
? component.props
.map(
(prop) =>
`\`${prop.name}: ${prop.type}${prop.required ? " (required)" : ` = ${prop.default}`}\``,
)
.join(", ")
: "None";
lines.push(
`### ${component.name}`,
"",
component.purpose,
"",
`- Mount: \`data-component="${component.mount}"\``,
`- Props: ${props}`,
`- Slots: ${component.slots.length ? component.slots.map((slot) => `\`${slot}\``).join(", ") : "None"}`,
`- Events: ${component.events.length ? component.events.map((event) => `\`${event}\``).join(", ") : "None"}`,
"",
);
}
}
const componentsPath = join(uiRoot, "COMPONENTS.md");
await writeFormatted(componentsPath, `${lines.join("\n").trimEnd()}\n`, "markdown");