シンボリックリンクを使用した `node_modules` の構造
この記事では、ピア依存関係のパッケージが存在しない場合、pnpm が node_modules
をどのように構成するのか説明します。 ピア依存関係を含むより複雑な状況については、ピア依存関係の解決方法を参照してください。
pnpmは入れ子になった依存関係をシンボリックリンクを使用してnode_modules
に配置します。
node_modules
に存在する全てのパッケージに含まれるそれぞれのファイルは、コンテンツストアへのハードリンクです。 依存関係にbar@1.0.0
を持つfoo@1.0.0
をインストールするところを見ていきましょう。 pnpmはnode_modules
にそれぞれのパッケージのハードリンクを作成します。
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
│ ├── index.js
│ └── package.json
└── foo@1.0.0
└── node_modules
└── foo -> <store>/foo
├── index.js
└── package.json
これらのファイルはnode_modules
において唯一の「実体のある」ファイルです。 すべてのパッケージについてnode_modules
にハードリンクを作成したら、入れ子になった依存関係のグラフ構造を反映するシンボリックリンクを作成します。
お気づきのとおり、どちらのパッケージもそれぞれのサブディレクトリnode_modules
へ自身のハードリンクを作成しています (foo@1.0.0/node_modules/foo
) 。 このハードリンクが必要な理由は次のとおりです。
- パッケージが自分自身をインポートできるようにするため。たとえばパッケージ
foo
でrequire('foo/package.json')
あるいはimport * as package from "foo/package.json"
のように記述できるようにするためです。 - シンボリックリンクの循環参照を避けるため。あるパッケージの依存パッケージは、同じディレクトリ階層に並んでいます。 Node.jsの場合、依存パッケージがパッケージ自身の
node_modules
にあっても、上位階層のどこかのnode_modules
にあっても違いはありません。
インストールの次の段階では、依存関係同士をシンボリックリンクで結びつけます。 たとえば、bar
のシンボリックリンクをfoo@1.0.0/node_modules
に作成します。
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
続いて、直接的な依存関係を処理します。 foo
はプロジェクトの依存関係になっているので、シンボリックリンクを最上位のnode_modules
に作成します。
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
例としてはあまりにも簡単です。 しかし、依存関係の数が増えても、依存関係同士のグラフ構造がどれほど深くなっても、基本的にこのような方法でレイアウトを(構造を)管理することは変わりません。
試しに、foo
とbar
の依存関係にqar@2.0.0
を追加してみましょう。 こちらが新しい構造です。
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar -> <store>/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo -> <store>/foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar -> <store>/qar
見てのとおり、依存関係のグラフ構造は深くなりましたが (foo > bar > qar
) 、ファイルシステム上のディレクトリ階層の深さは元のままです。
一見すると奇妙に見えるかもしれませんが、Node.jsのモジュール解決アルゴリズムと完全に互換性のある構造なのです。 Node.jsはモジュールを解決するときシンボリックリンクを無視します。ですから、foo@1.0.0/node_modules/foo/inde.js
が要求するbar
を解決するとき、シンボリックリンクのfoo@1.0.0/node_modules/bar
ではなく、実体のbar@1.0.0/node_modules/bar
として解決するのです。 結果として、bar
でも同じようにbar@1.0.0/node_modules
の依存関係を解決します。
このレイアウトの大きな利点は、依存関係に含まれるパッケージにのみアクセスできるようになることです。 平坦な構造のnode_modules
では、かき集めたあらゆるパッケージにアクセスできるようになってしまいます。 これが利点である理由について詳しく知りたいときは、「pnpmの厳格さは、ささいな間違いを犯さないようにするためです (pnpm's strictness helps to avoid silly bugs) 」を参照してください。