概要
npm に初めて触れるときは、package.json がどういう役割をもっているのか、パッケージをインポートするとはどういうことなのかなど分からないことだらけであり、筆者も少しずつ調べては試すことを繰り返した記憶がある。これから Node.js を学ぼうという人にはこのような部分でつまづいてほしくないため、この記事では npm を使う上で必要な概念的知識を説明する。この記事を読めばスムーズに Node.js の学習が始められると思われる。
NPM とは
NPM と名のつくものは実は 2 つあり、ひとつはオンライン上のパッケージレジストリ、つまり世界中の開発者が作った Node.js のパッケージが集められた場所である。もう一つは Node.js に付属している、パッケージを操作するための CLI(コマンドラインインターフェイス; コマンドラインから実行できるプログラム)である。
以降、曖昧さを回避するためにパッケージレジストリの方を大文字の NPM、CLI の方を小文字の npm で表記する。
NPM(レジストリ)
NPM は現時点で他の言語のものを含め世界最大のパッケージレジストリであり、主にブラウザ用のライブラリ、Node.js 用のライブラリが豊富に存在する。NPM に登録されているパッケージは公式サイトでブラウジング、検索できる。
ちなみに: 「なんでブラウザ用のライブラリがNPMにあるの?」と思うかもしれない。確かに、document.querySelector("div")
みたいなコードは Node.js で実行することはできない。しかし jQuery や Vue などのライブラリは確かに NPM に存在する。この理由は、近年ではフロントエンドアプリ(ブラウザ上で実行されるアプリ)も、Webpack のような Node.js 上で動くツールを使って開発することが多いからである。
パッケージを利用する点で注意すべきなのは、誰でもパッケージを公開できるため安全であるという保証はないということである1。特に業務レベルでパッケージを使う際には、その dependency (そのパッケージが使用しているまた別のパッケージ)を含めて本当に信頼できるかを吟味する必要がある。しかし React などのある程度高機能なライブラリになると、dependency の dependency...とたどっていくと膨大なパッケージ数となり、とても手作業でチェックできるものではないという問題がある。実際には、「有名なライブラリなんだから dependency もちゃんと安全なのを使っているだろう」と信じて使っている人が多いだろう。
Note: ちなみにほとんどのパッケージは GitHub にソースコードが載せられているが、そのコードがそのまま NPM に登録されているという保証はどこにもなく、コードをチェックしたい場合は NPM から直接ダウンロードするべきである。
npm(CLI)
npm (node package manager) はその名の通り Node.js のパッケージを管理するための CLI であり、パッケージを作成したり、NPM 上のパッケージをローカルにインストールしたり、自分のパッケージを NPM に公開したりと、Node.js の開発に欠かせないツールである。Node.js をインストールすると自動的に npm もインストールされる。
ちなみに: 似たような CLI として Facebook が開発した Yarn がある。これは npm の色々な欠点(スピードなど)を補うように作られたものであり、かなり人気がある。npm パッケージの README でしばしば npm と yarn でインストールする方法が両方書かれていたり、時には「yarn を使用することを推奨する」と書かれていたりする。しかし、npm も改善されてきており、わざわざ yarn をインストールして使用するメリットはあまりないと筆者は考えている。特に初心者にとっては、スタンダードでないツールを使用すると無駄に学ぶことが増えるのでおすすめしない。
パッケージ
パッケージとはプログラムがたくさん入ったディレクトリ/フォルダーのようなもの2であり、NPM で公開されているほとんどのパッケージは外から使うためのライブラリである。世の中にあるパッケージを使えば自分で一からコードを書かなくとも高度な機能を実現することができる。
パッケージを利用するとなったときに、「直接パッケージをダウンロードして自分のプロジェクトに含めればよいのではないか」、さらには「Git リポジトリに含めてよいのではないか」と思うかもしれない。しかし、もしそのパッケージに新しいバージョンが出て、バグ修正や機能の追加がされたとき、自分のプロジェクト内のパッケージもアップデートしたくなるかもしれない。そうすると自分のプロジェクト内のファイルを手動で更新しなければならなくなる。シェルを使えば簡単に更新できるかもしれないが、全く同じコードが複数の場所(今の場合 NPM と自分の Git リポジトリ)で管理されるというのは無駄であり、「本当に自分のプロジェクトに含まれているパッケージは NPM 上のパッケージと内容が一致しているのか」という懸念も生じる。
よって外部のパッケージは自分のプロジェクトに含めるのではなく、「このプロジェクトは NPM のこのパッケージに依存している」、という依存情報だけを「宣言」するのがよいということに落ち着く。このような依存先のパッケージをこの界隈の言葉で dependency (depend=依存する)と呼ぶ。NPM ではパッケージは別のパッケージに依存し、そのパッケージがまた別のパッケージに依存し...と、パッケージが dependency のネットワークを成すことになる。これは Maven、pip といった他の言語のパッケージレジストリについても同じである。例えば express というパッケージの dependency tree は下の図のようになっている。
(from NPMGraph)
プロジェクト=パッケージ
Note: ここでいうプロジェクトとは業務上のプロジェクトという意味ではなく、コードのまとまりのことである。Visual Studio や Intellij のような開発環境でも「Open Project」みたいなボタンがあるがこれのことである。
通常 Node.js のプロジェクトは npm のパッケージ 1 個に対応する。つまり、プロジェクトを 1 つ開発するということは、公開するしないにかかわらずパッケージを 1 つ作り上げるということに相当する。よって Node.js で開発するためには、パッケージというものがどういう構造になっているのかを理解する必要がある。
npm にとってパッケージというのは**package.json
というファイルの親ディレクトリに含まれるファイル群**である。例えばディレクトリ~/projects/my-project/
にpackage.json
があれば、~/projects/my-project/
がそのプロジェクトのルートディレクトリ(一番根っこのディレクトリ)となる。npm のコマンドは常にルートディレクトリで実行することになる。
依存パッケージを自分のパッケージに含めないと先に述べたが、実際には依存パッケージのファイルをローカルのどこかにダウンロードする必要がある。npm で依存パッケージをインストールすると、それらはルートディレクトリ直下のnode_modules
ディレクトリにあくまで仮置場としてダウンロードされる。そのため、このディレクトリは.gitignore
で Git リポジトリから除外するのが普通であり、このディレクトリ内のファイルは編集してはいけない。
これ以外には .npmrc
や npm-shrinkwrap.json
、後述の package-lock.json
などの特別なファイルがある。.npmrc
と npm-shrinkwrap.json
はとりあえず必要ないのでここでは説明しない。
以上のファイル、ディレクトリ以外についてはどのようにファイルをおいてもよい(当然、使用するフレームワークなどによってはファイルの配置に制約を受けることはある)。
パッケージの作成
Note: 以降、Node.js と npm (v6 以上)が正常にインストールされておりそれぞれコマンドラインでnode
、npm
という名前でアクセスできる(PATH に登録されている)ことを前提とする。
プロジェクト、すなわちパッケージを一から作成するにはまずpackage.json
を作成することから始まる(ただし、例えば React のようにプロジェクトを生成する CLI パッケージが用意されている場合は代わりにそれを用いればよい)。
以下を実行すれば、パッケージ名などがインタラクティブに質問されすべて答えるとpackage.json
が生成される。質問をすべてスキップするには-y
オプションをつければよい。
# 現在のディレクトリに package.json を生成する
npm init
この時点では dependency は何もない。
package.json の中身
世の中のパッケージのpackage.json
は例えば以下のようになっている。.json
という拡張子からもわかるように、このファイルは JSON と呼ばれる形式に従っている。
{
"name": "my-package",
"description": "my first package ever",
"license": "MIT",
"version": "1.0.0",
"bin": "./cli.js",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"axios": "^0.18.0"
},
"devDependencies": {
"eslint": "^5.14.1"
}
}
name, version, description, license などのデータは単なるパッケージのメタデータであり、パッケージを公開するつもりがないならばあまり気にする必要はない。機能的に重要なのは bin, main, dependencies, devDependencies, scripts であり、以下でそれぞれ説明する。
package.json
で指定できるすべての項目はここで確認できる。
dependencies & devDependencies
先述の通り、dependency とはそのパッケージが依存する別のパッケージであり、package.json
には dependency のパッケージ名とバージョンが書かれる。これらに変更を加える方法は後で説明する。
dependencies と devDependencies の違いであるが、意味としては前者は実行に必要なパッケージ、後者は開発やテストにのみ必要なパッケージである。機能的な違いとしては、あるパッケージ A を dependency としてインストールするときにデフォルトでは A の dependencies はインストールされるが A の devDependencies はインストールされない。
例えば JavaScript の Linter である ESLint は開発のときにのみ必要で実行するときには不要なため、devDep としてインストールするのが適切である。(ただし、どちらに分類すべきか自明でない場合もある。例えば Webpack を用いてフロントエンド(ブラウザ)アプリを開発するときは、そもそもパッケージとして外から実行できるものではないため分類しがたい。この場合は、Webpack などのビルドツールは devDep として、jQuery などのフロントエンドに含まれるべきライブラリは dep として指定するのが普通である。)
自分のパッケージを NPM で公開するつもりならば、両者の違いに気をつける必要がある。公開しないとしても、どのパッケージが実行に必要なのかを意識し dep と devDep を使い分けることは、dependency を整理する上でも良い習慣だと思われる。
package.json
では次のような形式で dependency が指定される。このときバージョンの先頭にキャレット^
またはチルダ~
がついていることが多い。
"dependencies": {
"express": "^4.17.1",
"request": "~2.88.0"
},
そもそもバージョンは.
で区切られた 3 つの数字から構成されているが、これは Semver (Semantic Versioning) という規則に則っており、下の図のようにそれぞれの数字に意味がある(無論、このルールに従うのは開発者の責任である)。「時間.分.秒」のように、順に細かくなっていくとイメージすればよい。
あるパッケージの新しいバージョンが公開されるとき、1つ目の数字 Major が上がっていれば「大きなAPIの変更があった」という意味である。Major が変われば使う側のコードもしばしば変更する必要があるだろう。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 runner3として使えるため、何度も実行するコマンドは 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
ちなみに npx の使い道はこれだけではない。詳しくは👇を参照。
npm 5.2.0の新機能! 「npx」でローカルパッケージを手軽に実行しよう
一方、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 がつけられており、特定のバージョンのソースコードを見たいときに便利である。