import { exec, execFile } from "child_process" import { promises as fs } from "fs" import path from "path" import { rimraf } from "rimraf" import { registrySchema } from "shadcn/schema" import { createStyleMap, transformStyle } from "shadcn/utils" import { getAllBlocks } from "@/lib/blocks" import { legacyStyles } from "@/registry/_legacy-styles" import { BASES, type Base } from "@/registry/bases" import { PRESETS } from "@/registry/config" import { STYLES } from "@/registry/styles" // This is a list of styles that we want to check into tracking. // This is used by the v4 site. const WHITELISTED_STYLES = ["new-york-v4"] function getStylesToBuild() { const stylesToBuild: { name: string; title: string }[] = [...legacyStyles] for (const base of BASES) { for (const style of STYLES) { stylesToBuild.push({ name: `${base.name}-${style.name}`, title: `${base.title} ${style.title}`, }) } } return stylesToBuild } try { console.log("๐Ÿ—๏ธ Building bases...") await buildBasesIndex(Array.from(BASES)) await buildBases(Array.from(BASES)) const stylesToBuild = getStylesToBuild() // We only need the legacy styles here for backward compatibility. console.log(`๐Ÿ“ฆ Building registry/__index__.tsx...`) await buildRegistryIndex([...legacyStyles]) console.log("๐Ÿ’… Building styles...") for (const style of stylesToBuild) { await buildRegistryJsonFile(style.name) await buildRegistry(style.name) console.log(` โœ… ${style.name}`) } console.log("\n๐Ÿ—‚๏ธ Building registry/__blocks__.json...") await buildBlocksIndex() console.log("\nโš™๏ธ Building public/r/config.json...") await buildConfig() // Clean up intermediate files and generated base directories. console.log("\n๐Ÿงน Cleaning up...") await cleanUp(stylesToBuild) console.log("\nโœ… Build complete!") } catch (error) { console.error(error) process.exit(1) } async function buildBasesIndex(bases: Base[]) { let index = `// @ts-nocheck // This file is autogenerated by scripts/build-registry.ts // Do not edit this file directly. import "server-only" import * as React from "react" export const Index: Record> = {` for (const base of bases) { // Dynamically import the registry for this style. const { registry: importedRegistry } = await import( `../registry/bases/${base.name}/registry.ts` ) // Validate the registry schema. const parseResult = registrySchema.safeParse(importedRegistry) if (!parseResult.success) { console.error(`โŒ Registry validation failed for ${base.name}:`) console.error(parseResult.error.format()) throw new Error(`Invalid registry schema for ${base.name}`) } const registry = parseResult.data index += ` "${base.name}": {` for (const item of registry.items) { const files = item.files?.map((file) => ({ path: typeof file === "string" ? file : file.path, type: typeof file === "string" ? item.type : file.type, target: typeof file === "string" ? undefined : file.target, })) ?? [] if (files.length === 0) { continue } const componentPath = item.files?.[0]?.path ? `@/registry/bases/${base.name}/${item.files[0].path}` : "" index += ` "${item.name}": { name: "${item.name}", title: "${item.title}", description: "${item.description ?? ""}", type: "${item.type}", registryDependencies: ${JSON.stringify(item.registryDependencies)}, files: [${files.map((file) => { const filePath = `registry/bases/${base.name}/${file.path}` return `{ path: "${filePath}", type: "${file.type}", target: "${file.target ?? ""}" }` })}], component: ${ componentPath ? `React.lazy(async () => { const mod = await import("${componentPath}") const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name return { default: mod.default || mod[exportName] } })` : "null" }, categories: ${JSON.stringify(item.categories)}, meta: ${JSON.stringify(item.meta)}, },` } index += ` },` } index += ` }` // Write unified index. rimraf.sync(path.join(process.cwd(), "registry/bases/__index__.tsx")) await fs.writeFile( path.join(process.cwd(), "registry/bases/__index__.tsx"), index ) } async function buildBases(bases: Base[]) { for (const base of bases) { const { registry: baseRegistry } = await import( `../registry/bases/${base.name}/registry.ts` ) // Validate the registry schema. const result = registrySchema.safeParse(baseRegistry) if (!result.success) { console.error(`โŒ Registry validation failed for ${base.name}:`) console.error(result.error.format()) throw new Error(`Invalid registry schema for ${base.name}`) } const registryItems = result.data.items for (const style of STYLES) { console.log(` โœ… ${base.name}-${style.name}...`) // Create the base-style output directory if it doesn't exist. const styleOutputDir = path.join( process.cwd(), `registry/${base.name}-${style.name}` ) rimraf.sync(styleOutputDir) await fs.mkdir(styleOutputDir, { recursive: true, }) // Create a registry.ts file in the output directory. const styleRegistry = { ...baseRegistry, items: registryItems, } const registryTs = `export const registry = ${JSON.stringify(styleRegistry, null, 2)}\n` await fs.writeFile(path.join(styleOutputDir, "registry.ts"), registryTs) const styleContent = await fs.readFile( path.join(process.cwd(), `registry/styles/style-${style.name}.css`), "utf8" ) const styleMap = createStyleMap(styleContent) for (const registryItem of registryItems) { // Process all files in the registry item. if (!registryItem.files || registryItem.files.length === 0) { continue } for (const file of registryItem.files) { const source = await fs.readFile( path.join( process.cwd(), `registry/bases/${base.name}/${file.path}` ), "utf8" ) // Transform the file if it's a TSX/TS file that needs style transformation. const fileExtension = path.extname(file.path) const shouldTransform = fileExtension === ".tsx" || fileExtension === ".ts" const transformedContent = shouldTransform ? await transformStyle(source, { styleMap: styleMap, }) : source const outputPath = path.join( process.cwd(), `registry/${base.name}-${style.name}/${file.path}` ) const outputDir = path.dirname(outputPath) await fs.mkdir(outputDir, { recursive: true }) await fs.writeFile(outputPath, transformedContent) } } } } } async function buildRegistryIndex(styles: { name: string; title: string }[]) { let index = `// @ts-nocheck // This file is autogenerated by scripts/build-registry.ts // Do not edit this file directly. import * as React from "react" export const Index: Record> = {` for (const style of styles) { // Dynamically import the registry for this style. const { registry: importedRegistry } = await import( `../registry/${style.name}/registry.ts` ) // Validate the registry schema. const parseResult = registrySchema.safeParse(importedRegistry) if (!parseResult.success) { console.error(`โŒ Registry validation failed for ${style.name}:`) console.error(parseResult.error.format()) throw new Error(`Invalid registry schema for ${style.name}`) } const registry = parseResult.data index += ` "${style.name}": {` for (const item of registry.items) { const files = item.files?.map((file) => ({ path: typeof file === "string" ? file : file.path, type: typeof file === "string" ? item.type : file.type, target: typeof file === "string" ? undefined : file.target, })) ?? [] if (files.length === 0) { continue } const componentPath = item.files?.[0]?.path ? `@/registry/${style.name}/${item.files[0].path}` : "" index += ` "${item.name}": { name: "${item.name}", title: "${item.title}", description: "${item.description ?? ""}", type: "${item.type}", registryDependencies: ${JSON.stringify(item.registryDependencies)}, files: [${files.map((file) => { const filePath = `registry/${style.name}/${file.path}` return `{ path: "${filePath}", type: "${file.type}", target: "${file.target ?? ""}" }` })}], component: ${ componentPath ? `React.lazy(async () => { const mod = await import("${componentPath}") const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name return { default: mod.default || mod[exportName] } })` : "null" }, categories: ${JSON.stringify(item.categories)}, meta: ${JSON.stringify(item.meta)}, },` } index += ` },` } index += ` }` // Write unified index. rimraf.sync(path.join(process.cwd(), "registry/__index__.tsx")) await fs.writeFile(path.join(process.cwd(), "registry/__index__.tsx"), index) } async function buildRegistryJsonFile(styleName: string) { // 1. Import the registry for this style. const { registry: importedRegistry } = await import( `../registry/${styleName}/registry.ts` ) // 2. Validate the registry schema. const parseResult = registrySchema.safeParse(importedRegistry) if (!parseResult.success) { console.error(`โŒ Registry validation failed for ${styleName}:`) console.error(parseResult.error.format()) throw new Error(`Invalid registry schema for ${styleName}`) } const registry = parseResult.data // 3. Fix the path for registry items. const fixedRegistry = { ...registry, items: registry.items.map((item) => { const files = item.files?.map((file) => { return { ...file, path: `registry/${styleName}/${file.path}`, } }) return { ...item, files, } }), // We're only going to build registry:ui for the new bases for now. // .filter((item) => ["registry:ui"].includes(item.type)), } // 3. Create the output directory and write registry.json. const outputDir = path.join(process.cwd(), `public/r/styles/${styleName}`) await fs.mkdir(outputDir, { recursive: true }) // 4. Write registry.json to output directory and format it. const registryJsonPath = path.join(outputDir, "registry.json") await fs.writeFile(registryJsonPath, JSON.stringify(fixedRegistry, null, 2)) await new Promise((resolve, reject) => { execFile("prettier", ["--write", registryJsonPath], (error) => { if (error) { reject(error) } else { resolve() } }) }) // 5. Write temporary registry file needed by shadcn build. const tempRegistryPath = path.join( process.cwd(), `registry-${styleName}.json` ) await fs.writeFile(tempRegistryPath, JSON.stringify(fixedRegistry, null, 2)) } async function buildRegistry(styleName: string) { return new Promise((resolve, reject) => { const outputPath = `public/r/styles/${styleName}` const process = exec( `node ../../packages/shadcn/dist/index.js build registry-${styleName}.json --output ${outputPath}` ) process.on("exit", (code) => { if (code === 0) { resolve(undefined) } else { reject(new Error(`Process exited with code ${code}`)) } }) }) } async function buildBlocksIndex() { const blocks = await getAllBlocks(["registry:block"]) const payload = blocks.map((block) => ({ name: block.name, description: block.description, categories: block.categories, })) rimraf.sync(path.join(process.cwd(), "registry/__blocks__.json")) await fs.writeFile( path.join(process.cwd(), "registry/__blocks__.json"), JSON.stringify(payload, null, 2) ) await exec(`prettier --write registry/__blocks__.json`) } async function cleanUp(stylesToBuild: { name: string; title: string }[]) { // Clean up intermediate registry JSON files. for (const style of stylesToBuild) { rimraf.sync(path.join(process.cwd(), `registry-${style.name}.json`)) } // Clean up generated base directories except whitelisted ones. for (const base of BASES) { for (const style of STYLES) { const baseName = `${base.name}-${style.name}` if (!WHITELISTED_STYLES.includes(baseName)) { const baseDir = path.join(process.cwd(), `registry/${baseName}`) console.log(` ๐Ÿ—‘๏ธ ${baseName}`) rimraf.sync(baseDir) } } } } async function buildConfig() { const config = { presets: PRESETS, } const outputPath = path.join(process.cwd(), "public/r/config.json") await fs.writeFile(outputPath, JSON.stringify(config, null, 2)) // Format with prettier. await new Promise((resolve, reject) => { execFile("prettier", ["--write", outputPath], (error) => { if (error) { reject(error) } else { resolve() } }) }) }