Corepackを使ってNode.jsをアップデートする ⬆️⬆️というタイトルで、Node.js 14からNode.js 18へのアップデートする方法について話した。

Note: Node.js 14は2023-04-30でEOLで、Node.js 16は2023-09-11でEOLなので、長期間動かすアプリケーションなら実質的にNode.js 18へのアップデートが必要になります。

Node.jsのアップデートの難しさはライブラリにある

Node.jsをアップデートするときに、一緒に直さないといけない問題は次のようなものがあると思います。

  • OSの問題
    • Node.js 18はglibc 2.28+が必要になるので、古いOSだとNode.jsがインストールできないことがあります
  • Native Addonの問題
    • Node-APIという抽象レイヤーがあるとはいえ、Native AddonはNode.jsのバージョンアップでよく壊れます
  • peerDependenciesの問題
    • これはNode.jsではなく、npmが6から8でpeerDependenciesの扱い方が変わったことに起因する問題です
  • Node.js Runtimeの問題
    • Node.jsのRuntimeが変わったり、fsといったコアモジュールの変更による影響を受けることがあります
  • 利用してるライブラリがサポートしていない問題
    • Node.js Runtimeが変わったことで、動かなくなるライブラリがあります

Corepackを使ってNode.jsをアップデートする ⬆️⬆️のスライドでも話していますが、実際にアップデートしてみると、大体はライブラリの問題にぶつかることが多いです。 そのため、根本的な対応はライブラリのアップデートをしましょうという話になったりします。

  • OSの問題 → OSの問題
  • Native Addonの問題 → ライブラリの問題
  • peerDependenciesの問題 → ライブラリの問題
  • Node.js Runtimeの問題 → アプリケーションコードの問題
  • 利用してるライブラリがサポートしていない問題 → ライブラリの問題

しかし、ライブラリが壊れてるという状況は古いバージョンのライブラリを利用していることがほとんどです。 そのため、ライブラリをアップデートするにはメジャーバージョンを上げる必要があったりして、結構大変になります。

特に今回のNode.js 18へのアップデートは、npmの変更が混ざっているため、npmの変更による影響を受けるライブラリが多いです。 Node.jsにはnpmが同梱されており、Node.jsとnpmのアップデートは同時に行われてしまい、問題の影響範囲が大きくなります。 これに対して、Node.jsのアップデートとnpmのアップデートを分けて行うと、一度に対応しないといけない問題が減ります。

また、Node.jsと同梱されているnpmのバージョンを見てみるとわかりますが、Node.jsのメジャーアップデート と npmのメジャーアップデートのタイミングは一致しません。Node.js 18のminor updateで、npmのmajor updateであるnpm 9がバックポートされたりしています。

Node.jsとnpmバージョン

これができる条件などは次のページにまとまっていますが、ユーザー的にはnpmのBREAKING CHANGEがNode.jsのminor updateに含まれる形にはなります。

Node.jsのアップデートとは別の問題として、チーム間でNode.js 18を使うといっても人によってnpmのバージョンが違うという問題も起きやすいです。Node.js自体のバージョン管理は.node-version.nvmrcなどで管理しているプロジェクトも多いと思います。

同じようにnpmのバージョンを管理する仕組みとして、Corepackがあります。

Corepackを使ってnpmのバージョンを管理する

Corepackは、パッケージごとに利用するパッケージ管理ツール(npmやyarnやpnpm)とバージョンを指定できる仕組みです。Corepackではpackage.jsonpackageManagerに利用するnpmのバージョンなどを指定できます。

{
  "name": "my-project",
  "version": "1.0.0",
  "packageManager": "[email protected]"
}

また、Corepackを使うことで、Node.jsとnpmのアップデートを同時にやらなくても良くなり、問題を分割して対応できます。 たとえば、npm 6のまま、Node.js 14からNode.js 18へアップデートするという動き方も可能です。

Node.jsとCorepack

CorepackはNode.jsに同梱されていますが、まだExperimentalというステータスなので、デフォルトでは無効となっています。そのため、利用するにはcorepack enable npm yarn pnpmというコマンドを実行して有効化する必要があります。

$ corepack enable npm yarn pnpm

peerDependenciesの問題を回避する

Corepackを使ってNode.jsをアップデートする ⬆️⬆️のスライドの話に戻ると、CorepackはpeerDependenciesの問題への対処として、npm 6のままNode.jsをアップデートできます。

別の方法として、--legacy-peer-depsオプションがnpmには用意されています。 おそらく大体の人は、次のようなpeerDependenciesの問題から発生するcode ERESOLVEのエラーをみると、--legacy-peer-depsオプションを使ってしまうと思います。

