Edited at

あなたがnpm installをしてはいけない時


概要

あなたは普段、何気なく npm install を使っていることでしょう。


しかし、 npm install が何をしているのか、実は誤解している人も多いと思います。

記事のタイトルは釣りではないので、どんな時に npm install は問題を起こすのか、説明できない人は以下を読み進めてください。これは多くの開発者が無意識に無視している、とても重要な事項だと思っています。

なお、npm 4.x系以下の方は本記事の対象ではありません。

追記: 強めに書きすぎて誤解を招く部分があったので何度か修正しています。


参考資料:


npm

npm はパッケージマネージャです。パッケージマネージャは、開発に必要なパッケージ(ライブラリとか、プラグインとか色々)を管理するためのツールです。全ての人が、全く同じ開発環境を再現するためにとても重要な役割を果たしています。

開発を始める時、 npm init コマンドによって package.json が追加されます。ここに、使用しているパッケージの名前とバージョンに関する情報が書かれていることは、おそらくこの記事を読んでいる人はだいたい知っているのではないでしょうか。


パッケージの追加

新しく、 xxx というパッケージを追加するとき、 npm install xxx --save を実行すると、 package.jsondependencies が更新されます。 これと同時に、 package-lock.json というファイルが追加されます。今回の話の主役はこのファイルなのですが、まずは package.json に書かれている内容の曖昧さについて知ってもらうために説明を続けます。


semantic versioningの範囲指定

xxx の最新バージョンが 1.0.0 の場合、package.jsondependencies には、次のような行が追加されます。

"xxx": "^1.0.0"

これが意味するところは、 1.x.x に該当する最新のバージョンという意味になります。

ですので、次にこの package.json を使って npm install するまでに xxx のバージョン 1.0.1 が公開されていた場合、次に npm install を行う人の開発環境には 1.0.1xxx がインストールされることになります。ここまでは、ご存知の方が多いと思います。

では、どのタイミングでも同じ 1.0.0xxx をインストールできるようにするにはどうすればいいと思いますか?


dependencies のバージョン固定

package.jsondependencies を、 "xxx": "1.0.0" にすれば、確かに xxx のバージョンは 1.0.0 になりそうですね。

ですが、これがまず大きな誤解の1つです。なぜなら、 xxx1.0.0package.json には、 次のような dependencies が書かれている可能性があるからです。

"yyy": "^1.0.0"

勘がいい人ならわかると思いますが、 xxx1.0.0npm install しようとした時に、 yyy の新しいバージョンが公開されていた場合どうなるでしょうか。あなたは同じ xxx1.0.0 をインストールしたと思うかもしれませんが、 yyy の品質によっては全く別の、バグや脆弱性が含まれたバージョン 1.1.0 が公開されているかもしれません。

(ちなみにこれは実例がありますし、よくテストされていないパッケージだと起こる可能性が十分にあります。また、npmの制約によって公開から72時間以上たった場合基本的に削除/unpublishができなくなるため、バグとして全世界から参照され続ける可能性もあります。)

これで npm installpackage.json だけでは、全ての人が全く同じ開発環境を再現することが不可能だということをご理解いただけたと思います。


同じ開発環境の再現

ちなみに、難しいことを考えずに同じ開発環境を再現しようと思った場合、特に何も考えなくても、パッケージがインストールされている node_modules ディレクトリをリポジトリにpushしておけば、別に新たな参加者が来た時にも追加で手順など必要ない状況を作ることも可能です。が、プロジェクトの規模が拡大するにつれ、 node_modules ディレクトリはheavyになります。(以下僕が好きな画像です 引用元

こんなに重たいものをリポジトリにpushするなんて正気の沙汰ではないわけですが、先に書いた package-lock.json というのが、基本的にはこの node_modules ディレクトリを完全再現することが可能なので、 node_modules をpushする代わりに package-lock.json をpushするわけです。

ですので、 package-lock.json は同じ環境を再現するために必要で、だからリポジトリにpushしたほうがよいというところまではみなさんなんとなくご存知だと思います。


npm installpackage-lock.json を上書きする

よし、 package-lock.json があるから安心だ。 npm install をすれば全く同じ環境が再現するぞ!と思っている人はそうならない場合に関して考慮する必要があります。実際、 package-lock.json が存在しても npm install はそれを上書きする場合があります。つまり同じ開発環境を構築したい時に、 npm installは使えない可能性があります。

可能性、というのは、npmのバージョンによる微妙な npm install の挙動の違いや、様々なユースケース、人為的なミスが存在するため同じ開発環境を構築することができない可能性について指しています。仮に誰かがpackage.json だけ書き換えてしまった、 package-lock.jsoncommitpush を忘れた、コンフリクトの解決が適切でなかったなどの場合で容易に起こります。

記事のタイトルの答えはもう出てしまいましたが、まだ問題が解決していないので読み進めてみてください。


package-lock.json からインストールする

もうお分かりだと思いますが、全く同じ環境を再現しようとした場合、 package-lock.json から node_modules ディレクトリを構築する必要がありますが、 npm install はそれをやってくれません(注: 絶対ではないので強めに書いています)。 npm ciを使いましょう。

詳細に関しては冒頭に添えた参考資料を参照してほしいのですが、 npm ci



  1. node_modules ディレクトリの削除


  2. package-lock.jsonpackage.json の整合性のチェック


  3. package-lock.json から node_modules を再現

という動作をします。また、 npm install のように勝手に package-lock.json を更新することはなく、なんども同じ node_modules を再現することができます。これでようやく、全く同じ環境を再現することができるようになりました。


パッケージのインストール

これらのことを踏まえると、新しいプロジェクトに参加する場合でも package-lock.json が正しくコミットされていれば、 npm install ではなく npm ci を使った方が良いです。 npm ci が失敗した場合、上の事柄を正しくチームに共有してあげてください。

余談ですが一般的に ci というと Continuous Integration を指しますが、動作的には Clean Install の略ともとれそうです。(これは想像です)


パッケージのアップデート/追加

ドキュメントにもある通り npm cipackage-lock.json を更新しませんし、個別にパッケージをインストールすることもできません。

ですのでバージョンをアップデートしたい場合には、 package-lock.json を更新することができる npm installnpm update などを使います。また、パッケージを追加したい時も今まで通り npm install zzz --save などとすれば大丈夫です。ただしこの時、 package-lock.json が更新されたら必ず変更点や動作に問題がないかの確認と、リポジトリへのpush、チームメンバーへの npm ci の実施を伝えるのがベストです。

ちなみに、とても想像力の豊かな人だと、この npm install によって依存パッケージの依存パッケージ...がバグを含んだ更新をしていた場合、どのように対処すればいいのか気になった方もいるのではないでしょうか。


依存パケージの依存パケージ(以下略)のバージョンを固定する方法

実は npm にはこの機能はありません。(あったら教えてください

が、 yarn にはあります。 package.jsonresolutions というフィールドを追加し、そこにパッケージのバージョンを指定することで依存ライブラリの依存ライブラリ()のバージョンを指定することが可能です。 参考までに


おわりに

ちなみにセキュリティ的な観点で言いますと、これらに関して理解が浅く、対策がされていないプロジェクトの場合、そのプロジェクトが使っていそうなライブラリをソースコードから特定し、そのライブラリに悪意のあるコードを埋め込みパッチバージョンリリースなどすると次回のリリースで汚染される可能性がありそうですね。

ぜひ現在の開発フローを見直してみてください。では!