#!/usr/bin/env tsx /** * Generate Catalog MDX Pages + Index * * Walks registry/blocks/ and registry/components/, reads each item's * registry-item.json, and emits: * * docs/catalog/blocks/.mdx — per-block detail page * docs/catalog/components/.mdx — per-component detail page * docs/public/catalog-index.json — flat manifest for the grid page * * Run before building docs (e.g., in a Mintlify pre-build script): * npx tsx scripts/generate-catalog-pages.ts */ import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { join, resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; // Import from source — bun workspace linking doesn't resolve for scripts outside packages/. import { type RegistryItem, isBlockItem, ITEM_TYPE_DIRS, } from "../packages/core/src/registry/types.js"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, ".."); const registryDir = resolve(repoRoot, "registry"); const docsDir = resolve(repoRoot, "docs"); // ── Types ────────────────────────────────────────────────────────────────── type ItemKind = "block" | "component"; interface CatalogEntry { name: string; type: ItemKind; title: string; description: string; tags: string[]; /** Relative href within the docs site. */ href: string; /** Preview poster image path (relative to docs root). */ preview?: string; } // ── Discovery ────────────────────────────────────────────────────────────── function discoverItems(): { kind: ItemKind; manifest: RegistryItem }[] { const items: { kind: ItemKind; manifest: RegistryItem }[] = []; const dirs: { kind: ItemKind; dir: string }[] = [ { kind: "block", dir: join(registryDir, "blocks") }, { kind: "component", dir: join(registryDir, "components") }, ]; for (const { kind, dir } of dirs) { if (!existsSync(dir)) continue; for (const entry of readdirSync(dir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifestPath = join(dir, entry.name, "registry-item.json"); if (!existsSync(manifestPath)) continue; let manifest: RegistryItem; try { manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as RegistryItem; } catch (err) { console.warn(` ⚠ Skipping ${manifestPath}: ${(err as Error).message}`); continue; } items.push({ kind, manifest }); } } return items.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name)); } // ── MDX generation ───────────────────────────────────────────────────────── function typeLabel(kind: ItemKind): string { return kind === "block" ? "Block" : "Component"; } function typeDir(kind: ItemKind): string { return ITEM_TYPE_DIRS[kind === "block" ? "hyperframes:block" : "hyperframes:component"]; } function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string { const tags = manifest.tags ?? []; const tagBadges = tags.map((t) => `\`${t}\``).join(" "); const installCmd = `npx hyperframes add ${manifest.name}`; const lines: string[] = [ "---", `title: "${manifest.title.replace(/"/g, '\\"')}"`, `description: "${manifest.description.replace(/"/g, '\\"')}"`, "---", "", `# ${manifest.title}`, "", manifest.description, "", ]; if (tagBadges) { lines.push(tagBadges, ""); } // Preview video with poster — muted loop, no autoPlay (matches examples page). const previewPath = `/images/catalog/${typeDir(kind)}/${manifest.name}`; lines.push( `