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ディレクトリが作成されています...!
どういうことでしょうか? これは、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が依存しているライブラリも、別のライブラリに依存しています。例えばaccepts
は2つのライブラリに依存しています。つまり、ライブラリのライブラリの...依存するすべてのライブラリが読み込まれて「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"
}
}
先ほどとの違いは、dependencies
かdevDependencies
かだけです。「テストやタスクランナー系は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
ほとんどのケースでは、dependencies
とdevDependencies
だけ使い分ければOKです。ただし、次のケースではpeerDependencies
の利用を考える必要があります。
- プラグインモジュール
- シングルトンモジュール
前者は、ESLintのプラグインなどのケースです。単体では機能せず、ESLintのインストールが必須であることを明示したいケースがこれにあたります。プラグインについて、共通していることは次の点です。
- あるモジュールに対応するインターフェースを持っているが、あまりバージョンに依存しない
- あるモジュールについて、親となるアプリケーション(やライブラリ)が指定するバージョンを優先したい
プラグインモジュール
話が込み入ってくるので、表にしてみましょう。例えば、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
ではdependencies
にriot-route
が登録されていました。最新版で試している限りは特に問題ないのですが、riot-route
のバージョンが上がった時に問題は起きました。
前項の表と同様にmy-application
は旧バージョンに固定されていたため、異なるバージョンのriot-route
がインストールされてしまったのです。riot-route
はルータなので、登録されたルーティング情報はアプリケーション全体から参照される必要があります(シングルトン)。が、複数のインスタンスがあっては挙動の予想がつきません。
この場合も、riot-action
のpeerDependencies
にriot-route
を登録することで、親アプリケーションのバージョンが優先されるようになります。(実際そのように変更しました)
peerDependencies と devDependencies の両方に書くというワザ
npmのバージョン2までは、peerDependencies
に指定がある場合も、自動的にインストールがされていましたが、バージョン3以降、ユーザが明示的にインストールするように仕様が変更になりました。
ただ、peerDependencies
に書いたものであっても、テストの際には必要になるかもしれません。そんなときは、devDependencies
にも書いておきましょう。そうすれば、プラグインの開発中は、自動的にdevDependencies
に従ってインストールされ、プラグインを利用する段にはpeerDependencies
が参照されます。
optionalDependencies
環境によって依存ライブラリが変わる場合に有効です。
$ npm install --save-optional fsevents
わかりやすい例は、chokidarです。chokidar
はfsevents
に依存していますが、これは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 |