pnpm workspaceとは:モノレポを支えるパッケージ管理の仕組み

pnpm workspaceは、単一のGitリポジトリ内で複数のnpmパッケージを統合管理するための機能です。monorepo(モノレポ)と呼ばれるこのリポジトリ構成により、フロントエンドアプリ・バックエンドAPI・共有ライブラリといった複数プロジェクトを1つのリポジトリで扱えます。

pnpmのworkspace機能がnpmやyarnの同等機能と異なる点は、content-addressable storeによるディスク効率と厳密な依存解決にあります。npmやyarnではnode_modulesのホイスティングにより、package.jsonに記載していないパッケージも暗黙的に利用できてしまう問題(phantom dependencies)が起きます。pnpmはシンボリックリンクベースのnode_modules構造でこの問題を根本的に排除し、各パッケージが宣言した依存のみを参照できるようにしています。

Next.js・Vite・Nuxt・Prisma・Vue・Material UIなど、多くの著名OSSプロジェクトがpnpm workspaceを採用しています(出典: pnpm公式ドキュメント)。

セットアップ手順:ゼロからモノレポを構築する

pnpmのインストール

Node.js v16.13以降に同梱されているcorepackを使うのが推奨の方法です。

corepack enable
corepack prepare pnpm@latest --activate

package.jsonpackageManagerフィールドにバージョンを固定しておくと、チームメンバー全員が同じバージョンを使用できます。

{
  "packageManager": "pnpm@10.30.3"
}

プロジェクトの初期化

mkdir my-monorepo && cd my-monorepo
pnpm init

pnpm-workspace.yamlの作成

プロジェクトルートにpnpm-workspace.yamlを配置します。このファイルがworkspaceの定義そのものです。

packages:
  - "apps/*"
  - "packages/*"

apps/にはアプリケーション(Webフロントエンド、APIサーバなど)を、packages/には共有ライブラリや設定パッケージを配置するのが一般的なパターンです。

ディレクトリ構成の例

my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── .npmrc
├── apps/
│   ├── web/          # Next.jsなどのフロントエンド
│   └── api/          # NestJSなどのバックエンド
└── packages/
    ├── ui/           # 共有UIコンポーネント
    ├── utils/        # 共有ユーティリティ
    └── configs/      # ESLint・Prettier等の共通設定

他パッケージマネージャの使用を禁止する

チーム開発では、npm・yarnの誤用を防ぐためにpackage.jsonに以下を追加しておくと安全です。

{
  "scripts": {
    "preinstall": "npx -y only-allow pnpm"
  },
  "engines": {
    "npm": "use pnpm",
    "yarn": "use pnpm"
  }
}

pnpm-workspace.yamlの設定パターン

基本的なglobパターン

packages:
  - "apps/*"
  - "packages/*"
  - "tools/*"
  - "!**/test/**"    # testディレクトリを除外

除外パターンには!プレフィックスを使います。テスト用のフィクスチャや一時的なパッケージをワークスペースの対象外にできます。

Catalogs:依存バージョンの一元管理(pnpm v9.5+)

pnpm v9.5で導入されたCatalogs機能を使うと、ワークスペース全体で使う依存パッケージのバージョンをpnpm-workspace.yamlに一括定義できます。

packages:
  - "apps/*"
  - "packages/*"

catalog:
  react: ^19.0.0
  react-dom: ^19.0.0
  typescript: ^5.7.0
  vitest: ^3.0.0

catalogs:
  eslint9:
    eslint: ^9.0.0
    "@eslint/js": ^9.0.0

各パッケージのpackage.jsonではcatalog:プロトコルで参照します。

{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  },
  "devDependencies": {
    "eslint": "catalog:eslint9"
  }
}

この仕組みには3つの利点があります。

  • バージョン統一: zodやtypescriptなどワークスペース全体で使うパッケージのバージョン不一致を防げます
  • アップグレードの効率化: バージョン変更がpnpm-workspace.yamlの1箇所で済みます
  • マージコンフリクトの削減: package.jsonのバージョン記載がcatalog:固定になるため、複数人が同時に依存を追加してもコンフリクトしません

