できない.dev

pnpm「Cannot find module」が移行後に出る原因と直し方

pnpm 移行後の Cannot find module は、読み込んでいるパッケージを pnpm add で宣言すれば直る。
pnpm の node_modules はフラットではなく、未宣言の依存は解決できないためだ。

公開: 更新:

症状できない · exit 1
node app.js
…
Error: Cannot find module 'ms'
Require stack:
- /tmp/tmp.Pz2Y2iElNd/app.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15)
解決後できた · exit 0
node app.js
ms(1h) = 3600000

2026-09-10 に node:20(Node.js 20.20.2 / npm 10.8.2) で実際に打って取った出力。検証環境の詳細

要約

npm や Yarn Classic から pnpm へ移行した直後に Cannot find module 'xxx' が出るのは、pnpm の node_modules がフラットではないからだ。
pnpm は実体を node_modules/.pnpm に置き、各パッケージの直接依存だけをシンボリックリンクで見せる。
そのため package.json に宣言していないパッケージは解決できない。

正しい対処は、まず読み込んでいるパッケージを宣言することだ。

pnpm add lodash        # 実際に読み込んでいるなら依存として宣言する

宣言を直せない相手(自分の管理外のツールなど)に限り、hoisting の設定で救う。

実行例

pnpm 9.15.0 で debug だけを依存に書いた状態で pnpm install すると、node_modules の直下に現れるのは debug 一つだけで、その依存である ms を require したところで MODULE_NOT_FOUND になる。pnpm add ms で自分の依存として宣言し直すと、同じコードがそのまま動いた。

$ pnpm install
Progress: resolved 1, reused 0, downloaded 0, added 0
Packages: +2
++
 
   ╭──────────────────────────────────────────────────────────────────╮
   │                                                                  │
   │                Update available! 9.15.0 → 12.3.4.                │
   │   Changelog: https://github.com/pnpm/pnpm/releases/tag/v12.3.4   │
   │         Run "corepack install -g pnpm@12.3.4" to update.         │
   │                                                                  │
   ╰──────────────────────────────────────────────────────────────────╯
 
Progress: resolved 2, reused 0, downloaded 2, added 2, done
 
dependencies:
+ debug 4.3.7 (4.4.3 is available)
 
Done in 487ms
$ echo $?
0
$ node app.js
node:internal/modules/cjs/loader:1210
  throw err;
  ^
 
Error: Cannot find module 'ms'
Require stack:
- /tmp/tmp.Pz2Y2iElNd/app.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15)
    at Module._load (node:internal/modules/cjs/loader:1038:27)
    at Module.require (node:internal/modules/cjs/loader:1289:19)
    at require (node:internal/modules/helpers:182:18)
    at Object.<anonymous> (/tmp/tmp.Pz2Y2iElNd/app.js:1:12)
    at Module._compile (node:internal/modules/cjs/loader:1521:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1623:10)
    at Module.load (node:internal/modules/cjs/loader:1266:32)
    at Module._load (node:internal/modules/cjs/loader:1091:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/tmp/tmp.Pz2Y2iElNd/app.js' ]
}
 
Node.js v20.20.2
$ echo $?
1
$ ls node_modules
debug
$ pnpm add ms
Progress: resolved 0, reused 1, downloaded 0, added 0
Already up to date
Progress: resolved 2, reused 2, downloaded 0, added 0, done
 
dependencies:
+ ms 2.1.3
 
Done in 320ms
$ echo $?
0
$ node app.js
ms(1h) = 3600000
$ echo $?
0

— 2026-09-10 時点の出力

検証環境Node.js 20.20.2 / npm 10.8.2 / Python 3.11.2 / 2026-09-10 検証(ほか 1 件)
検証日
実行環境
node:20 Debian GNU/Linux 12 (bookworm)
バージョン
  • Node.js 20.20.2
  • npm 10.8.2
  • Python 3.11.2
  • Git 2.39.5

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

解決策

1. 依存を明示的に宣言する(推奨)

もっとも健全な直し方だ。
エラーになったパッケージ名をそのまま追加する。

pnpm add <pkg>          # dependencies へ
pnpm add -D <pkg>       # devDependencies へ

pnpm が既定で作るのは semistrict な node_modules で、宣言漏れをこの段階で検出できるのが利点だと公式ドキュメント(新しいタブで開く)も説明している。
移行時に出たエラーは、npm 時代から潜んでいた宣言漏れが表面化したものと考えてよい。

2. 特定パッケージだけ引き上げる

自分では直せないツールが原因なら、そのパッケージだけをルートの node_modules へ引き上げる。
設定は pnpm-workspace.yaml に書く。

publicHoistPattern:
  - "*eslint-plugin*"

全部を引き上げる shamefullyHoist: true は publicHoistPattern を * にするのと同じで、pnpm の利点である厳密さを失う。
まずはパターンを絞って引き上げる。

3. フラットな node_modules にする

シンボリックリンクそのものが扱えない環境では、リンカを切り替えてフラット構造を作らせる。

nodeLinker: hoisted

nodeLinker の既定は isolated(node_modules/.pnpm からのシンボリックリンク)で、hoisted は npm や Yarn Classic と同じ形になる。
動く代わりに phantom dependency は再び検出できなくなる。

4. モノレポでは宣言位置を見直す

ワークスペース構成では、依存はそれを使うパッケージの package.json に書く。
ルートに置いた依存は配下のパッケージからは解決されない。

pnpm --filter @app/web add zod

--filter で対象パッケージを指定すれば、そのパッケージの package.json に追記される。

よくある原因

  1. package.json に書いていないパッケージ(phantom dependency)を読み込んでいる。
    npm のフラットな node_modules では、他パッケージの依存がたまたま同じ階層に並ぶため解決できてしまっていた。
  2. ツールがシンボリックリンクを辿れない。
    一部のプラグイン機構は node_modules 直下を実パスとして走査するが、pnpm の既定レイアウトではそこに実体が無い。
  3. 実行環境がシンボリックリンクに対応していない。
    React Native や一部のサーバーレスホスティング(AWS Lambda など)は、シンボリックリンクを扱えない。
  4. モノレポで依存の宣言位置がずれている。
    ルートの package.json にだけ入れた依存を、配下のワークスペースパッケージから参照している。