JavaScript
npm

ちゃんと使い分けてる? dependenciesいろいろ。

More than 1 year has passed since last update.

npmはJavaScriptのパッケージマネージャです(今更感)。依存するライブラリはpackage.jsonに登録されていき、npm installだけで必要なライブラリが読み込まれます。と書くとシンプルな話ですが、実は結構奥が深いのです。そんなnpmのdependencies5種類、ちゃんと使い分けしてますか? 本稿では、順番に見ていきたいと思います。(npmのドキュメントも見てね)

  • dependencies
  • devDependencies
  • peerDependencies
  • optionalDependencies
  • bundledDependencies

dependencies

試しに新しいプロジェクトで、

$ npm install --save co

とすると、package.jsonに次のように登録されます。

{
  "dependencies": {
    "co": "^4.6.0"
  }
}

このpackage.jsonさえプロジェクトに含まれていれば、別環境で使う場合も前述の通りnpm installすれば、同じ環境を作れます。便利ですね。

ライブラリ自体はnode_modules内にダウンロードされていますから、それも確認してみましょう。 coというディレクトリができているはずです。

dependencies の dependencies の...

例として挙げた、coはシンプルなライブラリなので、他のライブラリへの依存を持っていません。でも、こういうライブラリは稀で、大抵なんらかのライブラリに依存しています。今度は、比較のためにexpressを入れてみます。

$ npm install --save express

すると、あらためてnode_modules内を確認すると、新たに40ディレクトリが作成されています...!

1462772221-06B0C38D-80BF-47FC-A091-8811F3E332A5.png

どういうことでしょうか? これは、expressが 依存しているライブラリも自動的にインストールしている からです。ほうほう。でもちょっと待って! expressのpackage.jsonを見ると25個しか書かれていませんよ? express自身を足しても14個足りない計算になります。

{
  "dependencies": {
    "accepts": "~1.2.12",
    "array-flatten": "1.1.1",
    "content-disposition": "0.5.1",
    "content-type": "~1.0.1",
    "cookie": "0.1.5",
    ...(そのほか合わせて25個の依存ライブラリ)
  }
}

答えは簡単です。expressが依存しているライブラリも、別のライブラリに依存しています。例えばaccepts2つのライブラリに依存しています。つまり、ライブラリのライブラリの...依存するすべてのライブラリが読み込まれて「40」という数字になったわけです。

{
  "dependencies": {
    "mime-types": "~2.1.11",
    "negotiator": "0.6.1"
  }
}

devDependencies

実のところ、ユーザとしてライブラリを使うには前項のdependenciesだけ知っていれば十分なんです。ただ、ライブラリをnpmに公開したり、別のプロジェクトに組み込んだりし始めるとそうもいかなくなってきます。

よく目にするのは、devDependenciesでしょう。例えば、テストのためにmochaを入れてみます。

$ npm install --save-dev mocha

すると、package.jsonに次のように追記されます。

{
  "devDependencies": {
    "mocha": "^2.4.5"
  }
}

先ほどとの違いは、dependenciesdevDependenciesかだけです。「テストやタスクランナー系はdevDependenciesに入れろ!」と教えられてきたと思いますが、それは半分だけ正解です。エンドユーザとしてアプリケーションを書く分には 別にどっちでもいいのです。

極端な話、dependenciesに書いてある全てをdevDependenciesに書き直しても、基本的には同じように動きます。逆もしかりです。(作成しているアプリケーションが、ライブラリとして使われる可能性がなければ)

※注: ただし、deploydのように、dependenciesに入っているものを問答無用でプラグイン扱いにする(強引な)実装のものもあります。その場合、devDependenciesにするかどうかは、アプリケーションだとしてもクリティカルです。

いつ使うべきか

では、どういった状況でdevDependenciesを使うのでしょうか? それは、ライブラリをdev(開発)するときです。前項で例に出したcoに依存関係はないと書きましたが、devDependenciesにはいくつかのライブラリが登録されています。

{
  "devDependencies": {
    "browserify": "^10.0.0",
    "istanbul-harmony": "0",
    "mocha": "^2.0.0",
    "mz": "^1.0.2"
  }
}

これらは、coをライブラリとして使う場合はインストールされませんが、coをGitでクローンしてnpm installした場合は、インストールされます。開発時のみに必要なライブラリで、実行時には役に立たないので、含める必要がないのです。

  • テストツール
  • ビルドツール / バンドルツール
  • タスクランナー

などが主な用途だけど、「ライブラリとして読み込む際はいらないもの」というのが、より正しい理解です。(時には、テストツールであってもdependenciesに入れるべきこともあります)

状況 インストールされるもの
ライブラリとしてnpm install co co
coのルートでnpm install browserify, istanbul-harmony, mocha, mz

peerDependencies

ほとんどのケースでは、dependenciesdevDependenciesだけ使い分ければOKです。ただし、次のケースではpeerDependenciesの利用を考える必要があります。

  • プラグインモジュール
  • シングルトンモジュール

