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
がなくなってしまい、動作が保証されなくなるため、このような処理は行うべきではない。