T-CREATOR

Vue.js Monorepo 構築:pnpm/Turborepo でアプリとパッケージを一元管理

Vue.js Monorepo 構築:pnpm/Turborepo でアプリとパッケージを一元管理

Vue.js を使った複数のプロジェクトを管理していると、共通コンポーネントやユーティリティ関数を各プロジェクトで重複して作成してしまうことはありませんか。そんな課題を解決してくれるのが「Monorepo」という手法です。

本記事では、pnpmTurborepo を組み合わせて、Vue.js の複数アプリケーションと共有パッケージを一元管理する Monorepo 構築方法を、初心者の方にも分かりやすく解説していきます。実際に手を動かしながら学べるよう、具体的なコマンドと設定ファイルを段階的に紹介していきますね。

背景

Monorepo とは何か

Monorepo(モノレポ)は、複数のプロジェクトやパッケージを 1 つのリポジトリで管理する開発手法です。従来の Polyrepo(ポリレポ)では、各プロジェクトごとに個別のリポジトリを作成していましたが、Monorepo ではすべてを 1 つのリポジトリ内で管理します。

この手法は、Google や Facebook といった大規模な組織で採用されており、コードの共有や依存関係の管理が格段に効率化されます。

Vue.js プロジェクトにおける Monorepo の需要

Vue.js でフロントエンド開発を行う際、以下のようなケースで Monorepo が威力を発揮します。

  • 複数の Web アプリケーションを運用している
  • 共通の UI コンポーネントを複数プロジェクトで使いたい
  • ユーティリティ関数型定義を一元管理したい
  • 管理画面とユーザー向けサイトを同時に開発している

これらの状況では、コードの重複を避け、変更を一箇所で管理できる Monorepo が理想的な解決策となります。

下図は、Monorepo と Polyrepo の構造比較を示しています。

mermaidflowchart TB
    subgraph mono["Monorepo 構造"]
        direction TB
        root["ルートリポジトリ"]
        root --> app1["apps/web-app"]
        root --> app2["apps/admin-app"]
        root --> pkg1["packages/ui-components"]
        root --> pkg2["packages/utils"]
        app1 -.->|依存| pkg1
        app1 -.->|依存| pkg2
        app2 -.->|依存| pkg1
    end

    subgraph poly["Polyrepo 構造"]
        direction TB
        repo1["リポジトリ 1<br/>web-app"]
        repo2["リポジトリ 2<br/>admin-app"]
        repo3["リポジトリ 3<br/>ui-components"]
        repo4["リポジトリ 4<br/>utils"]
        repo1 -.->|npm 依存| repo3
        repo2 -.->|npm 依存| repo3
    end

Monorepo では、1 つのリポジトリ内で複数のアプリとパッケージを管理でき、ローカル開発時に即座に変更が反映されます。一方、Polyrepo では各リポジトリが独立しているため、パッケージの更新ごとに npm への公開が必要になるのです。

pnpm と Turborepo の役割

pnpm は、高速で効率的なパッケージマネージャーです。ディスク容量を節約し、npm や Yarn よりも高速なインストールを実現します。特に Monorepo 環境では、ワークスペース機能を使って複数パッケージを効率的に管理できます。

Turborepo は、Vercel が開発した Monorepo 向けのビルドシステムです。ビルドやテストなどのタスクを並列実行し、キャッシュ機能によって不要なビルドをスキップすることで、開発効率を大幅に向上させます。

#ツール主な役割特徴
1pnpmパッケージ管理ディスク容量節約、高速インストール、ワークスペース対応
2Turborepoタスク実行・ビルド管理並列実行、インクリメンタルビルド、リモートキャッシュ

この 2 つを組み合わせることで、Vue.js の Monorepo 開発が驚くほど快適になりますよ。

課題

従来の開発における問題点

Vue.js で複数プロジェクトを開発する際、従来の方法では以下のような課題がありました。

コードの重複問題