前者は、ESLintのプラグインなどのケースです。単体では機能せず、ESLintのインストールが必須であることを明示したいケースがこれにあたります。プラグインについて、共通していることは次の点です。

  1. あるモジュールに対応するインターフェースを持っているが、あまりバージョンに依存しない
  2. あるモジュールについて、親となるアプリケーション(やライブラリ)が指定するバージョンを優先したい

プラグインモジュール

話が込み入ってくるので、表にしてみましょう。例えば、something-coolというライブラリがあったして、最新版が1.2.0、親アプリケーションでは固定バージョン1.0.0が要求されているとします。このケースでは、something-cool-pluginは、1.0.0でも1.2.0でも問題なく動きますが、依存性をどこに書くかで挙動が変わります。

  状況 親アプリケーション プラグイン
A dependencies"something-cool": "^1.0.0" 1.0.0 1.2.0
B peerDependencies"something-cool": "^1.0.0" 1.0.0 1.0.0

Aの状況で、プラグインに最新版のsomething-cool@1.2.0がインストールされることに注意してください。これは、npmが適合する最新版を使おうとするためです。親アプリケーションですでに1.0.0が使われていても関係ありません。あくまでもモジュールごとに最適なバージョンが判断されるため、別のバージョンが入ってしまうことがありえるのです。

別バージョンが入ってしまうと、アプリケーション本体と、プラグイン内で参照しているsomething-coolのバージョンが違うため、予想しない挙動になることは大いにありえます。危険ですね。

※注: バージョン固定の方法としては、npm shrinkwrapを使う手もあります

シングルトンモジュール

プラグイン以外のケースでもpeerDependenciesを必要とすることがあります。それは、あるモジュールが アプリケーション全体でひとつ であることが望まれる場合です。例として、筆者の遭遇したケースを引き合いに説明します。

  • riot-route: ルータ
  • riot-action: MV*フレームワーク的ななにか
  • my-application: アプリケーション本体

最初の2つは筆者がメンテナンスしているライブラリですが、当初riot-actionではdependenciesriot-routeが登録されていました。最新版で試している限りは特に問題ないのですが、riot-routeのバージョンが上がった時に問題は起きました。

前項の表と同様にmy-applicationは旧バージョンに固定されていたため、異なるバージョンのriot-routeがインストールされてしまったのです。riot-routeはルータなので、登録されたルーティング情報はアプリケーション全体から参照される必要があります(シングルトン)。が、複数のインスタンスがあっては挙動の予想がつきません。

この場合も、riot-actionpeerDependenciesriot-routeを登録することで、親アプリケーションのバージョンが優先されるようになります。(実際そのように変更しました)

peerDependencies と devDependencies の両方に書くというワザ

npmのバージョン2までは、peerDependenciesに指定がある場合も、自動的にインストールがされていましたが、バージョン3以降、ユーザが明示的にインストールするように仕様が変更になりました。

ただ、peerDependenciesに書いたものであっても、テストの際には必要になるかもしれません。そんなときは、devDependenciesにも書いておきましょう。そうすれば、プラグインの開発中は、自動的にdevDependenciesに従ってインストールされ、プラグインを利用する段にはpeerDependenciesが参照されます。

optionalDependencies

環境によって依存ライブラリが変わる場合に有効です。

$ npm install --save-optional fsevents

わかりやすい例は、chokidarです。chokidarfseventsに依存していますが、これはMacでしか使えません。fseventsをLinuxでインストールしようとしても"os": ["darwin"]指定があるのでインストールでこけます。dependenciesに書いてあると、その場でインストールが止まってしまいますが、optionalDependenciesに書けば、スキップしてくれます。インストール時に、

npm WARN optional Skipping failed optional dependency

とか出るけれど、気にしなくてOKです。ただし、読み込めなかったライブラリを使おうとすると例外が発生するので、プログラム中でケアしてあげる必要があるので要注意。chockidarでの実装例:

try { fsevents = require('fsevents'); } catch (error) {}

bundledDependencies

ちょうど、さきほど例に挙げたfsevents該当します

{
  "bundledDependencies": [
    "node-pre-gyp"
  ]
}

注意点としては、バージョン指定がないので文字列の配列として指定します。基本的には次のようなケースで使います(たぶん)。

  • npmにないライブラリを使う
  • 変更を加えたライブラリを使う

詳しくはStackOverflowの回答にゆずります。あんまり自分で使う機会はなさそう。

まとめ

以上、dependenciesの違いがあやふやになりそうな人(主に将来の自分)のために、書き留めておきます。

npmを使っていると、昨日動いたプログラムが今日動かないみたいなことは、しょっちゅうです。でも、バージョンの依存解決がどのように行われるかを理解していると、あまり動じなくて済みますね(希望)。

おまけ・ショートカット一覧

関連して、1日のタイプ量が劇的に減るショートカットの一覧を挙げておきます。

コマンド 省略版
npm install npm i
npm install --save npm i -S
npm install --save-dev npm i -D
npm install --global npm i -g