Symlinked `node_modules` structure
This article only describes how pnpm's
node_modules
are 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 graph 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.
foo
should 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_modules
or in any othernode_modules
in 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 graph.
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 graph 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.