Read OSS

エントリーポイントと起動プロセス

中級

前提知識

  • ES modules の基本的な理解
  • Node.js CLI の基礎知識
  • package.json の bin フィールドへの理解

エントリーポイントと起動プロセス

Vite のエントリーポイントは驚くほどシンプルです。50,000 行を超えるコードベースでありながら、ターミナルで vite を実行してから開発サーバーが立ち上がるまでの経路で通過するファイルはほんのわずかです。これは偶然ではなく、意図的な設計です。Vite のアーキテクチャは関心の分離を徹底しており、各レイヤーがひとつのことを確実にこなす構造となっています。

CLI コマンドの実行から開発サーバーの起動まで、一連の流れを順を追って見ていきましょう。

bin エントリー

vite または vite dev を実行すると、シェルは packages/vite/package.jsonbin フィールドを参照し、packages/vite/bin/vite.js を呼び出します。このファイルは意図的に最小限の内容に抑えられています。

bin ファイル自体がやることはほとんどありません。起動時刻を記録し、process.argv からデバッグフラグとフィルターフラグをパースし、Node.js のコンパイルキャッシュを有効にしてから、CLI モジュールを動的にインポートします。「薄い bin エントリー、実質的な処理はモジュールへ」というこのパターンは、Node.js エコシステム全体でよく見られる手法です。起動を高速に保ちつつ、実際のロジックをテストしやすい形に切り出せるのが利点です。

flowchart TD
    A["$ vite dev"] --> B["bin/vite.js"]
    B --> C["cli.ts — parse args with cac"]
    C --> D{Command?}
    D -->|dev| E["createServer()"]
    D -->|build| F["build()"]
    D -->|preview| G["preview()"]
    D -->|optimize| H["optimizeDeps()"]
    E --> I["resolveConfig()"]
    I --> J["Plugin container init"]
    J --> K["HTTP server + HMR"]

CLI の引数パース

CLI の実際のロジックは packages/vite/src/node/cli.ts に集約されています。Vite は引数のパースに cac を採用しています。commander や yargs と比べて軽量な代替ライブラリですが、API はほぼ同じなので、初めて目にする場合でも迷うことはないでしょう。各サブコマンド(devbuildpreview)がそれぞれ独立したハンドラー関数に対応している点が重要です。

ひとつ覚えておきたいのは、dev コマンドがデフォルトとしても機能することです。サブコマンドなしで vite を実行した場合、vite dev と同等に扱われます。これは cac のデフォルトコマンド機能によって実現されています。

ヒント: cli.ts は上から下へ通して読むことをおすすめします。構造が整理されており、すべてのフラグが設定オプションと対応しています。CLI フラグの挙動が気になったときは、このファイルが最も信頼できる参照先です。

設定の解決

何かが動き始める前に、Vite はまず設定を解決する必要があります。config.ts にある resolveConfig 関数は、コードベース全体でも特に重要な関数のひとつです。

