PR

npm i をなんとなく使ってる人のための詳細解説

スポンサーリンク

この記事では、npm i(npm install)が内部で何をしているのかを、
「普段なんとなく使ってる人」向けに できるだけ丁寧に・深掘りして 解説します。


スポンサーリンク

この記事の対象読者

  • npm i を毎日打ってるけど、正直よく分かってない
  • package.json / package-lock.json の違いが曖昧
  • node_modules がなぜあんなに重いのか気になる
  • CI や Docker での npm i の挙動を理解したい

スポンサーリンク

npm i でまず起きること(全体像)

npm i を実行すると、ざっくり以下の流れが走ります。

  1. package.json を読む
  2. package-lock.json を読む(あれば)
  3. 依存関係ツリーを解決する
  4. パッケージをダウンロードする
  5. node_modules に配置する
  6. ライフサイクルスクリプトを実行する

ここから一つずつ細かく見ていきます。


スポンサーリンク

① package.json を読む

npm はまず package.json を読みます。

{
  "dependencies": {
    "react": "^18.2.0"
  }
}

ここで見ている主な項目

  • dependencies
  • devDependencies
  • optionalDependencies
  • peerDependencies
  • scripts
  • engines(node のバージョン制約)

📌 まだこの時点ではインストールは始まっていないです。


スポンサーリンク

② package-lock.json を読む(超重要)

package-lock.json がある場合、npm はこちらを 最優先 で使います。

なぜ lock ファイルが重要?

"react": {
  "version": "18.2.0",
  "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
  "integrity": "sha512-..."
}

ここには

  • 正確なバージョン
  • 取得元 URL
  • 改ざん検知用ハッシュ
  • 依存の依存(孫依存)

がすべて固定で書かれています。

📌 npm i = 「lock ファイル通りに再現する」コマンド


スポンサーリンク

③ バージョン解決(^ や ~ はどう扱われる?)

package.json では、次のような指定をよく見かけます。

"react": "^18.2.0"

この ^~バージョン範囲指定 と呼ばれ、
npm が「どこまでのバージョンを許可するか」を判断するためのルールです。

npm はこれを semver(Semantic Versioning) という考え方に基づいて解釈します。


semver(Semantic Versioning)とは?

バージョン番号は基本的に次の形をしています。

MAJOR.MINOR.PATCH
例: 18.2.0

それぞれの意味は以下の通りです。

要素意味変更内容
MAJORメジャー破壊的変更(API が壊れる)
MINORマイナー機能追加(後方互換あり)
PATCHパッチバグ修正

📌 npm は 「MAJOR が変わる=壊れる可能性が高い」 と判断します。


^(キャレット)の意味

"react": "^18.2.0"

これは npm 内部では次のように解釈されます。

18.2.0 以上、19.0.0 未満

つまり:

  • 18.2.1 → OK
  • 18.9.0 → OK
  • 19.0.0 → NG

^ は次の意味を持っています。

「MAJOR を固定したまま、最新を使いたい」

^18.2.0
 ↑
 MAJOR を固定

React のように
メジャーバージョンアップで破壊的変更が起きやすいライブラリでは、
この指定がもっとも一般的です。


~(チルダ)の意味

"react": "~18.2.0"

これは次の範囲を意味します。

18.2.0 以上、18.3.0 未満

つまり:

  • 18.2.1 → OK
  • 18.2.9 → OK
  • 18.3.0 → NG

~ は次のように解釈されます。

~18.2.0
   ↑
 MINOR を固定

👉 「バグ修正だけを取り込みたい」
という、より安全寄りな指定です。


^ と ~ の違いまとめ

指定実際の許可範囲特徴
^18.2.0< 19.0.0わりと積極的
~18.2.0< 18.3.0安定重視
18.2.018.2.0 のみ完全固定

0.x 系ライブラリの特殊ルール

semver では 0.x 系は不安定版扱い です。

"some-lib": "^0.2.3"

この場合、npm は次のように解釈します。

0.2.3 以上、0.3.0 未満

MAJOR が 0 の間は
MINOR アップでも破壊的変更が起きうる
と判断されるためです。

📌 ^0.x は、実質 ~ に近い挙動になります。


npm i 実行時に起きている実際の処理

npm i を実行すると:

  1. package.json の ^18.2.0 を読む
  2. npm registry に存在する全バージョンを確認
  3. 条件を満たす中で 最も新しいバージョン を選択
  4. その結果を package-lock.json に固定
package.json  : ^18.2.0
実際に入る   : 18.2.1
lock に固定  : 18.2.1

📌 次回以降の npm i は ^ や ~ を見ない
📌 package-lock.json の内容をそのまま再現する


「npm i したら勝手にバージョンが上がった」の正体

以下の状態だと、依存関係の再解決が走ります。

  • package-lock.json が存在しない
  • lock ファイルを削除した
  • Git 管理されていない

この場合 npm は:

^ や ~ を再解釈し、
その時点での最新バージョンを選択

という挙動になります。

依存の依存も全部解決する

your-app
 └─ react@18.2.0
    └─ loose-envify@1.4.0
       └─ js-tokens@4.0.0

この ツリー構造の依存関係をすべて解決 します。

📌 ここが npm i の中でも一番頭を使っている部分


スポンサーリンク

④ パッケージのダウンロード

npm は以下の場所からパッケージを取得します。

優先順位

  1. ローカルキャッシュ(~/.npm)
  2. npm registry

