T-CREATOR

Vite プラグインフック対応表:Rollup → Vite マッピング早見表

Vite プラグインフック対応表:Rollup → Vite マッピング早見表

Vite でプラグイン開発を行う際、Rollup のプラグインフックがそのまま使えることは大きなメリットです。しかし、Vite 独自のフックや、Rollup フックの拡張機能について理解していないと、せっかくの機能を活かしきれません。

本記事では、Rollup から Vite へのプラグインフックマッピングを体系的に整理し、どのフックがどのタイミングで実行されるのか、Vite ではどのような拡張が加えられているのかを明確にします。これにより、既存の Rollup プラグインを Vite に移行する際や、Vite プラグインを新規開発する際の指針となるでしょう。

プラグインフック早見表

ビルド時フック対応表

#Rollup フックVite での対応実行タイミング用途
1options✓ 対応ビルド開始前Rollup オプションの変更
2buildStart✓ 対応ビルド開始時初期化処理・グローバル状態の準備
3resolveId✓ 対応(拡張)モジュール解決時カスタムモジュール解決ロジック
4load✓ 対応(拡張)ファイル読み込み時カスタムローダー実装
5transform✓ 対応(拡張)コード変換時コードトランスパイル・変換処理
6buildEnd✓ 対応ビルド終了時クリーンアップ・統計情報出力
7closeBundle✓ 対応バンドル完了後最終処理・リソース解放

出力生成フック対応表

#Rollup フックVite での対応実行タイミング用途
1outputOptions✓ 対応出力前出力オプションの変更
2renderStart✓ 対応レンダリング開始時出力準備処理
3banner✓ 対応バンドル生成時ファイル先頭へのバナー追加
4footer✓ 対応バンドル生成時ファイル末尾へのフッター追加
5intro✓ 対応バンドル生成時ラッパー内部の先頭コード追加
6outro✓ 対応バンドル生成時ラッパー内部の末尾コード追加
7renderChunk✓ 対応チャンク生成時各チャンクの変換処理
8augmentChunkHash✓ 対応ハッシュ生成時チャンクハッシュのカスタマイズ
9generateBundle✓ 対応バンドル生成後アセット追加・バンドル変更
10writeBundle✓ 対応ファイル書き込み後書き込み完了後の処理

Vite 専用フック一覧

#Vite フック実行タイミング用途
1config設定解決前Vite 設定の変更
2configResolved設定解決後解決済み設定の参照
3configureServerdev サーバー構築時ミドルウェア追加・サーバーカスタマイズ
4configurePreviewServerpreview サーバー構築時プレビューサーバーのカスタマイズ
5transformIndexHtmlHTML 変換時HTML の変換・タグ注入
6handleHotUpdateHMR 更新時HMR 動作のカスタマイズ

フック実行順序対応表

#フェーズRollup/Vite 共通フックVite 専用フック備考
1設定-configconfigResolvedVite のみ
2サーバー起動-configureServer / configurePreviewServerdev/preview 時のみ
3ビルド開始optionsbuildStart--
4モジュール処理resolveIdloadtransform-各モジュールごとに繰り返し
5HTML 処理-transformIndexHtmlVite のみ
6ビルド終了buildEnd--
7出力生成outputOptionsrenderStart--
8チャンク処理banner​/​footer​/​intro​/​outrorenderChunk-各チャンクごとに実行
9バンドル生成augmentChunkHashgenerateBundlewriteBundle--
10完了closeBundle--

背景

Vite と Rollup の関係性

Vite は本番ビルドに Rollup を使用しており、プラグインシステムも Rollup のものをベースに設計されています。

mermaidflowchart TB
    vite["Vite プラグインシステム"]
    rollup["Rollup プラグインシステム"]
    original["Vite 独自フック"]
    extended["拡張された<br/>Rollup フック"]

    vite --> rollup
    vite --> original
    rollup --> extended

    rollup -.->|"options<br/>buildStart<br/>resolveId<br/>load<br/>など"| base["基本フック群"]
    original -.->|"config<br/>configureServer<br/>transformIndexHtml<br/>など"| viteonly["Vite 専用機能"]
    extended -.->|"ssr パラメータ<br/>追加プロパティ"| enhance["機能拡張"]

この図が示すように、Vite は Rollup のプラグイン仕様を継承しつつ、開発サーバーや HMR といった Vite 特有の機能に対応するための独自フックを追加しています。