従来はsyncpackのようなツールでバージョン統一を静的解析していましたが、Catalogsはパッケージマネージャ自体にバージョン管理を組み込んだアプローチです。既存プロジェクトへの導入にはマイグレーション用のcodemodも用意されています。

pnpx codemod pnpm/catalog

workspaceプロトコル(workspace:)の仕組みと使い分け

ワークスペース内のパッケージを依存として参照するには、workspace:プロトコルを使います。package.jsondependenciesに以下のように記述します。

{
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:^"
  }
}

3つのバージョン指定モード

記法npm publish時の変換用途
workspace:*1.2.0(固定バージョン)モノレポ内部でのみ使うパッケージ
workspace:^^1.2.0(メジャー互換)npmに公開し、semverで管理するパッケージ
workspace:~~1.2.0(マイナー互換)パッチ単位で厳密に管理したいパッケージ

workspace:*はローカルのどのバージョンにもマッチするため、モノレポ内のアプリケーション間で共有パッケージを参照する際に最も広く使われます。npm registryへの公開時には自動的に具体的なバージョン番号に置換されます。

pnpm addでの指定方法

pnpm addコマンドのデフォルトではworkspace:^が設定されます。workspace:*を明示的に使いたい場合は、以下のようにクォートで囲んで指定します。

pnpm add "@myorg/ui@workspace:*" --filter web

saveWorkspaceProtocolの設定

.npmrcまたはpnpm-workspace.yamlsaveWorkspaceProtocolで、pnpm add実行時のデフォルト動作を変更できます。

挙動
rolling(デフォルト)workspace:*workspace:~workspace:^の範囲指定を使用
trueworkspace:1.0.0のように固定バージョン付きで保存
falseworkspaceプロトコルを付与せず通常のバージョン指定で保存

モノレポ内でパッケージ間のバージョンを同時にリリースする運用ならrollingが適しています。npm registryに個別公開するパッケージ群ではtrueを選ぶと、バージョン整合性が明示的に担保されます。

–filterで特定パッケージを操作する

--filter(短縮形: -F)はpnpm workspaceの操作で最も頻繁に使うオプションです。パッケージ名・ディレクトリ・依存関係グラフのいずれかでフィルタリングできます。

パッケージ名によるフィルタ

# webパッケージにreactをインストール
pnpm add react --filter web

# uiパッケージのビルドだけ実行
pnpm run build --filter @myorg/ui

# 複数パッケージの指定
pnpm run test --filter web --filter api

globパターンによるフィルタ

# apps/配下の全パッケージでdevサーバを起動
pnpm run dev --filter "./apps/*"

# packages/配下の全パッケージをビルド
pnpm run build --filter "./packages/*"

依存関係グラフによるフィルタ

# webとその依存パッケージ全てをビルド
pnpm run build --filter web...

# uiに依存している全パッケージのテストを実行
pnpm run test --filter ...@myorg/ui

...サフィックスは依存先を再帰的に含め、...プレフィックスは依存元を再帰的に含めます。

ルートpackage.jsonへのスクリプト登録

よく使うフィルタコマンドはルートのpackage.jsonにまとめておくと便利です。

{
  "scripts": {
    "dev:web": "pnpm run dev --filter web",
    "dev:api": "pnpm run dev --filter api",
    "build:all": "pnpm run build --filter './packages/*'",
    "test:all": "pnpm run test -r",
    "lint:all": "pnpm run lint -r"
  }
}

-r--recursive)はワークスペース内の全パッケージで順に実行するフラグです。

failIfNoMatchの有効化

--filterで指定したパッケージ名にマッチするものがない場合、デフォルトでは何も実行されず正常終了します。CI環境ではこの挙動が意図しないサイレント失敗を引き起こすため、pnpm-workspace.yamlfailIfNoMatch: trueに設定しておくのが安全です。