各プロジェクトで同じようなコンポーネントやユーティリティを何度も作成してしまうことがあります。例えば、ボタンコンポーネントやフォームバリデーション関数などを、プロジェクトごとにコピー&ペーストしていると、バグ修正や機能追加のたびにすべてのプロジェクトを修正する必要が出てきます。

依存関係の管理の複雑さ

共通パッケージを npm に公開して使う方法もありますが、これには以下の課題があります。

  • パッケージの更新ごとにバージョン番号の更新が必要
  • npm への公開作業が発生
  • ローカル開発時に即座に変更が反映されない
  • バージョン管理が煩雑になる

ビルド時間の増大

プロジェクトが増えるにつれて、すべてのビルドやテストを実行する時間が増大します。変更していないパッケージまで毎回ビルドするのは非効率ですよね。

下図は、従来の開発フローにおける問題点を示しています。

mermaidflowchart TB
    dev["開発者"] -->|修正| pkg["共通パッケージ"]
    pkg -->|1. バージョン更新| version["package.json<br/>バージョン番号変更"]
    version -->|2. ビルド| build["npm run build"]
    build -->|3. 公開| publish["npm publish"]
    publish -->|4. インストール| app1["アプリ 1<br/>npm install"]
    publish -->|4. インストール| app2["アプリ 2<br/>npm install"]
    app1 -->|5. 動作確認| test1["テスト"]
    app2 -->|5. 動作確認| test2["テスト"]

    style pkg fill:#ffcccc
    style version fill:#ffcccc
    style publish fill:#ffcccc

このように、共通パッケージを修正するたびに多くのステップを踏む必要があり、開発速度が低下してしまいます。

Monorepo で解決できること

Monorepo を導入することで、これらの課題を以下のように解決できます。

#課題Monorepo による解決策
1コードの重複共通パッケージを作成し、複数アプリから参照
2依存関係管理の複雑さワークスペース機能で即座に変更を反映
3ビルド時間の増大Turborepo のキャッシュで不要なビルドをスキップ
4バージョン管理の煩雑さ単一リポジトリで統一的に管理

次のセクションでは、これらの課題を解決する具体的な構築方法を見ていきましょう。

解決策

pnpm Workspace による Monorepo の基盤構築

pnpm の Workspace 機能を使うことで、複数のパッケージを 1 つのリポジトリ内で管理できます。各パッケージは独立していながらも、相互に依存関係を持つことが可能です。

pnpm Workspace の仕組み

pnpm Workspace では、pnpm-workspace.yaml ファイルでワークスペース内のパッケージを定義します。これにより、pnpm は各パッケージを認識し、依存関係を適切に解決してくれます。

mermaidflowchart LR
    root["ルート<br/>pnpm-workspace.yaml"] --> apps["apps/*<br/>アプリケーション群"]
    root --> pkgs["packages/*<br/>共有パッケージ群"]
    apps --> app1["web-app"]
    apps --> app2["admin-app"]
    pkgs --> ui["ui-components"]
    pkgs --> utils["utils"]
    app1 -.->|workspace依存| ui
    app1 -.->|workspace依存| utils
    app2 -.->|workspace依存| ui

この構造により、各アプリケーションは共有パッケージを直接参照でき、パッケージの変更が即座に反映されます。

Workspace のメリット

pnpm Workspace を使うことで、以下のメリットが得られます。

  • シンボリックリンクによる高速な依存関係解決
  • ディスク容量の節約(同じパッケージは 1 度だけダウンロード)
  • ローカル開発の効率化(パッケージの変更が即座に反映)
  • 依存関係の一元管理

Turborepo によるタスク管理の最適化

Turborepo は、Monorepo 内のタスク(ビルド、テスト、リント等)を効率的に実行するためのツールです。以下の機能により、開発体験を大幅に向上させます。

インクリメンタルビルド

Turborepo は、変更されたパッケージとその依存パッケージのみをビルドします。変更がないパッケージはキャッシュから結果を取得するため、ビルド時間が劇的に短縮されます。

タスクの並列実行

