
package.jsonのdependenciesをアップデートする技術
Posted on
デザインエンジニアの安田(@_yuheiy)です。
プレイドの多くのシステムでは、サーバーサイドの実行環境としてNode.jsを採用しています。そのため、サーバーサイドもクライアントサイドもまとめてnpmを使って依存パッケージ(dependencies)を管理している場合がほとんどです。
システムのメンテナンスにおいては、このような依存パッケージのバージョンを継続的にアップデートし続けることが重要です。そこで今回は、社内向けに実施した、依存パッケージのバージョンアップをテーマにした発表を一部再編集してご紹介します。
なぜ依存パッケージのアップデートが必要か
筆者が所属するDeveloper Experience & Performanceチーム(通称: DXチーム)では、その活動の一つとして、さまざまなシステムの依存パッケージのバージョンをアップデートして回るような作業をする機会がよくあります。DXチームでは、現在アクティブに開発が行われていない一部システムのメンテナンスを担当したり、既存のチームの開発をヘルプしたりする場面があり、その一環として併せてこのような作業を行っています。
依存パッケージをアップデートすることには、おもに次のような理由があります。
まず最初は、Node.jsのバージョンアップに対応するためです。Node.jsのバージョンアップにともなって古いバージョンの依存パッケージが動作しなくなることがあるので、Node.jsに追従するために必要な作業です。
次は、一般的なソフトウェア品質や開発効率の向上のためです。多くのパッケージはバージョンアップにともなって、動作上のバグの修正やパフォーマンスの改善、セキュリティ上の問題の解決が行われているほか、便利な新機能が追加されたりしています。これによってソフトウェアの外部品質が改善したり、余計な実装が不要になって設計がシンプルになったりする可能性があります。また、古いバージョンのパッケージを使い続けていることで、それに携わる開発者が古いバージョンの使い方を調べたり覚えたりといった無駄な学習コストを払わなければならないという問題もあります。
デザインシステムの最新化のために
もう一つの理由は、利用されているデザインシステムを最新化するためです。社内向けのデザインシステムであるSourが多くのシステムに導入されていますが、Sour自体の内容は日々アップデートされ続けている一方、各システムで利用されているSourのバージョンはアップデートされずに古いものが使い続けられていることが少なくありません。
参考までに、KARTEのリポジトリにおいて使用されているSourのバージョンは次の通りです(件数は発表時のもの)。
- 4.x: 5件(うち3件はDXチームが管理するシステム)
- 3.x: 5件
- 2.x: 11件
- 1.x: 2件
- 0.x: 1件
筆者の本務はSourの開発ですが、ユーザー側でのバージョンアップが行われなければ開発している意味がありません。そのため私自身がそのアップデート作業を担当することもありますが、それに併せてその周辺環境を整備しなければならない場面もあります。というのも、Sour自身が依存しているパッケージのアップデートにともなって、ユーザー側の関連するライブラリのアップデートをしなければならなくなったり、もしくは、そのようなアップデートにともなう問題が発生しないかをテストするための環境が必要になったりすることもあるからです。
タイミングが遅れるほどアップデート作業が難しくなる
一般的に、依存パッケージが古くなればなるほど対処が必要な問題が増えて複雑化し、作業が難しくなる傾向にあります。特に古いシステムをメンテナンスしていると、複数の古い依存パッケージ同士が絡み合う問題が発生してデバッグが難しくなったり、古いバージョンで想定されていなかった環境依存の問題が発生して解決のために余計な手間を取られたりすることがよくあります。
また、関連する機能を担当した当時の開発者がチームや会社を離れたりしていることもあります。アップデートにともなう動作確認が必要になることがありますが、そのような場合、期待される正しい仕様や動作確認の方法がわからなくなる可能性もあります。あるいは、当時の担当者にしか意図を理解できないトリッキーな実装が行われているような場合もあります。
「システムのメンテナンスは天下の回りもの」
これは昔から個人的に唱えている理論です。システム開発の仕事をしていると、知らない誰かが作ったシステムを回り回って自分が引き継ぐというような場面は珍しくありません。そして、自分が作ったシステムもいずれまた別の誰かに引き継がれていくことになります。
システムの出来の良し悪しにかかわらず、引き継いだ人にはそのすべてを引き受ける責任が生じます。だからこそ、後任の誰かのために、自分が今担当しているシステムはできるだけ良い状態にしておくという心づもりでいることが大事だと思います。
作業の進め方
具体的な話に移ります。
まず、これから説明するような作業にあたっては、さまざまな観点でのソースコードの変更をしなければならない場合があります。そのため、必ずしも1つのpull requestで一気に対応しようとするのではなく、適切な粒度に分解しながら作業を進めてください。これについては普段の機能開発と同じです。
次に、CIが不十分だと感じる場合はあらかじめ整備しておくのがよいです。作業を細かく分割するほど、人力ではチェックの抜け漏れが発生しがちです。変更にともなうテストを効率的に行うために、できるかぎり自動化してしまいましょう。
不要な依存パッケージを削除する
アップデートの前に、不要な依存パッケージがあればあらかじめ削除しておきます。使わないものをアップデートしても仕方がないからです。
Knipを導入することで、未使用の依存パッケージやデッドコードの検出ができます。これらを削除することで余計な要素を削れます。さらに、KnipをCIに組み込むことで、今後余計なものが追加されないように予防することもできます。
また、そのほかにも不要な機能などが残っていれば削除しておきます。
言語の標準機能に置き換える
Node.jsのアップデートにともなって、これまでサードパーティのパッケージが必要だったことが、Node.jsのコアの機能として実現できるようになっていることがあります。「Node.js の進化に伴い不要となったかもしれないパッケージたち」などを参考にして置き換えてください。
また、ECMAScriptやDOM仕様のアップデートにともなって、同じくサードパーティのパッケージが不要になることもあります。「You-Dont-Need」などが参考にできます。
package.jsonのアップデート
package.json
に記述されているdependencies
やdevDependencies
のバージョンをアップデートする際には、次のようなコマンドを使用すると一気に書き換えができて便利です。--interactive
オプションを指定することで、アップデートする対象をチェックボックス方式で選択できます。
npmの場合:
cd path/to/monorepo-root
npx npm-check-updates --workspaces --root --interactive
pnpmの場合:
cd path/to/monorepo-root
pnpm update --latest --recursive --interactive
pnpm 10.12以降であれば、catalogsで管理しているバージョンもいっしょにアップデートできます。
またロックファイルは壊れやすいので、インストールに際して問題が発生した場合は再生成してみるとよいです。
rm -rf package-lock.json **/node_modules && npm install
破壊的変更への対応
セマンティックバージョニングにおいては、メジャーバージョンが上がるか、もしくは1.0.0に満たない場合のバージョンアップでは、常に破壊的変更の可能性があります。そのためアップデートをする前には、ざっとでもいいのでパッケージのCHANGELOGに目を通しておくことをおすすめします。
また、最新版よりもいくつも古いバージョンを使っている場合は、破壊的変更が積み重なって簡単にアップデートできないことがあります。そのような場合、バージョンは一気に上げるのではなく、ある程度の区切りに分けて段階的にアップデートしたほうが作業が簡単になります。
後継のライブラリに置き換える
場合によっては、あるパッケージの代わりとなる後継のパッケージ(ライブラリ)が登場しているがあります。既存のパッケージがメンテナンスされなくなったり、アーキテクチャが古くなって根本的な刷新が必要になったりしたときに、それを置き換えるために別のものが開発されて、コミュニティでもそちらが主流になっていくというパターンです。
KARTEでよく使われているところで言えば、次のような後継のパッケージがあります。
- Jade → Pug(参考: Renaming jade -> pug · Issue #2184 · pugjs/pug)
- ts-node → tsx(もしくは、Node.jsのtype strippingを使う)
- Jest → Vitest
- Recoil → Jotai
- Lodash → es-toolkit
- npm-check-update + patch-package + concurrently → pnpm CLI(同等の機能がpnpmに組み込まれている)
既存パッケージのアップデートと併せて、後継のパッケージへの移行も行いましょう。
気になる部分はついでに改善しながら進める
パッケージのアップデート作業をしていると、それに関係するほかの部分が気になることもあります。気になった部分についても、そのままにしておかず、せっかくなのでついでに改善してしまうのがよいです。たとえば次のようなものです。
- CIの構成をきれいにする
- いらないnpm scriptsを消す
- 冗長な設定ファイルをクリーンアップする
- deprecatedなメソッドの使用箇所を直す
- 破壊的変更がある箇所の周辺のコードをリファクタリングする
このようなメンテナンス作業は普段は軽視されがちで、ともすれば老朽化した仕組みがそのまま何年も動き続けていることになりがちです。だからこそ、みなさんにはぜひボーイスカウト・ルールを意識してもらいたいです。
ボーイスカウトには大切なルールがあります。それは、「来た時よりも美しく」です。たとえ自分が来た時にキャンプ場が汚くなっていたとしても、そしてたとえ汚したのが自分でなかったとしても、綺麗にしてからその場を去る、というルールです。そうやって、次にキャンプに来る人達が気持ちよく過ごせるようにするのです(このルールは元々、ボーイスカウトの父と呼ばれるロバート・スティーブンソン・スミス・ベーデン=パウエルの「自分が最初に見た時よりも、世界をいい場所にすべく努力しよう」という言葉から来ています)。
これに倣ってコーディングに関しても同じルールを定めるとしたら「モジュールをチェックインする際には、必ずチェックアウト時よりも美しくする」となります。最初にそのモジュールを書いたのが誰であるかに関係なく、皆がそうやって、たとえ少しずつでもモジュールを改善する努力を続けたとしたら、その結果どういうことが起きると思いますか?
出典: Kevlin Henney編、和田卓人監修『プログラマが知るべき97のこと』、オライリージャパン、2010年
こまめなアップデートを習慣化する
アップデート作業は後回しにするほど面倒な問題を引き起こしがちです。善は急げと思って、まずは今日からやってみましょう。
いったんひと通りのアップデートができたら、それ以降も定期的にアップデートを続けられるように、次に作業する日を決めてしまいましょう。月に1回程度が目安です。もしくは、Dependabotなどの自動化ツールを使って作業を自動化するのもよいと思います。