diff --git a/index.ts b/index.ts index 37ef6f5..f93f5b4 100644 --- a/index.ts +++ b/index.ts @@ -1 +1 @@ -export { shadcnIntegration, type ShadcnIntegrationOptions } from "../../integrations/shadcn.ts"; +export { shadcnIntegration, type ShadcnIntegrationOptions } from "./src/integration.ts"; diff --git a/src/integration.ts b/src/integration.ts new file mode 100644 index 0000000..9d51f81 --- /dev/null +++ b/src/integration.ts @@ -0,0 +1,50 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { basename, extname, join, resolve } from "node:path"; +import { cssToStcss, reactToSpug, type Integration, type IntegrationContext } from "muddle"; + +export interface ShadcnIntegrationOptions { + components: string[]; + registry?: string; + dir?: string; + fetch?: (input: string) => Promise; +} + +interface RegistryItem { + files?: { path: string; content: string; type?: string }[]; + registryDependencies?: string[]; +} + +/** Pulls ShadCN registry entries and writes their Muddle ports into the host project. */ +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); + }, + }; +}