複数のパッケージのビルドやテストを並列で実行できます。依存関係を考慮しながら、可能な限り並列化することで、全体の実行時間を短縮します。

リモートキャッシュ

チーム開発では、リモートキャッシュを使って他の開発者のビルド結果を共有できます。これにより、CI/CD パイプラインでも高速なビルドが可能になります。

#機能効果削減時間の目安
1インクリメンタルビルド変更部分のみビルド70-90% 削減
2並列実行複数タスクを同時実行40-60% 削減
3リモートキャッシュチームでキャッシュ共有80-95% 削減

アーキテクチャ設計のベストプラクティス

Monorepo を構築する際は、以下のディレクトリ構造を推奨します。

bashmonorepo-root/
├── apps/              # アプリケーション群
│   ├── web-app/       # ユーザー向けWebアプリ
│   └── admin-app/     # 管理画面
├── packages/          # 共有パッケージ群
│   ├── ui-components/ # UIコンポーネント
│   ├── utils/         # ユーティリティ関数
│   └── types/         # 型定義
├── pnpm-workspace.yaml
├── turbo.json
└── package.json

この構造により、役割が明確に分離され、メンテナンス性が向上します。

  • apps/:エンドユーザーが利用するアプリケーション
  • packages/:複数のアプリで共有するライブラリやコンポーネント

次のセクションでは、この構造を実際に構築する手順を詳しく見ていきましょう。

具体例

環境構築の準備

まずは、pnpm のインストールから始めます。pnpm は npm や Yarn の代替となるパッケージマネージャーです。

pnpm のインストール

以下のコマンドで pnpm をグローバルにインストールします。

bash# npm を使ってインストール
npm install -g pnpm

# インストール確認
pnpm --version

バージョンが表示されれば、インストールは成功です。執筆時点での最新版は 8.x 系ですが、7.x 以降であれば問題ありません。

プロジェクトルートの作成

新しいディレクトリを作成し、そこを Monorepo のルートとします。

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

# Git リポジトリを初期化
git init

この時点で、空のプロジェクトディレクトリが作成されました。

pnpm Workspace の設定

ワークスペース設定ファイルの作成

プロジェクトルートに pnpm-workspace.yaml を作成します。このファイルで、どのディレクトリをワークスペースとして認識するかを定義します。

yamlpackages:
  - 'apps/*'
  - 'packages/*'

この設定により、apps​/​packages​/​ 配下のすべてのディレクトリがワークスペースとして認識されます。

ルートの package.json 作成

ルートディレクトリに package.json を作成します。これは Monorepo 全体の設定を管理するファイルです。

json{
  "name": "vue-monorepo",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint"
  }
}

"private": true を設定することで、このルートパッケージが誤って npm に公開されるのを防ぎます。

Turborepo のインストール

Turborepo をプロジェクトに追加します。

bashpnpm add -Dw turbo

-Dw オプションは、ワークスペースのルートに開発依存パッケージとしてインストールすることを意味します。

ディレクトリ構造の作成

アプリケーションとパッケージのディレクトリを作成しましょう。

bash# アプリケーション用ディレクトリ
mkdir -p apps/web-app
mkdir -p apps/admin-app

# 共有パッケージ用ディレクトリ
mkdir -p packages/ui-components
mkdir -p packages/utils

これで、基本的なディレクトリ構造が完成しました。

Vue.js アプリケーションの作成

Web アプリの構築

apps​/​web-app に Vue.js アプリケーションを作成します。create-vue を使用して、TypeScript 対応のプロジェクトを生成しましょう。

bashcd apps/web-app
pnpm create vue@latest .

対話的なプロンプトが表示されるので、以下のように選択します。

  • Project name: web-app
  • Add TypeScript? → Yes
  • Add JSX Support? → No
  • Add Vue Router? → Yes
  • Add Pinia? → Yes
  • Add Vitest? → Yes
  • Add ESLint? → Yes
  • Add Prettier? → Yes

プロジェクトが生成されたら、依存パッケージをインストールします。

bashpnpm install

package.json の調整

