feat: add new registry build command (#6350)

* feat: implement shadcn/registry

* feat: add schema field

* fix: import

* chore: add changeset

* chore: remove console

* fix: tests

* fix: diff command

* feat: move to schema/registy-item.json

* fix

* ci: switch to node 20

* ci: build packages

* fix: types

* chore: update schema

* chore: update build registry script

* feat(shadcn): add build command
This commit is contained in:
shadcn
2025-01-14 15:59:41 +04:00
committed by GitHub
parent 254198b4bf
commit cb742e9825
17 changed files with 217 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": {

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"description": "A shadcn registry of components, hooks, pages, etc.",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"homepage": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "https://ui.shadcn.com/schema/registry-item.json"
}
}
},
"required": ["name", "homepage", "items"],
"uniqueItems": true,
"minItems": 1
}

View File

@@ -1,4 +1,4 @@
import { registryItemSchema } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
import { z } from "zod"
import { blocks } from "@/registry/registry-blocks"
@@ -10,15 +10,19 @@ import { lib } from "@/registry/registry-lib"
import { themes } from "@/registry/registry-themes"
import { ui } from "@/registry/registry-ui"
export const registry = [
...ui,
...blocks,
...charts,
...lib,
...hooks,
...themes,
export const registry = {
name: "shadcn/ui",
homepage: "https://ui.shadcn.com",
items: [
...ui,
...blocks,
...charts,
...lib,
...hooks,
...themes,
// Internal use only.
...internal,
...examples,
] satisfies z.infer<typeof registryItemSchema>[]
// Internal use only.
...internal,
...examples,
],
} satisfies Registry

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const blocks: Registry = [
export const blocks: Registry["items"] = [
{
name: "sidebar-01",
type: "registry:block",

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const charts: Registry = [
export const charts: Registry["items"] = [
// Area Charts
{
name: "chart-area-axes",

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const examples: Registry = [
export const examples: Registry["items"] = [
{
name: "accordion-demo",
type: "registry:example",

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const hooks: Registry = [
export const hooks: Registry["items"] = [
{
name: "use-mobile",
type: "registry:hook",

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const internal: Registry = [
export const internal: Registry["items"] = [
{
name: "sink",
type: "registry:internal",

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const lib: Registry = [
export const lib: Registry["items"] = [
{
name: "utils",
type: "registry:lib",

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const themes: Registry = [
export const themes: Registry["items"] = [
{
name: "theme-daylight",
type: "registry:theme",

View File

@@ -1,6 +1,6 @@
import { Registry } from "shadcn/registry"
import { type Registry } from "shadcn/registry"
export const ui: Registry = [
export const ui: Registry["items"] = [
{
name: "accordion",
type: "registry:ui",

View File

@@ -4,6 +4,7 @@ import path from "path"
import template from "lodash/template"
import { rimraf } from "rimraf"
import {
Registry,
registryItemSchema,
registryItemTypeSchema,
registrySchema,
@@ -54,7 +55,7 @@ async function syncStyles() {
rimraf.sync(path.join("registry", targetStyle, dir))
}
for (const item of registry) {
for (const item of registry.items) {
if (
!REGISTRY_INDEX_WHITELIST.includes(item.type) &&
item.type !== "registry:ui"
@@ -98,7 +99,7 @@ async function syncStyles() {
// ----------------------------------------------------------------------------
// Build __registry__/index.tsx.
// ----------------------------------------------------------------------------
async function buildRegistry(registry: z.infer<typeof registrySchema>) {
async function buildRegistry(registry: Registry) {
let index = `// @ts-nocheck
// This file is autogenerated by scripts/build-registry.ts
// Do not edit this file directly.
@@ -111,7 +112,7 @@ export const Index: Record<string, any> = {
index += ` "${style.name}": {`
// Build style index.
for (const item of registry) {
for (const item of registry.items) {
const resolveFiles = item.files?.map(
(file) =>
`registry/${style.name}/${
@@ -254,7 +255,7 @@ export const Index: Record<string, any> = {
// ----------------------------------------------------------------------------
// Build registry/index.json.
// ----------------------------------------------------------------------------
const items = registry
const items = registry.items
.filter((item) => ["registry:ui"].includes(item.type))
.map((item) => {
return {
@@ -288,7 +289,7 @@ export const Index: Record<string, any> = {
// ----------------------------------------------------------------------------
// Build registry/styles/[style]/[name].json.
// ----------------------------------------------------------------------------
async function buildStyles(registry: z.infer<typeof registrySchema>) {
async function buildStyles(registry: Registry) {
for (const style of styles) {
const targetPath = path.join(REGISTRY_PATH, "styles", style.name)
@@ -297,7 +298,7 @@ async function buildStyles(registry: z.infer<typeof registrySchema>) {
await fs.mkdir(targetPath, { recursive: true })
}
for (const item of registry) {
for (const item of registry.items) {
if (!REGISTRY_INDEX_WHITELIST.includes(item.type)) {
continue
}

View File

@@ -0,0 +1,97 @@
import * as fs from "fs/promises"
import * as path from "path"
import { preFlightBuild } from "@/src/preflights/preflight-build"
import { registryItemSchema, registrySchema } from "@/src/registry"
import { handleError } from "@/src/utils/handle-error"
import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
import { spinner } from "@/src/utils/spinner"
import { Command } from "commander"
import { z } from "zod"
export const buildOptionsSchema = z.object({
cwd: z.string(),
registryFile: z.string(),
outputDir: z.string(),
})
export const build = new Command()
.name("build")
.description("build components for a shadcn registry")
.argument("[registry]", "path to registry.json file", "./registry.json")
.option(
"-o, --output <path>",
"destination directory for json files",
"./public/r"
)
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
)
.action(async (registry: string, opts) => {
try {
const options = buildOptionsSchema.parse({
cwd: path.resolve(opts.cwd),
registryFile: registry,
outputDir: opts.output,
})
const { resolvePaths } = await preFlightBuild(options)
const content = await fs.readFile(resolvePaths.registryFile, "utf-8")
const result = registrySchema.safeParse(JSON.parse(content))
if (!result.success) {
logger.error(
`Invalid registry file found at ${highlighter.info(
resolvePaths.registryFile
)}.`
)
process.exit(1)
}
const buildSpinner = spinner("Building registry...")
for (const registryItem of result.data.items) {
if (!registryItem.files) {
continue
}
buildSpinner.start(`Building ${registryItem.name}...`)
// Add the schema to the registry item.
registryItem["$schema"] =
"https://ui.shadcn.com/schema/registry-item.json"
// Loop through each file in the files array.
for (const file of registryItem.files) {
file["content"] = await fs.readFile(
path.resolve(resolvePaths.cwd, file.path),
"utf-8"
)
}
// Validate the registry item.
const result = registryItemSchema.safeParse(registryItem)
if (!result.success) {
logger.error(
`Invalid registry item found for ${highlighter.info(
registryItem.name
)}.`
)
continue
}
// Write the registry item to the output directory.
await fs.writeFile(
path.resolve(resolvePaths.outputDir, `${result.data.name}.json`),
JSON.stringify(result.data, null, 2)
)
}
buildSpinner.succeed("Building registry.")
} catch (error) {
logger.break()
handleError(error)
}
})

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { add } from "@/src/commands/add"
import { build } from "@/src/commands/build"
import { diff } from "@/src/commands/diff"
import { info } from "@/src/commands/info"
import { init } from "@/src/commands/init"
@@ -27,6 +28,7 @@ async function main() {
.addCommand(diff)
.addCommand(migrate)
.addCommand(info)
.addCommand(build)
program.parse()
}

View File

@@ -0,0 +1,46 @@
import path from "path"
import { buildOptionsSchema } from "@/src/commands/build"
import * as ERRORS from "@/src/utils/errors"
import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
import fs from "fs-extra"
import { z } from "zod"
export async function preFlightBuild(
options: z.infer<typeof buildOptionsSchema>
) {
const errors: Record<string, boolean> = {}
const resolvePaths = {
cwd: options.cwd,
registryFile: path.resolve(options.cwd, options.registryFile),
outputDir: path.resolve(options.cwd, options.outputDir),
}
// Ensure registry file exists.
if (!fs.existsSync(resolvePaths.registryFile)) {
errors[ERRORS.BUILD_MISSING_REGISTRY_FILE] = true
}
// Create output directory if it doesn't exist.
await fs.mkdir(resolvePaths.outputDir, { recursive: true })
if (Object.keys(errors).length > 0) {
if (errors[ERRORS.BUILD_MISSING_REGISTRY_FILE]) {
logger.break()
logger.error(
`The path ${highlighter.info(
resolvePaths.registryFile
)} does not exist.`
)
}
logger.break()
process.exit(1)
}
return {
errors,
resolvePaths,
}
}

View File

@@ -1,5 +1,8 @@
import { z } from "zod"
// Note: if you edit the schema here, you must also edit the schema in the
// apps/www/public/schema/registry-item.json file.
export const registryItemTypeSchema = z.enum([
"registry:lib",
"registry:block",
@@ -57,11 +60,15 @@ export const registryItemSchema = z.object({
export type RegistryItem = z.infer<typeof registryItemSchema>
export const registrySchema = z.array(registryItemSchema)
export const registrySchema = z.object({
name: z.string(),
homepage: z.string(),
items: z.array(registryItemSchema),
})
export type Registry = z.infer<typeof registrySchema>
export const registryIndexSchema = registrySchema
export const registryIndexSchema = z.array(registryItemSchema)
export const stylesSchema = z.array(
z.object({

View File

@@ -10,3 +10,4 @@ export const COMPONENT_URL_UNAUTHORIZED = "9"
export const COMPONENT_URL_FORBIDDEN = "10"
export const COMPONENT_URL_BAD_REQUEST = "11"
export const COMPONENT_URL_INTERNAL_SERVER_ERROR = "12"
export const BUILD_MISSING_REGISTRY_FILE = "13"