T-CREATOR

Vite でモノレポ(Monorepo)開発を加速する方法

Vite でモノレポ(Monorepo)開発を加速する方法

現代の Web 開発において、複数のプロジェクトを効率的に管理することは避けて通れない課題です。特に、フロントエンドとバックエンド、複数のマイクロサービス、そして共通ライブラリを扱う場合、従来の単一リポジトリ管理では限界を感じることが多いでしょう。

Vite を活用したモノレポ開発は、この課題を解決する強力な手段です。高速な開発サーバー、優れたビルドパフォーマンス、そして豊富なプラグインエコシステムを組み合わせることで、開発効率を劇的に向上させることができます。

この記事では、Vite を使ったモノレポ開発の実践的なアプローチを紹介します。単なる設定方法だけでなく、実際の開発現場で直面する課題とその解決策、そして開発チーム全体の生産性を向上させるための具体的なテクニックをお伝えします。

Vite モノレポの基本概念

モノレポとは何か

モノレポ(Monorepo)は、複数のプロジェクトやパッケージを単一のリポジトリで管理する開発手法です。従来のマルチレポ(Multi-repo)アプローチとは異なり、コードの共有、依存関係の管理、そして開発ワークフローを統合することで、開発効率を大幅に向上させることができます。

Vite がモノレポに適している理由

Vite は、モノレポ開発において以下のような優位性を持っています:

高速な開発サーバー

  • ES Modules を活用した即座のモジュール変換
  • 必要なファイルのみを処理する効率的なアルゴリズム
  • ホットリロードによる開発体験の向上

柔軟な設定システム

  • 各パッケージごとに異なる設定が可能
  • プラグインシステムによる拡張性
  • TypeScript のネイティブサポート

優れたビルドパフォーマンス

  • Rollup ベースの高速ビルド
  • 依存関係の最適化
  • 本番環境での最適化機能

モノレポの構造例

典型的な Vite モノレポの構造は以下のようになります:

perlmy-monorepo/
├── packages/
│   ├── app1/          # フロントエンドアプリケーション
│   ├── app2/          # 別のフロントエンドアプリケーション
│   ├── shared/        # 共有ライブラリ
│   └── ui/            # UI コンポーネントライブラリ
├── tools/             # 開発ツール
├── package.json       # ルートパッケージ設定
├── vite.config.ts     # ルート Vite 設定
└── tsconfig.json      # TypeScript 設定

この構造により、各パッケージは独立して開発・テスト・デプロイが可能でありながら、共通の設定やツールを共有できます。

モノレポ開発の課題と解決策

よくある課題とその影響

モノレポ開発を始める際、多くの開発者が以下の課題に直面します:

依存関係の複雑化

  • パッケージ間の循環依存
  • バージョン管理の困難さ
  • インストール時間の増加

ビルド時間の増加

  • 全パッケージの一括ビルド
  • 不要な再ビルドの発生
  • 開発サーバーの起動遅延

開発環境の一貫性

  • 異なる Node.js バージョン
  • パッケージマネージャーの違い
  • 開発ツールの設定不統一

Vite による解決アプローチ

Vite は、これらの課題に対して以下のような解決策を提供します:

依存関係の最適化

typescript// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  optimizeDeps: {
    include: ['shared', 'ui'], // 事前バンドル対象
  },
  build: {
    commonjsOptions: {
      include: [/node_modules/],
    },
  },
});

条件付きビルド

typescript// packages/app1/vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      external: ['shared', 'ui'], // 外部依存として扱う
      output: {
        globals: {
          shared: 'Shared',
          ui: 'UI',
        },
      },
    },
  },
});

開発サーバーの最適化

typescript// vite.config.ts
export default defineConfig({
  server: {
    hmr: {
      overlay: false, // エラーオーバーレイを無効化
    },
  },
  plugins: [
    // カスタムプラグインで依存関係を監視
    {
      name: 'monorepo-resolver',
      resolveId(id) {
        if (id.startsWith('@monorepo/')) {
          return resolve(
            __dirname,
            'packages',
            id.replace('@monorepo/', '')
          );
        }
      },
    },
  ],
});

