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つです。
- オーサリング — コンポーネントを正規形式で一元管理する
- 変換 — 各プロジェクトの設定(エイリアス、アイコンライブラリ、CSS フレームワーク)に合わせてソースを変換する
- 配布 — 変換済みのソースを HTTP API 経由で提供する
- インストール — ファイルを正しいディレクトリ構造に書き込む
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_URL や COMPONENTS_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/ni と tinyexec の二つの依存パッケージは 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:ui → components/ui/、registry:hook → hooks/、registry:lib → lib/ という具合です。このマッピングはファイル書き込み時に行われます。詳細は第 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 のトポロジカルソートアルゴリズムが組み合わさって、コンポーネントの依存ツリー全体がどのように解決されるかを見ていきましょう。