pnpm完全ガイド — 高速パッケージ管理・Workspace・モノレポ・npm/yarn移行


Node.js エコシステムには npm・yarn・pnpm という3つの主要パッケージマネージャーが存在する。その中で pnpm(Performant npm) は、ディスク使用量の削減と高速なインストールを両立することで急速に普及した。Next.js・Vite・Turborepo など主要フレームワークの公式テンプレートでも採用され、今やモノレポ開発の標準ツールとなっている。

本記事では pnpm の仕組みから実践的なモノレポ構築・CI/CD 最適化まで、設定ファイルのサンプルコードを交えながら体系的に解説する。


1. pnpm とは — npm/yarn とのパフォーマンス比較・仕組み

なぜ pnpm が速いのか

npm や yarn がパッケージを node_modules にフラットにコピーするのに対して、pnpm は コンテンツアドレッサブルストレージ(CAS)ハードリンク を組み合わせた独自アーキテクチャを採用している。

インストールの仕組みを比較すると以下のようになる。

パッケージマネージャーストレージ方式node_modules 構造
npm v7+プロジェクトごとにコピーフラット
yarn v1プロジェクトごとにコピーフラット
yarn Berry (v2+)PnP(Plug’n’Play)仮想(.yarn/cache)
pnpmグローバル CAS + ハードリンクSymlink ベース

パフォーマンスベンチマーク

ベンチマーク(2024年、React+TypeScript プロジェクト相当)では以下の結果が一般的に報告されている。

  • 初回インストール(キャッシュなし): pnpm が npm より 20〜30% 速い
  • 2回目以降(ローカルキャッシュあり): pnpm が npm より 50〜60% 速い
  • CI 環境(ロックファイル固定): pnpm が npm より 40〜70% 速い
  • ディスク使用量: 同一パッケージが複数プロジェクトにある場合、pnpm は最大 90% 削減

モノレポ環境ではこの差がさらに顕著になる。10 個のパッケージで react@18.3.0 を使用する場合、npm はそれを 10 回コピーするが、pnpm は 1 回だけストアに保存してハードリンクで参照するため、ディスク IO も大幅に削減される。


2. インストール・設定

pnpm のインストール方法

pnpm はいくつかの方法でインストールできる。

# Corepack(推奨)— Node.js 16.13+ に同梱
corepack enable
corepack prepare pnpm@latest --activate

# npm 経由でグローバルインストール
npm install -g pnpm

# curl を使ったスタンドアロンインストール(macOS/Linux)
curl -fsSL https://get.pnpm.io/install.sh | sh -

# Homebrew(macOS)
brew install pnpm

# Winget(Windows)
winget install pnpm.pnpm

Corepack を使う方法が最も推奨される。package.jsonpackageManager フィールドでバージョンを固定できるためチーム全体で同一バージョンを使用できる。

{
  "packageManager": "pnpm@9.15.0"
}

バージョン確認

pnpm --version
# 9.15.0

pnpm store path
# ~/.local/share/pnpm/store/v3 (Linux/macOS)
# %LOCALAPPDATA%\pnpm\store\v3 (Windows)

.npmrc の基本設定

プロジェクトルートに .npmrc を作成して pnpm の動作をカスタマイズする。

# .npmrc — pnpm 基本設定

# 厳格なピア依存関係チェック(推奨)
strict-peer-dependencies=false

# 自動インストールピア依存関係
auto-install-peers=true

# シャローなsymlink(パフォーマンス向上)
node-linker=hoisted

# プライベートレジストリ設定(必要な場合)
# @myorg:registry=https://npm.mycompany.com
# //npm.mycompany.com/:_authToken=${NPM_TOKEN}

# ロックファイルのバージョン固定
lockfile-version=9.0

# ファントム依存関係を防ぐ(デフォルトで有効)
# shamefully-hoist=false

# Node.js バイナリのリンク
public-hoist-pattern[]=*types*
public-hoist-pattern[]=eslint*
public-hoist-pattern[]=prettier

3. 基本コマンド

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

# 全依存関係のインストール(package.json に従う)
pnpm install
pnpm i  # 短縮形

# フローズンロックファイル(CI 環境推奨)
pnpm install --frozen-lockfile

# 既存の node_modules を削除して再インストール
pnpm install --force

