npm とは

レジストリ

NPM はパッケージレジストリであり、主にブラウザ用のライブラリ、Node.js 用のライブラリが豊富に登録されている。NPM に登録されているパッケージは公式サイトで検索できる。

CLI

npm (node package manager) は Node.js のパッケージを管理するための CLI であり、パッケージを作成したり、NPM 上のパッケージをローカルにインストールしたり、自分のパッケージを NPM に公開したりと、Node.js の開発に欠かせないツールである。Node.js をインストールすると自動的に npm もインストールされる。

パッケージ

パッケージとはプログラムがたくさん入ったディレクトリであり、NPM で公開されているほとんどのパッケージは外から使うためのライブラリである。

パッケージを利用するとなったときに、「直接パッケージをダウンロードして自分のプロジェクトに含めればよいのではないか」、さらには「Git リポジトリに含めてよいのではないか」と思うかもしれない。しかし、もしそのパッケージに新しいバージョンが出て、バグ修正や機能の追加がされたとき、自分のプロジェクト内のパッケージもアップデートしたくなるかもしれない。そうすると自分のプロジェクト内のファイルを手動で更新しなければならなくなる。シェルを使えば簡単に更新できるかもしれないが、全く同じコードが複数の場所(今の場合 NPM と自分の Git リポジトリ)で管理されるというのは無駄であり、「本当に自分のプロジェクトに含まれているパッケージは NPM 上のパッケージと内容が一致しているのか」という懸念も生じる。

よって外部のパッケージは自分のプロジェクトに含めるのではなく、「このプロジェクトは NPM のこのパッケージに依存している」、という依存情報だけを「宣言」するのがよいということに落ち着く。このような依存先のパッケージを dependency と呼ぶ。NPM ではパッケージは別のパッケージに依存し、そのパッケージがまた別のパッケージに依存し...と、パッケージが dependency のネットワークを成すことになる。これは Maven、pip といった他の言語のパッケージレジストリについても同じである。

package.json

パッケージはpackage.jsonというファイルの親ディレクトリに含まれるファイル群である。例えばディレクトリ~/hoge/にpackage.jsonがあれば、~/hoge/がそのプロジェクトのルートディレクトリとなる。npm のコマンドは常にルートディレクトリで実行する。

依存パッケージを自分のパッケージに含めないと先に述べたが、実際には依存パッケージのファイルをローカルのどこかにダウンロードする必要がある。npm で依存パッケージをインストールすると、それらはルートディレクトリ直下のnode_modulesディレクトリにあくまで仮置場としてダウンロードされる。そのため、このディレクトリは.gitignoreで Git リポジトリから除外するのが普通であり、このディレクトリ内のファイルは編集してはいけない。

パッケージの作成

パッケージを一から作成するにはまずpackage.jsonを作成することから始まる(ただし、例えば React のようにプロジェクトを生成する CLI パッケージが用意されている場合は代わりにそれを用いればよい)。

以下を実行すれば、パッケージ名などがインタラクティブに質問されすべて答えるとpackage.jsonが生成される。質問をすべてスキップするには-yオプションをつければよい。
# 現在のディレクトリに package.json を生成する

npm init
この時点では dependency は何もない。

package.json