実際のエラーとその対処法

エラー 1: モジュール解決エラー

javascriptError: Cannot resolve module '@monorepo/shared' from '/packages/app1/src/main.ts'

解決策:

typescript// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@monorepo/*": ["packages/*/src"]
    }
  }
}

エラー 2: 循環依存エラー

bashCircular dependency detected: packages/app1/src/utils.ts -> packages/shared/src/index.ts -> packages/app1/src/utils.ts

解決策:

typescript// packages/shared/src/index.ts
// 循環依存を避けるため、必要な部分のみエクスポート
export { helperFunction } from './helpers';
// 問題のある依存は削除

エラー 3: ビルド時間の増加

sqlBuild time: 45.2s (increased from 12.3s)

解決策:

typescript// vite.config.ts
export default defineConfig({
  build: {
    target: 'esnext',
    minify: 'esbuild',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          shared: ['@monorepo/shared'],
        },
      },
    },
  },
});

Vite を使ったモノレポ構築の手順

プロジェクトの初期設定

まず、モノレポの基本構造を作成します。以下の手順で進めていきましょう。

1. プロジェクトの初期化

bash# プロジェクトディレクトリの作成
mkdir vite-monorepo
cd vite-monorepo

# ルートパッケージの初期化
yarn init -y

# ワークスペース設定

2. package.json の設定

json{
  "name": "vite-monorepo",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "dev": "yarn workspaces run dev",
    "build": "yarn workspaces run build",
    "test": "yarn workspaces run test",
    "lint": "yarn workspaces run lint"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "vite": "^5.0.0",
    "@vitejs/plugin-react": "^4.0.0"
  }
}

3. TypeScript 設定

json// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@monorepo/*": ["packages/*/src"]
    }
  },
  "include": ["packages/*/src"],
  "exclude": ["node_modules"]
}

パッケージ構造の作成

次に、各パッケージの基本構造を作成します。

共有ライブラリの作成

bashmkdir -p packages/shared/src
cd packages/shared
yarn init -y

共有ライブラリの設定

json// packages/shared/package.json
{
  "name": "@monorepo/shared",
  "version": "1.0.0",
  "main": "dist/index.js",
  "module": "dist/index.es.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch"
  }
}

共有ライブラリの Vite 設定

typescript// packages/shared/vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'Shared',
      fileName: (format) => `index.${format}.js`,
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
});

アプリケーションパッケージの作成

bashmkdir -p packages/app1/src
cd packages/app1
yarn init -y

アプリケーションの設定

json// packages/app1/package.json
{
  "name": "@monorepo/app1",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@monorepo/shared": "workspace:*",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "@vitejs/plugin-react": "^4.0.0"
  }
}

アプリケーションの Vite 設定

typescript// packages/app1/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@monorepo/shared': resolve(
        __dirname,
        '../shared/src'
      ),
    },
  },
  optimizeDeps: {
    include: ['@monorepo/shared'],
  },
});

開発環境の起動

設定が完了したら、開発環境を起動してみましょう。

依存関係のインストール

bash# ルートディレクトリで実行
yarn install

開発サーバーの起動

bash# 全パッケージの開発サーバーを起動
yarn dev

# 特定のパッケージのみ起動
cd packages/app1
yarn dev

よくある起動エラーと解決策

エラー: モジュールが見つからない

sqlModule not found: Can't resolve '@monorepo/shared'

解決策:

typescript// packages/app1/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@monorepo/shared': resolve(
        __dirname,
        '../shared/src/index.ts'
      ),
    },
  },
});

エラー: TypeScript パス解決エラー

luaCannot find module '@monorepo/shared' or its corresponding type declarations.

解決策:

