Read OSS

Vite 8 の内部構造:アーキテクチャ概要とコードベースの歩き方

中級

前提知識

  • Vite の基本的な役割(開発サーバー+本番バンドラー)への理解
  • JavaScript/TypeScript の基礎知識
  • ES Modules の構文と動的インポートの理解
  • Node.js の HTTP サーバーとファイルシステム API の概要把握

Vite 8 の内部構造:アーキテクチャ概要とコードベースの歩き方

Vite は、高速な開発サーバーの実験的プロジェクトとして始まり、今では主要なフロントエンドフレームワークの標準ビルドツールへと成長しました。これほど広く使われているにもかかわらず、packages/vite/src ディレクトリの中に踏み込んで仕組みを理解しようとした開発者はまだ多くありません。Vite 8 は大きな転換点を迎えています。依存関係の最適化と本番ビルドの両方において、esbuild と Rollup が Rolldown に置き換えられました。また、新しい Environment API により、単一のサーバーインスタンスで複数のコンパイルターゲット(ブラウザ、SSR、エッジワーカー)を統合管理できるようになっています。本記事では、これ以降のディープダイブに必要となる基本的なメンタルモデルを整理します。

モノレポの構成とパッケージの役割

Vite は pnpm モノレポとして管理されています。中心となるパッケージは packages/ 以下に配置されています。

ディレクトリ 役割
packages/vite コア — 開発サーバー、ビルドパイプライン、プラグイン、CLI
packages/create-vite スキャフォールディング CLI(npm create vite
packages/plugin-legacy レガシーブラウザサポート(@vitejs/plugin-legacy
playground/ 特定機能を検証する約 80 個の統合テストアプリ
docs/ VitePress で構築されたドキュメントサイト

コアパッケージの package.json は Vite 8.0.3 を "type": "module" として宣言しており、ランタイム依存関係は驚くほどシンプルです。lightningcsspicomatchpostcssrolldowntinyglobby のわずか 5 つだけです。cacchokidarwsconnect など数十のパッケージはすべて devDependency として扱われ、ビルド時に dist/ ディレクトリへ事前バンドルされます。これにより npm install vite が速く済み、依存ツリーが浅く保たれています。

graph TD
    subgraph "Monorepo Root"
        A[packages/vite<br/>Core package]
        B[packages/create-vite<br/>Scaffolding CLI]
        C[packages/plugin-legacy<br/>Legacy support]
        D[playground/<br/>~80 test apps]
        E[docs/<br/>VitePress site]
    end
    A -->|"runtime deps"| F[rolldown]
    A -->|"runtime deps"| G[postcss]
    A -->|"runtime deps"| H[lightningcss]

ヒント: package.json の 74 行目 には明確なコメントがあります。「READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!」 ランタイムで必要なものは dependencies に、それ以外は事前バンドルする、というルールです。

4つのソースディレクトリ

packages/vite/src/ 以下のコードは、それぞれ異なる実行環境をターゲットとした 4 つのディレクトリに分かれています(5 つ目の src/types/ はベンダー依存関係向けの .d.ts 宣言のみを含みます)。

ディレクトリ 実行環境 役割
src/node/ Node.js 開発サーバー、ビルドパイプライン、設定、プラグイン、CLI
src/client/ ブラウザ HMR クライアント、エラーオーバーレイ、WebSocket 接続
src/module-runner/ Node.js(SSR) モジュールの取得・評価・SSR 用ソースマップ
src/shared/ 両環境共通 HMR プロトコルロジック、ランタイム間で共有されるユーティリティ

この分割は整理上の都合ではなく、アーキテクチャ上の本質的な意味を持っています。src/client/ のコードはブラウザに配信されるため、ブラウザ環境で動作しなければなりません。src/module-runner/ は tree-shaking のために独立したパッケージエントリー(vite/module-runner)としてエクスポートされています。そして src/shared/ は、ブラウザとサーバーの両方のコードが再利用する HMR プロトコル実装を提供し、接続の両端で実装が乖離しないようにしています。

flowchart LR
    subgraph "src/node/"
        N1[Dev Server]
        N2[Build Pipeline]
        N3[Plugin System]
        N4[Config Resolution]
    end
    subgraph "src/client/"
        C1[HMR Client]
        C2[Error Overlay]
    end
    subgraph "src/module-runner/"
        M1[ModuleRunner]
        M2[ESModulesEvaluator]
    end
    subgraph "src/shared/"
        S1[HMRClient class]
        S2[HMRContext class]
        S3[Transport utils]
    end
    C1 --> S1
    M1 --> S1
    N1 --> S1

src/shared/hmr.tsHMRClientHMRContext をエクスポートしています。これらのクラスは src/client/client.ts のブラウザクライアントと src/module-runner/runner.ts の SSR モジュールランナーの両方で使われています。プロトコル処理ロジックを重複させることなく、両環境で HMR を実現しています。

CLI エントリーポイントとコマンドルーティング

ターミナルで vite を実行すると、処理は bin/vite.js から始まります。この 80 行のファイルは、本格的な処理に入る前に 4 つのことを行います。

  1. 起動時刻の記録16 行目global.__vite_start_time = performance.now() で記録し、後で「ready in X ms」の表示に使います
  2. デバッグ・フィルターフラグの解析process.argv を走査して --debug--filter を検出し、process.env.DEBUG に反映します(19〜46 行目
  3. コンパイルキャッシュの有効化module.enableCompileCache?.() を呼び出し、Node.js のモジュールコンパイルを高速化します(48〜63 行目
  4. CLI の動的インポートimport('../dist/node/cli.js') により、フラグ処理が完了するまで重い処理を読み込まないようにします
flowchart TD
    A["bin/vite.js"] --> B["Record performance.now()"]
    B --> C{"--debug flag?"}
    C -->|yes| D["Set process.env.DEBUG"]
    C -->|no| E{"--profile flag?"}
    D --> E
    E -->|yes| F["Start V8 Profiler, then import cli.ts"]
    E -->|no| G["enableCompileCache(), import cli.ts"]
    F --> H["cli.ts: cac command routing"]
    G --> H
    H --> I["vite dev / build / preview / optimize"]

CLI モジュール src/node/cli.tscac を使って 4 つのコマンドを定義しています。各コマンドハンドラーは遅延動的インポートを使い、重いモジュールの読み込みを必要なときまで先送りしています。

  • dev190〜304 行目): const { createServer } = await import('./server')
  • build307〜382 行目): const { createBuilder } = await import('./build')
  • optimize384〜420 行目): 非推奨としてマーク済み。オプティマイザーは自動実行されます
  • preview422〜475 行目): const { preview } = await import('./preview')

