import { spawn } from "child_process" import { promises as fs } from "fs" import path from "path" import { rimraf } from "rimraf" import { registrySchema } from "shadcn/schema" import { createStyleMap, transformDirection, 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"] // Collect paths for batch prettier formatting at the end. const prettierPaths: string[] = [] 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 { const totalStart = performance.now() 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...") // Build all styles in parallel. await Promise.all( stylesToBuild.map(async (style) => { 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() // Build RTL variants of examples. console.log("\n๐Ÿ”„ Building RTL examples...") await buildRtlExamples() console.log("\n๐Ÿ“‹ Building public/r/registries.json...") await buildRegistriesJson() // Batch format all collected files with prettier at the end. if (prettierPaths.length > 0) { console.log(`\nโœจ Formatting ${prettierPaths.length} files...`) await batchPrettier(prettierPaths) } // Clean up intermediate files and generated base directories. console.log("\n๐Ÿงน Cleaning up...") await cleanUp(stylesToBuild) const elapsed = ((performance.now() - totalStart) / 1000).toFixed(2) console.log(`\nโœ… Build complete in ${elapsed}s!`) } catch (error) { console.error(error) process.exit(1) } async function buildBasesIndex(bases: Base[]) { // Import all registries in parallel. const registryImports = await Promise.all( bases.map(async (base) => { const { registry: importedRegistry } = await import( `../registry/bases/${base.name}/registry.ts` ) return { base, importedRegistry } }) ) 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, importedRegistry } of registryImports) { // 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. await rimraf(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[]) { // Pre-import all base registries and style CSS files in parallel. const [baseImports, styleMaps] = await Promise.all([ Promise.all( bases.map(async (base) => { const { registry: baseRegistry } = await import( `../registry/bases/${base.name}/registry.ts` ) 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.filter( (item) => item.type !== "registry:internal" ) return { base, baseRegistry, registryItems } }) ), Promise.all( STYLES.map(async (style) => { const styleContent = await fs.readFile( path.join(process.cwd(), `registry/styles/style-${style.name}.css`), "utf8" ) return { style, styleMap: createStyleMap(styleContent) } }) ), ]) // Build all base-style combinations in parallel. const combinations: Array<{ base: Base style: (typeof STYLES)[number] baseRegistry: (typeof baseImports)[number]["baseRegistry"] registryItems: (typeof baseImports)[number]["registryItems"] styleMap: Record }> = [] for (const { base, baseRegistry, registryItems } of baseImports) { for (const { style, styleMap } of styleMaps) { combinations.push({ base, style, baseRegistry, registryItems, styleMap }) } } await Promise.all( combinations.map( async ({ base, style, baseRegistry, registryItems, styleMap }) => { console.log(` โœ… ${base.name}-${style.name}...`) const styleOutputDir = path.join( process.cwd(), `registry/${base.name}-${style.name}` ) await rimraf(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) // Process all files in parallel. await Promise.all( registryItems.flatMap((registryItem) => { if (!registryItem.files || registryItem.files.length === 0) { return [] } return registryItem.files.map(async (file: { path: string }) => { const source = await fs.readFile( path.join( process.cwd(), `registry/bases/${base.name}/${file.path}` ), "utf8" ) const fileExtension = path.extname(file.path) const shouldTransform = fileExtension === ".tsx" || fileExtension === ".ts" let transformedContent = source if (shouldTransform) { transformedContent = await transformStyle(source, { styleMap: styleMap, }) 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}` ) await fs.mkdir(path.dirname(outputPath), { recursive: true }) await fs.writeFile(outputPath, transformedContent) }) }) ) } ) ) } async function buildRegistryIndex(styles: { name: string; title: string }[]) { // Import all registries in parallel. const registryImports = await Promise.all( styles.map(async (style) => { const { registry: importedRegistry } = await import( `../registry/${style.name}/registry.ts` ) return { style, importedRegistry } }) ) 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, importedRegistry } of registryImports) { 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) { 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 += ` }` await rimraf(path.join(process.cwd(), "registry/__index__.tsx")) await fs.writeFile(path.join(process.cwd(), "registry/__index__.tsx"), index) } async function buildRegistryJsonFile(styleName: string) { const { registry: importedRegistry } = await import( `../registry/${styleName}/registry.ts` ) 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 const fixedRegistry = { ...registry, items: registry.items.map((item) => { const files = item.files?.map((file) => ({ ...file, path: `registry/${styleName}/${file.path}`, })) return { ...item, files } }), } const outputDir = path.join(process.cwd(), `public/r/styles/${styleName}`) await fs.mkdir(outputDir, { recursive: true }) const registryJsonPath = path.join(outputDir, "registry.json") await fs.writeFile(registryJsonPath, JSON.stringify(fixedRegistry, null, 2)) prettierPaths.push(registryJsonPath) const tempRegistryPath = path.join( process.cwd(), `registry-${styleName}.json` ) await fs.writeFile(tempRegistryPath, JSON.stringify(fixedRegistry, null, 2)) } async function buildRegistry(styleName: string) { const outputPath = `public/r/styles/${styleName}` await new Promise((resolve, reject) => { const proc = spawn( "node", [ "../../packages/shadcn/dist/index.js", "build", `registry-${styleName}.json`, "--output", outputPath, ], { cwd: process.cwd(), stdio: "pipe" } ) let stderr = "" proc.stderr?.on("data", (data) => (stderr += data)) proc.on("close", (code) => { if (code !== 0) { reject(new Error(`Process exited with code ${code}: ${stderr}`)) } else { resolve() } }) proc.on("error", reject) }) } async function buildBlocksIndex() { const blocks = await getAllBlocks(["registry:block"]) const payload = blocks.map((block) => ({ name: block.name, description: block.description, categories: block.categories, })) await rimraf(path.join(process.cwd(), "registry/__blocks__.json")) const blocksJsonPath = path.join(process.cwd(), "registry/__blocks__.json") await fs.writeFile(blocksJsonPath, JSON.stringify(payload, null, 2)) prettierPaths.push(blocksJsonPath) } async function cleanUp(stylesToBuild: { name: string; title: string }[]) { // Clean up all in parallel. const cleanupPromises: Promise[] = [] for (const style of stylesToBuild) { cleanupPromises.push( rimraf(path.join(process.cwd(), `registry-${style.name}.json`)) ) } 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}`) cleanupPromises.push(rimraf(baseDir)) } } } await Promise.all(cleanupPromises) } 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)) prettierPaths.push(outputPath) } async function copyUIToExamples() { const defaultStyle = "nova" const directories = ["ui", "lib", "hooks"] // Build all copy tasks. const copyTasks: Array<{ base: Base; dir: string }> = [] for (const base of BASES) { for (const dir of directories) { copyTasks.push({ base, dir }) } } await Promise.all( copyTasks.map(async ({ base, dir }) => { const sourceStyle = `${base.name}-${defaultStyle}` 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`) return } await rimraf(toDir) await fs.mkdir(toDir, { recursive: true }) const files = await fs.readdir(fromDir) await Promise.all( files.map(async (file) => { 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}` ) }) ) } async function buildRegistriesJson() { 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 }> const registries = directory.map((entry) => ({ name: entry.name, homepage: entry.homepage, url: entry.url, description: entry.description, })) const outputPath = path.join(process.cwd(), "public/r/registries.json") await fs.writeFile(outputPath, JSON.stringify(registries, null, 2)) prettierPaths.push(outputPath) } async function buildRtlExamples() { // Process all bases in parallel. await Promise.all( Array.from(BASES).map(async (base) => { const sourceDir = path.join(process.cwd(), `examples/${base.name}/ui`) const targetDir = path.join(process.cwd(), `examples/${base.name}/ui-rtl`) try { await fs.access(sourceDir) } catch { console.log(` โš ๏ธ examples/${base.name}/ui not found, skipping...`) return } await rimraf(targetDir) await fs.mkdir(targetDir, { recursive: true }) const files = await fs.readdir(sourceDir) await Promise.all( files .filter((file) => file.endsWith(".tsx") || file.endsWith(".ts")) .map(async (file) => { const sourcePath = path.join(sourceDir, file) const targetPath = path.join(targetDir, file) let content = await fs.readFile(sourcePath, "utf-8") content = await transformDirection(content, true) content = content.replace( new RegExp(`@/examples/${base.name}/ui/`, "g"), `@/examples/${base.name}/ui-rtl/` ) await fs.writeFile(targetPath, content) }) ) console.log(` โœ… examples/${base.name}/ui-rtl`) }) ) } async function batchPrettier(paths: string[]) { if (paths.length === 0) return await new Promise((resolve, reject) => { const proc = spawn("npx", ["prettier", "--write", ...paths], { cwd: process.cwd(), stdio: "pipe", }) proc.on("close", () => resolve()) proc.on("error", reject) }) }