Read OSS

shadcn/ui のアーキテクチャ:コンポーネント配布システムの仕組み

中級

前提知識

  • TypeScript の基本的な知識
  • npm/pnpm パッケージ管理の経験
  • React コンポーネントライブラリの概念的な理解

shadcn/ui のアーキテクチャ:コンポーネント配布システムの仕組み

多くのコンポーネントライブラリは、npm 経由でコンパイル済みの JavaScript を配布します。パッケージをインストールしてコンポーネントをインポートするだけですが、ライブラリ側のスタイリング・バンドル・API 設計の考え方が自分のプロジェクトと合うかどうかは運次第です。shadcn/ui はこのモデルをまるごと否定します。代わりにソースコードを配布し、コンポーネントファイルをプロジェクトに直接コピーすることで、すべてのコードをあなた自身が所有する形にします。本記事では、それを実現するアーキテクチャを掘り下げます。CLI を支えるレジストリプロトコル、静的な JSON API を生成するビルドパイプライン、そして単一のコードベースから人間・プログラム・AI アシスタントの三者に対応するパッケージ設計です。

「パッケージではなくコピペ」という思想

shadcn/ui の根幹にある考え方は、UI コンポーネントはインフラではないというものです。コンポーネントはあくまでアプリケーションコードであり、特定のプロダクトに合わせて必ず手を加えることになります。コンパイル済みパッケージではなくソースを配布することで、バージョンロック問題を根本から排除します。node_modules/shadcn-button の同期を気にする必要はなくなります。

この思想は独自のアーキテクチャ上の課題を生み出します。システムが担うべき役割は次の4つです。

  1. オーサリング — コンポーネントを正規形式で一元管理する
  2. 変換 — 各プロジェクトの設定(エイリアス、アイコンライブラリ、CSS フレームワーク)に合わせてソースを変換する
  3. 配布 — 変換済みのソースを HTTP API 経由で提供する
  4. インストール — ファイルを正しいディレクトリ構造に書き込む

packages/shadcn/src/index.ts の CLI エントリポイントには、このパイプラインを制御する 11 個のコマンドが登録されています。

flowchart LR
    A[shadcn init] --> B[Configure Project]
    C[shadcn add] --> D[Fetch from Registry]
    D --> E[Transform Source]
    E --> F[Write Files]
    G[shadcn build] --> H[Generate JSON API]

また、CLI は48 行目でレジストリ API をプログラムからも使えるよう再エクスポートしています。小さな実装上の判断ですが、後ほど見るように大きな意味を持ちます。

モノリポの構成とパッケージの境界

このリポジトリは pnpm ワークスペースで管理され、ビルドは Turborepo が制御しています。ルートの package.json にワークスペース構成が定義されています。