json// packages/app1/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@monorepo/*": ["../*/src"]
    }
  },
  "include": ["src"]
}

開発効率を向上させる設定とツール

ホットリロードの最適化

Vite の最大の魅力であるホットリロードを、モノレポ環境で最大限に活用するための設定を紹介します。

共有ライブラリの変更監視

typescript// packages/app1/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@monorepo/shared': resolve(
        __dirname,
        '../shared/src'
      ),
    },
  },
  server: {
    watch: {
      ignored: ['!**/node_modules/@monorepo/**'],
    },
  },
  optimizeDeps: {
    include: ['@monorepo/shared'],
  },
});

カスタムプラグインによる依存関係監視

typescript// tools/vite-monorepo-plugin.ts
import { Plugin } from 'vite';
import { resolve } from 'path';

export function monorepoPlugin(): Plugin {
  return {
    name: 'monorepo-resolver',
    config(config) {
      // 共有パッケージの変更を監視
      const watchDirs = [
        resolve(__dirname, '../packages/shared/src'),
        resolve(__dirname, '../packages/ui/src'),
      ];

      return {
        server: {
          watch: {
            ignored: ['!**/node_modules/@monorepo/**'],
          },
        },
        optimizeDeps: {
          include: ['@monorepo/shared', '@monorepo/ui'],
        },
      };
    },
    resolveId(id) {
      if (id.startsWith('@monorepo/')) {
        const packageName = id.replace('@monorepo/', '');
        return resolve(
          __dirname,
          '../packages',
          packageName,
          'src',
          'index.ts'
        );
      }
    },
  };
}

開発ツールの統合

効率的な開発のために、各種ツールを統合します。

ESLint の設定

json// .eslintrc.json
{
  "root": true,
  "extends": [
    "eslint:recommended",
    "@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "rules": {
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/explicit-function-return-type": "off"
  },
  "overrides": [
    {
      "files": [
        "packages/*/src/**/*.ts",
        "packages/*/src/**/*.tsx"
      ],
      "extends": [
        "plugin:react/recommended",
        "plugin:react-hooks/recommended"
      ]
    }
  ]
}

Prettier の設定

json// .prettierrc
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false
}

Husky による Git フック

json// package.json
{
  "scripts": {
    "prepare": "husky install",
    "lint:fix": "yarn workspaces run lint:fix",
    "type-check": "yarn workspaces run type-check"
  },
  "devDependencies": {
    "husky": "^8.0.0",
    "lint-staged": "^13.0.0"
  }
}

lint-staged の設定

json// .lintstagedrc
{
  "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.{js,jsx}": ["eslint --fix", "prettier --write"]
}

開発スクリプトの最適化

効率的な開発ワークフローを実現するためのスクリプトを設定します。

ルート package.json のスクリプト

json{
  "scripts": {
    "dev": "concurrently \"yarn dev:shared\" \"yarn dev:app1\" \"yarn dev:app2\"",
    "dev:shared": "yarn workspace @monorepo/shared dev",
    "dev:app1": "yarn workspace @monorepo/app1 dev",
    "dev:app2": "yarn workspace @monorepo/app2 dev",
    "build": "yarn workspaces run build",
    "build:shared": "yarn workspace @monorepo/shared build",
    "test": "yarn workspaces run test",
    "lint": "yarn workspaces run lint",
    "lint:fix": "yarn workspaces run lint:fix",
    "type-check": "yarn workspaces run type-check",
    "clean": "yarn workspaces run clean"
  },
  "devDependencies": {
    "concurrently": "^7.0.0"
  }
}

パッケージ固有のスクリプト

json// packages/app1/package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "type-check": "tsc --noEmit",
    "lint": "eslint src --ext .ts,.tsx",
    "lint:fix": "eslint src --ext .ts,.tsx --fix",
    "clean": "rm -rf dist node_modules/.vite"
  }
}

エラー処理とデバッグ

開発中によく発生するエラーとその対処法を紹介します。

