Read OSS

Vite プラグインと設定システム:2つのインターフェース、1つのランタイム

中級

前提知識

  • 本シリーズの Article 1(モノレポアーキテクチャ)と Article 4(Miniflare)
  • Vite プラグイン API および configureServer フックの基本知識
  • wrangler.toml / wrangler.json 設定フォーマットの基本的な理解

Vite プラグインと設定システム:2つのインターフェース、1つのランタイム

@cloudflare/vite-plugin は、ひとつの根本的なアーキテクチャ上の選択を体現しています。Wrangler をサブプロセスとしてラップするのではなく、Miniflare への独立した統合パスを作り出すという選択です。同じ設定ファイルを読み込み、同じ workerd ランタイムを使いながらも、Article 3 で見た DevEnv コントローラーバスではなく、Vite のプラグインフックを通じてすべてを調整します。

この記事では、Vite インテグレーションを構成する 16 個のサブプラグインを解説します。dev プラグインが独自の Miniflare インスタンスを作成する仕組みや、Wrangler と Vite プラグインの唯一の設定ソースとなる @cloudflare/workers-utils の設定レイヤーについても掘り下げます。

cloudflare() が返す 16 個のサブプラグイン

packages/vite-plugin-cloudflare/src/index.ts#L47-L94 にある cloudflare() ファクトリ関数は、16 個の Vite プラグインの配列を返します。

export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
    const ctx = new PluginContext(sharedContext);
    return [
        { name: "vite-plugin-cloudflare", /* root plugin */ },
        configPlugin(ctx),
        rscPlugin(ctx),
        devPlugin(ctx),
        previewPlugin(ctx),
        shortcutsPlugin(ctx),
        debugPlugin(ctx),
        cdnCgiPlugin(ctx),
        virtualModulesPlugin(ctx),
        virtualClientFallbackPlugin(ctx),
        outputConfigPlugin(ctx),
        wasmHelperPlugin(ctx),
        additionalModulesPlugin(ctx),
        nodeJsAlsPlugin(ctx),
        nodeJsCompatPlugin(ctx),
        nodeJsCompatWarningsPlugin(ctx),
    ];
}

なぜ 1 つではなく 16 個のプラグインに分かれているのでしょうか。Vite のプラグインシステムはコンポジションを中心に設計されており、各プラグインが特定の関心事を担い、Vite がフックのライフサイクルを通じてそれらを調整します。機能を分割することで、次のような利点が生まれます。

プラグイン 役割
ルートプラグイン プラグイン設定の解決、server.restart のパッチ適用
configPlugin Worker 設定の読み込みと検証
devPlugin Miniflare インスタンスとモジュールランナーの作成
rscPlugin React Server Components サポート
previewPlugin vite preview モードの処理
shortcutsPlugin キーボードショートカット(b、o、q)
debugPlugin デバッグロギングのサポート
cdnCgiPlugin cdn-cgi パスルーティングの処理
virtualModulesPlugin 仮想モジュールのインポート解決
virtualClientFallbackPlugin 仮想モジュールのクライアントサイドフォールバック
outputConfigPlugin 解決済み設定のディスクへの書き込み
wasmHelperPlugin WASM モジュール読み込みのサポート
additionalModulesPlugin JS 以外のモジュールインポートの処理
nodeJsAlsPlugin AsyncLocalStorage 互換性
nodeJsCompatPlugin Node.js API シム
nodeJsCompatWarningsPlugin 未サポートの Node.js API に関する警告

すべてのプラグインは PluginContext インスタンス(ctx)を共有しており、これがプラグインのライフサイクルをまたいだ共有のミュータブルな状態として機能します。コンテキストには、解決済みのプラグイン設定、Miniflare インスタンス、isRestartingDevServer などのライフサイクル状態が格納されています。

