【実務・中級編】Viteの『Virtual Modules』で実現する動的設定生成:ビルド時にファイルを生成せずにコードを差し込む裏技 – ビルド・パッケージ管理ツール生産性向上バイブル

こんにちは。テックリードの私だ。

近年のフロントエンド開発において、Viteはその圧倒的なHMR(Hot Module Replacement)の速度でWebpack時代の「ビルド待ちの苦行」を過去のものにしてくれた。しかし、Viteの真のポテンシャルが「ネイティブESMの高速配信」だけにあると思っているなら、それは氷山の一角に過ぎない。

Viteの内部アーキテクト層に踏み込むと、 Rollupのプラグイン機構をベースにした「Virtual Modules(仮想モジュール)」という、開発体験(DX)と型安全性を極限まで引き上げる強力な裏技が隠されている。

今回は、ビルド時に物理ファイルを一切生成せず、メモリ上だけで動的にコードを差し込み、さらにバックエンドのAPIスキーマから完全な型安全性をフロントエンドへ即座に反映させる、極めて実践的なプラグイン開発の手法を伝授しよう。

—

なぜ「仮想モジュール」が必要なのか?(アーキテクトの視点)

通常、アプリケーション内で環境変数やビルドメタデータ、外部スキーマを扱おうとすると、以下のような泥臭いアプローチになりがちだ。

1. Viteの `build.sart` やカスタムスクリプトで、JSONやTSファイルを一時ファイルとして物理的に生成する。
2. `.gitignore` にそれらの生成物を追加し忘れてGitが汚れる。
3. ファイルウォッチャー(Chokidar等)の競合により、HMRが無限ループする。
4. ビルド順序(Race Condition)の制御に失敗し、CI/CDでビルドが不定期に落ちる。

仮想モジュール(Virtual Modules)は、これらの問題を根絶する。

仮想モジュールとは、実態としてのファイルシステム上のファイルを持たないモジュールだ。名前に `virtual:` などの一意のプレフィックスを付与し、Vite(Rollup)の解決(Resolve)フックとロード(Load)フックをフックすることで、importされた瞬間にメモリ上で動的にコード文字列を生成して流し込む。

物理ファイルを作らないため、ディスクI/Oが発生せず、Gitの汚染もゼロ、HMRとの統合も完全にコントロールできる。

—

実践:APIスキーマを動的に読み込み型安全に注入するプラグイン

今回は、サーバーサイドのOpenAPIスキーマ(あるいはGraphQLのスキーマ定義)をビルド/開発サーバー起動時に読み込み、それを「仮想モジュール」としてフロントエンドに型付きでインポートできるようにするカスタムプラグインを実装する。

1. プロジェクト構造とベストプラクティス構成

まず、Viteの設定ファイルとプラグインの配置構成を確認する。大規模開発において、プラグインは `vite.config.ts` に直書きせず、独立したモジュールとして管理するのが鉄則だ。

my-enterprise-app/
├── 📁 src/
│ ├── 📁 generated/ <-- 手動生成物は置かない。型定義(.d.ts)のみを配置 │ │ └── api-schema.d.ts │ ├── main.ts │ └── vite-env.d.ts ├── 📁 plugins/ │ └── vite-plugin-virtual-schema.ts <-- 今回の主役となるプラグイン ├── openapi.yaml <-- バックエンドから共有されるスキーマ ├── package.json └── vite.config.ts

2. 神プラグインの実装:`vite-plugin-virtual-schema.ts`

以下のコードは、仮想モジュールの解決とロードを完全に制御するプロダクション品質のプラグイン実装だ。

import type { Plugin } from ‘vite’;
import fs from ‘node:fs/promises’;
import path from ‘node:path’;
import { parse } from ‘yaml’; // YAMLパーサー(例: yamlパッケージ)

interface VirtualSchemaPluginOptions {
schemaPath: string; // 読み込むスキーマファイルのパス
}

export function virtualSchemaPlugin(options: VirtualSchemaPluginOptions): Plugin {
// 仮想モジュールを識別するためのIDプレフィックス
const virtualModuleId = ‘virtual:api-schema’;
const resolvedVirtualModuleId = ‘\0’ + virtualModuleId; // Rollupの標準的な非実体ファイル用プレフィックス ‘\0’

let rawSchemaCache: any = null;

return {
name: ‘vite-plugin-virtual-schema’,

// 1. 開発サーバー起動時およびファイル変更時にスキーマをキャッシュ
async buildStart() {
try {
const absolutePath = path.resolve(process.cwd(), options.schemaPath);
const fileContent = await fs.readFile(absolutePath, ‘utf-8’);
rawSchemaCache = parse(fileContent);

// Viteの開発サーバー監視対象にスキーマファイルを追加する
// これにより、スキーマ変更時にHMR経由で仮想モジュールが再評価される
this.addWatchFile(absolutePath);
} catch (error) {
this.error(`[VirtualSchemaPlugin] Failed to load schema: ${error}`);
}
},

// 2. インポートされた識別子が仮想モジュールと一致するか判定
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
return null;
},

// 3. 一致した場合、メモリ上で動的にコード(JS/TS)を生成して返却
load(id) {
if (id === resolvedVirtualModuleId) {
// スキーマからエンドポイントの一覧やメタデータを抽出し、コードとして組み立てる
const endpoints = Object.keys(rawSchemaCache.paths || {});

// フロントエンドに提供するコードを動的構築
return `
// 仮想モジュールとして動的に生成されたエンドポイント定義
export const apiEndpoints = ${JSON.stringify(endpoints)};
export const rawSchema = ${JSON.stringify(rawSchemaCache)};

export function getEndpointSummary(path) {
return rawSchema.paths[path]?.summary || ‘No summary’;
}
`;
}
return null;
},
};
}

