import fs from 'node:fs' import path, { basename } from 'node:path' import { template } from 'lodash-es' import { rimraf } from 'rimraf' import { colorMapping, colors } from '../src/lib/registry/colors' import { registrySchema } from '../src/lib/registry/schema' import { styles } from '../src/lib/registry/styles' import { themes } from '../src/lib/registry/themes' import { buildRegistry } from '../src/lib/registry/registry' const REGISTRY_PATH = path.join(process.cwd(), 'src/public/registry') const registry = await buildRegistry() const result = registrySchema.safeParse(registry) if (!result.success) { console.error(result.error) process.exit(1) } // ---------------------------------------------------------------------------- // Build __registry__/index.ts. // ---------------------------------------------------------------------------- let index = ` // This file is autogenerated by scripts/build-registry.ts // Do not edit this file directly. export const Index = { ` for (const style of styles) { index += ` "${style.name}": {` // Build style index. for (const item of result.data) { if (item.type === 'components:ui') continue const resolveFiles = item.files.map( file => `../src/lib/registry/${style.name}/${file}`, ) // const type = item.type.split(':')[1] index += ` "${item.name}": { name: "${item.name}", type: "${item.type}", registryDependencies: ${JSON.stringify(item.registryDependencies)}, component: () => import("${resolveFiles[0]}").then((m) => m.default), files: [${resolveFiles.map(file => `"${file}"`)}], },` } index += ` },` } index += ` } ` // Write style index. rimraf.sync(path.join(process.cwd(), '__registry__/index.ts')) fs.writeFileSync(path.join(process.cwd(), '__registry__/index.ts'), index) // ---------------------------------------------------------------------------- // Build registry/styles/[style]/[name].json. // ---------------------------------------------------------------------------- const newLine = '\n' for (const style of styles) { const targetPath = path.join(REGISTRY_PATH, 'styles', style.name) // Create directory if it doesn't exist. if (!fs.existsSync(targetPath)) fs.mkdirSync(targetPath, { recursive: true }) for (const item of result.data) { if (item.type !== 'components:ui') continue const files = item.files?.map((file) => { let content = fs.readFileSync( path.join(process.cwd(), 'src/lib/registry', style.name, file), 'utf8', ) // Replace Windows-style newlines with Unix-style newlines content = content.replace(/\r\n/g, newLine) return { name: basename(file), content, } }) const payload = { ...item, files, } const payloadStr = JSON.stringify(payload, null, 2).replace(/\r\n/g, newLine) fs.writeFileSync( path.join(targetPath, `${item.name}.json`), payloadStr, 'utf8', ) } } // ---------------------------------------------------------------------------- // Build registry/styles/index.json. // ---------------------------------------------------------------------------- const stylesJson = JSON.stringify(styles, null, 2) fs.writeFileSync( path.join(REGISTRY_PATH, 'styles/index.json'), stylesJson, 'utf8', ) const REGISTRY_IGNORE = ['super-form'] // ---------------------------------------------------------------------------- // Build registry/index.json. // ---------------------------------------------------------------------------- const names = result.data.filter( item => item.type === 'components:ui' && !REGISTRY_IGNORE.includes(item.name), ) const registryJson = JSON.stringify(names, null, 2) rimraf.sync(path.join(REGISTRY_PATH, 'index.json')) fs.writeFileSync(path.join(REGISTRY_PATH, 'index.json'), registryJson, 'utf8') // ---------------------------------------------------------------------------- // Build registry/colors/index.json. // ---------------------------------------------------------------------------- const colorsTargetPath = path.join(REGISTRY_PATH, 'colors') rimraf.sync(colorsTargetPath) if (!fs.existsSync(colorsTargetPath)) fs.mkdirSync(colorsTargetPath, { recursive: true }) const colorsData: Record = {} for (const [color, value] of Object.entries(colors)) { if (typeof value === 'string') { colorsData[color] = value continue } if (Array.isArray(value)) { colorsData[color] = value.map(item => ({ ...item, rgbChannel: item.rgb.replace( /^rgb\((\d+),(\d+),(\d+)\)$/, '$1 $2 $3', ), hslChannel: item.hsl.replace( /^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/, '$1 $2 $3', ), })) continue } if (typeof value === 'object') { colorsData[color] = { ...value, rgbChannel: value.rgb.replace( /^rgb\((\d+),(\d+),(\d+)\)$/, '$1 $2 $3', ), hslChannel: value.hsl.replace( /^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/, '$1 $2 $3', ), } continue } } fs.writeFileSync( path.join(colorsTargetPath, 'index.json'), JSON.stringify(colorsData, null, 2), 'utf8', ) // ---------------------------------------------------------------------------- // Build registry/colors/[base].json. // ---------------------------------------------------------------------------- export const BASE_STYLES = `@tailwind base; @tailwind components; @tailwind utilities; ` export const BASE_STYLES_WITH_VARIABLES = `@tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: <%- colors.light["background"] %>; --foreground: <%- colors.light["foreground"] %>; --muted: <%- colors.light["muted"] %>; --muted-foreground: <%- colors.light["muted-foreground"] %>; --popover: <%- colors.light["popover"] %>; --popover-foreground: <%- colors.light["popover-foreground"] %>; --card: <%- colors.light["card"] %>; --card-foreground: <%- colors.light["card-foreground"] %>; --border: <%- colors.light["border"] %>; --input: <%- colors.light["input"] %>; --primary: <%- colors.light["primary"] %>; --primary-foreground: <%- colors.light["primary-foreground"] %>; --secondary: <%- colors.light["secondary"] %>; --secondary-foreground: <%- colors.light["secondary-foreground"] %>; --accent: <%- colors.light["accent"] %>; --accent-foreground: <%- colors.light["accent-foreground"] %>; --destructive: <%- colors.light["destructive"] %>; --destructive-foreground: <%- colors.light["destructive-foreground"] %>; --ring: <%- colors.light["ring"] %>; --radius: 0.5rem; } .dark { --background: <%- colors.dark["background"] %>; --foreground: <%- colors.dark["foreground"] %>; --muted: <%- colors.dark["muted"] %>; --muted-foreground: <%- colors.dark["muted-foreground"] %>; --popover: <%- colors.dark["popover"] %>; --popover-foreground: <%- colors.dark["popover-foreground"] %>; --card: <%- colors.dark["card"] %>; --card-foreground: <%- colors.dark["card-foreground"] %>; --border: <%- colors.dark["border"] %>; --input: <%- colors.dark["input"] %>; --primary: <%- colors.dark["primary"] %>; --primary-foreground: <%- colors.dark["primary-foreground"] %>; --secondary: <%- colors.dark["secondary"] %>; --secondary-foreground: <%- colors.dark["secondary-foreground"] %>; --accent: <%- colors.dark["accent"] %>; --accent-foreground: <%- colors.dark["accent-foreground"] %>; --destructive: <%- colors.dark["destructive"] %>; --destructive-foreground: <%- colors.dark["destructive-foreground"] %>; --ring: <%- colors.dark["ring"] %>; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } }` for (const baseColor of ['slate', 'gray', 'zinc', 'neutral', 'stone', 'lime']) { const base: Record = { inlineColors: {}, cssVars: {}, } for (const [mode, values] of Object.entries(colorMapping)) { base.inlineColors[mode] = {} base.cssVars[mode] = {} for (const [key, value] of Object.entries(values)) { if (typeof value === 'string') { const resolvedColor = value.replace( /{{base}}-/g, `${baseColor}-`, ) base.inlineColors[mode][key] = resolvedColor const [resolvedBase, scale] = resolvedColor.split('-') const color = scale ? colorsData[resolvedBase].find( (item: any) => item.scale === Number.parseInt(scale), ) : colorsData[resolvedBase] if (color) base.cssVars[mode][key] = color.hslChannel } } } // Build css vars. base.inlineColorsTemplate = template(BASE_STYLES)({}) base.cssVarsTemplate = template(BASE_STYLES_WITH_VARIABLES)({ colors: base.cssVars, }) fs.writeFileSync( path.join(REGISTRY_PATH, `colors/${baseColor}.json`), JSON.stringify(base, null, 2), 'utf8', ) } // ---------------------------------------------------------------------------- // Build registry/themes.css // ---------------------------------------------------------------------------- export const THEME_STYLES_WITH_VARIABLES = ` .theme-<%- theme %> { --background: <%- colors.light["background"] %>; --foreground: <%- colors.light["foreground"] %>; --muted: <%- colors.light["muted"] %>; --muted-foreground: <%- colors.light["muted-foreground"] %>; --popover: <%- colors.light["popover"] %>; --popover-foreground: <%- colors.light["popover-foreground"] %>; --card: <%- colors.light["card"] %>; --card-foreground: <%- colors.light["card-foreground"] %>; --border: <%- colors.light["border"] %>; --input: <%- colors.light["input"] %>; --primary: <%- colors.light["primary"] %>; --primary-foreground: <%- colors.light["primary-foreground"] %>; --secondary: <%- colors.light["secondary"] %>; --secondary-foreground: <%- colors.light["secondary-foreground"] %>; --accent: <%- colors.light["accent"] %>; --accent-foreground: <%- colors.light["accent-foreground"] %>; --destructive: <%- colors.light["destructive"] %>; --destructive-foreground: <%- colors.light["destructive-foreground"] %>; --ring: <%- colors.light["ring"] %>; --radius: 0.5rem; } .dark .theme-<%- theme %> { --background: <%- colors.dark["background"] %>; --foreground: <%- colors.dark["foreground"] %>; --muted: <%- colors.dark["muted"] %>; --muted-foreground: <%- colors.dark["muted-foreground"] %>; --popover: <%- colors.dark["popover"] %>; --popover-foreground: <%- colors.dark["popover-foreground"] %>; --card: <%- colors.dark["card"] %>; --card-foreground: <%- colors.dark["card-foreground"] %>; --border: <%- colors.dark["border"] %>; --input: <%- colors.dark["input"] %>; --primary: <%- colors.dark["primary"] %>; --primary-foreground: <%- colors.dark["primary-foreground"] %>; --secondary: <%- colors.dark["secondary"] %>; --secondary-foreground: <%- colors.dark["secondary-foreground"] %>; --accent: <%- colors.dark["accent"] %>; --accent-foreground: <%- colors.dark["accent-foreground"] %>; --destructive: <%- colors.dark["destructive"] %>; --destructive-foreground: <%- colors.dark["destructive-foreground"] %>; --ring: <%- colors.dark["ring"] %>; }` const themeCSS: Array = [] for (const theme of themes) { themeCSS.push( template(THEME_STYLES_WITH_VARIABLES)({ colors: theme.cssVars, theme: theme.name, }), ) } fs.writeFileSync( path.join(REGISTRY_PATH, 'themes.css'), themeCSS.join('\n'), 'utf8', ) console.log('✅ Done!')