
セマンティックバージョニングと package.json のバージョン指定 - ^ と ~ の違い、0.x の罠
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-alpha、1.0.0-rc.1のようにハイフンで付ける。通常版より優先順位は低い(1.0.0-alphaは1.0.0より前) - ビルドメタデータ:
1.0.0+20130313のようにプラスで付ける。優先順位の比較では無視される
優先順位の例(仕様より): 1.0.0-alpha → 1.0.0-alpha.1 → 1.0.0-beta → 1.0.0-rc.1 → 1.0.0。
キャレット ^ とチルダ ~
ここが本題です。npm の範囲指定で最頻出の2つを比べます(範囲の厳密な定義元は node-semver)。
| 指定 | 許可される範囲 | 上がる桁 |
|---|---|---|
^1.2.3 | >=1.2.3 かつ <2.0.0 | MINOR・PATCH |
~1.2.3 | >=1.2.3 かつ <1.3.0 | PATCH のみ |
^(キャレット): 「いちばん左の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.0 | PATCH のみ(~0.2.3 と同じ!) |
~0.2.3 | >=0.2.3 かつ <0.3.0 | PATCH のみ |
^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.3 は 2.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 ci | lock 必須・lock に厳密追従。lock と package.json が不整合ならエラーで停止し、書き換えない(CI 向け・決定的) |
npm update | semver の範囲を尊重して最新へ更新(^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——この役割分担が要です。


