package.jsonでのバージョン指定は完全固定にしよう
バージョン指定ゆるくなってませんか?
標準の状態でnpm install
などで依存ライブラリを追加すると、package.json
には
"package-name": "^5.4.3",
こんな形で登録されます。
これは「バージョン5.4.3を使う」の指定ではありません。
最初の^
は「0でない最初の数値を固定する」の意味で、この場合は「5.4.3以上かつ6.0.0未満のバージョンを使う」になります。
パッケージマネージャーが参照するlockfile、例えばnpmであればpackage-lock.json
に具体的な指定があればそのバージョンが使われるし、なければ条件に合致するその時点の最新版がlockfileに登録されます。
この挙動はsemver(semantic versioning)の考え方に基づいた挙動になっています。
semverの基本的な考え方として「メジャーバージョンが同じであれば後方互換性は保たれている」とされるため、こうした設定がデフォルトになっていますが現実はそう甘くはありません。
マイナーバージョンで機能を大きく変えるライブラリはたくさんあるし、パッチで挙動が変わる(意図するしないは別として)なんてのも日常茶飯事です。
そもそもソフトウェアというものは変更するためにバージョンを進めるものなので、そこに「変わらないこと」を期待するのに無理があるのです。
あなたがアプリケーション開発者であれば、開発するプロジェクトでのバージョン指定は
"package-name": "5.4.3",
のように、パッチ(3つめの数字)まで完全に決め打ちで固定した方が意図しない挙動を防げます。
package-lock.json
があればバージョンは固定できますが、このファイルは
- 間接依存しているライブラリに脆弱性が見つかった
- 複数のライブラリの依存関係を解決する際に複数バージョンが競合した
などの問題が起きた時に削除して作り直したくなることがあります。その際に意図せずバージョンが上がってしまうのは望ましくありません。
直接的に利用しているライブラリに脆弱性が見つかった場合などは、lockfileではなくpackage.json
でのバージョン指定を問題ない状態に保ちましょう。
そもそもpackage.json
でバージョンを指定しているのに、そこでの指定を不完全にして更に別のファイルに依存する必要はありません。
package-lock.json
などのlockfileは間接的に依存するライブラリのバージョンを管理するためのものと考えるのがいいでしょう。
バージョン固定で追加する方法
npmでもyarnでもpnpmでも、インストール時に-E
を付けるとバージョンを固定できます。
npm i -E package-name
みたいにして追加すればその時点の最新版で固定されます。-E
は--save-exact
の省略形です。
人類は毎回これを忘れずに付けられるほど高性能ではないので、設定ファイルに書いておくといいでしょう。
利用しているパッケージマネージャーがnpmかpnpmであれば~/.npmrc
に
save-exact=true
と書いておけばデフォルトでバージョン固定してくれます。プロジェクトで合意できるならば、プロジェクトのルートに.npmrc
を置いてもいいかもしれません。
yarnを使っている場合はv1なら~/.npmrc
の設定を見てくれるので同じで構いません。~/.yarnrc
に
save-prefix ""
と書いても実現できます。
yarn v2以降では~/.yarnrc.yml
に
defaultSemverRangePrefix: ""
みたいに書くようです(私はv2以降は使ってない)。
ライブラリを公開する場合
先に「あなたがアプリケーション開発者であれば」と書きましたが、逆にライブラリを外部に公開する場合はバージョンを完全に固定するのは望ましくありません。
仮にあなたのライブラリが依存する別のパッケージの指定したバージョンに、後から脆弱性が見つかった場合には
- package.jsonで指定するバージョンを問題のないものに変える
- その変更をリリースする
- 利用者にその修正版を使ってもらう
という対応が必要になります。これには手間がかかるし、時間がかかる分だけリスクも増します。
semverを使って範囲指定することで、利用者はlockfileの修正だけで対応できる可能性が高くなります。
アプリケーションであれば自分で使用するバージョンを更新すれば済む話なので固定すればいいのですが、ライブラリを提供する場合は利用者のことを考えて設定しましょう。
Discussion