脆弱性の報告が続く時期は、npm audit を流すと赤い行がまとめて出てきます。ここで npm audit fix --force を打ってしまうと、範囲外のメジャー更新まで一気に入り、ビルドが通らなくなったり、通っても本番で一部の機能が黙って壊れたりします。逆に、件数の多さに押されて何もしないのも危険です。実際にこのサイトで、本番依存の警告9件を2件まで減らしたときの作業を順に並べると、やることは「本番に効くものを分ける」「範囲内で済むものと済まないものを分ける」「済まないものは変更点を読んでから上げる」の3段に収まりました。この記事ではその手順と、途中で踏みやすい落とし穴を紹介します。
1. まず本番で動く依存だけに絞る
npm audit をそのまま流すと、開発時にしか使わないビルドツールやテストツールの警告も同じ並びで出てきます。これらも直すに越したことはありませんが、公開中のサイトに対する危険度はまったく違います。最初に npm audit --omit=dev を流し、本番の依存だけの件数を出しておくと、急ぐべきものがはっきりします。このサイトの場合、本番依存に絞ると9件でした。次に見るのは、警告が直接の依存についているのか、依存の依存(推移的な依存)についているのかです。npm ls パッケージ名 で、どの依存がそれを引き込んでいるかが木の形で表示されます。直接の依存なら自分の package.json で直せますが、推移的な依存は引き込んでいる側の更新を待つか、後の節で扱う overrides で固定する必要があります。
2. 範囲内で直るか、範囲外の更新が要るかを分ける
次に、それぞれの警告が package.json に書いてある範囲指定のままで直るのか、範囲を書き換える更新が要るのかを分けます。これは npm が教えてくれます。npm audit の出力で「fix available via npm audit fix」と書かれているものは範囲内で直り、「npm audit fix --force」と書かれ、続けて breaking change と添えられているものは範囲外のメジャー更新が要ります。npm outdated も合わせて見ると分かりやすく、Wanted の列がいまの範囲指定で入る最新版、Latest の列が公開されている最新版です。修正が入った版が Wanted 以下なら範囲内、Latest にしかなければ範囲外です。範囲の読み方も押さえておくと判断が速くなります。^ は左端の0でない数字を変えない範囲、~ はマイナー番号まで固定してパッチだけを動かす範囲で、^0.2.3 のように先頭が0だと範囲が一気に狭くなり 0.3.0 は含みません。このサイトでは Next.js の指定が ^14.2.0 で、修正が入ったのは 15.5.24 以降だったため、範囲外の更新が要る側に分かれました。
3. 更新前にコミットし、範囲内のものを先に片付けて差分を読む
更新を始める前に、いまの package.json とロックファイルをコミットしておきます。戻れる地点になるうえ、あとで更新前後を比べるときの「前」になります。そのうえで、範囲内で直るものを npm audit fix(--force を付けない)でまとめて更新します。互換性を壊さない更新だけが入るので、先に済ませて件数を減らしておくと、残った難しいものに集中できます。このサイトでは、この段階で本番依存の警告が9件から4件に減りました。注意したいのは、このとき package.json は変わらず、書き換わるのはロックファイルだけだという点です。範囲の内側で版が上がるだけなので、package.json を見比べても差分は出ません。ロックファイルはそのまま差分にすると取得元とハッシュの書き換えで埋まってしまうので、Lockfile Diff の左右に更新前後の package-lock.json を入れ、版がどう動いたかを確かめます。直前のコミットの中身は git show HEAD:package-lock.json で取り出せます。このサイトの更新では、版が動いたのは7本で、マイナー1本・パッチ6本、すべて本番の依存でした。範囲内の更新でも、思っていない依存の依存まで動いていないかは、ここで一度見ておく価値があります。
この手順で使うツール
Lockfile Diff
package-lock.json の更新前後を比較 — 版の動きをメジャー/マイナー/パッチで分類
4. npm audit fix --omit=dev で開発依存が消える落とし穴
本番依存だけ直したいからと npm audit fix --omit=dev を実行すると、node_modules から開発依存がまとめて消えることがあります。このサイトで実際にこれが起き、TypeScript がいなくなったため、次のビルドでパスの別名(@/ で始まる import)の解決に失敗しました。エラーの文面は依存の問題に見えないので、原因にたどり着くまでに時間がかかります。--omit=dev はあくまで「本番に入る依存だけを入れる」という指定で、audit で件数を絞るときには便利ですが、fix と組み合わせると手元の開発環境まで本番向けの状態に作り替えてしまいます。もし踏んでしまったら、npm install をもう一度実行すれば開発依存は戻ります。fix は --omit=dev を付けずに実行し、件数の確認にだけ --omit=dev を使う、と分けておくのが安全です。
5. メジャー更新は、変更点を読んでから上げる
範囲外の更新が必要なものは、package.json の指定を書き換えてメジャー版を上げることになります。ここで必ず読むのが、そのパッケージの移行ガイドやリリースノートにある破壊的変更の一覧です。このサイトで Next.js を 14 から 15 に上げたときは、動的ルートの params が Promise に変わり、受け取る側で await が必要になりました。あわせて React も 18 から 19 に上がり、型定義からグローバルの JSX 名前空間がなくなったため、React.JSX と書き直す箇所が出ました。どちらもビルドが型エラーで止めてくれたので気づけましたが、型で検出できない変更、たとえばキャッシュの既定値の変更などは、ビルドが通っても挙動だけが変わります。移行ガイドに挙がっている項目を1つずつ、自分のコードで使っているかどうかを検索して確かめると、使っていない項目は安心して読み飛ばせます。
6. 依存の依存が古い版で固定されているときは overrides
直接の依存を最新にしても警告が消えないことがあります。引き込んでいるパッケージが、問題のある版を含む範囲で依存を指定している場合です。まず npm ls パッケージ名 で引き込んでいる側を特定し、その package.json に書かれた指定(node_modules の中にあります)を確かめます。その指定に修正版が入るなら、npm update パッケージ名 で上げられます。入らないなら、引き込んでいる側の更新を待つか、overrides で差し替えるしかありません。この「親の指定に修正版が入るか」は npm audit が直接は教えてくれないので、Semver Calculator の範囲チェックに親の指定と修正版の番号を入れて確かめます。このサイトでは、Next.js 15.5.28 が postcss を 8.4.31 という範囲なしの固定で指定していて、修正版の 8.5.23 は入りませんでした。そこで package.json の overrides で、Next.js の中の postcss を ^8.5.23 に差し替えました。overrides は、引き込んでいる側が想定していない版を押し込む操作なので、差し替えがマイナーかパッチの更新で、破壊的変更がないことを確かめてから使ってください。書いたら npm ls で実際にその版が入っているかを確認し、なぜ入れたかをコミットメッセージに残しておくと、後で外すときの判断材料になります。
この手順で使うツール
Semver Calculator
セマンティックバージョンのパース・バンプ・比較・範囲チェック
7. 成功したかは終了コードで、最後は本番で確かめる
更新後のビルドが通ったかどうかを、出力の最後の数行で判断するのは危険です。npm run build | tail のようにパイプでつなぐと、表示される終了コードは tail のものになり、ビルドが失敗していても0になります。このサイトでも、これでビルドの失敗を3回続けて成功と読み違えました。npm run build > build.log 2>&1 のようにファイルへ書き出してから終了コードを見るか、シェルの pipefail を有効にしてから実行すると、本当の結果が分かります。ビルドが通っても、それで終わりではありません。このサイトでは、ビルドも型チェックも通っている状態で、ツールごとのOGP画像が0バイトで返っていました。画像を描くライブラリが一部のCSS指定を受け付けていなかったのが原因で、エラーはどこにも出ず、画像のURLを実際に開いて大きさを測って初めて分かりました。更新のあとは、主要なページと、画像やAPIのように見た目で分かりにくい出力を、本番で一度ずつ確かめてください。
まとめ
依存の脆弱性は「本番依存に絞る → 範囲内で直るかを npm audit と npm outdated で分ける → コミットしてから範囲内のものを先に更新し、ロックファイルの差分を読む → 範囲外のものは移行ガイドを読んでから上げる → 依存の依存は overrides で固定する → 終了コードと本番で確かめる」の順に進めると、壊さずに件数を減らせます。特に「範囲内の更新では package.json は変わらずロックファイルだけが変わる」「npm audit fix に --omit=dev を付けると開発依存が消える」「パイプの先の終了コードはビルドの結果ではない」の3点は、知らないと作業そのものより確認に時間を取られます。