パッケージの追加・削除

# 通常依存関係
pnpm add react react-dom
pnpm add react@18.3.0  # バージョン指定

# 開発依存関係
pnpm add -D typescript @types/node
pnpm add --save-dev vite

# オプション依存関係
pnpm add -O sharp

# グローバルインストール
pnpm add -g @antfu/ni

# パッケージの削除
pnpm remove react
pnpm rm lodash  # 短縮形

パッケージの更新

# インタラクティブ更新(選択 UI あり)
pnpm update --interactive
pnpm up -i  # 短縮形

# すべてを最新に更新
pnpm update --latest
pnpm up -L

# 特定パッケージの更新
pnpm update react react-dom

# マイナーバージョンまで更新(メジャーは除く)
pnpm update

スクリプトの実行

# package.json の scripts を実行
pnpm run build
pnpm build  # run を省略可能

# 環境変数を渡す
pnpm build --env-file .env.production

# 引数をスクリプトに渡す
pnpm test -- --watch

# pnpm の組み込みコマンドと区別する場合
pnpm run start

情報・診断コマンド

# インストール済みパッケージ一覧
pnpm list
pnpm ls --depth=2  # ネスト深さ指定

# 特定パッケージの詳細
pnpm info react

# 依存関係のライセンス確認
pnpm licenses list

# セキュリティ監査
pnpm audit

# 不要パッケージの確認
pnpm prune

4. Content-addressable storage(ハードリンク・ディスク節約)

ストアの仕組み

pnpm の最大の特徴は グローバルコンテンツアドレッサブルストレージ(CAS) にある。パッケージをインストールすると、まずグローバルストア(~/.pnpm-store/)にファイルが保存される。次に、プロジェクトの node_modules からストア内のファイルへ ハードリンク が作成される。

グローバルストア
~/.pnpm-store/v3/files/
  00/
    a1b2c3...(ファイルハッシュ)
  01/
    d4e5f6...
  ...

プロジェクト A/node_modules/react/index.js  ←─ ハードリンク
プロジェクト B/node_modules/react/index.js  ←─ 同じファイルを参照
プロジェクト C/node_modules/react/index.js  ←─ 同じファイルを参照

ハードリンクとはディスク上の同一ファイルへの複数の参照(inode を共有)であり、ファイルの実体はストアに 1 つだけ存在する。react@18.3.0 を 100 プロジェクトで使用しても、ディスク消費は 1 プロジェクト分と変わらない。

ストア管理コマンド

# ストアのパスを確認
pnpm store path

# ストアの整合性チェック
pnpm store verify

# 孤立したパッケージを削除(ディスク解放)
pnpm store prune

# ストアの状態確認
pnpm store status

ファイルシステムの注意点

ハードリンクはファイルシステムをまたいで作成できない。ストアとプロジェクトが別のドライブや Docker ボリュームにある場合、pnpm は自動的に コピー にフォールバックする。

# Docker 環境でのストアマウント例
# docker-compose.yml
services:
  app:
    volumes:
      - pnpm-store:/root/.local/share/pnpm/store

volumes:
  pnpm-store:

フラット node_modules の問題点

npm と yarn v1 はすべての依存関係を node_modules 直下にフラット展開する。このアーキテクチャには「幽霊依存関係(Phantom Dependencies)」という深刻な問題がある。

例えば package-alodash に依存している場合、フラット展開によって lodash がプロジェクトのルート node_modules に展開される。すると、package.jsonlodash を明示していないにも関わらず、コード内で require('lodash') が動いてしまう。

// package.json に lodash の記載なし
// npm/yarn の場合は動いてしまう(幽霊依存関係)
const _ = require('lodash');  // 危険!

// pnpm の場合は適切にエラーになる
// Error: Cannot find module 'lodash'

この状態は package-alodash への依存を削除した瞬間にビルドが壊れる、という時限爆弾を抱えることになる。

pnpm は node_modules にシンボリックリンクを使った独自の構造を構築する。

プロジェクト/
  node_modules/
    .pnpm/
      react@18.3.0/
        node_modules/
          react/          ← 実体(ハードリンク先)
      lodash@4.17.21/
        node_modules/
          lodash/         ← 実体(ハードリンク先)
    react -> .pnpm/react@18.3.0/node_modules/react
    # lodash は package.json に未記載なら展開されない