設定の解決は以下の優先順位で行われます。

  1. インライン設定 — API 経由で直接渡されたオプション(例:createServer({ root: './app' })
  2. 設定ファイルvite.config.tsvite.config.js、またはサポートされている他の形式
  3. デフォルト値 — すべての項目に対する合理的なフォールバック値
flowchart LR
    A["Inline config"] --> D["mergeConfig()"]
    B["vite.config.ts"] --> D
    C["Defaults"] --> D
    D --> E["ResolvedConfig"]
    E --> F["Plugins sorted & applied"]

設定ファイルの読み込みプロセスは、見た目以上に高度な仕組みになっています。TypeScript で書かれた設定ファイル、ESM と CJS の違い、node_modules からのインポートを含む設定ファイルなど、さまざまなケースに対応しなければなりません。デフォルトでは、起動時に Rolldown で設定ファイルをバンドルすることでこれを実現しています。そう、Vite は自身の設定をビルドするために自身のバンドラーを使うのです。後から考えると当然の設計ですが、この発想に至るには相当な洞察が必要だったはずです。

サーバーの生成:開発モードの中核

createServer 関数は、開発サーバーに必要なすべてのものを組み上げる司令塔です。丁寧に読む価値があります。独立した各サブシステムを共有のコンテキストオブジェクトを介して組み合わせるという、Vite のアーキテクチャ哲学がここに凝縮されているからです。

createServer がセットアップする内容を順番に見ていきましょう。

  1. 設定の解決 — 前述のとおり、ユーザー設定とデフォルト値をマージする
  2. plugin container — Rollup 互換の plugin パイプラインを初期化する
  3. module graph — すべてのモジュールとその依存関係を追跡する ModuleGraph インスタンス
  4. WebSocket サーバー — ブラウザとの HMR 通信に使用する
  5. ファイルウォッチャー — chokidar でプロジェクトのファイル変更を監視する
  6. Connect middleware スタック — トランスパイル済みモジュールを配信する HTTP サーバー

サーバーオブジェクト自体が共有コンテキストとして機能します。各サブシステムはこのオブジェクトへの参照を受け取り、互いのメソッドを呼び合うことで連携します。イベント駆動アーキテクチャではなくこの設計を選んだのは意図的な判断であり、明示的でデバッグしやすいコードが実現されています。

ヒント: サーバーで何ができるかを知るには、server/index.ts にある ViteDevServer インターフェースを確認するのが一番の近道です。実装の細部を読み解く前に、まずは型定義に目を通しておきましょう。

Module Graph

packages/vite/src/node/server/moduleGraph.ts にある EnvironmentModuleGraph クラスは特筆に値します。Vite の HMR が高速である理由はこのデータ構造にあります。ファイルが変更されると、module graph は影響を受けるモジュールを即座に特定し、ブラウザに再取得させます(mixedModuleGraph.ts には後方互換性のための ModuleGraph ラッパーがあり、環境ごとのグラフをひとつにまとめています)。

グラフの各ノード(EnvironmentModuleNode)が保持する情報は以下のとおりです。

  • URL とファイルパス
  • そのモジュールがインポートしているモジュール(importedModules
  • そのモジュールをインポートしているモジュール(importers
  • HMR の受け入れ状態
  • 最後にトランスパイルされたタイムスタンプ
graph TD
    A["main.ts"] --> B["App.vue"]
    A --> C["router.ts"]
    B --> D["Header.vue"]
    B --> E["Footer.vue"]
    C --> F["routes/Home.vue"]
    C --> G["routes/About.vue"]
    style A fill:#e8f4fd,stroke:#333
    style B fill:#fde8e8,stroke:#333

Header.vue が変更された場合、module graph はインポーターの連鎖を遡って最も近い HMR バウンダリーを探します。App.vue が子コンポーネントの HMR 更新を受け入れるよう設定されていれば、無効化されるのはそのサブツリーだけです。これが Vite のほぼ瞬時の HMR を支える仕組みであり、必要以上のモジュールを無効化しないことが速さの秘訣です。

理解しておくべき概念

Vite のソースコードを効率よく読むために、以下の概念を押さえておきましょう。

  • Rollup plugin API — Vite は Rollup の plugin インターフェースを拡張し、開発時向けのフックを追加しています。Rollup plugin を理解できれば、Vite の plugin システムの 8 割は把握できたも同然です。
  • ES module のセマンティクスimport.meta、動的な import()、そしてブラウザがネイティブ ESM を処理する仕組みを理解しておきましょう。Vite のアーキテクチャを成立させている根幹です。
  • Node.js HTTP サーバー — Vite は middleware フレームワークとして connect を使用しています。Express よりもずっとシンプルで、その簡潔さは意図的な選択です。
  • Rolldown と OXC — Rolldown は依存関係の事前バンドルとプロダクションビルドに使われ、OXC は TypeScript/JSX のトランスパイルを担います。これらのツールを理解すると、Vite 8 のパフォーマンス特性が見えてきます。

ディレクトリマップ

本記事で取り上げた主要ファイルのクイックリファレンスです。

パス 役割
packages/vite/bin/vite.js 薄い bin エントリーポイント
packages/vite/src/node/cli.ts cac による CLI 引数パース
packages/vite/src/node/config.ts 設定の解決とマージ
packages/vite/src/node/server/index.ts 開発サーバーの生成とオーケストレーション
packages/vite/src/node/server/moduleGraph.ts 環境ごとのモジュール依存グラフ
packages/vite/src/node/plugins/ 組み込み plugin の実装

次のステップ

次回は plugin システムを深く掘り下げます。Vite が Rollup の plugin インターフェースをどのように拡張しているか、plugin の実行順序、そして開発中に plugin container がモジュールをリアルタイムでトランスパイルする仕組みを詳しく解説します。