この遅延インポートのパターンには意図があります。vite build を実行した場合、開発サーバーのコードを読み込むコストは一切かかりません。

プログラマティック API とパッケージエクスポートマップ

package.json のエクスポートマップ では、4 つの公開エントリーポイントが定義されています。

インポートパス 解決先 用途
"vite" dist/node/index.js メインのプログラマティック API
"vite/module-runner" dist/node/module-runner.js SSR モジュールランナー(tree-shakeable)
"vite/internal" dist/node/internal.js フレームワーク作者向けの内部 API
"vite/client" client.d.ts(型定義のみ) クライアントサイドの型定義

メインエントリー src/node/index.ts は約 290 行のバレルファイルで、公開 API 全体を再エクスポートしています。主なランタイムエクスポートは次のとおりです。

  • createServerViteDevServer インスタンスを生成します
  • build / createBuilder — 本番ビルドのエントリーポイント
  • preview — 本番ビルドの出力をローカルで配信します
  • defineConfig / resolveConfig / loadConfigFromFile — 設定関連のユーティリティ
  • DevEnvironment / BuildEnvironment — 環境クラス
  • createRunnableDevEnvironment / createFetchableDevEnvironment — SSR 環境のファクトリー関数

残りの部分は型のエクスポートで、PluginResolvedConfig から HotPayloadEnvironmentModuleGraph まで、180 を超える型定義が含まれています。

Environment API:Vite のコア抽象化

Vite 8 で最も重要なアーキテクチャの変化が Environment API です。これまで「クライアント」と「SSR」はコードベース全体に散らばった boolean フラグとして扱われていましたが、Vite は今や各コンパイルターゲットを独立した Environment インスタンスとしてモデル化しています。それぞれが独自のモジュールグラフ、プラグインパイプライン、依存関係オプティマイザーを持ちます。

クラス階層は src/node/baseEnvironment.ts から始まります。

