npm パッケージマネージャを Yarn から pnpm へ移行した

更新 (2025-11-27)
Renovate が
minimumReleaseAgeExcludeの更新に対応する見込みがあることを pnpm を選んだ最大の理由としていたが、当該部分を削除した。 Renovate のセキュリティアップデートが間接依存パッケージのアップデートに対応していないことが記事公開後に分かり、結局セキュリティアップデートには Dependabot を利用することになったため。
自分が保守している JavaScript のソフトウェア (HamaColor やこのブログ) の依存関係管理に長らく Yarn を使っていたが、最近 pnpm に移行した。その理由と作業内容を簡単に残しておく。
※記事を読んでもらえば分かると思うが、 Yarn と pnpm のどちらが優れているといった議論をしたいのではなく、あくまで今の自分のニーズに合っていたのが pnpm だったという話だ。
環境
- Yarn 4.11.0 (PnP: off)
- pnpm 10.22.0
なぜ pnpm を選んだか
最近 npm エコシステムを標的にしたサプライチェーン攻撃の事例が散見される。パッケージマネージャは依存関係の取得という性質上、サプライチェーン攻撃の影響を強く受ける。 Nx リポジトリへの攻撃では、パッケージをインストールしただけでライフサイクルスクリプト実行によってクレデンシャルが窃取される事態が発生した [^1]。
このような流れを受けて、最近はサプライチェーン攻撃を防ぐための対策に関する議論が特に活発になってきたように思う。以下の記事ではパッケージを利用する側、パッケージを公開する側のそれぞれで実施できる対策がまとめられている。
npmパッケージ/GitHub Actionsを利用する側/公開する側でサプライチェーン攻撃を防ぐためにやることメモ - Zenn
自分も例に漏れずこのようなサプライチェーン攻撃への対策を検討した結果、パッケージマネージャを pnpm に移行するという結論に至った。主な理由は pnpm のライフサイクルスクリプトの扱いにある。
デフォルトでのライフサイクルスクリプト実行拒否
pnpm v10 ではデフォルトで postinstall などを勝手に実行しない [^2]。前述したように、サプライチェーン攻撃はライフサイクルスクリプトの実行がトリガーになる場合がある。 pnpm でライフサイクルスクリプトを実行する場合、実行を許可するパッケージを開発者が明示的に指定する必要がある (詳細は後述)。これによって、開発者の環境で悪意のあるスクリプトが意図せず実行されるリスクを下げることができる。
なお、 Yarn にもライフサイクルスクリプトの実行を拒否する設定があるが、デフォルトでは実行を許可するようになっており、この点で個人的には pnpm の方が都合が良いように感じた。複数のソフトウェアを保守する個人としては、ツールの設定項目の数はできるだけ少なくしたいという気持ちがあり、デフォルト設定が自分のポリシーと似通っていた方がそれを実現しやすいからだ。
minimumReleaseAge による意図的なインストール遅延
これは pnpm を選んだ理由とは直接関係ないが、セキュリティに関わる機能として minimumReleaseAge も紹介したい。 minimumReleaseAge は pnpm v10.16 から導入された設定で、公開直後のパッケージを一定期間インストールしないよう制御できる [^3]。 OSS がサプライチェーン攻撃の被害に遭った際、大抵はそのセキュリティパッチが数日以内に公開される。 minimumReleaseAge を適切に設定しておくことで、パッチが公開される前に問題のあるバージョンをインストールしてしまうリスクを下げることができる。
ただ minimumReleaseAge によってセキュリティパッチのインストールも遅延する点はデメリットとしてやや大きい。存在期間の長い脆弱性の場合、問題のあるバージョンを既にインストールしてしまっている可能性がある。したがってサプライチェーン攻撃を避けたからといって、セキュリティアップデートは依然として必要になる。しかし当然ながら minimumReleaseAge は新しいリリースバージョンが通常のアップデートかセキュリティアップデートかを区別しないので、セキュリティアップデートも minimumReleaseAge に設定した時間だけ遅延することになる。
この点については minimumReleaseAgeExclude という設定を用いた minimumReleaseAge のバイパスが主な手段になりそうだが、ある程度は開発者側が手をかけて対応する必要がありそうだ。 minimumReleaseAgeExclude に対象パッケージとバージョンを記載しておくと、 minimumReleaseAge の時間内であってもそのバージョンのインストールが可能となる。ただ逆に言えばその都度追記する必要があるということであり、セキュリティアップデートに対して間を空けずに対応したい場合は設定の更新作業が発生する。
Dependabot や Renovate のような依存関係管理ツールも、2025年11月26日時点では minimumReleaseAgeExclude の自動更新には対応していないようだ。 Renovate については対応の見込みはあるようだ [^4] が、 Renovate は間接依存パッケージに対してはセキュリティアップデートの PR を作成しないため、その点の改善まで含めると先の長い話になるかもしれない。
なお、 Yarn にも v4.10.0 で npmMinimalAgeGate という同様の機能が追加された [^5]。細かい仕様に差異はあるが、大枠としては pnpm と Yarn のどちらが優れているとは言えないように思う。
Yarn から pnpm への移行
pnpm は pnpm import という他のパッケージマネージャから移行するためのコマンドを提供している。このコマンドによって、 yarn.lock のようなロックファイルの内容を元に pnpm-lock.yaml が生成される。
pnpm import
この状態で pnpm install を実行すると、 esbuild などのパッケージのビルドに関する警告が出る。これは前述したようにライフサイクルスクリプトの実行がデフォルトで拒否されているため、 postinstall で実行されるはずのビルドが実行されなかったためだ。そこで、 pnpm approve-builds コマンドで特定のパッケージのビルドを許可する。
pnpm approve-builds
許可したパッケージは pnpm-workspace.yaml の onlyBuiltDependencies に追加される。
onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild
- unrs-resolver
ここまでは対話的な作業も含むため手動で行ったが、設定ファイルやドキュメントの修正などの残りの作業はすべて Claude Code に行ってもらった。
終わりに
パッケージマネージャ移行は何だかんだハマりどころがあって大変な作業になるんじゃないかと予想していたが、ふたを開けてみれば難なく完了した。この記事を書いている時間の方が断然長い。
しばらくの間はセキュリティアップデートの作業コストがやや増えそうなことは懸念ではあるが、一旦この状態で運用を続けられそうか試してみようと思う。
[^1]: Nx の攻撃から学べること #s1ngularity | blog.jxck.io
[^2]: Release pnpm 10 · pnpm/pnpm
[^3]: Release pnpm 10.16 · pnpm/pnpm
[^4]: feat(pnpm): add minimumReleaseAgeExclude for security updates · Issue #39168 · renovatebot/renovate
[^5]: Release v4.10.0 · yarnpkg/berry