生成された apps​/​web-app​/​package.json を確認し、name フィールドを調整します。

json{
  "name": "@vue-monorepo/web-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "vitest",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts"
  }
}

@vue-monorepo​/​ というスコープを付けることで、パッケージ名の衝突を防ぎます。

管理画面アプリの作成

同様の手順で、apps​/​admin-app にも Vue.js アプリケーションを作成します。

bashcd ../admin-app
pnpm create vue@latest .

設定は web-app と同様にし、name は @vue-monorepo​/​admin-app とします。

共有パッケージの作成

UI コンポーネントパッケージ

packages​/​ui-components に共有可能な Vue コンポーネントパッケージを作成します。

まず、packages​/​ui-components​/​package.json を作成します。

json{
  "name": "@vue-monorepo/ui-components",
  "version": "0.1.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch"
  }
}

このパッケージは、ビルドされた JavaScript と型定義ファイルをエクスポートします。

依存パッケージのインストール

UI コンポーネントパッケージに必要な Vue と Vite をインストールします。

bashcd packages/ui-components
pnpm add -D vue vite @vitejs/plugin-vue typescript

-D オプションで開発依存パッケージとしてインストールします。

Vite 設定ファイルの作成

packages​/​ui-components​/​vite.config.ts を作成し、ライブラリモードでビルドするよう設定します。

typescriptimport { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'UiComponents',
      fileName: 'index',
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
});

この設定により、Vue は外部依存として扱われ、バンドルサイズが最小化されます。

コンポーネントの作成

packages​/​ui-components​/​src​/​components​/​BaseButton.vue にシンプルなボタンコンポーネントを作成します。

vue<template>
  <button
    :class="['base-button', variant]"
    @click="handleClick"
  >
    <slot />
  </button>
</template>

<script setup lang="ts">
// Props の型定義
interface Props {
  variant?: 'primary' | 'secondary' | 'danger';
}

// Props のデフォルト値設定
const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
});

// Emits の定義
const emit = defineEmits<{
  click: [event: MouseEvent];
}>();

// クリックハンドラー
const handleClick = (event: MouseEvent) => {
  emit('click', event);
};
</script>

このコンポーネントは、3 つのバリエーション(primary、secondary、danger)を持つ汎用的なボタンです。

スタイルの追加

同じファイル内にスタイルを追加します。

vue<style scoped>
.base-button {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 0.25rem;
  font-size: 1rem;
  cursor: pointer;
  transition: all 0.2s;
}

.primary {
  background-color: #42b983;
  color: white;
}

.primary:hover {
  background-color: #359268;
}

.secondary {
  background-color: #6c757d;
  color: white;
}

.secondary:hover {
  background-color: #5a6268;
}

.danger {
  background-color: #dc3545;
  color: white;
}

.danger:hover {
  background-color: #c82333;
}
</style>

各バリエーションに応じた色とホバー効果が定義されています。

エントリーポイントの作成

packages​/​ui-components​/​src​/​index.ts を作成し、コンポーネントをエクスポートします。

typescript// コンポーネントのインポート
import BaseButton from './components/BaseButton.vue';

// 名前付きエクスポート
export { BaseButton };

// デフォルトエクスポート(プラグインとして使用可能)
export default {
  install(app: any) {
    app.component('BaseButton', BaseButton);
  },
};

これにより、個別にインポートすることも、Vue プラグインとして一括登録することも可能になります。

ユーティリティパッケージの作成

packages​/​utils に汎用的なユーティリティ関数パッケージを作成しましょう。

まず、packages​/​utils​/​package.json を作成します。

json{
  "name": "@vue-monorepo/utils",
  "version": "0.1.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts",
    "dev": "tsup src/index.ts --format esm --dts --watch"
  }
}

このパッケージでは、TypeScript のビルドに tsup を使用します。

tsup のインストール

bashcd packages/utils
pnpm add -D tsup typescript

tsup は、TypeScript のビルドを高速化するツールです。

ユーティリティ関数の作成

