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" として宣言しており、ランタイム依存関係は驚くほどシンプルです。lightningcss、picomatch、postcss、rolldown、tinyglobby のわずか 5 つだけです。cac、chokidar、ws、connect など数十のパッケージはすべて 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.ts は HMRClient と HMRContext をエクスポートしています。これらのクラスは src/client/client.ts のブラウザクライアントと src/module-runner/runner.ts の SSR モジュールランナーの両方で使われています。プロトコル処理ロジックを重複させることなく、両環境で HMR を実現しています。
CLI エントリーポイントとコマンドルーティング
ターミナルで vite を実行すると、処理は bin/vite.js から始まります。この 80 行のファイルは、本格的な処理に入る前に 4 つのことを行います。
- 起動時刻の記録 — 16 行目 の
global.__vite_start_time = performance.now()で記録し、後で「ready in X ms」の表示に使います - デバッグ・フィルターフラグの解析 —
process.argvを走査して--debugと--filterを検出し、process.env.DEBUGに反映します(19〜46 行目) - コンパイルキャッシュの有効化 —
module.enableCompileCache?.()を呼び出し、Node.js のモジュールコンパイルを高速化します(48〜63 行目) - 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.ts は cac を使って 4 つのコマンドを定義しています。各コマンドハンドラーは遅延動的インポートを使い、重いモジュールの読み込みを必要なときまで先送りしています。
- dev(190〜304 行目):
const { createServer } = await import('./server') - build(307〜382 行目):
const { createBuilder } = await import('./build') - optimize(384〜420 行目): 非推奨としてマーク済み。オプティマイザーは自動実行されます
- preview(422〜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 全体を再エクスポートしています。主なランタイムエクスポートは次のとおりです。
createServer—ViteDevServerインスタンスを生成しますbuild/createBuilder— 本番ビルドのエントリーポイントpreview— 本番ビルドの出力をローカルで配信しますdefineConfig/resolveConfig/loadConfigFromFile— 設定関連のユーティリティDevEnvironment/BuildEnvironment— 環境クラスcreateRunnableDevEnvironment/createFetchableDevEnvironment— SSR 環境のファクトリー関数
残りの部分は型のエクスポートで、Plugin や ResolvedConfig から HotPayload、EnvironmentModuleGraph まで、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.config に Proxy を生成しています。
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.tsはRollupとRolldown両方の型を再エクスポートしており、#types/internal/rollupTypeCompatという互換レイヤーも存在します- esbuild の非推奨化は明示的なマーカーで示されています。
esbuild?: ESBuildOptions | falseに@deprecated Use 'oxc' option insteadというコメントが付いています rolldown/experimentalのネイティブ Rolldown プラグインが JavaScript 実装を置き換えています(nativeAliasPlugin、nativeJsonPlugin、nativeWasmFallbackPlugin)
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から直接インポートするのではなく、viteのPluginを使いましょう。Vite のPluginインターフェースはRolldownPluginを拡張しており、互換レイヤーが残りの部分を処理してくれます。
次のステップ
モノレポの構成、ソースディレクトリの構造、エントリーポイント、Environment API の全体像が把握できました。これでコードベース内のどのファイルを読む際にも必要な語彙は揃っています。次の記事では、Vite の 2,700 行に及ぶ設定システムに踏み込みます。vite.config.ts がどのように検出・読み込まれるか、resolveConfig() がユーザー設定をどのように凍結された ResolvedConfig へ変換するか、そして約 30 の内部プラグインがどのように精密な実行パイプラインとして組み立てられるかを詳しく見ていきましょう。