本記事は「珠玉のアドベントカレンダー記事をリバイバル公開します」企画のために、以前Qiitaに投稿した記事をブラッシュアップし、フューチャー技術ブログに転載したものになります。Qiita側の元記事もアップデートしています。
はじめに
package.json の dependencies をメンテナンスするにはどこから手を付ければいいか、を解説します。
Node.js を使っている人にはおなじみ package.json。
package.json の中で一番よく更新されるのが dependencies(個人の感想、次点で scripts)。
そして、依存パッケージが着々とバージョンアップしていくにも関わらず放置されてしまって後々問題になりがちなのも dependencies 。
「npm install で追加したっきり。パッケージのアップデートなんて考えたことなかった」という人や「GitHub から security alert が届いてるけども見て見ぬふりをしている」という人向けに、package.json の dependencies をアップデートして、依存パッケージの最新版に追従していく方法について、個人の考えを書いてみます。
npm install したときの dependencies
まずはおさらい。
npm install lodash |
とすると、package.json の dependencies に以下の一行が追加されます。
"lodash": "^4.17.19" |
すこし古い記事とかだと「オプションに --save
をつけること」と書いていたりしますが、npm 5.0.0 以降デフォルトで save オプションが付くようになったので書かなくて大丈夫。
依存パッケージをメンテせず放置すると
npm install したパッケージをアップデートせずにいると、新しく追加された機能が使えないのはもちろんのこと、新しいバージョンで修正された脆弱性がそのまま残ってしまう危険があります。
特に npm からインストールしたパッケージの場合、パッケージが依存しているパッケージの、さらにその中で依存しているパッケージに脆弱性が見つかった、みたいなことも多々。2020年7月に見つかった lodash の脆弱性(CVE-2020-82031)が起因で 500 万件以上の GitHub Dependabot のアラートが発生したとか。2
Node.js で書かれたパッケージは大体何かしらのパッケージに依存しているので、汎用的なパッケージに脆弱性が見つかると影響する可能性が非常に高いです。
そして、GitHub Dependabot からアラートが来たときに慌ててバージョンを一気にあげてしまうと、いままで使っていた機能がいつの間にか廃止されてアプリが動かなくなった、みたいなこともよく起こるので、出来れば日頃からこまめに上げていきたいところ。
npm outdated && npm update の利点・欠点
では、パッケージをアップデートしよう、となったときによく使われるコマンドが以下の 2 つ。
npm outdated |
npm outdated
で最新バージョンがあるかどうかを確認して、npm update
で outdated なパッケージを一括更新します。
ただし注意点があります。
npm update では package.json の更新はしません。package-lock.json と node_modules の更新だけ行います。
加えて、^
付きのバージョン指定でもメジャーバージョンのアップデートまでは行いません。
これは npm update
だけでなく npm audix fix
も同様です。
npm update では package.json の更新をしない
例えば package.json の dependencies に下のように書きかえてから、npm install してみましょう。
"lodash": "^4.10.0" |
$ npm install |
lodash の 4.10.0 ではなく、4.17.19 がインストールされています。
そして、package-lock.json には以下のように記載されます。
"packages": { |
そうこうしている間に lodash が 4.17.19 から 4.17.20 へとアップデートされました。
$ npm outdated |
無事アップデートできました。package-lock.json 内も "version": "4.17.20"
となっています。
しかしながら、package.json の中は "lodash": "^4.10.0"
のままです。
^ 付きのバージョン指定
そもそも、^
付きのバージョン指定とは何か。
npm install から dependencies に追加したパッケージには "^4.17.19",
のように ^
(キャレット)がくっついています。
^
が付いていると最新版を取ってきてくれそうな感じを受けますが、微妙にニュアンスが違います。
この ^
は互換性のあるバージョンという意味。互換性のあるバージョン内、つまりメジャーバージョンアップを避けてアップデートを行います。jQuery 2.x.x を使っている場合、3.x.x に上げず 2.x.x の中で最新版をとってくるわけですね。
互換性のあるバージョンというのがまた微妙に複雑。
^0.2.3
の場合、0.2.x 内で最新版を探す(0.3.x までは上げない)^0.0.3
の場合、0.0.3 内(-bata 付きとか)で最新版を探す(0.0.4 までは上げない)^0.2.3 -beta2
の場合、0.2.3 内(-bata3 とか)で最新版を探す(0.2.4 までは上げない)
などのルールがあるのですが、長くなるので詳しく知りたい方は公式3を。
メジャーバージョンアップができないというのが親切ながらも微妙に厄介なところ。"lodash": "^3.10.1"
のときに npm update
をしても 4.17.20 には上がらず、先の脆弱性 CVE-2020-8203 は解消しません。
メジャーバージョンのアップデートを行う @ latest
npm update ではメジャーバージョンアップができない。最新版にするにはどうすればいいか。
答えの1つとしては以下。
npm install <package-name>@latest |
@latest
を末尾に付けるとメジャーバージョン含めた最新版に更新してくれます。package.json も更新してくれます。
複数インストールするときは並べて書きます。
npm install <package-name1>@latest <package-name2>@latest |
しかしながら、1 つ 1 つパッケージ名を打ち込んで @ latest をつけていくのは面倒です。
npm audit fix –force
npm install したときに脆弱性があると出てくる npm audit fix --force
も答えの1つです。こちらも package.json 内の "lodash": "3.10.1"
を "lodash": "^4.17.20"
に書き換えます。
メジャーバージョンアップや ^
も無視して強制的にアップデートしてしまう、--force
の付いた npm オプションです。
脆弱性が出てきたときの最終手段といった趣がありますね。
脆弱性があったときにしか使えないので日常の運用でこのコマンドを頼るのは悪手。もちろんメジャーバージョンアップをまたぐので、作ったアプリが動かなくなる可能性も十分あります。
日頃からこまめにパッケージのバージョンアップを確認しつつ、メジャーバージョンアップに対応しておきたいところです。
アップデート可能なパッケージ一覧表示・更新する ncu
日頃からこまめにパッケージのバージョンアップをするのに便利なのが、npm-check-updates。略して ncu。
依存パッケージを見て、アップデート可能なものを一覧にしつつ、package.json の dependencies をメジャーバージョン含めて最新版に書き換えてくれる便利な子です。
使い方は簡単です。
npm install -g npm-check-updates |
とするだけ。そうすると、 アップデート可能なパッケージを一覧にしてくれます。
$ ncu |
npxコマンドとしても呼び出せるのでお試しで使ってみたい場合はこちらでも。
$ npx npm-check-updates |
package.json の dependencies を最新版に書き換えたいときは以下。
ncu は package.json の更新だけするので、パッケージと package-lock.json を更新するために npm install はも忘れずに。
ncu -u |
特定のパッケージだけ最新版に書き換えたいときは、-u の後にパッケージ名を指定すれば OK。
ncu -u lodash |
ncu を覚えておけば、依存パッケージをさくっと最新バージョンにできます。
どのパッケージからアップデートしていくか
依存パッケージが全部最新版になったよ、良かったね。で、終わればラッキー。長らく package.json を更新していない場合、動かしてみると何かしら不具合が出るのではないかと。
理由はおそらくいろいろと。いつのまにか webpack loader のオプションの渡し方が変わってただとか、使っていた function が unstable_function にリネームされているだとか、機能ごと廃止されているだとか。
長らく蓄積された度重なるアップデートに追いつくのは大変ですが、一度元に戻して、少しづつ切り崩していきましょう。
パッケージのナンバリング
幸いにも npm のパッケージバージョンは規則が設けられています。
例として先ほど追加した lodash を見てみましょう。
"lodash": "^4.17.19" |
- メジャーバージョンアップ
一番左の4
は大きな機能追加や仕様変更、根本から変更する場合などに数字が 1 増えるというのがメジャーバージョンアップです。メジャーバージョンアップのときに機能が廃止になって動かなくなったとか、いままでと動きが変わったとかいうことが起きやすいので、該当ライブラリをアップデートした後にちゃんとテストをしておきましょう。メジャーバージョンアップしたパッケージは1つアップデートしたあとに動作確認しておくのが吉。横着してまとめてやろうとすると大変つらい思いをします(しました)。 - マイナーバージョンアップ
真ん中の17
がマイナーバージョンアップです。便利オプションが増えたよとかちょっとした機能改善系のアップデート。これまでと動きが変わる、ということは起きにくいので、他のライブラリと一緒にまとめてアップデートして動作確認で十分かなという感じです。 - パッチバージョンアップ
一番右の19
はパッチバージョンアップです。基本的にバグ修正。こちらもまとめてアップデートで大丈夫という印象。
上で紹介した ncu
を使うと、
- メジャーバージョンアップがある場合は赤
- マイナーバージョンアップは水色
- パッチバージョンアップは緑
で、表示してくれます。
まずは水色・緑で表示されているパッケージをまとめてアップデートしてから動作確認して、いつでも戻れるように Git commit してからメジャーバージョンアップの赤色に取り掛かるのがよさそうだなという経験則があります。
ncu
で -t patch
をつけるとパッチバージョンアップのみを、 -t minor
をつけるとマイナーバージョンアップとパッチバージョンアップをターゲットにしてくれるので、こちらも覚えておくと便利。
ncu -u -t minor |
また、メジャーバージョンアップは破壊的変更にあわせてコードの修正が入ることも多いので、バージョンを上げてうまく動作したら1パッケージごとに Git commit をしていきましょう。commit 打ち忘れると切り戻したいときに大変です(n 敗)。
破壊的変更の確認
パッケージのバージョンアップ時にどのような変更が入ったかは、基本的に各パッケージのReleasesページを見るしかないです。
しかしながら、動作の変わるような大きい変更が入る場合は Breaking Change の見出しとともにしっかりとアナウンスをしている場合が多いですし、親切なパッケージであれば代替手段の紹介やコードの自動修正ツールを用意してくれていることもあります。
お知らせやアナウンスといったようなものが見当たらないときは、残念ですが README やPR、issue などを見ながら変更箇所を調査しましょう。
いっそのこと別のパッケージを探すのも手。当時なかった便利パッケージや使いやすい loader が新たに生まれていることもあります。
まとめ
dependenciesのメンテナンスは ncu
でアップデートを確認して、マイナー・パッチバージョンアップを片付けてからメジャーバージョンアップを慎重に行う。
月一を目安に最新バージョンへの追従を心掛けておくと、GitHub から security alert が届いたときに冷静に対処できるのではないかと。
チームで脆弱性の管理などをしている場合、脆弱性の管理が追いつかなくなりがち。
npm パッケージだけでなくサーバの脆弱性の可視化やチケット管理を行える SaaS を Future から提供している4ので、ぜひ検討してみてください。
- 1.https://jvndb.jvn.jp/ja/contents/2020/JVNDB-2020-008656.html ↩
- 2.https://www.zdnet.com/article/open-source-software-security-vulnerabilities-exist-for-over-four-years-before-detection-study/ ↩
- 3.https://docs.npmjs.com/cli/v6/using-npm/semver#caret-ranges-123-025-004 ↩
- 4.チームで脆弱性を管理するFutureVuls( https://vuls.biz/) ↩