Read OSS

プラグインシステム:フック実行・順序制御・コアプラグイン

上級

前提知識

  • 第1回:アーキテクチャとコードベースの探索
  • 第2回:設定と環境システム
  • 第3回:Dev Serverとトランスフォームパイプライン
  • Rollup/Rolldownのプラグインフック(resolveId、load、transform)の基礎知識

プラグインシステム:フック実行・順序制御・コアプラグイン

ViteのプラグインシステムはReact、Vue、Svelte、そして数百ものコミュニティ統合を単一のインターフェースで支える拡張性の要です。RolldownのプラグインAPIをVite固有のフックで拡張し、精密な順序制御のための二層ソートシステムを実装しています。さらに、フィルターが一致しない場合にフック呼び出しを早期スキップする最適化レイヤーも備えています。

この記事ではプラグインインターフェースの設計、28以上のプラグインを正しい順序で組み立てる仕組み、不要なフック呼び出しを回避するフィルターキャッシュの仕組み、そして重要なコアプラグインの内部実装を掘り下げていきます。

プラグインインターフェースとVite固有のフック

ViteのPluginインターフェースはRolldownのRolldownPluginを拡張し、バンドラーコンテキストには存在しないフックを追加しています。

フック 実行タイミング 目的
config 設定解決前 ユーザー設定を変更する
configResolved 設定解決後 最終設定の読み取り・参照保存
configEnvironment 環境ごと 環境固有オプションの変更
configureServer サーバー作成時 middlewareの追加、サーバーインスタンスへのアクセス
configurePreviewServer プレビューサーバー作成時 プレビュー用に同様の処理
hotUpdate dev時のファイル変更 HMRの動作をカスタマイズ
buildApp ビルドのオーケストレーション 複数環境のビルド順序を制御
transformIndexHtml HTML処理 スクリプトの注入、HTMLの変更

TypeScriptとの統合も見どころのひとつです。Viteは86〜89行目で**宣言マージ(declaration merging)**を使ってRolldownの型を拡張しています。

declare module 'rolldown' {
  export interface MinimalPluginContext extends PluginContextExtension {}
  export interface PluginContextMeta extends PluginContextMetaExtension {}
}

この仕組みにより、すべてのRolldownプラグインコンテキストは自動的にthis.environment(現在のEnvironmentインスタンス)を持つようになります。プラグイン作成者はキャストなしにthis.environment.configthis.environment.mode、その他の環境固有データへアクセスできます。

コード内にはさらにappプラグインenvironmentプラグインという2種類のプラグインが定義されています。environmentプラグインは環境ごとに一度呼び出されるコンストラクタ関数で生成されるため、configconfigureServerといったアプリレベルのフックは定義できません。

プラグインの順序制御:enforceとorder

Viteは二層のソートシステムを採用しています。第一層はenforceフィールドでユーザープラグインを'pre'・normal(enforceなし)・'post'の3グループに分割します。第二層は各フックのorderフィールドを使って、グループ内の個々のフックをさらにソートします。

resolvePlugins関数が最終的なパイプラインを組み立てます。

flowchart LR
    subgraph "Pre Zone"
        A1["optimizedDepsPlugin"]
        A2["watchPackageDataPlugin"]
        A3["preAliasPlugin"]
        A4["aliasPlugin (native or JS)"]
        A5["...user pre plugins"]
    end
    subgraph "Normal Zone"
        B1["modulePreloadPolyfillPlugin"]
        B2["oxcResolvePlugin (2 instances)"]
        B3["htmlInlineProxyPlugin"]
        B4["cssPlugin"]
        B5["oxcPlugin"]
        B6["nativeJsonPlugin"]
        B7["wasmHelperPlugin / webWorkerPlugin"]
        B8["assetPlugin"]
        B9["...user normal plugins"]
    end
    subgraph "Post Zone"
        C1["nativeWasmFallbackPlugin"]
        C2["definePlugin"]
        C3["cssPostPlugin"]
        C4["buildHtmlPlugin"]
        C5["...build plugins"]
        C6["...user post plugins"]
        C7["clientInjectionsPlugin"]
        C8["cssAnalysisPlugin"]
        C9["importAnalysisPlugin"]
    end
    A5 --> B1
    B9 --> C1

第二層を実装しているのがgetSortedPluginsByHook関数です。フックが呼び出されるたびに、プラグインはフックのorderプロパティに基づいて再ソートされます。実装は最適化されており、一時配列を3つ作る代わりにインデックス追跡で結果配列に直接挿入します。

let pre = 0, normal = 0, post = 0
for (const plugin of plugins) {
  const hook = plugin[hookName]
  if (hook) {
    if (typeof hook === 'object') {
      if (hook.order === 'pre') {
        sortedPlugins.splice(pre++, 0, plugin)
        continue
      }
      if (hook.order === 'post') {
        sortedPlugins.splice(pre + normal + post++, 0, plugin)
        continue
      }
    }
    sortedPlugins.splice(pre + normal++, 0, plugin)
  }
}

ヒント: clientInjectionsPlugincssAnalysisPluginimportAnalysisPluginの3つのdev専用プラグインは常に最後尾に配置されます。これらは最終出力を解析してimportを書き換えたりHMRクライアントを注入したりするため、他のすべてのtransformが完了した後に実行される必要があります。

