jijij-kawaii’s blog

ソフトウェアエンジニアの情報シェア文化に貢献したいという気持ちで書き殴ります。

TS×ReactプロジェクトでNodeを14系から20系に上げた話。

どうも!ソフトウェアエンジニア1年目のjijiです!

実務でNodeのバージョンを14系から20系(v20.9.0)に一気に上げる機会がありました。Nodeのバージョンを上げるにあたりどういった変更が必要になってくるのかという点について、自分がつまづいた箇所を中心にお伝えできたらと思います。
全体的な話として、やってみて一番痛感したのは、

「やっぱり定期的にライブラリのバージョン上げは行った方が良い。」

ということです。
理由は、古いバージョンのパッケージが多ければ多いほど、一つをアップグレードした際の修正範囲が広くなるからです。アップグレードを紐づるで繰り返すことになり、この変更はなんのためにしたのか?というのがめちゃくちゃ分かりづらくなってきます。

では早速、自分がつまづいた箇所を一つ一つ紹介していきます。
※アップグレード前の主要なモジュールのバージョンおよび開発環境はこちらです。

  • Mac intelCore
  • macOS Sonoma
  • Node 14.17.2
  • yarn 1.22.4
  • typescript 3.9.7
  • react 16.13.1
  • @types/react ^16.9.0
  • react-scripts ^4.0.3
  • postcss-safe-parser 4.0.2, 5.0.2
  • postcss 7.0.21, 7.0.35, 8.2.6
  • node-sass 4.14.1
  • storybook(@storybook/react) ^6.1.21
  • styled-components ^5.3.3

OpenSSL

Nodeでreactのサーバーを立ち上げようとすると以下のエラーに遭遇しました。

Module build failed (from ../node_modules/file-loader/dist/cjs.js):
Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:68:19)
at Object.createHash (node:crypto:138:10)

色々調べた結果、Node14ではOpenSSL1.1を使用していましたが、Node17からはOpenSSL3.0に変更されたことで、プロジェクト内で利用しているライブラリと互換性が取れずにエラーになっているようです。 OpenSSLとは暗号化ライブラリのひとつで、アプリケーションをより安全なものにしてくれます。
私の場合、バンドル中に利用されるfile-loader内で呼びだしたハッシュ関数が、Node.jsのOpenSSLライブラリで提供されているcryptoから提供されていないため、エラーが起きていると想定できます。
解決策は以下の2つです。

1. Nodeのオプションに--openssl-legacy-providerをつける。
Nodeを動かす際に古いバージョンのOpenSSLを利用させることで解決します。自分はこの方法を使いました。 package.jsonのscriptプロパティを以下のようにし、scriptを動かしてみてください。

  "scripts": {
-    "develop": "gatsby develop",
+    "develop": "NODE_OPTIONS='--openssl-legacy-provider' gatsby develop",
  }

2. react-scriptsを5系にする。
react-scriptsを5系にすることで解消します。 (詳細)
というのも、そもそもこのOpenSSLエラーはwebpackの5系未満を利用している際に起こるからです。 (react-scriptsの4系以下はwebpack4を利用している。)
自分も本当はこちらで実装したかったのですが上手くいかずでした。4系のwebpack関連のモジュールが残ってしまっていたからなのかな... (参考)

node-sass

yarn installをしようとすると以下のエラーが出ました。

node_modules/node-sass: Command failed.

原因は複数あるようですが、自分の場合はNodeとnode-sassのバージョン互換性がなくなってしまったことのようです。 (参考) package.jsonのdependenciesに"node-sass": "^9.0.0"とすると無事yarn installできました。

react-scripts

Nodeでreactのサーバーを立ち上げようとすると以下のエラーに遭遇しました。

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/tokenize' is not defined by "exports  in /Users/name/project-name/node_modules/react-scripts/node_modules/postcss-safe-parser/node_modules/postcss/package.json" at new NodeError (node:internal/errors:406:5)
    at exportsNotFound (node:internal/modules/esm/resolve:268:10)
    at packageExportsResolve (node:internal/modules/esm/resolve:598:9)
    at resolveExports (node:internal/modules/cjs/loader:547:36)
    at Module._findPath (node:internal/modules/cjs/loader:621:31)
    at Module._resolveFilename (node:internal/modules/cjs/loader:1034:27)
    at Module._load (node:internal/modules/cjs/loader:901:27)
    at Module.require (node:internal/modules/cjs/loader:1115:19)
    at require (node:internal/modules/helpers:130:18)
    at Object.<anonymous> (/Users/name/project-name/node_modules/react-scripts/node_modules/postcss-safe-parser/lib/safe-parser.js:1:17) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'

package.jsonのexportフィールドに問題がある可能性が高いため重点的に調べていると、Node19にてexportフィールドへの非推奨の記法が登場したことが判明。そこでpostcss/package.jsonを見てみました。

 "exports": {
    ".": {
      "require": "./lib/postcss.js",
      "import": "./lib/postcss.mjs",
      "types": "./lib/postcss.d.ts"
    },
    "./": "./"
  },

特に間違ってはいないですね。
結局根本的な原因はわからずでしたが、react-scriptsを5系に挙げる事で解決しました。(どうせ後からreact-scriptsを5系にあげようと思っていたので。)
react-scriptsの5系からはdependencyにpostcss-safe-parserが入らなくなったようで、postcss-safe-parserの5.0.2バージョンがnode_moduleから削除されました。エラーの原因となっていたファイル自体が消えたことで解決したようです。
Node15から20のいずれかでpackage.jsonのexportフィールドに関する変更があった可能性が高いので、引き続き調査したいと思います。

TypeScript

react-scriptsを5系に上げる際に他のモジュールのバージョンも上がりました。その影響で、reactのサーバーを立ち上げようとするといくつかのモジュールにおいて型の不一致が起こってしまいました。
プロジェクトのTypeScriptのバージョンはそのままで、いくつかの型定義ファイルのみバージョンが上がってしまった事が原因のようです。

../node_modules/@types/react-router/node_modules/@types/react/ts5.0/index.d.ts:3285:50 - error TS1005: '(' expected.

3285         | `${OptionalPrefixToken<AutoFillSection>}${OptionalPrefixToken<
../node_modules/@types/express-serve-static-core/index.d.ts:93:68 - error TS1110: Type expected.

93 type RemoveTail<S extends string, Tail extends string> = S extends `${infer P}${Tail}` ? P : S;

こちらはTypeScriptを5系に上げることで解決させました。(どうせ後からTypeScriptを5系にあげようと思っていたので。)
ちなみに、TypeScript5系でreact-scripts5系を動かすことはできるようですが公式からの発表はないため、やはりcreat-react-appは卒業した方が良さそうですね。(参照

storybook

storybookを6から7系にアップグレードしました。
理由はtypeScriptとNodeのバージョンアップによってstorybook関連のモジュールとの依存関係エラーが多く出たためです。

@react/type styled-components

typescriptやreact-scriptsのバージョンアップに伴い、@react/typeとstyled-componentsのバージョンを上げる方針が適切かと思います。
自分は今回のPRにその変更を加えたくなかったためバージョンはそのままにしましたが、上げる際はプロジェクトのコードに多くの変更を加える必要が出てきます。

まとめ

個人開発ですらツールのバージョン上げの経験がなかったので、何もわからない状態からの挑戦でしたが、なんとかリリースする事ができました。 脆弱性もだいぶ減ったかなと思います😊

before
Severity: 6 Low | 285 Moderate | 354 High | 199 Critical

after
Severity: 8 Moderate | 6 High