WAT Note(III).
#52

pnpmについて

Tatsuroh Wakasugi
Tatsuroh Wakasugi

npmで長年開発してきた人が「pnpmに乗り換えてみたい」「業務で使うことになった」というケースは増えています。本記事はNode.js/npmの基礎は理解している前提で、pnpm特有の仕組みとコマンド対応、そして最小限のハンズオンを紹介します。

pnpmとは何か

pnpm(Performant npm)は、npmやYarnと同じpackage.json・node_modulesの世界で動作しつつ、依存関係の格納方法と参照方法が根本的に異なるパッケージマネージャです。pnpmは「Performant npm」を語源とする、Node.js用のパッケージマネージャで、npmやyarnと同じpackage.json・node_modulesの世界で動きますが、依存の格納方法と参照方法が根本から違います。

pnpmを特徴づけているのは大きく3点です。

  1. コンテンツアドレスストア + ハードリンクによる重複排除
  2. 非フラットなnode_modulesによる依存の明示性(=ファントム依存の防止)
  3. 依存の解決・取得・リンクを並列化した高速化

具体的には、ダウンロードしたパッケージは「コンテンツアドレスストア」に保存され、プロジェクトの node_modules からはそのストア内のファイルへリンク(ハードリンクやシンボリックリンク/Windowsではジャンクション等)されます。ストアの実際の場所は OS と設定(XDG vars や環境変数 PNPM_STORE_PATH)に依存します。実際のパスは pnpm store path で確認できます。

もう一つの重要な違いが「ファントム依存の防止」です。npmはフラットなnode_modules構造のため、package.jsonに書いていないパッケージ(依存の依存)が誤ってrequireできてしまうことがあります。pnpmはシンボリックリンクを使った階層構造でこれを防ぎ、「宣言していないパッケージは使えない」設計になっています。

npmとのコマンド対応表

まずは手に馴染んでいるnpmコマンドとの対応を押さえましょう。

npm pnpm
npm install pnpm install(pnpm i)
npm install <pkg> pnpm add <pkg>
npm install -D <pkg> pnpm add -D <pkg>
npm install -g <pkg> pnpm add -g <pkg>
npm uninstall <pkg> pnpm remove <pkg>
npm update pnpm update(pnpm up)
npm run build pnpm run build(pnpm buildでも可)
package-lock.json pnpm-lock.yaml
(なし/workspaces) pnpm-workspace.yaml

※ pnpm のバージョンは pnpm --version(pnpm -v)で確認できます。pnpm が動作する Node.js の最小要件は pnpm のリリースノートや公式ドキュメントに記載されているため、実際の互換性を確認するにはドキュメントページを参照してください。ローカルの Node.js バージョンは node --version で確認し、pnpm の要求バージョンと照合してください。

ハンズオン:pnpmを触ってみる

インストール

npmでグローバルインストールする場合は次の通りです。

npm install -g pnpm

インストール確認

pnpm --version

既存のnpmプロジェクトで試す

手元にpackage-lock.jsonのあるnpmプロジェクトがあれば、そのままpnpmでインストールし直してみましょう。

cd your-npm-project
# 既存のlockfileをpnpm-lock.yamlへ変換(推奨)
pnpm import

# 既存の node_modules を削除(Unix/macOSの場合)
rm -rf node_modules

# 依存をインストール
pnpm install

pnpm-lock.yamlが新しく生成されます。以降のインストール・スクリプト実行はnpmとほぼ同じ感覚で行えます。

pnpm add lodash
pnpm add -D typescript
pnpm run build
pnpm test

ストアの場所を確認する

コンテンツアドレスストアが実際にどこにあるか確認してみましょう。

pnpm store path

同じマシン内の別プロジェクトで同じバージョンのlodashをインストールしても、このストアの実体を再利用(ハードリンク)するため、2回目以降のインストールが高速になることを体感できます。

モノレポ(pnpm workspace)を組んでみる

pnpmの代表的な強みがworkspace機能によるモノレポ管理です。簡単な構成を作ってみます。

mkdir pnpm-workspace-demo && cd pnpm-workspace-demo
mkdir -p packages/ui packages/web

ルートにpnpm-workspace.yamlを作成します。

# pnpm-workspace.yaml
packages:
  - "packages/*"

ルートのpackage.json

{
  "name": "pnpm-workspace-demo",
  "private": true,
  "scripts": {
    "build": "pnpm -r run build"
  }
}

packages/ui/package.json(社内共有ライブラリ想定)

{
  "name": "ui",
  "version": "1.0.0",
  "main": "index.js"
}

packages/webからuiパッケージをワークスペース内の依存として参照します。

cd packages/web
pnpm add ui@workspace:*

これでpackages/web/package.jsonの依存に"ui": "workspace:*"が追加され、uiはnpm registryからではなくローカルのpackages/uiにシンボリックリンクされます。workspace機能はnpm / yarnでも提供されており、それらでも適用できる内容が多いため、既存のnpm workspacesの知識もある程度活かせます。

全パッケージへの一括実行は--filterか-r(recursive)を使います。

# ルートから全パッケージのbuildスクリプトを実行
pnpm -r run build

# 特定パッケージだけに絞って実行
pnpm --filter ui run build

一時的にツールを実行する(npx相当)

pnpm dlx create-vite my-app

ローカルのdevDependenciesにインストール済みのCLIを実行したい場合はpnpm execを使います。

pnpm exec eslint .

さらに知っておくと便利な機能

  • pnpm patch: 依存パッケージのソースをその場で修正し、パッチとして固定できる機能。緊急のバグ修正や脆弱性対応に有効
  • --frozen-lockfile: CI環境でpnpm-lock.yamlとpackage.jsonの不整合を検知して失敗させるオプション(npmのnpm ciに相当)
# CI用インストール(lockfileと不整合があれば失敗する)
pnpm install --frozen-lockfile

まとめ:最初に押さえておくべき3点

  1. node_modulesの構造がnpmと異なり、ストア+ハードリンクでディスクを節約しつつファントム依存を防止する
  2. コマンド体系はnpmとほぼ1対1対応(install→addが主な違い)で、学習コストは低い
  3. workspace機能がモノレポ運用の主戦力。pnpm-workspace.yamlと--filter/-rを覚えれば十分戦える

普段npmで書いているスクリプトやCIの多くは、コマンドをpnpmに置き換えるだけでほぼそのまま動きます。まずは既存プロジェクトのnode_modulesを一度pnpmで入れ直し、ディスク使用量とインストール速度の違いを体感してみるのがおすすめです。