node_modules/.pnpm/ 内に実際のファイルが配置され、node_modules/ 直下には package.json に明示したパッケージへの symlink のみが作成される。これにより幽霊依存関係が構造的に防止される。

hoisted モードとの使い分け

一部のツール(Jest・ESLint 等)は node_modules のルートにパッケージが存在することを期待する。この場合は .npmrc で設定を調整する。

# .npmrc

# 一部パッケージをルートに hoisting(フラット化)
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*jest*

# すべてをホイスト(npm 互換、非推奨だが互換性のため)
shamefully-hoist=true

# node-linker を hoisted に設定(完全フラット、npm 同等)
# node-linker=hoisted

6. Workspace 設定(モノレポ・フィルタリング)

pnpm Workspace の概要

pnpm には npm workspaces や yarn workspaces と同様のモノレポサポートが組み込まれている。pnpm-workspace.yaml で Workspace の構成を定義する。

プロジェクト構造の例

my-monorepo/
  package.json
  pnpm-workspace.yaml
  .npmrc
  apps/
    web/
      package.json
    api/
      package.json
    mobile/
      package.json
  packages/
    ui/
      package.json
    utils/
      package.json
    config/
      tsconfig/
        package.json
      eslint/
        package.json

pnpm-workspace.yaml の設定

# pnpm-workspace.yaml
packages:
  # apps ディレクトリ配下のすべてのパッケージ
  - 'apps/*'
  # packages ディレクトリ配下のすべてのパッケージ
  - 'packages/**'
  # 特定のパッケージを除外
  - '!**/test/**'
  - '!**/node_modules/**'

ルートの package.json

{
  "name": "my-monorepo",
  "private": true,
  "version": "0.0.0",
  "packageManager": "pnpm@9.15.0",
  "scripts": {
    "build": "pnpm -r run build",
    "test": "pnpm -r run test",
    "dev": "pnpm --parallel run dev",
    "lint": "pnpm -r run lint",
    "typecheck": "pnpm -r run typecheck",
    "clean": "pnpm -r exec rm -rf dist .turbo"
  },
  "devDependencies": {
    "typescript": "^5.7.0",
    "turbo": "^2.3.0"
  }
}

内部パッケージの参照

モノレポ内のパッケージを相互参照するには workspace: プロトコルを使用する。

{
  "name": "@myapp/web",
  "dependencies": {
    "@myapp/ui": "workspace:*",
    "@myapp/utils": "workspace:^",
    "react": "^18.3.0"
  }
}

workspace:* は現在のバージョンをそのまま使用する。workspace:^ は semver の ^ 範囲で解決する。npm publish 時には pnpm が自動的に実際のバージョン番号に置換する。


7. —filter フラグ(特定パッケージへのコマンド実行)

フィルタリングの基本

--filter(短縮形 -F)フラグはモノレポで最も多用するオプションだ。特定のパッケージだけにコマンドを実行できる。

# パッケージ名で指定
pnpm --filter @myapp/web build
pnpm -F @myapp/web build

# glob パターン
pnpm --filter "@myapp/*" build

# ディレクトリで指定
pnpm --filter ./apps/web build

# すべてのパッケージ(-r / --recursive)
pnpm -r run build

依存関係を含むフィルタリング

# @myapp/web とその依存パッケージをすべてビルド
pnpm --filter @myapp/web... build

# @myapp/ui に依存するすべてのパッケージをビルド
pnpm --filter ...*@myapp/ui build

# @myapp/utils に依存するパッケージを、utils 自身も含めて
pnpm --filter @myapp/utils^... build

変更されたパッケージのみ実行

# main ブランチとの差分があるパッケージのみ
pnpm --filter "[main]" build

# 特定コミット以降に変更されたパッケージ
pnpm --filter "[HEAD~3]" test

# 変更されたパッケージとその依存元も含む
pnpm --filter "[main]..." build

並列実行

# 並列実行(--parallel は依存関係を無視して並行実行)
pnpm --parallel -r run dev

# 依存グラフを考慮した並列実行(Turborepo 使用時は turbo run dev)
pnpm -r run build

8. Catalog 機能(共有バージョン管理)

Catalog とは