failIfNoMatch: true

packages:
  - "apps/*"
  - "packages/*"

.npmrcで開発体験を最適化する

.npmrcはpnpmの動作を細かく制御する設定ファイルです。プロジェクトルートに配置すると、ワークスペース全体に適用されます。モノレポ運用で推奨される設定を整理します。

# 厳密な依存解決を維持(デフォルト: true)
strict-peer-dependencies=false

# ワークスペースパッケージのリンク方式
link-workspace-packages=true

# workspace:プロトコルの保存形式
save-workspace-protocol=rolling

# バージョンプレフィックスの制御
save-prefix=

# 共有ロックファイル(デフォルト: true)
shared-workspace-lockfile=true

各設定の意図は以下のとおりです。

設定推奨値理由
strict-peer-dependenciesfalsepeer dependenciesの不一致でインストールが失敗する問題を回避
link-workspace-packagestrueワークスペース内パッケージをregistryではなくローカルリンクで解決
save-workspace-protocolrollingpnpm add時にworkspace:^形式で保存
save-prefix""(空文字)^~を付けず正確なバージョンで保存。Renovateと組み合わせて依存更新を管理
shared-workspace-lockfiletrue単一のpnpm-lock.yamlでワークスペース全体を管理

米国のpnpmユーザコミュニティでは、.npmrcを「モノレポのDX(Developer Experience)を決める設定ハブ」として重視する傾向があります(出典: adamcoster.com)。特にsave-workspace-protocolの3モード(false/true/rolling)の使い分けは、npm registryへのパッケージ公開戦略と直結するため、プロジェクト初期に方針を決めておくのが重要です。

TypeScriptプロジェクトのモノレポ設定

ルートtsconfig.jsonの設計

TypeScript Project Referencesを活用して、各パッケージの型チェックとビルドを独立かつ高速に実行できます。

// tsconfig.json(ルート)
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true
  },
  "references": [
    { "path": "./apps/web" },
    { "path": "./apps/api" },
    { "path": "./packages/ui" },
    { "path": "./packages/utils" }
  ]
}

各パッケージのtsconfig.jsonはルートを継承し、パッケージ固有のオプションだけ上書きします。

// packages/ui/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx"
  },
  "include": ["src"],
  "references": [
    { "path": "../utils" }
  ]
}

configsパッケージパターン

ESLint・Prettier・tsconfig.jsonなどの共通設定は、packages/configsのように専用パッケージに切り出すのが実務上のベストプラクティスです。ルートに設定ファイルを置いてシンボリックリンクや相対パスで参照する方法もありますが、以下の理由からパッケージ化が推奨されます。

  • Turborepoなどのタスクランナーが依存グラフを正しく認識し、キャッシュの破棄判定が正確になります
  • ディレクトリ構造が変わってもパッケージ名が変わらない限り設定の参照が壊れません
packages/configs/
├── package.json
├── eslint/
│   └── index.js
├── prettier/
│   └── index.cjs
└── tsconfig/
    ├── base.json
    ├── react.json
    └── node.json

各パッケージから設定を利用するには、workspace:*で依存に追加します。

{
  "devDependencies": {
    "@myorg/configs": "workspace:*"
  }
}
// packages/ui/.prettierrc.cjs
module.exports = require("@myorg/configs/prettier");

共有ライブラリの作成とビルド

Viteライブラリモードで共有パッケージをビルドする

ワークスペース内の共有ライブラリをビルドする方法として、Viteのライブラリモードが実用的です。TypeScriptの型定義ファイルもvite-plugin-dtsで自動生成できます。

// packages/ui/vite.config.ts
import { defineConfig } from "vite";
import { resolve } from "path";
import react from "@vitejs/plugin-react";
import dts from "vite-plugin-dts";

export default defineConfig({
  plugins: [react(), dts({ rollupTypes: true })],
  build: {
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      formats: ["es"],
      fileName: "index",
    },
    rollupOptions: {
      external: ["react", "react-dom"],
    },
  },
});