packages​/​utils​/​src​/​format.ts に日付フォーマット関数を作成します。

typescript/**
 * 日付を YYYY-MM-DD 形式にフォーマットします
 * @param date - フォーマットする日付オブジェクト
 * @returns フォーマットされた日付文字列
 */
export function formatDate(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(
    2,
    '0'
  );
  const day = String(date.getDate()).padStart(2, '0');

  return `${year}-${month}-${day}`;
}

/**
 * 日付を YYYY年MM月DD日 形式にフォーマットします
 * @param date - フォーマットする日付オブジェクト
 * @returns フォーマットされた日付文字列(日本語形式)
 */
export function formatDateJa(date: Date): string {
  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();

  return `${year}${month}${day}日`;
}

コメントを充実させることで、IDE の補完機能でも説明が表示されるようになります。

バリデーション関数の追加

packages​/​utils​/​src​/​validation.ts にフォームバリデーション関数を作成します。

typescript/**
 * メールアドレスの形式をチェックします
 * @param email - チェックするメールアドレス
 * @returns メールアドレスとして妥当な場合は true
 */
export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

/**
 * 文字列が空でないかチェックします
 * @param value - チェックする文字列
 * @returns 空でない場合は true
 */
export function isNotEmpty(value: string): boolean {
  return value.trim().length > 0;
}

/**
 * 文字列が最小文字数以上かチェックします
 * @param value - チェックする文字列
 * @param minLength - 最小文字数
 * @returns 最小文字数以上の場合は true
 */
export function hasMinLength(
  value: string,
  minLength: number
): boolean {
  return value.length >= minLength;
}

これらの関数は、フォームバリデーションで頻繁に使用される基本的なチェック処理です。

エントリーポイントの作成

packages​/​utils​/​src​/​index.ts ですべての関数をエクスポートします。

typescript// 日付フォーマット関数
export { formatDate, formatDateJa } from './format';

// バリデーション関数
export {
  isValidEmail,
  isNotEmpty,
  hasMinLength,
} from './validation';

これで、utils パッケージから必要な関数を簡単にインポートできるようになりました。

Turborepo の設定

turbo.json の作成

プロジェクトルートに turbo.json を作成し、タスクの依存関係とキャッシュ設定を定義します。

json{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    }
  }
}

この設定により、各タスクの実行順序とキャッシュ対象が定義されます。

設定の詳細解説

各フィールドの意味を説明します。

  • dependsOn: タスクの依存関係を定義
    • "^build": 依存パッケージの build が先に実行される
  • outputs: キャッシュ対象のディレクトリを指定
    • "dist​/​**": dist ディレクトリ配下をキャッシュ
  • cache: キャッシュの有効/無効を設定
    • false: 開発サーバーなど常に実行が必要なタスク
  • persistent: タスクが継続的に実行されるかを指定
    • true: dev サーバーなど、終了しないタスク

下図は、Turborepo のタスク実行フローを示しています。

mermaidflowchart TB
    start["turbo run build"] --> check1{"packages/utils<br/>変更あり?"}
    check1 -->|Yes| build1["utils をビルド"]
    check1 -->|No| cache1["キャッシュから取得"]

    build1 --> check2{"packages/ui-components<br/>変更あり?"}
    cache1 --> check2

    check2 -->|Yes| build2["ui-components をビルド<br/>(utils に依存)"]
    check2 -->|No| cache2["キャッシュから取得"]

    build2 --> parallel["アプリのビルド<br/>(並列実行)"]
    cache2 --> parallel

    parallel --> app1["web-app ビルド"]
    parallel --> app2["admin-app ビルド"]

    app1 --> done["完了"]
    app2 --> done

    style build1 fill:#ccffcc
    style build2 fill:#ccffcc
    style cache1 fill:#ffcccc
    style cache2 fill:#ffcccc

Turborepo は、変更されたパッケージのみをビルドし、それ以外はキャッシュから取得することで、大幅な時間短縮を実現します。

アプリケーションから共有パッケージを使用

パッケージの追加

