pnpmのバージョン指定を理解する — ^ の意味と install / update の違い

pnpmでパッケージを追加するとき、package.json に記録されるバージョン番号の先頭に ^(キャレット)が付く場合と付かない場合がある。この違いを正確に把握していないと、意図しないバージョンアップや、逆に「更新したはずなのに変わらない」といったトラブルにつながる。

ここでは ^ の意味、付与される条件、pnpm installpnpm 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.41.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 のみ

1.2.3 1.x.x 2.0.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 としても ^ なしの固定バージョンで保存される。チーム開発でバージョンを厳密に管理したい場合に有効だ。

なお、.npmrcsave-exact=true を書く方法も従来は使われていたが、pnpm v11以降では .npmrc から読み取られる設定は認証・レジストリ関連のみに限定されている。pnpm固有の設定は pnpm-workspace.yaml に書くのが正しい。

pnpm installpnpm update の違い

^ の範囲内であっても、いつバージョンが上がるのかはコマンドによって異なる。

pnpm install の挙動

pnpm installpnpm-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 vs pnpm update pnpm install ロックファイルのバージョンを使う → バージョンは変わらない pnpm update 範囲内の最新バージョンに更新 → ロックファイルも書き換わる ロックファイル削除 + pnpm install ロックファイルがない場合は ^ の範囲内で最新を解決する (意図しないバージョンアップの原因になりやすい)

整理すると

操作バージョンは上がるか
pnpm install(ロックファイルあり)上がらない
pnpm install(ロックファイルなし)^ の範囲内で最新に上がる
pnpm update^ の範囲内で最新に上がる
pnpm update --latestメジャーを含め最新に上がる

実務での使い分け

ここまでの内容を踏まえて、実務での判断基準を整理しておく。

バージョン指定なしで入れるケース

初回導入時や、マイナー・パッチの自動追従を許容できるライブラリ(ユーティリティ系など)は pnpm add lodash のようにバージョン指定なしで入れてよい。^ が付くことで、pnpm update 時にバグ修正を自動で取り込める。

バージョンを固定するケース

破壊的変更のリスクがあるライブラリや、チーム全体で厳密に揃えたい場合は pnpm add dompurify@3.4.8 のようにバージョンを明示する。^ が付かないため、意図的に変更しない限りバージョンは動かない。

プロジェクト全体のポリシーとして固定したい場合は、前述の pnpm-workspace.yamlsaveExact: true を設定しておくのが確実だ。Renovateやdependabotなどの自動更新ツールと組み合わせれば、固定バージョンでも定期的なアップデートの仕組みを維持できる。

pnpm-lock.yaml は必ずGitにコミットすること。これがバージョン固定の最後の砦になる。逆に言えば、ロックファイルをコミットしていない状態で ^ 付きのバージョンを使っていると、メンバーごとにインストールされるバージョンが異なるリスクがある。

目次