エラー: 依存関係の解決エラー

bashError: Cannot resolve dependency '@monorepo/shared' in /packages/app1/src/main.tsx

解決策:

typescript// packages/app1/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@monorepo/shared': resolve(
        __dirname,
        '../shared/src/index.ts'
      ),
    },
  },
  optimizeDeps: {
    include: ['@monorepo/shared'],
  },
  server: {
    fs: {
      allow: ['..'], // 親ディレクトリへのアクセスを許可
    },
  },
});

エラー: ホットリロードが動作しない

arduinoHMR update failed: Cannot find module '@monorepo/shared'

解決策:

typescript// packages/app1/vite.config.ts
export default defineConfig({
  plugins: [react()],
  server: {
    watch: {
      ignored: ['!**/node_modules/@monorepo/**'],
    },
  },
  optimizeDeps: {
    include: ['@monorepo/shared'],
    force: true, // 強制的に再構築
  },
});

パフォーマンス最適化のテクニック

ビルド時間の短縮

モノレポ環境でのビルド時間を短縮するためのテクニックを紹介します。

依存関係の事前バンドル

typescript// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    include: [
      '@monorepo/shared',
      '@monorepo/ui',
      'react',
      'react-dom',
    ],
    exclude: ['@monorepo/app1', '@monorepo/app2'],
  },
  build: {
    rollupOptions: {
      external: ['@monorepo/shared', '@monorepo/ui'],
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          shared: ['@monorepo/shared'],
          ui: ['@monorepo/ui'],
        },
      },
    },
  },
});

条件付きビルド

typescript// packages/app1/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig(({ mode }) => {
  const isDev = mode === 'development';

  return {
    plugins: [react()],
    build: {
      target: isDev ? 'esnext' : 'es2015',
      minify: isDev ? false : 'esbuild',
      sourcemap: isDev,
      rollupOptions: {
        output: {
          manualChunks: isDev
            ? undefined
            : {
                vendor: ['react', 'react-dom'],
                shared: ['@monorepo/shared'],
              },
        },
      },
    },
  };
});

並列ビルドの実装

typescript// tools/build-parallel.ts
import { execSync } from 'child_process';
import { readdirSync } from 'fs';
import { resolve } from 'path';

const packagesDir = resolve(__dirname, '../packages');
const packages = readdirSync(packagesDir, {
  withFileTypes: true,
})
  .filter((dirent) => dirent.isDirectory())
  .map((dirent) => dirent.name);

console.log('Building packages in parallel:', packages);

packages.forEach((packageName) => {
  try {
    console.log(`Building ${packageName}...`);
    execSync('yarn build', {
      cwd: resolve(packagesDir, packageName),
      stdio: 'inherit',
    });
    console.log(`✅ ${packageName} built successfully`);
  } catch (error) {
    console.error(
      `❌ Failed to build ${packageName}:`,
      error
    );
    process.exit(1);
  }
});

開発サーバーの最適化

開発時のパフォーマンスを向上させる設定を紹介します。

メモリ使用量の最適化

