最終更新:
pnpm Workspace完全ガイド - 効率的なモノレポ管理とパッケージリンク
はじめに
pnpm Workspaceは、単一リポジトリ内で複数のパッケージを効率的に管理するための機能です。2026年現在、npmやYarnと比較して最も高速かつディスク効率的なモノレポソリューションとして、Vercel、Microsoft、ByteDanceなど大規模プロジェクトで採用されています。
pnpm Workspaceとは
従来の複数リポジトリ:
app1/ app2/ shared/
├─ node_modules/ ├─ node_modules/ ├─ node_modules/
├─ package.json ├─ package.json ├─ package.json
pnpm Workspace(モノレポ):
monorepo/
├─ node_modules/ ← 共有依存関係(シンボリックリンク)
├─ pnpm-workspace.yaml
├─ package.json
├─ apps/
│ ├─ web/ ← Next.js
│ └─ mobile/ ← React Native
├─ packages/
│ ├─ ui/ ← UIコンポーネント
│ ├─ utils/ ← ユーティリティ
│ └─ config/ ← 共通設定
└─ services/
├─ api/ ← バックエンド
└─ worker/ ← ワーカー
メリット:
✅ 依存関係の一元管理
✅ コード共有が容易
✅ 一貫したバージョン管理
✅ ディスク容量削減(60-90%)
✅ インストール高速化(2-3倍)
pnpmの特徴
npm vs Yarn vs pnpm(100個のパッケージ):
インストール速度:
npm: 45秒
Yarn: 38秒
pnpm: 18秒 ← 最速
ディスク使用量:
npm: 500MB
Yarn: 480MB
pnpm: 250MB ← 最小
pnpmの仕組み:
- Content-addressable storage(グローバルストア)
- Hard linkでディスク節約
- Strict node_modules構造でゴースト依存回避
セットアップ
インストール
# pnpmインストール
npm install -g pnpm
# バージョン確認
pnpm --version
# シェル補完設定(任意)
pnpm completion bash >> ~/.bashrc
プロジェクト初期化
# プロジェクト作成
mkdir my-monorepo
cd my-monorepo
# 初期化
pnpm init
# Workspace設定ファイル作成
touch pnpm-workspace.yaml
pnpm-workspace.yaml
# pnpm-workspace.yaml
packages:
# すべてのパッケージを含む
- 'apps/*'
- 'packages/*'
- 'services/*'
# 除外パターン
- '!**/test/**'
ルートpackage.json
{
"name": "my-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm --parallel --filter \"./apps/*\" dev",
"build": "pnpm --filter \"./packages/*\" build && pnpm --filter \"./apps/*\" build",
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"clean": "pnpm -r clean && rm -rf node_modules"
},
"devDependencies": {
"typescript": "^5.3.0",
"prettier": "^3.2.0",
"eslint": "^8.56.0"
}
}
パッケージ構成
基本的なディレクトリ構造
# ディレクトリ作成
mkdir -p apps/web apps/mobile
mkdir -p packages/ui packages/utils packages/config
mkdir -p services/api
パッケージ1: UI Components
cd packages/ui
pnpm init
{
"name": "@myorg/ui",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./button": {
"import": "./dist/button.js",
"types": "./dist/button.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsup src/index.ts --format esm --dts --watch"
},
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"tsup": "^8.0.0",
"typescript": "^5.3.0"
}
}
// packages/ui/src/button.tsx
import React from 'react';
export interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ children, onClick }: ButtonProps) {
return <button onClick={onClick}>{children}</button>;
}
// packages/ui/src/index.ts
export { Button } from './button';
export type { ButtonProps } from './button';
パッケージ2: Utils
{
"name": "@myorg/utils",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"test": "vitest"
},
"devDependencies": {
"tsup": "^8.0.0",
"vitest": "^1.2.0"
}
}
// packages/utils/src/format.ts
export function formatCurrency(amount: number, currency: string = 'JPY'): string {
return new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency,
}).format(amount);
}
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('ja-JP').format(date);
}
アプリ: Next.js Web App
{
"name": "@myorg/web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:*",
"next": "^14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
// apps/web/app/page.tsx
import { Button } from '@myorg/ui';
import { formatCurrency } from '@myorg/utils';
export default function Home() {
return (
<main>
<h1>My App</h1>
<Button onClick={() => alert('Clicked!')}>
Click me
</Button>
<p>Price: {formatCurrency(1000)}</p>
</main>
);
}
依存関係管理
workspace: プロトコル
{
"dependencies": {
"@myorg/ui": "workspace:*", // 最新バージョン
"@myorg/utils": "workspace:^", // SemVerと互換
"@myorg/config": "workspace:~1.0.0" // 範囲指定
}
}
依存関係の追加
# ルートに追加(すべてのパッケージで共有)
pnpm add -w typescript
# 特定のパッケージに追加
pnpm --filter @myorg/web add react
# 開発依存関係
pnpm --filter @myorg/ui add -D @types/react
# Workspace内パッケージをリンク
pnpm --filter @myorg/web add @myorg/ui --workspace
依存関係の削除
# 特定のパッケージから削除
pnpm --filter @myorg/web remove react
# 全パッケージから削除
pnpm -r remove lodash
バージョンアップデート
# 対話的アップデート
pnpm -r update --interactive
# 最新版に更新
pnpm -r update --latest
# 特定パッケージのみ
pnpm --filter @myorg/web update next@latest
ビルドパイプライン
依存関係順のビルド
{
"scripts": {
"build": "pnpm -r --workspace-concurrency 4 build"
}
}
# 依存関係を考慮して並列実行
pnpm -r build
# 実行順序:
# 1. packages/config(依存なし)
# 2. packages/utils(依存なし)
# 3. packages/ui(依存なし)
# 4. apps/web(ui, utils に依存)← 3が終わってから
フィルタリング
# apps配下のみビルド
pnpm --filter "./apps/*" build
# 特定パッケージとその依存先
pnpm --filter @myorg/web... build
# 特定パッケージとその依存元
pnpm --filter ...@myorg/ui build
# 変更されたパッケージのみ(Git)
pnpm --filter "[HEAD^]" build
Turbopack統合
pnpm add -w turbo
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
{
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test"
}
}
スクリプト実行
再帰実行
# すべてのパッケージで実行
pnpm -r test
# 並列実行
pnpm -r --parallel dev
# トポロジカルソート順(依存関係順)
pnpm -r --sort build
エラーハンドリング
# エラーがあっても続行
pnpm -r --no-bail test
# 失敗したパッケージのみ再実行
pnpm -r --resume-from @myorg/web test
ログ管理
# ログを集約
pnpm -r --aggregate-output test
# ストリーム出力
pnpm -r --stream dev
TypeScript設定
ルートtsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"paths": {
"@myorg/ui": ["./packages/ui/src"],
"@myorg/utils": ["./packages/utils/src"]
}
}
}
パッケージごとのtsconfig.json
// packages/ui/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Project References
// apps/web/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true
},
"references": [
{ "path": "../../packages/ui" },
{ "path": "../../packages/utils" }
],
"include": ["**/*"]
}
# Project References ビルド
pnpm tsc --build --verbose
CI/CD統合
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm -r lint
- name: Type check
run: pnpm -r type-check
- name: Test
run: pnpm -r test
- name: Build
run: pnpm -r build
# 変更されたパッケージのみデプロイ
- name: Deploy changed apps
run: |
pnpm --filter "[HEAD^]" deploy
Docker統合
# Dockerfile
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@8.15.0 --activate
FROM base AS deps
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/web/package.json ./apps/web/
COPY packages/*/package.json ./packages/
RUN pnpm install --frozen-lockfile --prod
FROM base AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN pnpm -r build
RUN pnpm --filter @myorg/web --prod deploy /prod/web
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /prod/web ./
EXPOSE 3000
CMD ["pnpm", "start"]
デプロイコマンド
# 本番ビルド + プルーン
pnpm --filter @myorg/web deploy /dist/web
# 内容:
# 1. パッケージをビルド
# 2. 必要な依存のみコピー
# 3. devDependencies削除
実践的なパターン
パターン1: Shared Configuration
// packages/config/eslint.js
module.exports = {
extends: ['next/core-web-vitals', 'prettier'],
rules: {
'@typescript-eslint/no-unused-vars': 'error',
},
};
// apps/web/.eslintrc.js
module.exports = {
extends: ['@myorg/config/eslint'],
};
パターン2: Code Generation
// packages/api-client/package.json
{
"scripts": {
"generate": "openapi-typescript ../api/openapi.yaml -o ./src/types.ts"
}
}
// ルート package.json
{
"scripts": {
"codegen": "pnpm --filter @myorg/api-client generate"
}
}
パターン3: Version Management
# Changesets導入
pnpm add -Dw @changesets/cli
pnpm changeset init
# 変更セット作成
pnpm changeset
# バージョンバンプ
pnpm changeset version
# パブリッシュ
pnpm changeset publish
# .changeset/config.json
{
"changelog": "@changesets/changelog-github",
"commit": false,
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@myorg/web"]
}
トラブルシューティング
エラー1: Hoisting Issues
エラー: Cannot find module 'package'
解決策: .npmrc設定
# .npmrc
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
shamefully-hoist=false
エラー2: 循環依存
エラー: Cyclic dependency detected
解決策: 依存グラフ確認
# 依存関係可視化
pnpm list --depth 99 --json | jq '.dependencies'
# 循環依存チェック
pnpm -r exec madge --circular --extensions ts,tsx src/
エラー3: Phantom Dependencies
エラー: Module not found (開発時は動くが本番で失敗)
解決策: package.jsonに明示的追加
# 依存関係チェック
pnpm -r exec depcheck
まとめ
pnpm Workspaceの強み
- 高速: npm/Yarnの2-3倍速いインストール
- 省スペース: 60-90%のディスク節約
- 厳密性: Phantom dependency回避
- スケーラビリティ: 大規模モノレポに対応
ベストプラクティス
- workspace:プロトコルでパッケージリンク
- Turboでビルドキャッシュ
- Changesetsでバージョン管理
- Project Referencesで型チェック高速化
- CIでfrozen-lockfile使用
いつ使うべきか
最適な用途:
- マルチパッケージプロジェクト
- モノレポ構成
- コード共有が多いプロジェクト
- 一貫したバージョン管理が必要
不向きな用途:
- 単一パッケージプロジェクト
- npm/Yarn固有の機能に依存
次のステップ
- pnpm公式: https://pnpm.io/
- Turborepo: https://turbo.build/
- Changesets: https://github.com/changesets/changesets
- Nx: https://nx.dev/
pnpm Workspaceで、効率的なモノレポ開発を始めましょう。