DevToolBox無料
ブログ

Viteプラグイン開発:カスタムプラグインをゼロから作成 2026

13分by DevToolBox

Viteプラグイン開発:カスタム変換とDev Toolsの構築

ViteのプラグインAPIはRollupの薄いラッパーで、devサーバーフックが追加されています。

プラグインの解剖:フックとライフサイクル

Viteプラグインはnameとフックメソッドを持つシンプルなオブジェクトです。

// Minimal Vite plugin structure
// A plugin is just an object with a name and hooks
export default function myPlugin(options = {}) {
  return {
    name: 'vite-plugin-my-plugin',   // required, shown in warnings/errors
    enforce: 'pre',                   // 'pre' | 'post' | undefined

    // Called once when Vite starts
    configResolved(config) {
      console.log('Vite mode:', config.mode);
    },

    // Modify Vite config before it resolves
    config(config, { command }) {
      if (command === 'build') {
        return { build: { sourcemap: true } };
      }
    },

    // Transform file contents
    transform(code, id) {
      if (!id.endsWith('.vue')) return null; // skip non-Vue files
      return { code: transformedCode, map: sourceMap };
    },

    // Resolve module imports to file paths
    resolveId(source, importer) {
      if (source === 'virtual:my-module') {
        return 'virtual:my-module';  // '' prefix = virtual module
      }
    },

    // Load virtual module content
    load(id) {
      if (id === 'virtual:my-module') {
        return 'export const message = "Hello from virtual module!"';
      }
    }
  };
}

transformフック:ソースファイルの変換

transformフックは最もよく使われるフックです。

// Real-world transform: inject build metadata into source files
// Similar to how vite-plugin-inspect or vite-plugin-svgr work
import { readFileSync } from 'fs';
import { createHash } from 'crypto';

export default function buildMetaPlugin() {
  let config;
  const buildTime = new Date().toISOString();

  return {
    name: 'vite-plugin-build-meta',
    enforce: 'post',

    configResolved(resolvedConfig) {
      config = resolvedConfig;
    },

    transform(code, id) {
      // Only process files that import the meta module
      if (!code.includes('__BUILD_META__')) return null;

      const hash = createHash('sha256')
        .update(code)
        .digest('hex')
        .slice(0, 8);

      const meta = JSON.stringify({
        buildTime,
        mode: config.mode,
        hash,
        file: id.replace(config.root, ''),
      });

      const transformed = code.replace(
        /__BUILD_META__/g,
        meta
      );

      return {
        code: transformed,
        // Return null map to inherit original sourcemap
        map: null,
      };
    },
  };
}

// Usage in your app:
// import meta from 'virtual:build-meta';
// console.log(meta.buildTime);

resolveIdフック:カスタムモジュール解決

resolveIdフックはインポートパスを傍受してリダイレクトします。

// Advanced resolveId: path aliases + conditional resolution
// Replaces tsconfig paths in runtime (Vite handles build, but not always HMR)
import path from 'path';
import fs from 'fs';

export default function aliasPlugin(aliases = {}) {
  // aliases = { '@': './src', '~utils': './src/utils' }
  const resolvedAliases = Object.fromEntries(
    Object.entries(aliases).map(([key, value]) => [
      key,
      path.resolve(process.cwd(), value),
    ])
  );

  return {
    name: 'vite-plugin-advanced-alias',

    resolveId(source, importer, options) {
      for (const [alias, target] of Object.entries(resolvedAliases)) {
        if (source === alias || source.startsWith(alias + '/')) {
          const resolved = source.replace(alias, target);

          // Try multiple extensions
          for (const ext of ['', '.ts', '.tsx', '.js', '/index.ts']) {
            const candidate = resolved + ext;
            if (fs.existsSync(candidate)) {
              return candidate;
            }
          }
        }
      }

      // Return null to let other resolvers handle it
      return null;
    },
  };
}

// vite.config.ts usage:
// plugins: [aliasPlugin({ '@': './src', '~hooks': './src/hooks' })]

configureServer:Devサーバーミドルウェア

configureServerフックはVite devサーバーへの完全なアクセスを提供します。

// Custom dev server middleware: mock API + WebSocket HMR notifications
export default function devApiPlugin(routes = {}) {
  return {
    name: 'vite-plugin-dev-api',
    apply: 'serve',   // Only active in dev server, not build

    configureServer(server) {
      // Add middleware BEFORE Vite's internal middlewares
      server.middlewares.use('/api', (req, res, next) => {
        const handler = routes[req.url];
        if (!handler) return next();

        res.setHeader('Content-Type', 'application/json');
        res.setHeader('Access-Control-Allow-Origin', '*');

        // Support async handlers
        Promise.resolve(handler(req))
          .then(data => res.end(JSON.stringify(data)))
          .catch(err => {
            res.statusCode = 500;
            res.end(JSON.stringify({ error: err.message }));
          });
      });

      // Hook into Vite's WebSocket for custom HMR events
      server.ws.on('custom:ping', (data, client) => {
        client.send('custom:pong', { timestamp: Date.now() });
      });

      // Send custom event when a file changes
      server.watcher.on('change', (file) => {
        if (file.endsWith('.mock.ts')) {
          server.ws.send({ type: 'custom', event: 'mock-updated', data: { file } });
        }
      });
    },
  };
}

