多くのエンジニアがモノレポ導入を検討する際、「依存関係の管理が複雑になるのでは?」「ビルドが遅くなって結局生産性が下がるのでは?」といった不安に直面します。特に大規模プロジェクトやマイクロサービスが混在する環境では、この懸念は現実のものとなりがちです。
この記事では、Turborepoとpnpmを使ったモノレポ移行で遭遇しやすい依存解決の壁と、ビルドを劇的に高速化する秘策を、具体的なコードと設定例を交えて解説します。この記事を読めば、あなたのモノレポは堅牢かつ高速な開発体験を提供できるようになるでしょう。
なぜ今、Turborepo + pnpmモノレポなのか?
このセクションでは、Turborepoとpnpmを組み合わせるメリットと、大規模な開発でモノレポが選ばれる背景について解説します。
モノレポは、複数のプロジェクトを単一のリポジトリで管理する開発スタイルです。これにより、コードの再利用性向上、一貫したツール設定、そしてリファクタリングの容易さといったメリットが生まれます。しかし、従来のパッケージマネージャー(npm, Yarn v1)では、依存関係のホイスティング問題やビルド時間の増大といった課題がありました。
そこで登場するのが、pnpmとTurborepoです。
- pnpm: 独自のシンボリックリンク戦略により、依存関係の重複を排除し、ディスク使用量を削減します。また、ワークスペース機能でモノレポ内のパッケージ間依存を効率的に管理できます。これにより、依存関係のホイスティング問題を回避し、より予測可能な依存解決を実現します。
- Turborepo: JavaScript/TypeScriptのモノレポに特化した高性能ビルドシステムです。タスクの並列実行、スマートなキャッシュ(ローカル・リモート)、そして変更された部分のみをビルドする増分ビルドにより、ビルド時間を劇的に短縮します。
この2つのツールを組み合わせることで、モノレポのメリットを最大限に享受しつつ、課題を効果的に解決できるのです。
Turborepo + pnpmモノレポの初期セットアップ
ここでは、新規プロジェクトでTurborepoとpnpmを使ったモノレポを構築する基本的な手順を解説します。
1. プロジェクトの初期化とpnpmのセットアップ
まず、pnpmをグローバルにインストールし、モノレポのルートディレクトリを作成・初期化します。
npm install -g pnpm # pnpmをグローバルインストール(既にインストール済みの場合はスキップ)
mkdir my-monorepo
cd my-monorepo
git init
pnpm init
ルートの package.json には、誤ってnpmに公開されるのを防ぐために "private": true を追加するのがベストプラクティスです。
// my-monorepo/package.json
{
"name": "my-monorepo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"private": true // これを追加
}
2. pnpmワークスペースの設定
次に、pnpmにモノレポ内のパッケージの場所を教えるために、pnpm-workspace.yaml を作成します。
# my-monorepo/pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
この設定により、apps ディレクトリと packages ディレクトリ内のすべてのサブディレクトリがpnpmワークスペースとして認識されます。apps/ にはデプロイ可能なアプリケーション(例: web, api)、packages/ には再利用可能なUIコンポーネントやユーティリティライブラリなどを配置するのが一般的です。
mkdir -p apps/web apps/api packages/ui packages/utils
3. Turborepoのインストールと設定
プロジェクトルートでTurborepoを開発依存としてインストールします。
pnpm add turbo --save-dev
次に、Turborepoのタスク実行とキャッシュを制御する turbo.json をルートディレクトリに作成します。
// my-monorepo/turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
この turbo.json の設定は非常に重要です。
-
dependsOn: タスクの依存関係を定義します。^buildは、現在のワークスペースのbuildタスクが実行される前に、そのワークスペースが依存するすべてのパッケージのbuildタスクが完了する必要があることを示します。これにより、正しい順序でビルドが実行されます。 -
outputs: Turborepoがキャッシュするディレクトリを指定します。次回同じタスクが実行された際に、これらのキャッシュがあれば再実行せずに結果を再利用し、ビルド時間を大幅に短縮します。 -
cache: false/persistent: true:devタスクのように、永続的に実行され、頻繁に状態が変化するタスクはキャッシュせず、常に実行し続ける設定です。
4. パッケージとアプリケーションの作成例
ここでは、@repo/ui という共有UIパッケージと、それを消費する web アプリケーションの例を示します。
packages/ui/package.json:
// my-monorepo/packages/ui/package.json
{
"name": "@repo/ui", // npmレジストリとの競合を避けるため名前空間を使用
"version": "0.0.0",
"private": true, // npmに公開されないようにする
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc",
"lint": "eslint src/",
"dev": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0"
}
}
name フィールドに @repo/ui のように名前空間プレフィックスを使用するのは、内部パッケージがnpmレジストリ上の他のパッケージと競合するのを避けるためのベストプラクティスです。
apps/web/package.json で @repo/ui を利用する例:
// my-monorepo/apps/web/package.json
{
"name": "web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@repo/ui": "workspace:^" // ワークスペース内のパッケージを参照
},
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0"
}
}
"@repo/ui": "workspace:^" は、pnpmがワークスペース内でこのパッケージを見つけ、シンボリックリンクを張るための特別なプロトコルです。^ は、依存するパッケージのバージョンがワークスペース内で利用可能な最新のメジャーバージョンと互換性があることを示します。
5. Turborepoコマンドの実行
モノレポ全体でタスクを実行するには、pnpm turbo run <task-name> を使用します。
- すべてのワークスペースで
buildタスクを実行:pnpm turbo run build - 特定のワークスペース (
web) でdevタスクを実行:pnpm turbo run dev --filter=web
Turborepo + pnpmモノレポで遭遇しやすい依存解決の壁と回避策
モノレポでは、ポリレポでは経験しないような特有の依存解決やTypeScriptの設定に関する課題に直面することがあります。ここでは、よくあるハマりどころとその回避策を解説します。
1. turbo バイナリが見つからないエラー
このセクションでは、Turborepoコマンドが実行できない場合の対処法を説明します。
-
エラー内容:
Error: Cannot find module 'C:\Dev\my-monorepo\node_modules\turbo\bin\turbo'のようなエラーが発生し、devスクリプトなどが動作しない。 -
原因: ルートディレクトリにTurborepoが正しくインストールされていないか、
pnpm installが実行されていないため、依存関係が正しくリンクされていない。 -
回避策:
- ルートディレクトリで
pnpm installを実行し、すべての依存関係がリンクされていることを確認します。 -
turboがルートのpackage.jsonのdevDependenciesに含まれていることを確認します。 - 最も確実な方法は、
pnpm exec turbo <command>のようにpnpm execを使用して実行することです。これにより、ローカルにインストールされたturboコマンドが確実に実行されます。
- ルートディレクトリで
2. ワークスペース設定の問題
このセクションでは、pnpmがワークスペースを認識しない場合のトラブルシューティングを行います。
-
エラー内容:
No projects matched the filtersや、期待するパッケージが見つからないなどのワークスペース設定に関連するエラー。 -
原因:
pnpm-workspace.yamlが正しく設定されておらず、すべてのワークスペースがリストされていないか、パスパターンが間違っている。 -
回避策:
-
pnpm-workspace.yamlファイルがプロジェクトルートに存在し、packages:フィールドにすべてのワークスペースのパスパターンが正しく記述されていることを確認します(例:packages: - "packages/*" - "apps/*")。 -
pnpm installをルートで実行し、pnpmがワークスペースを正しく認識しているか確認します。 - 特定のワークスペースの依存関係をインストールするには、
pnpm install --filter <workspace-name>を実行してみます。
-
3. TypeScriptの依存解決問題
モノレポでのTypeScriptは、ポリレポよりも複雑な設定が必要です。ここでは、型解決の課題とその解決策を深掘りします。
-
エラー内容: モノレポ内でTypeScriptが内部依存関係を正しく解決できない(例:
Cannot find module '@repo/ui' or its corresponding type declarations.)。 -
原因: Node.jsは従来のモジュール解決を行う一方で、TypeScriptは各
tsconfig.jsonで依存関係の場所を明示的に指定する必要があるため、問題が発生しやすいです。特に、pnpmのシンボリックリンク構造が従来のnode_modulesと異なるため、TypeScriptのパス解決が追いつかないことがあります。 -
回避策:
-
共有
tsconfig.base.jsonの作成: ワークスペースのルートまたはpackages/configのような共有パッケージに、共通のTypeScript設定ファイル (tsconfig.base.jsonなど) を作成し、各パッケージがそれをextendsで継承するようにします。// my-monorepo/tsconfig.base.json { "compilerOptions": { "target": "es2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "incremental": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "declaration": true, // 型定義ファイルを生成 "declarationMap": true // ソースマップを生成 } } -
referencesとpathsの設定: 各パッケージのtsconfig.jsonで、依存する内部パッケージへのreferencesを追加し、pathsでエイリアスを設定します。これにより、TypeScriptがパッケージ間の参照を正しく解決できます。
例:apps/web/tsconfig.jsonで@repo/uiを参照する場合// my-monorepo/apps/web/tsconfig.json { "extends": "../../tsconfig.base.json", // 共有設定を継承 "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "baseUrl": ".", "paths": { "@repo/ui": ["../../packages/ui/src"] // パスエイリアスで解決 } }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules"], "references": [ { "path": "../../packages/ui" } // 参照を追加 ] }referencesは、TypeScriptのProject References機能を利用して、依存するプロジェクトの型情報を解決するために重要です。pathsは、インポートパスを簡略化するために使用されます。 -
共有パッケージの型定義生成: 共有パッケージ (
@repo/uiなど) のtsconfig.jsonでdeclaration: trueとdeclarationMap: trueを有効にすることで、正確な型情報が生成され、VS Codeの「定義へ移動」などがパッケージ境界を越えて機能するようになります。
-
共有
4. CI/CDでのネットワーク接続問題 (pnpm)
このセクションでは、CI/CD環境でpnpmのインストールが失敗するケースとその対策を説明します。
-
エラー内容: VercelやGitHub ActionsなどのCI/CD環境での
pnpm installプロセス中にネットワーク接続の問題が発生し、ERR_INVALID_THISやタイムアウトのようなエラーが表示される。 - 原因: npmレジストリからのパッケージ取得方法に問題があるか、ネットワークタイムアウトが発生している可能性があります。CI環境のネットワークが不安定だったり、リソースが制限されている場合に発生しやすいです。
-
回避策:
-
pnpmのバージョン固定:
package.jsonでpnpmのバージョンを固定します(例:"packageManager": "pnpm@9.15.9")。これにより、CI環境で常に同じpnpmバージョンが使用され、予期せぬ挙動を防ぎます。 -
.npmrcでネットワーク設定を調整: プロジェクトルートに.npmrcファイルを追加し、レジストリの指定やネットワークタイムアウト、リトライ回数を設定します。これにより、CI環境のネットワーク状況に合わせてpnpmの挙動を調整できます。# my-monorepo/.npmrc registry=https://registry.npmjs.org/ network-timeout=300000 # 5分 (ミリ秒単位) fetch-retries=5 fetch-retry-factor=2 -
pnpm-lock.yamlのコミット:pnpm-lock.yamlがバージョン管理下にコミットされていることを確認し、依存関係の解決が常に再現可能であることを保証します。キャッシュされた依存関係をクリアして再デプロイを試みることも有効です。
-
pnpmのバージョン固定:
モノレポ高速化の秘策:TurborepoキャッシュとCI/CD連携
Turborepoの真価は、その高速なビルドとキャッシュ戦略にあります。このセクションでは、ビルドを劇的に高速化する具体的な方法と、CI/CDパイプラインでの活用法を解説します。
1. リモートキャッシュの活用
Turborepoは、ローカルキャッシュだけでなく、リモートキャッシュもサポートしています。これにより、CI/CDパイプラインを含むすべての開発者が同じキャッシュを共有でき、ビルド時間を大幅に短縮できます。
- Vercel Remote Cache: Vercelが提供するリモートキャッシュは無料で利用でき、Vercelにアプリケーションをデプロイする必要はありません。
リモートキャッシュを設定するには、以下のコマンドを実行します。
pnpm turbo login # Vercelアカウントにマシンを紐付け
pnpm turbo link # 現在のリポジトリをVercelのリモートキャッシュに接続
CI/CD環境では、Vercelの環境変数(VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID)を設定することで、自動的にリモートキャッシュが利用されます。
2. 増分ビルドと並列実行
Turborepoは、変更されたパッケージとその依存関係のみをビルドする「増分ビルド」と、タスクを並列実行する機能により、ビルド時間を最適化します。turbo.json の pipeline 設定が、これらの挙動を制御します。
例えば、build タスクの dependsOn: ["^build"] は、依存関係グラフを解析し、必要なビルドを正しい順序で実行します。これにより、大規模なモノレポでも効率的なビルドが可能になります。
3. CI/CDでの影響範囲検出 (--filter の活用)
CI/CDパイプラインでは、すべてのパッケージを毎回ビルド・テスト・デプロイするのは非効率です。Turborepoの --filter オプションとGitの差分検出を組み合わせることで、変更されたパッケージのみを対象にタスクを実行できます。
# 変更されたパッケージとその依存関係のみをビルド
pnpm turbo run build --filter="[HEAD^1]..."
# 変更されたアプリケーションのみをデプロイ(例: Next.jsアプリ)
pnpm turbo run deploy --filter="@repo/web" # この例は特定のパッケージを指すが、実際は変更検出と組み合わせる
GitHub Actionsなどで、以下のようなステップを定義することで、変更されたプロジェクトのみにアクションを絞り込むことができます。
# .github/workflows/ci.yaml
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 差分検出のために履歴全体を取得
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build affected packages
run: pnpm turbo run build --filter="[HEAD^1]..."
env:
TURBO_TOKEN: ${{ secrets.VERCEL_TOKEN }} # リモートキャッシュ用
TURBO_TEAM: ${{ secrets.VERCEL_ORG_ID }}
TURBO_API: https://api.turbo.build
4. Dockerビルドの最適化
モノレポ内の各マイクロサービスをDockerizeする際、turbo prune コマンドが非常に役立ちます。これは、特定のアプリケーションとその依存関係のみを含む最小限の package.json と pnpm-lock.yaml を生成し、マルチステージDockerビルドと組み合わせることで、軽量なDockerイメージを作成できます。
# apps/web/Dockerfile
FROM node:20-alpine AS base
# Install pnpm and Turborepo
RUN npm install -g pnpm@9.0.0 turbo@2.0.0
# Base install stage
FROM base AS install
WORKDIR /app
COPY . .
# `turbo prune` で web アプリケーションとその依存関係のみをコピー
RUN turbo prune --scope=web --docker
# Add lockfile and package.json's of isolated subworkspace
FROM base AS builder
WORKDIR /app
COPY --from=install /app/out/json/ .
COPY --from=install /app/out/pnpm-lock.yaml .
RUN pnpm install --frozen-lockfile
# Build the project
COPY --from=install /app/out/full/ .
COPY turbo.json .
RUN pnpm turbo run build --filter=web
# Final stage
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Copy build output and production dependencies
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
CMD ["node", "server.js"]
まとめと次の一歩
この記事では、Turborepo + pnpmモノレポの導入から、依存解決の壁の乗り越え方、そしてビルド高速化の秘策までを解説しました。
- pnpmによる効率的な依存関係管理とワークスペース機能
- Turborepoによるタスク並列実行、スマートキャッシュ、増分ビルド
-
pnpm-workspace.yamlとturbo.jsonの適切な設定 - TypeScriptの
referencesとpathsを使った型解決 - リモートキャッシュやCI/CDでの
--filterを活用した高速化
これらの知識と実践的な設定例を活用することで、あなたのモノレポは開発体験を向上させ、開発生産性を飛躍的に高めることができるでしょう。
さらに深く学びたい場合は、公式ドキュメントが最も信頼できる情報源です。
モノレポの導入は、最初は学習コストがかかるかもしれませんが、長期的に見ればチームの生産性向上に大きく貢献します。ぜひ、この記事を参考に、あなたのプロジェクトにTurborepoとpnpmを導入してみてください。