pnpm v9.0 から追加された Catalog 機能は、モノレポ全体で依存パッケージのバージョンを一元管理する仕組みだ。複数パッケージで同じライブラリを使う際のバージョンの一貫性を保証する。

pnpm-workspace.yaml での Catalog 定義

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

# デフォルトカタログ(catalog: で参照)
catalog:
  react: ^18.3.0
  react-dom: ^18.3.0
  typescript: ^5.7.0
  vite: ^6.0.0
  vitest: ^2.1.0

# 名前付きカタログ(catalog:<name> で参照)
catalogs:
  react18:
    react: ^18.3.0
    react-dom: ^18.3.0

  react19:
    react: ^19.0.0
    react-dom: ^19.0.0

  testing:
    vitest: ^2.1.0
    '@testing-library/react': ^16.0.0
    '@testing-library/user-event': ^14.5.0

package.json での Catalog 参照

{
  "name": "@myapp/web",
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  },
  "devDependencies": {
    "typescript": "catalog:",
    "vite": "catalog:",
    "vitest": "catalog:testing"
  }
}

Catalog のメリット

バージョン番号を pnpm-workspace.yaml で一元管理することで、「あるパッケージは React 18.2、別のパッケージは React 18.3」という状況が構造的に防止される。バージョンを上げたいときは pnpm-workspace.yaml の 1 行を変更するだけでよい。

# Catalog のバージョン更新後に一括インストール
pnpm install

# catalog の依存関係を確認
pnpm list --filter @myapp/web

9. Patch 機能(パッケージパッチ適用)

パッチとは

サードパーティパッケージにバグがあるが公式修正を待てない場合、pnpm の patch 機能でローカルパッチを当てることができる。patch-package ライブラリの代替として機能する。

パッチの作成手順

# パッチ編集モードで起動(一時ディレクトリが作成される)
pnpm patch lodash@4.17.21

# 出力例:
# You can now edit the package at:
#   /tmp/pnpm-patch-lodash@4.17.21-xxx
# Once you're done, run:
#   pnpm patch-commit '/tmp/pnpm-patch-lodash@4.17.21-xxx'

# 一時ディレクトリでファイルを編集後、パッチをコミット
pnpm patch-commit '/tmp/pnpm-patch-lodash@4.17.21-xxx'

生成される設定

パッチコミット後、package.jsonpatches/ ディレクトリが自動更新される。

{
  "pnpm": {
    "patchedDependencies": {
      "lodash@4.17.21": "patches/lodash@4.17.21.patch"
    }
  }
}
# patches/lodash@4.17.21.patch
diff --git a/src/some-file.js b/src/some-file.js
index abc123..def456 100644
--- a/src/some-file.js
+++ b/src/some-file.js
@@ -10,7 +10,7 @@
-  return oldBehavior();
+  return fixedBehavior();

パッチの管理

# パッチ一覧の確認
pnpm patch-list

# パッチの削除(package.json から patchedDependencies を手動削除後)
pnpm install

パッチファイルは Git でコミットして管理する。チームメンバーが pnpm install を実行すると自動的にパッチが適用される。


10. npm/yarn 移行手順(lock file 変換・CI 設定変更)

事前準備

# pnpm のインストール
npm install -g pnpm
# または
corepack enable && corepack prepare pnpm@latest --activate

npm からの移行

# プロジェクトルートで実行
cd my-project

# 既存の lock ファイルから pnpm-lock.yaml を生成
pnpm import

# 古い lock ファイルと node_modules を削除
rm -rf node_modules package-lock.json

# pnpm でインストール
pnpm install

# 動作確認
pnpm run build
pnpm run test

yarn v1 からの移行

cd my-project

# yarn.lock から pnpm-lock.yaml を生成
pnpm import

# 古いファイルを削除
rm -rf node_modules yarn.lock .yarn

# pnpm でインストール
pnpm install

yarn Berry(v2+)からの移行

yarn Berry の PnP モードからの移行は少し手順が増える。

# .yarnrc.yml を確認してレジストリ設定を .npmrc に移行
cat .yarnrc.yml

# pnpm-workspace.yaml に workspaces を移行(yarn の workspaces フィールドから)
# package.json の workspaces:
#   - apps/*
#   - packages/*
# を pnpm-workspace.yaml に変換