web-app から ui-components と utils を使用できるようにします。

bashcd apps/web-app
pnpm add @vue-monorepo/ui-components @vue-monorepo/utils --workspace

--workspace オプションにより、ローカルのワークスペースパッケージとしてリンクされます。

コンポーネントの使用例

apps​/​web-app​/​src​/​App.vue で共有コンポーネントを使用します。

vue<script setup lang="ts">
// 共有パッケージからインポート
import { BaseButton } from '@vue-monorepo/ui-components';
import { formatDateJa } from '@vue-monorepo/utils';
import { ref } from 'vue';

// 現在の日付を表示
const currentDate = ref(formatDateJa(new Date()));

// ボタンクリックハンドラー
const handleClick = () => {
  alert(`今日は${currentDate.value}です`);
};
</script>

このように、作成した共有パッケージを簡単にインポートして使用できます。

テンプレートの作成

同じファイル内にテンプレートを追加します。

vue<template>
  <div class="app">
    <h1>Vue.js Monorepo サンプル</h1>

    <div class="date-display">
      <p>今日の日付: {{ currentDate }}</p>
    </div>

    <div class="button-group">
      <BaseButton variant="primary" @click="handleClick">
        Primary ボタン
      </BaseButton>

      <BaseButton variant="secondary" @click="handleClick">
        Secondary ボタン
      </BaseButton>

      <BaseButton variant="danger" @click="handleClick">
        Danger ボタン
      </BaseButton>
    </div>
  </div>
</template>

BaseButton コンポーネントが、まるで同じプロジェクト内にあるかのように自然に使用できていますね。

スタイルの追加

vue<style scoped>
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.date-display {
  margin: 2rem 0;
  font-size: 1.2rem;
}

.button-group {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}
</style>

これで、共有コンポーネントを使用したアプリケーションが完成しました。

開発とビルドの実行

開発モードの起動

プロジェクトルートから、すべてのアプリケーションを開発モードで起動できます。

bash# ルートディレクトリで実行
pnpm dev

Turborepo が各パッケージの dev スクリプトを並列で実行し、以下のようなログが表示されます。

sql• Packages in scope: @vue-monorepo/admin-app, @vue-monorepo/ui-components,
  @vue-monorepo/utils, @vue-monorepo/web-app
• Running dev in 4 packages

各アプリケーションが異なるポートで起動し、共有パッケージの変更も即座に反映されます。

ビルドの実行

本番用のビルドも同様にルートから実行できます。

bashpnpm build

Turborepo が依存関係を解析し、正しい順序でビルドを実行します。

sql• Packages in scope: @vue-monorepo/admin-app, @vue-monorepo/ui-components,
  @vue-monorepo/utils, @vue-monorepo/web-app
• Running build in 4 packages
• Remote caching disabled

初回ビルド後、変更がないパッケージは次回からキャッシュが使用されるため、ビルド時間が大幅に短縮されます。

特定のパッケージのみ実行

特定のアプリケーションのみを起動したい場合は、filter オプションを使用します。

bash# web-app のみ開発モードで起動
pnpm --filter @vue-monorepo/web-app dev

# ui-components のみビルド
pnpm --filter @vue-monorepo/ui-components build

このように、柔軟にタスクを実行できます。

TypeScript の型共有

型定義パッケージの作成

packages​/​types に共通の型定義を管理するパッケージを作成しましょう。

bashmkdir -p packages/types/src

packages​/​types​/​package.json を作成します。

json{
  "name": "@vue-monorepo/types",
  "version": "0.1.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts",
    "dev": "tsup src/index.ts --format esm --dts --watch"
  }
}

共通型の定義

packages​/​types​/​src​/​user.ts にユーザー関連の型を定義します。

typescript/**
 * ユーザーの基本情報
 */
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
  updatedAt: Date;
}

/**
 * ユーザー作成時の入力データ
 */
export interface CreateUserInput {
  name: string;
  email: string;
  password: string;
}

/**
 * ユーザー更新時の入力データ
 */
