New users of pnpm frequently ask me about the weird structure of node_modules that pnpm creates. Why is it not flat? Where are all the sub-dependencies?
I am going to assume that readers of the article are already familiar with flat
node_modulescreated by npm and Yarn. If you don't understand why npm 3 had to start using flatnode_modulesin v3, you can find some prehistory in Why should we use pnpm?.
So why is pnpm's node_modules unusual? Let's create two directories and run npm add express in one of them and pnpm add express in the other one. Here's the top of what you get in the first directory's node_modules:
.binacceptsarray-flattenbody-parserbytescontent-dispositioncookie-signaturecookiedebugdepddestroyee-firstencodeurlescape-htmletagexpressYou can see the whole directory here.
And this is what you get in the node_modules created by pnpm:
.pnpm.modules.yamlexpressYou can check it here.
So where are all the dependencies? There is only one folder in the node_modules called .pnpm and a symlink called express. Well, we installed only express, so that is the only package that your application has to have access to
Read more about why pnpm's strictness is a good thing here
Let's see what is inside express:
▾ node_modules ▸ .pnpm ▾ express ▸ lib History.md index.js LICENSE package.json Readme.md .modules.yamlexpress has no node_modules? Where are all the dependencies of express?
The trick is that express is just a symlink. When Node.js resolves dependencies, it uses their real locations, so it does not preserve symlinks. But where is the real location of express, you might ask?
Here: node_modules/.pnpm/express@4.17.1/node_modules/express.
OK, so now we know the purpose of the .pnpm/ folder. .pnpm/ stores all the packages in a flat folder structure, so every package can be found in a folder named by this pattern:
.pnpm/<name>@<version>/node_modules/<name>We call it the virtual store directory.
This flat structure avoids the long path issues that were caused by the nested node_modules created by npm v2 but keeps packages isolated unlike the flat node_modules created by npm v3,4,5,6 or Yarn v1.
Now let's look into the real location of express:
▾ express ▸ lib History.md index.js LICENSE package.json Readme.mdIs it a scam? It still lacks node_modules! The second trick of pnpm's node_modules structure is that the dependencies of packages are on the same directory level on which the real location of the dependent package. So dependencies of express are not in .pnpm/express@4.17.1/node_modules/express/node_modules/ but in .pnpm/express@4.17.1/node_modules/:
▾ node_modules ▾ .pnpm ▸ accepts@1.3.5 ▸ array-flatten@1.1.1 ... ▾ express@4.16.3 ▾ node_modules ▸ accepts ▸ array-flatten ▸ body-parser ▸ content-disposition ... ▸ etag ▾ express ▸ lib History.md index.js LICENSE package.json Readme.mdAll the dependencies of express are symlinks to appropriate directories in node_modules/.pnpm/. Placing dependencies of express one level up allows avoiding circular symlinks.
So as you can see, even though pnpm's node_modules structure seems unusual at first:
- it is completely Node.js compatible
- packages are nicely grouped with their dependencies
The structure is a little bit more complex for packages with peer dependencies but the idea is the same: using symlinks to create a nesting with a flat directory structure.
