Files
stanza/scripts/registry-build.ts
T
2026-05-22 18:37:24 -04:00

123 lines
4.5 KiB
TypeScript

#!/usr/bin/env bun
/**
* Static registry build. Scans `registry/modules/*`, imports each module's
* default export, writes:
* - dist/registry/index.json — registry index (slot/module summaries)
* - dist/registry/modules/<slot>-<id>.json — per-module full manifests
* - dist/schema.json — JSON Schema for stanza.json (served at the web root)
*
* The output directory is what gets uploaded to the CDN (Vercel) and what
* the CLI's HTTP loader hits at runtime.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { Logo, Module, RegistryIndex } from "@stanza/registry";
import { CATEGORIES, manifestJsonSchema } from "@stanza/registry";
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = findRepoRoot(here);
const modulesDir = path.join(repoRoot, "registry", "modules");
const outDir = path.join(repoRoot, "dist", "registry");
await main();
async function main() {
fs.mkdirSync(path.join(outDir, "modules"), { recursive: true });
const dirs = fs
.readdirSync(modulesDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
const summaries = [];
for (const dir of dirs) {
const entry = path.join(modulesDir, dir, "module.ts");
const imported: { default: Module } = await import(entry);
const mod = imported.default;
if (!mod) throw new Error(`Module ${dir} has no default export.`);
// Inline each template's file contents. The per-module JSON is the
// payload the CLI fetches from the CDN, so it has to be self-contained —
// no follow-up requests to retrieve template files. Local dev still works
// because the runner falls back to disk when `content` is absent.
const templatesDir = path.join(modulesDir, dir, "templates");
const logo = readLogo(path.join(modulesDir, dir));
const inlined: Module = {
...mod,
...(logo ? { logo } : {}),
adapters: mod.adapters.map((adapter) => ({
...adapter,
templates: adapter.templates?.map((tpl) => ({
...tpl,
content: fs.readFileSync(path.join(templatesDir, tpl.src), "utf8"),
})),
})),
};
// Dirs/files are keyed by `<category>-<id>` (e.g. testing-vitest). The
// CLI's HTTP loader builds the same filename from the category it's asked for.
fs.writeFileSync(
path.join(outDir, "modules", `${mod.category}-${mod.id}.json`),
JSON.stringify(inlined, null, 2),
);
// The index keeps a lightweight summary — no `content`, no per-adapter
// payloads — but it DOES carry top-level metadata like `logo` so the
// wizard / web builder can render module cards without fetching the
// full per-module JSON.
summaries.push({
...inlined,
adapters: inlined.adapters.map((a) => ({ key: a.key, match: a.match })),
});
}
const index: RegistryIndex = {
generatedAt: new Date().toISOString(),
schemaVersion: 1,
categories: [...CATEGORIES],
modules: summaries,
};
fs.writeFileSync(path.join(outDir, "index.json"), JSON.stringify(index, null, 2));
// The stanza.json JSON Schema is served at the web root (not under /registry/),
// so it lands at dist/ rather than dist/registry/.
fs.writeFileSync(
path.join(repoRoot, "dist", "schema.json"),
JSON.stringify(manifestJsonSchema(), null, 2),
);
console.log(`Wrote ${summaries.length} modules to ${outDir} (+ dist/schema.json)`);
}
/**
* Convention: a module ships a logo by dropping either `logo.svg` (a single
* theme-agnostic SVG) or `logo-light.svg` + `logo-dark.svg` (a theme pair)
* in its directory. We read whichever is present and return the inlined
* markup; if neither exists the module just renders without one.
*/
function readLogo(moduleDir: string): Logo | undefined {
const light = path.join(moduleDir, "logo-light.svg");
const dark = path.join(moduleDir, "logo-dark.svg");
if (fs.existsSync(light) && fs.existsSync(dark)) {
return {
light: fs.readFileSync(light, "utf8"),
dark: fs.readFileSync(dark, "utf8"),
};
}
const single = path.join(moduleDir, "logo.svg");
if (fs.existsSync(single)) return fs.readFileSync(single, "utf8");
return undefined;
}
function findRepoRoot(start: string): string {
let dir = start;
for (let i = 0; i < 8; i++) {
if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return dir;
dir = path.dirname(dir);
}
throw new Error("Could not locate repo root from " + start);
}