セマンティックバージョニングと package.json のバージョン指定 - ^ と ~ の違い、0.x の罠

セマンティックバージョニングと package.json のバージョン指定 - ^ と ~ の違い、0.x の罠

作成日:
読了:8
更新日:
この記事を読む人におすすめPR / Amazonアソシエイト

当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。

package.json"react": "^18.2.0"^、なんとなく付けていませんか。この記号ひとつで「次の npm install でどのバージョンまで上がるか」が変わります。この記事では、セマンティックバージョニング(semver)と npm のバージョン指定を、semver.org・npm 公式・node-semver を一次ソースに整理します。

セマンティックバージョニングとは

バージョンは MAJOR.MINOR.PATCH の3つの数値。上げる基準は仕様で決まっています。

いつ上げるか
MAJOR後方互換性を壊す API 変更をしたとき
MINOR後方互換を保ったまま機能追加したとき
PATCH後方互換のあるバグ修正をしたとき

上位を上げたら下位はゼロに戻します(MINOR を上げたら PATCH は 0)。1.0.0 で「公開 API を定義する」とされ、以降この公開 API の変化に応じて番号を動かします。

0.y.z(初期開発版)の特別扱い

仕様には「メジャー0(0.y.z)は初期開発用で、いつ何が変わってもよい。公開 API は安定とみなすべきでない」と明記されています。この特例が、後述する ^ の挙動にも効いてきます。

プレリリースとビルドメタデータ

  • プレリリース版: 1.0.0-alpha1.0.0-rc.1 のようにハイフンで付ける。通常版より優先順位は低い1.0.0-alpha1.0.0 より前)
  • ビルドメタデータ: 1.0.0+20130313 のようにプラスで付ける。優先順位の比較では無視される

優先順位の例(仕様より): 1.0.0-alpha1.0.0-alpha.11.0.0-beta1.0.0-rc.11.0.0

キャレット ^ とチルダ ~

ここが本題です。npm の範囲指定で最頻出の2つを比べます(範囲の厳密な定義元は node-semver)。

指定許可される範囲上がる桁
^1.2.3>=1.2.3 かつ <2.0.0MINOR・PATCH
~1.2.3>=1.2.3 かつ <1.3.0PATCH のみ
  • ^(キャレット): 「いちばん左の0でない桁」を固定し、それ未満を更新。^1.2.3 なら MINOR と PATCH が上がる
  • ~(チルダ): MAJOR.MINOR.PATCH が揃っていれば PATCH のみ更新

つまり通常は ^ のほうが広く(MINOR まで)、~ のほうが狭い(PATCH のみ)

0.x の罠: ^ が ~ に縮退する

初学者が必ずハマるのがこれです。0.x では ^ の挙動が変わります(前述の「メジャー0は不安定」が反映されるため)。

指定許可される範囲実質
^0.2.3>=0.2.3 かつ <0.3.0PATCH のみ~0.2.3 と同じ!)
~0.2.3>=0.2.3 かつ <0.3.0PATCH のみ
^0.0.3>=0.0.3 かつ <0.0.4更新なし(実質固定)

^1.x は MINOR まで上げるのに、^0.2.3 は MINOR を上げません。「メジャー0の MINOR 更新は破壊的変更でありうる」という semver の考えに沿った挙動です。依存が 0.x のうちは ^ でも MINOR は上がらない——これを知らないと「なぜ更新されない/されすぎる」で混乱します。

その他の範囲指定

  • x / *: 1.2.x>=1.2.0 かつ <1.3.0* は任意
  • ハイフン範囲: 1.2.3 - 2.3.4 は両端を含む(>=1.2.3 かつ <=2.3.4
  • OR: ^2 || ^3 || ^4 でいずれかを満たせば OK
  • 完全一致: 4.18.2 のように記号なしで固定

NOTE

範囲指定はデフォルトでプレリリース版を除外します(^1.2.32.0.0-rc.1 を含まない)。プレリリースを拾うには範囲側にもプレリリースタグを書く必要があります。

package.json と package-lock.json

範囲指定だけでは「いつ誰がインストールしても同じバージョン」は保証されません。そこを担うのが package-lock.json です。

  • package.json: 範囲^1.2.3 等)を書く=「これくらいの幅で許す」という意思表示
  • package-lock.json: 実際に入った正確なバージョンを記録=「次回も同一ツリーを再現」する。ソース管理にコミットするのが公式の想定

この2層構造で「柔軟な範囲指定」と「再現性」を両立します。

npm install / ci / update の違い

コマンド挙動
npm install範囲を満たす形で解決し、node_modules と lock を生成・更新(lock を書き換えうる)
npm cilock 必須・lock に厳密追従。lock と package.json が不整合ならエラーで停止し、書き換えない(CI 向け・決定的)
npm updatesemver の範囲を尊重して最新へ更新(^1.1.1 なら同一 MAJOR 内の最新へ)。lock を更新

CI やデプロイでは npm ci を使うと、毎回まったく同じ依存が入って事故が減ります。

実務の指針

  • ライブラリ作者は semver を守る1.0.0 で公開 API を確定し、互換を壊すなら MAJOR を上げる。0.x の間は「不安定」と明示する
  • アプリは package-lock.json をコミットし、CI で npm ci。範囲の広さに関わらず実バージョンを固定できる
  • ^ のメリットとリスク: バグ修正・機能追加を自動で取り込める反面、提供側が semver を守らないと壊れる。lock があれば実害は出にくいが、lock を持たない/更新する場面では「範囲内の新しい版」が入る点に注意
  • 依存が 0.x なら ^ でも MINOR は上がらないことを前提に置く

まとめ

  • semver は MAJOR(破壊的)・MINOR(互換ある機能追加)・PATCH(互換あるバグ修正)
  • ^ は MINOR まで、~ は PATCH のみ。ただし 0.x では ^0.2.3~ と同じ・^0.0.3 は実質固定
  • プレリリースは範囲からデフォルト除外、ビルドメタデータは順序比較で無視
  • package.json=範囲、package-lock.json=固定の2層。再現性は lock + npm ci で担保
  • npm install は lock を更新しうる、npm ci は lock に厳密追従、npm update は範囲内で最新へ

^~、そして 0.x の特例を理解しておくだけで、「勝手に壊れた」「更新されない」の多くは説明がつきます。範囲は意思表示、固定は lock——この役割分担が要です。

参考リンク

自己増殖する npm ワーム Miasma と Phantom Gyp - binding.gyp が npm install を乗っ取る手口と防御

自己増殖する npm ワーム Miasma と Phantom Gyp - binding.gyp が npm install を乗っ取る手口と防御

12

preinstall/postinstall を一切宣言せず、binding.gyp と node-gyp の暗黙ビルドを悪用して npm install 時にコードを実行する新手口 Phantom Gyp と、それを使って自己増殖する npm サプライチェーンワーム Miasma(Shai-Hulud 系)を、StepSecurity/Snyk/Semgrep の一次調査に沿って解説します。2026年6月3日の57パッケージ汚染波、7月14日の AsyncAPI 波(別技術の require() 時ドロッパー)、そして開発者・CI/CD での現実的な防御策まで。