From cb742e98252fe8aa5cad3377d06e1d8a884953db Mon Sep 17 00:00:00 2001 From: shadcn Date: Tue, 14 Jan 2025 15:59:41 +0400 Subject: [PATCH] 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 --- apps/www/public/schema/registry-item.json | 2 +- apps/www/public/schema/registry.json | 22 +++++ apps/www/registry/index.ts | 28 +++--- apps/www/registry/registry-blocks.ts | 4 +- apps/www/registry/registry-charts.ts | 4 +- apps/www/registry/registry-examples.ts | 4 +- apps/www/registry/registry-hooks.ts | 4 +- apps/www/registry/registry-internal.ts | 4 +- apps/www/registry/registry-lib.ts | 4 +- apps/www/registry/registry-themes.ts | 4 +- apps/www/registry/registry-ui.ts | 4 +- apps/www/scripts/build-registry.mts | 13 +-- packages/shadcn/src/commands/build.ts | 97 +++++++++++++++++++ packages/shadcn/src/index.ts | 2 + .../shadcn/src/preflights/preflight-build.ts | 46 +++++++++ packages/shadcn/src/registry/schema.ts | 11 ++- packages/shadcn/src/utils/errors.ts | 1 + 17 files changed, 217 insertions(+), 37 deletions(-) create mode 100644 apps/www/public/schema/registry.json create mode 100644 packages/shadcn/src/commands/build.ts create mode 100644 packages/shadcn/src/preflights/preflight-build.ts diff --git a/apps/www/public/schema/registry-item.json b/apps/www/public/schema/registry-item.json index d5aab0a9882..ee8c10b9148 100644 --- a/apps/www/public/schema/registry-item.json +++ b/apps/www/public/schema/registry-item.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft-07/schema#", "type": "object", "properties": { "name": { diff --git a/apps/www/public/schema/registry.json b/apps/www/public/schema/registry.json new file mode 100644 index 00000000000..6f21ab4b24e --- /dev/null +++ b/apps/www/public/schema/registry.json @@ -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 +} diff --git a/apps/www/registry/index.ts b/apps/www/registry/index.ts index 1ccf2b9e9f7..7adb0a720df 100644 --- a/apps/www/registry/index.ts +++ b/apps/www/registry/index.ts @@ -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[] + // Internal use only. + ...internal, + ...examples, + ], +} satisfies Registry diff --git a/apps/www/registry/registry-blocks.ts b/apps/www/registry/registry-blocks.ts index 2a96811377b..956be895488 100644 --- a/apps/www/registry/registry-blocks.ts +++ b/apps/www/registry/registry-blocks.ts @@ -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", diff --git a/apps/www/registry/registry-charts.ts b/apps/www/registry/registry-charts.ts index 9da7dea4eee..551fffc470e 100644 --- a/apps/www/registry/registry-charts.ts +++ b/apps/www/registry/registry-charts.ts @@ -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", diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts index 5971bc0290d..4f07b12ed65 100644 --- a/apps/www/registry/registry-examples.ts +++ b/apps/www/registry/registry-examples.ts @@ -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", diff --git a/apps/www/registry/registry-hooks.ts b/apps/www/registry/registry-hooks.ts index b7ec8fe3447..bcc7354f128 100644 --- a/apps/www/registry/registry-hooks.ts +++ b/apps/www/registry/registry-hooks.ts @@ -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", diff --git a/apps/www/registry/registry-internal.ts b/apps/www/registry/registry-internal.ts index 9e50f78942c..aeb9a38dad1 100644 --- a/apps/www/registry/registry-internal.ts +++ b/apps/www/registry/registry-internal.ts @@ -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", diff --git a/apps/www/registry/registry-lib.ts b/apps/www/registry/registry-lib.ts index 5b467ec26a2..e6012e45b54 100644 --- a/apps/www/registry/registry-lib.ts +++ b/apps/www/registry/registry-lib.ts @@ -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", diff --git a/apps/www/registry/registry-themes.ts b/apps/www/registry/registry-themes.ts index 9bff2eecec8..6337f1e3d27 100644 --- a/apps/www/registry/registry-themes.ts +++ b/apps/www/registry/registry-themes.ts @@ -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", diff --git a/apps/www/registry/registry-ui.ts b/apps/www/registry/registry-ui.ts index 9e07f43ffec..a644339f4ee 100644 --- a/apps/www/registry/registry-ui.ts +++ b/apps/www/registry/registry-ui.ts @@ -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", diff --git a/apps/www/scripts/build-registry.mts b/apps/www/scripts/build-registry.mts index f645fbc9b58..ace66f32380 100644 --- a/apps/www/scripts/build-registry.mts +++ b/apps/www/scripts/build-registry.mts @@ -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) { +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 = { 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 = { // ---------------------------------------------------------------------------- // 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 = { // ---------------------------------------------------------------------------- // Build registry/styles/[style]/[name].json. // ---------------------------------------------------------------------------- -async function buildStyles(registry: z.infer) { +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) { 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 } diff --git a/packages/shadcn/src/commands/build.ts b/packages/shadcn/src/commands/build.ts new file mode 100644 index 00000000000..760b8287eeb --- /dev/null +++ b/packages/shadcn/src/commands/build.ts @@ -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 ", + "destination directory for json files", + "./public/r" + ) + .option( + "-c, --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) + } + }) diff --git a/packages/shadcn/src/index.ts b/packages/shadcn/src/index.ts index 293e4f7503e..8e17bc87c10 100644 --- a/packages/shadcn/src/index.ts +++ b/packages/shadcn/src/index.ts @@ -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() } diff --git a/packages/shadcn/src/preflights/preflight-build.ts b/packages/shadcn/src/preflights/preflight-build.ts new file mode 100644 index 00000000000..7bc59fb1943 --- /dev/null +++ b/packages/shadcn/src/preflights/preflight-build.ts @@ -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 +) { + const errors: Record = {} + + 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, + } +} diff --git a/packages/shadcn/src/registry/schema.ts b/packages/shadcn/src/registry/schema.ts index d43d29c9520..c6ad858c134 100644 --- a/packages/shadcn/src/registry/schema.ts +++ b/packages/shadcn/src/registry/schema.ts @@ -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 -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 -export const registryIndexSchema = registrySchema +export const registryIndexSchema = z.array(registryItemSchema) export const stylesSchema = z.array( z.object({ diff --git a/packages/shadcn/src/utils/errors.ts b/packages/shadcn/src/utils/errors.ts index c1ff21b3773..8fe1f730c6d 100644 --- a/packages/shadcn/src/utils/errors.ts +++ b/packages/shadcn/src/utils/errors.ts @@ -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"