flowchart TD
    CF["cloudflare()"] --> CTX["PluginContext"]
    CTX --> ROOT["Root plugin"]
    CTX --> CONFIG["configPlugin"]
    CTX --> DEV["devPlugin"]
    CTX --> RSC["rscPlugin"]
    CTX --> PREVIEW["previewPlugin"]
    CTX --> SHORT["shortcutsPlugin"]
    CTX --> VIRTUAL["virtualModulesPlugin"]
    CTX --> WASM["wasmHelperPlugin"]
    CTX --> NODE["nodeJsCompatPlugin"]
    CTX --> MORE["...3 more plugins"]

devPlugin:独立した Miniflare インスタンス

packages/vite-plugin-cloudflare/src/plugins/dev.ts#L40 にある devPlugin は、アーキテクチャ上で最も興味深い決断が表れている場所です。configureServer フックの中で次の処理を行います。

  1. getDevMiniflareOptions(ctx, viteDevServer) を呼び出して Miniflare の設定を計算する
  2. ctx.startOrUpdateMiniflare() を通じて Miniflare インスタンスを作成する
  3. Worker コード用の Vite モジュールランナーを初期化する
  4. Vite の dev サーバーと Miniflare の間で WebSocket プロキシを設定する

この Miniflare インスタンスは、wrangler dev が使う DevEnv/LocalRuntimeController パイプラインとは完全に独立しています。DevEnv もなく、コントローラーバスもなく、BundlerController もありません。Vite プラグインは esbuild の代わりに Vite 自身のモジュール変換パイプラインを使い、カスタムのファイルウォッチャーの代わりに Vite の HMR システムを使います。

74〜79 行目のモジュールランナーの初期化こそが、Vite と Miniflare が接続される場所です。

