mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-23 12:45:47 +00:00
* feat: init * fix * fix * fix * feat * feat * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * feat: implement icons * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * feat: update init command * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * feat: dialog * feat * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * feat: add registry:base item type * feat: rename frame to canva * fix * feat * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fi * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * feat: add all colors * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * feat: add outfit font * fix * fix * fix * fix * fix * chore: changeset * fix * fix * fix * fix * fix * fix * fix * fix
454 lines
13 KiB
TypeScript
454 lines
13 KiB
TypeScript
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()
|
||
|
||
// We only need the legacy styles here for backward compatibility.
|
||
console.log(`📦 Building registry/__index__.tsx...`)
|
||
await buildRegistryIndex([...legacyStyles])
|
||
|
||
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()
|
||
|
||
// 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<string, Record<string, any>> = {`
|
||
|
||
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) {
|
||
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}`)
|
||
}
|
||
|
||
const registryItems = result.data.items
|
||
|
||
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"
|
||
const transformedContent = shouldTransform
|
||
? await transformStyle(source, {
|
||
styleMap: styleMap,
|
||
})
|
||
: source
|
||
|
||
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<string, Record<string, any>> = {`
|
||
|
||
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) {
|
||
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<void>((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<void>((resolve, reject) => {
|
||
execFile("prettier", ["--write", outputPath], (error) => {
|
||
if (error) {
|
||
reject(error)
|
||
} else {
|
||
resolve()
|
||
}
|
||
})
|
||
})
|
||
}
|