typescript// vite.config.ts
export default defineConfig({
  server: {
    hmr: {
      overlay: false, // エラーオーバーレイを無効化してメモリ使用量を削減
    },
  },
  optimizeDeps: {
    force: false, // 不要な再構築を防ぐ
    entries: [
      'packages/app1/src/main.tsx',
      'packages/app2/src/main.tsx',
    ],
  },
  build: {
    chunkSizeWarningLimit: 1000,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
});

ファイル監視の最適化

typescript// vite.config.ts
export default defineConfig({
  server: {
    watch: {
      ignored: [
        '**/node_modules/**',
        '**/dist/**',
        '**/.git/**',
        '**/coverage/**',
      ],
    },
  },
  plugins: [
    {
      name: 'watch-optimizer',
      configureServer(server) {
        // 不要なファイルの監視を無効化
        server.watcher.add(['packages/*/dist/**']);
        server.watcher.unwatch([
          'packages/*/node_modules/**',
        ]);
      },
    },
  ],
});

キャッシュ戦略

ビルド時間を短縮するためのキャッシュ戦略を実装します。

依存関係のキャッシュ

typescript// vite.config.ts
export default defineConfig({
  cacheDir: '.vite-cache',
  optimizeDeps: {
    force: false,
    include: ['@monorepo/shared', '@monorepo/ui'],
  },
  build: {
    rollupOptions: {
      cache: true,
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
});

カスタムキャッシュプラグイン

typescript// tools/vite-cache-plugin.ts
import { Plugin } from 'vite';
import { resolve } from 'path';
import {
  readFileSync,
  writeFileSync,
  existsSync,
} from 'fs';

interface CacheEntry {
  timestamp: number;
  hash: string;
  content: string;
}

export function cachePlugin(): Plugin {
  const cacheFile = resolve(__dirname, '.vite-cache.json');
  let cache: Record<string, CacheEntry> = {};

  if (existsSync(cacheFile)) {
    try {
      cache = JSON.parse(readFileSync(cacheFile, 'utf-8'));
    } catch (error) {
      console.warn('Failed to load cache:', error);
    }
  }

  return {
    name: 'vite-cache',
    transform(code, id) {
      if (id.includes('node_modules')) return null;

      const hash = require('crypto')
        .createHash('md5')
        .update(code)
        .digest('hex');
      const cacheKey = `${id}:${hash}`;

      if (
        cache[cacheKey] &&
        Date.now() - cache[cacheKey].timestamp < 3600000
      ) {
        return cache[cacheKey].content;
      }

      // キャッシュに保存
      cache[cacheKey] = {
        timestamp: Date.now(),
        hash,
        content: code,
      };

      writeFileSync(
        cacheFile,
        JSON.stringify(cache, null, 2)
      );
      return null;
    },
  };
}

エラーとトラブルシューティング

エラー: ビルド時間が長すぎる

cssBuild time: 120.5s (too slow)

解決策:

typescript// vite.config.ts
export default defineConfig({
  build: {
    target: 'esnext',
    minify: 'esbuild',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          shared: ['@monorepo/shared'],
        },
      },
    },
  },
  optimizeDeps: {
    include: ['@monorepo/shared'],
    exclude: ['@monorepo/app1', '@monorepo/app2'],
  },
});

エラー: メモリ不足エラー

csharpJavaScript heap out of memory

解決策:

json// package.json
{
  "scripts": {
    "build": "node --max-old-space-size=4096 node_modules/.bin/vite build"
  }
}

実際のプロジェクトでの活用例

実践的なプロジェクト構造

実際のプロジェクトで使用される構造と設定を紹介します。

プロジェクト構造

bashe-commerce-monorepo/
├── packages/
│   ├── admin/           # 管理画面アプリケーション
│   ├── customer/        # 顧客向けアプリケーション
│   ├── shared/          # 共有ライブラリ
│   ├── ui/              # UI コンポーネントライブラリ
│   └── api/             # API クライアントライブラリ
├── tools/
│   ├── build-parallel.ts
│   └── vite-cache-plugin.ts
├── package.json
├── vite.config.ts
└── tsconfig.json

共有ライブラリの実装例

typescript// packages/shared/src/types/index.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'customer';
}

export interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

export interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

共有ユーティリティの実装

typescript// packages/shared/src/utils/index.ts
export const formatPrice = (price: number): string => {
  return new Intl.NumberFormat('ja-JP', {
    style: 'currency',
    currency: 'JPY',
  }).format(price);
};

export const validateEmail = (email: string): boolean => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};

export const debounce = <T extends (...args: any[]) => any>(
  func: T,
  wait: number
): ((...args: Parameters<T>) => void) => {
  let timeout: NodeJS.Timeout;
  return (...args: Parameters<T>) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
};

