Skip to content

Commit

Permalink
feat: add new registry build command (#6350)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shadcn authored Jan 14, 2025
1 parent 254198b commit cb742e9
Show file tree
Hide file tree
Showing 17 changed files with 217 additions and 37 deletions.
2 changes: 1 addition & 1 deletion apps/www/public/schema/registry-item.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": {
Expand Down
22 changes: 22 additions & 0 deletions apps/www/public/schema/registry.json
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 16 additions & 12 deletions apps/www/registry/index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
4 changes: 2 additions & 2 deletions apps/www/registry/registry-blocks.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/www/registry/registry-charts.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/www/registry/registry-examples.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/www/registry/registry-hooks.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/www/registry/registry-internal.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/www/registry/registry-lib.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/www/registry/registry-themes.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/www/registry/registry-ui.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
13 changes: 7 additions & 6 deletions apps/www/scripts/build-registry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from "path"
import template from "lodash/template"
import { rimraf } from "rimraf"
import {
Registry,
registryItemSchema,
registryItemTypeSchema,
registrySchema,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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}/${
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand All @@ -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
}
Expand Down
97 changes: 97 additions & 0 deletions packages/shadcn/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -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)
}
})
2 changes: 2 additions & 0 deletions packages/shadcn/src/index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -27,6 +28,7 @@ async function main() {
.addCommand(diff)
.addCommand(migrate)
.addCommand(info)
.addCommand(build)

program.parse()
}
Expand Down
46 changes: 46 additions & 0 deletions packages/shadcn/src/preflights/preflight-build.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
11 changes: 9 additions & 2 deletions packages/shadcn/src/registry/schema.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions packages/shadcn/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit cb742e9

Please sign in to comment.