pnpmでパッケージを追加するとき、package.json に記録されるバージョン番号の先頭に ^(キャレット)が付く場合と付かない場合がある。この違いを正確に把握していないと、意図しないバージョンアップや、逆に「更新したはずなのに変わらない」といったトラブルにつながる。
ここでは ^ の意味、付与される条件、pnpm install と pnpm update の挙動の違いを、公式ドキュメントをエビデンスとして整理する。
セマンティックバージョニングの基本
npmエコシステムのバージョン番号は MAJOR.MINOR.PATCH の3つの数字で構成される。それぞれの数字が上がるタイミングには明確なルールがある。
| 区分 | 意味 | 例 |
|---|---|---|
| MAJOR | 後方互換性を壊す変更 | 1.x.x → 2.0.0 |
| MINOR | 後方互換性を保った機能追加 | 1.2.x → 1.3.0 |
| PATCH | バグ修正 | 1.2.3 → 1.2.4 |
これがセマンティックバージョニング(semver)と呼ばれるルールで、パッケージの作者はこの規約に従ってバージョン番号を付けることが期待されている。
^(キャレット)の意味
^ はsemverにおける「キャレット範囲」の指定で、メジャーバージョンを固定しつつ、マイナーとパッチの更新を許容する。
^1.2.3 → >=1.2.3 かつ <2.0.0
つまり ^1.2.3 と書かれていれば、1.2.4 や 1.9.9 はインストール対象になるが、2.0.0 は絶対にインストールされない。メジャーバージョンの変更は破壊的変更を意味するため、それを自動では取り込まない設計になっている。
npm公式のsemverドキュメントにも次のように記載されている。
allows patch and minor updates for versions 1.0.0 and above — https://docs.npmjs.com/cli/v6/using-npm/semver/
参考として、~(チルダ)はパッチバージョンのみ許容する、より保守的な指定になる。
| 記法 | 許容範囲 | 例 |
|---|---|---|
^1.2.3 | マイナー+パッチ | >=1.2.3 <2.0.0 |
~1.2.3 | パッチのみ | >=1.2.3 <1.3.0 |
1.2.3 | 完全固定 | 1.2.3 のみ |
0系の例外に注意
^0.x.y の場合はルールが変わる。メジャーが 0 のパッケージはまだ不安定なAPIとみなされるため、より保守的な範囲になる。
^0.2.3 → >=0.2.3 かつ <0.3.0(マイナーも固定)
^0.0.3 → >=0.0.3 かつ <0.0.4(ほぼ完全固定)
0系のパッケージを扱うときは「^ が付いているから安心」とは言えない点を覚えておきたい。
^ が付く条件・付かない条件
pnpmでパッケージを追加する際、コマンドの書き方によって ^ が付くかどうかが変わる。
| コマンド | package.jsonの記録 |
|---|---|
pnpm add dompurify | "dompurify": "^3.4.8" |
pnpm add dompurify@^3.4.8 | "dompurify": "^3.4.8" |
pnpm add dompurify@3.4.8 | "dompurify": "3.4.8" |
pnpm add dompurify@3.4.8 --save-exact | "dompurify": "3.4.8" |
ポイントは、バージョンを明示的に指定すると ^ は付かないという点だ。pnpm公式ドキュメントの savePrefix の説明にも次のように書かれている。
This setting is ignored when the added package has a range specified. For instance, pnpm add foo@2 will set the version of foo in package.json to 2, regardless of the value of savePrefix. — https://pnpm.io/settings
つまり savePrefix のデフォルト値 ^ は、バージョン指定なしでインストールした場合にのみ適用される。
savePrefix をプロジェクト単位で変更する
プロジェクト全体で常にバージョンを固定したい場合は、pnpm-workspace.yaml に設定を追加する。
saveExact: true
または
savePrefix: ''
この設定がされているプロジェクトでは、pnpm add dompurify としても ^ なしの固定バージョンで保存される。チーム開発でバージョンを厳密に管理したい場合に有効だ。
なお、.npmrc に save-exact=true を書く方法も従来は使われていたが、pnpm v11以降では .npmrc から読み取られる設定は認証・レジストリ関連のみに限定されている。pnpm固有の設定は pnpm-workspace.yaml に書くのが正しい。
pnpm install と pnpm update の違い
^ の範囲内であっても、いつバージョンが上がるのかはコマンドによって異なる。
pnpm install の挙動
pnpm install は pnpm-lock.yaml(ロックファイル)に記録されたバージョンをそのまま使う。ロックファイルが存在する限り、^ の範囲内に新しいバージョンが公開されていても勝手には上がらない。
pnpm公式のissueでも「pnpm install では最新パッケージがインストールされない。pnpm-lock.yaml を削除しないと最新が入らない」という報告があり、これは仕様どおりの動作だ(参考: GitHub Issue #7740)。
ロックファイルの役割は、チーム全員が同じバージョンを使うことを保証することにある。CI環境では --frozen-lockfile オプションでロックファイルと package.json の不整合を検出してエラーにすることもできる。
pnpm update の挙動
一方、pnpm update はロックファイルを更新し、package.json に記載された範囲内で最新バージョンをインストールする。公式ドキュメントにも次のように記載されている。
pnpm update updates packages to their latest version based on the specified range. — https://pnpm.io/cli/update
整理すると
| 操作 | バージョンは上がるか |
|---|---|
pnpm install(ロックファイルあり) | 上がらない |
pnpm install(ロックファイルなし) | ^ の範囲内で最新に上がる |
pnpm update | ^ の範囲内で最新に上がる |
pnpm update --latest | メジャーを含め最新に上がる |
実務での使い分け
ここまでの内容を踏まえて、実務での判断基準を整理しておく。
バージョン指定なしで入れるケース
初回導入時や、マイナー・パッチの自動追従を許容できるライブラリ(ユーティリティ系など)は pnpm add lodash のようにバージョン指定なしで入れてよい。^ が付くことで、pnpm update 時にバグ修正を自動で取り込める。
バージョンを固定するケース
破壊的変更のリスクがあるライブラリや、チーム全体で厳密に揃えたい場合は pnpm add dompurify@3.4.8 のようにバージョンを明示する。^ が付かないため、意図的に変更しない限りバージョンは動かない。
プロジェクト全体のポリシーとして固定したい場合は、前述の pnpm-workspace.yaml に saveExact: true を設定しておくのが確実だ。Renovateやdependabotなどの自動更新ツールと組み合わせれば、固定バージョンでも定期的なアップデートの仕組みを維持できる。
pnpm-lock.yaml は必ずGitにコミットすること。これがバージョン固定の最後の砦になる。逆に言えば、ロックファイルをコミットしていない状態で ^ 付きのバージョンを使っていると、メンバーごとにインストールされるバージョンが異なるリスクがある。