30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

package.json の resolutions を使って依存パッケージのバージョンを指定する

package.jsonresolutionsフィールドを使うことで、依存ツリーの深い部分にあるパッケージのバージョンを固定することが可能になる。
現在のところ Yarn でのみ使える機能だが、サードパーティが公開しているライブラリを使うことで npm でも使えるようになる。

動作確認に使った npm のバージョンは6.14.5。Yarn は1.22.4
後述するnpm-force-resolutionsについては、0.0.3を使っている。

npm や Yarn でパッケージをインストールすると、指定したパッケージだけでなく、そのパッケージが依存しているパッケージもインストールされる。
そうしてインストールされたパッケージが他のパッケージに依存していれば、そのパッケージもインストールされ、それが繰り返されていく。
この仕組みについては、以下の記事で詳しく触れた。

numb86-tech.hatenablog.com

この際、インストールされる依存パッケージのバージョンは、^1.0.0のように^をつけた形で指定されていることが多い。
つまり、バージョンを厳密に指定することはできず、インストールするタイミングによってどのバージョンがインストールされるかが決まる。
そして、インストールしたタイミングでpackage-lock.jsonyarn.lockに書き込まれ、それ以降はそこに書き込まれたバージョンがインストールされることになる。

この仕組みだと、何らかの理由で依存パッケージのバージョンを指定したいときに、問題となる。

reactを例にして説明する。

まず、reactをインストールする。

$ yarn add react

記事執筆時点での最新バージョンは16.13.1なので、そのバージョンのreactがインストールされる。
そして、reactが依存しているパッケージもインストールされる。
prop-typesもそのひとつ。reactpackage.jsondependenciesprop-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.jsondependenciesで指定しているのは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.jsondependenciesdevDependenciesに記述するのは望ましくない。

このような状況で使えるのが、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に基づいたインストールを行えるようになる。

github.com

まず普通に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.jsonresolutionsを指定する。そして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/としてインストールされる。

依存パッケージにバージョンのコンフリクトが発生した場合

これも詳細は以下の記事に書いたのだが、複数のパッケージから依存されているパッケージの場合、それぞれに異なるバージョンが指定されていることがある。

numb86-tech.hatenablog.com

例えばbabel-loaderstyle-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-loaderstyle-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_modulesnode_modules/babel-loader/node_modulesにそれぞれ、loader-utils@2.0.0がインストールされる。

なお、今回は検証のためにこのような処理を行ったが、babel-loaderが依存しているloader-utils^1.4.0がなくなってしまい、動作が保証されなくなるため、このような処理は行うべきではない。

参考資料