构建 Registry:从组件源码到静态 JSON API
前置知识
- ›第 3 篇:代码转换流水线
- ›了解 PostCSS 与 CSS 自定义属性
- ›熟悉构建流水线与并行 worker 机制
构建 Registry:从组件源码到静态 JSON API
在第 2、3 篇中,我们跟随一个组件从 registry 走到了你的项目。现在让我们反过来看:组件的源码是如何变成 CLI 所拉取的静态 JSON 文件的?答案是 apps/v4/scripts/build-registry.mts 中一条包含 9 个步骤的构建流水线——它将两个组件库与六种视觉风格相结合,借助并行转换,最终将数百个 JSON 文件输出到 public/r/。
双基座架构:Radix vs Base UI
shadcn/ui 支持两个 headless 组件库:Radix UI 和 Base UI。两者各有一套完整的 registry,分别存放在 apps/v4/registry/bases/radix/ 和 apps/v4/registry/bases/base/ 中,且均采用相同的 cn-* class 命名规范。这正是整个架构的核心——同一套样式定义可以同时适配两个基座。
classDiagram
class RadixRegistry {
+accordion.tsx (uses @radix-ui/react-accordion)
+button.tsx
+dialog.tsx
+...
}
class BaseRegistry {
+accordion.tsx (uses @base-ui-components/react)
+button.tsx
+dialog.tsx
+...
}
class StyleNova["style-nova.css"]
class StyleVega["style-vega.css"]
StyleNova --> RadixRegistry : cn-* classes
StyleNova --> BaseRegistry : cn-* classes
StyleVega --> RadixRegistry : cn-* classes
StyleVega --> BaseRegistry : cn-* classes
也就是说,Radix 基座的 Button 组件与 Base UI 基座的 Button 组件在实现上可能完全不同——使用了不同的 headless 原语和 prop 接口——但它们共享 cn-button、cn-button-variant-default 等 class 名称。构建流水线在应用样式时,会将这些抽象 class 解析为具体的 Tailwind 工具类,无论你选择了哪个 headless 库,结果都能正常运行。
样式定义与 cn-* 抽象层
apps/v4/registry/styles/ 目录下共有六个样式 CSS 文件:
| 文件 | 风格 |
|---|---|
style-nova.css |
Nova — Lucide / Geist |
style-vega.css |
Vega — Lucide / Inter |
style-maia.css |
Maia — Hugeicons / Figtree |
style-lyra.css |
Lyra — Phosphor / JetBrains Mono |
style-mira.css |
Mira — Hugeicons / Inter |
style-luma.css |
Luma — Lucide / Inter |
每个文件都将所有 class 定义包裹在一个父选择器下(如 .style-nova),并使用 @apply 指令。以下是 style-nova.css 的简化摘录(实际文件中每个选择器包含更多工具类):
.style-nova {
.cn-accordion-item {
@apply not-last:border-b;
}
.cn-accordion-trigger {
@apply focus-visible:ring-ring/50 rounded-lg py-2.5 text-sm font-medium hover:underline;
}
}
正如第 3 篇所介绍的,createStyleMap 函数会解析这段 CSS,构建出 cn-* 到 Tailwind 的映射关系。有了这个抽象层,视觉设计与组件逻辑就彻底解耦了。设计师只需编写一个将每个 cn-* class 映射到不同工具类的 CSS 文件,就能创造出全新的风格,完全无需修改任何组件代码。
提示: 想创建自定义风格,可以复制一个现有的
style-*.css文件,然后修改其中的@apply值。cn-*class 名称是你的"契约"——每个组件都期望这些 class 在样式映射中存在。
笛卡尔积:基座 × 风格
构建脚本在 第 58-65 行 计算所有风格组合的笛卡尔积:
const STYLE_COMBINATIONS = Array.from(BASES).flatMap((base) =>
STYLES.map((style) => ({
base,
style,
name: `${base.name}-${style.name}`,
title: `${base.title} ${style.title}`,
}))
)
2 个基座 × 6 种风格,共产生 12 种组合:radix-nova、radix-vega、radix-maia、radix-lyra、radix-mira、radix-luma、base-nova、base-vega 等。
graph LR
subgraph Bases
R[radix]
B[base]
end
subgraph Styles
N[nova]
V[vega]
M[maia]
L[lyra]
Mi[mira]
Lu[luma]
end
subgraph "Output (12 combinations)"
RN[radix-nova]
RV[radix-vega]
BN[base-nova]
BV[base-vega]
More[...]
end
R --> RN
R --> RV
R --> More
B --> BN
B --> BV
B --> More
N --> RN
N --> BN
V --> RV
V --> BV
每种组合都会在 public/r/styles/ 下拥有独立的目录。当用户的 components.json 中配置了 style: "radix-nova" 时,CLI 便会从 https://ui.shadcn.com/r/styles/radix-nova/button.json 拉取对应文件。
9 步构建流水线
主流水线位于 第 309-368 行,依次执行以下步骤:
flowchart TD
S1["1. Build bases/__index__.tsx"] --> S2["2. Build base registries"]
S2 --> S3["3. Build registry/__index__.tsx"]
S3 --> S4["4. Build examples/__index__.tsx"]
S4 --> S5["5. Build styled JSON + CLI export per style"]
S5 --> S6["6. Build blocks index"]
S6 --> S7["7. Build config, index, registries, colors"]
S7 --> S8["8. Copy UI to styles + build RTL"]
S8 --> S9["9. Clean up temporaries"]
第 5 步是整个流水线最核心的部分。针对每种风格组合,流水线会:
- 从基座 registry 读取每个组件的源码
- 通过
transformStyle应用样式转换(即第 3 篇介绍的createStyleMap和transformStyleMap) - 将内部 import 路径从
@/registry/bases/<base>/重写为@/registry/<style>/ - 使用内容哈希 manifest 缓存结果,跳过未变更的文件
- 写入临时的
registry-<style>.jsonmanifest 文件 - 调用
shadcn buildCLI 命令,生成最终的 JSON 文件
第 67-75 行 的并发配置根据可用 CPU 核心数进行了调优:
const CPU_COUNT = availableParallelism()
const STYLE_BUILD_CONCURRENCY = Math.max(1, Math.min(CPU_COUNT, 4))
const FILE_BUILD_CONCURRENCY = Math.max(4, Math.min(CPU_COUNT, 8))
const CLI_BUILD_CONCURRENCY = Math.max(1, Math.min(Math.floor(CPU_COUNT / 2), 4))
通用的 runWithConcurrency 函数(第 285-307 行)实现了 worker 池模式:它启动 limit 个并发 worker,从共享的索引计数器中依次取任务执行。这样可以避免一次性启动所有任务,防止文件系统或进程数量被压垮。
转换缓存机制同样值得关注。每个经过样式处理的文件都会以 SHA-256 内容哈希为 key,缓存到 node_modules/.cache/build-registry/transforms/,manifest 记录了 style:filepath → hash 的映射关系。在后续构建中,只有源码或样式 CSS 发生变化的文件才需要重新转换。这让增量构建的速度大幅提升——从几分钟缩短到几秒钟。
CLI build 命令
packages/shadcn/src/commands/build.ts 中的 shadcn build 命令会读取 registry.json manifest,读取每个引用文件的内容,用 registryItemSchema 进行校验,并将独立的 JSON 文件写入输出目录。
sequenceDiagram
participant Script as build-registry.mts
participant CLI as shadcn build
participant FS as File System
Script->>FS: Write registry-radix-nova.json
Script->>CLI: shadcn build registry-radix-nova.json -o public/r/styles/radix-nova
CLI->>FS: Read registry-radix-nova.json
CLI->>CLI: Validate with registrySchema
loop For each item
CLI->>FS: Read source file content
CLI->>CLI: Validate with registryItemSchema
CLI->>FS: Write button.json, card.json, etc.
end
CLI->>FS: Copy registry.json to output
每个输出的 JSON 文件都包含完整的 registry 条目:名称、类型、依赖、文件内容、CSS 变量以及元数据。CLI 还会添加一个 $schema 字段,指向 https://ui.shadcn.com/schema/registry-item.json,以便 IDE 进行校验。
这个命令同样对第三方 registry 作者开放。你只需编写一个 registry.json manifest,将文件路径指向你的组件源码,然后运行 shadcn build,就能生成与 shadcn registry 完全相同的静态 JSON 格式。无需任何特殊基础设施,输出文件可以托管在任意静态文件服务器上。
提示: RTL 样式仅为
base-nova和radix-nova生成(第 133-135 行)。如果你需要 RTL 支持,请选择 Nova 风格作为基础。shouldGenerateRtlStyles函数是控制这一行为的开关。
下一步
至此,我们已经完整梳理了从组件源码到静态 JSON API 的全生命周期。在第 5 篇中,我们将聚焦另一端——init 命令如何从模板搭建新项目、检测框架、应用预设,以及如何处理 monorepo workspace 路由这一特殊场景。