[Node.js] npm installするときはどのパッケージもローカルインストールで十分

npmではパッケージをインストールする際、-gオプションを付けることでグローバルインストールできます。Web上の色々な記事を見ても、グローバルインストールを使って解説しているものが多く見られます。

ただ、個人的には、たとえコマンドを利用するパッケージでも、ほとんどのパッケージはローカルインストールしたほうが良いと考えています。その理由と、ローカルでどのようにパッケージを扱う方法について記述します。

グローバルインストールをするのは、コマンドのため

なぜグローバルインストールをするのでしょうか。最大の理由は、パスを通してコマンドとして実行できるようにするためです。

> npm install -g @babel/core @babel/cli

グローバルインストールしたことで自動的に実行ファイルへのパスが通り、

> babel --version
7.0.0-beta.38 (@babel/core 7.0.0-beta.38)

このようにコマンドでパッケージを実行することができるようになります。

グローバルインストールのデメリット

コマンドを扱う場合には便利なグローバルインストールですが、デメリットもあります。

package.jsonに記載されない

グローバルインストールしたパッケージはローカルのpackage.jsonに載らないので、複数人で開発する場合や別端末に環境を移行する場合などに、グローバルにも依存関係があることを別途伝えなければなりません。

いざ実行してみると動かない、そういえばグローバルにあのパッケージも必要だった…と、トラブルになる可能性が高いです。

バージョン違いに対応できない

同じパッケージでも、プロジェクトによって異なるバージョンを前提に開発することがあり、グローバルインストールでは対応できないおそれがあります。

バージョンがちょっと変わるだけで、それまで動作していたものが動作しなくなったというのは日常茶飯事ですので、開発時にはパッケージのバージョンをきっちり合わせることは基本中の基本です。

プロジェクトに合ったバージョンを指定して、パッケージをグローバルインストールしなおすという対応はできますが、いちいち複数のプロジェクト間で切り替えるというのは手間が増え、とても現実的ではありません。

ローカルインストールではコマンドが実行できない

では、同じ操作をローカルインストールで行った場合はどうでしょうか。

(もしグローバルインストールしたままなら)一度グローバルからパッケージをアンインストールしておきます。

> npm uninstall -g @babel/core @babel/cli

グローバルにBabelが無い状態にしてから、ローカルにBabelをインストールします。

> npm install --save-dev @babel/core @babel/cli

-gの代わりに--save-devを指定しています。インストールが完了したら、早速コマンドを実行してみます。

> babel --version
'babel' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

そのようなコマンドは無い、と怒られてしまいました。ただ、実行ファイルはグローバルインストールと同様に取得されているので、今度はパス付きで実行してみます。

> node_modules\.bin\babel --version
7.0.0-beta.38 (@babel/core 7.0.0-beta.38)

パスの付与は必要でしたが、ローカルインストールでもコマンドを実行することができました。とは言え、毎回パス付きでコマンドを実行するというのは、あまりにも面倒です。

--save-devオプションを付けると、package.jsonのdevDependenciesにインストールしたパッケージの情報が記載されます。--save-devオプション無しでnpm installだけだとdependenciesに記載されます。単にパッケージを利用するだけなら大差ありませんが、開発したパッケージを公開するなどして第三者が利用するときには違いがあります。
基本的には、開発時だけ使用して利用時には不要なパッケージは--save-devオプションを付けてdevDependenciesに記載しておく、と覚えておけば十分です。

npm scriptsならグローバルと同じコマンドを記述できる

npmには、あらかじめコマンドをpackage.jsonに定義しておき、簡単に呼び出せるようにした仕組みがあります。これをnpm scriptsと呼んでいます。

ローカルインストールしたパッケージでも、npm scriptsに記述するコマンドでは、グローバルインストールと同じコマンドでパッケージを実行することができます。

npm scriptsの記述はpackage.jsonのscriptsで行います。

