日付を読みやすく表示する機能を追加しただけなのに、画面のファイルだけでなく、package.jsonpackage-lock.jsonが変わりました。ターミナルには、次のような結果も出ています。

added 83 packages, and audited 412 packages in 4s

自分で指定したのは一つのパッケージなのに、なぜ83個も増えたのでしょうか。

Next.jsのアプリは、ReactやNext.jsだけで動いているわけではありません。日付処理、入力検証、認証、テストなど、目的の異なるパッケージを組み合わせて作られています。パッケージを使えば、広く使われている機能を一から作らずに済みます。その代わり、追加したコードと、そのコードが必要とする別のコードも、アプリの一部として管理することになります。

npmがプロジェクトへパッケージを入れるまで

packageは、再利用できるJavaScriptやTypeScriptのコードと、名前、バージョン、依存関係などの情報をまとめたものです。公開されたpackageの多くは、npm registryという配布場所に置かれています。

npmは、registryから必要なpackageを探し、プロジェクトへ取り込むための道具です。ターミナルで使うnpm CLIと、packageが置かれるnpm registryは同じ名前で呼ばれますが、役割は違います。

たとえば、入力値を検証するzodを追加するとします。

npm install zod

この一行によって、通常は次の処理が行われます。

  1. package.jsonへ、プロジェクトがzodを必要としていることを記録する
  2. zodが必要とする別のpackageも含めて、使える組み合わせを計算する
  3. 決まったバージョンと入手先をpackage-lock.jsonへ記録する
  4. 実際のファイルをnode_modulesへ配置する
  5. packageにinstall scriptがあれば、決められたタイミングで実行する

コマンドを一つ実行しても、変更される場所は一つではありません。package.jsonpackage-lock.jsonnode_modulesは、同じ依存関係を別の役割で表しています。

package.jsonは、プロジェクト側の意思を書く

package.jsonには、プロジェクトの名前、実行できるコマンド、必要なpackageなどが書かれます。

{
  "name": "my-app",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "check": "eslint . && tsc --noEmit",
    "test": "vitest run",
    "build": "next build"
  },
  "dependencies": {
    "next": "16.1.0",
    "react": "19.2.0",
    "zod": "^4.0.0"
  },
  "devDependencies": {
    "typescript": "^5.9.0",
    "vitest": "^4.0.0"
  }
}

このファイルを見ると、zodを直接使うこと、開発時にはTypeScriptとVitestを使うこと、npm run checknpm run testというコマンドがあることが分かります。一方、zodが内部で必要とするpackageのすべてが並ぶわけではありません。

JSONは、末尾のカンマやコメントを許さない形式です。手で編集したあとに構文が壊れると、npmだけでなく、デプロイや開発ツールも設定を読めなくなります。

private: trueは、このプロジェクト自体を誤ってnpm registryへ公開しにくくする設定です。Webアプリは配布用packageではないことが多いため、確認しておきたい項目です。ただし、コードやSecretsを守る仕組みそのものではありません。Gitの公開範囲や環境変数は別に管理します。

scriptsに同じ名前でも、動く内容は同じとは限らない

scriptsは、長いコマンドにプロジェクト内の名前を付ける場所です。

{
  "scripts": {
    "check": "eslint . && tsc --noEmit",
    "test": "vitest run",
    "deploy": "next build && wrangler deploy"
  }
}
npm run check

この例では、ESLintによる確認が成功したあと、TypeScriptの型チェックが動きます。しかし、別のプロジェクトのcheckが同じ処理をするとは限りません。testが単体テストだけを指すこともあれば、DBを使う結合テストまで含むこともあります。

「checkが通った」「testを実行した」という報告を受けたら、コマンド名だけで判断せず、scriptsの中身を見ます。反対に、buildだけ通っていても、testlintが別scriptなら、それらを実行した証拠にはなりません。

scriptには、デプロイ、データベース変更、ファイル削除なども書けます。名前から安全そうに見えるscriptでも、初めて実行する前に実体を確認します。

