できない.dev

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

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

公開:

要約

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

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

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

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

よくある原因

  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 に追記される。

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