npm installの仕組みとは?依存関係とpackage.json・package-lock.jsonの違いを解説!

JavaScriptの開発では、npm installで必要なパッケージをインストールします。

しかし、1つ追加しただけなのにnode_modulesに大量のフォルダができたり、package-lock.jsonというファイルが作られたりして、何が起きているのかわかりにくいこともあります。

また、別のパソコンでも同じパッケージを使いたいときに、どのファイルを共有すればよいのか迷うこともあるのではないでしょうか。

この記事では、次の内容を順に説明します。

  • npm installの基本的な動作
  • 「依存パッケージの仕組み」と「npm list --allによる確認方法」
  • package.jsonの役割
  • package-lock.jsonの役割
  • package.jsonとpackage-lock.jsonの違い
  • 別のパソコンで同じ依存関係をそろえる方法

npm installとは?

npm installとは?

npm installとは、プロジェクトで必要なパッケージをインストールするコマンドです。指定したパッケージだけでなく、その動作に必要な依存パッケージもインストールします。

例えば、日付処理に使うDay.jsを追加する場合は、プロジェクトのフォルダで次のコマンドを実行します。

npm install dayjs

上記のコマンドを実行すると、Day.jsがインストールされ、package.jsonのdependenciesに登録されます。登録される内容は、例えば次のようになります。

{
  "dependencies": {
    "dayjs": "^1.11.0"
  }
}

^1.11.0は「1.11.0以上、2.0.0未満の範囲にあるDay.jsを使う」という意味です。

なお、インストールされたパッケージのファイルは、プロジェクト内のnode_modulesに保存されます。今回の場合は、node_modules/dayjsに入ります。

node_modules内にdayjsのフォルダだけがある状態

あわせて読みたい

package.jsonのdependenciesについては、以下の記事で詳しく説明しています。興味のある方は下記のリンクからぜひチェックをしてみてください。

依存先のパッケージにも依存関係がある

先ほど紹介したDay.jsは、動作するために依存パッケージを必要としません。

そのため、繰り返しになりますが、Day.jsをプロジェクトにインストールすると、node_modulesに追加されるパッケージのフォルダは、dayjsの1個だけです。

node_modules内にdayjsのフォルダだけがある状態

一方、依存パッケージを使って機能を実現しているライブラリもあります。

この違いを見るために、HTTP通信に使うAxiosを追加してみましょう。

npm install axios

インストール後にnode_modulesを確認すると、axios以外のフォルダも追加されています。今回の確認では、Axiosを追加すると、node_modulesのパッケージのフォルダが27個増えました。

Axiosの追加後、node_modules内に多数のパッケージのフォルダが増えた状態

自分ではnpm install axiosとaxiosしか指定していないのに、なぜ27個も増えたのでしょうか。

まず、Axiosがどのパッケージに依存しているかを調べるために、node_modules/axios/package.jsonのdependenciesを確認してみましょう。

今回インストールしたAxios 1.20.0には、次の4個が書かれていました。

{
  "dependencies": {
    "follow-redirects": "^1.16.0",
    "form-data": "^4.0.6",
    "https-proxy-agent": "^5.0.1",
    "proxy-from-env": "^2.1.0"
  }
}

しかし、この4個にAxios自身の1個を足しても、合計は5個です。

実際には27個増えているので、残りの22個はどこから来たのでしょうか。

理由は、Axiosが使う依存パッケージも、さらに別の依存パッケージを必要としているからです。つまり、「依存先のさらにその依存先」があるのです。

例えば、Axiosの依存先の1つであるform-dataを見てみましょう。

今回インストールされたform-dataのpackage.jsonには、次の5個が書かれていました。

{
  "dependencies": {
    "asynckit": "^0.4.0",
    "combined-stream": "^1.0.8",
    "es-set-tostringtag": "^2.1.0",
    "hasown": "^2.0.4",
    "mime-types": "^2.1.35"
  }
}

Axiosを動かすにはform-dataが必要で、form-dataを動かすには、さらに上記の依存パッケージが必要なのです。

そして、この5個の中にも、別の依存パッケージを必要とするものがあります。

このように、依存関係はその先へと続いていきます。

npm list --allで依存関係を確認する

この依存関係のつながりは、次のコマンドでも確認できます。プロジェクトのフォルダで実行してみましょう。

npm list --all

npm listは、インストールされているパッケージを一覧表示するコマンドです。--allを付けると、自分で追加したパッケージだけでなく、その先の依存パッケージも表示されます。

