import { mkdir, writeFile } from "node:fs/promises"; import { basename, extname, join, resolve } from "node:path"; import type { Integration, IntegrationContext } from "../contracts/index.ts"; import { cssToStcss, reactToSpug } from "../core/backport.ts"; export interface ShadcnIntegrationOptions { /** Registry component names, for example `button`, `card`, or `dialog`. */ components: string[]; /** Registry origin or `{name}` URL template. Defaults to ShadCN's New York registry. */ registry?: string; /** Destination relative to the Muddle project root. */ dir?: string; /** Injectable for offline registries and tests. */ fetch?: (input: string) => Promise; } interface RegistryItem { files?: { path: string; content: string; type?: string }[]; registryDependencies?: string[]; } /** * Pulls declaratively selected ShadCN React components and backports them * into SPUG and STCSS during application setup. */ export function shadcnIntegration(options: ShadcnIntegrationOptions): Integration { return { name: "shadcn", async setup(context: IntegrationContext) { const fetcher = options.fetch ?? globalThis.fetch; const destination = resolve(context.config.root, options.dir ?? "modules/app/views/components"); const styles = resolve(context.config.root, "styles/shadcn"); const seen = new Set(); const pull = async (name: string): Promise => { if (seen.has(name)) return; seen.add(name); const base = options.registry ?? "https://ui.shadcn.com/r/styles/new-york/{name}.json"; const url = base.includes("{name}") ? base.replace("{name}", encodeURIComponent(name)) : `${base.replace(/\/$/, "")}/${encodeURIComponent(name)}.json`; const response = await fetcher(url); if (!response.ok) throw new Error(`shadcn registry request failed for ${name} with ${response.status}`); const item = await response.json() as RegistryItem; for (const dependency of item.registryDependencies ?? []) await pull(dependency.replace(/^@[^/]+\//, "")); for (const file of item.files ?? []) { const extension = extname(file.path).toLowerCase(); const stem = basename(file.path).replace(/\.(tsx|jsx|css)$/i, ""); if (extension === ".tsx" || extension === ".jsx") { await mkdir(destination, { recursive: true }); await writeFile(join(destination, `${stem}.spug`), reactToSpug(file.content, file.path)); } else if (extension === ".css") { await mkdir(styles, { recursive: true }); await writeFile(join(styles, `${stem}.stcss`), cssToStcss(file.content, file.path)); } } }; for (const component of options.components) await pull(component); }, }; }