Rollup プラグインの互換性

Vite は Rollup プラグインとの高い互換性を持ちますが、完全な互換性があるわけではありません。

互換性がある部分:

  • ビルド時のコアフック(resolveIdloadtransform など)
  • 出力生成フック(generateBundlewriteBundle など)
  • フックの実行順序とライフサイクル

互換性に注意が必要な部分:

  • 開発時と本番ビルド時で動作が異なるフック
  • Vite が拡張したパラメータや戻り値
  • SSR 固有の動作

多くの Rollup プラグインは Vite でそのまま動作しますが、開発サーバー特有の最適化を活用するには Vite 専用フックの理解が必要です。

課題

Rollup プラグインを Vite で使う際の混乱ポイント

Rollup プラグインを Vite に移行する際、以下のような課題に直面することがあります。

1. フックの実行タイミングの違い

開発時(dev サーバー)と本番ビルド時で、同じフックでも実行されるタイミングや回数が異なる場合があります。

typescriptexport default function myPlugin() {
  return {
    name: 'my-plugin',
    // このフックは開発時には各リクエストごとに実行される
    // 本番ビルド時には全モジュールに対して一度だけ実行される
    transform(code, id) {
      console.log('transform called for:', id);
      return code;
    },
  };
}

2. Vite 拡張パラメータの見落とし

Vite は Rollup のフックにパラメータを追加していますが、これを見落とすと機能を十分に活用できません。

typescript// Rollup での resolveId
resolveId(source: string, importer: string | undefined)

// Vite での resolveId(SSR パラメータが追加されている)
resolveId(
  source: string,
  importer: string | undefined,
  options: { ssr?: boolean }  // Vite が追加
)

3. 開発サーバー機能への対応不足

Rollup プラグインをそのまま使うと、HMR や開発サーバーのミドルウェアといった Vite の開発体験を向上させる機能が利用できません。

以下の図は、Rollup プラグインと Vite プラグインでカバーできる機能範囲の違いを示しています。

mermaidflowchart LR
    subgraph rollup["Rollup プラグインでカバー可能"]
        build["本番ビルド処理"]
        bundle["バンドル生成"]
        transform_r["コード変換"]
    end

    subgraph vite["Vite プラグイン独自機能"]
        dev["開発サーバー"]
        hmr["HMR 制御"]
        html["HTML 変換"]
        middleware["ミドルウェア"]
    end

    rollup -.->|"そのまま移植"| partial["部分的な機能のみ"]
    vite -.->|"専用フック追加"| full["完全な Vite 体験"]

解決策

フック対応マッピングの理解

Vite で効果的なプラグイン開発を行うには、各フックの対応関係と特性を理解することが重要です。

ビルド時フックの活用

Rollup の基本的なビルドフックは、Vite でもほぼそのまま使用できます。

options フック

typescriptimport type { Plugin } from 'vite';

export default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    // Rollup オプションを変更する
    options(opts) {
      return {
        ...opts,
        // 外部依存関係を追加
        external: [
          ...(opts.external || []),
          'my-external-lib',
        ],
      };
    },
  };
}

このフックは、ビルドプロセスが開始される前に Rollup の設定を動的に変更できます。外部ライブラリの指定や、入力オプションの調整に使用されます。

buildStart フック

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    // ビルド開始時の初期化処理
    buildStart(options) {
      console.log('ビルドを開始します');
      // グローバル状態の初期化
      // キャッシュのクリア
      // 統計情報の準備
    },
  };
}

ビルドの開始時に一度だけ実行され、プラグインの初期化処理に最適です。

モジュール処理フックの拡張

Vite は resolveIdloadtransform といったモジュール処理フックに対して、SSR や開発サーバー特有のパラメータを追加しています。

resolveId フック(Vite 拡張版)

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    resolveId(source, importer, options) {
      // Vite が追加した SSR フラグを活用
      if (options.ssr) {
        // SSR ビルド時の特別な解決ロジック
        if (source === 'client-only-lib') {
          return {
            id: 'ssr-alternative-lib',
            external: true,
          };
        }
      }

      // カスタムプロトコルの処理
      if (source.startsWith('virtual:')) {
        return source;
      }

      return null; // 他のプラグインや標準解決に委ねる
    },
  };
}