UI コンポーネントライブラリ

typescript// packages/ui/src/components/Button.tsx
import React from 'react';

interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  onClick?: () => void;
  disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
  children,
  variant = 'primary',
  size = 'medium',
  onClick,
  disabled = false,
}) => {
  const baseClasses =
    'px-4 py-2 rounded font-medium transition-colors';
  const variantClasses = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-500 text-white hover:bg-gray-600',
    danger: 'bg-red-500 text-white hover:bg-red-600',
  };
  const sizeClasses = {
    small: 'px-2 py-1 text-sm',
    medium: 'px-4 py-2',
    large: 'px-6 py-3 text-lg',
  };

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
};

アプリケーション間の連携

複数のアプリケーションが共有ライブラリを活用する例を紹介します。

管理画面アプリケーション

typescript// packages/admin/src/App.tsx
import React from 'react';
import { User, Product } from '@monorepo/shared';
import { Button } from '@monorepo/ui';

const AdminApp: React.FC = () => {
  const [users, setUsers] = React.useState<User[]>([]);
  const [products, setProducts] = React.useState<Product[]>(
    []
  );

  const handleUserDelete = (userId: string) => {
    setUsers(users.filter((user) => user.id !== userId));
  };

  const handleProductEdit = (product: Product) => {
    // 商品編集ロジック
  };

  return (
    <div className='admin-app'>
      <h1>管理画面</h1>

      <section>
        <h2>ユーザー管理</h2>
        {users.map((user) => (
          <div key={user.id}>
            <span>{user.name}</span>
            <Button
              variant='danger'
              size='small'
              onClick={() => handleUserDelete(user.id)}
            >
              削除
            </Button>
          </div>
        ))}
      </section>

      <section>
        <h2>商品管理</h2>
        {products.map((product) => (
          <div key={product.id}>
            <span>{product.name}</span>
            <Button
              variant='secondary'
              size='small'
              onClick={() => handleProductEdit(product)}
            >
              編集
            </Button>
          </div>
        ))}
      </section>
    </div>
  );
};

export default AdminApp;

顧客向けアプリケーション

typescript// packages/customer/src/App.tsx
import React from 'react';
import { Product, formatPrice } from '@monorepo/shared';
import { Button } from '@monorepo/ui';

const CustomerApp: React.FC = () => {
  const [products, setProducts] = React.useState<Product[]>(
    []
  );
  const [cart, setCart] = React.useState<Product[]>([]);

  const handleAddToCart = (product: Product) => {
    setCart([...cart, product]);
  };

  const totalPrice = cart.reduce(
    (sum, product) => sum + product.price,
    0
  );

  return (
    <div className='customer-app'>
      <h1>オンラインショップ</h1>

      <section>
        <h2>商品一覧</h2>
        {products.map((product) => (
          <div key={product.id} className='product-card'>
            <h3>{product.name}</h3>
            <p>{product.description}</p>
            <p className='price'>
              {formatPrice(product.price)}
            </p>
            <Button
              variant='primary'
              onClick={() => handleAddToCart(product)}
            >
              カートに追加
            </Button>
          </div>
        ))}
      </section>

      <section>
        <h2>ショッピングカート</h2>
        <p>合計: {formatPrice(totalPrice)}</p>
        {cart.map((product) => (
          <div key={product.id}>
            <span>{product.name}</span>
            <span>{formatPrice(product.price)}</span>
          </div>
        ))}
      </section>
    </div>
  );
};

export default CustomerApp;

開発ワークフローの実装

効率的な開発ワークフローを実現するための設定とスクリプトを紹介します。

開発環境の起動スクリプト

typescript// tools/dev-server.ts
import { createServer } from 'vite';
import { resolve } from 'path';

