Read OSS

构建 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-buttoncn-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-novaradix-vegaradix-maiaradix-lyraradix-miraradix-lumabase-novabase-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 步是整个流水线最核心的部分。针对每种风格组合,流水线会:

  1. 从基座 registry 读取每个组件的源码
  2. 通过 transformStyle 应用样式转换(即第 3 篇介绍的 createStyleMaptransformStyleMap
  3. 将内部 import 路径从 @/registry/bases/<base>/ 重写为 @/registry/<style>/
  4. 使用内容哈希 manifest 缓存结果,跳过未变更的文件
  5. 写入临时的 registry-<style>.json manifest 文件
  6. 调用 shadcn build CLI 命令,生成最终的 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-novaradix-nova 生成(第 133-135 行)。如果你需要 RTL 支持,请选择 Nova 风格作为基础。shouldGenerateRtlStyles 函数是控制这一行为的开关。

下一步

至此,我们已经完整梳理了从组件源码到静态 JSON API 的全生命周期。在第 5 篇中,我们将聚焦另一端——init 命令如何从模板搭建新项目、检测框架、应用预设,以及如何处理 monorepo workspace 路由这一特殊场景。