npmはパッケージマネージャーとして非常に優れた仕組みを持っており、特にサブ依存関係の処理においてその真価を発揮します。

あるパッケージが「request」のバージョン2に依存しており、別のライブラリが「request」のバージョン1に依存していても、npmはそれらを適切に隔離して管理します。

この仕組みのおかげで、異なるバージョンのライブラリが混在しても、コードが干渉し合うことなく正常に動作します。

しかし、この便利な仕組みが仇となってしまうケースが、特定のホストパッケージに対して機能を追加する「プラグイン」の開発において発生していました。

本記事では、2013年2月にnpm 1.2.xで導入された「Peer Dependencies」という概念が、どのようにプラグインのエコシステムを救ったのかを詳しく解説します。

プラグイン開発における従来の依存関係の課題

Node.jsのエコシステムには、GruntのプラグインやExpressのミドルウェアなど、別の「ホスト」パッケージと一緒に使用されることを前提としたモジュールが数多く存在します。

これらのプラグインは、特定のバージョンのホストパッケージと組み合わせて動作するように設計されています。

もし、プラグインがホストパッケージを通常の dependencies として定義してしまうと、npmの標準的な挙動によって深刻な問題が引き起こされます。

例えば、アプリケーション本体が「winston」の最新版を使用し、プラグインが古い「winston」に依存している場合、プロジェクト内には2つの異なるWinstonが存在することになります。

この状況を図示すると、以下のような依存関係ツリーが形成されます。

text
my-app
├── winston@0.6.2 (アプリケーションが直接使用)
└── winston-mail@0.2.3 (プラグイン)
    └── winston@0.5.x (プラグインの内部に閉じ込められたコピー)

プラグインは自身のディレクトリ内にある古いWinstonを使用し、メインアプリケーションは最新のWinstonを使用することになり、APIの不整合や予期せぬ動作不良を招く原因となります。

本来、プラグインはアプリケーションが持っているホストパッケージと「同じインスタンス」を共有すべきなのです。

解決策としての「Peer Dependencies」の導入

このような「プラグインとホスト」の関係性を正しく表現するために導入されたのが、peerDependencies という新しい仕組みです。

これは、「自分をインストールするなら、隣に指定したバージョンのパッケージも一緒にインストールしておいてください」という依存関係の表明です。

npm 1.2.0から試験的に導入され、2013年2月リリースのnpm 1.2.10 (Node.js 0.8.19に同梱) において、実用的な形へと洗練されました。

この機能により、プラグインがホストパッケージの特定のバージョンを要求しつつ、自分自身の配下にそのコピーを持たないように制御することが可能になりました。

依存関係の種類の比較

npmにおける主な依存関係の違いを以下の表にまとめました。

項目dependenciespeerDependencies
主な用途ライブラリが直接内部で使用するモジュールプラグインが前提とするホストパッケージ
インストールの挙動自動的にそのパッケージの配下にインストールされる親プロジェクトの環境に存在することを要求する
依存の方向垂直方向 (親子関係)水平方向 (対等な関係)

Peer Dependenciesの具体的な記述方法

プラグインを開発する際、package.json に以下のような設定を追加することで Peer Dependencies を定義できます。

例えば、Chaiというアサーションライブラリのプラグインを作成する場合の設定例を以下に示します。

JSON
{
  "name": "my-chai-plugin",
  "version": "1.0.0",
  // ホストパッケージであるchaiへの依存を表明する
  "peerDependencies": {
    "chai": ">= 1.5.0 < 2"
  }
}

このように記述することで、このプラグインをインストールするユーザーに対し、適切なバージョンのChaiをインストールすることを促すことができます。

もしユーザーが互換性のないバージョンのホストパッケージを使用しようとした場合、npmは依存関係の競合としてエラーや警告を発生させます。

実行結果
npm ERR! peerinvalid The package chai does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer my-chai-plugin@1.0.0 wants chai@>= 1.5.0 < 2

運用上の注意点とバージョニングの指針

Peer Dependencies を使用する際には、「寛容なバージョン指定」を心がけることが非常に重要です。

特定のパッチバージョン (例: 1.5.2) に固定してしまうと、他のプラグインが要求するバージョンと競合しやすくなり、ユーザーに不必要な苦労を強いることになります。

基本的にはセマンティックバージョニング (SemVer) に従い、メジャーバージョンが同じであれば動作すると仮定して範囲を指定するのがベストプラクティスです。

具体的には "~1.0""1.x" といった記法、あるいは ">= 1.0.0 < 2" のような指定が推奨されます。

なお、npmのバージョンによって peerDependencies の挙動は変遷してきました。

npm v1、v2、および v7 以降では、依存ツリーに存在しない場合に自動インストールを試みますが、npm v3 から v6 までは警告を表示するのみの仕様となっていました。

開発者は自身の開発環境だけでなく、広く利用されることを想定して、どのnpmバージョンでも問題が起きないようなドキュメントを提供することも大切です。

まとめ

Peer Dependenciesは、Node.jsのプラグインエコシステムを健全に保つために欠かせない機能です。

標準の依存関係が「垂直な所有」を表すのに対し、Peer Dependenciesは「水平な共存」を定義します。

プラグイン開発者がこの仕組みを正しく理解し活用することで、ライブラリの重複を防ぎ、アプリケーション全体の安定性を向上させることができます。

もしあなたがプラグインやミドルウェアを公開するなら、peerDependencies を適切に設定し、ユーザーにとって使いやすいモジュールを提供しましょう。