package-lock.jsonとは?Yarnとnpmのパッケージでロックファイルが機能する仕組み
2019年3月14日
0 分で読めますpackage-lock.jsonとは?
この記事では、npmのパッケージロックファイル「package-lock.json」と、Yarnの「_yarn.lock」の両方について解説します。パッケージロックファイルは、プロジェクトの依存関係を詳細に記録したマニフェストです。インストールする依存関係の正確なバージョンだけでなく、その依存関係が持つ依存関係なども指定し、依存関係ツリー全体を網羅します。
パッケージロックファイルは、プロジェクトで初めて依存関係をインストールするときに作成されます。インストール時に依存関係ツリー全体が計算され、各依存関係に関する次のようなメタデータとともにロックファイルに保存されます。
インストールするパッケージのバージョン
パッケージが改ざんされていないことを確認するための整合性ハッシュ
パッケージの取得元と、今後のインストール時に取得する場所を示すレジストリの解決先

ロックファイルが必要な理由
ロックファイルは、作成時点で依存関係ツリー全体のすべてのバージョンを固定するためのものです。パッケージロックファイルを使ってパッケージのバージョンを固定することが重要なのはなぜでしょうか?
パッケージロックファイルがない場合、Yarnやnpmなどのパッケージマネージャーは、特定のパッケージで本来意図されていたバージョンではなく、依存関係のインストール時点で最も新しいパッケージのバージョンをリアルタイムで解決します。

たとえば、プロジェクトがパッケージ dummy-pkg: ^1.0.0 に依存している場合、別々の時点で実行した2回のインストールで、異なるバージョンのdummy-pkgが取得される可能性があります。あるユーザーが`dummy-pkg`をインストールしてバージョン1.0.0を取得した後、数分後にそのパッケージの新バージョン1.0.1がリリースされるといったケースです。その後、プロジェクトでインストールを実行した2人目のユーザーは、バージョン1.0.0ではなく、`dummy-pkg`のバージョン1.0.1を取得することになります。
ロックファイルを使えば、チームメンバーなどのユーザー間や、CIビルドの実行時などのシステム間で、依存関係ツリー全体にわたって同一で再現可能なインストールを実現できます。
ロックファイルの仕組み
npmエコシステムの大半で使われているパッケージロックファイルは、次の2種類です。
Yarnのyarn.lock
npmのpackage-lock.json
この2つの使われ方は、フローチャートで説明するのが分かりやすいでしょう。

この図ではnpmのpackage-lock.jsonを使っていますが、どの箇所もyarn.lockに置き換えられます。ただし、npmクライアントでの公開プロセスではyarn.lockファイルは自動的に無視されないため、.npmignoreファイルで明示的に除外しない限り、パッケージのtarballに含まれます。一方、図の左側に示すように、そのパッケージの一部としてパッケージロックファイルが存在していても、ライブラリを利用するエンドユーザーには使われません。
Yarnもnpmも、一時的な依存関係のロックファイルは考慮しません。これらはパッケージマネージャーによって完全に無視されます。インストール操作が行われる最上位のプロジェクトだけが、ロックファイルを使って依存関係ツリー全体を確認します。パッケージマネージャーは、このロックファイルを依存関係マニフェストとして参照します。
Shrinkwrapロックファイル
例外がないとは限りません。一時的な依存関係にも適用される特別なロックファイルが存在するケースが1つあります。npm-shrinkwrap.jsonファイルは他のロックファイルと同様に依存関係ツリーを固定しますが、npmの公開プロセスではこのファイルもレジストリに登録されます。さらに重要なのは、エンドユーザーが一般的なアプリケーションでライブラリを使い、npm installを実行すると、インストール時のsemverによる解決ではなく、ライブラリのshrinkwrapファイルによって取得するバージョンが決まることです。
npm-shrinkwrap.jsonを含むパッケージの扱いは、Yarnとnpmで異なります。
npmは、
npm-shrinkwrap.jsonで指定された依存関係を、パッケージ内のnode_modules/フォルダー直下に必ず配置し、ツリーの上位階層にフラット化しようとはしません。Yarnは
npm-shrinkwrap.jsonを考慮せず、完全に無視します。
Shrinkwrapファイルは何に役立つのでしょうか?ライブラリのメンテナーが依存関係を固定・管理し、既知のバージョンを含めて配布できます。ただし、依存関係ツリー全体を注意深くメンテナンスする必要があります。また、shrinkwrapファイルを使う場合、ソース管理にほかのロックファイルを置く必要はありません。この方法を採用している有名なライブラリの例として、hapijsプロジェクトがあります。
ロックファイルのずれ
ロックファイルは、依存関係の追加や、クローンしたばかりのプロジェクトへの依存関係のインストールなど、開発者がプロジェクトを操作するときに作成されます。開発サイクル中に依存関係を追加・削除するのは一般的ですが、package.jsonを変更したのに、対応するロックファイルをコミットし忘れたらどうなるでしょうか?
プロジェクトのpackage.jsonとロックファイルの内容が一致しない場合、npmやYarnなどのパッケージマネージャーは差異を解消し、新しいマニフェストを生成しようとします。一見よさそうですが、CI中に発生すると問題の原因になります。
ビルドステップで、package.jsonに宣言された依存関係とロックファイルのずれを検出して失敗させない場合、ビルドまたはテスト対象の成果物で、ビルド時点で入手可能な任意のバージョンが使われる可能性があります。これはロックファイルの利点を損ないます。
そのため、依存関係のインストール時にはロックファイルを使うよう、パッケージマネージャーを設定することをおすすめします。たとえば、次のようにします。
アプリケーションとライブラリのロックファイル
プロジェクトがメインのアプリケーションなのか、それともアプリケーションや別のライブラリで利用されるライブラリなのかによって、ロックファイルの使い方についての考え方は異なります。
アプリケーションプロジェクトではロックファイルを使うべき、という点では意見が一致しています。一方、ライブラリプロジェクトでは必ずしもそうではありません。それはなぜでしょうか?
ライブラリでロックファイルを使わない理由としてよく挙げられるのは、利用者がライブラリとともに取得する依存関係に差が生じることです。その結果、パッケージのメンテナーがビルドの破損に気づけなくなります。
しかし、ロックファイルを使わなければ、依存関係はインストール時にしか解決されず、いずれにせよメンテナーと利用者で異なるものになる可能性があります。
私としては、チームでの共同作業と再現可能なビルドのために、ライブラリプロジェクトもほかのプロジェクトと同様にロックファイルを含めるべきだと考えています。
ただし、改善案を提案させてください。ライブラリの依存関係が利用者の環境で壊れることを本当に懸念しているなら、問題を洗い出すために、次の2種類のCIビルドを設定できます。
1つ目は、通常のプロジェクトのビルド設定と同様に、プロジェクトのロックファイルを使うビルドです。
もう1つは、プロジェクトのロックファイルを無視し、最新のsemverに基づいて依存関係を解決するビルドです。
この方法なら、開発ワークフローで再現可能なビルドと一貫性のある依存関係を維持しながら、ライブラリを利用する開発者に影響する可能性のある破壊的変更も検出できます。しかも、チームの開発者全員が安心して作業できます。
CTFを始めよう
オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。



