Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new registry build command #6350

Merged
merged 16 commits into from
Jan 14, 2025
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"
Loading