package.jsonのscriptsに書かれたcheckとdeployが、それぞれ異なる検査や外部環境の変更を実行することを示す図

package-lock.jsonは、実際に選ばれた組み合わせを残す

package.jsonには、^4.0.0のように許可するバージョンの範囲を書くことがあります。その条件から実際に選ばれたバージョン、ダウンロード元、完全性を確認する情報、間接依存の関係まで記録するのがpackage-lock.jsonです。

同じpackage.jsonでも、数か月後には範囲内の新しいバージョンが公開されているかもしれません。lockfileがあれば、開発者のパソコン、CI、デプロイ先で、同じ組み合わせを再現しやすくなります。

package-lock.jsonはnpmが生成・更新するため、通常は一行ずつ手で直しません。しかし、Gitの差分では確認する必要があります。

  • 追加を頼んだpackage以外に、直接依存が増えていないか
  • 関係のないpackageが大量に更新されていないか
  • lockfile全体が削除され、作り直されていないか
  • npm以外のpackage managerへ変わっていないか
  • package.jsonと一緒にcommitされているか

lockfileは長いため、すべての行を暗記する必要はありません。まずpackage.jsonで直接依存の変更を把握し、そのpackageからどの間接依存が増えたかをlockfileでたどります。差分の大きさに対して理由が説明できるかを見るのが要点です。

エラーを直すためにlockfileを削除して作り直すと、問題のpackageだけでなく、多数の間接依存が新しい版へ変わることがあります。偶然エラーが消えても、どの変更が効いたのか分からず、別の不具合を持ち込む可能性があります。削除する前に、競合しているpackageとバージョンを特定します。

node_modulesは、インストールした結果そのもの

node_modulesには、npmが取得したpackageの実体が置かれます。ファイル数も容量も大きくなりやすく、通常はGitへcommitしません。package.jsonpackage-lock.jsonを使って作り直せる、生成結果だからです。

node_modules内のファイルを直接編集して不具合を直しても、再インストールすれば消えます。自分のパソコンでだけ直り、CIや別の開発者の環境では再現しません。package自体へ修正が必要なら、更新版へ上げる、上流へ修正を送る、管理されたpatchを適用するなど、インストールし直しても再現できる方法を選びます。

同じ理由で、node_modulesが手元に残っている状態だけで動作確認を終えると、宣言漏れを見逃すことがあります。コードで使っているpackageがpackage.jsonにないのに、以前のインストール結果が残っていたため動いているケースです。CIのクリーンな環境で失敗するなら、この違いを疑います。

自分のアプリが直接使うpackage AとBから、それぞれが必要とする間接依存C、D、Eへ枝分かれする依存ツリーの図

一つ追加しても、多くの間接依存が入る

package.jsonへ自分で追加したものを直接依存、そのpackageが必要とするものを間接依存、またはtransitive dependencyと呼びます。直接依存AがBとCを使い、CがさらにDを使うなら、アプリは結果としてA、B、C、Dを受け入れます。

added 83 packagesという表示は、自分が指定した一つに加え、その先の依存もインストールされたことを表します。83個という数字だけで危険とは判断できません。大きなフレームワークやビルドツールなら、多くの部品を必要とすることがあります。

一方、数行で済む処理のために巨大な依存ツリーが増えたなら、その選択が妥当かは見直せます。確認と更新の対象、インストール時間、ブラウザへ送る容量、脆弱性の入口が増えるからです。

間接依存は、自分のコードから直接importしていなくても影響します。脆弱性の報告やライセンスの条件が間接依存に見つかることもあります。直接依存を削除すると、そのpackageだけが必要としていた間接依存もlockfileとnode_modulesから消えます。

npm installnpm ciは、変更の目的が違う

npm install

npm installは、package.jsonの条件を満たす依存を入れます。packageを追加・更新するときにも使われ、必要に応じてpackage-lock.jsonが変わります。開発中に依存関係そのものを変更する場面に向いています。

npm ci

