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 が無いか確かめる
  1. pnpm import でロックファイルを変換(このとき minimumReleaseAge が効く点に注意)
  2. Cannot find module が出たら、そのパッケージを pnpm add する
  3. ERR_PNPM_IGNORED_BUILDS が出たら、pnpm approve-builds で許可する
  4. 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?