package.jsonは以下のようになっている。
{
  "name": "hoge",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
name, version, description, license などのデータは単なるパッケージのメタデータであり、パッケージを公開するつもりがないならばあまり気にする必要はない。機能的に重要なのは bin, main, dependencies, devDependencies, scripts であり、以下でそれぞれ説明する。

package.json で指定できるすべての項目はSpecifics of npm's package.json handlingで確認できる。

dependencies & devDependencies
先述の通り、dependency とはそのパッケージが依存する別のパッケージであり、package.json には dependency のパッケージ名とバージョンが書かれる。これらに変更を加える方法は後で説明する。

dependencies と devDependencies の違いであるが、意味としては前者は実行に必要なパッケージ、後者は開発やテストにのみ必要なパッケージである。機能的な違いとしては、あるパッケージ A を dependency としてインストールするときにデフォルトでは A の dependencies はインストールされるが A の devDependencies はインストールされない。

例えば JavaScript の Linter である ESLint は開発のときにのみ必要で実行するときには不要なため、devDep としてインストールするのが適切である。

package.jsonでは次のような形式で dependency が指定される。このときバージョンの先頭にキャレット^またはチルダ~がついていることが多い。
  "dependencies": {
    "express": "^4.17.1",
    "request": "~2.88.0"

  },
バージョンはドット('.')で区切られた 3 つの数字から構成されているが、これは Semver (Semantic Versioning) という規則に則っており、下の図のようにそれぞれの数字に意味がある。

 

 

Major

あるパッケージの新しいバージョンが公開されるとき、1つ目の数字 Major が上がっていれば「大きなAPIの変更があった」という意味である。Major が変われば使う側のコードもしばしば変更する必要があるだろう。

Minor

Minor が上がれば「新しい機能が追加された」という意味であり、使用する側のコードはおそらく変更する必要はない。Patch は「バグが修正された」という意味であり、これも自分のコードを変更する必要はない。

dependencies または devDependencies でバージョンを指定する時、キャレット^をつけると「Major は一致し Minor と Patch は指定されたもの以上」という意味になり、チルダ~をつけると「Major と Minor は一致し Patch は指定されたもの以上」という意味になる。例えば、^4.17.1は4.17.10や4.20.0にマッチするが4.16.8や3.20.4にはマッチしない。~2.88.0は2.88.5にマッチするが2.90.3にはマッチしない。逆に ^ も ~ もつけなければ ちょうどそのバージョンにのみマッチする。

後述の package-lock.json ファイルが存在せず、dependency がローカルにインストールされていない状態で npm install を実行すると、上記のルールにしたがい、package.json に指定されたバージョンにマッチする中で最も新しいバージョンがインストールされる。

NOTE: バージョンを固定したいときは後述のpackage-lock.jsonを使えばよいため、わざわざキャレットやチルダを外す必要はない。

scripts
scripts は簡単に言えばコマンドのエイリアスであり、任意のコマンド(i.e. コマンドラインのコマンド)に名前をつけることができる。例えば以下のような形である。

  "scripts": {
    "start": "node index.js",
    "lint": "eslint"
  },
ここに記載された script はnpm run <name>で実行できる。例えば上のlintはnpm run lintで実行できる。

ただしいくつかの名前は特別扱いされ、例えばstartは普通プログラムを実行するコマンドを指定し、npm startで実行できる。testはテストを実行するコマンドを普通指定し、npm testで実行できる。また、script 名の先頭にpreがついていると、ついてない名前の script が実行される前に自動的に実行される。例えば scriptbuildとprebuildが存在するとき、npm run buildを実行すると、buildの前にprebuildが自動的に実行される。逆にpostを先頭につけると元の名前の script の後に実行される。

preinstallとpostinstallはそのパッケージをインストールする前後で自動的に実行されるものであり、何らかのパッケージのインストールが失敗するときはそのパッケージのpreinstallやpostinstallをチェックすると解決することもある。

npm run <name>は簡単な task runnerとして使えるため、何度も実行するコマンドは script として登録すると開発を効率化できる。また、scripts はプロジェクトのテンプレートに最初から含まれていることが多い。例えば、react のプロジェクトをcreate-react-appで生成するとstartやbuildといった script が用意されており、すぐに開発やビルドができるようにセットアップされている。

main
main はそのパッケージを外からインポートするときにどの JavaScript ファイルが入り口であるかを指定するものである。誰かのパッケージを外から使うときに、そのパッケージをインポートするとは具体的にどのファイルをインポートするということなのかを確認するときに見ればよい。自分のパッケージのpackage.jsonの main は、そのパッケージを NPM で公開しない限り重要ではない。

例えば、HTTP リクエストライブラリである request というパッケージを下のようにインポートしたいとする。Node.js では外部の JavaScript ファイルをインポートするときrequireまたはimportを使う。下のコードで、このreqという変数には具体的に何が入るのかを調べたいとする。
const req = require('request')
ここで、request をインストールしたあとにその package.json (node_modules/request/package.json)を見ると、下のように main はindex.jsとなっている。

 

これは、index.jsがエクスポートした値が、require('request')の戻り値になることを表している。index.jsの中身を見ると以下のようになっている。

 

module.exportsにrequestという変数の値が代入されていることがわかる。

これはこのモジュールが変数 request の値をエクスポートしているということである。

この request という変数に代入されている値をコードをたどって調べれば、最初の変数reqに代入される値が分かる。

bin
パッケージを外から使うときにのみ重要になる項目である。

パッケージ A の package.json の bin に何らかの実行可能ファイルが指定されていると、パッケージ A をインストールすればそれを CLI として実行できるようになる。

例えば自分のパッケージの dependency としてパッケージ my-cli をインストールしており、scripts に同パッケージを実行するスクリプトを指定したとしよう。

{
  "dependencies": {
    "my-cli": "~1.0.0"
  },
  "scripts": {
    "foo": "my-cli 12345"
  }
}
my-cli の package.json が以下のようになっているとする。
{
  "bin": "./src/cli.js"
}
このとき、自分のパッケージで npm run foo を実行すると、node_modules/my-cli/src/cli.js 12345 が実行されることになる(実際にはこの cli.js ファイルを直接実行しているわけではなく、node_modules/.bin ディレクトリに自動生成された my-cli というファイル(内容は cli.js と同じ)を実行している)。

ただし、scripts からではなくコマンドラインで直接 my-cli を CLI として実行したいときは、
my-cli 12345
と実行することはできない。
なぜなら node_modules/.bin ディレクトリは PATH に登録されていないからである。
代わりに以下のように実行する必要がある。
./node_modlues/.bin/my-cli 12345

# または
npx my-cli 12345

一方、bin が指定されたパッケージをグローバルインストールするとコマンドラインから直接実行できるようになる。
例えば今の my-cli をグローバルインストール(npm install -g my-cli)すると、直接コマンドラインで my-cli 12345 のように実行することができる。グローバルインストールされたパッケージは、特定のローカルのパッケージの dependency としてインストールされるわけではなく、PC上のある決まった場所にインストールされるので注意されたい。

dependency の編集
すでにnpm installですべての dependency をインストールした状態で、dependency を追加・削除・アップデートしたいときは普通直接package.jsonを編集せず、npm を通じて行う。npm コマンドで dependency を変更すると自動的にpackage.jsonにも反映される。もしpackage.jsonを直接編集した場合は再度npm installを実行してnode_modules内のファイルを更新する必要がある。

Note: npm v4 以下では dependencies として追加するときに--saveを指定しないとpackage.jsonに反映されない。

dependency の追加/バージョン変更
devDep として追加する場合は-D(--save-devのエイリアス)を指定する。
# dependencies に追加

npm install <package>

# devDependencies に追加

npm install -D <package>
バージョンを指定したい場合は @ の後に書けば良い。
# react の v16.8.6 を追加

npm install react@16.8.6



# react の最新バージョンを追加

npm install react@latest
dependency の削除
npm uninstall <package>
package-lock.json
NOTE: 以降、dependencies と devDependencies をまとめて dependency と呼ぶ。

これは dependency のバージョンを lock(ロック、固定)するためのファイルであり、npm installの実行時に自動的に作成される。

一般に npm パッケージは他のチームメンバーの開発マシン、テスト環境、本番環境など複数の環境で実行されるため、すべての環境で全く同じバージョンの dependency をインストールしたいと思うのは自然である(バージョンが同じでなければ「ある環境ではうまく実行でき、ある環境ではエラーが出る」といったことが起こりうる)。

実は、package.jsonだけではこれは実現できない。キャレット^やチルダ~を使わなければいいのではないかと思うかもしれないが、問題はそう単純ではない。例えば自分のパッケージの dependency として"A": "1.2.0"と指定したとしよう。パッケージ A はまた別のパッケージ B に依存しており、"B": "^2.5.0"が指定されている。いま、B の最新が v2.8.0 とすると、npm installすれば A の v1.2.0、B の v2.8.0 がインストールされる。しばらくして B の新バージョン v2.9.0 がリリースされたとする。いま、別のマシンで自分のパッケージをnpm installすると A の v1.2.0 と B のv2.9.0がインストールされる。このように、同じpackage.jsonでもインストールされる dependency のバージョンが異なってしまうということが起こりうる。

これを解決するために npm v5 以降でpackage-lock.jsonが導入された。このファイルには dependency、dependency の dependency...と間接的なものも含めすべての dependency のバージョン(とその integrity)が記録される。

npm v6.9.0 時点ではnpm installの具体的な挙動は次のようになっている。

package-lock.jsonが存在しないとき

package.jsonに基づいて dependency がインストールされ、実際にインストールされたバージョンがpackage-lock.jsonに書かれる。

package-lock.jsonが存在するとき

package-lock.jsonに基づいてインストールされるが、package.jsonに指定されたバージョンとの矛盾があれば、package.jsonが優先され、実際にインストールされたバージョンがpackage-lock.jsonに書かれる。
package-lock.jsonを優先したい場合はnpm ciを実行すればよい。このコマンドはpackage-lock.jsonに基づいて dependency をインストールし、もしpackage.jsonとの矛盾があればエラーを出力する。また、インストール前に node_modules を削除するので、クリーンインストールしたいときにも使える。

npm install

npm install (パッケージ名なし)


パッケージ名を指定せず単に npm install を実行すると、package.json と package-lock.json に基づいて dependency がすべてローカル(node_modules)にインストールされる。

これを実行する必要があるのは、開発中のパッケージのソースコードだけ手元にある状態で dependency がインストールされていないときである。これに当てはまるのは例えば以下のケースである。

GitHub 上のパッケージを開発/実行したいので、新しいマシン上に clone してきた
create-react-app などの、新しくプロジェクトを生成するツールを使ってパッケージを作成した
そのツールが自動で npm install まで実行してくれることもある
node_modules 内のファイルを誤っていじってしまい、一旦 node_modules ごと削除した

npm install <パッケージ名>

パッケージ名を指定すれば、そのパッケージがローカルにインストールされる。これを使うのは、「dependency は既にすべてインストールされており、新しく dependency を追加したいとき」である。

GitHub でパッケージのコードを見るときの注意点
NPM パッケージは高確率でソースコードが GitHub でも公開されており、ローカルにインストールすることなくコードを確認したいときに便利である。パッケージのページの Repository の欄をクリックすれば該当の repo に飛ぶことができる。

 

先に述べたように、repo にあるコードがそのまま NPM にアップロードされているとは限らない。何らかのビルド処理をしたあと必要なファイルだけアップロードされることもよくあり、package.jsonのscriptsを見ればどのようなビルド処理をしたのか推測することもできる。

パッケージの repo はしばしば NPM でのバージョンに対応した tag がつけられており、特定のバージョンのソースコードを見たいときに便利である。

npm auditで脆弱性のある dependency があるか自動でチェックすることができるが、「報告されている」脆弱性しか考慮されないので安心はできない。

実際にはjsファイルとは限らず、cssファイルだけのライブラリなども存在する。

task runner とは複雑なビルド、テストなどの処理を行いやすくするツールのことであり、Node.js では gulp.js や Grunt などが有名である。