import { exec } from 'child_process'; import { promises as fs } from 'fs'; import path from 'path'; import { rimraf } from 'rimraf'; const REGISTRY_JSON_PATH = path.join( process.cwd(), 'public', 'r', 'registry.json', ); /** * Replace registry paths with component paths. * @param inputStr - The input string to process. * @returns The processed string with registry paths replaced. */ function replaceRegistryPaths(inputStr: string): string { return inputStr.replace(/(['"])([\s\S]*?)\1/g, (match, quote, content) => { if (content.startsWith('@/registry/')) { const rest = content.slice('@/registry/'.length); return `${quote}@/components/animate-ui/${rest}${quote}`; } else if (content.startsWith('@workspace/ui/')) { const rest = content.slice('@workspace/ui/'.length); return `${quote}@/${rest}${quote}`; } return match; }); } /** * Function to build the merged registry.json file. * It searches for all registry-item.json files in the registry directory, * removes the $schema property, and merges them into the base registry.json items array. */ async function buildRegistryFile() { const registryJsonContent = await fs.readFile(REGISTRY_JSON_PATH, 'utf-8'); const registryData = JSON.parse(registryJsonContent); const registryFolderPath = path.join(process.cwd(), 'registry'); const newItems = await getRegistryItemsFromFolder(registryFolderPath); registryData.items = [ { name: 'index', type: 'registry:style', dependencies: [ 'tw-animate-css', 'class-variance-authority', 'lucide-react', ], registryDependencies: ['utils'], cssVars: {}, files: [], }, ...newItems, ]; await fs.writeFile(REGISTRY_JSON_PATH, JSON.stringify(registryData, null, 2)); } /** * Recursively search for registry-item.json files in a given directory. * @param dir - Directory to search in. * @returns An array of registry item objects. */ async function getRegistryItemsFromFolder(dir: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const items: any[] = []; // Read directory entries with file type information const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Define the expected path of registry-item.json in the current directory const registryItemPath = path.join(fullPath, 'registry-item.json'); try { // Check if registry-item.json exists in this directory await fs.access(registryItemPath); // Read and parse the registry item file const content = await fs.readFile(registryItemPath, 'utf-8'); const item = JSON.parse(content); // Remove the $schema property if it exists if (item.$schema) { delete item.$schema; } items.push(item); } catch { // If registry-item.json does not exist in the current directory, // recursively search in the subdirectories const subItems = await getRegistryItemsFromFolder(fullPath); items.push(...subItems); } } } return items; } /** * Function to build the registry index file. * This function reads the registry.json items and builds a dynamic index file. */ async function buildRegistryIndex() { const registryJsonContent = await fs.readFile(REGISTRY_JSON_PATH, 'utf-8'); const registryItems = JSON.parse(registryJsonContent); let index = `/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ // @ts-nocheck // This file is autogenerated by scripts/build-registry.mts // Do not edit this file directly. import * as React from "react" export const index: Record = {`; // Remove duplicates: only keep the last item with a given name const uniqueItemsMap = new Map(); // Use the base items from registry.json merged file for (const item of registryItems.items) { if (uniqueItemsMap.has(item.name)) { console.warn( `Duplicate item name detected: ${item.name}. Overwriting previous entry.`, ); } uniqueItemsMap.set(item.name, item); } // Process only unique items for (const item of uniqueItemsMap.values()) { // Skip items without files if (!item.files) continue; console.log('Processing item:', item.name); // Define the component path from the first file if exists const componentPath = item.files[0]?.path ? `@/${item.files[0].path}` : ''; // Read files and add content preserving newlines const filesWithContent = await Promise.all( item.files.map(async (file: any) => { const filePath = typeof file === 'string' ? file : file.path; const resolvedFilePath = path.resolve(filePath); try { // Read the file content (preserving newlines) const content = await fs.readFile(resolvedFilePath, 'utf-8'); const processedContent = replaceRegistryPaths(content).trim(); // Trim leading/trailing spaces return { path: filePath, type: file.type || 'unknown', target: file.target || '', content: processedContent, // Keep original formatting (newlines will be \n in JSON) }; } catch (error) { console.error(`Error reading file ${filePath}:`, error); return { path: filePath, type: file.type || 'unknown', target: file.target || '', content: '', }; } }), ); index += ` "${item.name}": { name: ${JSON.stringify(item.name)}, description: ${JSON.stringify(item.description ?? '')}, type: "${item.type}", dependencies: ${JSON.stringify(item.dependencies)}, devDependencies: ${JSON.stringify(item.devDependencies)}, registryDependencies: ${JSON.stringify(item.registryDependencies)}, files: ${JSON.stringify(filesWithContent, null, 2)}, keywords: ${JSON.stringify(item.meta?.keywords ?? [])}, component: ${ componentPath ? `(function() { const LazyComp = 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}"; const Comp = mod.default || mod[exportName]; if (mod.animations) { (LazyComp as any).animations = mod.animations; } return { default: Comp }; }); LazyComp.demoProps = ${JSON.stringify(item?.meta?.demoProps ?? {})}; return LazyComp; })()` : 'null' }, command: 'https://animate-ui.com/r/${item.name}', },`; } index += ` }`; // Remove the previous registry index file and write the new one. rimraf.sync(path.join(process.cwd(), '__registry__/index.tsx')); await fs.writeFile(path.join(process.cwd(), '__registry__/index.tsx'), index); } /** * Function to build the registry. * It clears the previous registry directory, builds the registry files, * and replaces specific path strings in the generated files. */ async function buildRegistry() { // 1. Ensure 'public/r' exists await fs.mkdir('public/r', { recursive: true }); // 2. Remove everything except registry.json const entries = await fs.readdir('public/r'); await Promise.all( entries.map(async (entry) => { if (entry === 'registry.json') return; const entryPath = path.join('public/r', entry); await fs.rm(entryPath, { recursive: true, force: true }); }), ); // 2. Build the registry using the shadcn build command await new Promise((resolve, reject) => { const process = exec( `pnpm dlx shadcn build public/r/registry.json --output ./public/r/`, ); process.on('exit', (code) => { if (code === 0) { resolve(undefined); } else { reject(new Error(`Process exited with code ${code}`)); } }); }); // 3. Replace `@/registry/animate-ui/` with `@/components/animate-ui/` in all files const files = await fs.readdir(path.join(process.cwd(), 'public/r')); await Promise.all( files.map(async (file) => { const content = await fs.readFile( path.join(process.cwd(), 'public/r', file), 'utf-8', ); const registryItem = JSON.parse(content); // Replace `@/registry` in file contents registryItem.files = registryItem.files?.map((file: any) => { if ( file.content?.includes('@/registry') || file.content?.includes('@workspace/ui/') ) { file.content = replaceRegistryPaths(file.content); } return file; }); // Write the updated file back to disk await fs.writeFile( path.join(process.cwd(), 'public/r', file), JSON.stringify(registryItem, null, 2), ); }), ); } // Execute the build process in the following order: // 1. Build the merged registry.json file with new items from registry-item.json files. // 2. Build the registry index. // 3. Build the registry. try { console.log('🔨 Building merged registry file...'); await buildRegistryFile(); console.log('🗂️ Building registry/__index__.tsx...'); await buildRegistryIndex(); console.log('🏗️ Building registry...'); await buildRegistry(); } catch (error) { console.error(error); process.exit(1); }