できない.dev

pnpm に移行したら「Cannot find module」で依存パッケージが解決できない

pnpm の node_modules は npm のようなフラット構造ではない。
package.json に書いていない依存を直接読み込んでいたコードが、移行した途端に解決できなくなる。
まず依存を明示的に宣言し、直せない相手だけ hoisting 設定で救う。

公開: 更新:

実行例あり(2026-09-10 に実環境で検証)

要約

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
終了コード: 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
終了コード: 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
終了コード: 0
$ node app.js
ms(1h) = 3600000
終了コード: 0

— 2026-09-10 時点の出力

検証環境

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

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

よくある原因

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

解決策

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: truepublicHoistPattern* にするのと同じで、pnpm の利点である厳密さを失う。
まずはパターンを絞ること。

3. フラットな node_modules にする

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

nodeLinker: hoisted

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

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

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

pnpm --filter @app/web add zod

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

この記事は役立ちましたか?