T-CREATOR

Nuxt Monorepo 構築:Yarn Workspaces/Turborepo で共有コンポーネント基盤を整える

Nuxt Monorepo 構築:Yarn Workspaces/Turborepo で共有コンポーネント基盤を整える

複数の Nuxt プロジェクトを運用していると、共通のコンポーネントやユーティリティを各プロジェクトで重複して管理する手間に悩まされますよね。コピー&ペーストでコードを使い回すと、バグ修正や機能追加のたびに全プロジェクトを手作業で更新しなければならず、メンテナンスコストが膨れ上がってしまいます。

そこで注目したいのが Monorepo(モノレポ) という開発手法です。複数のプロジェクトを 1 つのリポジトリで管理し、共通コードを効率的に共有できます。本記事では、Yarn Workspaces と Turborepo を組み合わせて、Nuxt プロジェクト向けの Monorepo 環境を構築する方法を、初心者の方にもわかりやすく解説していきますね。

背景

Monorepo とは何か

Monorepo は「Monolithic Repository(一枚岩のリポジトリ)」の略で、複数のプロジェクトやパッケージを 1 つの Git リポジトリで管理する開発手法です。Google や Facebook といった大手企業でも採用されており、コードの再利用性やメンテナンス性を大幅に向上させます。

従来の開発では、プロジェクトごとに別々のリポジトリを作成し、共通コードは npm パッケージとして公開するか、コピー&ペーストで対応していました。しかし、この方法ではバージョン管理が煩雑になり、コードの同期が困難になります。

Yarn Workspaces の役割

Yarn Workspaces は、Monorepo 内の複数パッケージを効率的に管理するための機能です。依存関係を一元管理し、共通の node_modules を使用することでディスク容量を節約できます。

以下の図は、Yarn Workspaces による依存関係の一元管理を示しています。

mermaidflowchart TB
  root["ルートディレクトリ"]
  ws1["apps/web-app"]
  ws2["apps/admin-app"]
  pkg1["packages/ui"]
  pkg2["packages/utils"]
  nm["node_modules<br/>(共有)"]

  root --> ws1
  root --> ws2
  root --> pkg1
  root --> pkg2
  root --> nm

  ws1 -->|依存| pkg1
  ws1 -->|依存| pkg2
  ws2 -->|依存| pkg1
  ws2 -->|依存| pkg2

  pkg1 -.->|共有| nm
  pkg2 -.->|共有| nm

この図から分かるように、各ワークスペースは共通の node_modules を参照し、パッケージ間の依存関係も明確に管理されています。

図のポイント

  • ルートディレクトリが全体を統括
  • 共通パッケージ(ui、utils)を複数アプリが参照
  • node_modules が一元化されディスク容量を節約

Turborepo が解決する課題

Yarn Workspaces だけでは、ビルドやテストの実行を効率化できません。複数のパッケージを持つ Monorepo では、変更されたパッケージだけをビルドしたり、並列処理で高速化したりする仕組みが必要です。

Turborepo は、タスクの実行を最適化するビルドシステムです。キャッシュ機能により、変更のないパッケージのビルドをスキップし、開発者の待ち時間を大幅に削減します。

課題

複数 Nuxt プロジェクトの管理における問題点

実際の開発現場では、以下のような課題に直面することが多いでしょう。

1. コードの重複管理

各プロジェクトで同じコンポーネント(ボタン、モーダル、フォームなど)を個別に管理すると、修正のたびに全プロジェクトを更新する必要があります。これはバグの温床となり、開発効率を著しく低下させますね。

2. 依存関係のバージョン不一致

プロジェクトごとに異なるバージョンの Vue や Nuxt を使用していると、予期しない動作やバグが発生しやすくなります。バージョンの統一は手動では困難です。

3. ビルド時間の増大

プロジェクトが増えるほど、全体のビルドやテストに時間がかかります。CI/CD パイプラインでの待ち時間が長くなり、開発サイクルが遅延してしまいます。

以下の図は、従来の開発手法と Monorepo の違いを比較したものです。