resolveId は、モジュールのインポートパスを解決する際に呼び出されます。仮想モジュールの実装や、SSR 時の条件分岐に活用できます。

load フック(Vite 拡張版)

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    load(id, options) {
      // 仮想モジュールのコンテンツを返す
      if (id === 'virtual:my-module') {
        return {
          code: `export const message = "Hello from virtual module";`,
          map: null, // ソースマップ
        };
      }

      // SSR 時の特別な処理
      if (options?.ssr && id.endsWith('.client.ts')) {
        return {
          code: `export default {};`, // クライアント専用モジュールを空にする
        };
      }

      return null;
    },
  };
}

load フックは、実際のファイルシステムからではなく、プラグインが生成したコードを返すことができます。仮想モジュールの実装に必須のフックです。

transform フック(Vite 拡張版)

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    transform(code, id, options) {
      // 特定の拡張子のファイルのみ処理
      if (!id.endsWith('.custom')) {
        return null;
      }

      // SSR フラグによる条件分岐
      const isSSR = options?.ssr === true;

      const transformedCode = code
        .replace(/CLIENT_SIDE/g, isSSR ? 'false' : 'true')
        .replace(/SERVER_SIDE/g, isSSR ? 'true' : 'false');

      return {
        code: transformedCode,
        map: null, // ソースマップを返すことも可能
      };
    },
  };
}

transform は最も頻繁に使用されるフックで、コードの変換処理を行います。トランスパイラやプリプロセッサの実装に使われます。

ビルド終了フック

buildEnd フック

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    buildEnd(error) {
      if (error) {
        console.error('ビルドエラーが発生しました:', error);
        // エラー処理
      } else {
        console.log('ビルドが正常に完了しました');
        // 統計情報の出力
        // 一時ファイルのクリーンアップ
      }
    },
  };
}

ビルドプロセスの終了時に呼び出され、エラーハンドリングやクリーンアップに使用されます。

closeBundle フック

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    closeBundle() {
      // すべてのバンドル処理が完了した後に実行
      console.log('すべてのバンドル処理が完了しました');
      // リソースの解放
      // 最終レポートの生成
    },
  };
}

すべての出力ファイルが書き込まれた後に実行されます。最終的なクリーンアップ処理に適しています。

出力生成フックの活用

出力生成フェーズでは、生成されるバンドルファイルを直接操作できます。

generateBundle フック

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    generateBundle(options, bundle) {
      // バンドルに新しいアセットを追加
      this.emitFile({
        type: 'asset',
        fileName: 'my-asset.txt',
        source: 'This is my custom asset',
      });

      // 既存のチャンクを変更
      for (const fileName in bundle) {
        const chunk = bundle[fileName];
        if (chunk.type === 'chunk') {
          // コードの末尾にコメントを追加
          chunk.code += '\n// Generated by my-plugin';
        }
      }
    },
  };
}

このフックでは、生成されたバンドルに新しいファイルを追加したり、既存のファイルを変更したりできます。

renderChunk フック

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    renderChunk(code, chunk, options) {
      // 各チャンクごとに実行される
      console.log(`Processing chunk: ${chunk.fileName}`);

      // コードの最適化や変換
      const optimizedCode = code.replace(
        /console\.log/g,
        '// console.log'
      );

      return {
        code: optimizedCode,
        map: null,
      };
    },
  };
}

各チャンクが生成されるたびに実行され、チャンク単位でのコード変換が可能です。本番ビルドでの最適化処理に使用されます。

Vite 専用フックの活用

Vite 独自のフックを活用することで、開発体験を大幅に向上させることができます。

config フック

typescriptimport type { Plugin, UserConfig } from 'vite';

export default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    // Vite 設定を変更
    config(config, { command, mode }) {
      // 開発時のみ適用する設定
      if (command === 'serve') {
        return {
          server: {
            port: 3000,
          },
        };
      }

      // 本番ビルド時の設定
      if (command === 'build') {
        return {
          build: {
            minify: 'terser',
          },
        };
      }
    },
  };
}

config フックは、ユーザーの設定と統合される前に Vite の設定を変更できます。プラグインが特定の設定を必要とする場合に使用します。

configResolved フック

