npm v7以降のpeerDependencies採用方針

## 先に結論だけ

このような依存関係のパッケージがあるとします。

```
パッケージ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がどのように振る舞うかの実験と、その結果から依存パッケージをどのフィールドにインストールすべきかの方針を記録、共有するためのものです。

### 想定する環境

```bash
% node --version
v18.15.0
% npm --version
9.5.0
```

:::note warn
対象となるバージョンが変わると、記事の内容がそのまま適用できないかもしれません。記事を読む前に、お手元の環境をご確認ください。
:::

### 想定する読者

- npmモジュールの開発者
- 開発しているモジュールが、他のモジュールに依存している

## npm v7での破壊的変更

https://github.blog/2021-02-02-npm-7-is-now-generally-available/#peer-dependencies

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_

```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_

```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_

```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_

```json
  "version": "1.0.0",
  "peerDependencies": {
    "package-a": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
  }
```

 _▼package-c/package.json_

```json
  "dependencies": {
    "package-b": "^1.0.0"
  }
```

この場合、パッケージCではパッケージAの`4.0.0`がインストールされます。

#### パッケージBでpeerDependenciesを指定し、パッケージCで矛盾したバージョンを指定する

  _▼package-b/package.json_

```json
  "version": "1.0.0",
  "peerDependencies": {
    "package-a": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
  }
```

 _▼package-c/package.json_

```json
  "dependencies": {
    "package-b": "^1.0.0",
    "package-a": "^0.1.0"
  }
```

```console
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
```

バージョンの解決に失敗し、エラーが発生します。インストールは中断されます。

#### パッケージBとCでdependenciesを指定する

 _▼package-b/package.json_

```json
  "dependencies": {
    "package-a": "^2.0.0"
  }
```

 _▼package-c/package.json_

```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はインストール中にエラーを発生させます。下位互換を削除する場合はセマンティックバージョニングにしたがってメジャーバージョンを変更しましょう。

---

以上、ありがとうございました。