{
  ...
  "scripts": [
    "babel": "babel --version"
  ],
  ...
}

npm initでpackage.jsonを新規作成した場合、はじめからscriptsという項目が設けられていると思いますが、ここにnpm scriptsを追加していきます。:の左が名前、右が実行するコマンドです。

ここではbabelという名前でbabel --versionを実行するnpm scriptを定義しました。なお、名前とコマンドが一致している必要はありません。

npm scriptsの実行は、npm run スクリプト名というコマンドで行います。teststartなど、一部のコマンドではrunを省略してnpm testのようなコマンドで実行できますが、それ以外はrunも必要です。

では早速、定義したnpm scriptを実行してみます。

> npm run babel

> test@1.0.0 babel C:\test
> babel --version

7.0.0-beta.38 (@babel/core 7.0.0-beta.38)

実行ファイルへのパスを含めることなく、ローカルインストールしたパッケージを実行することができました。これならいちいちパスを記述しなくて済むので、実用に堪えられます。

npm scriptsの長所と短所

npm scriptsを使う利点として、一度package.jsonにスクリプトを記述しておけば、同じコマンドを簡単に繰り返し実行できるという点が挙げられます。特に、多数のオプションを使う長いコマンドで威力を発揮します。

逆に、細かい動作の違いでもいちいちpackage.jsonを開いて編集しなければならないという点が、短所になることもあります。開いて書いて保存して実行という一連の作業も、積み重なるとなかなかの労力です。

また、コマンドの中でクォーテーションを使う必要がある場合、JSONでは文字列にダブルクォーテーションしか使えず、シングルクォーテーションは認められないため、

{
  ...
  "scripts": [
    "mycommand": 'some-bin "test test"'
  ],
  ...
}

このような記述はできません。JSONレベルでの文法違反となります。

{
  ...
  "scripts": [
    "mycommand": "some-bin 'test test'"
  ],
  ...
}

逆に外側をダブル、内側をシングルにすることは可能ですが、Windowsではコマンドのパラメータにクォーテーションを使う場合、ダブルクォーテーションを使わなければならないため、これも失敗します。

{
  ...
  "scripts": [
    "mycommand": "some-bin \"test test\""
  ],
  ...
}

Windowsでは最終的に、このようにして内側もエスケープしたダブルクォーテーション使わなければならないため、少し読みづらくなります。

コマンドの実行はOSに依存するところがあるため、同じnpm scriptsでも環境の違いで利用できないことがあります。

新コマンドnpxでもローカルのパッケージを実行可能

npm5.2.0からはnpxというコマンドが実行できるようになっています。私の環境は5.6.0なので、既にnpxが利用可能です。

npxの利点は、ローカルインストールしたパッケージでも、グローバルインストールしたときのようにそのままコマンドを実行できることです。

> npx babel --version
C:\test\node_modules\@babel\cli\bin\babel.js
7.0.0-beta.38 (@babel/core 7.0.0-beta.38)

npxを頭に付ける必要がありますが、npm scriptsを記述しなくても、コマンドを実行することができます。

特に、package.jsonに"gulp": "gulp"のように一言だけのnpm scriptを記述している場合は、npxに乗り換えると、実行時のタイプ量やpackage.jsonの中身を減らすことができます。

npm5.2.0未満でもnpxパッケージを利用可能

npm5.2.0未満でも、npmのパッケージとして公開されているので、インストールすれば利用可能です。

> npm install -g npx

さすがにこのパッケージでは、グローバルインストールを使わざるを得ませんね。

まとめ

コマンドを利用するパッケージをローカルインストールした場合でも、手軽に利用するための十分な仕組みが備わっていますので、グローバルインストールに頼らずとも十分開発を進めることができます。

グローバルにたくさんのパッケージをインストールしている方は、これを機にローカルインストールに移行してみてはいかがでしょうか。同じ考えを持つ仲間が増えたらいいなと思っています。