シンボリックリンクを使用した `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
    │           ├── index.js     -> <store>/001
    │           └── package.json -> <store>/002
    └── foo@1.0.0
        └── node_modules
            └── foo
                ├── index.js     -> <store>/003
                └── package.json -> <store>/004

これらのファイルはnode_modulesにおいて唯一の「実体のある」ファイルです。すべてのパッケージについてnode_modulesにハードリンクを作成したら、入れ子になった依存関係のグラフ構造を反映するシンボリックリンクを作成します。

お気づきのとおり、どちらのパッケージもそれぞれのサブディレクトリnode_modulesへ自身のハードリンクを作成しています (foo@1.0.0/node_modules/foo) 。このハードリンクが必要な理由は次のとおりです。

  1. **パッケージが自分自身をインポートできるようにするため。**たとえばパッケージfooでrequire('foo/package.json')あるいはimport * as package from "foo/package.json"のように記述できるようにするためです。
  2. **シンボリックリンクの循環参照を避けるため。**あるパッケージの依存パッケージは、同じディレクトリ階層に並んでいます。 Node.jsの場合、依存パッケージがパッケージ自身のnode_modulesにあっても、上位階層のどこかのnode_modulesにあっても違いはありません。

インストールの次の段階では、依存関係同士をシンボリックリンクで結びつけます。たとえば、barのシンボリックリンクをfoo@1.0.0/node_modulesに作成します。

node_modules
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>
            └── 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>
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>
            └── 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>
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>

見てのとおり、依存関係のグラフ構造は深くなりましたが (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では、かき集めたあらゆるパッケージにアクセスできるようになってしまいます。 To read more about why this is an advantage, see "pnpm's strictness helps to avoid silly bugs"

Unfortunately, many packages in the ecosystem are broken — they use dependencies that are not listed in their package.json. To minimize the number of issues new users encounter, pnpm hoists all dependencies by default into node_modules/.pnpm/node_modules. To disable this hoisting, set hoist to false.