# pnpm import は yarn Berry の lock ファイルもサポート
pnpm import

rm -rf node_modules .yarn .yarnrc.yml
pnpm install

package.json の scripts 更新

{
  "scripts": {
    "prepare": "pnpm run build",
    "prepublishOnly": "pnpm run test && pnpm run build"
  },
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=9.0.0"
  }
}

.gitignore の更新

# .gitignore

# pnpm
node_modules/
.pnpm-debug.log

# pnpm-lock.yaml はコミットする(削除しない)
# !pnpm-lock.yaml

# yarn/npm の残骸を除外
package-lock.json
yarn.lock
.yarn/

11. GitHub Actions 統合(キャッシュ・setup-node)

基本的な CI ワークフロー

# .github/workflows/ci.yml
name: CI

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

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20, 22]

    steps:
      - uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'

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

      - name: Type check
        run: pnpm typecheck

      - name: Lint
        run: pnpm lint

      - name: Test
        run: pnpm test

      - name: Build
        run: pnpm build

pnpm/action-setup の詳細設定

- name: Install pnpm
  uses: pnpm/action-setup@v4
  with:
    version: 9.15.0           # バージョンを固定
    run_install: false         # 後で明示的に install する場合

# または package.json の packageManager から自動検出
- name: Install pnpm
  uses: pnpm/action-setup@v4  # version 省略で packageManager を参照

キャッシュの最適化