typescriptexport default function myPlugin(): Plugin {
  let resolvedConfig: ResolvedConfig;

  return {
    name: 'my-plugin',
    // 解決済みの設定を保存
    configResolved(config) {
      resolvedConfig = config;
      console.log('最終的な設定:', config);
      // 他のフックで使用するために設定を保存
    },

    transform(code, id) {
      // 保存した設定を活用
      if (resolvedConfig.command === 'serve') {
        // 開発時の処理
      }
      return code;
    },
  };
}

すべての設定が解決された後に呼び出され、最終的な設定を参照できます。他のフックで設定情報を使用する際に便利です。

configureServer フック

typescriptimport type { Plugin, ViteDevServer } from 'vite';

export default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    configureServer(server: ViteDevServer) {
      // カスタムミドルウェアの追加
      server.middlewares.use((req, res, next) => {
        if (req.url === '/api/custom') {
          res.end('Custom API response');
          return;
        }
        next();
      });

      // サーバーインスタンスを保存して後で使用
      // HMR の手動トリガーなどに活用
    },
  };
}

開発サーバーが起動する際に呼び出され、カスタムミドルウェアの追加や、サーバーインスタンスへのアクセスが可能です。

transformIndexHtml フック

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    transformIndexHtml(html) {
      // HTML にタグを注入
      return html.replace(
        '</head>',
        `  <script src="/my-injected-script.js"></script>\n</head>`
      );
    },
  };
}

より高度な HTML 変換も可能です。

typescriptexport default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    transformIndexHtml: {
      // 実行順序を指定(pre または post)
      order: 'pre',
      handler(html, ctx) {
        // コンテキスト情報を活用
        console.log('処理中のファイル:', ctx.filename);
        console.log('バンドル情報:', ctx.bundle);

        // タグ形式で注入することも可能
        return {
          html,
          tags: [
            {
              tag: 'meta',
              attrs: {
                name: 'description',
                content: 'My app',
              },
              injectTo: 'head',
            },
            {
              tag: 'script',
              attrs: {
                src: '/my-script.js',
                type: 'module',
              },
              injectTo: 'body',
            },
          ],
        };
      },
    },
  };
}

transformIndexHtml は、index.html に対して変換処理を行います。メタタグの追加やスクリプトの注入に使用されます。

handleHotUpdate フック

typescriptimport type { Plugin, HmrContext } from 'vite';

export default function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    handleHotUpdate(ctx: HmrContext) {
      // 特定のファイル変更時の HMR 動作をカスタマイズ
      if (ctx.file.endsWith('.custom')) {
        console.log(
          'カスタムファイルが更新されました:',
          ctx.file
        );

        // 影響を受けるモジュールをフィルタリング
        const affectedModules = ctx.modules.filter(
          (mod) => {
            // 条件に基づいてモジュールを選択
            return mod.id?.includes('specific-path');
          }
        );

        // カスタムイベントをクライアントに送信
        ctx.server.ws.send({
          type: 'custom',
          event: 'custom-update',
          data: { file: ctx.file },
        });

        // HMR を適用するモジュールを返す
        return affectedModules;
      }
    },
  };
}

ファイルが変更された際の HMR の動作をカスタマイズできます。特定のファイルタイプに対する HMR の最適化や、カスタムイベントの送信に使用されます。

具体例

実践的なプラグイン開発例

ここでは、Rollup フックと Vite 専用フックを組み合わせた実践的なプラグインの例を紹介します。

環境変数注入プラグイン

開発時と本番ビルド時で異なる環境変数を注入するプラグインを作成します。

プラグインの基本構造

typescriptimport type { Plugin, ResolvedConfig } from 'vite';

interface EnvPluginOptions {
  prefix?: string;
  envFile?: string;
}

export default function envPlugin(
  options: EnvPluginOptions = {}
): Plugin {
  const { prefix = 'APP_', envFile = '.env' } = options;
  let config: ResolvedConfig;
  let envVariables: Record<string, string> = {};

  return {
    name: 'vite-plugin-env-injector',

    // ここにフックを実装していきます
  };
}

プラグインの基本構造を定義します。オプションで環境変数のプレフィックスと.envファイルのパスを指定できるようにしています。

設定解決フックの実装

