先に結論だけ
このような依存関係のパッケージがあるとします。
パッケージA
┗ パッケージB
┗ パッケージC
npm v7以降のpackage.jsonではpeerDependencies
を以下の方針で採用しましょう。
- パッケージBとCで、違うバージョンのパッケージAを使う場合
dependencies
を使う - パッケージBとCで、同じバージョンのパッケージAが必要な場合は
peerDependencies
を使う
パッケージBでpeerDependencies
を使う場合は
- パッケージBでは、パッケージAを
dependencies
やdevDependencies
には入れない - パッケージCでも、パッケージAを
dependencies
やdevDependencies
には入れない
はじめに
この記事は、npm v7以降のpackage.jsonでpeerDependenciesがどのように振る舞うかの実験と、その結果から依存パッケージをどのフィールドにインストールすべきかの方針を記録、共有するためのものです。
想定する環境
% node --version
v18.15.0
% npm --version
9.5.0
対象となるバージョンが変わると、記事の内容がそのまま適用できないかもしれません。記事を読む前に、お手元の環境をご確認ください。
想定する読者
- npmモジュールの開発者
- 開発しているモジュールが、他のモジュールに依存している
npm v7での破壊的変更
npm V7でpeerDependencies
フィールドの挙動に破壊的変更が加えられました。大きな変更点は以下の2点です。
peerDependenciesのバージョン評価が厳密になった
従来のpeerDependenciesでは、矛盾するバージョンのパッケージがインストールされても警告が表示されるだけでした。npm v7以降では、peerDependenciesの設定と矛盾するバージョンがインストールされるとエラーが発生し、インストールが中断されます。
peerDependenciesがインストールされるようになった
npm v7以降では、peerDependenciesで指定したパッケージが自動的にインストールされるようになりました。以前のようにdependenciesやdevDependenciesに追加する必要はありません。
peerDependenciesを指定した場合のバージョン解決
それぞれのパッケージで、peerDependenciesとdependenciesを指定した場合どのようにバージョンが解決されるかを実験してみます。
パッケージBの場合
まずは、直接peerDependenciesを指定したパッケージの振る舞いを確認します。
peerDependenviesのみを指定する
▼package-b/package.json
"peerDependencies": {
"package-a": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
}
この場合、指定範囲内のもっとも高いバージョン4.0.0
がインストールされます。
peerDependenciesとdependenciesを併記する
▼package-b/package.json
"peerDependencies": {
"package-a": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
},
"dependencies": {
"package-a": "^3.0.0"
},
この場合、両方の指定範囲内のもっとも高いバージョン3.0.0
がインストールされます。
peerDependenciesとdependenciesを併記し、矛盾させる
▼package-b/package.json
"peerDependencies": {
"package-a": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
},
"dependencies": {
"package-a": "^0.1.0"
},
この場合、dependenciesが優先され0.1.0
がインストールされます。インストール時にエラーは発生しません。peerDependenciesが強制力を発揮するのは、このパッケージを参照する先であり、自分自身には強制しません。
パッケージCの場合
パッケージBに依存するパッケージCで、依存パッケージがどのようにバージョン解決されるかを実験します。
パッケージBでpeerDependenciesのみを指定する
▼package-b/package.json
"version": "1.0.0",
"peerDependencies": {
"package-a": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
}
▼package-c/package.json
"dependencies": {
"package-b": "^1.0.0"
}
この場合、パッケージCではパッケージAの4.0.0
がインストールされます。
パッケージBでpeerDependenciesを指定し、パッケージCで矛盾したバージョンを指定する
▼package-b/package.json
"version": "1.0.0",
"peerDependencies": {
"package-a": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
}
▼package-c/package.json
"dependencies": {
"package-b": "^1.0.0",
"package-a": "^0.1.0"
}
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
バージョンの解決に失敗し、エラーが発生します。インストールは中断されます。
パッケージBとCでdependenciesを指定する
▼package-b/package.json
"dependencies": {
"package-a": "^2.0.0"
}
▼package-c/package.json
"dependencies": {
"package-b": "^1.0.0",
"package-a": "^4.0.0"
}
この場合、以下の2つがインストールされます。
- package-c/node_modules : package-a 4.0.0
- package-c/node_modules/package-b/node_modules : package-a 2.0.0
dependenciesとpeerDependenciesの採用方針
実験結果から、それぞれのパッケージでdependenciesとpeerDependenciesのどちらを採用すべきか方針を割り出します。
パッケージAの複数バージョンが併存するか否か
パッケージBでdependenciesとpeerDependenciesのどちらを利用するかは、パッケージAの複数のバージョンが併存するか否かで決定します。
パッケージBとCがそれぞれパッケージAに依存するが、パッケージAのグローバル変数を利用しない場合、dependenciesを利用します。それぞれがパッケージAのインスタンスを作成し、使い終われば破棄する場合はこの条件に該当します。
パッケージAのグローバル変数を利用し、その変数がパッケージBとCで共有されなければいけない場合、peerDependenciesを利用します。プラグインモジュールを作成する場合はこの条件に該当します。
たとえばパッケージAがシングルトンを作り状態を保存する場合、バージョンが共通していないと動作しません。
peerDependenciesとdependencies / devDependenciesの併用
パッケージBでpeerDependenciesを採用したなら、パッケージAはdependencies / devDependenciesに追記してはいけません。npm v7以降では、peerDependenciesは自動的にインストールされます。
過去のバージョンの動作確認をする時のみdevDependenciesを利用し、公開時には削除しましょう。
GitHub ActionsのDependabotを使う場合のみ併用する
あなたがGitHub ActionsのDependabotなど、依存関係を自動更新するCI環境を持っている場合は、peerDependenciesとdevDependenciesの併用を検討してください。
DependabotはpeerDependenciesのバージョンを確認しません。devDependenciesに同じ依存先を指定しておけば、Dependabotはバージョンの確認と更新PRを発行します。
peerDepencenciesのバージョン指定
peerDependenciesを使う場合、動作確認ができる範囲で広いバージョンを指定しましょう。
peerDependenciesはインストール中にエラーを発生させます。下位互換を削除する場合はセマンティックバージョニングにしたがってメジャーバージョンを変更しましょう。
以上、ありがとうございました。