🙄

pnpmはなぜエラーを吐くのか

に公開

「pnpmはよくエラーが出やすい!」という話をよく聞きます。

エラーが出るのは良いことだと思うのですが、それが原因でpnpmの使用に対して疑念を抱く人がいるのはもったいないなと思い、なぜエラーが出やすいのかを調べました。その際に以下のページにたどり着き、そこから着想を得てこの記事を書きました。
https://pnpm.io/faq#why-do-i-get-errors-after-upgrading-nodejs

もし間違った情報がありましたら修正いたしますので、気軽にコメントいただけますと幸いです。

エラー文から見るこの記事を読むメリット

具体的にどんなエラーが出るのか。

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の比較で見る原因

追記 : より詳しい内容はこちらの方の記事にわかりやすく書いてあります。
https://zenn.dev/azukiazusa/articles/pnpm-feature

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はエラーを出しやすいのではなく、

  • 「たまたま他の部品にくっついてきただけの部品」が存在しない、またはそのような状況が極めて起こりにくい仕組みをとっている

ということが原因で依存関係のエラーがちゃんと出るんだよ、というお話なわけですね。

エラーが出たらどうしたらいい?

0, まずはpnpm list <パッケージ名> --depth 0で調べる

まずは下調べで対象のパッケージが入っているのか、入っているならバージョンは何かを調べましょう。
入ってないならaddすると良いと思います。

<PC名> <プロジェクト名> % pnpm list <パッケージ名> --depth 0

dependencies:
<パッケージ名> <バージョン>    <<<ここに表示される

1, pnpm add <パッケージ名>で追加する

よく使うaddコマンドですね。素直にpackage.jsonに自分で書き足す方法です。これが最適解です。

2, nodeLinker: hoistedを使う

pnpmに「npmみたいなやり方で部品を置いてみて」とお願いする方法です。しかし、これは一時的な解決策です。package.jsonに足りないものaddして尚解決しない場合は検討ください。
↓公式の記載
公式ページの記載

3, フックという特別な仕組みを使う (.pnpmfile.cjs)

少し複雑ですが、pnpmが部品を読み込む前に、「ちょっと待って!もしAっていう部品だったら、Bも一緒に必要だって書いておいて!」と、pnpmに特別な指示をするファイルを作る方法です。
https://pnpm.io/ja/pnpmfile


ここまで読んでいただきありがとうございました。
普段はフロントエンドの開発をしており、それ関連のつぶやきをしていますのでどうぞこれからもよろしくお願いいたします!

Discussion