ディレクトリ 役割
apps/v4 Next.js ドキュメントサイト + レジストリソース(二役)
packages/shadcn npm に shadcn として公開される CLI パッケージ
packages/tests インテグレーションテストスイート
templates/* 10 種類のスターターテンプレート(next-app, next-monorepo, vite-app, vite-monorepo, react-router-app, react-router-monorepo, start-app, start-monorepo, astro-app, astro-monorepo)
graph TD
    Root["ui (pnpm workspace)"]
    Root --> Apps["apps/"]
    Root --> Packages["packages/"]
    Root --> Templates["templates/"]
    Apps --> V4["v4 - Next.js docs + registry source"]
    Packages --> CLI["shadcn - CLI + registry API"]
    Packages --> Tests["tests - Integration tests"]
    Templates --> Next["next-app / next-monorepo"]
    Templates --> Vite["vite-app / vite-monorepo"]
    Templates --> RR["react-router-app / react-router-monorepo"]
    Templates --> Start["start-app / start-monorepo"]
    Templates --> Astro["astro-app / astro-monorepo"]

turbo.json のパイプライン設定では、build タスクに dependsOn: ["^build"] が指定されています。これにより、CLI パッケージが依存先より先にコンパイルされることが保証されます。また、REGISTRY_URLCOMPONENTS_REGISTRY_URL といった環境変数が明示的に渡されている点にも注目してください。レジストリ URL はビルド時に切り替え可能であり、ローカル開発時は localhost:4000 が本番の ui.shadcn.com エンドポイントの代わりに使われます。

ヒント: ローカルで開発する場合、pnpm shadcn:dev で CLI をウォッチモードで起動し、pnpm v4:dev でドキュメントサイトを起動します。CLI の package.json に定義された start:dev スクリプトが REGISTRY_URL=http://localhost:4000/r を設定するため、CLI はローカルのレジストリからデータを取得します。

複数の用途を持つ CLI パッケージ

shadcn npm パッケージは単なる CLI ではありません。packages/shadcn/package.json には 7 つのサブパスエクスポートと CSS ファイルが定義されています。

エクスポート 役割
shadcn(ルート) CLI エントリポイント + レジストリ API の再エクスポート
shadcn/registry ライブラリ利用者向けのプログラマブルなレジストリ API
shadcn/schema 設定とレジストリアイテム用の Zod スキーマ
shadcn/mcp AI コーディングアシスタント向け MCP サーバー
shadcn/utils スタイル変換とユーティリティ
shadcn/icons アイコンライブラリ定義
shadcn/preset プリセット設定システム
shadcn/tailwind.css ベース Tailwind CSS

tsup.config.ts はツリーシェイキングを有効にした 7 つの ESM エントリポイントを生成します。@antfu/nitinyexec の二つの依存パッケージは noExternal でバンドルに含める設定になっています。これは npx 経由で CLI を実行する際(依存ツリーが不完全な一時インストール環境)の解決失敗を防ぐためです。

classDiagram
    class shadcn {
        +CLI commands
        +registry API re-export
    }
    class registry {
        +getRegistryItems()
        +searchRegistries()
        +resolveRegistryItems()
    }
    class schema {
        +rawConfigSchema
        +registryItemSchema
        +registryItemTypeSchema
    }
    class mcp {
        +MCP Server
        +7 tools
    }
    class utils {
        +transformStyle()
        +createStyleMap()
    }
    shadcn --> registry
    shadcn --> schema
    mcp --> registry
    mcp --> schema
    utils --> schema

この多面的な設計により、同じコードベースから三種類の利用者に対応できます。CLI を使う開発者、shadcn/registry をインポートするプログラム、そして MCP サーバー経由で接続する AI アシスタントです。各インターフェースの詳細は後続の記事で取り上げます。

レジストリアイテムの種別と型階層

shadcn/ui のデータモデルの中心にあるのが「レジストリアイテム」です。コンポーネント・hook・ユーティリティ・設定を記述した JSON ドキュメントで、registryItemTypeSchema では 14 種類の型が定義されています。

classDiagram
    class RegistryItem {
        +name: string
        +type: RegistryItemType
        +files: RegistryItemFile[]
        +dependencies: string[]
        +registryDependencies: string[]
        +cssVars: CssVars
        +css: CssProperties
    }
    class UI["registry:ui"]
    class Lib["registry:lib"]
    class Hook["registry:hook"]
    class Block["registry:block"]
    class Style["registry:style"]
    class Theme["registry:theme"]
    class Base["registry:base"]
    class Font["registry:font"]
    class Page["registry:page"]
    class File["registry:file"]
    RegistryItem <|-- UI
    RegistryItem <|-- Lib
    RegistryItem <|-- Hook
    RegistryItem <|-- Block
    RegistryItem <|-- Style
    RegistryItem <|-- Theme
    RegistryItem <|-- Base
    RegistryItem <|-- Font
    RegistryItem <|-- Page
    RegistryItem <|-- File

スキーマでは type フィールドに Zod の discriminatedUnion が使われています。二つの型は特別扱いされます。registry:base アイテムには config フィールド(rawConfigSchema の一部)が付き、registry:font アイテムにはファミリー・プロバイダー・CSS 変数のメタデータを持つ font フィールドが付きます。これは registryItemSchema に定義されています。

型はファイルの出力先も決定します。registry:uicomponents/ui/registry:hookhooks/registry:liblib/ という具合です。このマッピングはファイル書き込み時に行われます。詳細は第 3 回の記事で解説します。

apps/v4 の二役:ドキュメントとレジストリソース

apps/v4 ディレクトリは Next.js アプリケーションで、二つの役割を担っています。実行時はインタラクティブなコンポーネントプレビューを備えたドキュメントサイト ui.shadcn.com として機能します。ビルド時は ui.shadcn.com/r/ に公開される静的 JSON レジストリのソースとして機能します。

flowchart TD
    A["apps/v4/registry/bases/radix/"] --> B["build-registry.mts"]
    C["apps/v4/registry/bases/base/"] --> B
    D["apps/v4/registry/styles/*.css"] --> B
    B --> E["Style Transforms"]
    E --> F["shadcn build command"]
    F --> G["apps/v4/public/r/*.json"]
    G --> H["ui.shadcn.com/r/"]

コンポーネントの作成者は registry/bases/radix/registry/bases/base/ にソースを書きます。apps/v4/scripts/build-registry.mts のビルドスクリプトが各ベースとスタイルを組み合わせ、変換を適用して静的 JSON ファイルを public/r/ に出力します。これらの JSON ファイルにはコンポーネントのソースコード・依存関係・CSS 変数・メタデータが含まれており、CLI がサーバーサイドの処理なしにコンポーネントをインストールするために必要なすべての情報が揃っています。

components.json による設定

shadcn/ui を使うプロジェクトには、ルートに components.json が必要です。スキーマは rawConfigSchema で定義されています。

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "radix-nova",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "rtl": false,
  "menuAccent": "subtle",
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "registries": {
    "@acme": "https://acme.com/r/{name}.json"
  }
}

注目すべきは registries フィールドです。@acme のような名前空間プレフィックスを、{name}{style} プレースホルダーを含む URL テンプレートにマッピングします。shadcn add @acme/button を実行すると、CLI が URL を解決して JSON を取得し、コンポーネントをインストールします。認証ヘッダーが必要なプライベートレジストリにも対応しています。一方、@shadcn のような組み込みレジストリは constants.ts で定義されており、上書きはできません。

実行時には、get-config.ts が cosmiconfig を使って設定を読み込み、tsconfig-paths で TypeScript パスエイリアスを解決し、組み込みレジストリとユーザー定義レジストリをマージします。絶対パスに解決されたこの設定が、システム全体の処理の基盤となります。

ヒント: getConfig 関数(31 行目)はスタイル名に基づいてアイコンライブラリのデフォルト値を決定します。new-york スタイルは radix アイコン、それ以外は lucide アイコンになります。これは後方互換性のために維持されている挙動です。

次回予告

本記事では、アーキテクチャの基盤を概観しました。明確な境界を持つモノリポ、三者に対応する CLI、npm の従来型配布を置き換えるレジストリプロトコルです。第 2 回では、そのレジストリプロトコルを深掘りします。名前空間のパース、URL の構築、キャッシュ付き HTTP フェッチ、Kahn のトポロジカルソートアルゴリズムが組み合わさって、コンポーネントの依存ツリー全体がどのように解決されるかを見ていきましょう。