npm ciは、既存のpackage-lock.jsonどおりに、クリーンな依存関係を入れるためのコマンドです。現在のnode_modulesを取り除いてからインストールし、package.jsonとlockfileが一致していなければ、勝手に直さず失敗します。CIやデプロイで、commitされた組み合わせを再現したいときによく使われます。

ローカルのnpm installでは動くのにCIのnpm ciで失敗するなら、package.jsonだけを変更してlockfileを更新していない、または両者に矛盾がある可能性があります。CI側でnpm installへ変えて通すと、矛盾を隠し、毎回違う依存が選ばれる原因になります。

プロジェクトがpnpm、Yarn、Bunを使っている場合は、そのpackage managerとlockfileへ統一します。package-lock.jsonpnpm-lock.yamlyarn.lockが同時に増えたら、意図してpackage managerを移行したのか確認します。エージェントが手元で使いやすい道具へ勝手に変えるべき場所ではありません。

npm installは依存関係を追加・更新しlockfileを変えることがあり、npm ciはlockfileどおりにクリーンな環境を再現する違い

バージョンの記号は、更新を許す範囲を表す

1.4.2のような三つの数字は、一般にmajor、minor、patchと呼ばれます。semantic versioningに従うpackageでは、互換性を壊す変更をmajor、互換性を保った機能追加をminor、互換性を保った修正をpatchで表します。

  • 1.4.2:そのバージョンを指定する
  • ^1.4.2:通常はmajorが変わらない範囲の更新を許す
  • ~1.4.2:通常はminorが変わらない範囲のpatch更新を許す

記号は安全性の保証ではありません。package側の誤りで、patch更新に互換性のない変更が入ることもあります。反対に、major更新でも自分が使う範囲には影響しないことがあります。lockfileで実際に変わる版を固定し、変更履歴とテストで影響を確認します。

React向けのUIライブラリなどは、「React本体は利用するアプリ側で、この範囲の版を用意してください」という条件をpeerDependenciesに書くことがあります。条件が合わないと、次のような警告やエラーが出ます。

package-a requires react ^19

--force--legacy-peer-depsでインストールを進められる場合はありますが、互換性が生まれるわけではありません。動作時にhookのエラーが出たり、型が食い違ったりする可能性が残ります。対応版があるか、どちらのpackageを更新・据え置きするかを決めてからlockfileを変えます。

enginesには、対応するNode.jsやnpmのバージョンが書かれることがあります。ローカルとCIでNode.jsのmajorが違うと、インストールやビルドの結果が変わるため、実行環境の版も合わせます。

追加前に、便利さと引き換えに増えるものを見る

新しいpackageを提案されたとき、最初に確認するのは「何のために必要か」です。すでに入っているpackageやWeb標準のAPIで十分なら、新しい依存を持つ必要はありません。一方、暗号、HTMLのsanitize、認証のように、独自実装より検証された仕組みを選ぶべき分野もあります。

必要性が分かったら、次を確認します。

  • 公式サイトや公式リポジトリから正確なpackage名を確認できるか
  • 現在のNode.js、React、Next.jsに対応しているか
  • ブラウザとサーバーのどちらで実行されるか
  • ブラウザへ送るJavaScriptの量へどれだけ影響するか
  • 保守とセキュリティ対応が続いているか
  • install時にscriptを実行するか
  • どのライセンスで、利用目的に合うか
  • 同じ目的のpackageをすでに使っていないか

ダウンロード数やGitHubのstarは手がかりにはなりますが、それだけで安全とは言えません。よく知られたpackageに似せた名前もあります。READMEに書かれたinstallコマンドを無条件で使わず、配布元と名前を確認します。

特にinstall scriptは、アプリを起動する前、インストールした時点で動くコードです。ネイティブコードの準備など正当な用途もありますが、CIのsecretや開発者のファイルへ接触できる可能性があります。必要性が説明できないscriptや、出所の分からないpackageは実行しません。

更新は小さく分け、npm auditは影響まで読む

Next.js、React、認証ライブラリ、テスト環境を一度にmajor更新すると、失敗したときに原因を切り分けにくくなります。更新は目的ごとに分け、各段階で変更履歴と移行ガイドを読みます。

