Skip to main content
Version: 4.x

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 [email protected] is linked to node_modules from the global store only once. Let's say you install [email protected] that depends on [email protected]. 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:

  1. allow packages to require themselves. foo should be able to do require('foo/package.json').
  2. 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 other node_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 [email protected] 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.