3.8 KiB
Symlinked node_modules structure
This article only describes how pnpm's
node_modulesare structured when there are no packages with peer dependencies. For the more complex scenario of dependencies with peers, see How peers are resolved.
pnpm's node_modules layout uses symbolic links to create a nested structure of dependencies.
Every package@version is linked to node_modules from the global store only once.
Let's say you install foo@1.0.0 that depends on bar@1.0.0. pnpm will hard link both packages to node_modules like this:
node_modules
└─ .registry.npmjs.org
├─ bar/1.0.0/node_modules/bar
| ├─ index.js
| └─ package.json
└─ foo/1.0.0/node_modules/foo
├─ index.js
└─ package.json
These are the only "real files" in node_modules. Once all the packages are hard linked to node_modules, symlinks are
created to build the nested dependency tree structure.
As you might have noticed, both packages are hard linked into a subfolder inside a node_modules folder (foo/1.0.0/node_modules/foo).
This is needed to:
- allow packages to require themselves.
fooshould be able to dorequire('foo/package.json'). - avoid circular symlinks. Dependencies of packages are placed in the same folder in which the dependent packages are.
For Node.js it doesn't make a difference whether dependencies are inside the package's
node_modulesor in any othernode_modulesin the parent directories.
The next stage of installation is symlinking dependencies. bar is going to be symlinked to the foo/1.0.0/node_modules folder:
node_modules
└─ .registry.npmjs.org
├─ bar/1.0.0/node_modules/bar
└─ foo/1.0.0/node_modules
├─ foo
└─ bar -> ../../../bar/1.0.0/node_modules/bar
foo is going to be symlinked to the root node_modules folder because foo is a dependency of the project:
node_modules
├─ foo -> .registry.npmjs.org/foo/1.0.0/node_modules/foo
└─ .registry.npmjs.org
├─ bar/1.0.0/node_modules/bar
└─ foo/1.0.0/node_modules
├─ foo
└─ bar -> ../../../bar/1.0.0/node_modules/bar
This is a very simple example. However, the layout will stay flat in the file system regardless of the number of dependencies and the depth of the dependency tree.
Let's add qar@2.0.0 as a dependency of bar and foo. This is how the node_modules will look like:
node_modules
├─ foo -> .registry.npmjs.org/foo/1.0.0/node_modules/foo
└─ .registry.npmjs.org
├─ qar/2.0.0/node_modules/qar
├─ bar/1.0.0/node_modules
| ├─ bar
| └─ qar -> ../../../qar/2.0.0/node_modules/qar
└─ foo/1.0.0/node_modules
├─ foo
├─ qar -> ../../../qar/2.0.0/node_modules/qar
└─ bar -> ../../../bar/1.0.0/node_modules/bar
As you can see, even though the depth of the tree is bigger (foo > bar > qar), the directory depth in the file system is still the same.
This layout might look weird at first glance, but it is completely Node.js-compatible! When resolving modules, Node.js ignores symlinks.
So when bar is required from foo/1.0.0/node_modules/foo/index.js, Node.js is not using bar from foo/1.0.0/node_modules/bar.
bar is resolved to its real location: bar/1.0.0/node_modules/bar. As a consequence, bar can also resolve its dependencies
which are in bar/1.0.0/node_modules.
A great bonus of this layout is that only packages that are really in the dependencies are accessible. With flattened node_modules, all hoisted
packages are accessible. To read more about why this is an advantage, see pnpm's strictness helps to avoid silly bugs.