// Usage:
// plugins: [devApiPlugin({
//   '/users': () => [{ id: 1, name: 'Alice' }],
//   '/auth/me': () => ({ id: 1, role: 'admin' }),
// })]

HMR:ホットモジュールリプレースメント統合

プラグインはHMRコードを注入してカスタム更新イベントを処理できます。

// Plugin with HMR (Hot Module Replacement) support
// Allows modules to opt-in to custom update logic
export default function statePlugin() {
  return {
    name: 'vite-plugin-state-preserve',

    // Inject HMR handling code into every JS/TS file
    transform(code, id) {
      if (!/.(js|ts|jsx|tsx)$/.test(id)) return null;
      if (id.includes('node_modules')) return null;

      // Append HMR boundary acceptance
      const hmrCode = `
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // Module updated - newModule has the fresh exports
    console.log('[HMR] Module updated:', import.meta.url);
  });

  import.meta.hot.dispose((data) => {
    // Clean up side effects before module is replaced
    // data is passed to the next module version
    data.savedState = window.__APP_STATE__;
  });

  // Receive data from old module instance
  if (import.meta.hot.data.savedState) {
    window.__APP_STATE__ = import.meta.hot.data.savedState;
  }
}
`;

      return { code: code + hmrCode, map: null };
    },

    handleHotUpdate({ file, server, modules }) {
      // Custom logic: invalidate dependent modules when a .schema file changes
      if (file.endsWith('.schema.json')) {
        const affectedModules = server.moduleGraph.getModulesByFile(file);
        return [...(affectedModules || []), ...modules];
      }
    },
  };
}

完全なプラグイン設定例

複数のプラグインを組み合わせた本番対応のvite.config.ts。

// Complete vite.config.ts with multiple plugins
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import myPlugin from './plugins/my-plugin';

export default defineConfig(({ command, mode }) => ({
  plugins: [
    react(),
    myPlugin({ debug: mode === 'development' }),

    // Bundle analyzer - only in build mode
    command === 'build' && visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
    }),
  ].filter(Boolean),

  resolve: {
    alias: { '@': '/src' },
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
  },

  build: {
    rollupOptions: {
      output: {
        // Manual chunk splitting for better caching
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },

  server: {
    port: 5173,
    proxy: {
      '/api': { target: 'http://localhost:3000', changeOrigin: true },
    },
    hmr: {
      overlay: true,    // Show errors as overlay
      port: 24678,
    },
  },
}));

Viteフック クイックリファレンス

HookWhen CalledTypical UseApplies
configBefore config resolveModify Vite configBoth
configResolvedAfter config resolveRead final configBoth
configureServerBefore server startsAdd middlewareDev only
transformOn each file requestModify source codeBoth
resolveIdOn import resolutionRedirect importsBoth
loadAfter resolveIdSupply file contentBoth
handleHotUpdateOn file change (HMR)Custom HMR logicDev only
buildStartBuild beginsInitialize stateBuild only
generateBundleBundle generatedModify output assetsBuild only

よくある質問

enforce: "pre"と"post"はいつ使うべきですか?

"pre"は他の変換より先に実行する必要がある場合に使用します。

Viteで仮想モジュールを作るには?

resolveIdで\0プレフィックスのIDを返し、loadでコンテンツを返します。

Viteプラグインはdevとbuildモードで動作しますか?

はい、applyプロパティでモードを制御します。

Viteプラグインのデバッグ方法は?

環境変数にDEBUG=vite:*を追加してください。

関連ツール

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

{ }JSON FormatterB→Base64 Encode Online

Related Articles

Bunパッケージマネージャー:2026年最速JavaScriptランタイム完全ガイド

Bun完全ガイド2026:インストール、ワークスペース管理、スクリプト、npm/yarn/pnpmより速い理由。

Monoレポツール2026:Turborepo vs Nx vs Lerna vs pnpm Workspaces比較

Monoレポツール完全比較2026:Turborepo、Nx、Lerna、pnpm workspacesでチームに最適なツールを選択。

Webpack vs Vite 2026:どちらのビルドツールを選ぶべき?

2026年のWebpackとViteの包括的比較:パフォーマンス、エコシステム、移行戦略。