diff --git a/_posts/2023/2023-04-29-node.js-14-to-18.md b/_posts/2023/2023-04-29-node.js-14-to-18.md new file mode 100644 index 0000000000..4e20ecef6c --- /dev/null +++ b/_posts/2023/2023-04-29-node.js-14-to-18.md @@ -0,0 +1,179 @@ +--- +title: "Node.js 14から18へアップデートする方法について" +author: azu +layout: post +date: 2023-04-29T19:46 +category: JavaScript +tags: + - Node.js + - slide + - JavaScript + +--- + +[Corepackを使ってNode.jsをアップデートする ⬆️⬆️](https://azu.github.io/slide/2023/nodejs-corepack/corepack.html)というタイトルで、Node.js 14からNode.js 18へのアップデートする方法について話した。 + +

Node.js 14は4月末でEOLで、Node.js 18までアップデートする必要があるけど、npmの変更が混ざって大変です。
Corepackを使うことで、Node.jsとnpmのアップデートを同時にやらなくても良くなり、問題を分割して対応できます!

Corepackを使ってNode.jsをアップデートする ⬆️⬆️https://t.co/mRHsBcYbpn pic.twitter.com/HiiCe7c5YE

— azu (@azu_re) April 28, 2023
+ +- スライド: [Corepackを使ってNode.jsをアップデートする ⬆️⬆️](https://azu.github.io/slide/2023/nodejs-corepack/corepack.html) + +## Node.jsのアップデートの難しさはライブラリにある + +Node.jsをアップデートするときに、一緒に直さないといけない問題は次のようなものがあると思います。 + +- OSの問題 + - Node.js 18はglibc 2.28+が必要になるので、古いOSだとNode.jsがインストールできないことがあります +- [Native Addon](https://nodejs.org/api/addons.html)の問題 + - 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をアップデートする ⬆️⬆️](https://azu.github.io/slide/2023/nodejs-corepack/corepack.html)のスライドでも話していますが、実際にアップデートしてみると、大体はライブラリの問題にぶつかることが多いです。 +そのため、根本的な対応はライブラリのアップデートをしましょうという話になったりします。 + +- OSの問題 → OSの問題 +- [Native Addon](https://nodejs.org/api/addons.html)の問題 → ライブラリの問題 +- 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バージョン](https://azu.github.io/slide/2023/nodejs-corepack/img/node-js-npm-version.png) + +これができる条件などは次のページにまとまっていますが、ユーザー的にはnpmのBREAKING CHANGEがNode.jsのminor updateに含まれる形にはなります。 + +- [Integrating with node · npm/cli Wiki](https://github.com/npm/cli/wiki/Integrating-with-node) + +Node.jsのアップデートとは別の問題として、チーム間でNode.js 18を使うといっても人によってnpmのバージョンが違うという問題も起きやすいです。Node.js自体のバージョン管理は`.node-version`や`.nvmrc`などで管理しているプロジェクトも多いと思います。 + +同じようにnpmのバージョンを管理する仕組みとして、[Corepack](https://nodejs.org/api/corepack.html)があります。 + +## Corepackを使ってnpmのバージョンを管理する + +[Corepack](https://nodejs.org/api/corepack.html)は、パッケージごとに利用するパッケージ管理ツール(npmやyarnやpnpm)とバージョンを指定できる仕組みです。Corepackでは`package.json`に`packageManager`に利用するnpmのバージョンなどを指定できます。 + +```json +{ + "name": "my-project", + "version": "1.0.0", + "packageManager": "npm@8.1.0" +} +``` + +また、Corepackを使うことで、Node.jsとnpmのアップデートを同時にやらなくても良くなり、問題を分割して対応できます。 +たとえば、npm 6のまま、Node.js 14からNode.js 18へアップデートするという動き方も可能です。 + +![Node.jsとCorepack](https://azu.github.io/slide/2023/nodejs-corepack/img/node-js-npm-version-corepack.png) + +[Corepack](https://nodejs.org/api/corepack.html)はNode.jsに同梱されていますが、まだExperimentalというステータスなので、デフォルトでは無効となっています。そのため、利用するには`corepack enable npm yarn pnpm`というコマンドを実行して有効化する必要があります。 + +```sh +$ corepack enable npm yarn pnpm +``` + +## peerDependenciesの問題を回避する + +[Corepackを使ってNode.jsをアップデートする ⬆️⬆️](https://azu.github.io/slide/2023/nodejs-corepack/corepack.html)のスライドの話に戻ると、Corepackは`peerDependenciesの問題`への対処として、npm 6のままNode.jsをアップデートできます。 + +別の方法として、`--legacy-peer-deps`オプションがnpmには用意されています。 +おそらく大体の人は、次のような`peerDependenciesの問題`から発生する`code ERESOLVE`のエラーをみると、`--legacy-peer-deps`オプションを使ってしまうと思います。 + +```shell +$ npm install +npm ERR! code ERESOLVE +npm ERR! ERESOLVE could not resolve +npm ERR! +npm ERR! While resolving: @shiftcoders/dynamo-easy@7.1.0 +npm ERR! Found: tslib@2.3.0 +npm ERR! node_modules/tslib +npm ERR! tslib@"^2.1.0" from the root project +npm ERR! tslib@"^2.0.0" from @aws-sdk/abort-controller@3.20.0 +npm ERR! node_modules/@aws-sdk/abort-controller +npm ERR! @aws-sdk/abort-controller@"3.20.0" from @aws-sdk/node-http-handler@3.21.0 +npm ERR! node_modules/@aws-sdk/node-http-handler +npm ERR! @aws-sdk/node-http-handler@"3.21.0" from @aws-sdk/client-sso@3.21.0 +npm ERR! node_modules/@aws-sdk/client-sso +npm ERR! @aws-sdk/client-sso@"3.21.0" from @aws-sdk/credential-provider-sso@3.21.0 +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/dynamo-easy@7.1.0 +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: tslib@1.14.1 +npm ERR! node_modules/tslib +npm ERR! peer tslib@"^1.10.0" from @shiftcoders/dynamo-easy@7.1.0 +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.json`に`packageManager`フィールドの追加が必要です。 + +`peerDependenciesの問題`の一時的な対処として、何かしらの設定を足す点はどちらも同じです。 +そのため、どのような流れでアップデートしていくかは好みの問題と言えます。 + +どちらの場合も最終的な根本対応は、ライブラリをアップデートするに行き着くと思います。 + +## アップデートを小さく進める + +Node.jsのアップデートする際に、Node.jsとライブラリを同時にアップデートしてしまう方法も選べると思います。 +小さなアプリケーションならこの方法が簡単で良いですが、大きなアプリケーションになるとデメリットが目立ってきます。 + +実際にNode.js 18へのアップデートをいくつかやってみて、Corepackを使った方法が実際の差分やPRの小さく作って進められたのでよかったと思います(`--legacy-peer-deps`も多分同じステップを取れますが、好みではなかった)。 +実際の作業も、次のようなステップに分けて進めることができ、実際に出したPull Requestもそれぞれのステップごとにできました。 + +1. CI: Ubuntuマシンイメージのアップデート +2. (Node.js 14のまま): プロジェクトのインストールステップやDockerfileでcorepackを有効化 +3. (Node.js 14のまま): Node.js 18では動かなかったライブラリをアップデート + - 具体的にはJestがReferenceError: AbortSignal is not definedで壊れていました +4. Node.js のバージョン番号だけを変更する + - Dockerfileの`FROM node:14.x.x` を `FROM 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も簡単です。 + +## まとめ + +[Corepackを使ってNode.jsをアップデートする ⬆️⬆️](https://azu.github.io/slide/2023/nodejs-corepack/corepack.html)というスライドの補足的な記事でした。 + +Node.jsのアップデート時にライブラリもコードもまとめてアップデートしたくなってしまいますが、アプリケーションが多くなるほど大変です。 +そのため、問題を分割してアップデートしていくのが、アップデートする人、レビューする人、アプリケーションにとっても良いと思います。 +Node.jsでは[Corepack](https://nodejs.org/api/corepack.html)という、Node.jsとnpmの問題の切り分けに使えるツールがあります。 + +npm 6からnpm8には、かなり大きな影響がある変更が含まれているので、Corepackを使って問題を切り分けて進めるのはどうでしょうか?という話でした。 \ No newline at end of file