Node.jsを安全にアップデートするためにやったこと

# はじめに

はじめまして、リンクアンドモチベーションのプラットフォームチームに所属している菊池です。
弊社で開発しているモチベーションクラウドのフロントエンドビルド環境は長らくNode.js 8を使っており、「さすがにやばい」という感覚と、シンプルにモダンなライブラリをインストール出来ない、セキュリティリスクなどの観点から当時のActive LTSである14系へのアップデート作業をすることになりました[^1]。

[^1]: 2021年12月現在のActive LTSは16となっています。https://nodejs.org/en/about/releases/

しかしながら、（みなさんもご存知の通り）Node.jsのアップデート作業は簡単には終わりませんでした。
今回は１度の失敗を経て、安全にアップデートするためにやったことを紹介したいと思います。

※ この記事は[モチベーションクラウドシリーズアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/mcs)の3日目の記事です。

# １度目のアップデート

１度目のアップデート作業ではNode.jsのアップデートに合わせて、関連するライブラリのアップデートも行いました。
というのもNode.jsのバージョンが変わるとうまくビルドできないライブラリがあり（たとえばnode-sass）、それらを含めてライブラリ全体を更新するためです。
具体的には[yarn upgrade](https://classic.yarnpkg.com/lang/en/docs/cli/upgrade/)コマンドを使用しました。

```shell
yarn upgrade
```

このコマンドはpackage.jsonに指定されている依存パッケージを[semver](https://docs.npmjs.com/about-semantic-versioning)の範囲内で最新のバージョンに更新します。

また同時期にESLintの整備も進めており、Node.jsアップデート後に対応可能なルールの導入と、そのコード修正も合わせて行いました。
これは先のライブラリ更新に伴ってUIのリグレッションテストを実施した方が良いという判断もあり、どうせリグレッションテストをするなら一緒に混ぜ込んでしまおう、と考えたのです。

軽く動作確認したところではいい感じに動作し、「お、いけそう」と思っていたのですが、、

# 失敗する

このアップデート作業は結果的には失敗しました。
リグレッションテストを実施したものの、リリース後に一部のUI崩れや挙動不具合が判明したのです。

テストに漏れがあったとも言えますが、それ以前に以下の過程に問題があったと考えました。

① Node.jsのアップデートに合わせてライブラリの全更新を行ったこと

フロントエンドの依存パッケージを一括更新したことで、UIライブラリに影響が及んでしまいました。
具体的には、[Vue-Multiselect](https://vue-multiselect.js.org/)というライブラリを使用しており、ライブラリのCSSを読み込んでいた箇所がアップデートによってパスが変わり、上手く読み込めなくなっていました。

② 別の修正を「検証方法が同じ」という理由で一緒に混ぜてしまったこと

今回の場合は「Node.jsをアップデートする作業」に「ESLintへの対応」を混ぜてしまいましたが、この2つは目的も緊急度も全く異なります。
検証という過程が同じという理由で混ぜてしまうと、影響範囲が余計に広がるだけでなく、問題があって切り戻したときに（実際に今回それが起きましたが）いずれもやり直さなくてなりません。

そこで、より安全にアップデートするために「影響を減らす」方法を再検討しました。

# どうなっていれば影響を減らせるか

改めて、影響範囲が小さくなる理想パターンを洗い出しました。それが以下の表です。
![スクリーンショット 2021-12-02 10.16.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939205/548b764c-f8fc-17cc-3e82-6cbfdbcffd59.png)
Node.jsはあくまでビルド時の実行環境であり、そのバージョンが変わっても出力結果（HTML/JS/CSS/etc..）が同じであればブラウザでの動作は変わらないということです。
出来るだけ影響を減らす（表の①に近づける）ため、以下の対応方針を取ることにしました。

- アップデートに必要な最小限のライブラリのみアップデートする
- Node.jsアップデート以外の修正は別で対応する

# 最小限のライブラリのみアップデートする

色々と調査した結果、Sassのビルドツールである[node-sass](https://github.com/sass/node-sass)だけアップデートすれば良いことが分かりました。


```package.json
{
  "devDependencies": {
    ...
    "node-sass": "^4.14.1",
    ...
  }
}
```

ただし、単にpackage.jsonを上記のように変えてもアプリケーション直下のnode_modules/node-sassは更新されますが、依存パッケージが使用しているnode-sassまでアップデートすることは出来ません。

この「依存パッケージの依存パッケージ」のバージョンを指定するためにはYarnの[selective version resolutions](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/)が利用できます[^2]。

[^2]: npmでもライブラリを使うことで可能なようです。https://github.com/rogeriochaves/npm-force-resolutions

```package.json
{
  "devDependencies": {
    ...
    "node-sass": "^4.14.1",
    ...
  },
  "resolutions": {
    "node-sass": "^4.14.1"
  }
}
```

これでnode-sassのみアップデートすることが出来ました。

# 2度目のアップデート

この状態でビルドを行い、ビルド出力結果を比較しました。
その結果、差分が発生したのは以下の内容でした。

① 一部の正規表現からバックスラッシュのエスケープがなくなった

Before

```js
match(/([^\\/\\\\]+)\\.vue$/)
```

After

```js
match(/([^/\\\\]+)\\.vue$/)
```

JSでは正規表現の`[]`の中ではスラッシュはエスケープ不要なので、おそらくNode.jsのバージョンアップに伴い出力が修正されたのだと思われます。

② CSSのtransparent表現が変わった

Before

```css
box-shadow: 0 0 0 0 transparent;
```

After

```css
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
```

実質CSSの表現としては変わらないため、おそらくnode-sassのバージョンアップに伴い出力が変更されたのだと思われます。

とまあ、差分ゼロとまではいかないものの、コードの出力結果の差分を大分減らすことができました。

この時点でほぼほぼ問題は起きないだろうと考えていましたが、
とはいえ変更差分があるにはあるので、念の為リグレッションテストをして、2度目のリリースを行いました。

結果、問題も発生せずに無事リリースを完了することが出来ました。

# まとめ

今回のNode.jsアップデート作業で改めて感じたのが「小さくリリースすること」「影響範囲を減らすこと」の大事さです。
つい「どうせならあれも入れよう」「あれも一緒にやっちゃおう」となってしまいがちですが、本来の目的や緊急度を整理して、本当に一緒にやるべきか、後から対応できないか、考えてみるのが良いと思います。