
pnpm 12.10 can install without a node_modules tree
Quick answer: pnpm 12.10.0, released 6 October 2026, adds an experimental fourth nodeLinker value called loaded. It loads dependencies straight out of the content-addressable store through a Node.js loader pnpm registers for you, with no package files materialised into node_modules. It ships with an escape hatch, nodeLinker.excluded, and pnpm's own documentation example excludes Vitest — which tells you more about the trade-off than the announcement does.
Every few years someone tries to delete node_modules. Yarn did it with Plug'n'Play, and spent the years afterwards discovering which packages read their own files off disk. pnpm just tried again with a different mechanism, and the interesting thing is that it shipped the list of exceptions in the same release.
That is the right way to ship this. It is also the reason to read the settings page before the changelog.
What loaded actually does
pnpm's nodeLinker setting decides the physical shape of your dependencies on disk. Per the node-modules settings reference, it now takes four values, with isolated still the default:
| Value | Shape on disk |
|---|---|
isolated (default) | Dependencies symlinked from a virtual store at node_modules/.pnpm |
hoisted | A flat node_modules without symlinks, like npm or Yarn Classic |
pnp | No node_modules; Yarn Berry's Plug'n'Play strategy. Docs recommend also setting symlink to false |
loaded (new, experimental) | Loading directly from the content-addressable store, with pnpm's Node.js loader preloaded for scripts and commands |
The distinction between pnp and loaded matters. Plug'n'Play works by generating a resolution map and patching Node's module resolver. loaded keeps pnpm's existing content-addressable store as the source of truth and registers a Node.js loader that reads from it. Same outcome — no materialised package directories — reached from the store side rather than the resolver side, which means it inherits the deduplication pnpm already does.
One configuration detail that trips people up: nodeLinker is not an .npmrc-anywhere setting. pnpm's settings page states that settings defining the structure of node_modules “can only be set in pnpm-workspace.yaml”. If you put it in a user-level config and nothing changes, that is why.
The escape hatch is the honest part
loaded also accepts an object form: type: loaded plus an optional excluded list. The docs are explicit that other object forms are not supported yet. nodeLinker.excluded defaults to an empty array, takes a string[], and does this:
“materialize these package names and their complete dependency trees in the global virtual store” — and names match all installed versions and peer contexts.
The documented example is this:
nodeLinker: type: loaded excluded: - vitest
Read that example as documentation of the failure mode. Test runners, bundlers and anything that walks directories, resolves worker entry points by path, spawns child processes with its own location, or ships native binaries tends to assume real files in real directories. Those are not edge cases in a modern JavaScript repo — they are the toolchain. The honest reading of loaded today is: your application code can live in the store, your tooling probably needs to be excluded, and you will find out which by running your suite.
Note also what excluded costs. It materialises the named package and its complete dependency tree. Exclude one popular test runner and a large part of your graph comes back onto disk. The all-or-nothing feel of Plug'n'Play has been replaced by a dial, but the dial is coarse.
The lockfile change in the same release
12.10.0 also adds lockfile.includeResolutionSettings, which records four resolution settings into pnpm-lock.yaml. Installs then treat a lockfile whose recorded values differ from the current configuration as outdated. The release notes credit it with a second, very practical effect under issue #16583: it stops pnpm run from triggering another install after a pnpm install --frozen-lockfile.
That second effect is the one CI engineers will care about. A frozen-lockfile install followed by a script that reinstalls is pure wasted pipeline time, and it is the kind of bug that hides because the build still passes. If your pipeline does install --frozen-lockfile and then pnpm run build, time both steps before and after upgrading.
Worth pairing with the groundwork in 12.9.0, four days earlier, which made pnpm install record every project it installs in the store's projects directory as a symlink — with the caveat that --frozen-store installs without the global virtual store still record nothing. A store that knows which projects consume it is a precondition for a linker that serves them directly. 12.9.0 also added a per-registry networkConcurrency option that caps in-flight requests to one registry's origin while others keep the overall limit.
The release cadence is the bigger signal
Look at pnpm's release index for the last two weeks of this cycle: 12.8 on 28 September, 12.8.2 on 30 September, 12.9.0 on 2 October, 12.9.1 on 3 October, then 12.10.0, 12.10.1 and a 11.28.5 backport all on 6 October. Two minors and four patches in nine days, with the 11.x line still being maintained alongside.
For a package manager — the one tool in the stack whose failures block everything else — that pace cuts both ways. Fixes land fast. It also means “pnpm 12” is not a version you can reason about; the behaviour you get depends on the patch. If you have not pinned your package manager version through packageManager in package.json or a devEngines entry, two developers on “pnpm 12” this month are not running the same resolver. We made the same argument when pnpm 12's Rust rewrite quietly became the default on the latest dist-tag.
12.10.0's patch notes also include a compatibility fix worth knowing in a mixed team: pnpm 11 releases before 11.28.4 can run pnpm 12 again, under issue #16594.
Should you turn it on
Not in anything you ship from. “Experimental” is pnpm's word, the object form is partially implemented by its own admission, and the exclusion mechanism means adoption is an iterative discovery process rather than a flag flip.
Where it is worth an afternoon: a branch, in a repo with a fast test suite, to find out how long your exclusion list gets. That number is the actual migration cost, and it is specific to your dependency graph — nobody else's benchmark will tell you. If the list is one or two test-tooling entries, this is a feature you will want in a year. If it grows to a dozen, isolated is already doing the job and the store-level deduplication you came to pnpm for is unaffected either way.
Keep an eye on the workspace file while you do it. pnpm-workspace.yaml is now carrying structural decisions, and a YAML to JSON converter is a quick way to diff two configurations without squinting at indentation, while a JSON formatter helps when you are reading a lockfile's recorded settings. Other package managers and build tools worth benchmarking against sit in the developer tools category, and the open-source listings cover the ones whose linker internals you can read for yourself.
FAQ
What is pnpm's loaded node linker?
It is an experimental fourth value for nodeLinker, added in pnpm 12.10.0 on 6 October 2026, that loads packages directly from the content-addressable store instead of materialising them into node_modules. pnpm preloads its own Node.js loader for scripts and commands so resolution works.
How is loaded different from Yarn's Plug'n'Play?
Both eliminate materialised package directories, and pnpm offers pnp as a separate nodeLinker value for the Yarn Berry strategy. loaded instead serves files from pnpm's existing content-addressable store through a registered Node.js loader, so it builds on pnpm's deduplication rather than on a separate resolution map.
Why does pnpm's own example exclude Vitest?
Because some packages need real files in real directories. nodeLinker.excluded exists to materialise those packages and their complete dependency trees in the global virtual store. Test runners and bundlers are the usual candidates, and treating the documented example as a warning rather than a sample is the right instinct.
Where do I set nodeLinker?
In pnpm-workspace.yaml. pnpm's settings documentation states that settings defining the structure of node_modules can only be set there, so a user-level or .npmrc value will not take effect.
What does lockfile.includeResolutionSettings change?
It records four resolution settings in pnpm-lock.yaml and treats a lockfile with different values as outdated. Its practical side effect, credited to issue #16583, is that pnpm run no longer triggers a second install after pnpm install --frozen-lockfile.
Is the loaded linker safe for CI?
pnpm labels it experimental and says the object form's other shapes are not supported yet, so no. Trial it on a branch, measure how long your exclusion list becomes, and keep isolated on anything that gates a release.
A feature that ships with its own exception list is telling you the truth. Read the exception list first.