mermaidflowchart LR
  subgraph old["従来の方式"]
    direction TB
    p1["プロジェクト A<br/>リポジトリ"]
    p2["プロジェクト B<br/>リポジトリ"]
    p3["プロジェクト C<br/>リポジトリ"]

    p1 -.->|コピー| comp1["共通<br/>コンポーネント"]
    p2 -.->|コピー| comp2["共通<br/>コンポーネント"]
    p3 -.->|コピー| comp3["共通<br/>コンポーネント"]
  end

  subgraph new["Monorepo 方式"]
    direction TB
    mono["単一リポジトリ"]
    mono --> app1["apps/A"]
    mono --> app2["apps/B"]
    mono --> app3["apps/C"]
    mono --> shared["packages/shared"]

    app1 -->|参照| shared
    app2 -->|参照| shared
    app3 -->|参照| shared
  end

図から読み取れる違い

  • 従来方式ではコンポーネントが 3 箇所に重複
  • Monorepo では共通パッケージを一元管理
  • 修正が 1 箇所で済み、同期の手間が不要

解決策

Yarn Workspaces と Turborepo による統合環境

Nuxt Monorepo を構築することで、上記の課題をすべて解決できます。Yarn Workspaces で依存関係を一元管理し、Turborepo でビルドを最適化する、この 2 つのツールの組み合わせが鍵となります。

以下の図は、Monorepo におけるビルドフローを示しています。

mermaidflowchart TD
  dev["開発者がコード変更"]
  turbo["Turborepo が<br/>変更を検出"]
  cache{キャッシュ<br/>あり?}
  skip["ビルドスキップ"]
  parallel["並列ビルド実行"]
  result["ビルド完了"]

  dev --> turbo
  turbo --> cache
  cache -->|Yes| skip
  cache -->|No| parallel
  skip --> result
  parallel --> result

ビルドフローの利点

  • 変更のないパッケージはキャッシュを利用
  • 複数パッケージを並列処理で高速化
  • 開発者の待ち時間を大幅に削減

アプローチの全体像

Monorepo 構築は、以下の 3 つのステップで進めていきます。

#ステップ主な作業内容
1環境準備Yarn、Node.js のインストール
2Workspaces 設定package.json の設定、ディレクトリ構成
3Turborepo 導入ビルド最適化、キャッシュ設定

それでは、具体的な手順を見ていきましょう。

具体例

ステップ 1:プロジェクトの初期化

まず、Monorepo のルートディレクトリを作成し、基本的な設定を行います。

プロジェクトディレクトリの作成

以下のコマンドで、新しいプロジェクトディレクトリを作成しましょう。

bashmkdir nuxt-monorepo
cd nuxt-monorepo

Yarn の初期化

次に、Yarn を初期化します。このコマンドで package.json が生成されます。

bashyarn init -y

package.json の設定

生成された package.json を編集して、Workspaces を有効化します。private フィールドを true に設定することで、誤って npm に公開されるのを防げます。

json{
  "name": "nuxt-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

設定のポイント

  • private: true でリポジトリ全体を非公開に設定
  • workspacesapps​/​*packages​/​* を指定
  • これにより、これらのディレクトリ配下がワークスペースとして認識されます

ステップ 2:ディレクトリ構成の作成

Monorepo の標準的なディレクトリ構成を作成します。apps には実際のアプリケーション、packages には共有パッケージを配置しますね。

bashmkdir -p apps/web apps/admin packages/ui packages/utils

作成後のディレクトリ構成は以下のようになります。

plaintextnuxt-monorepo/
├── apps/
│   ├── web/          # ユーザー向け Web アプリ
│   └── admin/        # 管理画面アプリ
├── packages/
│   ├── ui/           # 共有 UI コンポーネント
│   └── utils/        # 共有ユーティリティ
└── package.json

ステップ 3:Nuxt アプリケーションの作成

各アプリケーションディレクトリで Nuxt プロジェクトを初期化します。まずは web アプリから始めましょう。

web アプリの作成

apps​/​web ディレクトリに移動して、Nuxt 3 プロジェクトを作成します。

bashcd apps/web
npx nuxi init .

package.json の name フィールド設定

生成された package.jsonname フィールドを、ワークスペース内で一意になるように変更します。

json{
  "name": "@monorepo/web",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt build",
    "preview": "nuxt preview"
  }
}