typescriptexport default function envPlugin(
  options: EnvPluginOptions = {}
): Plugin {
  // ... 前述の変数定義

  return {
    name: 'vite-plugin-env-injector',

    // 設定が解決された後に環境変数を読み込む
    configResolved(resolvedConfig) {
      config = resolvedConfig;

      // .env ファイルから環境変数を読み込む
      const envPath = path.resolve(config.root, envFile);
      if (fs.existsSync(envPath)) {
        const envContent = fs.readFileSync(
          envPath,
          'utf-8'
        );
        envContent.split('\n').forEach((line) => {
          const match = line.match(/^([^=]+)=(.*)$/);
          if (match && match[1].startsWith(prefix)) {
            envVariables[match[1]] = match[2];
          }
        });
      }

      console.log(
        `読み込んだ環境変数: ${
          Object.keys(envVariables).length
        }個`
      );
    },
  };
}

configResolved フックで、最終的な設定を取得し、環境変数ファイルを読み込みます。

コード変換フックの実装

typescriptexport default function envPlugin(
  options: EnvPluginOptions = {}
): Plugin {
  // ... 前述の実装

  return {
    name: 'vite-plugin-env-injector',
    // ... 前述のフック

    // コード内の環境変数プレースホルダーを置換
    transform(code, id, transformOptions) {
      // node_modules は処理しない
      if (id.includes('node_modules')) {
        return null;
      }

      let transformedCode = code;
      let hasReplacement = false;

      // process.env.APP_* の形式を置換
      Object.entries(envVariables).forEach(
        ([key, value]) => {
          const pattern = new RegExp(
            `process\\.env\\.${key}`,
            'g'
          );
          if (pattern.test(transformedCode)) {
            transformedCode = transformedCode.replace(
              pattern,
              JSON.stringify(value)
            );
            hasReplacement = true;
          }
        }
      );

      // 変更がなければ null を返す
      return hasReplacement
        ? { code: transformedCode }
        : null;
    },
  };
}

transform フックで、コード内の環境変数参照を実際の値に置き換えます。

開発サーバーフックの実装

typescriptexport default function envPlugin(
  options: EnvPluginOptions = {}
): Plugin {
  // ... 前述の実装

  return {
    name: 'vite-plugin-env-injector',
    // ... 前述のフック

    // 開発サーバーに環境変数情報を表示するエンドポイントを追加
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        if (req.url === '/__env__') {
          res.setHeader('Content-Type', 'application/json');
          res.end(
            JSON.stringify(
              {
                variables: Object.keys(envVariables),
                count: Object.keys(envVariables).length,
                prefix,
              },
              null,
              2
            )
          );
          return;
        }
        next();
      });

      console.log(
        '環境変数情報: http://localhost:5173/__env__'
      );
    },
  };
}

configureServer フックで、開発サーバーに環境変数の情報を表示するカスタムエンドポイントを追加します。

以下の図は、このプラグインの処理フローを示しています。

mermaidflowchart TD
    start["プラグイン初期化"] --> configResolved["configResolved フック"]
    configResolved --> readEnv["環境変数ファイル読み込み"]
    readEnv --> store["envVariables に保存"]

    store --> server["configureServer<br/>(dev 時のみ)"]
    server --> middleware["/__env__ エンドポイント追加"]

    store --> transform["transform フック"]
    transform --> check["process.env.APP_* を検出"]
    check -->|"一致あり"| replace["実際の値に置換"]
    check -->|"一致なし"| skip["処理スキップ"]
    replace --> output["変換後のコード"]
    skip --> original["元のコード"]

マークダウンローダープラグイン

マークダウンファイルを Vue コンポーネントに変換するプラグインの例です。

resolveId と load フックの組み合わせ

typescriptimport type { Plugin } from 'vite';
import { marked } from 'marked';

export default function markdownPlugin(): Plugin {
  return {
    name: 'vite-plugin-markdown',

    // .md ファイルを仮想モジュールとして解決
    resolveId(id) {
      if (id.endsWith('.md')) {
        // 実際のファイルパスを返す
        return id;
      }
      return null;
    },
  };
}

.md ファイルのインポートを許可するために、resolveId でファイルを解決します。

load フックでマークダウンを読み込む

