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() // Build index for legacy styles and whitelisted base-style combinations. console.log(`๐Ÿ“ฆ Building registry/__index__.tsx...`) const stylesForIndex = WHITELISTED_STYLES.map((name) => ({ name, title: name, })) await buildRegistryIndex(stylesForIndex) 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() // Copy UI to examples before cleanup. console.log("\n๐Ÿ“‹ Copying UI to examples...") await copyUIToExamples() console.log("\n๐Ÿ“‹ Building public/r/registries.json...") await buildRegistriesJson() // 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) { // Skip demos - they're handled by the examples index. if (item.type === "registry:internal") { continue } 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}`) } // Filter out demos - they're handled by the examples index. const registryItems = result.data.items.filter( (item) => item.type !== "registry:internal" ) 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" let transformedContent = source if (shouldTransform) { // Transform style classes (cn-* -> Tailwind). transformedContent = await transformStyle(source, { styleMap: styleMap, }) // Transform import paths from base to style-specific paths. // e.g., @/registry/bases/radix/ui/button -> @/registry/radix-nova/ui/button transformedContent = transformedContent.replace( new RegExp( `@/registry/bases/${base.name}/`, "g" ), `@/registry/${base.name}-${style.name}/` ) } 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) { // Skip demos - they're handled by the examples index. if (item.type === "registry:internal") { continue } 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() } }) }) } async function copyUIToExamples() { const defaultStyle = "nova" const directories = ["ui", "lib", "hooks"] for (const base of BASES) { const sourceStyle = `${base.name}-${defaultStyle}` for (const dir of directories) { const fromDir = path.join(process.cwd(), `registry/${sourceStyle}/${dir}`) const toDir = path.join(process.cwd(), `examples/${base.name}/${dir}`) try { await fs.access(fromDir) } catch { console.log(` โš ๏ธ registry/${sourceStyle}/${dir} not found, skipping`) continue } rimraf.sync(toDir) await fs.mkdir(toDir, { recursive: true }) const files = await fs.readdir(fromDir) for (const file of files) { const sourcePath = path.join(fromDir, file) const targetPath = path.join(toDir, file) let content = await fs.readFile(sourcePath, "utf-8") content = content.replace( new RegExp(`@/registry/${sourceStyle}/`, "g"), `@/examples/${base.name}/` ) await fs.writeFile(targetPath, content) } console.log( ` โœ… registry/${sourceStyle}/${dir} โ†’ examples/${base.name}/${dir}` ) } } } // Build public/r/registries.json from registry/directory.json. // This generates a slim version without logos for CLI consumption. async function buildRegistriesJson() { // Read the source directory.json. const directoryPath = path.join(process.cwd(), "registry/directory.json") const directoryContent = await fs.readFile(directoryPath, "utf8") const directory = JSON.parse(directoryContent) as Array<{ name: string homepage?: string url: string description?: string featured?: boolean logo?: string }> // Transform to slim format (without logos and featured). const registries = directory.map((entry) => ({ name: entry.name, homepage: entry.homepage, url: entry.url, description: entry.description, })) // Write to public/r/registries.json. const outputPath = path.join(process.cwd(), "public/r/registries.json") await fs.writeFile(outputPath, JSON.stringify(registries, null, 2)) // Format with prettier. await new Promise((resolve, reject) => { execFile("prettier", ["--write", outputPath], (error) => { if (error) { reject(error) } else { resolve() } }) }) }