この記事の要点
��� pnpm 10はデフォルトでlifecycle scriptsをブロックし、supply chain攻撃を防止
• Catalogs機能でモノレポの依存バージョンを一元管理、workspace protocolも強化
• hardlink + symlinkのアーキテクチャでnpm/yarnより高速・省容量
pnpm は Node.js エコシステムで利用される高速・省容量なパッケージマネージャーです。シンボリックリンクと content-addressable store を用いた独自のアーキテクチャにより、npm や yarn に対して高速なインストールとディスク効率を提供しています。pnpm 10 はセキュリティとモノレポ運用の改善にフォーカスしたメジャーリリースです。
pnpm 10の概要
アーキテクチャ
flowchart TB
subgraph Store["Global Store (~/.pnpm-store)"]
S1["content-addressable<br/>hash keyed blobs"]
end
subgraph Project["Project node_modules"]
P1[".pnpm/ virtual store"]
P2["symlinked packages"]
end
S1 -->|hardlink| P1
P1 -->|symlink| P2
subgraph Features["pnpm 10 Features"]
F1["secure lifecycle scripts"]
F2["improved lockfile v9"]
F3["workspace protocol"]
F4["catalogs"]
end
背景
Node.js エコシステムにおける supply chain 攻撃の増加により、postinstall をはじめとするライフサイクルスクリプトの自動実行が長年問題視されてきました。pnpm 10 はこの課題に真正面から取り組み、デフォルトで信頼されたパッケージ以外のスクリプト実行をブロックします。
主要な新機能
1. Lifecycle scripts がデフォルトで無効
pnpm 10 からは、インストール時に任意のパッケージの preinstall / install / postinstall スクリプトがデフォルトで実行されません。明示的に許可したパッケージだけが実行されます。
// package.json
{
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"sharp",
"@prisma/client"
]
}
}
許可されていないパッケージのスクリプトは警告とともにスキップされ、一覧を確認できます。
pnpm approve-builds
# インタラクティブにビルドを許可するパッケージを選択
2. Lockfile のフォーマット改善
pnpm-lock.yaml v9 は差分の少なさと決定論的な並び替えが強化され、レビュー時の diff ノイズが削減されました。
# pnpm-lock.yaml (v9)
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
react:
specifier: ^19.0.0
version: 19.0.0
react-dom:
specifier: ^19.0.0
version: 19.0.0(react@19.0.0)
3. Catalogs: 依存バージョンの一元管理
モノレポで依存バージョンを統一するための catalog 機能が安定化しました。
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
catalog:
react: ^19.0.0
react-dom: ^19.0.0
typescript: ^5.6.0
catalogs:
legacy:
react: ^18.3.0
react-dom: ^18.3.0
// apps/web/package.json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
}
}
// apps/legacy/package.json
{
"dependencies": {
"react": "catalog:legacy"
}
}
4. Workspace protocol の拡張
{
"dependencies": {
"@acme/ui": "workspace:*",
"@acme/utils": "workspace:^",
"@acme/types": "workspace:~"
}
}
workspace:*: 常に現在のワークスペース内のバージョンを利用workspace:^: publish 時に^x.y.zに変換workspace:~: publish 時に~x.y.zに変換
5. pnpm dlx の高速化
# npx 相当のワンショット実行
pnpm dlx create-vite@latest my-app --template react-ts
# キャッシュ済みの場合は即起動
pnpm dlx tsx script.ts
実践サンプル
モノレポのセットアップ
mkdir my-monorepo && cd my-monorepo
pnpm init
# workspace 定義
cat > pnpm-workspace.yaml <<'EOF'
packages:
- 'apps/*'
- 'packages/*'
catalog:
react: ^19.0.0
react-dom: ^19.0.0
typescript: ^5.6.0
vitest: ^3.0.0
EOF
mkdir -p apps/web packages/ui packages/utils
// apps/web/package.json
{
"name": "@acme/web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest"
},
"dependencies": {
"@acme/ui": "workspace:*",
"@acme/utils": "workspace:*",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:"
}
}
// packages/ui/package.json
{
"name": "@acme/ui",
"version": "0.0.1",
"main": "./src/index.ts",
"types": "./src/index.ts",
"peerDependencies": {
"react": "catalog:"
}
}
// packages/ui/src/index.ts
import { type ReactNode } from 'react';
export interface ButtonProps {
children: ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary';
}
export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
フィルタリングによるビルド
# 特定のパッケージとその依存関係だけをビルド
pnpm --filter @acme/web... build
# 変更されたパッケージだけをテスト
pnpm --filter '...[origin/main]' test
# 依存するパッケージを含めてテスト
pnpm --filter '@acme/ui...' test
CI 設定例
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Approve trusted builds
run: pnpm approve-builds --all
- name: Type check
run: pnpm -r run typecheck
- name: Test changed packages
run: pnpm --filter '...[origin/main]' test
Docker での利用
# Dockerfile
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@10 --activate
FROM base AS deps
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/web/package.json ./apps/web/
COPY packages/ui/package.json ./packages/ui/
RUN pnpm install --frozen-lockfile
FROM base AS build
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN pnpm --filter @acme/web... build
FROM base AS runner
WORKDIR /app
COPY /app/apps/web/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
比較表
パッケージマネージャー比較
| 項目 | pnpm 10 | npm 10 | yarn 4 (berry) |
|---|---|---|---|
| インストール方式 | hardlink + symlink | フラットコピー | フラット / PnP |
| ディスク効率 | 非常に高い | 低 | 中 |
| モノレポ対応 | ネイティブ | workspaces | workspaces |
| Lockfile | pnpm-lock.yaml | package-lock.json | yarn.lock |
| デフォルト postinstall | ブロック | 実行 | 実行 |
| Catalog | あり | なし | なし |
インストール速度の傾向
pnpm は store からの hardlink を多用するため、特に CI でのキャッシュヒット時に高速に動作します。実測値はプロジェクトや環境によって変わるため、自身のプロジェクトで測定することをおすすめします。
ベストプラクティス
1. onlyBuiltDependencies の明示
実践メモ: onlyBuiltDependenciesにはesbuild/sharp/prismaなどネイティブバイナリが必要なもののみを許可し、それ以外はブロックしたままにしましょう。
必要なネイティブ依存のみに限定することで、未知のスクリプト実行を防ぎます。
{
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "sharp"],
"neverBuiltDependencies": ["core-js"]
}
}
2. Peer dependencies の自動インストール
# .npmrc
auto-install-peers=true
strict-peer-dependencies=false
3. フィルタを活用した差分ビルド
# ルートpackage.jsonに定義
{
"scripts": {
"build": "pnpm -r --filter='...[origin/main]' build",
"test": "pnpm -r --filter='...[origin/main]' test"
}
}
4. Workspace 間の依存を workspace protocol に揃える
外部バージョンが紛れ込むと monorepo 内で重複が発生します。workspace:* を徹底しましょう。
5. --frozen-lockfile を CI で必ず使う
lockfile のずれによるビルド失敗を早期発見できます。
注意点
注意: pnpm 10ではデフォルトでpostinstallがブロックされるため、初回セットアップ時にapprove-buildsを忘れないようにしましょう。
- 既存の yarn / npm プロジェクトから移行する場合、
node_modulesの構造が異なるため、ツールによってはパッチが必要です。特に__dirnameベースで兄弟パッケージを直接参照するコードは動かないことがあります。 pnpm patchを用いた依存パッチはリポジトリ内のpatches/に保存されるため、Git 管理が必須です。shamefully-hoist=trueはあくまで互換性のための設定であり、長期的には避けるべきです。- pnpm 10 ではデフォルトで postinstall がブロックされるため、
approve-buildsを初回に忘れないようにします。 corepackを使ってバージョンを固定すると開発者間の齟齬を防げます。
導入手順
新規プロジェクト
corepack enable
corepack prepare pnpm@10 --activate
pnpm init
pnpm add react react-dom
pnpm add -D typescript vitest
既存プロジェクトからの移行
# npm / yarn から
rm -rf node_modules package-lock.json yarn.lock
corepack prepare pnpm@10 --activate
pnpm import # 既存 lockfile から pnpm-lock.yaml を生成
pnpm install
Corepack による固定
// package.json
{
"packageManager": "pnpm@10.0.0",
"engines": {
"node": ">=20",
"pnpm": ">=10"
}
}
パフォーマンス
pnpm は content-addressable store を使っているため、同じパッケージバージョンはマシン内で 1 度しかダウンロード・展開されません。これはモノレポや複数のプロジェクトを並行管理する開発者にとって、ディスク占有量を大きく削減します。またインストールは並列化されており、特に CI でのキャッシュを組み合わせるとインストール時間が安定します。
計測のヒント
pnpm install --reporter=ndjson | tee install.log
pnpm store status
du -sh ~/.local/share/pnpm/store
FAQ
Q: npm / yarn と混在できますか?
A: 混在は非推奨です。packageManager フィールドと only-allow を使って pnpm のみを強制しましょう。
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
Q: Node.js のバージョンはどれを使うべきですか? A: Node.js 20 以上の LTS を推奨します。
Q: postinstall を許可するパッケージをどう選べばよいですか?
A: ネイティブバイナリが必要なもの (esbuild, sharp, prisma, puppeteer など) のみを許可し、それ以外はブロックしたままにします。
Q: shamefully-hoist は使うべき?
A: 互換性のためにやむを得ない場合のみ使用してください。可能なら依存元のパッケージに修正を送る方が健全です。
Q: pnpm dlx と pnpm exec の違いは?
A: dlx はパッケージを一時的に取得して実行、exec はローカルの node_modules/.bin を実行します。
まとめ
pnpm 10 は「安全で速く、モノレポに強い」パッケージマネージャーとして成熟しました。特にデフォルトで lifecycle scripts をブロックする変更は、supply chain セキュリティ強化に大きく貢献します。モノレポにおいては catalogs と workspace protocol の組み合わせが非常に強力で、依存バージョンの一元管理と高速な差分ビルドを両立できます。
Node.js エコシステムで新しくプロジェクトを始める、あるいはモノレポの運用で npm/yarn に限界を感じている場合、pnpm 10 への移行を強く推奨します。