Read OSS

レジストリのビルド:コンポーネントのソースから静的 JSON API へ

上級

前提知識

  • 第3回:コード変換パイプライン
  • PostCSS と CSS カスタムプロパティの基礎知識
  • ビルドパイプラインとワーカー並列処理の経験

レジストリのビルド:コンポーネントのソースから静的 JSON API へ

第2回・第3回では、レジストリからプロジェクトへとコンポーネントが届くまでの流れを追いました。今回はその逆方向を見ていきましょう。作成されたコンポーネントが、CLI がフェッチする静的 JSON ファイルになるまでにどのような処理が行われるのでしょうか。答えは apps/v4/scripts/build-registry.mts に実装された9ステップのビルドパイプラインです。このパイプラインは2種類のコンポーネントライブラリと6つのビジュアルスタイルを組み合わせ、並列変換を適用したうえで、数百の JSON ファイルを public/r/ に出力します。

デュアルベースアーキテクチャ:Radix vs Base UI

shadcn/ui は Radix UI と Base UI という2つのヘッドレスコンポーネントライブラリをサポートしています。それぞれのレジストリは apps/v4/registry/bases/radix/apps/v4/registry/bases/base/ に独立して管理されており、コンポーネントも別々に作成されています。どちらのレジストリも cn-* というクラス命名規則を使用しています。これがアーキテクチャ上の重要な設計判断であり、同じスタイル定義がどちらのベースでも機能する仕組みの核心です。

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 は、ヘッドレスプリミティブや props のインターフェースが全く異なっていても、cn-buttoncn-button-variant-default といったクラス名を共通して持ちます。ビルドパイプラインがスタイルを適用する際、これらの抽象クラスは具体的な Tailwind ユーティリティへと解決されます。そのため、どちらのヘッドレスライブラリを選んでも同じ見た目が得られます。

スタイル定義と cn-* 抽象化レイヤー

スタイルを定義する CSS ファイルは6つあり、apps/v4/registry/styles/ に格納されています。

ファイル スタイル
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

各ファイルでは、すべてのクラス定義が親セレクター(例:.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-* クラスを異なるユーティリティにマッピングする CSS ファイルを書くだけで、コンポーネントのコードに一切触れることなく全く新しいスタイルを作成できます。

ヒント: カスタムスタイルを作りたい場合は、既存の style-*.css をコピーして @apply の値を書き換えるだけです。cn-* クラス名はコントラクト(契約)であり、すべてのコンポーネントはスタイルマップ上にそれらが存在することを前提としています。

直積演算:Bases × Styles

ビルドスクリプトは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つのスタイルを掛け合わせると、radix-novaradix-vegaradix-maiaradix-lyraradix-miraradix-lumabase-novabase-vega など、合計12通りの組み合わせが生成されます。

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.jsonstyle: "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. ベースレジストリから各コンポーネントのソースを読み込む
  2. 第3回で解説した createStyleMaptransformStyleMap を使用する transformStyle 関数でスタイル変換を適用する
  3. 内部インポートパスを @/registry/bases/<base>/ から @/registry/<style>/ へ書き換える
  4. コンテンツハッシュのマニフェストを使ってキャッシュし、変更のないファイルの処理をスキップする
  5. 一時マニフェスト registry-<style>.json を書き出す
  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行目)はワーカープールパターンを実装しています。limit 個の並列ワーカーを起動し、共有インデックスカウンターからタスクを取り出して処理します。すべてのタスクを一度に起動するのではなく順番に割り当てることで、ファイルシステムやプロセスへの過負荷を防いでいます。

変換キャッシュも重要な仕組みです。スタイルが適用された各ファイルは SHA-256 コンテンツハッシュとともに node_modules/.cache/build-registry/transforms/ にキャッシュされます。マニフェストは style:filepath → hash のマッピングを管理しており、次回以降のビルドではソースまたはスタイル CSS が変更されたファイルだけを再変換します。これにより、インクリメンタルビルドは数分ではなく数秒で完了します。

CLI の build コマンド

packages/shadcn/src/commands/build.ts にある shadcn build コマンドは、registry.json マニフェストを読み込みます。参照された各ファイルの内容を取得したうえで 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 ファイルには、名前・タイプ・依存関係・ファイルの内容・CSS 変数・メタデータなど、レジストリアイテムのすべての情報が含まれます。また、IDE でのバリデーションをサポートするため、CLI は https://ui.shadcn.com/schema/registry-item.json を指す $schema フィールドも付加します。

このコマンドはサードパーティのレジストリ作者も利用できます。registry.json マニフェストを作成してコンポーネントのソースファイルを指定し、shadcn build を実行するだけで、shadcn レジストリと同じ静的 JSON 形式の出力が得られます。特別なインフラは不要で、静的ファイルサーバーであればどこにでもホストできます。

ヒント: RTL スタイルは base-novaradix-nova のみ生成されます(133〜135行目)。RTL サポートが必要な場合は Nova スタイルを選択しましょう。制御しているのは shouldGenerateRtlStyles 関数です。

次回予告

これで、コンポーネントのソースから静的 JSON API に至るまでの完全なライフサイクルを追うことができました。第5回では反対側のエンド、すなわち init コマンドがテンプレートから新しいプロジェクトをスキャフォールドし、フレームワークを検出してプリセットを適用し、モノレポのワークスペースルーティングという特殊なケースをどう処理するかを見ていきます。