$ npm install
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR! 
npm ERR! While resolving: @shiftcoders/[email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/tslib
npm ERR!   tslib@"^2.1.0" from the root project
npm ERR!   tslib@"^2.0.0" from @aws-sdk/[email protected]
npm ERR!   node_modules/@aws-sdk/abort-controller
npm ERR!     @aws-sdk/abort-controller@"3.20.0" from @aws-sdk/[email protected]
npm ERR!     node_modules/@aws-sdk/node-http-handler
npm ERR!       @aws-sdk/node-http-handler@"3.21.0" from @aws-sdk/[email protected]
npm ERR!       node_modules/@aws-sdk/client-sso
npm ERR!         @aws-sdk/client-sso@"3.21.0" from @aws-sdk/[email protected]
npm ERR!         node_modules/@aws-sdk/credential-provider-sso
npm ERR!       1 more (@aws-sdk/client-sts)
npm ERR!   46 more (@aws-sdk/client-sso, @aws-sdk/client-sts, ...)
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer tslib@"^1.10.0" from @shiftcoders/[email protected]
npm ERR! node_modules/@shiftcoders/dynamo-easy
npm ERR!   dev @shiftcoders/dynamo-easy@"^7.1.0" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: [email protected]
npm ERR! node_modules/tslib
npm ERR!   peer tslib@"^1.10.0" from @shiftcoders/[email protected]
npm ERR!   node_modules/@shiftcoders/dynamo-easy
npm ERR!     dev @shiftcoders/dynamo-easy@"^7.1.0" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR! 

--legacy-peer-depsもこのpeerDependenciesの問題を回避するためのオプションです。 しかし、npm i --legacy-peer-depsのように毎回つけている場合は、つけ忘れの問題があります。(つけ忘れると当然peerDependenciesの問題が復活します。) --legacy-peer-depsを使う場合は、npm config --location=project set legacy-peer-deps=trueを実行して.npmrcに設定した方が良いでしょう。

npm config --location=project set legacy-peer-deps=trueを実行すると、次のような.npmrcが作成されます。 この.npmrcがあるディレクトリでは、npmコマンドが自動的に設定を読み取るようになります。

legacy-peer-deps=true

--legacy-peer-depsも宣言的に設定するためには、.npmrcを利用する必要があります。 Corepackもpackage.jsonpackageManagerフィールドの追加が必要です。

peerDependenciesの問題の一時的な対処として、何かしらの設定を足す点はどちらも同じです。 そのため、どのような流れでアップデートしていくかは好みの問題と言えます。

どちらの場合も最終的な根本対応は、ライブラリをアップデートするに行き着くと思います。

アップデートを小さく進める

Node.jsのアップデートする際に、Node.jsとライブラリを同時にアップデートしてしまう方法も選べると思います。 小さなアプリケーションならこの方法が簡単で良いですが、大きなアプリケーションになるとデメリットが目立ってきます。

実際にNode.js 18へのアップデートをいくつかやってみて、Corepackを使った方法が実際の差分やPRの小さく作って進められたのでよかったと思います(--legacy-peer-depsも多分同じステップを取れますが、好みではなかった)。 実際の作業も、次のようなステップに分けて進めることができ、実際に出したPull Requestもそれぞれのステップごとにできました。

  1. (Node.js 14のまま): プロジェクトのインストールステップやDockerfileでcorepackを有効化
  2. (Node.js 14のまま): Node.js 18では動かなかったライブラリをアップデート
  • 具体的にはJestがReferenceError: AbortSignal is not definedで壊れていました
  1. CI: Ubuntuマシンイメージのアップデート
  2. Node.js のバージョン番号だけを変更する
  • DockerfileのFROM node:14.x.xFROM node:18.16.0に変更
  • CIのバージョン指定を変更

ライブラリも一緒にアップデートしてしまうと、Node.js 18にアップデートして問題があったのかライブラリに問題があったのか切り分けが難しくなります。 そのため、Node.js 18へのアップデートは、Node.jsを14 → 18とするだけのPRを出せるようにすると良いと思います。

これができるのは、Node.js 14では動くけど、Node.js 18では動かないライブラリはかなり少ないからです。 Node.js 14のままNode.js 18でも動くアプリケーションにしてから、Node.jsだけをアップデートすると問題があった時のrevertも簡単です。

実際にスライドでも出てきたNode.js 15+でのUnhandled Rejectionの挙動変更は、デプロイしようとして気付いた問題でした。 その時は、すぐrevertしてNode.js 14に戻すことができたので、バージョンアップ作業のPRを分割しておいてよかったなと思いました。

まとめ

Corepackを使ってNode.jsをアップデートする ⬆️⬆️というスライドの補足的な記事でした。

Node.jsのアップデート時にライブラリもコードもまとめてアップデートしたくなってしまいますが、アプリケーションがおおきくなるほど大変です。 そのため、問題を分割してアップデートしていくのが、アップデートする人、レビューする人、アプリケーションにとっても良いと思います。 Node.jsではCorepackという、Node.jsとnpmの問題の切り分けに使えるツールがあります。

npm 6からnpm8には、かなり大きな影響がある変更が含まれているので、Corepackを使って問題を切り分けて進めるのはどうでしょうか?という話でした。