命名規則のポイント

  • @monorepo​/​ というスコープを付けることで、パッケージ名の衝突を防止
  • 各ワークスペースに一貫性のある命名を適用

admin アプリの作成

同様に、admin アプリも作成しましょう。

bashcd ../admin
npx nuxi init .

package.jsonname@monorepo​/​admin に変更します。

json{
  "name": "@monorepo/admin",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "nuxt dev --port 3001",
    "build": "nuxt build",
    "preview": "nuxt preview"
  }
}

ポート番号の設定

  • --port 3001 を追加して、web アプリ(デフォルト 3000)と競合しないように設定
  • 複数のアプリを同時に起動できるようになります

ステップ 4:共有パッケージの作成

共通の UI コンポーネントを管理するパッケージを作成します。これにより、複数のアプリで同じコンポーネントを再利用できますね。

ui パッケージの初期化

packages​/​ui ディレクトリに移動して、パッケージを初期化します。

bashcd ../../packages/ui
yarn init -y

package.json の設定

UI パッケージの package.json を以下のように設定します。

json{
  "name": "@monorepo/ui",
  "version": "1.0.0",
  "main": "index.ts",
  "types": "index.ts",
  "dependencies": {
    "vue": "^3.4.0"
  }
}

エクスポート設定のポイント

  • maintypes フィールドでエントリーポイントを指定
  • TypeScript の型情報も提供されるため、型安全性が向上

共通コンポーネントの作成

サンプルとして、シンプルなボタンコンポーネントを作成しましょう。

vue<!-- packages/ui/components/BaseButton.vue -->
<template>
  <button :class="buttonClass" @click="handleClick">
    <!-- デフォルトスロットでボタンのテキストを受け取る -->
    <slot />
  </button>
</template>

<script setup lang="ts">
import { computed } from 'vue';

// Props の型定義
interface Props {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'medium',
});

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

// バリアントに基づくクラス名を計算
const buttonClass = computed(() => {
  return `btn btn--${props.variant} btn--${props.size}`;
});

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

コンポーネント設計のポイント

  • TypeScript で Props と Emits を厳密に型定義
  • variantsize で柔軟なスタイリングに対応
  • スロットを使用して、ボタンの内容をカスタマイズ可能に

スタイルの追加

ボタンの見た目を定義するスタイルを追加します。

vue<style scoped>
/* ベーススタイル */
.btn {
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.2s ease;
}

/* サイズバリエーション */
.btn--small {
  padding: 8px 16px;
  font-size: 14px;
}

.btn--medium {
  padding: 12px 24px;
  font-size: 16px;
}

.btn--large {
  padding: 16px 32px;
  font-size: 18px;
}

/* カラーバリエーション */
.btn--primary {
  background-color: #3b82f6;
  color: white;
}

.btn--primary:hover {
  background-color: #2563eb;
}

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

.btn--secondary:hover {
  background-color: #4b5563;
}

.btn--danger {
  background-color: #ef4444;
  color: white;
}

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

エクスポートファイルの作成

作成したコンポーネントを他のパッケージから利用できるように、エクスポートファイルを作成します。

typescript// packages/ui/index.ts
export { default as BaseButton } from './components/BaseButton.vue';

エクスポートの仕組み

  • 他のワークスペースから import { BaseButton } from '@monorepo​/​ui' で利用可能
  • 複数のコンポーネントを追加する場合もこのファイルに追記

ステップ 5:utils パッケージの作成

ユーティリティ関数を管理するパッケージも作成しましょう。日付フォーマットやバリデーションなど、共通的な処理を集約します。

パッケージの初期化

bashcd ../utils
yarn init -y

package.json の設定