package.jsonでエントリポイントを設定します。

{
  "name": "@myorg/ui",
  "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"
  }
}

クロスパッケージのウォッチモード開発

共有パッケージを変更しながらアプリケーションを開発するには、ターミナルを2つ起動してウォッチモードを並行実行します。

# ターミナル1: 共有パッケージのウォッチビルド
pnpm run dev --filter @myorg/ui

# ターミナル2: アプリケーションの開発サーバ
pnpm run dev --filter web

共有パッケージのソースコードを変更するとViteが自動リビルドし、アプリ側のHMR(Hot Module Replacement)が変更を即座に反映します。

Turborepoと組み合わせてビルドを高速化する

Turborepoは、モノレポ内の複数パッケージのタスク実行を最適化するツールです。pnpm workspaceとの組み合わせが公式に推奨されています。

導入手順

pnpm add turbo -Dw

ルートにturbo.jsonを作成します。

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

Turborepoが解決する3つの課題

1. タスク実行順序の自動制御

"dependsOn": ["^build"]^プレフィックスは「依存パッケージのbuildを先に実行する」を意味します。packages/utilspackages/uiapps/webのように、依存グラフの順序でビルドが自動実行されます。

2. ローカルキャッシュによる差分ビルド

変更がないパッケージのビルド結果はキャッシュから取得されるため、2回目以降の実行が大幅に高速化されます。

3. 並列実行

独立したパッケージのタスクは並列実行されます。CIでの実行時間短縮に直結します。

パッケージ間でタスク名を統一する

Turborepoはタスク名ベースで実行を制御するため、各パッケージのnpm scriptsで同じタスク名を使うのが重要です。

タスクスクリプト名
ビルドbuild
開発サーバdev
静的解析lint
テストtest
型チェックtypecheck

turbo genによるパッケージジェネレータ

新しいパッケージを追加するたびにESLint・Prettier・tsconfig.jsonなどの設定ファイルを手動で作成するのは手間がかかります。Turborepoのturbo genコマンドでパッケージテンプレートを定義しておくと、ボイラープレートを自動生成できます。

turbo gen workspace --name @myorg/new-package

GitHub ActionsでのCI設定

pnpm workspaceプロジェクトをGitHub Actionsでビルドする際の設定例です。

name: CI

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: "pnpm"

      - run: pnpm install --frozen-lockfile

      - run: pnpm run build --filter './packages/*'

      - run: pnpm run lint -r

      - run: pnpm run test -r

pnpm/action-setup@v4はpnpmの公式GitHub Actionです。package.jsonpackageManagerフィールドからバージョンを自動検出してインストールします。--frozen-lockfileを付けるとpnpm-lock.yamlが変更される場合にエラーになるため、CI環境での意図しない依存変更を防げます。

npm・yarnからpnpm workspaceへ移行する手順

既存のnpmまたはyarnプロジェクトをpnpm workspaceに移行する際のチェックリストです。

移行前の準備

  1. node_modulesを全て削除
find . -name "node_modules" -type d -prune -exec rm -rf {} +
  1. pnpm-workspace.yamlを作成

npmのworkspaces(package.jsonworkspacesフィールド)に相当する定義をpnpm-workspace.yamlに記述します。

  1. workspace:プロトコルへの書き換え

モノレポ内の相互参照をworkspace:*に変更します。

  1. npm / npxコマンドの置換

スクリプトやドキュメント中のnpm runpnpm runnpxpnpm exec(またはpnpm dlx)に置換します。

  1. 依存のインストールとロックファイル生成
pnpm install

phantom dependenciesの検出と修正

pnpmの厳密な依存解決に移行すると、これまで暗黙的に利用できていたパッケージが解決できなくなるケースがあります。これがphantom dependencies問題です。

検出方法は2段階で行うのが効果的です。

1. TypeScriptの型チェックによる検出

pnpm -r exec tsc --noEmit --no-bail

