0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Turborepo + pnpmモノレポ移行で遭遇!依存解決の壁と高速化の秘策

0
Posted at

多くのエンジニアがモノレポ導入を検討する際、「依存関係の管理が複雑になるのでは?」「ビルドが遅くなって結局生産性が下がるのでは?」といった不安に直面します。特に大規模プロジェクトやマイクロサービスが混在する環境では、この懸念は現実のものとなりがちです。

この記事では、Turborepoとpnpmを使ったモノレポ移行で遭遇しやすい依存解決の壁と、ビルドを劇的に高速化する秘策を、具体的なコードと設定例を交えて解説します。この記事を読めば、あなたのモノレポは堅牢かつ高速な開発体験を提供できるようになるでしょう。

なぜ今、Turborepo + pnpmモノレポなのか?

このセクションでは、Turborepoとpnpmを組み合わせるメリットと、大規模な開発でモノレポが選ばれる背景について解説します。

モノレポは、複数のプロジェクトを単一のリポジトリで管理する開発スタイルです。これにより、コードの再利用性向上、一貫したツール設定、そしてリファクタリングの容易さといったメリットが生まれます。しかし、従来のパッケージマネージャー(npm, Yarn v1)では、依存関係のホイスティング問題やビルド時間の増大といった課題がありました。

そこで登場するのが、pnpmTurborepoです。

  • 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 が実行されていないため、依存関係が正しくリンクされていない。
  • 回避策:
    1. ルートディレクトリで pnpm install を実行し、すべての依存関係がリンクされていることを確認します。
    2. turbo がルートの package.jsondevDependencies に含まれていることを確認します。
    3. 最も確実な方法は、pnpm exec turbo <command> のように pnpm exec を使用して実行することです。これにより、ローカルにインストールされた turbo コマンドが確実に実行されます。

2. ワークスペース設定の問題

このセクションでは、pnpmがワークスペースを認識しない場合のトラブルシューティングを行います。

  • エラー内容: No projects matched the filters や、期待するパッケージが見つからないなどのワークスペース設定に関連するエラー。
  • 原因: pnpm-workspace.yaml が正しく設定されておらず、すべてのワークスペースがリストされていないか、パスパターンが間違っている。
  • 回避策:
    1. pnpm-workspace.yaml ファイルがプロジェクトルートに存在し、packages: フィールドにすべてのワークスペースのパスパターンが正しく記述されていることを確認します(例: packages: - "packages/*" - "apps/*")。
    2. pnpm install をルートで実行し、pnpmがワークスペースを正しく認識しているか確認します。
    3. 特定のワークスペースの依存関係をインストールするには、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のパス解決が追いつかないことがあります。
  • 回避策:
    1. 共有 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 // ソースマップを生成
        }
      }
      
    2. referencespaths の設定: 各パッケージの 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 は、インポートパスを簡略化するために使用されます。
    3. 共有パッケージの型定義生成: 共有パッケージ (@repo/ui など) の tsconfig.jsondeclaration: truedeclarationMap: true を有効にすることで、正確な型情報が生成され、VS Codeの「定義へ移動」などがパッケージ境界を越えて機能するようになります。

4. CI/CDでのネットワーク接続問題 (pnpm)

このセクションでは、CI/CD環境でpnpmのインストールが失敗するケースとその対策を説明します。

  • エラー内容: VercelやGitHub ActionsなどのCI/CD環境での pnpm install プロセス中にネットワーク接続の問題が発生し、ERR_INVALID_THIS やタイムアウトのようなエラーが表示される。
  • 原因: npmレジストリからのパッケージ取得方法に問題があるか、ネットワークタイムアウトが発生している可能性があります。CI環境のネットワークが不安定だったり、リソースが制限されている場合に発生しやすいです。
  • 回避策:
    1. pnpmのバージョン固定: package.json でpnpmのバージョンを固定します(例: "packageManager": "pnpm@9.15.9")。これにより、CI環境で常に同じpnpmバージョンが使用され、予期せぬ挙動を防ぎます。
    2. .npmrc でネットワーク設定を調整: プロジェクトルートに .npmrc ファイルを追加し、レジストリの指定やネットワークタイムアウト、リトライ回数を設定します。
      # my-monorepo/.npmrc
      registry=https://registry.npmjs.org/
      network-timeout=300000 # 5分 (ミリ秒単位)
      fetch-retries=5
      fetch-retry-factor=2
      
      これにより、CI環境のネットワーク状況に合わせてpnpmの挙動を調整できます。
    3. pnpm-lock.yaml のコミット: pnpm-lock.yaml がバージョン管理下にコミットされていることを確認し、依存関係の解決が常に再現可能であることを保証します。キャッシュされた依存関係をクリアして再デプロイを試みることも有効です。

モノレポ高速化の秘策: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.jsonpipeline 設定が、これらの挙動を制御します。

例えば、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.jsonpnpm-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.yamlturbo.json の適切な設定
  • TypeScriptの referencespaths を使った型解決
  • リモートキャッシュやCI/CDでの --filter を活用した高速化

これらの知識と実践的な設定例を活用することで、あなたのモノレポは開発体験を向上させ、開発生産性を飛躍的に高めることができるでしょう。

さらに深く学びたい場合は、公式ドキュメントが最も信頼できる情報源です。

モノレポの導入は、最初は学習コストがかかるかもしれませんが、長期的に見ればチームの生産性向上に大きく貢献します。ぜひ、この記事を参考に、あなたのプロジェクトにTurborepoとpnpmを導入してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?