実体は .tgz

react-18.2.0.tgz

という tar.gz ファイル が落ちてきます。

📌 node_modules に直接落としているわけではない


スポンサーリンク

⑤ node_modules に配置される仕組み

昔(npm v2 以前)

node_modules/
 └─ A
    └─ node_modules/
       └─ B
          └─ node_modules/
             └─ C

今(npm v3+)

node_modules/
 ├─ A
 ├─ B
 └─ C

可能な限り フラット化 されます。

なぜnpm iは重い?

  • JS ファイル数が多い
  • 同じ依存が複数バージョン存在
  • OS のファイルシステム負荷

スポンサーリンク

⑥ ライフサイクルスクリプト(npm i が裏で勝手に実行している処理)

npm i を実行すると、
依存関係を入れるだけでなく、スクリプトも自動で実行されます。

これを npm ライフサイクルスクリプト と呼びます。


ライフサイクルとは?

package.json の scripts に書かれた処理のうち、
npm が決められたタイミングで自動実行するものです。

{
  "scripts": {
    "preinstall": "echo preinstall",
    "install": "echo install",
    "postinstall": "echo postinstall"
  }
}

npm i を実行すると、何も指定していなくても
次の順番で動きます。

preinstall
↓
install
↓
postinstall

📌 ユーザーが npm run しなくても実行されるのがポイント


代表的なライフサイクルスクリプト

スクリプト実行タイミング
preinstallインストール前
installインストール中
postinstallインストール後
prepublish / preparepublish や install 前後
preuninstall / postuninstall削除時

なぜライフサイクルが存在するのか?

主な理由は以下です。

  • ネイティブモジュールのビルド
  • 環境ごとの初期セットアップ
  • 自動コード生成

代表例が node-gyp を使うライブラリです。

bcrypt
sharp
canvas

これらは:

  • npm i
  • postinstall で C/C++ のビルド
  • 環境依存のバイナリが生成される

📌 「npm i が遅い」「CI で落ちる」原因の多くはここ


セキュリティ的な注意点

ライフサイクルスクリプトは:

  • 任意のコマンドが実行可能
  • npm i しただけで動く

つまり理論上は:

"postinstall": "rm -rf ~"

のようなことも可能です。

📌 実際に 悪意のある postinstall が問題になった事例もある
📌 企業環境では --ignore-scripts を使うこともある

npm i --ignore-scripts

「npm i が失敗する」時の典型原因

  • postinstall 内で node のバージョンが合わない
  • Python / make / gcc が入っていない
  • OS 依存のビルド失敗

👉 エラーログに gyppostinstall が出てきたら、
ほぼライフサイクルが原因


スポンサーリンク

pm ci とは何か?(npm i との正確な違い)

npm cinpm i と同様に、
package-lock.json に記載された正確なバージョンのパッケージをインストールします。

その点だけを見ると、両者はよく似ています。
しかし、実際の動作には 明確で重要な違い があります。


package-lock.json が必須

npm cipackage-lock.json の存在を前提 としたコマンドです。

  • package-lock.json が存在しない
  • lock ファイルを削除している

この状態で npm ci を実行すると、即エラーになります。

npm ERR! package-lock.json is required

📌 npm i のように
📌 「なければ再解決して作る」という動きはしません。


package.json と package-lock.json の不整合は許されない

npm ci は、次の状態もエラーとして扱います。

  • package.json に依存が追加されている
  • しかし package-lock.json が更新されていない

つまり:

package.json と package-lock.json が完全に一致していること
が実行条件です。

依存定義にズレがある
↓
npm ci は即失敗

📌 これは「多少ズレてても直してくれる」
📌 npm i とは真逆の思想です。


node_modules の扱いが決定的に違う

ここが、挙動の一番大きな違いです。

npm i の場合

  • 既存の node_modules を残す
  • 足りないものだけを追加
  • 条件次第で再利用する

👉 柔軟だが、状態に依存しやすい


npm ci の場合

  • 既存の node_modules を 完全に削除
  • 毎回ゼロからインストール
  • 再利用は一切しない
node_modules 削除
↓
package-lock.json 通りに再構築

👉 常にクリーンな状態が保証される


npm ci は何を目的にしたコマンドか

npm ci は一言で言うと:

再現性を最優先したインストール

そのために:

  • lock ファイル必須
  • 不整合は即エラー
  • node_modules は毎回破棄

という 厳しいルール が設けられています。


なぜ CI 環境で npm ci が使われるのか

CI(ビルド・テストの自動実行)では:

  • 毎回同じ依存関係であること
  • 昨日動いたものが今日も動くこと

が何より重要です。

npm i のように:

  • 状態に依存する
  • 微妙に結果が変わる

挙動は、CI 環境では致命的になります。

👉 そのため、

  • ローカル開発 → npm i
  • CI / 自動化 → npm ci

という使い分けが定着しています。

スポンサーリンク

npm i が失敗する典型パターン

よくある原因

  • node バージョン違い
  • package-lock.json 不整合
  • ネットワーク
  • postinstall の失敗

デバッグのコツ

npm i --verbose

スポンサーリンク

まとめ(npm i を一言で言うと)

npm i は

「依存関係の完全再現 + 配置 + スクリプト実行」

を一気にやるコマンド

なんとなく打ってた npm i も、
中身を知るとトラブル対応が一気に楽になります。

コメント