Read OSS

Cloudflare Workers SDK を読み解く:アーキテクチャとコードベース全体像

中級

前提知識

  • モノレポの基本的な概念を理解していること
  • pnpm / npm workspaces の基礎知識があること
  • Turborepo などのビルドオーケストレーターを触ったことがあること

Cloudflare Workers SDK を読み解く:アーキテクチャとコードベース全体像

workers-sdk モノレポは、Cloudflare のデベロッパーツール群の中核をなすリポジトリです。Wrangler(CLI)、Miniflare(ローカル Workers ランタイムシミュレーター)、Vite plugin、プロジェクトスキャフォールディングツールの C3、そして多数のサポートパッケージが含まれています。これらは pnpm workspaces と Turborepo で管理されたひとつのリポジトリに収まっています。wrangler devnpx create-cloudflare を実行したことがあるなら、あなたはすでにこのコードを動かしていたことになります。

この記事はそのための「地図」です。コマンドパース、dev サーバーのオーケストレーション、バンドルパイプラインといったテーマを後の記事で掘り下げる前に、まずは各パーツがどう組み合わさっているか、どのパッケージが何に依存しているか、そしてチームがなぜあえて一風変わったパッケージング設計を選んだのかを把握しておきましょう。

モノレポのレイアウトとパッケージ数

リポジトリは目的ごとに明確に分けられた、3 つのトップレベルディレクトリで構成されています。

ディレクトリ 用途 概数
packages/ 公開 npm パッケージおよび内部ライブラリ 約 30 パッケージ
fixtures/ 統合テスト用プロジェクトとサンプルアプリ 約 77 fixtures
tools/ 内部ビルドユーティリティとスクリプト ワークスペースルート 1 つ

ワークスペースの設定は pnpm-workspace.yaml で定義されており、Vite plugin の playground が独立したワークスペースルートとして含まれています。

packages:
  - "packages/*"
  - "packages/vite-plugin-cloudflare/playground/*"
  - "packages/vite-plugin-cloudflare/playground"
  - "fixtures/*"
  - "tools"

packages/ ディレクトリには wranglerminiflarevite-plugin-cloudflarecreate-cloudflare といった主役パッケージが並んでいます。それだけでなく、workers-shared(Cloudflare のエッジで動くアセットワーカー)、workers-utils(共有設定パース)、cli(インタラクティブ CLI フレームワーク)などの内部パッケージも含まれています。fixtures はエンドツーエンドテスト用の Worker プロジェクト群で、それぞれ独自の package.json を持ち、場合によっては turbo.json のオーバーライドも備えています。

graph TD
    ROOT["workers-sdk root"]
    ROOT --> PKG["packages/ (~30)"]
    ROOT --> FIX["fixtures/ (~77)"]
    ROOT --> TOOLS["tools/"]
    PKG --> WRANGLER["wrangler"]
    PKG --> MF["miniflare"]
    PKG --> VITE["vite-plugin-cloudflare"]
    PKG --> C3["create-cloudflare"]
    PKG --> UTILS["workers-utils"]
    PKG --> SHARED["workers-shared"]
    PKG --> CLI["cli"]

コアパッケージの依存グラフ

各パッケージは明確な階層構造を持つ有向依存グラフを形成しています。このグラフを理解することは非常に重要です。ビルド順序を決定し、変更の影響範囲を左右し、特定のアーキテクチャ上の境界がなぜ存在するかを説明してくれるからです。

グラフの最下層に位置するのが workerd — Cloudflare のオープンソース Workers ランタイムで、ネイティブバイナリとして配布されています。Miniflare はその workerd を Node.js フレンドリーな API でラップし、ライフサイクルを管理します。Wrangler はローカル開発のために Miniflare に依存し、Vite plugin も Miniflare に依存しますが、まったく異なる統合経路をたどります(第 6 回の記事で詳しく取り上げます)。

