package.jsonのresolutionsフィールドを使うことで、依存ツリーの深い部分にあるパッケージのバージョンを固定することが可能になる。
現在のところ Yarn でのみ使える機能だが、サードパーティが公開しているライブラリを使うことで npm でも使えるようになる。
動作確認に使った npm のバージョンは6.14.5。Yarn は1.22.4。
後述するnpm-force-resolutionsについては、0.0.3を使っている。
npm や Yarn でパッケージをインストールすると、指定したパッケージだけでなく、そのパッケージが依存しているパッケージもインストールされる。
そうしてインストールされたパッケージが他のパッケージに依存していれば、そのパッケージもインストールされ、それが繰り返されていく。
この仕組みについては、以下の記事で詳しく触れた。
この際、インストールされる依存パッケージのバージョンは、^1.0.0のように^をつけた形で指定されていることが多い。
つまり、バージョンを厳密に指定することはできず、インストールするタイミングによってどのバージョンがインストールされるかが決まる。
そして、インストールしたタイミングでpackage-lock.jsonやyarn.lockに書き込まれ、それ以降はそこに書き込まれたバージョンがインストールされることになる。
この仕組みだと、何らかの理由で依存パッケージのバージョンを指定したいときに、問題となる。
reactを例にして説明する。
まず、reactをインストールする。
$ yarn add react
記事執筆時点での最新バージョンは16.13.1なので、そのバージョンのreactがインストールされる。
そして、reactが依存しているパッケージもインストールされる。
prop-typesもそのひとつ。reactのpackage.jsonのdependenciesにprop-types@^15.6.2と指定されているので、この時点での15.xの最新バージョンである15.7.2がインストールされる。
yarn.lockにもそのように記録されるため、これ以降$ yarnを実行すると、yarn.lockの内容に基づいてprop-types@15.7.2がインストールされる。
prop-types@^15.6.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
しかしその後、prop-types@15.7.2に脆弱性が見つかったと仮定する(あくまでも例であって、現在のところそういった話はない)。
取り急ぎ15.7.3にバージョンアップするか、15.6に戻すように、アナウンスされた。
しかし、package.jsonのdependenciesで指定しているのはreactのみであり、prop-typesのバージョンを指定することはできない。
最も望ましいのは、reactが対応を行い、prop-types@^15.7.3に依存したバージョンをリリースしてくれることである。そうすれば、単にreactをバージョンアップすれば済む。
しかし、reactのように開発が活発なパッケージならともかく、メンテナンスが行き届いていないパッケージでは、対応が遅れる可能性は十分にある。
$ yarn add prop-types@15.6のように、明示的にprop-typesをインストールするという方法も考えられるが、これは上手くいかない。
この場合、既にインストールされているprop-types@15.7.2とは別に15.6.xがインストールされるだけであり、prop-types@15.7.2は引き続き使われ続ける。
そもそも、いくら脆弱性対応のためとはいえ、直接依存しているわけではないパッケージをpackage.jsonのdependenciesやdevDependenciesに記述するのは望ましくない。
このような状況で使えるのが、resolutionsフィールドである。
このフィールドで、バージョンを固定したい依存パッケージの名前とバージョンを指定する。
"resolutions": { "prop-types": "~15.6.0" }
このようにしたうえで$ yarnを実行すると、15.6.xの最新バージョンのprop-typesがインストールされる。
yarn.lockにもそれが反映されている。
prop-types@^15.6.2, prop-types@~15.6.0:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
dependencies:
loose-envify "^1.3.1"
object-assign "^4.1.1"
この仕組みは Yarn でのみ利用可能で、今のところ npm には実装されていない。
だがnpm-force-resolutionsというライブラリを使うことで、npm でもresolutionsに基づいたインストールを行えるようになる。
まず普通にreactをインストールすると、当然、prop-types@15.7.2がインストールされる。
$ npm ls prop-types └─┬ react@16.13.1 └── prop-types@15.7.2
"prop-types": { "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.8.1" } },
次に、package.jsonでresolutionsを指定する。そしてscripts.preinstallを使って、インストールが行われる前にnpm-force-resolutionsが実行されるようにしておく。
"resolutions": { "prop-types": "~15.6.0" }, "scripts": { "preinstall": "npx npm-force-resolutions" }
この状態で$ npm iを実行すると、Yarn のときと同じように、15.6.xの最新バージョンのprop-typesがインストールされる。
$ npm ls prop-types └─┬ react@16.13.1 └── prop-types@15.6.2
"prop-types": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "requires": { "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } }
Yarn のときとの違いは、インストールされる場所。
Yarn ではnode_modules/prop-typesとしてインストールされたが、npm ではnode_modules/react/node_modules/prop-types/としてインストールされる。
依存パッケージにバージョンのコンフリクトが発生した場合
これも詳細は以下の記事に書いたのだが、複数のパッケージから依存されているパッケージの場合、それぞれに異なるバージョンが指定されていることがある。
例えばbabel-loaderとstyle-loaderはどちらもloader-utilsというパッケージに依存しているが、バージョンはそれぞれ^1.4.0と^2.0.0を指定しており、この 2 つは両立しない。
babel-loader@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3"
integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
dependencies:
find-cache-dir "^2.1.0"
loader-utils "^1.4.0"
mkdirp "^0.5.3"
pify "^4.0.1"
schema-utils "^2.6.5"
style-loader@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a"
integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg==
dependencies:
loader-utils "^2.0.0"
schema-utils "^2.6.6"
こういった場合、両方のバージョンがインストールされ、ロックファイルにもそのように記録される。
loader-utils@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^2.1.2"
このようなケースにおいてresolutionsで特定のバージョンを指定した場合、どのように処理されるだろうか。
まずは Yarn から見ていく。
今回は^2.0.0を指定してみる。
"resolutions": { "loader-utils": "^2.0.0" }
この状態で$ yarnを実行すると、2.xの最新バージョンである2.0.0がインストールされ、1.4.0は削除された。
loader-utils@^1.4.0, loader-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^2.1.2"
つまり、resolutionsフィールドで指定したバージョンに統一され、それ以外のバージョンは削除されるということである。
次に、npm-force-resolutionsの挙動を見ていく。
babel-loaderとstyle-loaderをインストールし直して、2 種類のloader-utilsがインストールされていることを確認する。
$ npm ls loader-utils ├─┬ babel-loader@8.1.0 │ └── loader-utils@1.4.0 └─┬ style-loader@1.2.1 └── loader-utils@2.0.0
prop-typesのときと同じ要領で、loader-utilsのバージョンを指定する。
"resolutions": { "loader-utils": "^2.0.0" }, "scripts": { "preinstall": "npx npm-force-resolutions" }
この状態で、$ npm iを実行する。
そうするとやはり同じように、2.xの最新バージョンで統一されている。
$ npm ls loader-utils ├─┬ babel-loader@8.1.0 │ └── loader-utils@2.0.0 invalid └─┬ style-loader@1.2.1 └── loader-utils@2.0.0
prop-typesのときと同様、インストールされる場所が、Yarn とは異なる。
Yarn のときはnode_modules直下にloader-utils@2.0.0がインストールされたが、npm-force-resolutionsではnode_modules/style-loader/node_modulesとnode_modules/babel-loader/node_modulesにそれぞれ、loader-utils@2.0.0がインストールされる。
なお、今回は検証のためにこのような処理を行ったが、babel-loaderが依存しているloader-utils^1.4.0がなくなってしまい、動作が保証されなくなるため、このような処理は行うべきではない。