pnpmはなぜエラーを吐くのか
「pnpmはよくエラーが出やすい!」という話をよく聞きます。
エラーが出るのは良いことだと思うのですが、それが原因でpnpm
の使用に対して疑念を抱く人がいるのはもったいないなと思い、なぜエラーが出やすいのかを調べました。その際に以下のページにたどり着き、そこから着想を得てこの記事を書きました。
もし間違った情報がありましたら修正いたしますので、気軽にコメントいただけますと幸いです。
エラー文から見るこの記事を読むメリット
具体的にどんなエラーが出るのか。
Error: Cannot find module 'パッケージの名前'
Require stack:
- /path/to/your/project/ファイル名.js
- ... (他の require 元ファイル)
よく見かけますね、Cannot find module
です。これはpnpm
が出しているものではなく、Nodejs
が吐き出すものです。依存関係のエラーですので、パッケージマネージャの問題であるとはわかりますが、なぜ起きているのか一見ではわかりにくいです。
そのため、この記事を読むことでCannot find module
が出た時、かつパッケージマネージャにpnpm
使用している時に、なぜCannot find module
が起きるのかを仕組みとして理解し、対処できるようなること目標に書きたいと思います。
ほぼ自分のためですが、誰かの参考になれば幸いです。
pnpmとは
意味: "performant npm"の略
パッケージマネージャーの一つ。npm
由来で作られているが、npm
との決定的な違いとして、PCの中にグローバルストアというものを作ることが挙げられる。一度ダウンロードしたパッケージをグローバルストアに保存して再利用する仕組みによって、結果的にインストールプロセス全体が高速になるという点がポイントの優れもの。
ではなぜそんな優れものがよくエラーを吐くと言われるのでしょうか。
npmとpnpmの比較で見る原因
追記 : より詳しい内容はこちらの方の記事にわかりやすく書いてあります。
npm の場合:
npm
は、プログラムが使う部品(パッケージ)をnode_modules
というフォルダの中に、さらにその部品が必要とする部品もその部品のnode_modules
の中に入れる、という入れ子のような構造(ネスト構造)で作っています。
npm の node_modules のイメージ
node_modules/
├── A/ <-- 直接使いたい部品A
│ └── node_modules/
│ └── B/ <-- 部品Aが使う部品B
たとえ部品Aの製作者が、部品A自身が使う部品Bを直接package.json
に書いていなかったとしても、npm
の場合、部品Bが元々部品Aのnode_modules
フォルダ内に存在することがあります。
その場合、Node.js
は部品を探す際に、近くのnode_modules
フォルダから順番に探索するため、たまたま部品Aのnode_modules
内にあった部品Bを見つけてしまい、エラーにならずに動いてしまいます。これは、本来はプログラムが直接必要としている部品ではないのに、たまたま他の部品と一緒にインストールされたことで動いてしまっている状態です。
さらに厄介なのは、上記の状態の部品Aを自身のプロジェクトにダウンロードしてプロジェクトで node_modules
を再生成した場合です。再生成すると、通常は部品Bは部品Aのnode_modules
に存在しないはずです。
しかしながら、もし自身の環境の他のnode_modules
フォルダ(プロジェクトのトップレベルや、さらに上位の階層など)に、何らかの関係で部品Bが既に存在する場合、エラーは出ないことがあります。
npm の node_modules のイメージ
node_modules/
├── A/ <-- 直接使いたい部品A
│ └── node_modules/
├── X/ <-- 別の目的で入れた部品X
│ └── node_modules/
│ └── B/ <-- 部品Aが使う部品B
npm
は、現在のnode_modules
フォルダで見つからない場合、上位の node_modules
フォルダも探索するため、他の部品がたまたま部品Bをインストールしていると、それを見つけて動作してしまう可能性があります。しかし、自身の環境に部品Bが存在しない場合は、部品Aのコードが「部品Bが見つからない!」というエラーを出します。
(この状態はそもそも部品Aの製作者が、必要な依存関係を自身のpackage.json
に明記していない責任であると言えます。)
こんなたまたまで動く可能性のある環境は嫌ですよね...
そこで生まれたのがpnpm
です!
pnpm の場合:
pnpm
は、プロジェクトの中にグローバルストアという特別な場所を作り、そのグローバルストアにpackage.json
を参照してダウンロードした部品を一度だけ保存し、各プロジェクトのnode_modules
フォルダには、そこへの「近道」(シンボリックリンク)を置くという方法を使っています。さらに、直接使う部品だけでなく、その部品が使う部品も、node_modules
フォルダの直下に並べて置きます(フラット化)。どの部品がどの部品に必要とされているかの関係性は、内部でしっかり管理しています。
pnpm の node_modules のイメージ
node_modules/ <<< PC/プロジェクトの node_modules フォルダ
├── A -> ../../.pnpm/A@バージョン/node_modules/A <<< ここが A へのシンボリックリンク
├── B -> ../../.pnpm/B@バージョン/node_modules/B <<< ここが B へのシンボリックリンク
└── .pnpm/ <<< グローバルストア (実際のファイルが保管されている場所)
├── A@バージョン/
│ └── node_modules/
│ └── A/ <<< A の実際のファイル
└── B@バージョン/
└── node_modules/
└── B/ <<< B の実際のファイル
<<< フラット化されているのは node_modules/ 直下の A と B のシンボリックリンクがある構造
この構造では、プログラムが部品Bを必要としている場合、node_modules
の直下にある部品Bへの近道を探します。もし、プロジェクトのpackage.json
に部品Bがきちんと書かれていなければ、pnpm
は部品Bへの近道を作らないため、Node.js
は部品Bを見つけることができず、「見つからない!」というエラーを出してプログラムが止まってしまいます。
結論
pnpmは、package.json
に書かれている情報だけを信頼し、明示的に宣言されていない依存関係には頼りません。npm
のように、たまたま他の部品にくっついてきただけの部品に依存したままプログラムが動いてしまう状態を避けることで、より正確で信頼性の高い依存関係管理を実現しようとしているからです。
つまりpnpm
はエラーを出しやすいのではなく、
- 「たまたま他の部品にくっついてきただけの部品」が存在しない、またはそのような状況が極めて起こりにくい仕組みをとっている
ということが原因で依存関係のエラーがちゃんと出るんだよ、というお話なわけですね。
エラーが出たらどうしたらいい?
pnpm list <パッケージ名> --depth 0
で調べる
0, まずはまずは下調べで対象のパッケージが入っているのか、入っているならバージョンは何かを調べましょう。
入ってないならadd
すると良いと思います。
<PC名> <プロジェクト名> % pnpm list <パッケージ名> --depth 0
dependencies:
<パッケージ名> <バージョン> <<<ここに表示される
pnpm add <パッケージ名>
で追加する
1, よく使うadd
コマンドですね。素直にpackage.json
に自分で書き足す方法です。これが最適解です。
nodeLinker: hoisted
を使う
2, pnpmに「npmみたいなやり方で部品を置いてみて」とお願いする方法です。しかし、これは一時的な解決策です。package.jsonに足りないものadd
して尚解決しない場合は検討ください。
↓公式の記載
フック
という特別な仕組みを使う (.pnpmfile.cjs)
3, 少し複雑ですが、pnpmが部品を読み込む前に、「ちょっと待って!もしAっていう部品だったら、Bも一緒に必要だって書いておいて!」と、pnpmに特別な指示をするファイルを作る方法です。
ここまで読んでいただきありがとうございました。
普段はフロントエンドの開発をしており、それ関連のつぶやきをしていますのでどうぞこれからもよろしくお願いいたします!
Discussion