async function startDevServer() {
  const packages = ['admin', 'customer'];

  for (const packageName of packages) {
    const configPath = resolve(
      __dirname,
      `../packages/${packageName}/vite.config.ts`
    );

    try {
      const server = await createServer({
        configFile: configPath,
        root: resolve(
          __dirname,
          `../packages/${packageName}`
        ),
      });

      await server.listen();
      console.log(
        `✅ ${packageName} dev server started at ${server.config.server.port}`
      );
    } catch (error) {
      console.error(
        `❌ Failed to start ${packageName} dev server:`,
        error
      );
    }
  }
}

startDevServer();

ビルドスクリプト

typescript// tools/build-all.ts
import { build } from 'vite';
import { resolve } from 'path';
import { readdirSync } from 'fs';

async function buildAll() {
  const packagesDir = resolve(__dirname, '../packages');
  const packages = readdirSync(packagesDir, {
    withFileTypes: true,
  })
    .filter((dirent) => dirent.isDirectory())
    .map((dirent) => dirent.name);

  console.log('Building packages:', packages);

  for (const packageName of packages) {
    try {
      console.log(`Building ${packageName}...`);

      const configPath = resolve(
        packagesDir,
        packageName,
        'vite.config.ts'
      );
      await build({
        configFile: configPath,
        root: resolve(packagesDir, packageName),
      });

      console.log(`✅ ${packageName} built successfully`);
    } catch (error) {
      console.error(
        `❌ Failed to build ${packageName}:`,
        error
      );
      process.exit(1);
    }
  }
}

buildAll();

実際のエラーと解決例

エラー: 共有ライブラリの型定義が見つからない

luaCannot find type definitions for '@monorepo/shared'

解決策:

typescript// packages/shared/src/index.ts
export * from './types'
export * from './utils'

// packages/shared/package.json
{
  "name": "@monorepo/shared",
  "version": "1.0.0",
  "main": "dist/index.js",
  "module": "dist/index.es.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "vite build && tsc --emitDeclarationOnly",
    "dev": "vite build --watch"
  }
}

エラー: アプリケーション間の依存関係エラー

bashCircular dependency detected between packages/admin and packages/shared

解決策:

typescript// packages/shared/src/index.ts
// 循環依存を避けるため、必要な部分のみエクスポート
export { User, Product, ApiResponse } from './types';
export {
  formatPrice,
  validateEmail,
  debounce,
} from './utils';

// 問題のある依存は削除し、インターフェースを分離
export interface UserService {
  getUsers(): Promise<User[]>;
  createUser(user: Omit<User, 'id'>): Promise<User>;
}

まとめ

Vite を活用したモノレポ開発は、現代の Web 開発において非常に強力なアプローチです。この記事で紹介した手法とテクニックを実践することで、開発効率を劇的に向上させることができます。

重要なポイントを振り返ると:

  1. 適切なプロジェクト構造の設計が成功の鍵です。共有ライブラリとアプリケーションの境界を明確にし、依存関係を整理することで、保守性の高いコードベースを構築できます。

  2. 開発環境の最適化により、開発者の体験を大幅に改善できます。ホットリロード、型チェック、リンティングを統合することで、バグの早期発見と修正が可能になります。

  3. パフォーマンス最適化は、プロジェクトの規模が大きくなるにつれて重要性を増します。ビルド時間の短縮、メモリ使用量の最適化、キャッシュ戦略の実装により、開発サイクルを加速できます。

  4. 実践的なワークフローの構築により、チーム全体の生産性を向上させることができます。並列ビルド、自動化されたテスト、継続的インテグレーションを組み合わせることで、高品質なソフトウェアを効率的に開発できます。

Vite の高速な開発サーバーと優れたビルドパフォーマンスを活用し、モノレポの利点を最大限に引き出すことで、スケーラブルで保守性の高いアプリケーションを構築できるでしょう。

この記事で紹介した手法を参考に、あなたのプロジェクトでも Vite モノレポ開発を始めてみてください。きっと、開発効率の向上とコードの品質向上を実感できるはずです。

関連リンク