以下は、npm list --allの実行結果からAxiosの依存関係を抜き出したものです。矢印と説明は、読み方を示すために付けています。

axios@1.20.0
├── follow-redirects@1.16.1
├─┬ form-data@4.0.6              ← 1. Axiosはform-dataを必要とする
│ ├── asynckit@0.4.0
│ ├─┬ combined-stream@1.0.8      ← 2. form-dataはcombined-streamを必要とする
│ │ └── delayed-stream@1.0.0     ← 3. combined-streamはdelayed-streamを必要とする
│ ├─┬ es-set-tostringtag@2.1.0
│ │ ├── es-errors@1.3.0
│ │ ├─┬ get-intrinsic@1.3.0
│ │ │ ├─┬ call-bind-apply-helpers@1.0.2
│ │ │ │ ├── es-errors@1.3.0 deduped
│ │ │ │ └── function-bind@1.1.2 deduped
│ │ │ ├── es-define-property@1.0.1
│ │ │ ├── es-errors@1.3.0 deduped
│ │ │ ├─┬ es-object-atoms@1.1.2
│ │ │ │ └── es-errors@1.3.0 deduped
│ │ │ ├── function-bind@1.1.2 deduped
│ │ │ ├─┬ get-proto@1.0.1
│ │ │ │ ├─┬ dunder-proto@1.0.1
│ │ │ │ │ ├── call-bind-apply-helpers@1.0.2 deduped
│ │ │ │ │ ├── es-errors@1.3.0 deduped
│ │ │ │ │ └── gopd@1.2.0 deduped
│ │ │ │ └── es-object-atoms@1.1.2 deduped
│ │ │ ├── gopd@1.2.0
│ │ │ ├── has-symbols@1.1.0
│ │ │ ├── hasown@2.0.4 deduped
│ │ │ └── math-intrinsics@1.1.0
│ │ ├─┬ has-tostringtag@1.0.2
│ │ │ └── has-symbols@1.1.0 deduped
│ │ └── hasown@2.0.4 deduped
│ ├─┬ hasown@2.0.4
│ │ └── function-bind@1.1.2
│ └─┬ mime-types@2.1.35
│   └── mime-db@1.52.0
├─┬ https-proxy-agent@5.0.1
│ ├─┬ agent-base@6.0.2
│ │ └── debug@4.4.3 deduped
│ └─┬ debug@4.4.3
│   └── ms@2.1.3
└── proxy-from-env@2.1.0

この表示から、次のつながりを読み取れます。

  • Axiosはform-dataを必要とする
  • form-dataはcombined-streamを必要とする
  • combined-streamはdelayed-streamを必要とする

このように、npm list --allを使うと、どのパッケージが何を必要としているのかを確認できます。

npmはこの依存関係をたどり、必要なパッケージをまとめてインストールします。

その結果、今回の確認では、「Axios自身」と「その先の依存パッケージ」を合わせて27個が追加されました。

このように、自分が追加するパッケージは1個でも、それを動かすために多数のパッケージがインストールされることがあります。

27個という数は、Day.js 1.11.23だけが入ったプロジェクトに、npm 10.9.3でAxios 1.20.0を追加したときの結果です。追加される個数は、バージョンや、すでにインストールされているパッケージによって変わります。

dedupedについて

例えば、先ほどの実行結果を見ると、es-errors@1.3.0 dedupedと書いてあります。

このdedupedは、必要なパッケージがすでにインストールされていて、条件も合っているため、それを共有して使っていることを示します。

例えば、複数のパッケージがes-errorsの1.3.0を必要としている場合、それぞれに別のコピーを用意するのではなく、インストール済みの1.3.0を共有して使えます。このような箇所に、es-errors@1.3.0 dedupedと表示されます。

つまり、同じ名前がツリーに何度も出てきても、その数だけパッケージのコピーが作られているとは限りません。

package.jsonとは?

package.jsonとは?

package.jsonとは、プロジェクトの名前やバージョン、使用するパッケージなどを記録する設定ファイルです。

例えば、「このプロジェクトではDay.jsというパッケージを使う」「Day.jsは1.11.0以上、2.0.0未満を使う」といった条件を記録できます。

次は、日付処理にDay.jsを使うプロジェクトの例です。名前やバージョンは説明用です。

{
  "name": "sample-app",
  "version": "1.0.0",
  "dependencies": {
    "dayjs": "^1.11.0"
  }
}