typescriptexport default function markdownPlugin(): Plugin {
  return {
    name: 'vite-plugin-markdown',
    // ... resolveId

    // マークダウンファイルの内容を読み込む
    load(id) {
      if (!id.endsWith('.md')) {
        return null;
      }

      // ファイルを読み込み
      const markdown = fs.readFileSync(id, 'utf-8');

      // マークダウンを HTML に変換
      const html = marked.parse(markdown);

      // Vue コンポーネントとしてエクスポート
      const component = `
        <template>
          <div class="markdown-content" v-html="html"></div>
        </template>

        <script setup>
        const html = ${JSON.stringify(html)};
        </script>
      `;

      return {
        code: component,
      };
    },
  };
}

load フックで、マークダウンファイルを読み込み、HTML に変換して Vue コンポーネントとして返します。

HMR 対応の追加

typescriptexport default function markdownPlugin(): Plugin {
  return {
    name: 'vite-plugin-markdown',
    // ... 前述のフック

    // マークダウンファイルが更新されたときの HMR 処理
    handleHotUpdate({ file, server }) {
      if (file.endsWith('.md')) {
        console.log(
          `マークダウンファイルが更新されました: ${file}`
        );

        // 該当モジュールを再読み込み
        const module =
          server.moduleGraph.getModuleById(file);
        if (module) {
          server.moduleGraph.invalidateModule(module);
        }

        // クライアントに更新を通知
        server.ws.send({
          type: 'full-reload',
        });

        return [];
      }
    },
  };
}

handleHotUpdate フックで、マークダウンファイルが更新された際に HMR を適用します。

バンドル分析プラグイン

ビルド後のバンドルサイズを分析して、レポートを生成するプラグインです。

ビルド情報の収集

typescriptimport type { Plugin, OutputBundle } from 'rollup';

interface BundleInfo {
  fileName: string;
  size: number;
  type: 'chunk' | 'asset';
}

export default function bundleAnalyzerPlugin(): Plugin {
  const bundleInfos: BundleInfo[] = [];

  return {
    name: 'vite-plugin-bundle-analyzer',

    // バンドル生成時に情報を収集
    generateBundle(options, bundle: OutputBundle) {
      for (const fileName in bundle) {
        const item = bundle[fileName];

        let size = 0;
        if (item.type === 'chunk') {
          size = Buffer.byteLength(item.code, 'utf-8');
        } else if (item.type === 'asset') {
          size = Buffer.byteLength(item.source, 'utf-8');
        }

        bundleInfos.push({
          fileName,
          size,
          type: item.type,
        });
      }
    },
  };
}

generateBundle フックで、各バンドルファイルのサイズ情報を収集します。

レポート生成

typescriptexport default function bundleAnalyzerPlugin(): Plugin {
  // ... 前述の実装

  return {
    name: 'vite-plugin-bundle-analyzer',
    // ... generateBundle フック

    // すべてのバンドル処理が完了した後にレポートを生成
    closeBundle() {
      // サイズでソート
      bundleInfos.sort((a, b) => b.size - a.size);

      // 合計サイズを計算
      const totalSize = bundleInfos.reduce(
        (sum, info) => sum + info.size,
        0
      );

      // レポートを生成
      console.log('\n📊 バンドル分析レポート\n');
      console.log(`合計サイズ: ${formatSize(totalSize)}\n`);

      bundleInfos.forEach((info, index) => {
        const percentage = (
          (info.size / totalSize) *
          100
        ).toFixed(2);
        console.log(
          `${index + 1}. ${info.fileName} (${
            info.type
          }): ${formatSize(info.size)} (${percentage}%)`
        );
      });

      // JSON ファイルとして出力
      const reportPath = 'dist/bundle-report.json';
      fs.writeFileSync(
        reportPath,
        JSON.stringify(
          { total: totalSize, bundles: bundleInfos },
          null,
          2
        )
      );

      console.log(`\n✅ 詳細レポート: ${reportPath}\n`);
    },
  };
}

