Part of pnpm/tasks#191. Wrong claims: - npmrc: `ca` defaults to the system trust store (v12) or the Node.js CA set (v11), not "the npm CA certificate". - pnpm-cli: `--config.<key>` does not populate `npm_config_*` for scripts (v11 and v12). - pnpm-vs-npm, package-sources: `file:` directories are copied into the virtual store and installed; only `link:` is a symlink. - pnpmfile: `importPackage` is removed in v12; changing the pnpmfile re-resolves, so the lockfile need not be deleted (v11 and v12). - scripts: list every environment variable a script receives (v11 and v12), and the full set of built-ins a script can shadow. - supply-chain-security: build scripts are blocked by `allowBuilds` and `strictDepBuilds`, and `blockExoticSubdeps` is on by default. - workspaces: pnpm has built-in release management (v11 and v12). Missing: - npmrc: `NODE_EXTRA_CA_CERTS`. - pnpm-cli: `--workspace-packages`, `-y`, and the `NO_COLOR`, `pnpm_config_*`, `PNPM_HOME`, `PNPM_SHIM_BYPASS` variables. - package_json: a `packageManager` section, `@<scope>:registry` in the `publishConfig` list, the built-in injected-deps sync. - pnpmfile: `beforePacking(pkg, dir, context)`, TL;DR rows for `updateConfig`, `preResolution`, `finders`, the `.cjs` fallback. - typescript: `addMissingPeerTypes`; uninstall: cache, state, and config directories; scripts: project lifecycle stages; workspaces: a minimal `packages` example and live sync while a script runs. - sidebar: `only-allow-pnpm`, `production`, and `cli/cache`. Clarity and duplication: - One home for git-dependency preparation (package-managers), Python index rules (registries), finders (finders.md). - motivation, symlinked-node-modules-structure: files are hard-linked or cloned; package directories are real directories; Node resolves symlinks to real paths. - podman copies the lockfile and workspace manifest; webcontainers leads with `pmOnFail: ignore`; task priority applies within a concurrency group; logos get alt text; store-loader spells out CAS. - using-changesets workflow uses the same action versions and Node.js 24 as the CI guide (v11 and v12). Stale history on v12 pages: - Removed "Added in" markers for v11 and older, and rewrote "Since v11..." and pnpm 10/11 narration as current behavior. Review follow-up: the script environment notes say which npm_config_* keys are filtered (auth, scope registry, per-registry) and that pnpm sets npm_config_user_agent and npm_config_node_gyp; a string `bin` sets npm_package_bin; NODE_EXTRA_CA_CERTS adds to whichever CA set applies; the v10 codemod command notes pnpx; motivation notes that pnpm copies files when neither hard links nor clones work; production says `pnpm fetch` reads the lockfile, not the package manifests.
5.0 KiB
id, title
| id | title |
|---|---|
| symlinked-node-modules-structure | Symlinked `node_modules` structure |
:::info
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 file of every package inside node_modules is a hard link to the
content-addressable store. 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
└── .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
These are the only "real" files in node_modules. Once all the packages are
hard linked to node_modules, symbolic links 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 import themselves.
fooshould be able torequire('foo/package.json')orimport * as package from "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
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar/ (files linked from <store>)
└── foo@1.0.0
└── node_modules
├── foo/ (files linked from <store>)
└── bar -> ../../bar@1.0.0/node_modules/bar
Next, direct dependencies are handled. foo is going to be symlinked into the
root node_modules folder because foo is a dependency of the project:
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar/ (files linked from <store>)
└── foo@1.0.0
└── node_modules
├── foo/ (files linked from <store>)
└── bar -> ../../bar@1.0.0/node_modules/bar
This is a very simple example. However, the layout will maintain this structure 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 new
structure will look:
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar/ (files linked from <store>)
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo/ (files linked from <store>)
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar/ (files linked from <store>)
As you may see, even though the graph is deeper now (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 compatible
with Node's module resolution algorithm. When resolving modules, Node resolves
a symlink to its real path. So when bar is required from
foo@1.0.0/node_modules/foo/index.js, Node finds the symlink at
foo@1.0.0/node_modules/bar and loads bar from 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 a flattened node_modules structure, all
hoisted packages are accessible. 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.