--no-bailを付けると最初のエラーで止まらず全パッケージのエラーを網羅的に収集できます。

2. knipによるCommonJS requireの検出

TypeScriptのtscではESM importのみチェックされ、CommonJSのrequire()で読み込んでいるパッケージは検出できません。knipを使うとunlisted dependenciesを静的解析で検出できます。

pnpm add knip -Dw
pnpm knip --include unlisted

preserveSymlinksの無効化

pnpmはシンボリックリンクでnode_modulesを構成するため、tsconfig.jsonpreserveSymlinksが有効だとモジュール解決が正しく動作しない場合があります。

// tsconfig.json
{
  "compilerOptions": {
    "preserveSymlinks": false
  }
}

Viteを使っている場合はvite.config.tsでも設定が必要です。

export default defineConfig({
  resolve: {
    preserveSymlinks: false,
  },
});

トラブルシューティング

ワークスペース内のパッケージが見つからない

pnpm installを実行しても内部パッケージが解決できない場合は、以下を確認します。

  • pnpm-workspace.yamlのglobパターンが対象パッケージのディレクトリに一致しているか
  • 対象パッケージにpackage.jsonが存在し、nameフィールドが正しく設定されているか
  • .npmrclink-workspace-packages=trueが設定されているか(workspace:プロトコル未使用の場合)

循環依存の警告

パッケージA → パッケージB → パッケージAの依存ループが検出されると警告が表示されます。設計上やむを得ない場合はignoreWorkspaceCycles: trueで抑制できますが、可能であれば共通部分を別パッケージに切り出して循環を解消するのが望ましい対応です。

CI環境ではdisallowWorkspaceCycles: trueを設定すると循環依存が検出された時点でインストールを失敗させられます。

knipで未使用・未宣言の依存を検出する

knipは、各パッケージで使用されていない依存(unused)と、コード中で使っているのにpackage.jsonに未宣言の依存(unlisted)を検出するツールです。ESM importだけでなくCommonJSのrequire()やTypeScriptのtype importにも対応しており、モノレポの依存管理に適しています。

pnpm add knip -Dw
pnpm knip

移行後の棚卸しに加え、定期的なメンテナンスにも有効です。

syncpackでバージョン不整合を検出する

ワークスペース内で同じパッケージの異なるバージョンが混在すると、バンドルサイズの増大や挙動の不一致が発生します。syncpackをCIに組み込むと静的にチェックできます。

pnpm add syncpack -Dw
pnpm exec syncpack list-mismatches

Catalogsを導入済みの場合はsyncpackの役割がCatalogs側に集約されるため、併用の必要性は減ります。

npm・yarn・pnpmのワークスペース機能比較

項目npm workspacesyarn workspacespnpm workspace
定義ファイルpackage.json内package.json内pnpm-workspace.yaml
依存解決ホイスティングホイスティングシンボリックリンク(厳密)
phantom dependencies発生する発生する発生しない
ディスク効率低い中程度高い(content-addressable store)
フィルタリング--workspace--filter(yarn v2+)--filter(高機能)
バージョン一元管理なしconstraints(yarn v2+)Catalogs(v9.5+)
タスクランナー連携手動yarn v2+ pluginTurborepo公式サポート

まとめ

pnpm workspaceは、モノレポ構成に必要な機能を高い完成度で備えたパッケージ管理の仕組みです。厳密な依存解決によるphantom dependenciesの排除、content-addressable storeによるディスク効率、Catalogs機能によるバージョン一元管理、そしてTurborepoとの緊密な連携により、中〜大規模プロジェクトの開発生産性を向上させます。

構築にあたっては、初期セットアップ時に.npmrcの設定方針(特にsaveWorkspaceProtocolの3モード)とConfigsパッケージパターンの採用を決めておくと、後からの手戻りが少なくなります。既存プロジェクトからの移行ではphantom dependenciesの検出がポイントとなるため、tsc --no-bailとknipの2段階チェックで網羅的に問題を洗い出すのが効率的です。