Read OSS

Vite が設定を解決し、プラグインパイプラインを組み立てる仕組み

上級

前提知識

  • 第 1 回:アーキテクチャとコードベースの全体像
  • Rollup/Rolldown プラグインフックモデル(resolveId、load、transform)の理解
  • TypeScript のジェネリクスとユーティリティ型

Vite が設定を解決し、プラグインパイプラインを組み立てる仕組み

Vite の設定システムは、コードベースの中でも最も複雑な部分です。それには十分な理由があります。ユーザーの vite.config.ts(関数・Promise・プレーンオブジェクトのいずれにもなりえます)、プログラムから呼び出す側のインライン設定、環境ごとのオーバーライド、後方互換性を維持した SSR オプション、そしてプラグインが提供する設定の変更。これらすべてを一元的に統合し、最終的に ResolvedConfig としてフリーズした状態でシステム全体に提供しなければなりません。このすべての処理は src/node/config.ts に実装されています。そのコード量は 2,700 行を超えます。

設定ファイルの読み込み戦略

設定を解決する前に、Vite はまずユーザーの設定ファイルを見つけて読み込む必要があります。loadConfigFromFile() 関数は configLoader オプションによって制御される 3 つの読み込み戦略をサポートしています。

  1. bundle(デフォルト)— vite.config.ts を Rolldown でコンパイルして一時的な JS ファイルを生成し、それをインポートする。TypeScript、パスエイリアス、node_modules からのインポートを透過的に処理できる、最も堅牢なアプローチである
  2. runner(実験的)— Vite 独自のモジュールランナーを使って設定をオンザフライで処理する。一時ファイルの生成を省略できる
  3. native(実験的)— Node.js のネイティブ ESM ローダーに依存する。設定ファイルがすでにプレーンな JavaScript である場合や、Node.js の組み込み TypeScript サポートを活用している場合に有効である

CLI からは --configLoader オプションで指定できます。

// From cli.ts, line 182-183
.option('--configLoader <loader>',
  `[string] use 'bundle' to bundle the config with Rolldown, ...`)

設定ファイルの探索には DEFAULT_CONFIG_FILES が使われます。これは vite.config.jsvite.config.mjsvite.config.tsvite.config.cjsvite.config.mtsvite.config.cts を含むリストで、最初に見つかったファイルが採用されます。

flowchart TD
    A["loadConfigFromFile()"] --> B{"configFile provided?"}
    B -->|yes| C["Use specified file"]
    B -->|no| D["Search DEFAULT_CONFIG_FILES"]
    C --> E{"configLoader?"}
    D --> E
    E -->|bundle| F["Rolldown-compile to temp .js"]
    E -->|runner| G["Vite module runner eval"]
    E -->|native| H["Node.js native import()"]
    F --> I["import(tempFile)"]
    G --> I
    H --> I
    I --> J{"Export is function?"}
    J -->|yes| K["Call with ConfigEnv"]
    J -->|no| L["Use object directly"]
    K --> M["Return { path, config, dependencies }"]
    L --> M

defineConfig() ヘルパーは型レベルの恒等関数です。渡した引数をそのまま返すだけですが、TypeScript のオートコンプリートが効くようになります。プレーンオブジェクト・Promise・関数といった、すべてのエクスポート形式に対応しています。

export function defineConfig(config: UserConfigExport): UserConfigExport {
  return config
}

resolveConfig() パイプライン

設定システムの核心は resolveConfig() です。この関数はユーザーの入力を、完全に解決・フリーズされた設定オブジェクトへと変換する多段階のパイプラインを統括しています。

flowchart TD
    A["resolveConfig(inlineConfig, command)"] --> B["Setup Rollup compat shims"]
    B --> C["loadConfigFromFile()"]
    C --> D["mergeConfig(fileConfig, inlineConfig)"]
    D --> E["Filter plugins by 'apply' field"]
    E --> F["Sort into pre/normal/post"]
    F --> G["Run plugin 'config' hooks"]
    G --> H["Ensure client & ssr environments"]
    H --> I["Resolve sub-options:<br/>server, build, CSS, SSR, preview"]
    I --> J["Resolve per-environment options"]
    J --> K["Run 'configEnvironment' hooks"]
    K --> L["Resolve plugins via resolvePlugins()"]
    L --> M["Run 'configResolved' hooks"]
    M --> N["Return frozen ResolvedConfig"]

主要なステップを順に見ていきましょう。関数は line 1356 から始まります。