flowchart BT
    WORKERD["workerd (native binary)"] --> MF["miniflare"]
    MF --> WRANGLER["wrangler"]
    MF --> VITE["vite-plugin-cloudflare"]
    UTILS["workers-utils"] --> WRANGLER
    UTILS --> VITE
    UTILS --> C3["create-cloudflare"]
    WRANGLER --> VITEST["vitest-pool-workers"]

これらの依存関係は各パッケージの package.json で確認できます。Wrangler のランタイム依存は packages/wrangler/package.json#L67-L76 に記載されており、miniflareworkspace:* として指定されています。Miniflare の依存は packages/miniflare/package.json#L50-L57 で確認でき、workerd が特定の互換性日付バージョンに固定されています。

この階層化は単なる整理整頓ではありません。関心の分離を強制するための設計です。Miniflare は CLI 引数のパースを一切知りません。Wrangler は Cap'n Proto による設定シリアライズを知りません。workers-utils パッケージが設定パースを担い、Wrangler と Vite plugin の両方がそれを使うことで、wrangler.toml の解釈がどちらのツールでも一致することを保証しています。

workers-utils 共有パッケージ

@cloudflare/workers-utils パッケージは、設定のパースと検証における唯一の信頼できる情報源です。Wrangler と Vite plugin の両方がここからインポートしているため、wrangler.tomlwrangler.jsonwrangler.jsonc のいずれも、どのツールが読み込んでも同じように解釈されます。

設定のエントリポイント packages/workers-utils/src/config/index.ts は、拡張子でファイル形式を判別する configFormat() 関数と、すべての設定データの形を定義した正規化済みの Config 型・RawConfig 型をエクスポートしています。

flowchart LR
    TOML["wrangler.toml"] --> PARSE["workers-utils config parser"]
    JSON["wrangler.json"] --> PARSE
    JSONC["wrangler.jsonc"] --> PARSE
    PARSE --> CONFIG["Normalized Config"]
    CONFIG --> WRANGLER["Wrangler readConfig()"]
    CONFIG --> VITE["Vite plugin"]

この設計により、新しい設定フィールドを追加する際の変更箇所は常に一か所で済みます。また、廃止予定フィールドへの警告、未知のキーの検出、環境継承に関するバリデーション診断も、すべてのコンシューマーで一貫して機能します。

ヒント: コードベースを読んでいて @cloudflare/workers-utils からの config 関連インポートを見かけたら、それは共有レイヤーです。../config からのインポートであれば、.env の読み込みやアップデートチェックなど CLI 固有の動作を追加した Wrangler のラッパーを見ています。

依存パッケージのバンドル戦略

ここで workers-sdk は一般的な npm パッケージングから大きく外れた選択をしています。Wrangler の package.json を見ると、ランタイム依存がたった 8 件 しかないのに対し、devDependencies は 90 件以上 あります。これは偶然ではありません。

packages/wrangler/package.json#L67-L76 に記載されている 8 つのランタイム依存は次のとおりです。

依存パッケージ ランタイム依存である理由
miniflare ワークスペース依存。独自のネイティブ依存を持つ
workerd ネイティブバイナリ — バンドル不可
esbuild ネイティブバイナリ — バンドル不可
blake3-wasm ランタイムで解決が必要な WASM モジュール
unenv ポリフィルパスのために require.resolve が必要
@cloudflare/unenv-preset unenv のコンパニオンパッケージ
@cloudflare/kv-asset-handler ワークスペース依存
path-to-regexp ランタイム依存

それ以外の chalkyargsundicichokidarwsprompts など多数のパッケージは devDependencies として管理され、ビルド時に Wrangler の出力へバンドルされます。ビルドには tsup(esbuild のラッパー)を使い、自己完結したバンドルを生成します。