それぞれの項目は、次の内容を表しています。

  • name:自分のプロジェクトの名前。この例ではsample-app。
  • version:自分のプロジェクトのバージョン。この例では1.0.0。
  • dependencies:動作に必要なパッケージと、そのバージョンの条件。この例ではDay.jsの^1.11.0。

versionの1.0.0は、自分のプロジェクトのバージョンです。Day.jsのバージョンを指定しているのは、dependencies内の"dayjs": "^1.11.0"です。

package.jsonは設定を記録するファイルで、パッケージ本体が入っているファイルではありません。実際のファイルは、インストール後にnode_modulesへ保存されます。

新しいプロジェクトでpackage.jsonを作る場合は、プロジェクトのフォルダで次のコマンドを実行できます。

npm init -y

npm initはpackage.jsonを作成するコマンドです。-yを付けると、対話形式の質問を省略して初期設定で作成します。

npm initで作成される内容は初期設定です。Day.jsなどの依存パッケージは、このあとnpm installで追加します。

すでにpackage.jsonがあるプロジェクトでは、新たに作成する必要はありません。

バージョン番号とロックファイルを理解しよう

先ほど、Day.jsのバージョンを^1.11.0と指定しました。

この1.11.0はバージョン番号で、先頭の^は、どの範囲のバージョンを使ってよいかを指定する記号です。

まず、バージョン番号の数字から見てみましょう。

1.11.0のように3つの数字で表す場合、それぞれを次のように呼びます。

数字の位置名前1.11.0の場合
最初の数字メジャーバージョン1
真ん中の数字マイナーバージョン11
最後の数字パッチバージョン0

これらの数字は次のような変更に応じて増やします。

  • メジャーバージョン:これまでの使い方と互換性のない変更
  • マイナーバージョン:これまでの使い方を維持した機能追加
  • パッチバージョン:これまでの使い方を維持した不具合修正

例えば、1.11.0から1.11.1への変更はパッチバージョンの更新、1.12.0への変更はマイナーバージョンの更新です。

ここまでが、バージョン番号の数字の意味です。

次に、どこまでの更新を許可するかを指定する記号を見てみましょう。

バージョン番号は、そのまま書くことも、先頭に^や~を付けることもできます。通常の正式版では、それぞれ次の意味になります。

指定使えるバージョンの範囲意味
1.11.01.11.0だけバージョンを固定する
^1.11.01.11.0以上、2.0.0未満マイナー・パッチの更新を許可する
~1.11.01.11.0以上、1.12.0未満パッチの更新を許可する

例えば、^1.11.0なら、1.11.1や1.12.0は範囲内ですが、2.0.0はメジャーバージョンを上げているので範囲外です。

一方、~1.11.0なら、1.11.1は範囲内ですが、1.12.0はマイナーバージョンを上げているので範囲外です。

package.jsonではバージョンを範囲で指定できる

パッケージをインストールするときには、どのバージョンを使うかも重要です。

例えば、package.jsonに次の内容が記録されているとします。

{
  "dependencies": {
    "dayjs": "^1.11.0"
  }
}

^1.11.0は「1.11.0以上、2.0.0未満の範囲にあるDay.jsを使う」という意味です。

そのため、package-lock.jsonがない状態で、別のパソコンから次のコマンドを実行すると、指定した範囲の中からDay.jsが選ばれます。

npm install

バージョンを固定したい場合

Day.jsのバージョンを1.11.0に固定したい場合は、^を付けずに、次のように番号だけを書きます。

{
  "dependencies": {
    "dayjs": "1.11.0"
  }
}

この指定では、Day.jsの1.11.0を使います。

ただし、この方法で固定できるのは、ここで指定したDay.jsのバージョンです。依存先のパッケージまで、まとめて固定できるわけではありません。

例えば、仮にDay.jsがsample-dayという架空のパッケージを必要としているとします。"dayjs": "1.11.0"と書いても、sample-dayのバージョンまで指定したことにはなりません。sample-dayは、Day.js側で指定されたバージョンの範囲から選ばれます。

依存先も含めて、実際にインストールされたバージョンを記録するのが、この後に説明するpackage-lock.jsonです。別のパソコンでも同じバージョンを使えるように、通常はpackage.jsonとpackage-lock.jsonを一緒に管理します。

package-lock.jsonとは?

package-lock.jsonとは?