export async function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development',
  defaultNodeEnv = 'development',
  isPreview = false,
  patchConfig, patchPlugins,
): Promise<ResolvedConfig> {

patchConfigpatchPlugins は内部フックです。マルチ環境ビルド時に createBuilder() が環境ごとに config.build をオーバーライドするために使われます。

ファイル設定を読み込んだ後、プラグインは apply プロパティによってフィルタリングされます(lines 1418-1428)。

const filterPlugin = (p: Plugin | FalsyPlugin): p is Plugin => {
  if (!p) return false
  if (!p.apply) return true
  if (typeof p.apply === 'function') {
    return p.apply({ ...config, mode }, configEnv)
  }
  return p.apply === command
}

これが、プラグインに apply: 'build' を指定することで開発時に除外できる仕組みです。より複雑な条件が必要なら、関数として指定することもできます。

続いて重要な処理が lines 1443-1460 で行われます。Vite は clientssr の環境が設定に必ず存在することを保証し、その順序も強制します。

config.environments ??= {}
if (!config.environments.ssr && (!isBuild || config.ssr || config.build?.ssr)) {
  config.environments = { ssr: {}, ...config.environments }
}
if (!config.environments.client) {
  config.environments = { client: {}, ...config.environments }
}

スプレッドによる並び替えパターンにより、client が先頭に来て、次に ssr、そしてカスタム環境が続く順序が保証されます。システムの残りの部分はこの一貫した順序を前提に動いています。

UserConfig → ResolvedConfig の型変換

型システムを理解することで、設定解決の契約が明確になります。UserConfig はユーザーが書く側の型で、ほぼすべてのフィールドがオプショナルです。

export interface UserConfig extends DefaultEnvironmentOptions {
  root?: string
  base?: string
  publicDir?: string | false
  cacheDir?: string
  mode?: string
  plugins?: PluginOption[]
  css?: CSSOptions
  server?: ServerOptions
  environments?: Record<string, EnvironmentOptions>
  // ... 30+ more optional fields
}

ResolvedConfig はシステムが消費する側の型で、フィールドは必須・readonly・完全解決済みになっています。

export interface ResolvedConfig extends Readonly<
  Omit<UserConfig, 'plugins' | 'css' | 'json' | ...> & {
    root: string                              // was optional, now required
    base: string                              // resolved to absolute
    plugins: readonly Plugin[]                // flattened and sorted
    css: ResolvedCSSOptions                   // all defaults filled
    environments: Record<string, ResolvedEnvironmentOptions>
    // ...
  } & PluginHookUtils
> {}
classDiagram
    class UserConfig {
        root?: string
        base?: string
        plugins?: PluginOption[]
        environments?: Record~string, EnvironmentOptions~
        server?: ServerOptions
        build?: BuildEnvironmentOptions
    }
    class ResolvedConfig {
        root: string
        base: string
        plugins: readonly Plugin[]
        environments: Record~string, ResolvedEnvironmentOptions~
        server: ResolvedServerOptions
        build: ResolvedBuildOptions
        +getSortedPlugins()
        +getSortedPluginHooks()
    }
    UserConfig ..> ResolvedConfig : resolveConfig()

PluginHookUtils ミックスインは getSortedPlugins()getSortedPluginHooks() を追加します。これらはフックの順序でソートされたプラグインリストをキャッシュするユーティリティメソッドで、リクエスト処理中の効率的なアクセスを実現します。

環境オプションの解決

環境ごとのオプションは resolveEnvironmentOptions() によって解決されます。グローバルなデフォルト値と環境固有のオーバーライドをマージし、環境名をもとに consumer タイプ(client か server か)を決定します。

const consumer = options.consumer ?? (isClientEnvironment ? 'client' : 'server')

開発時固有のオプションは resolveDevEnvironmentOptions() が解決します。consumer に応じてデフォルト値を使い分けており、client 環境には preTransformRequests: truerecoverable: true が、server 環境には moduleRunnerTransform: true が設定されます。

第 1 回で触れたように、PartialEnvironment コンストラクタの Proxy パターンにより、この解決処理は実行時に透過的に行われます。プラグインが this.environment.config.build.outDir を参照すると、Proxy はまず環境オプション内に build が存在するか確認し(存在するので)環境固有の値を返します。一方、this.environment.config.root のようにトップレベルの設定にしか存在しないフィールドは、Proxy がフォールスルーしてトップレベルの設定値を返します。

Tip: configDefaults はフリーズされたオブジェクトで、Vite が使用するすべてのデフォルト値が記述されています。「ユーザーが X を指定しなかった場合の動作」を知りたいときの唯一の情報源です。

内部プラグインの順序

設定が解決された後、resolvePlugins() が完全なプラグイン配列を組み立てます。その順序は精密かつ意図的に設計されています。

flowchart TD
    subgraph "Pre-user plugins"
        A1[optimizedDeps]
        A2[watchPackageData]
        A3[preAlias]
        A4[alias - native or JS]
    end
    subgraph "User pre plugins"
        B[enforce: 'pre' plugins]
    end
    subgraph "Core plugins"
        C1[modulePreloadPolyfill]
        C2[oxcResolve]
        C3[htmlInlineProxy]
        C4[css]
        C5[oxc - TS/JSX transform]
        C6[json - native]
        C7[wasm / webWorker / asset]
    end
    subgraph "User normal plugins"
        D[normal plugins]
    end
    subgraph "Post-core plugins"
        E1[wasmFallback]
        E2[define]
        E3[cssPost]
        E4[buildHtml]
        E5[workerImportMetaUrl]
        E6[dynamicImportVars / importGlob]
    end
    subgraph "User post plugins"
        F[enforce: 'post' plugins]
    end
    subgraph "Build post plugins"
        G1[importAnalysisBuild / terser]
        G2[license / manifest / reporter]
    end
    subgraph "Dev-only tail"
        H1[clientInjections]
        H2[cssAnalysis]
        H3[importAnalysis]
    end
    A1 --> B --> C1 --> D --> E1 --> F --> G1 --> H1

末尾に追加される 3 つの開発専用プラグイン(lines 126-132)に注目してください。clientInjectionsPlugincssAnalysisPluginimportAnalysisPlugin の 3 つです。特に importAnalysisPlugin は、ソースコード内のすべての import をアンバンドル ESM として動作するように書き換える、開発モードで最も重要なプラグインです。他のすべての transform の出力を受け取る必要があるため、必ず最後に実行されます。

グローバルな順序付けに加えて、各プラグイン内の個々のフックは order: 'pre' | 'post' を指定できます。getSortedPluginsByHook() がこのフックごとの並び替えを、効率的なインプレース挿入によって処理します。

Vite 固有のプラグインフック

Vite の Plugin インターフェース は Rolldown の RolldownPlugin を拡張しており、Rollup/Rolldown に相当するものがない独自のフックをいくつか備えています。

フック タイミング 用途
config 解決前 生のユーザー設定を変更・拡張する
configEnvironment 環境ごと 環境固有の設定を変更する
configResolved 解決後 フリーズされた最終設定を読み取る
configureServer サーバー作成時 ミドルウェアの追加やサーバー参照の保持
configurePreviewServer プレビュー作成時 プレビューサーバーでも同様
transformIndexHtml HTML 配信時 タグの注入や HTML コンテンツの変換
hotUpdate ファイル変更時 HMR の更新伝播を制御する
buildApp ビルド開始時 マルチ環境ビルドを統括する

enforce プロパティ(lines 202-214)は、ユーザープラグインが Vite 内部プラグインに対してどの位置に挿入されるかを制御します。

enforce?: 'pre' | 'post'
// Plugin invocation order:
// - alias resolution
// - `enforce: 'pre'` plugins
// - vite core plugins
// - normal plugins
// - vite build plugins
// - `enforce: 'post'` plugins
// - vite build post plugins

applyToEnvironment フック(lines 227-229)は Environment API で新たに追加されたもので、プラグインを環境ごとに条件付きで有効化したり、環境によってまったく異なるプラグインインスタンスを返したりできます。

applyToEnvironment?: (
  environment: PartialEnvironment,
) => boolean | Promise<boolean> | PluginOption

Tip: sharedDuringBuild フラグ(line 184)は、vite build --app 実行時にプラグインインスタンスを環境間で共有するかどうかを制御します。デフォルトでは後方互換性のため環境ごとにプラグインが再生成されますが、true に設定することでより効率的な共有モードを有効化できます。

次回予告

設定解決のパイプラインとプラグインの順序を理解したところで、次はブラウザが開発サーバーにモジュールをリクエストしたときの処理を追いかけてみましょう。次回の記事では、connect のミドルウェアスタックから transform パイプライン、プラグインコンテナを経て、HMR のためにすべてのモジュール関係を追跡するモジュールグラフに至るまでの一連の流れを詳しく解説します。