Files
2026-07-13 12:49:11 +08:00

485 lines
14 KiB
TypeScript

import { exec } from "child_process"
import { existsSync, promises as fs } from "fs"
import path from "path"
import { rimraf } from "rimraf"
import { getAllBlocks } from "@/lib/blocks"
import { registry } from "@/registry/index"
async function buildRegistryIndex() {
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.ts
// Do not edit this file directly.
import * as React from "react"
export const Index: Record<string, any> = {`
for (const item of registry.items) {
const resolveFiles = item.files?.map(
(file) => `registry/creative-tim/${file.path}`
)
if (!resolveFiles) {
continue
}
const componentPath = item.files?.[0]?.path
? `@/registry/creative-tim/${item.files[0].path}`
: ""
index += `
"${item.name}": {
name: "${item.name}",
description: "${item.description ?? ""}",
type: "${item.type}",
registryDependencies: ${JSON.stringify(item.registryDependencies)},
files: [${item.files?.map((file) => {
const filePath = `registry/creative-tim/${typeof file === "string" ? file : file.path}`
const resolvedFilePath = path.resolve(filePath)
return typeof file === "string"
? `"${resolvedFilePath}"`
: `{
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 += `
}`
console.log(`#️⃣ ${Object.keys(registry.items).length} items found`)
// Write style index.
rimraf.sync(path.join(process.cwd(), "registry/__index__.tsx"))
await fs.writeFile(path.join(process.cwd(), "registry/__index__.tsx"), index)
}
async function buildRegistryJsonFile() {
// 1. Add registry/creative-tim prefix for the build to work
const fixedRegistry = {
...registry,
items: registry.items.map((item) => {
const files = item.files?.map((file) => {
return {
...file,
path: `registry/creative-tim/${file.path}`,
}
})
return {
...item,
files,
}
}),
}
// 2. Write the content of the registry to `registry.json`
rimraf.sync(path.join(process.cwd(), `registry.json`))
await fs.writeFile(
path.join(process.cwd(), `registry.json`),
JSON.stringify(fixedRegistry, null, 2)
)
// 3. Format the registry.json file.
await exec(`prettier --write registry.json`)
// 3. Copy the registry.json to the www/public/r/styles/creative-tim directory.
await fs.cp(
path.join(process.cwd(), "registry.json"),
path.join(
process.cwd(),
"../www/public/r/styles/creative-tim/registry.json"
),
{ recursive: true }
)
}
async function buildRegistry() {
return new Promise((resolve, reject) => {
exec(
`pnpm dlx shadcn build registry.json --output public/r`,
async (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`)
reject(error)
return
}
if (stderr) {
console.error(`stderr: ${stderr}`)
}
if (stdout) {
console.log(stdout)
}
// Post-process the generated JSON files to remove registry/creative-tim prefix
console.log("📝 Cleaning up paths in generated JSON files...")
await cleanupGeneratedPaths()
resolve(undefined)
}
)
})
}
async function cleanupGeneratedPaths() {
const publicDir = path.join(process.cwd(), "public/r")
// Get all JSON files in the public/r directory
const files = await fs.readdir(publicDir)
const jsonFiles = files.filter(file => file.endsWith('.json'))
for (const file of jsonFiles) {
const filePath = path.join(publicDir, file)
const content = await fs.readFile(filePath, 'utf-8')
const json = JSON.parse(content)
// Clean up the paths in the files array
if (json.files) {
// Build a mapping of source paths to target paths for this block
const pathMappings = new Map<string, string>()
json.files.forEach((file: any) => {
if (typeof file === 'object' && file.path && file.target) {
// Map the source registry path to the target path
const sourcePath = file.path.replace(/^registry\/creative-tim\//, '')
pathMappings.set(sourcePath, file.target)
}
})
json.files = json.files.map((file: any) => {
if (typeof file === 'object' && file.path) {
// Remove only registry/ prefix, keep creative-tim/ for branding
let cleanPath = file.path.replace(/^registry\//, '')
// For UI components, prepend with components/
if (cleanPath.startsWith('creative-tim/ui/')) {
cleanPath = 'components/' + cleanPath.replace(/^creative-tim\//, '')
}
// For examples, prepend with components/
else if (cleanPath.startsWith('creative-tim/examples/')) {
cleanPath = 'components/' + cleanPath.replace(/^creative-tim\//, '')
}
file.path = cleanPath
// Clean up imports in the content field using the path mappings
if (file.content) {
file.content = rewriteImports(file.content, pathMappings)
}
}
return file
})
}
// Write the cleaned JSON back
await fs.writeFile(filePath, JSON.stringify(json, null, 2))
}
console.log(`✨ Cleaned paths in ${jsonFiles.length} files`)
}
function rewriteImports(content: string, pathMappings: Map<string, string>): string {
// Sort mappings by path length (longest first) to handle more specific paths first
const sortedMappings = Array.from(pathMappings.entries()).sort(
([a], [b]) => b.length - a.length
)
// Handle block-specific imports using the exact path mappings
sortedMappings.forEach(([sourcePath, targetPath]) => {
if (sourcePath.startsWith('blocks/')) {
// Remove the file extension from sourcePath to match import statements
const sourcePathNoExt = sourcePath.replace(/\.(tsx?|jsx?)$/, '')
// Extract the directory from the target path (removing filename)
const targetPathNoExt = targetPath.replace(/\.(tsx?|jsx?)$/, '')
// Create regex to match this specific import path
const escapedPath = sourcePathNoExt.replace(/\//g, '\\/')
const importRegex = new RegExp(
`@\\/registry\\/creative-tim\\/${escapedPath}`,
'g'
)
content = content.replace(importRegex, `@/${targetPathNoExt}`)
}
})
// Rewrite imports from @/registry/creative-tim/ui/... to @/components/ui/...
content = content.replace(
/@\/registry\/creative-tim\/ui\//g,
'@/components/ui/'
)
// Rewrite imports from @/registry/creative-tim/examples/... to @/components/examples/...
content = content.replace(
/@\/registry\/creative-tim\/examples\//g,
'@/components/examples/'
)
// Rewrite imports from @/registry/creative-tim/lib/... to @/lib/...
content = content.replace(
/@\/registry\/creative-tim\/lib\//g,
'@/lib/'
)
return content
}
async function buildPackageJson(
packageType: "ui" | "blocks" | "agents",
fileName: string,
description: string
) {
console.log(`🎨 Building ${fileName} for ${packageType}...`)
// Filter components by type
const typeMap = {
ui: "registry:ui",
blocks: "registry:block",
agents: "registry:agent"
}
const components = registry.items.filter(item => item.type === typeMap[packageType])
// Aggregate all dependencies and registryDependencies
const allDependencies = new Set<string>()
const allRegistryDependencies = new Set<string>()
const allFiles: any[] = []
// Build path mappings for import rewriting
const pathMappings = new Map<string, string>()
for (const component of components) {
// Collect dependencies
if (component.dependencies) {
component.dependencies.forEach(dep => allDependencies.add(dep))
}
// Collect registryDependencies
if (component.registryDependencies) {
component.registryDependencies.forEach(dep => allRegistryDependencies.add(dep))
}
// Collect files and read their content
if (component.files) {
for (const file of component.files) {
const filePath = `registry/creative-tim/${file.path}`
const absolutePath = path.join(process.cwd(), filePath)
// Read file content
let content = ""
if (existsSync(absolutePath)) {
content = await fs.readFile(absolutePath, 'utf-8')
}
// Clean path - remove registry/creative-tim/ prefix for UI components
let cleanPath = file.path
if (packageType === "ui") {
if (cleanPath.startsWith('ui/')) {
cleanPath = 'components/' + cleanPath
} else if (cleanPath.startsWith('examples/')) {
cleanPath = 'components/' + cleanPath
}
}
// Build path mapping
if (file.target) {
pathMappings.set(file.path, file.target)
}
allFiles.push({
path: cleanPath,
type: file.type,
target: file.target ?? "",
content: content
})
}
}
}
// Rewrite imports in all file contents
allFiles.forEach(file => {
if (file.content) {
file.content = rewriteImports(file.content, pathMappings)
}
})
// Create the JSON structure
const packageJson = {
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
name: fileName.replace('.json', ''),
type: typeMap[packageType],
description: description,
dependencies: Array.from(allDependencies).sort(),
registryDependencies: Array.from(allRegistryDependencies).sort(),
files: allFiles
}
// Write the JSON file
const outputPath = path.join(process.cwd(), "public", "r", fileName)
await fs.writeFile(outputPath, JSON.stringify(packageJson, null, 2))
console.log(`✨ Generated ${fileName} with ${components.length} ${packageType} components`)
}
async function buildAllJson() {
console.log("📦 Building all.json...")
// Combine all components
const allDependencies = new Set<string>()
const allRegistryDependencies = new Set<string>()
const allFiles: any[] = []
const pathMappings = new Map<string, string>()
for (const component of registry.items) {
if (component.dependencies) {
component.dependencies.forEach(dep => allDependencies.add(dep))
}
if (component.registryDependencies) {
component.registryDependencies.forEach(dep => allRegistryDependencies.add(dep))
}
if (component.files) {
for (const file of component.files) {
const filePath = `registry/creative-tim/${file.path}`
const absolutePath = path.join(process.cwd(), filePath)
let content = ""
if (existsSync(absolutePath)) {
content = await fs.readFile(absolutePath, 'utf-8')
}
let cleanPath = file.path
if (component.type === "registry:ui") {
if (cleanPath.startsWith('ui/')) {
cleanPath = 'components/' + cleanPath
} else if (cleanPath.startsWith('examples/')) {
cleanPath = 'components/' + cleanPath
}
}
if (file.target) {
pathMappings.set(file.path, file.target)
}
allFiles.push({
path: cleanPath,
type: file.type,
target: file.target ?? "",
content: content
})
}
}
}
allFiles.forEach(file => {
if (file.content) {
file.content = rewriteImports(file.content, pathMappings)
}
})
const allJson = {
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
name: "all",
type: "registry:all",
description: "All components from Creative Tim UI (UI components, blocks, and agents)",
dependencies: Array.from(allDependencies).sort(),
registryDependencies: Array.from(allRegistryDependencies).sort(),
files: allFiles
}
// Write to public/r/all.json
const outputPath = path.join(process.cwd(), "public/r/all.json")
await fs.writeFile(outputPath, JSON.stringify(allJson, null, 2))
console.log(`✨ Generated all.json with ${registry.items.length} total components`)
}
async function buildPackageRegistries() {
console.log("📦 Building package-specific registries...")
// Build UI components registry
await buildPackageJson(
"ui",
"ui.json",
"All UI components from @creative-tim/ui"
)
// Build blocks registry
await buildPackageJson(
"blocks",
"blocks.json",
"All blocks from @creative-tim/blocks"
)
// Future: Build agents registry
// await buildPackageJson(
// "agents",
// "agents.json",
// "All agent components from @creative-tim/agents"
// )
}
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 new Promise((resolve, reject) => {
exec(`prettier --write registry/__blocks__.json`, (error) => {
if (error) {
reject(error)
return
}
resolve(undefined)
})
})
}
try {
console.log("🗂️ Building registry/__index__.tsx...")
await buildRegistryIndex()
console.log("🗂️ Building registry/__blocks__.json...")
await buildBlocksIndex()
console.log("💅 Building registry.json...")
await buildRegistryJsonFile()
console.log("🏗️ Building registry...")
await buildRegistry()
console.log("📦 Building package registries...")
await buildPackageRegistries()
console.log("📦 Building all.json...")
await buildAllJson()
} catch (error) {
console.error(error)
process.exit(1)
}