フィルター最適化システム

Viteにはフィルターシステムが組み込まれており、プラグインは自身のフックが処理対象とするファイルを事前に宣言できます。これによりプラグインコンテナは一致しないファイルへのフック呼び出しを完全にスキップできます。この仕組みはpluginFilter.tsに実装されています。

getCachedFilterForPlugin関数はプラグインフックからフィルター宣言を取り出し、WeakMapにキャッシュします。

flowchart TD
    HOOK["Plugin hook with filter"] --> EXTRACT["extractFilter(hook)"]
    EXTRACT --> CHECK{"Hook type?"}
    CHECK -->|resolveId| IDFilter["createIdFilter(rawFilter)"]
    CHECK -->|load| IDFilter
    CHECK -->|transform| TFilter["createFilterForTransform(id, code, moduleType)"]
    IDFilter --> CACHE["WeakMap<Plugin, FilterValue>"]
    TFilter --> CACHE
    CACHE --> USE["Plugin container checks filter before calling hook"]

transformフックのフィルターはモジュールIDとコードの内容の両方をチェックできます。特定のパターンを含むコードだけを処理したいプラグインに特に有効です(例:JSXを変換するプラグインは、JSX構文を含むコードのときだけ実行すれば十分です)。

patternToIdFilter関数はRegExpとglobパターンの両方をサポートし、globマッチングにはpicomatchを使用しています。マッチング前にすべてのパスはフォワードスラッシュに正規化されます。

ネイティブプラグインとJSプラグインの使い分け

Vite 8における重要な設計上の決断のひとつが、可能な限りネイティブのRolldownプラグインを使用するという方針です。resolvePluginsを見ると、そのパターンが確認できます。

import {
  viteAliasPlugin as nativeAliasPlugin,
  viteJsonPlugin as nativeJsonPlugin,
  viteWasmFallbackPlugin as nativeWasmFallbackPlugin,
  oxcRuntimePlugin,
} from 'rolldown/experimental'

60〜73行目のaliasプラグイン選択がフォールバック戦略をわかりやすく示しています。

isBundled && !config.resolve.alias.some((v) => v.customResolver)
  ? nativeAliasPlugin({ entries: config.resolve.alias.map(/*...*/) })
  : aliasPlugin({ entries: config.resolve.alias, customResolver: viteAliasCustomResolver })

カスタムリゾルバーがない場合はビルド時にネイティブのRustプラグインが使われ、それ以外はJSの@rollup/plugin-aliasにフォールバックします。一般的なケースで最大のパフォーマンスを発揮しながら、柔軟性も損なわない設計です。

コアプラグイン詳解

Resolveプラグイン

oxcResolvePluginはRolldownのネイティブviteResolvePluginをVite固有のロジックでラップします。開発モード用にオプションでoptimizerResolvePluginを含むプラグインの配列を返し、perEnvironmentOrWorkerPluginを使って環境ごとにネイティブリゾルバーを生成します。

このリゾルバーはViteの特殊なURLプレフィックス(/@fs//@id/)、browserフィールドマッピング、条件付きexports、オプショナルpeer deps、依存関係オプティマイザーとの統合など多岐にわたる処理を担います。finalizeBareSpecifierコールバックがimport React from 'react'のようなベアインポートを事前バンドル済みの依存関係URLにリダイレクトします。

Import Analysisプラグイン

importAnalysisPluginはモジュール内のすべてのimport文を書き換えるdev専用プラグインです。完全なASTを構築せずにes-module-lexerでimportをパースし、MagicStringで書き換えます。ベアインポートは最適化された依存関係のURLに変換され、相対importにはキャッシュバスティング用のタイムスタンプクエリが付加され、HMR API(import.meta.hot)も検出・処理されます。

CSSプラグイン

3500行以上にのぼるcssPluginは最大のコアプラグインです。担当する処理は多岐にわたります。

  • CSSプリプロセッサ(Sass、Less、Stylus)のオプショナルpeer依存関係による対応
  • 設定の自動検出を伴うPostCSS処理
  • スコープ付きクラス名によるCSS Modules
  • 代替エンジンとしてのLightning CSS
  • すべてのtransformステージをまたぐソースマップの処理
  • devモードでのインジェクション(インライン<style>タグ)とビルドモードでの抽出

このプラグインはcssPlugin(前処理)、cssPostPlugin(後処理と抽出)、cssAnalysisPlugin(dev専用のimport追跡)の3つに分割されています。

次回の内容

プラグインがどのようにソートされ、フィルタリングされ、28以上のコンポーネントからなるパイプラインに組み立てられるかを見てきました。次回はファイルが変更されたときに何が起こるかを追います。HMRシステムがモジュールグラフを辿って更新境界を探し、ブラウザにペイロードを送信し、クライアントが更新されたモジュールを再インポートする仕組みを解説します。あわせて依存関係の事前バンドルについても掘り下げていきます。RolldownがどのようにimportをスキャンしてNode modulesをバンドルし、キャッシュから配信するかまで踏み込んで見ていきましょう。