json{
  "name": "@monorepo/utils",
  "version": "1.0.0",
  "main": "index.ts",
  "types": "index.ts"
}

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

日付フォーマット関数を実装してみましょう。

typescript// packages/utils/formatDate.ts

/**
 * 日付を YYYY-MM-DD 形式にフォーマットする
 * @param date - フォーマット対象の Date オブジェクト
 * @returns フォーマットされた日付文字列
 */
export const 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}`;
};

エクスポートファイルの作成

typescript// packages/utils/index.ts
export { formatDate } from './formatDate';

ステップ 6:ワークスペース間の依存関係設定

作成した共有パッケージを、各アプリケーションから利用できるように設定します。

web アプリでの依存関係追加

apps​/​webpackage.json に、共有パッケージへの依存を追加します。

json{
  "name": "@monorepo/web",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@monorepo/ui": "*",
    "@monorepo/utils": "*",
    "nuxt": "^3.13.0",
    "vue": "^3.4.0"
  }
}

バージョン指定のポイント

  • * を使用することで、常に最新のローカルバージョンを参照
  • Monorepo 内部のパッケージはバージョン管理が不要

依存関係のインストール

ルートディレクトリに戻り、すべての依存関係をインストールします。

bashcd ../../
yarn install

このコマンドにより、Yarn Workspaces が自動的に依存関係を解決し、共通の node_modules にパッケージをインストールしてくれます。

ステップ 7:共有コンポーネントの利用

web アプリで、作成した共有コンポーネントを実際に使ってみましょう。

Nuxt プラグインの作成

共有 UI コンポーネントをグローバルに登録するため、Nuxt プラグインを作成します。

typescript// apps/web/plugins/components.ts
import { BaseButton } from '@monorepo/ui';

export default defineNuxtPlugin((nuxtApp) => {
  // グローバルコンポーネントとして登録
  nuxtApp.vueApp.component('BaseButton', BaseButton);
});

プラグインの役割

  • アプリ全体でコンポーネントを import なしで利用可能
  • 大規模アプリでの記述量を削減

ページでの利用

トップページで共有コンポーネントとユーティリティを使用します。

vue<!-- apps/web/app.vue -->
<template>
  <div class="container">
    <h1>Welcome to Nuxt Monorepo</h1>

    <!-- 共有 UI コンポーネントの利用 -->
    <BaseButton
      variant="primary"
      size="large"
      @click="handleClick"
    >
      クリックしてください
    </BaseButton>

    <!-- 日付表示 -->
    <p>今日の日付: {{ formattedDate }}</p>
  </div>
</template>

<script setup lang="ts">
import { formatDate } from '@monorepo/utils';

// 現在の日付を取得してフォーマット
const today = new Date();
const formattedDate = formatDate(today);

// ボタンクリック時の処理
const handleClick = () => {
  alert('ボタンがクリックされました!');
};
</script>

<style scoped>
.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 40px 20px;
  text-align: center;
}

h1 {
  margin-bottom: 24px;
  color: #1f2937;
}

p {
  margin-top: 16px;
  color: #6b7280;
}
</style>

ステップ 8:Turborepo の導入

ビルドを最適化するため、Turborepo を導入します。これにより、キャッシュや並列処理の恩恵を受けられますね。

Turborepo のインストール

ルートディレクトリで Turborepo をインストールします。

bashyarn add -D -W turbo

オプションの意味

  • -D は開発依存として追加
  • -W はワークスペースのルートに追加することを明示

turbo.json の作成

Turborepo の設定ファイルを作成します。この設定により、タスクの実行順序やキャッシュの動作を制御できます。

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

設定項目の解説

項目説明
dependsOn依存するタスクの実行順序を定義
outputsキャッシュ対象のディレクトリを指定
cacheキャッシュの有効/無効を設定
persistent継続実行するタスクかどうか

^build の意味

  • ^ は「依存するワークスペース」を表す記号
  • 依存パッケージのビルドが完了してから、このパッケージのビルドを実行

ルート package.json にスクリプト追加

Turborepo を使ったタスク実行コマンドを追加します。

json{
  "name": "nuxt-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "^2.3.0"
  }
}

ステップ 9:開発サーバーの起動

すべての設定が完了したら、開発サーバーを起動してみましょう。

bashyarn dev

このコマンドにより、apps​/​webapps​/​admin の両方の開発サーバーが並列で起動します。Turborepo が自動的にタスクを管理してくれるため、手動で各アプリを起動する必要はありません。

以下のような出力が表示されれば成功です。

plaintext• Packages in scope: @monorepo/admin, @monorepo/web
• Running dev in 2 packages
• Remote caching disabled

@monorepo/web:dev: cache bypass, force executing
@monorepo/admin:dev: cache bypass, force executing
@monorepo/web:dev: Nuxt 3.13.0 with Nitro 2.9.0
@monorepo/web:dev:   ➜ Local:   http://localhost:3000/
@monorepo/admin:dev: Nuxt 3.13.0 with Nitro 2.9.0
@monorepo/admin:dev:   ➜ Local:   http://localhost:3001/

起動確認のポイント

  • web アプリは http:​/​​/​localhost:3000
  • admin アプリは http:​/​​/​localhost:3001
  • 両方同時にアクセス可能

ステップ 10:ビルドとキャッシュの確認

Turborepo のキャッシュ機能を確認してみましょう。

初回ビルド

bashyarn build

初回は全パッケージがビルドされ、数秒〜数十秒かかります。

2 回目のビルド

何も変更せずに、再度ビルドコマンドを実行します。

bashyarn build

キャッシュが効いているため、ほぼ瞬時にビルドが完了します。以下のような出力が表示されますね。

plaintext@monorepo/web:build: cache hit, replaying logs
@monorepo/admin:build: cache hit, replaying logs

キャッシュの効果

  • 変更のないパッケージはビルドをスキップ
  • CI/CD での待ち時間を大幅に削減
  • 開発者の生産性が向上

ステップ 11:TypeScript の型チェック設定

Monorepo 全体で TypeScript の型安全性を確保するため、共通の設定を行います。

ルート tsconfig.json の作成

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}

各パッケージでの tsconfig.json

各ワークスペースは、ルートの設定を継承します。

json// packages/ui/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@monorepo/ui": ["./index.ts"]
    }
  },
  "include": ["**/*.ts", "**/*.vue"]
}

ステップ 12:VSCode の設定

開発体験を向上させるため、VSCode の設定も追加しましょう。

.vscode/settings.json の作成

json{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

設定の効果

  • ファイル保存時に自動フォーマット
  • ESLint のエラーを自動修正
  • Monorepo 内の TypeScript を正しく認識

まとめ

ここまで、Yarn Workspaces と Turborepo を使った Nuxt Monorepo の構築方法を、段階的に解説してきました。この環境を導入することで、複数のプロジェクトを効率的に管理し、共通コードの再利用性を最大化できます。

改めて、Monorepo 導入のメリットを整理しましょう。

得られる主な効果

メリット具体的な効果
コードの一元管理共通コンポーネントの修正が全プロジェクトに即座に反映
依存関係の統一バージョン不一致によるバグを防止
ビルド時間の短縮Turborepo のキャッシュで 2 回目以降が高速化
開発体験の向上型安全性と自動補完でミスを削減
メンテナンス性の改善1 つのリポジトリで全体を把握しやすい

導入時の注意点としては、初期セットアップに一定の学習コストがかかること、リポジトリのサイズが大きくなる可能性があることが挙げられます。しかし、これらのデメリットを補って余りあるメリットがありますね。

小規模なプロジェクトから始めて、徐々に Monorepo の恩恵を実感していくことをおすすめします。まずは 2〜3 個のアプリケーションと、基本的な共有パッケージから始めてみてはいかがでしょうか。

Monorepo は現代的な開発手法として、多くの企業で採用が進んでいます。本記事の手順を参考に、ぜひあなたのプロジェクトでも導入してみてください。

関連リンク