// サイズをフォーマットする補助関数
function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024)
    return `${(bytes / 1024).toFixed(2)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}

closeBundle フックで、収集した情報を基にレポートを生成します。コンソールに出力するだけでなく、JSON ファイルとしても保存します。

以下の図は、バンドル分析プラグインの処理フローを示しています。

mermaidflowchart TD
    build["ビルド開始"] --> generate["generateBundle フック"]
    generate --> loop["各バンドルファイルを<br/>ループ処理"]
    loop --> chunk{type は?}
    chunk -->|chunk| codeSize["code のサイズ計算"]
    chunk -->|asset| assetSize["source のサイズ計算"]
    codeSize --> collect["bundleInfos に追加"]
    assetSize --> collect
    collect --> next{次のファイル?}
    next -->|"あり"| loop
    next -->|"なし"| close["closeBundle フック"]
    close --> sort["サイズでソート"]
    sort --> calc["合計サイズ計算"]
    calc --> console["コンソール出力"]
    console --> json["JSON ファイル出力"]
    json --> done["完了"]

フック実行順序の確認

実際にプラグインを作成して、各フックがどの順序で実行されるかを確認してみます。

実行順序確認プラグイン

typescriptimport type { Plugin } from 'vite';

export default function hookOrderPlugin(): Plugin {
  let counter = 0;
  const log = (hookName: string, detail?: string) => {
    console.log(
      `[${++counter}] ${hookName}${
        detail ? ': ' + detail : ''
      }`
    );
  };

  return {
    name: 'hook-order-debugger',

    // Vite 専用フック(設定フェーズ)
    config() {
      log('config');
    },

    configResolved() {
      log('configResolved');
    },

    configureServer() {
      log('configureServer');
    },
  };
}

設定フェーズのフックをログ出力します。

ビルドフェーズのフック

typescriptexport default function hookOrderPlugin(): Plugin {
  // ... 前述の実装

  return {
    name: 'hook-order-debugger',
    // ... 設定フェーズのフック

    // ビルドフェーズ
    options(opts) {
      log('options');
      return opts;
    },

    buildStart() {
      log('buildStart');
    },

    resolveId(source, importer) {
      log('resolveId', source);
      return null;
    },

    load(id) {
      log('load', id);
      return null;
    },

    transform(code, id) {
      log('transform', id);
      return null;
    },
  };
}

モジュール解決とコード変換のフックをログ出力します。これらは各モジュールごとに複数回実行されます。

出力フェーズのフック

typescriptexport default function hookOrderPlugin(): Plugin {
  // ... 前述の実装

  return {
    name: 'hook-order-debugger',
    // ... 前述のフック

    buildEnd() {
      log('buildEnd');
    },

    // 出力生成フェーズ
    outputOptions(opts) {
      log('outputOptions');
      return opts;
    },

    renderStart() {
      log('renderStart');
    },

    renderChunk(code, chunk) {
      log('renderChunk', chunk.fileName);
      return null;
    },

    generateBundle() {
      log('generateBundle');
    },

    writeBundle() {
      log('writeBundle');
    },

    closeBundle() {
      log('closeBundle');
    },
  };
}

出力生成からファイル書き込みまでのフックをログ出力します。

このプラグインを実行すると、以下のような順序でフックが実行されることが確認できます。

開発サーバー起動時:

csharp[1] config
[2] configResolved
[3] configureServer
[4] buildStart

本番ビルド時:

less[1] config
[2] configResolved
[3] options
[4] buildStart
[5] resolveId: ./src/main.ts
[6] load: /path/to/src/main.ts
[7] transform: /path/to/src/main.ts
... (各モジュールごとに resolveId → load → transform が繰り返される)
[N] buildEnd
[N+1] outputOptions
[N+2] renderStart
[N+3] renderChunk: index.js
[N+4] generateBundle
[N+5] writeBundle
[N+6] closeBundle

まとめ

Vite のプラグインシステムは、Rollup のフックを基盤としつつ、開発サーバーや HMR といった Vite 特有の機能に対応するための独自フックを追加しています。

重要なポイント:

  • Rollup フックの継承: resolveIdloadtransformgenerateBundle などの基本フックは Rollup と互換性があります
  • Vite による拡張: SSR パラメータや追加のコンテキスト情報が提供され、より柔軟な処理が可能です
  • Vite 専用フック: configconfigureServertransformIndexHtmlhandleHotUpdate などは Vite でのみ使用できます
  • 実行順序の理解: 設定 → ビルド → 出力の順序で各フックが実行されることを把握することが重要です
  • 適切なフック選択: 目的に応じて最適なフックを選択することで、効率的なプラグイン開発が可能です

Rollup プラグインを Vite に移行する際は、基本的な互換性を活かしつつ、Vite 専用フックを追加することで、より優れた開発体験を提供できます。

本記事で紹介したフック対応表と具体例を参考に、効果的な Vite プラグイン開発を進めていただければ幸いです。

関連リンク