Read OSS

ソースからエッジへ:バンドルシステムとデプロイパイプライン

中級

前提知識

  • このシリーズの第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-L52COMMON_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 の機能をサポートしているため、モダンな構文をダウンレベルする必要がありません。また、.jsjsx のローダーマッピングにより、.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.jsonexports フィールドの解決方法が決まります。

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:emailcloudflare: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 オブジェクトを構築します。アップロードフォーマットはマルチパートフォームで、以下の要素で構成されます。

  1. metadata — Worker のバインディング、互換性の日付やフラグ、使用モデル、placement、tail consumers、observability 設定、アセット設定を含む JSON blob
  2. メインモジュール — エントリーポイントスクリプト(最初の名前付きパート)
  3. 追加モジュール — WASM ファイル・テキストアセット・サブモジュールなど、それぞれ適切な MIME タイプを持つ別個の名前付きパート
  4. ソースマップ — エラーのスタックトレース対応のためのオプションのソースマップファイル

この関数はバインディングをタイプ別に抽出します。対象は plain_textjsonsecret_textkv_namespacedurable_object_namespacequeuer2_bucketd1 などです。抽出されたバインディングは 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 を同一に解釈することを保証する共有設定システムを解説します。