- 技術 💻
pnpmの使い方|npmとのコマンド対応表とworkspace
pnpm(ピーエヌピーエム)は、npm と同じ package.json を使うパッケージマネージャーです。 パッケージの中身を1か所の「ストア」に1つだけ置き、各プロジェクトの node_modules からはハードリンクとシンボリックリンクで参照します。そのため、プロジェクトが増えてもディスクがほとんど増えません。
| やりたいこと | npm | pnpm |
|---|---|---|
| package.json どおりに入れる | npm install |
pnpm install(pnpm i) |
| パッケージを追加 | npm i express |
pnpm add express |
| 開発用に追加 | npm i -D vitest |
pnpm add -D vitest |
| グローバルに追加 | npm i -g pkg |
pnpm add -g pkg |
| 削除 | npm uninstall express |
pnpm remove express(pnpm rm) |
| 更新 | npm update |
pnpm update(pnpm up) |
| スクリプト実行 | npm run dev |
pnpm dev(run を省ける) |
| 入れたコマンドを実行 | npx tsc |
pnpm exec tsc(pnpm tsc も可) |
| 入れずに一度だけ実行 | npx cowsay hi |
pnpm dlx cowsay hi(pnx cowsay hi) |
| CI でロックどおりに入れる | npm ci |
pnpm install --frozen-lockfile |
| 一覧 | npm ls |
pnpm ls(pnpm list) |
| なぜ入っているか | npm explain debug |
pnpm why debug |
| 新しい版があるか確認 | npm outdated |
pnpm outdated |
| package-lock.json から移行 | ― | pnpm import |
確認環境:pnpm 12.11.2(npm の latest)・npm 10.9.7・Node.js 22.22.2(Linux)。同じ package.json(express・zod・typescript・vite・vitest)を npm と pnpm で入れて比べました。サイズ・秒数・エラー文は、この環境で得たものです。設定の既定値は pnpm 公式ドキュメントで確かめました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「pnpmの使い方|npmとのコマンド対応表とworkspace」
入れ方
npm i -g pnpm # 2026年10月13日時点の latest は 12.11.2
pnpm --version
公式ドキュメントでは、Node.js に付いている Corepack を使う方法や、スタンドアロンのスクリプトで入れる方法も紹介されています。npm i pnpm で入れると、pnpm・pn・pnpx・pnx の4つのコマンドができました。
pnpm 12 は Rust で書き直された版です(公式ブログ「pnpm 12.0」)。版によって既定の動きが違うので、チームでは package.json の packageManager などで版をそろえておくと混乱しません。
node_modules の構造が違う
同じ package.json を入れたあとの node_modules の一番上です。
| npm | pnpm | |
|---|---|---|
| 一番上にあるもの | 99 個(依存の依存も全部並ぶ) | package.json に書いた5つ + .bin・.pnpm |
| 実体の置き場所 | node_modules/express/ など |
node_modules/.pnpm/express@5.3.0/node_modules/express/ |
| 一番上の express | フォルダ | .pnpm/... へのシンボリックリンク |
$ ls -la node_modules # pnpm
drwxr-xr-x .bin
drwxr-xr-x .pnpm
lrwxrwxrwx express -> .pnpm/express@5.3.0/node_modules/express
lrwxrwxrwx typescript -> .pnpm/typescript@7.0.2/node_modules/typescript
lrwxrwxrwx vite -> .pnpm/vite@8.3.4/node_modules/vite
lrwxrwxrwx vitest -> .pnpm/vitest@5.0.3_vite@8.3.4/node_modules/vitest
lrwxrwxrwx zod -> .pnpm/zod@4.6.6/node_modules/zod
.pnpm の中のファイルは、ストアのファイルへのハードリンクです(同じ中身を2つの名前で指す仕組み)。express/package.json のリンク数は 2(ストアと node_modules)で、npm 側は 1 でした。
| 用語 | 意味 |
|---|---|
| ストア(content-addressable store) | パッケージのファイルを中身のハッシュで1つだけ置く場所。pnpm store path で確かめられる |
| 仮想ストア(virtual store) | node_modules/.pnpm。版ごとのフォルダが並ぶ |
| シンボリックリンク | node_modules/express → .pnpm/... の参照 |
実ディスク使用量を比べる
du はハードリンクを1回の実行の中で1回だけ数えるので、ストアと node_modules を一緒に測ると実際の使用量になります。
| 状態 | npm | pnpm |
|---|---|---|
| 1プロジェクト目の node_modules | 81 MB | 80 MB(ストアと同じファイル) |
| ストア | ― | 80 MB |
| 1プロジェクト目の実使用量 | 81 MB | 81 MB(ストア+node_modules) |
| 同じ依存の2プロジェクト目を入れた後の合計 | 161 MB | 81 MB |
| 2プロジェクト目のインストール時間 | 11 秒 | 65 ms(Done in 65ms) |
1プロジェクトだけなら差はほぼありません。 2つ目からは、npm はもう1つ全部をコピーし、pnpm はストアのファイルにリンクを張るだけでした。2つ目の pnpm の出力は reused 103, downloaded 0 です。時間は1回ずつ測った値で、ネットワークやキャッシュの状態で変わります。
Progress: resolved 103, reused 103, downloaded 0, added 103, done
Done in 65ms using pnpm v12.11.2
ハードリンクは同じファイルシステムの中でしか張れません。 公式ブログでは、pnpm 12 はハードリンクを張れる場所が見つからないとき、ストアを <プロジェクト>/node_modules/.pnpm-store に作るとされています。この場合は、プロジェクトをまたいだ共有になりません。
phantom dependency(幽霊依存)でエラーになる例
phantom dependency は、package.json に書いていないのに、依存の依存として入っているパッケージを使えてしまうことです。 express は内部で debug を使っています。
// index.js(package.json には express しか書いていない)
import express from 'express'
import createDebug from 'debug'
const log = createDebug('app')
console.log('ok', typeof express, typeof log)
| 結果 | |
|---|---|
| npm で入れた | ok function function(動いてしまう) |
| pnpm で入れた | Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'debug' imported from .../index.js |
pnpm・CommonJS の require('debug') |
Error: Cannot find module 'debug' |
npm は依存の依存も node_modules の一番上に並べるので、debug が見つかります。pnpm の一番上には package.json に書いたものしか無いので、見つかりません。npm で動いていても、express が将来 debug を使わなくなると壊れるコードです。 pnpm はそれを早い段階でエラーにします。
pnpm why で、どこから入っているかが分かりました。
$ pnpm why debug
debug@4.4.3
├─┬ body-parser@2.3.0
│ └─┬ express@5.3.0
│ └── demo@1.0.0 (dependencies)
├── express@5.3.0 [deduped]
...
直し方は、使うパッケージを自分の package.json に書くことです。 pnpm add debug のあと、ok function function と動きました。
npm から移ったときに Cannot find module が出たら、この原因を最初に疑います。原因の見分け方は、関連記事「Node.js Cannot find moduleの原因と直し方」で扱っています。ツール側の事情で直せないときは、pnpm-workspace.yaml の publicHoistPattern や nodeLinker: hoisted(npm と同じ平らな形)で回避する方法が公式ドキュメントにあります。
npm から移ったときに戸惑う既定の動き
① 公開から24時間以内の版は入らない
npm と pnpm で、入った版が違いました。
| パッケージ | npm で入った版 | pnpm で入った版 | 新しい方の公開日時(UTC) |
|---|---|---|---|
| zod | 4.6.7 | 4.6.6 | 2026-10-12 16:29 |
| vitest | 5.0.4 | 5.0.3 | 2026-10-12 09:47 |
公式ドキュメントでは、minimumReleaseAge(公開から何分たった版を入れるか)の既定値は v11 から 1440(24時間)、それより前は 0 です。依存の依存にも効きます。乗っ取られたパッケージの悪い版が出ても、すぐには入らないようにする設定です。
pnpm import で package-lock.json から移したときも、zod は 4.6.7 ではなく 4.6.6 になりました。minimumReleaseAge: 0 にして import し直すと 4.6.7 になりました。
# pnpm-workspace.yaml
minimumReleaseAge: 0 # すぐ入れたいとき(分)
minimumReleaseAgeExclude:
- "@my-company/*" # 社内パッケージは除外する、など
② 依存のビルドスクリプトは承認制
esbuild のように postinstall スクリプトを持つパッケージを追加すると、スクリプトは実行されず、エラーになりました。
$ pnpm add -D esbuild
devDependencies:
+ esbuild 0.28.2
Error: ERR_PNPM_IGNORED_BUILDS
× adding a new package
╰─▶ Ignored build scripts: esbuild@0.28.2
help: Run "pnpm approve-builds" to pick which dependencies should be allowed
to run scripts.
そのあとの pnpm install も終了コード 1 でした。pnpm approve-builds esbuild で許可すると、postinstall が実行され、pnpm-workspace.yaml に次の行が足されました。
allowBuilds:
esbuild: true
| 設定 | 既定値(公式ドキュメント) | 意味 |
|---|---|---|
allowBuilds |
― | パッケージごとにスクリプトを許可(true)・拒否(false)。書いていないものは実行しない |
strictDepBuilds |
true |
承認していないビルドがあると、終了コードを 0 以外にする |
dangerouslyAllowAllBuilds |
false |
全部のスクリプトを実行する |
pnpm approve-builds を引数無しで実行すると、待っているパッケージを選ぶ画面になります。CI で止まらないよう、allowBuilds をコミットしておきます。
③ 知らないオプションはエラーになる
$ pnpm install --foo
error: unexpected argument '--foo' found
tip: a similar argument exists: '--frozen-lockfile'
④ CI ではロックファイルと合わないと止まる
CI 環境(CI=true)では、package.json を変えてロックファイルを更新していないと止まりました。
Error: ERR_PNPM_OUTDATED_LOCKFILE
× installing dependencies
╰─▶ Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up
to date with package.json.
⑤ 設定は pnpm-workspace.yaml に書く
pnpm config set store-dir ... --location project を実行すると、.npmrc ではなく pnpm-workspace.yaml に storeDir: が書かれました。公式ドキュメントでも、.npmrc から読むのは認証とレジストリの設定だけで、node_modules の形に関わる設定は pnpm-workspace.yaml にしか書けないとされています。
| 設定(pnpm-workspace.yaml) | 既定値(公式ドキュメント) |
|---|---|
nodeLinker |
isolated(.pnpm の仮想ストア+シンボリックリンク) |
hoist / hoistPattern |
true / ['*'](.pnpm/node_modules にだけ引き上げる) |
publicHoistPattern |
[](一番上の node_modules には引き上げない) |
shamefullyHoist |
false |
minimumReleaseAge |
1440(v11 から) |
strictDepBuilds |
true |
workspace(モノレポ)と --filter
workspace は、1つのリポジトリに複数のパッケージを置き、互いに参照しながらまとめて管理する仕組みです。
mono/
├── package.json
├── pnpm-workspace.yaml
├── apps/
│ ├── api/ (@demo/api … @demo/utils を使う)
│ └── web/ (@demo/web)
└── packages/
└── utils/ (@demo/utils)
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
// package.json(ルート)
{ "name": "mono", "private": true,
"scripts": { "build": "pnpm -r build", "test": "pnpm -r test" } }
// packages/utils/index.js(package.json の name は @demo/utils)
export const slugify = (s) => s.trim().toLowerCase().replace(/\s+/g, '-')
// apps/api/index.js
import { slugify } from '@demo/utils'
console.log(slugify('Pnpm Workspace Demo'))
workspace のパッケージを依存に追加する
package.json に書く前に動かすと、phantom dependency と同じエラーになりました。 workspace の中にあっても、書いていないパッケージは見えません。
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@demo/utils' imported from /.../apps/api/index.js
pnpm --filter @demo/api add '@demo/utils@workspace:*'
* をシェル(zsh)が展開しようとして no matches found になったので、引用符で囲んでいます。追加すると package.json に "@demo/utils": "workspace:*" が入り、apps/api/node_modules/@demo/utils が ../../../../packages/utils へのシンボリックリンクになりました。
$ pnpm --filter @demo/api start
$ node index.js
pnpm-workspace-demo
workspace:* は、公開(pnpm pack・pnpm publish)するときに実際の版に置き換わります。 pnpm pack で作った tgz の中の package.json は "@demo/utils": "1.0.0" でした。
-r と --filter
pnpm -r build は、依存の順番どおり(utils が api より先)に実行しました。
apps/web build: build web
packages/utils build: build utils
apps/api build: build api
| 書き方 | 対象 | この例で動いたもの |
|---|---|---|
pnpm -r build |
全パッケージ | web・utils・api |
pnpm --filter @demo/api build |
api だけ | api |
pnpm --filter "@demo/api..." build |
api とその依存 | utils・api |
pnpm --filter "...@demo/utils" build |
utils とそれを使うもの | utils・api |
pnpm --filter "./apps/*" build |
フォルダで指定 | api・web |
pnpm --filter "...[origin/main]" build |
変更があったものと、それを使うもの(公式ドキュメント) | ― |
pnpm --filter "!@demo/web" -r build |
web を除く(zsh では \!) |
― |
ルートで pnpm add ms とすると止まりました。 ルートに入れるつもりなら -w を付けます。
Error: ERR_PNPM_ADDING_TO_ROOT
× Running this command will add the dependency to the workspace root, which
│ might not be what you want - if you really meant it, make it explicit by
│ running this command again with the -w flag (or --workspace-root).
npm から移る手順
pnpm import # package-lock.json から pnpm-lock.yaml を作る
rm -rf node_modules package-lock.json
pnpm install
pnpm test # phantom dependency が無いか確かめる
pnpm importでロックファイルを変換(このときminimumReleaseAgeが効く点に注意)Cannot find moduleが出たら、そのパッケージをpnpm addするERR_PNPM_IGNORED_BUILDSが出たら、pnpm approve-buildsで許可する- CI の
npm ciをpnpm install --frozen-lockfileに変える
npm 側のコマンドは、関連記事「npm コマンド 一覧|package.json 書き方と実行結果・npm 12対応」で扱っています。
よくある質問
Q. pnpm run は省略できますか。
A. できます。pnpm dev は pnpm run dev と同じです。スクリプトに無い名前は、入っているコマンドとして実行され(pnpm tsc --version は Version 7.0.2)、どちらにも無いと ERR_PNPM_NO_SCRIPT と Missing script: nosuch で止まりました。
Q. npx の代わりは?
A. 入っているコマンドは pnpm exec、入れずに一度だけ使うなら pnpm dlx です。pnpm 12.11.2 の pnx --help は pnpm dlx と同じ使い方を表示し、pnx cowsay@1.6.0 hi で牛の絵が出ました。
Q. ストアが大きくなりすぎたら?
A. pnpm store path で場所を確かめ、pnpm store prune で、どのプロジェクトからも使われていないパッケージを消せます(公式ドキュメント)。
Q. Docker で使うときは?
A. ロックファイルと pnpm-workspace.yaml をコピーしてから pnpm install --frozen-lockfile にすると、キャッシュが効きやすくなります。Dockerfile の考え方は、関連記事「Dockerfileの書き方|軽く・速く・安全なイメージを作るベストプラクティス」で扱っています。
まとめ
- pnpm は ストア+ハードリンク+シンボリックリンク。同じ依存の2プロジェクトで npm 161 MB、pnpm 81 MB だった
- 一番上の node_modules には package.json に書いたものだけ。書いていない
debugはERR_MODULE_NOT_FOUND - v11 からは 公開24時間以内の版を入れない。依存のビルドスクリプトは
pnpm approve-buildsで許可 - 設定は
pnpm-workspace.yaml。workspace はworkspace:*で参照し、--filter "名前..."で絞る
参考・出典
確認日はいずれも 2026年10月13日です。
- pnpm ドキュメント「pnpm CLI」(npm との違い):https://pnpm.io/pnpm-cli
- pnpm ドキュメント「pnpm vs npm」:https://pnpm.io/pnpm-vs-npm
- pnpm ドキュメント「Settings」:https://pnpm.io/settings
- pnpm ドキュメント「Dependency Resolution Settings」(minimumReleaseAge):https://pnpm.io/settings/dependency-resolution
- pnpm ドキュメント「Build Settings」(allowBuilds・strictDepBuilds):https://pnpm.io/settings/build
- pnpm ドキュメント「node_modules Settings」:https://pnpm.io/settings/node-modules
- pnpm ドキュメント「Workspace」:https://pnpm.io/workspaces
- pnpm ドキュメント「Filtering」:https://pnpm.io/filtering
- pnpm ブログ「pnpm 12.0」:https://pnpm.io/blog/releases/12.0
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「pnpmの使い方|npmとのコマンド対応表とworkspace」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。