if (ctx.resolvedPluginConfig.type === "workers") {
    debuglog("Initializing the Vite module runners");
    await initRunners(
        ctx.resolvedPluginConfig,
        viteDevServer,
        ctx.miniflare
    );

Vite モジュールランナーを使うことで、Worker コードを Vite の変換パイプライン(TypeScript のコンパイルや HMR サポートを含む)を通じてロードしながら、Miniflare の workerd ランタイム上で実行することができます。これは比較的新しい Vite の機能で、サーバーサイドのコードでも Vite の開発体験を享受できるようにするものです。

ヒント: ローカル開発で wrangler dev と Vite プラグインのどちらを選ぶかを検討する際、決め手となるのはバンドルの違いです。wrangler dev は esbuild を使い、Vite プラグインは Vite の Rollup ベースのパイプラインを使います。すでに Vite を使っているプロジェクトであれば、プラグインを選ぶことで HMR が速くなり、Vite エコシステムとの統合も深まります。

並べて比較:wrangler dev と Vite プラグイン

アーキテクチャを比較すると、同じ目標に対する 2 つのまったく異なるアプローチが見えてきます。

flowchart TD
    subgraph "wrangler dev"
        WC["ConfigController"] -->|configUpdate| WB["BundlerController<br/>(esbuild)"]
        WB -->|bundleComplete| WR["LocalRuntimeController"]
        WR -->|"manages"| WMF["Miniflare instance A"]
        WR -->|reloadComplete| WP["ProxyController"]
        WP -->|"manages"| WPM["Miniflare instance B<br/>(proxy)"]
    end
    subgraph "Vite plugin"
        VC["configPlugin<br/>(reads wrangler config)"] --> VD["devPlugin"]
        VD -->|"manages"| VMF["Miniflare instance"]
        VITE["Vite dev server<br/>(Rollup transforms)"] --> VD
    end

主な違いをまとめると次のとおりです。

  • バンドリング:Wrangler はカスタムプラグインを伴う esbuild を使用。Vite プラグインは Vite ネイティブの変換パイプラインを使用
  • プロキシレイヤー:Wrangler は 2 層のプロキシアーキテクチャ(ProxyController 専用の Miniflare)を持つ。Vite プラグインは Vite の dev サーバーの middleware を通じてプロキシする
  • イベント調整:Wrangler は DevEnv コントローラーバスを使用。Vite プラグインは Vite のプラグインフック(configureServerbuildEnd など)を使用
  • HMR:Wrangler は workerd プロセス全体を再起動する。Vite プラグインは Vite のモジュールランナーを使ってより高速な更新が可能
  • Miniflare インスタンス数:Wrangler は 2 つ作成(ランタイム + プロキシ)。Vite プラグインは 1 つ

どちらのパスも最終的には Article 4 で見た同じ Miniflare クラスに到達し、同じ 28 個のプラグインが同じ workerd 設定を生成します。ランタイムの動作は同一です——違いはオーケストレーションの方法だけです。

workers-utils の設定レイヤー:3 つのフォーマット

Wrangler と Vite プラグインはどちらも、設定のパースを @cloudflare/workers-utils からインポートしています。packages/workers-utils/src/config/index.ts#L39-L52 でのフォーマット検出はシンプルです。

export function configFormat(configPath: string | undefined): "json" | "jsonc" | "toml" | "none" {
    if (configPath?.endsWith("toml")) return "toml";
    if (configPath?.endsWith("jsonc")) return "jsonc";
    if (configPath?.endsWith("json")) return "json";
    return "none";
}

TOML のパースには smol-toml を使い、JSON と JSONC のパースには jsonc-parser(通常の JSON もパース可能)を使います。重要な保証として、wrangler.toml と意味的に等価な wrangler.json は、同一の正規化された Config オブジェクトを生成します。

flowchart LR
    TOML["wrangler.toml<br/>(smol-toml)"] --> NORM["normalizeAndValidateConfig()"]
    JSON["wrangler.json<br/>(jsonc-parser)"] --> NORM
    JSONC["wrangler.jsonc<br/>(jsonc-parser)"] --> NORM
    NORM --> CONFIG["Config<br/>(normalized + validated)"]
    NORM --> DIAG["Diagnostics<br/>(warnings + errors)"]

設定の型階層:RawConfig から Config へ

packages/workers-utils/src/config/config.ts#L28-L56 の設定型を見ると、明確な正規化の階層が見えてきます。

export type Config = ComputedFields & ConfigFields<DevConfig> & PagesConfigFields & Environment;

export type RawConfig = Partial<ConfigFields<RawDevConfig>> & PagesConfigFields
                      & RawEnvironment & EnvironmentMap & { $schema?: string };

2 つの型の本質的な違いは次のとおりです。

  • RawConfig は設定ファイルに書く内容です。フィールドはオプショナルで、環境設定はネストされており、ランタイムのメタデータは存在しません。
  • Config は正規化の結果です。ComputedFields によってランタイムのメタデータが追加されます。
    • configPath — 設定ファイルへの解決済みパス
    • userConfigPath — リダイレクト解決前の元のパス
    • topLevelName — 環境フラット化前の元の Worker 名
    • definedEnvironments — Raw Config に宣言された環境のリスト
    • targetEnvironment — 選択された環境

正規化のステップでは、環境の継承(環境固有の設定がトップレベルから引き継ぐ仕組み)を解決し、すべてのフィールドを検証し、非推奨フィールドや未知のフィールドに関する警告を含む Diagnostics を生成します。

classDiagram
    class Config {
        +configPath: string | undefined
        +topLevelName: string | undefined
        +definedEnvironments: string[]
        +targetEnvironment: string | undefined
        +name: string
        +main: string
        +compatibility_date: string
        +...bindings, routes, etc
    }
    class RawConfig {
        +name?: string
        +main?: string
        +env?: EnvironmentMap
        +$schema?: string
    }
    class ComputedFields {
        +configPath
        +userConfigPath
        +topLevelName
        +definedEnvironments
        +targetEnvironment
    }
    Config --|> ComputedFields : intersected with
    RawConfig --> Config : normalization

Wrangler の readConfig():workers-utils をラップする

packages/wrangler/src/config/index.ts#L1-L60 にある Wrangler の readConfig() は、workers-utils レイヤーをラップして Wrangler 固有の動作を追加します。

  1. アップデート確認のヒント:設定に未知のフィールドが含まれていて(新機能の可能性)、かつ新しいバージョンの Wrangler が利用可能な場合に、アップグレードを提案します。これは logWarningsWithUpgradeHint() 関数で実装されており、Diagnostics に未知フィールドの警告が含まれる場合にのみ動作します。

  2. .env.dev.vars の読み込み:Wrangler は環境変数ファイルをサポートしており、ローカル開発時に設定で定義された変数とマージされます。

  3. 環境の選択--env / -e フラグで使用する名前付き環境を選択でき、設定の解決時にその環境の設定がトップレベルのデフォルト値に上書きされます。

  4. 設定のリダイレクト解決useConfigRedirectIfAvailable オプションは .wrangler/deploy/config.json を検索して実際の設定ファイルのパスを見つけます。これは、Pages のビルド出力パターン(設定が自動生成される場合)をサポートするためのものです。

flowchart TD
    ARGS["CLI args (--config, --env)"] --> RC["Wrangler readConfig()"]
    RC --> RESOLVE["resolveWranglerConfigPath()"]
    RESOLVE --> PARSE["workers-utils<br/>normalizeAndValidateConfig()"]
    PARSE --> DIAG["Diagnostics"]
    DIAG --> HINT["logWarningsWithUpgradeHint()"]
    PARSE --> CONFIG["Config"]
    DOTENV[".env / .dev.vars"] --> RC
    RC --> FINAL["Final Config<br/>(with env vars merged)"]

Vite プラグインは別の経路をたどります。アップデート確認や .env の読み込み、Wrangler 固有の環境変数処理を経由せず、直接 workers-utils を通じて設定を読み込みます。これは適切な設計判断です。Vite には独自の .env ファイル規約があり、Vite プラグインには異なる UX 要件があるためです。

ヒント: Workers SDK に新しい設定フィールドを追加する場合は、まず workers-utils(共有パーサー側)に追加し、その後 Wrangler の readConfig() と Vite プラグインの設定リーダーの両方で対応する必要があります。Wrangler パッケージで pnpm run generate-json-schema を実行すると、エディターの自動補完に使われる JSON スキーマが更新されます。

シリーズのまとめ

この 6 本の記事を通じて、Cloudflare Workers SDK のライフサイクル全体を追ってきました。モノレポの pnpm ワークスペース構成と依存関係のバンドル戦略から始まり、Wrangler の宣言的なコマンドシステム、ローカル開発を調整する DevEnv コントローラーバス、workerd の子プロセスを管理する Miniflare の 28 プラグインアーキテクチャ、Workers のデプロイ用パッケージングを担う esbuild ベースのバンドルパイプライン、そして同じランタイムコアへの代替開発インターフェースを提供する Vite プラグインまで、一通り見てきました。

繰り返し登場するテーマは、共通の基盤の上に積み上げられた抽象レイヤーです。workers-utils の設定レイヤーは、Wrangler と Vite プラグインが設定ファイルの意味について同じ解釈を持つことを保証します。Miniflare は両ツールが同一のローカルランタイム動作を生成することを保証します。そして最下層の workerd バイナリが、ローカル開発環境を Cloudflare の本番ランタイムにできる限り近づけることを保証しています。

SDK にコントリビュートする際は、まず自分の変更がどのレイヤーに触れるのかを理解するところから始めましょう。設定のパース?それは workers-utils です。バインディングのシミュレーション?それは Miniflare のプラグインです。CLI の動作?それは createCommand() の定義です。ローカル開発のオーケストレーション?それは DevEnv コントローラーです。外から見るとアーキテクチャは複雑に見えるかもしれませんが、各レイヤーには明確な責務があり、隣接するレイヤーとの間に明確に定義されたインターフェースがあります。