mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-10-30 18:22:59 +00:00
Upstream sync from shadcn-ui/ui (1686 commits since last merge base). Key changes: - packages/shadcn upgraded to 4.1.0 (CLI font transformers, scaffold from github, chart colors) - apps/v4 replaces apps/www as the primary docs/registry app - Font system: 25+ heading font registries, font markers utility, @supports override - Chart improvements: recharts v3 compat, radial chart fixes - templates/ updated with monorepo variants (astro, vite, react-router, start) - skills/shadcn AI skill definitions added - deprecated/ removed (auth, auth-firebase, cli, www) Preserved Hanzo-only paths: app/, pkg/, demo/, docs/, template/next/
775 lines
24 KiB
TypeScript
775 lines
24 KiB
TypeScript
import { spawn } from "child_process"
|
|
import { promises as fs } from "fs"
|
|
import path from "path"
|
|
import { rimraf } from "rimraf"
|
|
import { registrySchema, type RegistryItem } from "shadcn/schema"
|
|
import {
|
|
createStyleMap,
|
|
transformDirection,
|
|
transformIcons,
|
|
transformStyle,
|
|
} from "shadcn/utils"
|
|
import { Project, ScriptKind } from "ts-morph"
|
|
|
|
import { getAllBlocks } from "@/lib/blocks"
|
|
import { legacyStyles } from "@/registry/_legacy-styles"
|
|
import { BASE_COLORS } from "@/registry/base-colors"
|
|
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"]
|
|
|
|
// Collect paths for batch prettier formatting at the end.
|
|
const prettierPaths: string[] = []
|
|
|
|
// Create a ts-morph project for icon transformations.
|
|
const iconProject = new Project({
|
|
compilerOptions: {},
|
|
})
|
|
|
|
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 {
|
|
const totalStart = performance.now()
|
|
|
|
console.log("šļø Building bases...")
|
|
await buildBasesIndex(Array.from(BASES))
|
|
await buildBases(Array.from(BASES))
|
|
|
|
// Format base files before building styles so the JSON output contains formatted code.
|
|
const baseDirs = Array.from(BASES).flatMap((base) =>
|
|
STYLES.map((style) =>
|
|
path.join(process.cwd(), `registry/${base.name}-${style.name}`)
|
|
)
|
|
)
|
|
console.log("\n⨠Formatting base files...")
|
|
await batchPrettier(baseDirs)
|
|
|
|
const stylesToBuild = getStylesToBuild()
|
|
|
|
// Build index for legacy styles and whitelisted base-style combinations.
|
|
console.log(`š¦ Building registry/__index__.tsx...`)
|
|
const stylesForIndex = WHITELISTED_STYLES.map((name) => ({
|
|
name,
|
|
title: name,
|
|
}))
|
|
await buildRegistryIndex(stylesForIndex)
|
|
|
|
console.log("š
Building styles...")
|
|
// Build all styles in parallel.
|
|
await Promise.all(
|
|
stylesToBuild.map(async (style) => {
|
|
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()
|
|
|
|
// Copy UI to examples before cleanup.
|
|
console.log("\nš Copying UI to examples...")
|
|
await copyUIToExamples()
|
|
|
|
// Build RTL variants of examples.
|
|
console.log("\nš Building RTL examples...")
|
|
await buildRtlExamples()
|
|
|
|
console.log("\nš¦ Building public/r/index.json...")
|
|
await buildIndex()
|
|
|
|
console.log("\nš Building public/r/registries.json...")
|
|
await buildRegistriesJson()
|
|
|
|
console.log("\nšØ Building public/r/colors...")
|
|
await buildColors()
|
|
|
|
// Batch format all collected files with prettier at the end.
|
|
if (prettierPaths.length > 0) {
|
|
console.log(`\n⨠Formatting ${prettierPaths.length} files...`)
|
|
await batchPrettier(prettierPaths)
|
|
}
|
|
|
|
// Clean up intermediate files and generated base directories.
|
|
console.log("\nš§¹ Cleaning up...")
|
|
await cleanUp(stylesToBuild)
|
|
|
|
const elapsed = ((performance.now() - totalStart) / 1000).toFixed(2)
|
|
console.log(`\nā
Build complete in ${elapsed}s!`)
|
|
} catch (error) {
|
|
console.error(error)
|
|
process.exit(1)
|
|
}
|
|
|
|
async function buildBasesIndex(bases: Base[]) {
|
|
// Import all registries in parallel.
|
|
const registryImports = await Promise.all(
|
|
bases.map(async (base) => {
|
|
const { registry: importedRegistry } = await import(
|
|
`../registry/bases/${base.name}/registry.ts`
|
|
)
|
|
return { base, importedRegistry }
|
|
})
|
|
)
|
|
|
|
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, importedRegistry } of registryImports) {
|
|
// 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) {
|
|
// Skip demos - they're handled by the examples index.
|
|
if (item.type === "registry:internal") {
|
|
continue
|
|
}
|
|
|
|
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.
|
|
await rimraf(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[]) {
|
|
// Pre-import all base registries and style CSS files in parallel.
|
|
const [baseImports, styleMaps] = await Promise.all([
|
|
Promise.all(
|
|
bases.map(async (base) => {
|
|
const { registry: baseRegistry } = await import(
|
|
`../registry/bases/${base.name}/registry.ts`
|
|
)
|
|
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.filter(
|
|
(item) => item.type !== "registry:internal"
|
|
)
|
|
return { base, baseRegistry, registryItems }
|
|
})
|
|
),
|
|
Promise.all(
|
|
STYLES.map(async (style) => {
|
|
const styleContent = await fs.readFile(
|
|
path.join(process.cwd(), `registry/styles/style-${style.name}.css`),
|
|
"utf8"
|
|
)
|
|
return { style, styleMap: createStyleMap(styleContent) }
|
|
})
|
|
),
|
|
])
|
|
|
|
// Build all base-style combinations in parallel.
|
|
const combinations: Array<{
|
|
base: Base
|
|
style: (typeof STYLES)[number]
|
|
baseRegistry: (typeof baseImports)[number]["baseRegistry"]
|
|
registryItems: (typeof baseImports)[number]["registryItems"]
|
|
styleMap: Record<string, string>
|
|
}> = []
|
|
|
|
for (const { base, baseRegistry, registryItems } of baseImports) {
|
|
for (const { style, styleMap } of styleMaps) {
|
|
combinations.push({ base, style, baseRegistry, registryItems, styleMap })
|
|
}
|
|
}
|
|
|
|
await Promise.all(
|
|
combinations.map(
|
|
async ({ base, style, baseRegistry, registryItems, styleMap }) => {
|
|
console.log(` ā
${base.name}-${style.name}...`)
|
|
|
|
const styleOutputDir = path.join(
|
|
process.cwd(),
|
|
`registry/${base.name}-${style.name}`
|
|
)
|
|
await rimraf(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)
|
|
|
|
// Process all files in parallel.
|
|
await Promise.all(
|
|
registryItems.flatMap((registryItem) => {
|
|
if (!registryItem.files || registryItem.files.length === 0) {
|
|
return []
|
|
}
|
|
return registryItem.files.map(async (file: { path: string }) => {
|
|
const source = await fs.readFile(
|
|
path.join(
|
|
process.cwd(),
|
|
`registry/bases/${base.name}/${file.path}`
|
|
),
|
|
"utf8"
|
|
)
|
|
|
|
const fileExtension = path.extname(file.path)
|
|
const shouldTransform =
|
|
fileExtension === ".tsx" || fileExtension === ".ts"
|
|
|
|
let transformedContent = source
|
|
|
|
if (shouldTransform) {
|
|
transformedContent = await transformStyle(source, {
|
|
styleMap: styleMap,
|
|
})
|
|
transformedContent = transformedContent.replace(
|
|
new RegExp(`@/registry/bases/${base.name}/`, "g"),
|
|
`@/registry/${base.name}-${style.name}/`
|
|
)
|
|
}
|
|
|
|
const outputPath = path.join(
|
|
process.cwd(),
|
|
`registry/${base.name}-${style.name}/${file.path}`
|
|
)
|
|
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
|
await fs.writeFile(outputPath, transformedContent)
|
|
})
|
|
})
|
|
)
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
async function buildRegistryIndex(styles: { name: string; title: string }[]) {
|
|
// Import all registries in parallel.
|
|
const registryImports = await Promise.all(
|
|
styles.map(async (style) => {
|
|
const { registry: importedRegistry } = await import(
|
|
`../registry/${style.name}/registry.ts`
|
|
)
|
|
return { style, importedRegistry }
|
|
})
|
|
)
|
|
|
|
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, importedRegistry } of registryImports) {
|
|
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) {
|
|
if (item.type === "registry:internal") {
|
|
continue
|
|
}
|
|
|
|
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 += `
|
|
}`
|
|
|
|
await rimraf(path.join(process.cwd(), "registry/__index__.tsx"))
|
|
await fs.writeFile(path.join(process.cwd(), "registry/__index__.tsx"), index)
|
|
}
|
|
|
|
async function buildRegistryJsonFile(styleName: string) {
|
|
const { registry: importedRegistry } = await import(
|
|
`../registry/${styleName}/registry.ts`
|
|
)
|
|
|
|
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
|
|
|
|
const fixedRegistry = {
|
|
...registry,
|
|
items: registry.items.map((item) => {
|
|
const files = item.files?.map((file) => ({
|
|
...file,
|
|
path: `registry/${styleName}/${file.path}`,
|
|
}))
|
|
return { ...item, files }
|
|
}),
|
|
}
|
|
|
|
const outputDir = path.join(process.cwd(), `public/r/styles/${styleName}`)
|
|
await fs.mkdir(outputDir, { recursive: true })
|
|
|
|
const registryJsonPath = path.join(outputDir, "registry.json")
|
|
await fs.writeFile(registryJsonPath, JSON.stringify(fixedRegistry, null, 2))
|
|
prettierPaths.push(registryJsonPath)
|
|
|
|
const tempRegistryPath = path.join(
|
|
process.cwd(),
|
|
`registry-${styleName}.json`
|
|
)
|
|
await fs.writeFile(tempRegistryPath, JSON.stringify(fixedRegistry, null, 2))
|
|
}
|
|
|
|
async function buildRegistry(styleName: string) {
|
|
const outputPath = `public/r/styles/${styleName}`
|
|
await new Promise<void>((resolve, reject) => {
|
|
const proc = spawn(
|
|
"node",
|
|
[
|
|
"../../packages/shadcn/dist/index.js",
|
|
"build",
|
|
`registry-${styleName}.json`,
|
|
"--output",
|
|
outputPath,
|
|
],
|
|
{ cwd: process.cwd(), stdio: "pipe" }
|
|
)
|
|
let stderr = ""
|
|
proc.stderr?.on("data", (data) => (stderr += data))
|
|
proc.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(`Process exited with code ${code}: ${stderr}`))
|
|
} else {
|
|
resolve()
|
|
}
|
|
})
|
|
proc.on("error", reject)
|
|
})
|
|
}
|
|
|
|
async function buildBlocksIndex() {
|
|
const blocks = await getAllBlocks(["registry:block"])
|
|
|
|
const payload = blocks.map((block) => ({
|
|
name: block.name,
|
|
description: block.description,
|
|
categories: block.categories,
|
|
}))
|
|
|
|
await rimraf(path.join(process.cwd(), "registry/__blocks__.json"))
|
|
const blocksJsonPath = path.join(process.cwd(), "registry/__blocks__.json")
|
|
await fs.writeFile(blocksJsonPath, JSON.stringify(payload, null, 2))
|
|
prettierPaths.push(blocksJsonPath)
|
|
}
|
|
|
|
async function cleanUp(stylesToBuild: { name: string; title: string }[]) {
|
|
// Clean up all in parallel.
|
|
const cleanupPromises: Promise<boolean>[] = []
|
|
|
|
for (const style of stylesToBuild) {
|
|
cleanupPromises.push(
|
|
rimraf(path.join(process.cwd(), `registry-${style.name}.json`))
|
|
)
|
|
}
|
|
|
|
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}`)
|
|
cleanupPromises.push(rimraf(baseDir))
|
|
}
|
|
}
|
|
}
|
|
|
|
await Promise.all(cleanupPromises)
|
|
}
|
|
|
|
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))
|
|
prettierPaths.push(outputPath)
|
|
}
|
|
|
|
async function applyIconTransform(content: string, filename: string) {
|
|
const sourceFile = iconProject.createSourceFile(filename, content, {
|
|
scriptKind: ScriptKind.TSX,
|
|
overwrite: true,
|
|
})
|
|
|
|
// Create a minimal config with just iconLibrary.
|
|
// transformIcons only uses config.iconLibrary, so we can safely cast this.
|
|
type TransformIconsConfig = Parameters<typeof transformIcons>[0]["config"]
|
|
type IconTransformInput = {
|
|
filename: string
|
|
raw: string
|
|
sourceFile: typeof sourceFile
|
|
config: TransformIconsConfig
|
|
}
|
|
const config = { iconLibrary: "lucide" } as TransformIconsConfig
|
|
|
|
await (transformIcons as (opts: IconTransformInput) => Promise<unknown>)({
|
|
filename,
|
|
raw: content,
|
|
sourceFile,
|
|
config,
|
|
})
|
|
|
|
return sourceFile.getText()
|
|
}
|
|
|
|
async function copyUIToExamples() {
|
|
const defaultStyle = "nova"
|
|
const directories = ["ui", "lib", "hooks"]
|
|
|
|
// Build all copy tasks.
|
|
const copyTasks: Array<{ base: Base; dir: string }> = []
|
|
for (const base of BASES) {
|
|
for (const dir of directories) {
|
|
copyTasks.push({ base, dir })
|
|
}
|
|
}
|
|
|
|
await Promise.all(
|
|
copyTasks.map(async ({ base, dir }) => {
|
|
const sourceStyle = `${base.name}-${defaultStyle}`
|
|
const fromDir = path.join(process.cwd(), `registry/${sourceStyle}/${dir}`)
|
|
const toDir = path.join(process.cwd(), `examples/${base.name}/${dir}`)
|
|
|
|
try {
|
|
await fs.access(fromDir)
|
|
} catch {
|
|
console.log(` ā ļø registry/${sourceStyle}/${dir} not found, skipping`)
|
|
return
|
|
}
|
|
|
|
await rimraf(toDir)
|
|
await fs.mkdir(toDir, { recursive: true })
|
|
|
|
const files = await fs.readdir(fromDir)
|
|
await Promise.all(
|
|
files.map(async (file) => {
|
|
const sourcePath = path.join(fromDir, file)
|
|
const targetPath = path.join(toDir, file)
|
|
let content = await fs.readFile(sourcePath, "utf-8")
|
|
content = content.replace(
|
|
new RegExp(`@/registry/${sourceStyle}/`, "g"),
|
|
`@/examples/${base.name}/`
|
|
)
|
|
|
|
// Transform icons for TSX files.
|
|
if (file.endsWith(".tsx")) {
|
|
content = await applyIconTransform(content, file)
|
|
}
|
|
|
|
await fs.writeFile(targetPath, content)
|
|
})
|
|
)
|
|
|
|
console.log(
|
|
` ā
registry/${sourceStyle}/${dir} ā examples/${base.name}/${dir}`
|
|
)
|
|
})
|
|
)
|
|
}
|
|
|
|
async function buildIndex() {
|
|
// Import ui/_registry.ts from each base.
|
|
const baseUiRegistries = await Promise.all(
|
|
Array.from(BASES).map(async (base) => {
|
|
const { ui } = await import(
|
|
`../registry/bases/${base.name}/ui/_registry.ts`
|
|
)
|
|
return { baseName: base.name, items: ui as RegistryItem[] }
|
|
})
|
|
)
|
|
|
|
// Dedupe components by name and merge links across bases.
|
|
type IndexItem = Omit<RegistryItem, "meta"> & {
|
|
meta?: { links?: Record<string, RegistryItem["meta"]> }
|
|
}
|
|
const componentMap = new Map<string, IndexItem>()
|
|
for (const { baseName, items } of baseUiRegistries) {
|
|
for (const item of items) {
|
|
if (!componentMap.has(item.name)) {
|
|
const { meta, ...rest } = item
|
|
componentMap.set(item.name, {
|
|
...rest,
|
|
...(meta?.links
|
|
? { meta: { links: { [baseName]: meta.links } } }
|
|
: {}),
|
|
})
|
|
} else if (item.meta?.links) {
|
|
const existing = componentMap.get(item.name)!
|
|
existing.meta = existing.meta || {}
|
|
existing.meta.links = existing.meta.links || {}
|
|
existing.meta.links[baseName] = item.meta.links
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort alphabetically by name.
|
|
const index = Array.from(componentMap.values()).sort((a, b) =>
|
|
a.name.localeCompare(b.name)
|
|
)
|
|
|
|
const outputPath = path.join(process.cwd(), "public/r/index.json")
|
|
await fs.writeFile(outputPath, JSON.stringify(index, null, 2))
|
|
prettierPaths.push(outputPath)
|
|
}
|
|
|
|
async function buildRegistriesJson() {
|
|
const directoryPath = path.join(process.cwd(), "registry/directory.json")
|
|
const directoryContent = await fs.readFile(directoryPath, "utf8")
|
|
const directory = JSON.parse(directoryContent) as Array<{
|
|
name: string
|
|
homepage?: string
|
|
url: string
|
|
description?: string
|
|
featured?: boolean
|
|
logo?: string
|
|
}>
|
|
|
|
const registries = directory.map((entry) => ({
|
|
name: entry.name,
|
|
homepage: entry.homepage,
|
|
url: entry.url,
|
|
description: entry.description,
|
|
}))
|
|
|
|
const outputPath = path.join(process.cwd(), "public/r/registries.json")
|
|
await fs.writeFile(outputPath, JSON.stringify(registries, null, 2))
|
|
prettierPaths.push(outputPath)
|
|
}
|
|
|
|
async function buildRtlExamples() {
|
|
// Process all bases in parallel.
|
|
await Promise.all(
|
|
Array.from(BASES).map(async (base) => {
|
|
const sourceDir = path.join(process.cwd(), `examples/${base.name}/ui`)
|
|
const targetDir = path.join(process.cwd(), `examples/${base.name}/ui-rtl`)
|
|
|
|
try {
|
|
await fs.access(sourceDir)
|
|
} catch {
|
|
console.log(` ā ļø examples/${base.name}/ui not found, skipping...`)
|
|
return
|
|
}
|
|
|
|
await rimraf(targetDir)
|
|
await fs.mkdir(targetDir, { recursive: true })
|
|
|
|
const files = await fs.readdir(sourceDir)
|
|
await Promise.all(
|
|
files
|
|
.filter((file) => file.endsWith(".tsx") || file.endsWith(".ts"))
|
|
.map(async (file) => {
|
|
const sourcePath = path.join(sourceDir, file)
|
|
const targetPath = path.join(targetDir, file)
|
|
|
|
let content = await fs.readFile(sourcePath, "utf-8")
|
|
content = await transformDirection(content, true)
|
|
content = content.replace(
|
|
new RegExp(`@/examples/${base.name}/ui/`, "g"),
|
|
`@/examples/${base.name}/ui-rtl/`
|
|
)
|
|
await fs.writeFile(targetPath, content)
|
|
})
|
|
)
|
|
|
|
console.log(` ā
examples/${base.name}/ui-rtl`)
|
|
})
|
|
)
|
|
}
|
|
|
|
async function batchPrettier(paths: string[]) {
|
|
if (paths.length === 0) return
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
const prettierBin = path.join(process.cwd(), "node_modules/.bin/prettier")
|
|
const proc = spawn(prettierBin, ["--write", ...paths], {
|
|
cwd: process.cwd(),
|
|
stdio: "inherit",
|
|
})
|
|
proc.on("close", () => resolve())
|
|
proc.on("error", reject)
|
|
})
|
|
}
|
|
|
|
async function buildColors() {
|
|
const colorsTargetPath = path.join(process.cwd(), "public/r/colors")
|
|
await fs.mkdir(colorsTargetPath, { recursive: true })
|
|
|
|
await Promise.all(
|
|
BASE_COLORS.map(async (baseColor) => {
|
|
const light = (baseColor.cssVars?.light ?? {}) as Record<string, string>
|
|
const dark = (baseColor.cssVars?.dark ?? {}) as Record<string, string>
|
|
|
|
const cssVarKeys = Object.keys(light).filter(
|
|
(key) => !key.startsWith("sidebar")
|
|
)
|
|
|
|
const rootVars = cssVarKeys
|
|
.map((key) => ` --${key}: ${light[key]};`)
|
|
.join("\n")
|
|
const darkVars = cssVarKeys
|
|
.filter((key) => dark[key])
|
|
.map((key) => ` --${key}: ${dark[key]};`)
|
|
.join("\n")
|
|
|
|
const payload = {
|
|
inlineColors: { light, dark },
|
|
cssVars: { light, dark },
|
|
cssVarsV4: baseColor.cssVars,
|
|
inlineColorsTemplate:
|
|
"@tailwind base;\n@tailwind components;\n@tailwind utilities;\n ",
|
|
cssVarsTemplate: `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n${rootVars}\n }\n\n .dark {\n${darkVars}\n }\n}\n\n@layer base {\n * {\n @apply border-border;\n }\n body {\n @apply bg-background text-foreground;\n }\n}`,
|
|
}
|
|
|
|
const outputPath = path.join(colorsTargetPath, `${baseColor.name}.json`)
|
|
await fs.writeFile(outputPath, JSON.stringify(payload, null, 2))
|
|
prettierPaths.push(outputPath)
|
|
console.log(` ā
${baseColor.name}.json`)
|
|
})
|
|
)
|
|
}
|