Files
shadcn-ui/apps/v4/scripts/build-registry.mts
shadcn 86d9b00084 chore: update deps (#9022)
* 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
2025-12-12 21:01:44 +04:00

454 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
})
})
}