classDiagram
    class PartialEnvironment {
        +name: string
        +config: ResolvedConfig & ResolvedEnvironmentOptions
        +logger: Logger
        +getTopLevelConfig(): ResolvedConfig
    }
    class BaseEnvironment {
        +plugins: readonly Plugin[]
    }
    class DevEnvironment {
        +mode: "dev"
        +moduleGraph: EnvironmentModuleGraph
        +pluginContainer: EnvironmentPluginContainer
        +depsOptimizer?: DepsOptimizer
        +hot: NormalizedHotChannel
        +transformRequest(url): Promise~TransformResult~
    }
    class BuildEnvironment {
        +mode: "build"
        +isBuilt: boolean
    }
    class ScanEnvironment {
        +mode: "scan"
    }
    PartialEnvironment <|-- BaseEnvironment
    BaseEnvironment <|-- DevEnvironment
    BaseEnvironment <|-- BuildEnvironment
    BaseEnvironment <|-- ScanEnvironment

特に巧みな設計が、PartialEnvironment における Proxy ベースの設定マージです。47〜60 行目 のコンストラクターで、this.configProxy を生成しています。

this.config = new Proxy(
  options as ResolvedConfig & ResolvedEnvironmentOptions,
  {
    get: (target, prop: keyof ResolvedConfig) => {
      if (prop === 'logger') return this.logger
      if (prop in target) {
        return this._options[prop as keyof ResolvedEnvironmentOptions]
      }
      return this._topLevelConfig[prop]
    },
  },
)

プラグインが environment.config.build を読み取ると、その環境固有のビルドオプションが返されます。environment.config.root を読み取ると、トップレベルの設定にフォールスルーします。この透過的なオーバーレイにより、プラグインは環境固有の設定かグローバルな設定かを意識する必要がなく、Proxy がルーティングを自動的に処理してくれます。

Environment 型ユニオン がこれらをまとめています。

export type Environment =
  | DevEnvironment
  | BuildEnvironment
  | ScanEnvironment
  | UnknownEnvironment

Rolldown への移行

Vite 8 では、Rollup 互換 API を持つ Rust 製バンドラー Rolldown への移行が完了しています。従来のバージョンでは依存関係の事前バンドルに esbuild、本番ビルドに Rollup を使っていましたが、Vite 8 ではその両方を Rolldown が担います。コードベースの随所にその証拠が見られます。

  • package.json では rolldown(現在 1.0.0-rc.12)が直接依存関係として記載されています
  • src/node/index.tsRollupRolldown 両方の型を再エクスポートしており、#types/internal/rollupTypeCompat という互換レイヤーも存在します
  • esbuild の非推奨化は明示的なマーカーで示されています。esbuild?: ESBuildOptions | false@deprecated Use 'oxc' option instead というコメントが付いています
  • rolldown/experimental のネイティブ Rolldown プラグインが JavaScript 実装を置き換えています(nativeAliasPluginnativeJsonPluginnativeWasmFallbackPlugin
sequenceDiagram
    participant Vite7 as Vite 7 (Previous)
    participant Vite8 as Vite 8 (Current)
    Note over Vite7: Dev: esbuild (dep optimization)
    Note over Vite7: Build: Rollup (production bundle)
    Note over Vite7: Transform: esbuild (TS/JSX)
    Note over Vite8: Dev: Rolldown (dep optimization)
    Note over Vite8: Build: Rolldown (production bundle)
    Note over Vite8: Transform: OXC (TS/JSX)

TypeScript と JSX の変換では、esbuild に代わって OXC トランスフォーマーが使われています。index のエクスポートを見ると、現行 API が transformWithOxc であり、後方互換性のために transformWithEsbuild が残されていることが確認できます。

ヒント: Vite プラグインを書く際は、フックの型として rolldown から直接インポートするのではなく、vitePlugin を使いましょう。Vite の Plugin インターフェースは RolldownPlugin を拡張しており、互換レイヤーが残りの部分を処理してくれます。

次のステップ

モノレポの構成、ソースディレクトリの構造、エントリーポイント、Environment API の全体像が把握できました。これでコードベース内のどのファイルを読む際にも必要な語彙は揃っています。次の記事では、Vite の 2,700 行に及ぶ設定システムに踏み込みます。vite.config.ts がどのように検出・読み込まれるか、resolveConfig() がユーザー設定をどのように凍結された ResolvedConfig へ変換するか、そして約 30 の内部プラグインがどのように精密な実行パイプラインとして組み立てられるかを詳しく見ていきましょう。