なぜこの設計なのでしょうか。エンドユーザーへの依存チェーン汚染を防ぐためです。npm install wrangler を実行したとき、ユーザーが受け取るのは Wrangler のコードと、node_modules に独立したエントリとして必ず存在しなければならないパッケージ(ネイティブバイナリ、WASM、require.resolve が必要なパッケージ)だけです。90 以上のパッケージの間接的な依存ツリーを引き継ぐことがなく、バージョン競合やサプライチェーンリスクを最小化できます。

flowchart LR
    DEV["~90 devDependencies"] -->|"bundled by tsup"| DIST["wrangler-dist/cli.js"]
    RT["8 runtime dependencies"] -->|"installed normally"| NM["node_modules/"]
    DIST --> USER["End user"]
    NM --> USER

pnpm Catalog によるバージョン固定

モノレポ全体では、重要な依存パッケージのバージョンを統一するために pnpm の catalog: プロトコルを活用しています。pnpm-workspace.yaml#L18-L48 で定義されたこの仕組みは、一元管理されたバージョンレジストリとして機能します。

主な固定バージョンは次のとおりです。

パッケージ バージョン 用途
workerd 1.20260317.1 Workers ランタイムバイナリ — 全パッケージで一致必須
esbuild 0.27.3 バンドラーバージョンの統一
vitest 4.1.0 テストランナーのバージョン
vite ^8.0.0 Vite フレームワークのバージョン
typescript ~5.8.3 コンパイラのバージョン
undici 7.24.4 HTTP クライアント。undici-types も合わせて固定

package.json"workerd": "catalog:default" と書くと、pnpm は catalog に宣言されたバージョンに解決します。モノレポ内の 2 つのパッケージが異なる workerd バージョンを実行して互換性の問題が起きる、という悪夢を防ぐための仕組みです。

ヒント: catalog には、@cloudflare/vitest-pool-workers がなぜ workspace:* ではなく catalog バージョンを使うのかを説明するコメントも記載されています。vitest-pool-workers に 含まれる パッケージが、それ自身で テストされる 必要もあるため、循環依存を避けるためにこの方式が採用されています。

Turborepo のタスクグラフ

Turborepo はモノレポ全体のビルド、テスト、型チェックをオーケストレーションします。turbo.json にタスクの依存グラフが定義されています。

flowchart TD
    BUILD["build"] -->|"^build (topological)"| BUILD
    TEST["test"] -->|"depends on"| BUILD
    TESTCI["test:ci"] -->|"depends on"| BUILD
    TESTE2E["test:e2e"] -->|"depends on"| BUILD
    CHECKTYPE["check:type"] -->|"depends on"| BUILD
    DEV["dev"] -.->|"persistent, no cache"| DEV

dependsOn に書かれた "^build" という構文は、Turborepo のトポロジカル依存マーカーです。「パッケージ X をビルドする前に、X が依存するすべてのパッケージを先にビルドする」という意味です。これにより、Wrangler のビルド時には Miniflare と workers-utils が必ず先にビルドされていることが保証されます。

テストタスクが依存するのは ^build ではなく build です。自分自身のパッケージさえビルドされていればよく、依存パッケージの再ビルドは不要(すでにビルド済みと見なす)だからです。dev タスクは persistent: true かつ cache: false に設定されており、長時間実行される watch プロセスとして適切な設定です。

globalPassThroughEnv には、キャッシュキーに影響を与えずに Turborepo が素通しすべき環境変数が列挙されています。CI トークン、Docker 設定、WRANGLER_LOGCLOUDFLARE_API_TOKEN といった Wrangler 固有の環境変数などが含まれます。

次回予告

全体像が把握できたところで、いよいよ最初のサブシステムに踏み込んでいきましょう。次回は、Wrangler がシェルのエントリポイントから起動して宣言的なコマンド登録システムに至るまでの流れを追います。このシステムは yargs の上に構築されたカスタムレイヤーで、TypeScript のジェネリクスを活用してランタイムオーバーヘッドゼロの型安全性を実現しています。