ソースからエッジへ:バンドルシステムとデプロイパイプライン
前提知識
- ›このシリーズの第1回・第2回
- ›esbuild の基礎知識(plugins・conditions・targets)
- ›Cloudflare Workers アップロード API への理解
ソースからエッジへ:バンドルシステムとデプロイパイプライン
wrangler deploy を実行しても、TypeScript Worker のソースコードが直接 Cloudflare のネットワークに届くわけではありません。ソースはまず esbuild によって変換・バンドルされ、カスタム plugin が Workers 固有のモジュールを解決し、モジュールコレクターが出力を MIME タイプごとに分類します。最終的には、バインディング・互換性設定・デプロイ設定を含むメタデータ blob とともに、マルチパートの FormData アップロードとしてパッケージされます。
この記事では、そのパイプラインのすべてのステップを順に追っていきます。Workers のバンドルをブラウザ向けバンドルと異なるものにする esbuild の設定の選択から、5つのカスタム esbuild plugin、そしてアップロード前にリモートの設定差分を処理する deploy() 関数まで、一つひとつ解説します。
bundleWorker() vs noBundleWorker()
Wrangler には2つのバンドルモードがあります。主要なパスは packages/wrangler/src/deployment-bundle/bundle.ts#L149 にある bundleWorker() で、フルの plugin パイプラインを使って esbuild を呼び出します。もう一方の noBundleWorker()(packages/wrangler/src/deployment-bundle/no-bundle-worker.ts#L9-L32)は esbuild を完全にスキップし、追加モジュールのスキャンのみを行います。
noBundleWorker() は、ユーザーが --no-bundle を指定した場合に呼び出されます。Worker が別ツールでバンドル済みの場合や、変換不要な単一ファイルの Worker である場合に典型的に使われます。この場合でも findAdditionalModules() は実行され、アップロードに含める必要のある WASM ファイル・テキストアセット・その他のモジュールタイプが検出されます。
共通の esbuild 設定は、packages/wrangler/src/deployment-bundle/bundle.ts#L46-L52 で COMMON_ESBUILD_OPTIONS として定義されています。
export const COMMON_ESBUILD_OPTIONS = {
target: "es2024",
supported: { "import-source": true },
loader: { ".js": "jsx", ".mjs": "jsx", ".cjs": "jsx" },
} as const;
target: "es2024" は注目すべき設定です。workerd の V8 は ES2024 の機能をサポートしているため、モダンな構文をダウンレベルする必要がありません。また、.js → jsx のローダーマッピングにより、.jsx 拡張子を使わなくても通常の .js ファイルで JSX が使えます。これは Workers エコシステムの慣例に合わせたものです。
flowchart TD
SRC["Worker source (TS/JS)"] --> DECISION{--no-bundle?}
DECISION -->|No| BUNDLE["bundleWorker()<br/>Full esbuild pipeline"]
DECISION -->|Yes| NOBUNDLE["noBundleWorker()<br/>Module scan only"]
BUNDLE --> RESULT["BundleResult<br/>modules + dependencies + sourcemap"]
NOBUNDLE --> RESULT
Workers Build Conditions:workerd・worker・browser
packages/wrangler/src/deployment-bundle/bundle.ts#L69-L76 にある getBuildConditions() 関数は、esbuild の conditions を設定します。これにより package.json の exports フィールドの解決方法が決まります。
export function getBuildConditions() {
const envVar = getBuildConditionsFromEnv();
if (envVar !== undefined) {
return envVar.split(",");
} else {
return ["workerd", "worker", "browser"];
}
}
順序には意味があります。ライブラリの package.json が conditional exports を使っている場合を見てみましょう。
{
"exports": {
".": {
"workerd": "./dist/workerd.js",
"worker": "./dist/worker.js",
"browser": "./dist/browser.js",
"default": "./dist/node.js"
}
}
}
esbuild は workerd(最も具体的な Cloudflare ランタイムターゲット)を優先し、次に worker(汎用 Web Worker)、browser、そして default の順に解決します。これにより、workerd ランタイム向けに最適化されたコードパスを持つライブラリが優先的に使用されます。
| Condition | 用途 |
|---|---|
workerd |
Cloudflare の Workers ランタイム — 最も具体的 |
worker |
汎用 Web Worker 標準 |
browser |
ブラウザ環境(isomorphic コードのフォールバック) |
default |
常に有効(esbuild 組み込み) |
import |
ESM 構文で有効(esbuild 組み込み) |
ヒント:
WRANGLER_BUILD_CONDITIONS環境変数を使って conditions を上書きできます。異なるライブラリのエクスポートパスで Worker の動作をテストする際に便利です。
カスタム esbuild Plugins
bundleWorker() 関数はいくつかのカスタム esbuild plugin を登録します。中でも特に注目すべき3つを見ていきましょう。
Node.js 互換 Plugins
packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts モジュールは、unenv パッケージを通じて Node.js API の互換 shim を提供します。nodejs_compat または nodejs_compat_v2 の互換フラグが有効な場合、これらの plugin が node:* 組み込みモジュールのインポートをインターセプトします。その上で Workers ランタイムで動作する polyfill 実装へリダイレクトします。
Cloudflare 内部モジュール解決
packages/wrangler/src/deployment-bundle/esbuild-plugins/cloudflare-internal.ts plugin は cloudflare:* 仮想モジュールと workerd:* インポートの解決を担います。cloudflare:email・cloudflare:sockets などのランタイム提供モジュールを external としてマークし、esbuild によるバンドルを防ぎます。
Config Provider Plugin
packages/wrangler/src/deployment-bundle/esbuild-plugins/config-provider.ts はビルド時に wrangler の設定値を注入し、ランタイムオーバーヘッドなしにコンパイル時に設定へアクセスできるようにします。
flowchart LR
ESBUILD["esbuild"] --> NODEJS["nodejs-plugins<br/>Node.js polyfills via unenv"]
ESBUILD --> CF["cloudflare-internal<br/>cloudflare:* & workerd:* modules"]
ESBUILD --> CP["config-provider<br/>Build-time config injection"]
ESBUILD --> BR["buildResultPlugin<br/>Captures initial build result"]
ESBUILD --> LOG["log-build-output<br/>Bundle size reporting"]
bundleWorker() 関数は開発モード向けに middleware ローダーも注入します。開発時には、リクエストボディのドレイン・スケジュールイベントのテスト・JSON エラーフォーマット(Miniflare でのエラーページ表示用)のための middleware で Worker がラップされます。
モジュール収集と MIME タイプマッピング
esbuild が出力を生成した後、モジュールコレクターが各出力ファイルをタイプ別に分類します。packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts#L22-L32 の MIME タイプマッピングがその対応を定義しています。
| モジュールタイプ | MIME タイプ |
|---|---|
esm |
application/javascript+module |
commonjs |
application/javascript |
compiled-wasm |
application/wasm |
buffer |
application/octet-stream |
text |
text/plain |
python |
text/x-python |
python-requirement |
text/x-python-requirement |
このマッピングは非常に重要です。Cloudflare の Workers API は、マルチパートアップロードの各パートの Content-Type ヘッダーを使ってモジュールの処理方法を決定するからです。誤った MIME タイプで送信された WASM ファイルは、ランタイムでのロードに失敗します。
createModuleCollector() 関数は esbuild plugin として動作します。esbuild のモジュール解決をインターセプトしてバンドルに含まれるファイルを追跡・分類し、アップロードフォームの構築前にその情報を整理します。
createWorkerUploadForm():マルチパートアップロードの構築
createWorkerUploadForm() 関数は、Cloudflare の API に送信する FormData オブジェクトを構築します。アップロードフォーマットはマルチパートフォームで、以下の要素で構成されます。
metadata— Worker のバインディング、互換性の日付やフラグ、使用モデル、placement、tail consumers、observability 設定、アセット設定を含む JSON blob- メインモジュール — エントリーポイントスクリプト(最初の名前付きパート)
- 追加モジュール — WASM ファイル・テキストアセット・サブモジュールなど、それぞれ適切な MIME タイプを持つ別個の名前付きパート
- ソースマップ — エラーのスタックトレース対応のためのオプションのソースマップファイル
この関数はバインディングをタイプ別に抽出します。対象は plain_text・json・secret_text・kv_namespace・durable_object_namespace・queue・r2_bucket・d1 などです。抽出されたバインディングは packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts#L115-L134 の API が期待するメタデータフォーマットへ変換されます。
flowchart TD
WORKER["Worker init data"] --> FORM["createWorkerUploadForm()"]
BINDINGS["Bindings"] --> FORM
FORM --> META["metadata (JSON):<br/>bindings, compat date,<br/>placement, limits"]
FORM --> MAIN["main module:<br/>application/javascript+module"]
FORM --> MODS["additional modules:<br/>WASM, text, etc."]
FORM --> SMAP["source maps"]
META --> FD["FormData"]
MAIN --> FD
MODS --> FD
SMAP --> FD
deploy() 関数:エンドツーエンドのフロー
packages/wrangler/src/deploy/deploy.ts のデプロイフロー全体は、すべてのステップを統括しています。
flowchart TD
START["deploy(props)"] --> CHECK["Check if worker exists"]
CHECK --> DIFF["Fetch remote config<br/>Compare with local"]
DIFF --> WARN{Destructive changes?}
WARN -->|Yes| CONFIRM["Prompt user to confirm"]
WARN -->|No| BUNDLE
CONFIRM --> BUNDLE["Bundle worker<br/>(bundleWorker or noBundleWorker)"]
BUNDLE --> FORM["createWorkerUploadForm()"]
FORM --> UPLOAD["Upload to Workers API"]
UPLOAD --> ROUTES["Publish routes/custom domains"]
ROUTES --> QUEUES["Reconcile queue consumers"]
QUEUES --> VERSION["Create version/deployment"]
VERSION --> DONE["Log success + URL"]
packages/wrangler/src/deploy/deploy.ts#L448-L460 の設定差分ステップは安全装置として機能します。Worker が最後に Wrangler ではなく Cloudflare ダッシュボードからデプロイされた場合、この関数はリモートの設定を取得してローカルの設定と比較します。ローカルのデプロイがダッシュボードでの手動編集を上書きする(破壊的な変更)場合、ユーザーに警告が表示され、確認を求められます。
packages/wrangler/src/deploy/index.ts#L36-L42 のコマンド定義自体は、第2回で紹介した createCommand() パターンを使っています。
export const deployCommand = createCommand({
metadata: {
description: "🆙 Deploy a Worker to Cloudflare",
owner: "Workers: Deploy and Config",
status: "stable",
category: "Compute & AI",
},
// ...
});
ヒント:
wrangler deployの--dry-runフラグを使うと、バンドルパイプライン全体を実行してアップロードフォームを構築しますが、実際には送信しません。デプロイせずに Worker が正しくバンドルされることを検証したい CI パイプラインで非常に役立ちます。
次回に向けて
ここまで、ソースコードから Cloudflare のエッジまでのパスを追ってきました。しかし、Wrangler をまったく経由しないローカル開発のための別のパスも存在します。@cloudflare/vite-plugin は、同じ Miniflare ランタイムコアを使いながら、まったく異なるオーケストレーションレイヤーを通じてファーストクラスの Vite 統合を提供します。第6回では、この Vite plugin と、両ツールが wrangler.toml を同一に解釈することを保証する共有設定システムを解説します。