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 つの読み込み戦略をサポートしています。
bundle(デフォルト)—vite.config.tsを Rolldown でコンパイルして一時的な JS ファイルを生成し、それをインポートする。TypeScript、パスエイリアス、node_modulesからのインポートを透過的に処理できる、最も堅牢なアプローチであるrunner(実験的)— Vite 独自のモジュールランナーを使って設定をオンザフライで処理する。一時ファイルの生成を省略できる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.js、vite.config.mjs、vite.config.ts、vite.config.cjs、vite.config.mts、vite.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> {
patchConfig と patchPlugins は内部フックです。マルチ環境ビルド時に 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 は client と ssr の環境が設定に必ず存在することを保証し、その順序も強制します。
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: true と recoverable: 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)に注目してください。clientInjectionsPlugin、cssAnalysisPlugin、importAnalysisPlugin の 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 のためにすべてのモジュール関係を追跡するモジュールグラフに至るまでの一連の流れを詳しく解説します。