package-lock.jsonとは、依存先も含めて、実際に使うパッケージのバージョンや依存関係を記録するファイルです。

npm installの実行に応じて、プロジェクトのpackage-lock.jsonが作成・更新されます。

例えば、package.jsonに"dayjs": "^1.11.0"と書かれていて、実際に1.11.23が選ばれた場合、package-lock.jsonには、その具体的なバージョンが記録されます。

次は、package-lock.jsonからDay.jsのバージョンが記録されている部分だけを抜き出した例です。

{
  "packages": {
    "node_modules/dayjs": {
      "version": "1.11.23"
    }
  }
}

node_modules/dayjsは対象のパッケージを表し、その中の"version": "1.11.23"が、実際に使うDay.jsのバージョンを表しています。

package.jsonには^1.11.0という範囲が書かれていても、package-lock.jsonには1.11.23という具体的な番号が記録されます。

package-lock.jsonがある場合のnpm install

package-lock.jsonがあり、そこに記録されたバージョンがpackage.jsonの条件を満たしていれば、npm installはその記録を使ってインストールします。

例えば、次の組み合わせの場合、npm installすると、Day.jsは記録されている1.11.23がインストールされます。

ファイル記録されている内容
package.jsonDay.jsは^1.11.0の範囲を使う
package-lock.json実際に使うDay.jsは1.11.23

このため、package.jsonに^が付いていても、package-lock.jsonがあれば、毎回範囲内の最新版に入れ替わるわけではありません。

ただし、package.jsonの指定を変えて、記録されているバージョンが条件に合わなくなった場合は、npm installが条件に合うものを選び直し、package-lock.jsonも更新します。

package.json・package-lock.jsonの違い

package.jsonが「使ってよいバージョンの条件」、package-lock.jsonが「実際に使うバージョンの記録」と考えると、違いをつかみやすくなります。

ファイル主な役割Day.jsの記録例主な管理方法
package.json使うパッケージと、バージョンの条件などを指定する^1.11.0(1.11.0以上、2.0.0未満)開発者が設定する。パッケージの追加コマンドなどでも更新される
package-lock.json依存先も含めて、実際に使うバージョンや依存関係を記録する1.11.23などの具体的なバージョン手作業で書き換えるのではなくコマンドで管理する

例えば、package.jsonでDay.jsを^1.11.0と指定しても、実際に1.11.0が選ばれるとは限りません。1.11.23など、条件に合う別のバージョンが選ばれる場合があります。

そのときに選ばれた具体的なバージョンを、依存先も含めて記録するのがpackage-lock.jsonです。アプリで直接使うパッケージだけでなく、そのパッケージが必要とする依存先のバージョンも記録されます。

別のパソコンでも同じ依存関係をそろえる方法

別のパソコンでも同じ依存関係をそろえるために、通常はpackage.jsonとpackage-lock.jsonを一緒に管理します。例えば、Gitでプロジェクトを共有する場合は、この2つのファイルも共有します。

package-lock.jsonに記録された依存関係を再現するためのコマンドとして、npm ciがあります。

プロジェクトのフォルダで、次のコマンドを実行します。

npm ci

npm ciコマンドが行っている内容を箇条書きで以下に示します。

  • package-lock.jsonに記録されたバージョンや依存関係に従って、パッケージをインストールする。
  • node_modulesディレクトリが存在する場合は、npm ciによるインストールが始まる前に自動的に削除される。
  • package-lock.jsonをチェックする。
    • package-lock.jsonが存在しない場合にはエラーになる。
    • package-lock.jsonとpackage.jsonの間に不一致がある場合にはエラーになる。
  • npm ciコマンドを実行してもpackage-lock.jsonやpackage.jsonは更新しない。

本記事のまとめ

この記事では、npm installの仕組みと依存関係、package.json・package-lock.jsonの役割と違いを説明しました。

  • npm installは、指定したパッケージだけでなく、その動作に必要な依存パッケージもインストールする。
  • インストールされたパッケージのファイルは、通常node_modulesに保存される。
  • npm list --allで、依存先のさらに先まで含めた、パッケージ同士のつながりを確認できる。
  • package.jsonには使うパッケージやバージョンの条件を指定し、package-lock.jsonには依存先も含めて実際に使うバージョンや依存関係を記録する。
  • package.jsonとpackage-lock.jsonを一緒に管理し、記録された依存関係を変更せずにそろえたい場合はnpm ciを使える。

お読みいただきありがとうございました。

スポンサーリンク