// @sts-nocheck import { existsSync, promises as fs } from 'fs' import { tmpdir } from 'os' import path from 'path' import { cwd } from 'process' import { rimraf } from 'rimraf' import { Project, ScriptKind, SyntaxKind } from 'ts-morph' import { registry } from '../registry/registry' import { Registry, registrySchema } from '../registry/schema' import { styles } from '../registry/styles' const REGISTRY_PATH = path.join(process.cwd(), 'public/registry') // ---------------------------------------------------------------------------- // Build __registry__/index.tsx. // ---------------------------------------------------------------------------- async function buildRegistry(registry: Registry) { const project = new Project({ compilerOptions: {}, }) async function createTempSourceFile(filename: string) { const dir = await fs.mkdtemp(path.join(tmpdir(), 'shadcn-')) return path.join(dir, filename) } 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) { index += ` "${style.name}": {` // Build style index. for (const item of registry) { const resolveFiles = item.files.map((file) => `registry/${style.name}/${file}`) const type = item.type.split(':')[1] let sourceFilename = '' let chunks: any = [] if (item.type === 'components:block') { const file = resolveFiles[0] const filename = path.basename(file) const raw = await fs.readFile(file, 'utf8') const tempFile = await createTempSourceFile(filename) const sourceFile = project.createSourceFile(tempFile, raw, { scriptKind: ScriptKind.TSX, }) // Find all imports. const imports = new Map< string, { module: string text: string isDefault?: boolean } >() sourceFile.getImportDeclarations().forEach((node) => { const module = node.getModuleSpecifier().getLiteralValue() node.getNamedImports().forEach((item) => { imports.set(item.getText(), { module, text: node.getText(), }) }) const defaultImport = node.getDefaultImport() if (defaultImport) { imports.set(defaultImport.getText(), { module, text: defaultImport.getText(), isDefault: true, }) } }) // Find all opening tags with x-chunk attribute. const components = sourceFile .getDescendantsOfKind(SyntaxKind.JsxOpeningElement) .filter((node) => { return node.getAttribute('x-chunk') !== undefined }) chunks = await Promise.all( components.map(async (component, index) => { const chunkName = `${item.name}-chunk-${index}` // Get the value of x-chunk attribute. const attr = component .getAttributeOrThrow('x-chunk') .asKindOrThrow(SyntaxKind.JsxAttribute) const description = attr .getInitializerOrThrow() .asKindOrThrow(SyntaxKind.StringLiteral) .getLiteralValue() // Delete the x-chunk attribute. attr.remove() // Add a new attribute to the component. component.addAttribute({ name: 'x-chunk', initializer: `"${chunkName}"`, }) // Get the value of x-chunk-container attribute. const containerAttr = component .getAttribute('x-chunk-container') ?.asKindOrThrow(SyntaxKind.JsxAttribute) const containerClassName = containerAttr ?.getInitializer() ?.asKindOrThrow(SyntaxKind.StringLiteral) .getLiteralValue() containerAttr?.remove() const parentJsxElement = component.getParentIfKindOrThrow(SyntaxKind.JsxElement) // Find all opening tags on component. const children = parentJsxElement .getDescendantsOfKind(SyntaxKind.JsxOpeningElement) .map((node) => { return node.getTagNameNode().getText() }) .concat( parentJsxElement .getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement) .map((node) => { return node.getTagNameNode().getText() }) ) const componentImports = new Map>() children.forEach((child) => { const importLine = imports.get(child) if (importLine) { const imports = componentImports.get(importLine.module) || [] const newImports = importLine.isDefault ? importLine.text : new Set([...imports, child]) componentImports.set( importLine.module, importLine?.isDefault ? newImports : Array.from(newImports) ) } }) const componnetImportLines = Array.from(componentImports.keys()).map((key) => { const values = componentImports.get(key) const specifier = Array.isArray(values) ? `{${values.join(',')}}` : values return `import ${specifier} from "${key}"` }) const code = ` ${componnetImportLines.join('\n')} export default function Component() { return (${parentJsxElement.getText()}) }` const targetFile = file.replace(item.name, `${chunkName}`) const targetFilePath = path.join( cwd(), `registry/${style.name}/${type}/${chunkName}.tsx` ) // Write component file. rimraf.sync(targetFilePath) await fs.writeFile(targetFilePath, code, 'utf8') return { name: chunkName, description, component: `React.lazy(() => import("@/registry/${style.name}/${type}/${chunkName}")),`, file: targetFile, container: { className: containerClassName, }, } }) ) // // Write the source file for blocks only. sourceFilename = `__registry__/${style.name}/${type}/${item.name}.tsx` const sourcePath = path.join(process.cwd(), sourceFilename) if (!existsSync(sourcePath)) { await fs.mkdir(sourcePath, { recursive: true }) } rimraf.sync(sourcePath) await fs.writeFile(sourcePath, sourceFile.getText()) } // console.log('type', type) // console.log('item', item) let packagePath = '' let componentImportPath = '' if (type === 'ui') { packagePath = `../../packages/ui/src/components/shadcn/ui` componentImportPath = `@/${packagePath}/${item.name}` } if (type === 'fragment') { packagePath = `../../packages/ui-patterns/src${item.optionalPath}` // Check if the file is index.tsx - if so, don't append the item name const isIndexFile = item.files.some((file) => { const basename = path.basename(file) return basename === 'index.tsx' || basename === 'index.ts' }) componentImportPath = isIndexFile ? `@/${packagePath}` : `@/${packagePath}/${item.name}` } if (type === 'example') { packagePath = `registry/${style.name}/${type}` componentImportPath = `@/${packagePath}/${item.name}` } if (type === 'block') { packagePath = `registry/${style.name}/${type}` componentImportPath = `@/${packagePath}/${item.name}` } index += ` "${item.name}": { name: "${item.name}", type: "${item.type}", registryDependencies: ${JSON.stringify(item.registryDependencies)}, component: React.lazy(() => import("${componentImportPath}")), source: "${sourceFilename}", files: [${resolveFiles.map((file) => `"${file}"`)}], category: "${item.category}", subcategory: "${item.subcategory}", chunks: [${chunks.map( (chunk) => `{ name: "${chunk.name}", description: "${chunk.description}", component: ${chunk.component} file: "${chunk.file}", container: { className: "${chunk.container.className}" } }` )}] },` } index += ` },` } index += ` } ` // ---------------------------------------------------------------------------- // Build registry/index.json. // ---------------------------------------------------------------------------- // const names = registry.filter((item) => item.type === 'components:ui') // const registryJson = JSON.stringify(names, null, 2) // rimraf.sync(path.join(REGISTRY_PATH, 'index.json')) // await fs.writeFile(path.join(REGISTRY_PATH, 'index.json'), registryJson, 'utf8') // Write style index. rimraf.sync(path.join(process.cwd(), '__registry__/index.tsx')) await fs.writeFile(path.join(process.cwd(), '__registry__/index.tsx'), index) } // ---------------------------------------------------------------------------- // Build registry/styles/[style]/[name].json. // ---------------------------------------------------------------------------- // async function buildStyles(registry: Registry) { // for (const style of styles) { // const targetPath = path.join(REGISTRY_PATH, 'styles', style.name) // console.log('targetPath', targetPath) // // Create directory if it doesn't exist. // if (!existsSync(targetPath)) { // await fs.mkdir(targetPath, { recursive: true }) // } // for (const item of registry) { // if (item.type !== 'components:ui') { // continue // } // const files = item.files?.map((file) => { // console.log('path', path.join(process.cwd(), 'registry', style.name, file)) // const content = readFileSync(path.join(process.cwd(), 'registry', style.name, file), 'utf8') // return { // name: basename(file), // content, // } // }) // const payload = { // ...item, // files, // } // await fs.writeFile( // path.join(targetPath, `${item.name}.json`), // JSON.stringify(payload, null, 2), // 'utf8' // ) // } // } try { console.log('🚀 Building registry...') const result = registrySchema.safeParse(registry) if (!result.success) { console.error(result.error) process.exit(1) } await buildRegistry(result.data) // await buildStyles(result.data) // await buildThemes() console.log('✅ Done!') } catch (error) { console.error(error) process.exit(1) }