3. `vite.config.ts` での設定統合

作成したプラグインをViteの設定に組み込む。環境ごとの挙動制御もここで完結させる。

import { defineConfig } from ‘vite’;
import { virtualSchemaPlugin } from ‘./plugins/vite-plugin-virtual-schema’;

export default defineConfig({
plugins: [
// 仮想モジュールプラグインの登録
virtualSchemaPlugin({
schemaPath: ‘./openapi.yaml’,
}),
],
server: {
port: 3000,
open: true, // 起動時にブラウザを自動オープン
},
resolve: {
alias: {
‘@’: ‘/src’,
},
},
});

4. TypeScriptへの型定義の教え込み(型安全性の担保)

仮想モジュールは実ファイルが存在しないため、TypeScriptのコンパイラ(tsc)はそのままでは型を解決できずエラーを吐く。`src/vite-env.d.ts` または専用の型定義ファイルにモジュールの型宣言を記述する。

// src/vite-env.d.ts

///

// ‘virtual:api-schema’ という仮想モジュールの型インターフェースをTSに教える
declare module ‘virtual:api-schema’ {
export const apiEndpoints: string[];
export const rawSchema: Record;
export function getEndpointSummary(path: string): string;
}

これで、アプリケーションコード側からは以下のように完全に型安全かつクリーンにインポートできる。

// src/main.ts
import { apiEndpoints, getEndpointSummary } from ‘virtual:api-schema’;

console.log(‘Available Endpoints:’, apiEndpoints);
// IDEの補完が完璧に効き、ビルド成果物は極限までクリーンに保たれる

—

チーム開発を加速させる実践設定・ツールチェーン

ここからは、上記の仮想モジュールパターンやVite環境をチーム全体で運用する上で、開発生産性を劇的に跳ね上げるための「実務知見」を共有しよう。

1. チーム全員の環境を統一する設定共有ルール(`settings.json`)

VSCodeを前提とした場合、拡張機能やフォーマッターの不一致は無駄なレビューコストを生む。プロジェクトルートの `.vscode/settings.json` に以下の設定を強制し、Vite開発のストレスをゼロにする。

{
// 保存時に自動でESLintとPrettierを走らせ、コードの揺れを完全排除
“editor.codeActionsOnSave”: {
“source.fixAll.eslint”: “explicit”
},
“editor.formatOnSave”: true,
“editor.defaultFormatter”: “esbenp.prettier-vscode”,

// 仮想モジュールの型解決切れをVSCode上で即座に検知させるためのTypeScript言語サーバー設定
“typescript.tsdk”: “node_modules/typescript/lib”
}

2. 開発スピードを最大化する隠れたショートカット & CLIテクニック

テックリードとして、メンバーにはマウスを使った操作を極力排除させたい。ViteのCLIとターミナル操作を極めることで、開発のリズムが劇的に向上する。

  • `r` + `Enter`(開発サーバー起動中のCLIコマンド):

Viteの開発サーバー起動中にターミナルで `r` を押してEnterを押すと、サーバーを再起動せずにクライアントモジュールの手動リロード(Manual Reload)が瞬時に走る。仮想モジュールのキャッシュを強制クリアしたい時に極めて有効。

  • `u` + `Enter`:

ViteのサーバーURL(`http://localhost:3000` など)をクリップボードに自動コピーする。ブラウザを開く手間すら省ける。

  • `h` + `Enter`:

利用可能なインタラクティブショートカットの一覧をターミナルに表示する。

—

まとめ:アーキテクトがもたらす「静けさと強靭さ」

今回紹介したViteの「仮想モジュール」を活用した動的設定生成は、単なるテクニックではない。
「ビルドプロセスを汚さない」「ディスクI/Oを発生させない」「型安全性を一切妥協しない」という、モダンフロントエンドアーキテクチャにおける3つの美徳を高次元で両立させるアプローチだ。

物理ファイル生成の呪縛から解放されたとき、あなたのプロジェクトのビルドパイプラインは驚くほど軽快になり、開発チームの背中には確かなスピードが宿る。ぜひ今日のプロダクトコードから導入し、その圧倒的な恩恵を体感してほしい。

タイトルとURLをコピーしました