- name: Get pnpm store directory
  shell: bash
  run: |
    echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
  uses: actions/cache@v4
  with:
    path: ${{ env.STORE_PATH }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-store-

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

モノレポでの CI 最適化(変更パッケージのみテスト)

jobs:
  changed:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.filter.outputs.changes }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Detect changed packages
        id: filter
        uses: dorny/paths-filter@v3
        with:
          filters: |
            web: apps/web/**
            api: apps/api/**
            ui: packages/ui/**

  test:
    needs: changed
    runs-on: ubuntu-latest
    if: needs.changed.outputs.packages != '[]'
    steps:
      - uses: actions/checkout@v4
      
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      
      - run: pnpm install --frozen-lockfile
      
      - name: Test changed packages
        run: pnpm --filter "[main]" test

Turborepo とのキャッシュ統合

- name: Build with Turbo (remote cache)
  run: pnpm turbo run build
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ vars.TURBO_TEAM }}

12. .npmrc 設定チートシート

完全な .npmrc サンプル

# .npmrc — pnpm 完全設定チートシート

# ===== 依存関係の解決 =====

# ピア依存関係を自動インストール(pnpm v8+)
auto-install-peers=true

# 厳格なピア依存チェック(不一致時にエラー)
strict-peer-dependencies=false

# 存在しないピア依存を警告のみにする
# peer-dependency-rules.ignoreMissing[]=@types/*

# ===== node_modules の構造 =====

# デフォルト: isolated(symlink ベース)
# hoisted: npm 互換のフラット構造
# pnp: Plug'n'Play
node-linker=isolated

# ルートに hoisting するパッケージのパターン
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=prettier
public-hoist-pattern[]=*jest*
public-hoist-pattern[]=*babel*

# フラット展開(互換性が必要な場合のみ)
# shamefully-hoist=true

# ===== ストア設定 =====

# グローバルストアのパス(デフォルト: ~/.pnpm-store)
# store-dir=/custom/path/pnpm-store

# パッケージの整合性検証
verify-store-integrity=true

# ===== レジストリ設定 =====

# デフォルトレジストリ
registry=https://registry.npmjs.org/

# スコープ別レジストリ
# @myorg:registry=https://npm.mycompany.com/
# //npm.mycompany.com/:_authToken=${NPM_TOKEN}

# ===== パフォーマンス =====

# 並列ダウンロード数
# fetch-concurrency=16

# ネットワークタイムアウト(ms)
# fetch-timeout=60000

# ===== セキュリティ =====

# ライフサイクルスクリプトの実行を許可するパッケージ
# onlyBuiltDependencies[]=esbuild
# onlyBuiltDependencies[]=sharp

# ライフサイクルスクリプトを一切実行しない
# ignore-scripts=true

# ===== その他 =====

# ロックファイルの自動更新を禁止(CI 推奨)
# frozen-lockfile=true

# 詳細ログ
# reporter=append-only

# カラー出力
color=true

よく使う設定のユースケース別まとめ

# ===== ケース1: 厳格なモノレポ設定 =====
strict-peer-dependencies=true
auto-install-peers=false
node-linker=isolated
verify-store-integrity=true

# ===== ケース2: npm 互換モード(移行期間中) =====
shamefully-hoist=true
strict-peer-dependencies=false
auto-install-peers=true

# ===== ケース3: CI/CD 最適化 =====
prefer-offline=true
verify-store-integrity=false
reporter=silent

# ===== ケース4: プライベートレジストリ =====
@company:registry=https://verdaccio.company.com/
//verdaccio.company.com/:_authToken=${VERDACCIO_TOKEN}
strict-peer-dependencies=false

13. pnpm + Turborepo/Nx モノレポ最適化

pnpm + Turborepo の組み合わせ

Turborepo は pnpm workspace を完全サポートしており、タスクの依存グラフ・インクリメンタルキャッシュ・リモートキャッシュを提供する。この組み合わせがモノレポの標準構成となっている。

初期セットアップ

# 新規モノレポを作成
pnpm dlx create-turbo@latest my-monorepo --package-manager pnpm

# 既存の pnpm workspace に Turborepo を追加
pnpm add -D turbo -w

turbo.json の設定

{
  "$schema": "https://turbo.build/schema.json",
  "ui": "tui",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": [".next/**", "dist/**", "!dist/**/*.map"]
    },
    "test": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tests/**", "*.config.ts"],
      "outputs": ["coverage/**"],
      "cache": true
    },
    "lint": {
      "dependsOn": [],
      "inputs": ["src/**", "*.config.*", ".eslintrc*"],
      "cache": true
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig*.json"],
      "cache": true
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  },
  "globalEnv": [
    "NODE_ENV",
    "DATABASE_URL"
  ]
}

Turborepo + pnpm の実行コマンド

# すべてビルド(依存グラフ順に実行)
pnpm turbo run build

# 並列実行(依存関係を無視)
pnpm turbo run lint typecheck --parallel

# 特定パッケージのみ
pnpm turbo run build --filter=@myapp/web

# キャッシュを無効化して再実行
pnpm turbo run build --force

# 実行グラフの可視化
pnpm turbo run build --graph

pnpm + Nx の組み合わせ

Nx は Turborepo と同様のキャッシュ機能に加えて、コードジェネレーターや依存グラフの可視化 UI を提供する。

Nx のセットアップ

# 新規 Nx ワークスペース(pnpm 指定)
pnpm dlx create-nx-workspace@latest my-nx-app --packageManager=pnpm

# 既存プロジェクトに Nx を追加
pnpm add -D nx @nx/js -w
pnpm dlx nx@latest init

nx.json の設定

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "defaultBase": "main",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)",
      "!{projectRoot}/src/test-setup.[jt]s"
    ],
    "sharedGlobals": []
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default"],
      "cache": true
    }
  },
  "plugins": [
    {
      "plugin": "@nx/vite/plugin",
      "options": {
        "buildTargetName": "build",
        "testTargetName": "test",
        "serveTargetName": "serve"
      }
    }
  ]
}

Nx の便利コマンド

# 影響を受けるプロジェクトのみビルド(CI 最適化)
pnpm nx affected -t build

# 依存グラフの可視化
pnpm nx graph

# コードジェネレーター(React コンポーネント生成例)
pnpm nx g @nx/react:component Button --project=ui

# Nx Cloud(リモートキャッシュ)の設定
pnpm nx connect

Turborepo vs Nx の選択基準

観点TurborepoNx
学習コスト低い中〜高い
設定ファイルturbo.json のみnx.json + プロジェクト設定
コードジェネレーターなし豊富(プラグイン多数)
依存グラフ UICLI のみブラウザ UI あり
リモートキャッシュVercel(有料)Nx Cloud(無料枠あり)
向いているケースシンプルな構成大規模・複雑な構成

小〜中規模のモノレポは Turborepo + pnpm の組み合わせが導入コストが低くておすすめだ。大規模で多数のチームが関わるプロジェクトは Nx + pnpm が管理機能の充実さで有利になる。


実践:完全なモノレポ設定例

ディレクトリ構造

my-monorepo/
  package.json
  pnpm-workspace.yaml
  .npmrc
  turbo.json
  tsconfig.base.json
  .eslintrc.base.js
  apps/
    web/
      package.json
      tsconfig.json
      vite.config.ts
    api/
      package.json
      tsconfig.json
  packages/
    ui/
      package.json
      tsconfig.json
      src/
        index.ts
    utils/
      package.json
      src/
        index.ts
    config/
      eslint/
        package.json
        index.js
      tsconfig/
        package.json
        base.json
        nextjs.json
        react-library.json

各設定ファイル

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/**'

catalog:
  react: ^18.3.0
  react-dom: ^18.3.0
  typescript: ^5.7.0
  vite: ^6.0.0
  vitest: ^2.1.0
  eslint: ^9.0.0
// packages/ui/package.json
{
  "name": "@myapp/ui",
  "version": "0.1.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "build": "tsc --build",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/"
  },
  "peerDependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  },
  "devDependencies": {
    "typescript": "catalog:",
    "@myapp/eslint-config": "workspace:*",
    "@myapp/tsconfig": "workspace:*"
  }
}
// apps/web/package.json
{
  "name": "@myapp/web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "test": "vitest run",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/"
  },
  "dependencies": {
    "@myapp/ui": "workspace:*",
    "@myapp/utils": "workspace:*",
    "react": "catalog:",
    "react-dom": "catalog:"
  },
  "devDependencies": {
    "typescript": "catalog:",
    "vite": "catalog:",
    "vitest": "catalog:",
    "@myapp/eslint-config": "workspace:*",
    "@myapp/tsconfig": "workspace:*"
  }
}

よくあるトラブルシューティング

エラー: ERR_PNPM_PEER_DEP_ISSUES

# ピア依存関係の問題を確認
pnpm install --reporter=append-only 2>&1 | grep -i peer

# 解決方法1: .npmrc でピア依存を自動インストール
# auto-install-peers=true

# 解決方法2: peerDependenciesMeta で optional に設定
# package.json
{
  "peerDependenciesMeta": {
    "react": { "optional": true }
  }
}

エラー: Cannot find module ‘xxx’

# 幽霊依存関係を使っている可能性がある
# package.json に明示的に追加する
pnpm add xxx

# または public-hoist-pattern で hoisting
# .npmrc に追記:
public-hoist-pattern[]=xxx

ロックファイルの競合解消

# pnpm-lock.yaml のコンフリクトを解消する場合
# コンフリクトを修正後に再インストール
pnpm install

# どうしても解消できない場合は再生成
rm pnpm-lock.yaml
pnpm install
# .npmrc(Windows 環境)
# 管理者権限なしで symlink を作成できない場合
node-linker=hoisted

package.json の検証に DevToolBox を活用

モノレポを運用していると、複数の package.json のバージョン整合性や依存関係の確認が煩雑になる。DevToolBox の JSON バリデーターを使えば、package.json の構文エラーや不正なフィールドを即座にブラウザ上で検証できる。

pnpm-workspace.yaml の設定を確認したり、Catalog で管理するバージョン文字列(^18.3.0 など)が正しい semver フォーマットかどうかをチェックする際に便利だ。インストール不要でブラウザだけで動作するため、チームメンバーへの共有も簡単にできる。


まとめ

pnpm は単なる「npm の高速版」ではなく、モノレポ開発に必要な機能をすべて備えたパッケージマネージャーだ。本記事で解説したポイントを改めて整理する。

機能ポイント
CAS + ハードリンクディスク使用量を大幅削減・高速インストール
Symlink node_modules幽霊依存関係を構造的に防止
Workspacepnpm-workspace.yaml でモノレポを一元管理
—filter特定パッケージへの精密なコマンド実行
Catalogバージョンの一元管理でドリフトを防止
Patchサードパーティの修正をローカルで適用
CI/CD--frozen-lockfile とストアキャッシュで高速ビルド
Turborepo/Nxタスクのインクリメンタルキャッシュでさらに高速化

npm から pnpm への移行は pnpm import で lock ファイルを変換するだけで始められる。まず既存プロジェクトの 1 つに適用して効果を体感してみることを強くおすすめする。

関連記事