画像はpnpm Logosより引用しました。
2022年に入社したTIGコアテクノロジーユニット所属の齋藤と申します。
Vercelに触れてみようVercel CLIのインストール方法を調べていたところ、パッケージ管理ツールを用いたインストール方法としてnpm, Yarnと並んでpnpm1が書かれていました。npmとYarnは利用したことがある一方で、pnpmは馴染みがなく、触れたことがありませんでした。
pnpmではどのようにパッケージを管理しているかなどの特徴を調べました。
pnpmの概要
pnpmはJavaScript系のパッケージ管理ツールです。pnpmはperformant npm
の略であり2、その名の通りパフォーマンス性を重視した設計になっています。
package.json
を利用するほか、npmリポジトリにあるほとんどのパッケージが利用可能であるなど、npmとは互換性を持っています。
pnpm公式ドキュメントによると、pnpmは以下の特徴を有しています。
- 高速: 他のツールより最大2倍高速に動作する
- 高効率:
node_modules
配下にあるファイルはコンテンツのアドレス指定可能な単一のストレージ(contents-addressable storage)にあるファイルの複製、または、ハードリンクになっている - モノレポのサポート: 1つのリポジトリにパッケージを複数配置する構成をビルトインでサポートしている(workspace機能)
- 厳格: デフォルトではnon-flat node_modulesを採用しており、
node_modules
配下にある任意のパッケージにはアクセスできないようになっている(dependecies
にないパッケージにアクセスできない)
npmやYarnでも対応している「モノレポのサポート」以外について、どのようにして、上記特徴を実現しているのかを後ほど見ていきます。
pnpmの初歩的な利用方法
pnpmでインストールしたパッケージの管理方法を見る前に、ごく簡単に利用方法を見ていきます。
以降Linux環境で用いることを想定してコマンド等を記載します。
pnpmのインストール方法はpnpm Installationを参照してください。
パッケージの追加/削除
コマンド自体は他のツールと大差ありません。expressを追加する場合を例とします。
expressを追加する |
pnpm add
を実行するとpackage.json
, pnpm-lock.yml
が生成され、node_modules/
配下にパッケージがインストールされます。package.json
のフォーマットはnpm
と同様です。また、pnpm-lock.yaml
はnpmのpackage-lock.json
と同様にインストールしたパッケージのバージョン等の情報が記録されています。
既にあるpackage.json
から依存パッケージをインストールする場合は、pnpm install
を実行すればよいです。
スクリプトの実行
こちらもnpm run <script_name>
と同様のコマンドで実行できます。pnpm start
やpnpm test
などの短縮形でstart
スクリプトやtest
スクリプトを実行できることも同様にできます。
cat package.json |
内部構成を見てみる
基本的な利用方法はnpmと大差ないpnpmですが、インストールした依存パッケージの管理方法は大きく異なります。pnpmがどのように管理しているかを見ていきます。
以下のpnpm, npmのバージョンで確認しています。
pnpm --version |
また、今回は特にオプション等を設定していない場合の構造について説明します(設定によってはnpmと同様のディレクトリ構造にすることなども可能です)
node_modules
まずは、node_modules/
配下を見てみます。依存パッケージにexpress@4.18.2
のみを指定したpackage.json
でpnpm install
を実行します。実行後のnode_modules/
配下は以下の通りです。
pnpm |
同様に、npmでnpm install
した場合のnode_modules
配下を確認します。
npm |
pnpmの場合、/node_modules/
直下には隠しファイルを除くとdependencies
に記述したexpress
のみ存在し、express
はシンボリックリンクになっています。リンク先と同階層にはexpress
が依存するパッケージへのシンボリックリンクが張られています。リンクの関係を下の図で示しました。
Node.jsでは実際に存在する場所(リンク先)から依存パッケージを解決するため、このようにシンボリックリンクを用いても依存パッケージを利用できます。
一方で、npm(V3以降)では基本的に/node_modules/
直下にdependenciesの書かれたパッケージ以外にも、それらが再帰的に依存する全パッケージが配置されています。
npmでは、V2以前はある依存パッケージが依存するパッケージは依存元のパッケージ内にあるnode_modules
配下に置かれる階層構造になっていましたが、V3以降はフラットな構造になっています。(ただし、依存パッケージfoo@1.0
, bar@1.0
がそれぞれbaz@1.x
, baz@1.y
に依存している場合、/node_modules/foo/node_modules/baz
にbaz@1.x
パッケージが置かれる可能性があるなど、階層が生じる場合もあります3)
しかし、フラットな構造にしたことでpackage.json
のdependencies
などで指定していないパッケージをアプリで利用できるようになりました。例えば、express
のみをdependencies
に指定した状態で、以下のスクリプトをnodeで実行しようとします。
// index.js |
npmでパッケージをインストールして、実行した場合エラーになりません。cookie
はexpress
に依存されているため、node_modules
直下にあり、nodeは解決できるからです。
dependencies
で指定していないパッケージを利用することは問題が起きる可能性があります。例えば、express
のメジャーバージョンを固定していたとします。cookie
でメジャーアップデートが行われ、express
が対応した場合、express
に破壊的な変更がなければマイナーアップデートで更新されます。しかし、この状態でnpm update
を実行するとcookie
がバージョンアップされるため、意図しない不具合が生じる可能性があります。
pnpmでは/node_modules/
直下にはdependencies
などで指定したパッケージしかないため、このような問題はおきません。上記スクリプトを実行するとエラーになります。
node index.js |
contents-addressable storage
pnpmでは、各プロジェクトのnode_modules
にパッケージを追加したとき、必ずディスク上にファイルの中身が配置されるとは限りません。pnpmを用いてダウンロードしたファイルなどはグローバルなストレージに一元管理することで、ディスク使用容量の削減や、高速化を図っています。
リポジトリからダウンロードしてきたファイルはグローバルなストレージに保存され、各プロジェクトのパッケージからハードリンクが張られます。
ハードリンクはファイルの属性情報を保持しているinodeに対して、リンクが張られます。シンボリックリンクとは異なり、ファイルに対するリンクではないため、最初に作成したファイルが削除されてもリンクおよびファイルの中身は維持され、あるinodeを参照するファイルがなくなったときファイルが削除されます。
※上では「グローバルなストレージに保存され…」と書きましたが、システム的にはこれもハードリンクの1つになります。
pnpmでは$PNPM_HOME/store
がグローバルなストレージとして用いられます。Linux環境のデフォルトでは$HOME/.local/share/pnpm/store
です。中身を見てみます。
tree store/ |
このように、ファイルの中身のsha512とファイルの中身のkey-valueストアになっていることがわかります。ファイルのsha512が一致するファイルがstoreに存在すればそれをハードリンクし、なければstoreに作成してからハードリンクするという仕組みになっています。
また、ファイルの中身以外にもパッケージのあるディレクトリに含まれるファイル情報を管理するindexファイルも存在し、Gitのblobとtreeオブジェクトの管理方法に似ていると思いました。
例えば、あるパッケージの複数のバージョンが依存パッケージとして必要なとき。2つのバージョン間では少数のファイルのみが変更され、多数のファイルには変更がないことも多いです。pnpmでは差分があるファイルのみ、新たにストレージに保存されるため、効率的に保存できます。
なお、ハードリンクでは先述の通りinodeの被参照数が0になったときファイルが削除されますが、storeにあるファイルが参照しているため、どのプロジェクトでも使われなくなったファイルもstoreに残り続けます。これらのファイルはpnpm store prune
コマンドを実行することで、削除できます。
pnpmを利用するか
ここまでpnpmの概要や、内部ではどのように管理することで高効率になっているかなどを見てきましたが、pnpmを趣味や、仕事の開発で使ってみたいか考えます。
まず、学習や趣味の開発などの個人利用ではpnpmの恩恵を受けやすく、利用しやすいと思います。以下の理由で恩恵を受けやすいと考えます。
- 学習や、趣味の開発では様々なリポジトリからクローンするなどして、多数のnpmプロジェクトがローカルに保存されることになりやすいため、ディスク使用容量削減効果は大きい
- 色々なパッケージを試す機会が多く、パッケージをインストールする頻度が高くなるため、インストール高速化の恩恵を受けやすい
一方、業務利用の場合でもpnpmは利用できそうですが、いくつか懸念点があります。
- pnpmのネットでの情報量が少ない
- 例えば、
<ツール名> error
でのGoogle検索件数を見ると、npmが4,200万件、yarnが4,000万件であるのに対して、pnpmは70万件です。トラブルで開発が止まった時に多大な損失が生じる業務での開発では情報量の少なさは問題になりやすいでしょう
- 例えば、
- 他のツールから移行する場合、メンバーのキャッチアップする必要があったり、作業自動化に用いるスクリプトを改修をしなければいけなくなったりして、移行コストがかかる(pnpmだけではなく、移行全体にいえる問題ですが)
そして、pnpmを用いることで早くなるのはパッケージのインストールにかかる時間であり、アプリのパフォーマンスが向上するわけではありません。CIでテストを実行するときなどで、セットアップに多くの時間がかかっている場合はpnpmを使う効果が高いですが、そうではないことも多いでしょう。
pnpmを導入する手間に対して、pnpmを使うことによるメリットが見合っているかは考える必要があります。
まとめ
pnpmの概要と、内部でどのように管理しているのかを眺めました。
まだpnpmをそこまで本格的には利用してはいませんが、使っていて特に問題なければ、少なくとも個人開発では利用したいと考えています。
次は森さんのWebAssemblyとEmscriptenに入門した です。
- 1.
pnpm
が正確な表記です。ちなみに、公式ドキュメントトップページのタイトルではpnpm
の各文字の大文字小文字がランダムに決まる(20秒ごとに再生成される)仕掛けになっており、筆者が最初に表記を確認したときはPnpm
だったため、これが正確な表記と暫く勘違いしていました。 ↩ - 2.https://pnpm.io/faq#what-does-pnpm-stand-for ↩
- 3.詳細はnpmのHow npm3 Worksを参照してください ↩