確認も、型チェックだけに寄せません。

  1. クリーンな状態で依存をインストールする
  2. lintと型チェックを実行する
  3. 単体・結合テストを実行する
  4. production buildを作る
  5. 認証、保存、表示など影響する操作を実際に試す

パッケージの更新目的を一つに絞り、差分、クリーンインストール、検査、主要操作の順に確認する手順

自動更新PRのCIが緑でも、外部サービスとの接続やブラウザでの挙動が検査に含まれていなければ、その部分は未確認です。何を実行した結果なのかをpackage.jsonとCI設定から読みます。

npm auditは、現在の依存ツリーに既知の脆弱性が報告されていないかを調べます。結果を読むときは、件数やseverityだけでなく、どの直接依存を通って入ったか、危険な機能へ外部入力が届くか、本番で使われるか、修正版へ上げると互換性が変わるかを確認します。

npm audit fix --forceは、major更新を含む変更を行う場合があります。警告が消えても、アプリが壊れたり、別のpackageと衝突したりすれば解決ではありません。修正候補の差分を先に出し、影響する機能を決めてから更新します。

auditで問題が0件でも、安全が証明されたわけではありません。まだ登録されていない脆弱性、悪意あるpackage、アプリ側の使い方、漏れたsecretは対象外です。auditは確認の一つであり、依存関係全体の保証ではありません。

削除するときも、コードと設定を一緒に片付ける

機能を消したあと、importだけを削除してpackageが残ることがあります。

npm uninstall パッケージ名

この操作でpackage.jsonとlockfileが更新されます。その後、コード内のimport、設定ファイル、環境変数、初期化処理、CIの処理が残っていないか検索します。逆に、packageだけを先に消すと、ビルド時に見つからないmoduleとして失敗します。

使わない依存を減らせば、更新、脆弱性、ライセンスを追う対象も減ります。ただし、lockfileから大量の間接依存が消えても、それが削除した直接依存だけにつながっているなら自然な差分です。何が増減したかだけでなく、どの直接依存からつながっているかを見ます。

パッケージ変更の差分を読む

変更を受け取ったら、まずpackage.jsonで直接依存とscriptsを確認し、次にlockfileで実際の版と間接依存を確認します。最後に、クリーンなインストールから必要な検査が再現するかを確かめます。

  • 追加、更新、削除した直接依存は何か
  • 各packageはどの機能で、ブラウザとサーバーのどちらから使うか
  • dependenciesdevDependenciesの置き場所は妥当か
  • scriptsにデプロイやDB変更などの新しい処理が入っていないか
  • major更新やpeer dependencyの警告を隠していないか
  • lockfile全体を理由なく作り直していないか
  • install script、ライセンス、保守状況を確認したか
  • 型、テスト、build、主要操作のどこまで確認したか

変更の理由と確認範囲を揃えるには、次のように依頼できます。

今回追加・更新・削除する直接依存を一覧にし、各packageの用途、実行場所、選んだ理由、バージョン変更を説明してください。既存packageや標準機能で代替できる場合は先に示してください。

package-lock.jsonを作り直さず、必要な依存だけを変更してください。間接依存を含め、意図しない更新やpackage managerの変更が起きる場合は、実行前に止めてください。

npm auditの結果を、severity、依存経路、本番での到達可能性、修正版、互換性への影響に分けてください。--forceは使わず、変更候補と確認方法を先に示してください。

クリーンな環境でlockfileどおりにインストールし、実行したlint、型チェック、テスト、buildをpackage.jsonのscriptと対応させて報告してください。未実施の確認は未実施と明記してください。

パッケージを一つ追加する変更は、そのファイルだけを受け取ることではありません。直接依存の先にあるコードを実行し、決まったバージョンを再現し、将来の更新と脆弱性にも対応する選択です。package.jsonで意図を読み、lockfileで実際の組み合わせを読み、クリーンな環境で同じ結果を作れるところまで確認すると、83個という数字に振り回されず、変更の大きさを判断できます。