こんにちは。テックリードの私だ。
日々のフロントエンド開発において、`vite dev` の圧倒的な立ち上がり速度や、プロダクションビルドにおける Rollup の堅牢なバンドル機構に恩恵を受けていない現場はもはやないだろう。しかし、プロダクトが大規模化し、カスタムのコード変換、HTMLの動的インジェクション、あるいはビルド成果物を特定のリモートストレージやバックエンドの静的ディレクトリへ自動同期させる必要に迫られた瞬間、多くのエンジニアが「プラグインの挙動の闇」に迷い込む。
「なぜかこのプラグインの変換結果が別のプラグインに反映されない」
「`writeBundle` でファイルを書き出したつもりが、次のプロセスで消えている」
「開発サーバー(Dev Server)とプロダクションビルドで、フックの実行順序がブレてバグる」
これらはすべて、Viteの基盤である Rollup 互換フックのライフサイクル と `enforce` オプションの物理的意味 を解像度高く理解していないことが原因だ。
今回は、Viteプラグイン開発における `buildStart` から `closeBundle` までのパイプラインを完全に解剖し、実務で絶対に踏むべき地雷を回避しながら、開発・ビルドパイプラインを極限まで最適化するプロの知見を授けよう。
—
1. Vite / Rollup プラグインライフサイクルの全体像
Viteのプラグインは、Rollupのプラグインシステムをベースに拡張されている。だが、開発サーバー起動時(Serve) と プロダクションビルド時(Build) で、通るパイプラインの位相が異なる点に注意が必要だ。
まずは、プロダクションビルド(`vite build`)における標準的なフックの実行順序を脳内に焼き付けてほしい。
[CLI: vite build]
│
├─► config (Vite設定の最終調整)
├─► configResolved (解決済みの設定オブジェクト共有)
│
├─► options (Rollupへ渡すオプションの調整)
├─► buildStart ★【第一関門】ビルドの口火。AST解析の準備や初期化
│
├─► (各モジュールの解決・ロード・変換ループ)
│ ├─► resolveId
│ ├─► load
│ └─► transform ★ 最も頻繁に呼ばれるコード変形フック
│
├─► moduleParsed (モジュールのパース完了通知)
├─► resolveDynamicImport (動的インポートの解決)
│
├─► buildEnd ★ すべてのモジュール走査終了。エラー有無の検知
│
├─► outputOptions (出力設定の調整)
├─► renderStart (バンドル生成プロセスの開始)
├─► banner / footer / intro / outro (コード装飾)
├─► augmentChunkHash(チャンクハッシュのカスタム)
├─► renderChunk ★ バンドルされたチャンク単位でのコード変換
├─► generateBundle ★【最重要】メモリ上の全成果物(Chunk/Asset)の操作・追加
│
├─► writeBundle ★【実ファイル書き出し】ディスクへの物理出力完了
└─► closeBundle ★【終端】プロセス終了、クリーンアップ
このパイプラインにおいて、初心者が最も混同しやすいのが `generateBundle` と `writeBundle` の違い、そして 「どこでファイルを操作すべきか」 という点だ。
—
2. 決定版:ファイルシステム操作の最適フック選択
独自のHTML書き換え、Sitemap自動生成、あるいはビルド成果物をZIPにまとめて別サーバーへ飛ばす処理を実装する場合、どのフックを選ぶべきか?
結論から言えば、「ディスクに書き出す前(メモリ上)」に何かをねじ込みたいなら `generateBundle` を使い、「ディスクに書き出された実ファイル群を確定させてから何かをしたい(デプロイ等)」なら `writeBundle` または `closeBundle` を使うべきだ。
実例:ビルド成果物のサイズを検証し、特定のメタデータをJSONとして出力するプラグイン
以下のコードを見てほしい。これは `generateBundle` フックを利用して、バンドルされた全アセットのサイズを計算し、ビルド成果物の中に `build-manifest.json` を動的に追加する実用的なプラグインのコードだ。
import { Plugin } from ‘vite’;
export function assetAnalyzerPlugin(): Plugin {
return {
name: ‘vite-plugin-asset-analyzer’,
// プロダクションビルド時のみ動作させる
apply: ‘build’,
// メモリ上のバンドルが確定し、ディスクへ書き出される直前のフック
generateBundle(options, bundle) {
const manifest: Record
// bundleオブジェクトには、生成されたChunk(JS/CSS)とAsset(画像等)がすべて格納されている
for (const fileName of Object.keys(bundle)) {
const file = bundle[fileName];
// チャンクかアセットかに応じてサイズを取得
const size = file.type === ‘chunk’
? Buffer.byteLength(file.code, ‘utf-8’)
: Buffer.byteLength(file.source);
manifest[fileName] = size;
}
// ──【超重要】メモリ上のバンドルへ動的にファイルを追加する ──
// ディスクに触れていないこのタイミングであれば、ロールアップの出力物としてインクルードされる
this.emitFile({
type: ‘asset’,
fileName: ‘build-manifest.json’,
source: JSON.stringify(manifest, null, 2),
});
}
};
}
なぜ `writeBundle` ではなく `generateBundle` なのか?
`generateBundle` の段階では、成果物はまだ Node.js のファイルシステム(HDD/SSD)に書き出されておらず、Vite(Rollup)の内部メモリ上に存在する。そのため、`this.emitFile()` を使って新たなアセットをバンドルプロセスに安全にねじ込むことができる。これが `writeBundle` だと、すでにディスク書き出しが完了しているため、ファイルを追加してもマニフェストやハッシュの整合性が崩れるリスクがある。
—
3. プラグインの競合を粉砕する `enforce` オプションの科学
複数の大規模プラグイン(例: `@vitejs/plugin-vue` と独自のマジックトランスフォーマー)を同時に動かした際、「期待した順序で変換がかからない」という現象に直面したことはないか?
Viteのプラグインはデフォルトで以下の順序・グループで実行される。
1. `alias` の設定(パスエイリアスの解決)
2. `enforce: ‘pre’` が指定されたユーザープラグイン
3. Viteのコアプラグイン
4. 通常のユーザープラグイン(`enforce` なし)
5. Viteのビルド用ポストプラグイン
6. `enforce: ‘post’` が指定されたユーザープラグイン(ミニファイアやバンドル分析など)
7. Viteの圧縮(Minify)プラグイン
競合を防ぐための設定ベストプラクティス (`vite.config.ts`)
チーム開発において、プラグインの実行順序を暗黙の了解に頼るのはバグの温床だ。以下のように、明示的に `enforce` を制御しつつ、環境に応じたロード制御(`apply`)を記述するのがプロの作法である。
import { defineConfig } from ‘vite’;
import vue from ‘@vitejs/plugin-vue’;
import { assetAnalyzerPlugin } from ‘./plugins/assetAnalyzer’;
export default defineConfig({
plugins: [
// 1. フレームワークのコアプラグイン(通常は自動挿入されるが明示的にも書ける)
vue(),
// 2. コードの根底を書き換えるため、最優先(pre)で走らせる独自マッパー
{
name: ‘vite-plugin-env-injector’,
enforce: ‘pre’, // コアプラグインや通常プラグインよりも先に実行
apply: ‘serve’, // 開発サーバー起動時のみ有効化
transform(code, id) {
if (id.endsWith(‘.ts’)) {
// 例: 開発時特有のデバッグコードをインジェクション
return code.replace(‘__DEV_BARRIER__’, ‘console.log(“[DEBUG MODE ACTIVE]”);’);
}
}
},
// 3. 通常のプラグイン(enforce指定なし)
assetAnalyzerPlugin(),
// 4. すべてのバンドル・変換が終わった後に結果を監査するポストプラグイン
{
name: ‘vite-plugin-bundle-auditor’,
enforce: ‘post’, // 圧縮や最終アセット生成の直前・直後に割り込む
apply: ‘build’, // プロダクションビルド時のみ
writeBundle(options, bundle) {
console.log(‘✨ [Auditor] 全てのバンドル成果物の書き出しが完了しました。CIへの通知処理を実行可能です。’);
}
}
]
});
—
4. チーム開発の生産性を爆発させる設計と共有化ルール
プラグインやViteの設定は、個人のローカル環境で勝手に最適化されていてはチーム開発において意味がない。組織全体のビルド速度と開発体験(DX)を底上げするための実践知を共有しよう。
A. 開発スピードを極限まで高める隠れたキーボードショートカット
ViteのインタラクティブなCLI操作を見落としていないか? `vite dev` 実行中のターミナルで以下のキーを押すことで、開発効率が跳ね上がる。
- `r` + `Enter`: 手動でサーバーを再起動(Restart server)。設定ファイル (`vite.config.ts`) を書き換えた際の強制リフレッシュに。
- `u` + `Enter`: ローカルネットワーク用のURL(QRコード付き)を表示。スマホや別端末での実機検証を一瞬で開始。
- `o` + `Enter`: デフォルトブラウザでアプリを自動オープン。
- `h` + `Enter`: ヘルプメニューの呼び出し。
B. チーム全員の環境を統一する設定ファイル群のベストプラクティス
チームメンバー間でNodeのバージョンやViteの挙動差異による「私の環境では動くのに」を根絶するため、プロジェクトルートには以下の構成を厳格に敷く。
1. `.nvmversion` または `package.json` の `engines`
{
“name”: “enterprise-frontend-app”,
“private”: true,
“engines”: {
“node”: “>=20.11.0”,
“npm”: “>=10.2.0”
},
“scripts”: {
“dev”: “vite –host”,
“build”: “tsc && vite build”,
“preview”: “vite preview”
}
}
> 知見: `–host` フラグをデフォルトの `dev` スクリプトに仕込んでおくことで、チームメンバーが社内ネットワーク内の別PCや検証用タブレットから `http://
2. チーム共有の `vite.config.ts` 堅牢アーキテクチャ
環境変数(`mode`)を駆使し、開発(Dev)と本番(Build)でプラグインのオーバーヘッドを完全に分離する構造がこれだ。
import { defineConfig, loadEnv } from ‘vite’;
import vue from ‘@vitejs/plugin-vue’;
import { visualizer } from ‘rollup-plugin-visualizer’;
export default defineConfig(({ command, mode }) => {
// 開発/本番に応じた .env.
const env = loadEnv(mode, process.cwd(), ”);
const isProduction = command === ‘build’;
return {
// 開発時のベースパスやプロキシ設定の共通化
server: {
port: 3000,
strictPort: true, // 3000番が埋まっている時に勝手に別ポートへ逃げずエラーにする(CIやDockerでの誤動作防止)
proxy: {
‘/api’: {
target: env.VITE_API_BASE_URL || ‘http://localhost:8080’,
changeOrigin: true,
}
}
},
// ビルド最適化設定
build: {
target: ‘esnext’,
sourcemap: !isProduction, // 本番ではソースマップを隠蔽、開発では高速デバッグ用に有効化
rollupOptions: {
output: {
// キャッシュバスティングとCDN配信を見据えたアセット命名規則の統一
chunkFileNames: ‘assets/js/[name]-[hash].js’,
assetFileNames: ‘assets/[ext]/[name]-[hash].[ext]’,
}
}
},
plugins: [
vue(),
// 依存関係の肥大化を防ぐため、アナライザーはANALYZE=true環境変数時、かつビルド時のみ走らせる
isProduction && process.env.ANALYZE === ‘true’ && visualizer({
open: true,
filename: ‘dist/stats.html’,
gzipSize: true,
})
].filter(Boolean) // booleanのfalseをフィルタリングして安全にプラグイン配列を構築
};
});
—
5. テックリードからの総括
Vite/Rollupのプラグイン開発やライフサイクル制御は、一見すると複雑な黒魔術のように見えるかもしれない。しかし、「どのデータがどのメモリ空間に存在し、どのタイミングでディスクに落ちるのか」というデータの流れ(パイプライン)さえロジカルに把握してしまえば、これほど柔軟で強力なビルドシステムはない。
`buildStart` で仕込み、`generateBundle` でデータを巧みに操り、`writeBundle` で安全に地面へ着地させる。この一連のライフサイクルをあなたのプロジェクトの要件に合わせて完璧にハンドリングできた時、チームのビルドパイプラインは異次元のスピードと堅牢性を手に入れることになる。
さあ、今すぐ `vite.config.ts` と自作プラグインを見直し、無駄な処理や曖昧なフックの実行順序を洗練されたアーキテクチャへと昇華させてほしい。