はじめに
フロントエンドの開発において私たちは当たり前のようにyarn
(またはyarn install
)というコマンドを実行するかと思います。すると魔法のようにnode_modulesフォルダが作られ、開発に必要なパッケージが揃います。
この当たり前のように行われるnode_modulesの誕生の裏側では一体何が起きているのかを本記事ではまとめます!
そもそもYarnとは
Yarnは一言で言うと、「JavaScriptのパッケージマネージャ」 です。
Web開発においてReactやVueのようなライブラリ,フレームワーク,ツールを組み合わせてアプリケーションを構築することが一般的で、これらのライブラリやツールを パッケージ と呼びます。
プロジェクトにおいてどのパッケージに依存しているかと言う関係性を 依存関係(dependencies) と言います。
プロジェクトが大規模になっていくにつれて依存関係は複雑に絡み合います。手動で管理するのは非常に困難になります。「AというパッケージはBに依存し、BはCに依存している…」といった状況をすべて把握するのはとても辛いです。
これを解決するのがyarnのようなパッケージマネージャです。
Yarnは2016年にFacebookが中心となって開発され、当時主流だったnpmが抱えていた課題、特にインストールの信頼性と速度の改善に重点を置いて設計されました。その信頼性の核となるのが、次のセクションで解説するyarn.lockというファイルです。このファイルによって、開発者全員が寸分違わず同じバージョンのパッケージを利用できる仕組みが実現されました。
補足: Yarn以外にもnpmやpnpmといったパッケージマネージャがあります。それぞれに特徴がありますが、本記事ではYarnに焦点を当てて解説します。
yarnを支える主要なファイルたち
yarnは、いくつかのファイルと連携して動作します。
ここでは、特に重要なpackage.jsonとyarn.lockの役割を詳しく紹介します。この2つのファイルの関係性を理解することが、yarn installの謎を解く鍵となります。
1. package.json – プロジェクトの「設計図」
package.jsonは、すべてのNode.jsプロジェクトの中心となるファイルです。Yarnにとっては、これから何をすべきかを知るための「設計図」や「レシピ」の役割を果たします。
{
"name": "my-sample-project",
"version": "1.0.0",
"dependencies": {
"react": "^18.2.0",
"axios": "^1.6.0"
},
"devDependencies": {
"jest": "^29.7.0",
"eslint": "^8.55.0"
}
}
このファイルには、以下のような情報が含まれています:
- dependencies: アプリケーションが本番環境で動作するために必要なパッケージ
- devDependencies: 開発やテストの時にだけ必要なパッケージ
ここで注目すべきは、バージョンの前に付いている^(キャレット)や~(チルダ)といった記号です。これはセマンティックバージョニング(SemVer)に基づく記法で、「バージョンの範囲」を指定しています。
- ^1.2.3: 1.2.3以上、2.0.0未満の最新バージョンを許可(メジャーバージョンは固定)
- ~1.2.3: 1.2.3以上、1.3.0未満の最新バージョンを許可(マイナーバージョンは固定)
- 1.2.3: 正確に1.2.3のバージョンのみ
つまり、package.jsonは「だいたいこのくらいのバージョンのパッケージが欲しい」という、ある程度柔軟な要求を示しています。この柔軟性が、チーム開発で「人によってインストールされるバージョンが違う」という問題を引き起こす原因となり得ます。
2. yarn.lock – 依存関係の「確定版」
package.jsonの柔軟性から生じる問題を解決するのが、yarn.lockファイルです。これは、Yarnが自動で生成・更新する、いわば「確定版のルールブック」です。
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#..."
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kG...
dependencies:
loose-envify "^1.1.0"
このファイルには、以下のような確定情報が、プロジェクトが依存するすべてのパッケージ(パッケージが依存する孫パッケージまで含む)について、漏れなく記録されます。
- 実際にインストールされた正確なバージョン
- パッケージのダウンロード元URL
- 改ざんチェック用のハッシュ値(integrity)
- そのパッケージが依存する他のパッケージ
yarn installを実行すると、Yarnはpackage.jsonよりも先にこのyarn.lockファイルを参照します。そして、このファイルに書かれた情報に従って、寸分違わずパッケージをインストールします。
これにより、誰がいつ実行しても全く同じnode_modulesフォルダが再現されるのです。このファイルは、以下の2点が非常に重要です。
- 決して手動で編集しないこと
- 必ずGitなどのバージョン管理に含めること
このルールがあるため、ブランチの変更を取り込んだ際などにyarn.lockが競合(コンフリクト)することがあります。その際は、決して手動で競合を解決しようとせず、package.jsonの競合を正しく解決した上で、改めてyarn installを実行しましょう。そうすることで、Yarnがpackage.jsonを元にyarn.lockを正しく再生成してくれます。
yarn installの3つのステップ
設計図(package.json)と確定版(yarn.lock)の役割がわかったところで、いよいよyarn installコマンドが実行する具体的な処理を見ていきましょう。
ステップ1:依存関係の解決 (Resolution)
Yarnはまず、インストールすべき全パッケージのリストとその正確なバージョンを確定させる「解決」のステップに入ります。
最初にyarn.lockファイルを確認します。このファイルが存在すれば、Yarnはそこに書かれている情報を絶対の正義として、依存関係のツリーを素早く構築します。
もしyarn.lockがない場合や、package.jsonに新しいパッケージが追加されてyarn.lockと矛盾が生じた場合は、package.jsonの記述(^1.2.3など)を元に、依存関係を満たす最新のバージョンを探し出します。
このステップで解決された依存関係の確定情報は、最終的にyarn.lockファイルに書き込まれ、チーム全員で共有できる状態になります。
ステップ2:パッケージの取得 (Fetching)
インストールすべきパッケージのリストが確定したら、Yarnはそれらをダウンロードする「取得」のステップに移ります。
Yarnの賢い点は、一度ダウンロードしたパッケージをPC内のグローバルキャッシュ(通常は ~/.yarn/cache/ または設定されたディレクトリ)に保存していることです。もしキャッシュに必要なパッケージがあれば、ネットワーク通信を行わずにそこから瞬時に取得します。これが、2回目以降のインストールが非常に高速な理由です。
キャッシュに目的のパッケージがなければ、npmレジストリなどのリモートサーバーからダウンロードし、キャッシュに保存してから次のステップに進みます。
ステップ3:node_modulesへの展開 (Linking)
最後に、取得した全パッケージをプロジェクトのnode_modulesディレクトリに配置する「展開」のステップです。
このときYarnは、ただファイルをコピーするだけではありません。依存関係の重複を減らすために、可能な限りnode_modulesのトップレベルにパッケージを配置する**「巻き上げ(Hoisting)」**という最適化を行います。
# 巻き上げ前の構造
node_modules/
├── package-a/
│ └── node_modules/
│ └── lodash/
└── package-b/
└── node_modules/
└── lodash/ # 重複している
# 巻き上げ後の構造
node_modules/
├── package-a/
├── package-b/
└── lodash/ # トップレベルに配置されている
これにより、node_modulesの階層が深くなりすぎるのを防ぎ、プロジェクト全体の容量を削減しています。
この3つのステップ(解決 → 取得 → 展開)を経て、私たちのプロジェクトに、あのnode_modulesフォルダが誕生するのです。
まとめ
本記事では、「魔法のように」実行されるyarn installの裏側を、関連するファイルと具体的な処理ステップから紐解いてきました。
この仕組みを理解した今、もうyarn installは「おまじない」ではありません。依存関係で問題が起きたときも、どこに注目すれば良いか見当がつくようになったはずです。
最後まで読んでいただきありがとうございました!
Views: 0