Minor Changes
-
Security fix. Affects projects using
namedRegistrieson pnpm 11.1.0–11.19.x. It is semi-breaking for those projects — see "If you use named registries" below.The lockfile recorded no marker for which registry a package came from. Packages were keyed by
name@versionalone, and entry lookup went throughrefToRelative(ref, name), so a dependency you declared against one registry could be satisfied by an entry that was actually resolved from another. When two registries served the same name and version, both collapsed onto a singlepackages:entry and whichever resolved first decided the tarball every consumer got.That is a package-substitution risk: a package you expect from your private registry could be installed from a different registry that publishes the same name and version, and the lockfile recorded nothing that would let you tell.
Packages resolved from a named registry are now recorded under registry-qualified keys (
<name>@<registryName>:<version>, e.g.foo@work:1.0.0), so each registry gets its own entry and the lockfile pins which one a dependency came from.The lockfile format version is unchanged. Registry-qualified keys appear only for packages resolved from a named registry, so a project that does not use
namedRegistriessees no difference, and older pnpm versions keep reading the file.If you use named registries
Your next non-frozen install re-keys those entries, which shows up as a lockfile diff. Commit it — that diff is the fix being applied. Review it: an entry that moves to a registry you did not expect is worth investigating.
Everyone working on the project should be on this version or newer before you do. An older pnpm reads the re-keyed lockfile fine — frozen installs are unaffected — but it does not produce registry-qualified keys itself, so any install that updates the lockfile writes those entries back to the old shape, and the next install on a current pnpm re-qualifies them. The result is a lockfile that flips back and forth, and while it is in the old shape the project is exposed again. Because the lockfile format version is deliberately unchanged, pnpm cannot detect this and warn you about it.
There is no setting to keep the old behavior: the old shape is the vulnerability.
Tarball URLs that follow the standard registry layout are no longer written to the lockfile for named-registry packages; they are recomputed from the
namedRegistriessetting on demand.To use named registries, map your aliases in
pnpm-workspace.yaml:namedRegistries: work: https://npm.enterprise.example.com/
New built-in
npmjs:aliasnpmjs:now resolves tohttps://registry.npmjs.org/with no configuration, alongside the existinggh:alias for GitHub Packages. It pins a dependency to the public registry even whenregistrypoints elsewhere, such as an internal proxy:{ "dependencies": { "left-pad": "npmjs:^1.3.0" } }npm:cannot do this — it is the alias protocol (npm:<name>@<range>) and resolves through whateverregistrypoints at.If you mirror or proxy npmjs, point the alias at your mirror:
namedRegistries: npmjs: https://npm.internal.example.com/
Built-in registry URLs are also the prefixes a lockfile's recorded tarball URL is matched against when pnpm verifies a package. Without the override, an entry whose tarball URL is on
registry.npmjs.orgis verified against the public registry rather than your mirror. This only affects lockfiles that record such URLs — a canonical URL for your configured registry is omitted from the lockfile and unaffected — and only when a tarball-URL,minimumReleaseAge, ortrustPolicycheck runs. Overriding the alias is the same escape hatch GHES users already have forgh.Every alias the lockfile references must stay in
namedRegistries: reading an entry whose alias is gone fails withERR_PNPM_MISSING_NAMED_REGISTRYrather than silently falling back to the default registry, since that would fetch a different package. Renaming an alias re-resolves the packages that used it.Named registry aliases that shadow a reserved dependency specifier prefix (
file,link,workspace,runtime,npm,jsr, ...) are now rejected withERR_PNPM_RESERVED_NAMED_REGISTRY_NAMEinstead of being silently shadowed by the corresponding resolver.pnpm licensesandpnpm sbomnow keep the two artifacts apart as well: license records carry the registry alias, and SBOM components carry the purlrepository_urlqualifier.
Patch Changes
-
Installing a workspace whose projects auto-install peer dependencies is substantially faster. Each round of the peer-hoist loop no longer scans the whole workspace once per project, so the cost of resolution grows with the workspace instead of with its square.
-
Installing a dependency chain whose packages carry peer dependencies no longer expands exponentially with the depth of the chain. A single project with a single such dependency could exhaust memory before finishing; it now resolves in tens of megabytes.
-
Fixed non-deterministic resolution on multi-project workspaces: two consecutive installs of the same inputs could bind peer-suffixed packages to different (still valid) providers, rewriting
pnpm-lock.yamlon every install #13567. -
Installing a workspace now produces the same
pnpm-lock.yamlevery time. Two installs of the same workspace could previously bind a peer dependency to a different — still valid — version, which changed the lockfile without anything in the project changing. -
An empty
http-proxy,https-proxy,proxy, orno-proxyvalue — from the.npmrc,pnpm-workspace.yaml, the CLI, or theHTTP_PROXY/HTTPS_PROXY/PROXY/NO_PROXYenvironment variables — no longer fails the install withERR_PNPM_INVALID_PROXY. Empty settings read as unset, so a shell exportingHTTP_PROXY=disables the proxy, and an emptyproxy=in the.npmrcno longer suppressesHTTPS_PROXY#13533.proxy=falsein the.npmrcorproxy: falseinpnpm-workspace.yamlnow turns proxying off instead of being read as a proxy host namedfalse.falseandnullonhttps-proxy/http-proxy/no-proxyread as unset, and on the command line they are ordinary host names, since a flag carries its value verbatim. -
The env lockfile no longer pins
@pnpm/exealongsidepnpmwhen the wanted pnpm version is 12 or newer. From v12 the unscopedpnpmpackage is itself the native executable, so@pnpm/exeis not published for it and resolving it would fail. The engine identity check now verifies the native binary through whichever package ships it. -
Resolution on large peer-heavy workspaces got faster: a Bit workspace with 114 projects and ~21,000 lockfile entries resolves in ~13.4s instead of ~16.0s. The resolved dependency graph is unchanged.
-
Fixed nondeterministic peer bindings in large multi-project workspaces.
-
Resolving a workspace whose dependency chains are deep is faster: deciding which missing peer dependencies another project's resolution already covers now answers once per shared chain segment instead of once per report.
-
Peer resolution on large workspaces got faster: each hoist round now refreshes its view of the dependency graph from what the round changed instead of re-reading every resolved package. The resolved dependency graph is unchanged.
-
pnpm installno longer crashes on a machine whose system certificate store is empty or absent — for example a minimal container or build sandbox that ships no CA certificates #13588. Such a system now falls back to the Mozilla root certificates bundled into the binary, the same set Node.js ships, so both offline and online installs work again. Certificates from the system store,NODE_EXTRA_CA_CERTS, and the.npmrcca/cafilesettings keep taking precedence whenever any of them is available. -
Fixed the order in which pnpm matches a lockfile's recorded tarball URL against known registry URLs. Two registry URLs of equal length were previously ordered arbitrarily, so which one a tarball URL matched could differ between runs.
-
pnpm login/pnpm addusernow read thescopesetting frompnpm-workspace.yaml, the globalconfig.yaml, and thePNPM_CONFIG_SCOPEenvironment variable, not only from the--scopecommand-line flag. Whenscopeis configured, the granted token is keyed to that scope and the scope-to-registry mapping is recorded.--scopestill takes precedence when both are set. Note thatscopein an.npmrcis not read — pnpm keeps only auth and registry keys from that file. -
Resolution spends less time in its final peer pass: the package-name cycle graph it consults is now derived once per package instead of once per occurrence of that package.
-
npm's
--prefixis accepted as a spelling of--dir, and--storeas a spelling of--store-dir, sopnpm --prefix ../ run testno longer fails with "unexpected argument '--prefix' found" #13583. -
pnpm now ships
node-gypagain, so packages whose install scripts shell out to it build out of the box. Previously they failed withspawn node-gyp ENOENTunless anode-gypwas already onPATH— affectingnode-gyp-buildwith no matching prebuild,node-pre-gyp, a plain"install": "node-gyp rebuild", and any package shipping abinding.gypwithout an install script. As in pnpm 11, the wholenode-gypdependency tree is resolved from pnpm's own lockfile when pnpm is released, so it is frozen per release rather than resolved on your machine, andnpm_config_node_gyp, a workspacenode-gyp, and a package's ownnode-gypdependency all still take precedence.