mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-26 14:16:08 +00:00
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:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
|
||||
22
apps/www/public/schema/registry.json
Normal file
22
apps/www/public/schema/registry.json
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
97
packages/shadcn/src/commands/build.ts
Normal file
97
packages/shadcn/src/commands/build.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
46
packages/shadcn/src/preflights/preflight-build.ts
Normal file
46
packages/shadcn/src/preflights/preflight-build.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user