LoginSignup
1585

More than 3 years have passed since last update.

【初心者向け】NPMとpackage.jsonを概念的に理解する

Last updated at Posted at 2019-06-24

概要

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 は下の図のようになっている。

npm-network-express.png
(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 リポジトリから除外するのが普通であり、このディレクトリ内のファイルは編集してはいけない

これ以外には .npmrcnpm-shrinkwrap.json、後述の package-lock.json などの特別なファイルがある。.npmrcnpm-shrinkwrap.json はとりあえず必要ないのでここでは説明しない。

以上のファイル、ディレクトリ以外についてはどのようにファイルをおいてもよい(当然、使用するフレームワークなどによってはファイルの配置に制約を受けることはある)。

パッケージの作成

Note: 以降、Node.js と npm (v6 以上)が正常にインストールされておりそれぞれコマンドラインでnodenpmという名前でアクセスできる(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) という規則に則っており、下の図のようにそれぞれの数字に意味がある(無論、このルールに従うのは開発者の責任である)。「時間.分.秒」のように、順に細かくなっていくとイメージすればよい。

fig-semver.png

あるパッケージの新しいバージョンが公開されるとき、1つ目の数字 Major が上がっていれば「大きなAPIの変更があった」という意味である。Major が変われば使う側のコードもしばしば変更する必要があるだろう。Minor が上がれば「新しい機能が追加された」という意味であり、使用する側のコードはおそらく変更する必要はない。Patch は「バグが修正された」という意味であり、これも自分のコードを変更する必要はない。

dependencies または devDependencies でバージョンを指定する時、キャレット^をつけると「Major は一致し Minor と Patch は指定されたもの以上」という意味になり、チルダ~をつけると「Major と Minor は一致し Patch は指定されたもの以上」という意味になる。例えば、^4.17.14.17.104.20.0にマッチするが4.16.83.20.4にはマッチしない。~2.88.02.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>で実行できる。例えば上のlintnpm run lintで実行できる。

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

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

npm run <name>は簡単な task runner3として使えるため、何度も実行するコマンドは script として登録すると開発を効率化できる。また、scripts はプロジェクトのテンプレートに最初から含まれていることが多い。例えば、react のプロジェクトをcreate-react-appで生成するとstartbuildといった 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となっている。

ss-request-packagejson.png

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

ss-request-export.png

module.exportsrequestという変数の値が代入されていることがわかる。これはこのモジュールが変数 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-clipackage.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.jsonpackage-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 に飛ぶことができる。

ss-npm-express.png

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

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

ss-express-tags.png


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

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1585