export interface UpdateUserInput {
  name?: string;
  email?: string;
}

これらの型定義は、フロントエンドとバックエンドで共有できます。

エントリーポイントの作成

packages​/​types​/​src​/​index.ts で型をエクスポートします。

typescript// ユーザー関連の型
export type {
  User,
  CreateUserInput,
  UpdateUserInput,
} from './user';

型パッケージの使用

アプリケーションから型パッケージを使用します。

bashcd apps/web-app
pnpm add @vue-monorepo/types --workspace

コンポーネント内で型を使用する例です。

typescriptimport { ref } from 'vue';
import type { User } from '@vue-monorepo/types';

// User 型を使用
const currentUser = ref<User | null>(null);

// API からユーザー情報を取得する関数
async function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

型が共有されることで、フロントエンドとバックエンドで一貫性のある開発が可能になります。

CI/CD パイプラインの設定例

GitHub Actions の設定

.github​/​workflows​/​ci.yml に CI パイプラインを設定します。

yamlname: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'pnpm'

この部分では、リポジトリのチェックアウトと pnpm のセットアップを行っています。

依存パッケージのインストール

yaml- name: Install dependencies
  run: pnpm install --frozen-lockfile

--frozen-lockfile オプションにより、lockfile の変更を防ぎ、再現性を確保します。

Turborepo のキャッシュ設定

yaml- name: Setup Turborepo cache
  uses: actions/cache@v3
  with:
    path: .turbo
    key: ${{ runner.os }}-turbo-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-turbo-

Turborepo のキャッシュを GitHub Actions でも活用することで、CI 実行時間を短縮できます。

ビルドとテストの実行

yaml- name: Build
  run: pnpm build

- name: Lint
  run: pnpm lint

- name: Test
  run: pnpm test

すべてのタスクが Turborepo によって最適化され、並列実行されます。

この設定により、プルリクエストごとに自動的にビルド、リント、テストが実行され、コード品質が保たれるようになります。

まとめ

本記事では、pnpm と Turborepo を使った Vue.js Monorepo の構築方法を、環境構築から実際のアプリケーション開発まで、段階的に解説してきました。

Monorepo 導入の主なメリット

#メリット具体的な効果
1コードの再利用性向上共通コンポーネントやユーティリティを一元管理
2開発効率の改善パッケージの変更が即座に反映される
3ビルド時間の短縮Turborepo のキャッシュで 70-90% の時間削減
4型安全性の向上TypeScript の型定義を複数プロジェクトで共有
5メンテナンス性の向上すべてのコードが 1 つのリポジトリで管理される

実装のポイント振り返り

pnpm Workspace により、複数のパッケージを効率的に管理できるようになりました。シンボリックリンクによる依存関係の解決は、npm や Yarn よりも高速でディスク容量も節約できます。

Turborepo の導入により、タスクの並列実行とインクリメンタルビルドが可能になり、開発体験が大幅に向上しました。特にキャッシュ機能は、CI/CD パイプラインでも威力を発揮します。

共有パッケージの作成を通じて、UI コンポーネント、ユーティリティ関数、型定義などを一元管理できるようになりました。これにより、コードの重複が減り、保守性が向上します。

今後の拡張の可能性

構築した Monorepo は、以下のような拡張が可能です。

  • Storybook の導入による UI コンポーネントのカタログ化
  • Vitest による共有パッケージのテスト強化
  • ESLintPrettier の共通設定によるコード品質の統一
  • Changesets によるバージョン管理の自動化
  • VercelNetlify へのデプロイ設定

Monorepo は初期構築に多少の労力が必要ですが、プロジェクトが成長するにつれて、その恩恵を実感できるようになります。複数の Vue.js プロジェクトを管理している方は、ぜひこの手法を試してみてくださいね。

最初は小規模な構成から始めて、必要に応じてパッケージを追加していくアプローチがおすすめです。本記事で紹介した基本的な構成を土台に、あなたのプロジェクトに最適な Monorepo を構築していってください。

関連リンク