You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Added pnpm stage with publish, list, view, approve, reject, and download subcommands for npm staged publishing.
Added a new setting trustLockfile. When true, pnpm install skips the supply-chain verification pass that re-applies minimumReleaseAge / trustPolicy='no-downgrade' to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author. Defaults to false; verification stays on by default. Set in pnpm-workspace.yaml.
Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (~4k lockfile entries with minimumReleaseAge + trustPolicy: no-downgrade enabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (time, per-version _npmUser.trustedPublisher, dist.attestations.provenance). The abbreviated-metadata cache is similarly projected to just the package-level modified field and the set of currently-listed version names. Fixes #11860.
Implemented pnpm pkg command natively, following npm pkg standards.
Implemented pnpm repo command natively, following npm repo standards.
Implemented pnpm set-script (alias ss) natively. Adds or updates an entry in the scripts field of the project manifest, supporting package.json, package.json5, and package.yaml formats.
Add a skip-manifest-obfuscation option for pnpm pack and pnpm publish. When enabled, the original packageManager field and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specific pnpm field continues to be omitted.
Patch Changes
Fixed pnpm dlx failing with ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND when the installed package's CAS slot is missing its package.json. Observed in the wild for pnpm dlx node@runtime:<version> when the GVS slot was populated without the synthesized manifest runtime archives need (they don't ship a package.json of their own, so the synthesized one is the only way it gets there; an existing slot from an earlier code path that skipped the synthesis stays incomplete). The bin link itself is wired up from the resolution and remains valid, so dlx now falls back to the scopeless package name when the slot's manifest is unreadable — for single-bin packages (the dlx common case, including every runtime: spec) this matches what manifest.bin would have named. Multi-bin packages already require --package=<spec> <bin> to disambiguate and don't enter this code path.
Fixed non-determinism in pnpm dedupe and pnpm install when a dependency graph contains packages with transitive peer dependencies on each other (e.g. @aws-sdk/client-sts and @aws-sdk/client-sso-oidc) and auto-install-peers is enabled. The lockfile no longer flips between two equally-valid forms across consecutive runs. The root cause was that resolveDependencies pushed onto its pkgAddresses / postponedResolutionsQueue arrays from inside Promise.all-spawned callbacks, so completion-order timing leaked into the array order and downstream cyclic-peer suffix assignment. Fixes #8155.
Fixed a regression introduced by #11711 where pnpm add <github-shorthand> (and any other wanted-dependency whose alias can't be parsed from the user-supplied spec, e.g. tarball URLs or pnpm/test-git-fetch#sha) was silently dropped from the manifest update and from pendingBuilds. The alias-keyed lookup added in that PR couldn't find a wantedDependency whose alias was undefined at parse time but resolved to a package name only after fetching, so the entry never made it into specsToUpsert. Restored the original index-based pairing between directDependencies and wantedDependencies; the catalog-protocol preservation that PR was originally fixing is unaffected because it's driven by rdd.catalogLookup.userSpecifiedBareSpecifier, not by the lookup. Fixes the three rebuilds dependencies / rebuilds specific dependencies / rebuild with pending option failures in building/commands/test/build/index.ts.
Fixed pnpm add --config leaving orphan entries in pnpm-lock.env.yaml (the optional subdependencies of the previously resolved version of the updated config dependency).
When the install engine is delegated to pacquet via configDependencies, the user's CLI flags passed to pnpm install (e.g. --no-runtime, --prod, --dev, --no-optional, --node-linker, --cpu/--os/--libc, --offline, --prefer-offline) are now forwarded to pacquet's install subcommand verbatim. Previously pacquet was invoked with a fixed argument list, so flags like --no-runtime were silently dropped. Flag forwarding is gated on the command being install/i; add, update, and dedupe still don't forward (their flag surface doesn't line up with pacquet's install).
Fixed pnpm up (and pnpm add / pnpm remove) failing with pacquet_package_manager::outdated_lockfile when pacquet is declared in configDependencies. pnpm now passes --ignore-manifest-check to pacquet so its --frozen-lockfile check doesn't fire against the (pre-mutation) package.json pnpm hasn't written yet #11797. Requires a pacquet release that supports the flag — bump PACQUET_VERSION in the e2e tests once it ships.
Mark optional subdependency snapshots of config dependencies with optional: true in the env lockfile, matching how optional dependencies are recorded elsewhere in pnpm-lock.yaml. Previously, snapshots for the platform-specific subdeps pulled in via a config dep's optionalDependencies were written as empty objects, which was inconsistent with the rest of the lockfile and made it look like those non-host platform variants were required.
Fix pickRegistryForPackage returning the wrong registry for an unscoped npm: alias under a scoped local name. A manifest entry like "@​private/foo": "npm:lodash@^1" was routing the lodash fetch through registries["@​private"], even though lodash is unscoped and doesn't live on that registry. The npm-alias branch now returns the alias target's own scope (or null for an unscoped target, falling through to registries.default) instead of leaking into the local key's scope.
Don't print "Installing config dependencies..." when config dependencies are already installed and nothing needs to be fetched, re-linked, or removed.
Experimental: Adding @pnpm/pacquet (the Rust port of pnpm) to configDependencies in pnpm-workspace.yaml now delegates the materialization phase of pnpm install to the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.
To configure pacquet in a project, run:
pnpm add @​pnpm/pacquet --config
You'll see changes in pnpm-workspace.yaml and pnpm-lock.yaml that should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.
configDependencies now resolve and install one level of optionalDependencies declared by the config dependency, with os/cpu/libc platform filtering applied at install time. This unlocks the esbuild/swc-style pattern where a package ships platform-specific binaries via optionalDependencies — a config dependency can now do the same and have the matching binary symlinked next to it in the global virtual store, so require('pkg-platform-arch') from inside the config dependency resolves correctly.
The env lockfile records all platform variants regardless of host platform, so it remains portable across machines. Each entry in a config dependency's optionalDependencies must declare an exact version — ranges and tags are rejected to keep installs reproducible.
Implement the documented pnpm login --scope <scope> flag. The scope is normalized (a leading @ is added if missing; blank values are ignored) and an @<scope>:registry=<registry> mapping is written to the pnpm auth file alongside the auth token. Subsequent installs of @<scope>/* packages then route to the chosen registry. Previously pnpm login --scope foo errored with Unknown option: 'scope' despite the flag being listed in the online documentation #11716.
pnpm outdated and pnpm update --interactive now report Node.js, Deno, and Bun runtimes installed as project dependencies (runtime: specifiers). Previously these were silently skipped.
Patch Changes
Fix cafile=<relative-path> in .npmrc being read from the wrong directory when pnpm is invoked from a different cwd (e.g. pnpm --dir <project> install from a CI wrapper or monorepo script). The path is now resolved against the directory of the .npmrc that declared it, not process.cwd(). Before this fix the CA file silently failed to load — the install proceeded without the configured CA and the user only saw TLS errors against a private registry, with no log line tying back to the wrongly resolved path #11624.
Fix config.registry getting a trailing slash appended when registry is set in .npmrc and no registries.default is provided by pnpm-workspace.yaml. The sync from registries.default to config.registry introduced in #11744 now only fires when the workspace manifest actually contributes a different default.
Fix global add/update to handle minimumReleaseAge policy violations instead of surfacing an internal resolver guardrail error.
Fix two crashes with injectWorkspacePackages: true when the lockfile has been pruned (e.g. by turbo prune --docker):
Cannot use 'in' operator to search for 'directory' in undefined: a peer-dependency-variant injected snapshot inherits its resolution from the base packages: entry; when a pruner drops that base entry the readers crash. convertToLockfileObject now reconstructs the directory resolution from the file: depPath at load time — a single normalization point, so every reader sees a fully-formed snapshot.
ERR_PNPM_ENOENT on node_modules/.bin/<tool>: after prepare/postinstall, runLifecycleHooksConcurrently re-imported each injected workspace package; the scanDir-into-filesMap workaround fed target-internal paths to the importer, which the makeEmptyDir fast path (#11088) then wiped. Drop the workaround and pass keepModulesDir: true so the importer preserves the target's existing node_modules (bin links + transitive deps) and source files keep their hardlinks.
Fixed pnpm login and pnpm logout ignoring registries.default from pnpm-workspace.yaml#10099.
Fix the minimumReleaseAge (publishedBy) maturity shortcut to be inclusive at the cutoff. Previously, abbreviated metadata whose modified field equalled the cutoff fell off the fast path and triggered a full-metadata re-fetch (or a MISSING_TIME error when full metadata wasn't permitted). Since modified is an upper bound on every version's publish time, modified == publishedBy already implies every version passes the per-version <= filter in filterPkgMetadataByPublishDate, so the shortcut now accepts the boundary case directly. Strictly > (was >=) at the rejection branch.
Honor publishConfig.access when publishing packages.
pnpm install now re-validates pnpm-lock.yaml entries against the active minimumReleaseAge and trustPolicy: 'no-downgrade' policies before any tarball is fetched. Lockfiles resolved elsewhere (committed to the repo, restored from a CI cache, produced by an older pnpm) under a weaker or absent policy can no longer install a freshly-published or trust-downgraded version silently. Violating entries abort the install with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION, ERR_PNPM_TRUST_DOWNGRADE, or the generic ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION when both policies trip in the same batch; minimumReleaseAgeExclude and trustPolicyExclude are honored. Verification results are cached so repeat installs against an unchanged lockfile take a fast path, and pnpm shows a transient progress line while the registry round-trip runs.
When fresh resolution picks an immature version, the behavior depends on minimumReleaseAgeStrict:
Loose mode — the default, in effect whenever minimumReleaseAge keeps its built-in 24-hour value — auto-adds the immature picks to minimumReleaseAgeExclude in pnpm-workspace.yaml and lets the install proceed. A single info message lists what was persisted.
Strict mode in an interactive terminal collects every immature direct AND transitive pick in one pass and prompts once with the full list. Approving adds them to minimumReleaseAgeExclude and the install continues; declining aborts before the lockfile, package.json, or node_modules is touched.
Strict mode in CI (or any non-TTY context) aborts with ERR_PNPM_NO_MATURE_MATCHING_VERSION listing every offending entry, instead of failing on the first one the resolver hit.
minimumReleaseAgeStrict auto-enables whenever the user explicitly sets minimumReleaseAge (CLI flag, env var, global config.yaml, or pnpm-workspace.yaml); set minimumReleaseAgeStrict: false to keep loose-mode auto-collect even with an explicit minimumReleaseAge value. Closes #10438, #10488, #11687.
Allow redundant trailing base64 padding in .npmrc auth values and report invalid auth base64 with a pnpm error.
Make pnpm self-update respect minimumReleaseAge (and minimumReleaseAgeExclude) when resolving which pnpm version to install.
When the latest dist-tag points to a version newer than the configured age threshold, self-update now selects the newest mature version instead unless excluded by minimumReleaseAgeExclude.
Also makes dlx and outdated surface invalid minimumReleaseAgeExclude patterns under the same ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE error code already used by install, instead of leaking the internal ERR_PNPM_INVALID_VERSION_UNION / ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION codes.
Global installs respect global config build policy (e.g., dangerouslyAllowAllBuilds from config.yaml) when GVS is enabled #9249.
The global virtual-store (GVS) default allowBuilds = {} was applied before workspace manifest settings were read and before global config values (stripped by extractAndRemoveDependencyBuildOptions) were re-applied via globalDepsBuildConfig. This caused hasDependencyBuildOptions to return true (because {} is not null), blocking restoration of global config values like dangerouslyAllowAllBuilds. As a result, global installs skipped all build scripts even when the config explicitly allowed them.
This fix moves the GVS default to after workspace manifest reading and globalDepsBuildConfig re-application, so that:
Global config dangerouslyAllowAllBuilds is properly restored (if set and no workspace policy exists)
Empty {} is only applied as a last resort when no policy is configured anywhere
Honor --silent when verifyDepsBeforeRun: install auto-installs dependencies before pnpm run or pnpm exec, preventing install output from being written to stdout #11636.
Fix lockfile parsing failures when pnpm-lock.yaml contains CRLF line endings and multiple YAML documents #11612.
Anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — engines.runtime pin when present, shell node otherwise — instead of pnpm's own runtime.
ENGINE_NAME (the <platform>;<arch>;node<major> prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from process.version — the Node that runs pnpm itself. That was wrong in two situations:
@pnpm/exe SEA bundle. The bundle has its own embedded Node, not the node on the user's PATH that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell node.
engines.runtime / devEngines.runtime pin. When a project pins a Node version via devEngines.runtime (pnpm v11+), pnpm downloads that Node into node_modules/node/ and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node.
Three changes:
@pnpm/engine.runtime.system-node-version now exports engineName(nodeVersion?). Resolves the version in this order: explicit override → getSystemNodeVersion() (which already prefers node --version over process.version in SEA contexts) → process.version.
@pnpm/deps.graph-hasher now exports findRuntimeNodeVersion(snapshotKeys) — scans an iterable of lockfile snapshot keys for a node@runtime:<version> entry and returns its bare version string. calcDepState and calcGraphNodeHash/iterateHashedGraphNodes accept a nodeVersion? (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to engineName(). The default (no override) preserves the pre-change behaviour. The legacy ENGINE_NAME constant in @pnpm/constants is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up.
Every install-side caller of the graph-hasher (@pnpm/installing.deps-resolver, @pnpm/installing.deps-restorer, @pnpm/installing.deps-installer, @pnpm/building.during-install, @pnpm/building.after-install, @pnpm/deps.graph-builder) now derives the project's pinned runtime via findRuntimeNodeVersion(Object.keys(graph)) once per invocation and threads it through.
On upgrade, two one-time GVS slot churns are possible:
SEA-pnpm users without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. node26) now hash under the shell-Node major (e.g. node24), matching what pacquet, the npm-published pnpm package, and any other pnpm-compatible tool already produce.
Projects with a devEngines.runtime pin: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on.
In both cases the old slots become prune-eligible.
Resolve the GVS hash's engine portion per-snapshot when a dependency declares its own engines.runtime, instead of using an install-wide value.
Pnpm's resolver desugars a dep's engines.runtime into dependencies.node: 'runtime:<version>', and the bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into <pkgDir>/node_modules/node/. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so a pinning snapshot's slot encoded the wrong Node major, and a reinstall on the same host could read the cached side-effects under a key whose <platform>;<arch>;node<major> triple disagreed with the Node the build actually ran on.
Per-snapshot resolution now matches what bins/linker already does on a per-package basis:
@pnpm/deps.graph-hasher adds readSnapshotRuntimePin(children) — reads the node entry from one snapshot's graph children and extracts the version from a node@runtime: value. Pairs with the existing findRuntimeNodeVersion(snapshotKeys) install-wide fallback (also now exported from @pnpm/deps.graph-hasher rather than @pnpm/engine.runtime.system-node-version, where it was a poor fit — system-node-version is about probing the host Node, not parsing lockfile-derived strings).
calcDepState and calcGraphNodeHash consult readSnapshotRuntimePin(graph[depPath].children) first and only fall back to the install-wide nodeVersion parameter when the snapshot doesn't pin its own Node.
Pacquet mirrors the same precedence at the calc_graph_node_hash call site in package-manager/src/virtual_store_layout.rs — a new find_own_runtime_node_major(snapshot) helper reads each snapshot's dependencies for a node entry with Prefix::Runtime and overrides the install-wide engine when present.
On upgrade, snapshots of dependencies that declare their own engines.runtime re-hash under that dep's pinned Node instead of the install-wide value. The old slots become prune-eligible. Closes #11690.
Fixed pnpm publish failing with a 404 when authentication relied on OIDC trusted publishing alongside an .npmrc written by actions/setup-node (_authToken=${NODE_AUTH_TOKEN}) without NODE_AUTH_TOKEN being set. Unresolved ${VAR} placeholders in auth values are now treated as empty rather than passed through verbatim, so the literal placeholder no longer surfaces as a bearer token when OIDC fallback is the intended auth source #11513.
Fix devEngines.packageManager (singular form, without onFail) defaulting to onFail: "error" instead of the documented pmOnFail: "download". As a result, a project that pinned a different pnpm version via devEngines.packageManager and ran pnpm install from a mismatched pnpm version failed with a hard error, even though the migration table from managePackageManagerVersions: true to pmOnFail: download (default) promises the install would auto-download the wanted version #11676.
The array form of devEngines.packageManager keeps its existing per-element defaults (error for the last entry, ignore for the rest), since those reflect explicit prioritization by the user. Explicit onFail values continue to win.
Fix devEngines.packageManager not writing packageManagerDependencies to pnpm-lock.yaml when the lockfile lacks an env-doc entry. Previously the lockfile sync skipped resolution unless an existing packageManagerDependencies.pnpm entry needed refreshing, so a fresh install without onFail: "download" left the resolved pnpm version unrecorded — contradicting the documented behavior that the resolved version is stored in pnpm-lock.yaml#11674.
Warn when package.json contains a legacy pnpm field with settings pnpm no longer reads from package.json (e.g. pnpm.overrides, pnpm.patchedDependencies). Previously these were silently ignored after the upgrade from v10, leaving users unaware that their overrides/patched dependencies had stopped taking effect #11677.
Configuration
📅 Schedule: (UTC)
Branch creation
"on Monday"
Automerge
At any time (no schedule defined)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
If you want to rebase/retry this PR, check this box
Next steps: Take a moment to review the security alert above. Review
the linked package source code to understand the potential risk. Ensure the
package is not malicious before proceeding. If you're unsure how to proceed,
reach out to your security team or ask the Socket team for help at
support@socket.dev.
Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.
Mark the package as acceptable risk. To ignore this alert only
in this pull request, reply with the comment
@SocketSecurity ignore npm/@typescript-eslint/eslint-plugin@8.59.4. You can
also ignore all packages with @SocketSecurity ignore-all.
To ignore an alert for all future pull requests, use Socket's Dashboard to
change the triage state of this alert.
Warn
Obfuscated code: npm eslint-plugin-jsdoc is 90.0% likely obfuscated
Next steps: Take a moment to review the security alert above. Review
the linked package source code to understand the potential risk. Ensure the
package is not malicious before proceeding. If you're unsure how to proceed,
reach out to your security team or ask the Socket team for help at
support@socket.dev.
Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.
Mark the package as acceptable risk. To ignore this alert only
in this pull request, reply with the comment
@SocketSecurity ignore npm/eslint-plugin-jsdoc@62.9.0. You can
also ignore all packages with @SocketSecurity ignore-all.
To ignore an alert for all future pull requests, use Socket's Dashboard to
change the triage state of this alert.
renovateBot
changed the title
chore(deps): update pnpm to v11.2.2
chore(deps): update pnpm to v11.3.0
May 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
11.1.2→11.3.0Release Notes
pnpm/pnpm (pnpm)
v11.3.0Compare Source
Minor Changes
Added
pnpm stagewithpublish,list,view,approve,reject, anddownloadsubcommands for npm staged publishing.Added a new setting
trustLockfile. Whentrue,pnpm installskips the supply-chain verification pass that re-appliesminimumReleaseAge/trustPolicy='no-downgrade'to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author. Defaults tofalse; verification stays on by default. Set inpnpm-workspace.yaml.Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (
~4klockfile entries withminimumReleaseAge+trustPolicy: no-downgradeenabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (time, per-version_npmUser.trustedPublisher,dist.attestations.provenance). The abbreviated-metadata cache is similarly projected to just the package-levelmodifiedfield and the set of currently-listed version names. Fixes #11860.Implemented
pnpm pkgcommand natively, followingnpm pkgstandards.Implemented
pnpm repocommand natively, followingnpm repostandards.Implemented
pnpm set-script(aliasss) natively. Adds or updates an entry in thescriptsfield of the project manifest, supportingpackage.json,package.json5, andpackage.yamlformats.Add a
skip-manifest-obfuscationoption forpnpm packandpnpm publish. When enabled, the originalpackageManagerfield and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specificpnpmfield continues to be omitted.Patch Changes
pnpm dlxfailing withERR_PNPM_NO_IMPORTER_MANIFEST_FOUNDwhen the installed package's CAS slot is missing itspackage.json. Observed in the wild forpnpm dlx node@runtime:<version>when the GVS slot was populated without the synthesized manifest runtime archives need (they don't ship apackage.jsonof their own, so the synthesized one is the only way it gets there; an existing slot from an earlier code path that skipped the synthesis stays incomplete). The bin link itself is wired up from the resolution and remains valid, sodlxnow falls back to the scopeless package name when the slot's manifest is unreadable — for single-bin packages (the dlx common case, including everyruntime:spec) this matches whatmanifest.binwould have named. Multi-bin packages already require--package=<spec> <bin>to disambiguate and don't enter this code path.pnpm dedupeandpnpm installwhen a dependency graph contains packages with transitive peer dependencies on each other (e.g.@aws-sdk/client-stsand@aws-sdk/client-sso-oidc) andauto-install-peersis enabled. The lockfile no longer flips between two equally-valid forms across consecutive runs. The root cause was thatresolveDependenciespushed onto itspkgAddresses/postponedResolutionsQueuearrays from insidePromise.all-spawned callbacks, so completion-order timing leaked into the array order and downstream cyclic-peer suffix assignment. Fixes #8155.pnpm add <github-shorthand>(and any other wanted-dependency whose alias can't be parsed from the user-supplied spec, e.g. tarball URLs orpnpm/test-git-fetch#sha) was silently dropped from the manifest update and frompendingBuilds. The alias-keyed lookup added in that PR couldn't find awantedDependencywhosealiaswasundefinedat parse time but resolved to a package name only after fetching, so the entry never made it intospecsToUpsert. Restored the original index-based pairing betweendirectDependenciesandwantedDependencies; the catalog-protocol preservation that PR was originally fixing is unaffected because it's driven byrdd.catalogLookup.userSpecifiedBareSpecifier, not by the lookup. Fixes the threerebuilds dependencies/rebuilds specific dependencies/rebuild with pending optionfailures inbuilding/commands/test/build/index.ts.pnpm add --configleaving orphan entries inpnpm-lock.env.yaml(the optional subdependencies of the previously resolved version of the updated config dependency).v11.2.2Compare Source
Patch Changes
configDependencies, the user's CLI flags passed topnpm install(e.g.--no-runtime,--prod,--dev,--no-optional,--node-linker,--cpu/--os/--libc,--offline,--prefer-offline) are now forwarded to pacquet'sinstallsubcommand verbatim. Previously pacquet was invoked with a fixed argument list, so flags like--no-runtimewere silently dropped. Flag forwarding is gated on the command beinginstall/i;add,update, anddedupestill don't forward (their flag surface doesn't line up with pacquet'sinstall).pnpm up(andpnpm add/pnpm remove) failing withpacquet_package_manager::outdated_lockfilewhen pacquet is declared inconfigDependencies. pnpm now passes--ignore-manifest-checkto pacquet so its--frozen-lockfilecheck doesn't fire against the (pre-mutation)package.jsonpnpm hasn't written yet #11797. Requires a pacquet release that supports the flag — bumpPACQUET_VERSIONin the e2e tests once it ships.v11.2.1Compare Source
Patch Changes
optional: truein the env lockfile, matching how optional dependencies are recorded elsewhere inpnpm-lock.yaml. Previously, snapshots for the platform-specific subdeps pulled in via a config dep'soptionalDependencieswere written as empty objects, which was inconsistent with the rest of the lockfile and made it look like those non-host platform variants were required.pickRegistryForPackagereturning the wrong registry for an unscopednpm:alias under a scoped local name. A manifest entry like"@​private/foo": "npm:lodash@^1"was routing thelodashfetch throughregistries["@​private"], even thoughlodashis unscoped and doesn't live on that registry. The npm-alias branch now returns the alias target's own scope (ornullfor an unscoped target, falling through toregistries.default) instead of leaking into the local key's scope.v11.2.0Compare Source
Minor Changes
Experimental: Adding
@pnpm/pacquet(the Rust port of pnpm) toconfigDependenciesinpnpm-workspace.yamlnow delegates the materialization phase ofpnpm installto the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.To configure pacquet in a project, run:
You'll see changes in
pnpm-workspace.yamlandpnpm-lock.yamlthat should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.configDependenciesnow resolve and install one level ofoptionalDependenciesdeclared by the config dependency, withos/cpu/libcplatform filtering applied at install time. This unlocks the esbuild/swc-style pattern where a package ships platform-specific binaries viaoptionalDependencies— a config dependency can now do the same and have the matching binary symlinked next to it in the global virtual store, sorequire('pkg-platform-arch')from inside the config dependency resolves correctly.The env lockfile records all platform variants regardless of host platform, so it remains portable across machines. Each entry in a config dependency's
optionalDependenciesmust declare an exact version — ranges and tags are rejected to keep installs reproducible.Implement the documented
pnpm login --scope <scope>flag. The scope is normalized (a leading@is added if missing; blank values are ignored) and an@<scope>:registry=<registry>mapping is written to the pnpm auth file alongside the auth token. Subsequent installs of@<scope>/*packages then route to the chosen registry. Previouslypnpm login --scope fooerrored withUnknown option: 'scope'despite the flag being listed in the online documentation #11716.pnpm outdatedandpnpm update --interactivenow report Node.js, Deno, and Bun runtimes installed as project dependencies (runtime:specifiers). Previously these were silently skipped.Patch Changes
Fix
cafile=<relative-path>in.npmrcbeing read from the wrong directory when pnpm is invoked from a different cwd (e.g.pnpm --dir <project> installfrom a CI wrapper or monorepo script). The path is now resolved against the directory of the.npmrcthat declared it, notprocess.cwd(). Before this fix the CA file silently failed to load — the install proceeded without the configured CA and the user only saw TLS errors against a private registry, with no log line tying back to the wrongly resolved path #11624.Fix
config.registrygetting a trailing slash appended whenregistryis set in.npmrcand noregistries.defaultis provided bypnpm-workspace.yaml. The sync fromregistries.defaulttoconfig.registryintroduced in #11744 now only fires when the workspace manifest actually contributes a different default.Fix global add/update to handle minimumReleaseAge policy violations instead of surfacing an internal resolver guardrail error.
Fix two crashes with
injectWorkspacePackages: truewhen the lockfile has been pruned (e.g. byturbo prune --docker):Cannot use 'in' operator to search for 'directory' in undefined: a peer-dependency-variant injected snapshot inherits itsresolutionfrom the basepackages:entry; when a pruner drops that base entry the readers crash.convertToLockfileObjectnow reconstructs the directory resolution from thefile:depPath at load time — a single normalization point, so every reader sees a fully-formed snapshot.ERR_PNPM_ENOENTonnode_modules/.bin/<tool>: afterprepare/postinstall,runLifecycleHooksConcurrentlyre-imported each injected workspace package; thescanDir-into-filesMapworkaround fed target-internal paths to the importer, which themakeEmptyDirfast path (#11088) then wiped. Drop the workaround and passkeepModulesDir: trueso the importer preserves the target's existingnode_modules(bin links + transitive deps) and source files keep their hardlinks.Fixed
pnpm loginandpnpm logoutignoringregistries.defaultfrompnpm-workspace.yaml#10099.Fix the
minimumReleaseAge(publishedBy) maturity shortcut to be inclusive at the cutoff. Previously, abbreviated metadata whosemodifiedfield equalled the cutoff fell off the fast path and triggered a full-metadata re-fetch (or aMISSING_TIMEerror when full metadata wasn't permitted). Sincemodifiedis an upper bound on every version's publish time,modified == publishedByalready implies every version passes the per-version<=filter infilterPkgMetadataByPublishDate, so the shortcut now accepts the boundary case directly. Strictly>(was>=) at the rejection branch.Honor
publishConfig.accesswhen publishing packages.v11.1.3Compare Source
Patch Changes
pnpm installnow re-validatespnpm-lock.yamlentries against the activeminimumReleaseAgeandtrustPolicy: 'no-downgrade'policies before any tarball is fetched. Lockfiles resolved elsewhere (committed to the repo, restored from a CI cache, produced by an older pnpm) under a weaker or absent policy can no longer install a freshly-published or trust-downgraded version silently. Violating entries abort the install withERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION,ERR_PNPM_TRUST_DOWNGRADE, or the genericERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATIONwhen both policies trip in the same batch;minimumReleaseAgeExcludeandtrustPolicyExcludeare honored. Verification results are cached so repeat installs against an unchanged lockfile take a fast path, and pnpm shows a transient progress line while the registry round-trip runs.When fresh resolution picks an immature version, the behavior depends on
minimumReleaseAgeStrict:minimumReleaseAgekeeps its built-in 24-hour value — auto-adds the immature picks tominimumReleaseAgeExcludeinpnpm-workspace.yamland lets the install proceed. A single info message lists what was persisted.minimumReleaseAgeExcludeand the install continues; declining aborts before the lockfile,package.json, ornode_modulesis touched.ERR_PNPM_NO_MATURE_MATCHING_VERSIONlisting every offending entry, instead of failing on the first one the resolver hit.minimumReleaseAgeStrictauto-enables whenever the user explicitly setsminimumReleaseAge(CLI flag, env var, globalconfig.yaml, orpnpm-workspace.yaml); setminimumReleaseAgeStrict: falseto keep loose-mode auto-collect even with an explicitminimumReleaseAgevalue. Closes #10438, #10488, #11687.Allow redundant trailing base64 padding in
.npmrcauth values and report invalid auth base64 with a pnpm error.Make
pnpm self-updaterespectminimumReleaseAge(andminimumReleaseAgeExclude) when resolving which pnpm version to install.When the
latestdist-tag points to a version newer than the configured age threshold,self-updatenow selects the newest mature version instead unless excluded byminimumReleaseAgeExclude.Also makes
dlxandoutdatedsurface invalidminimumReleaseAgeExcludepatterns under the sameERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDEerror code already used byinstall, instead of leaking the internalERR_PNPM_INVALID_VERSION_UNION/ERR_PNPM_NAME_PATTERN_IN_VERSION_UNIONcodes.Global installs respect global config build policy (e.g.,
dangerouslyAllowAllBuildsfrom config.yaml) when GVS is enabled #9249.The global virtual-store (GVS) default
allowBuilds = {}was applied before workspace manifest settings were read and before global config values (stripped byextractAndRemoveDependencyBuildOptions) were re-applied viaglobalDepsBuildConfig. This causedhasDependencyBuildOptionsto returntrue(because{}is not null), blocking restoration of global config values likedangerouslyAllowAllBuilds. As a result, global installs skipped all build scripts even when the config explicitly allowed them.This fix moves the GVS default to after workspace manifest reading and
globalDepsBuildConfigre-application, so that:allowBuildstakes precedence (if present)dangerouslyAllowAllBuildsis properly restored (if set and no workspace policy exists){}is only applied as a last resort when no policy is configured anywhereHonor
--silentwhenverifyDepsBeforeRun: installauto-installs dependencies beforepnpm runorpnpm exec, preventing install output from being written to stdout #11636.Fix lockfile parsing failures when
pnpm-lock.yamlcontains CRLF line endings and multiple YAML documents #11612.Anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node —
engines.runtimepin when present, shellnodeotherwise — instead of pnpm's own runtime.ENGINE_NAME(the<platform>;<arch>;node<major>prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed fromprocess.version— the Node that runs pnpm itself. That was wrong in two situations:@pnpm/exeSEA bundle. The bundle has its own embedded Node, not thenodeon the user'sPATHthat actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shellnode.engines.runtime/devEngines.runtimepin. When a project pins a Node version viadevEngines.runtime(pnpm v11+), pnpm downloads that Node intonode_modules/node/and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node.Three changes:
@pnpm/engine.runtime.system-node-versionnow exportsengineName(nodeVersion?). Resolves the version in this order: explicit override →getSystemNodeVersion()(which already prefersnode --versionoverprocess.versionin SEA contexts) →process.version.@pnpm/deps.graph-hashernow exportsfindRuntimeNodeVersion(snapshotKeys)— scans an iterable of lockfile snapshot keys for anode@runtime:<version>entry and returns its bare version string.calcDepStateandcalcGraphNodeHash/iterateHashedGraphNodesaccept anodeVersion?(in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded toengineName(). The default (no override) preserves the pre-change behaviour. The legacyENGINE_NAMEconstant in@pnpm/constantsis unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up.@pnpm/installing.deps-resolver,@pnpm/installing.deps-restorer,@pnpm/installing.deps-installer,@pnpm/building.during-install,@pnpm/building.after-install,@pnpm/deps.graph-builder) now derives the project's pinned runtime viafindRuntimeNodeVersion(Object.keys(graph))once per invocation and threads it through.On upgrade, two one-time GVS slot churns are possible:
node26) now hash under the shell-Node major (e.g.node24), matching what pacquet, the npm-publishedpnpmpackage, and any other pnpm-compatible tool already produce.devEngines.runtimepin: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on.In both cases the old slots become prune-eligible.
Resolve the GVS hash's engine portion per-snapshot when a dependency declares its own
engines.runtime, instead of using an install-wide value.Pnpm's resolver desugars a dep's
engines.runtimeintodependencies.node: 'runtime:<version>', and the bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into<pkgDir>/node_modules/node/. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so a pinning snapshot's slot encoded the wrong Node major, and a reinstall on the same host could read the cached side-effects under a key whose<platform>;<arch>;node<major>triple disagreed with the Node the build actually ran on.Per-snapshot resolution now matches what
bins/linkeralready does on a per-package basis:@pnpm/deps.graph-hasheraddsreadSnapshotRuntimePin(children)— reads thenodeentry from one snapshot's graph children and extracts the version from anode@runtime:value. Pairs with the existingfindRuntimeNodeVersion(snapshotKeys)install-wide fallback (also now exported from@pnpm/deps.graph-hasherrather than@pnpm/engine.runtime.system-node-version, where it was a poor fit —system-node-versionis about probing the host Node, not parsing lockfile-derived strings).calcDepStateandcalcGraphNodeHashconsultreadSnapshotRuntimePin(graph[depPath].children)first and only fall back to the install-widenodeVersionparameter when the snapshot doesn't pin its own Node.Pacquet mirrors the same precedence at the
calc_graph_node_hashcall site inpackage-manager/src/virtual_store_layout.rs— a newfind_own_runtime_node_major(snapshot)helper reads each snapshot'sdependenciesfor anodeentry withPrefix::Runtimeand overrides the install-wide engine when present.On upgrade, snapshots of dependencies that declare their own
engines.runtimere-hash under that dep's pinned Node instead of the install-wide value. The old slots become prune-eligible. Closes #11690.Fixed
pnpm publishfailing with a 404 when authentication relied on OIDC trusted publishing alongside an.npmrcwritten byactions/setup-node(_authToken=${NODE_AUTH_TOKEN}) withoutNODE_AUTH_TOKENbeing set. Unresolved${VAR}placeholders in auth values are now treated as empty rather than passed through verbatim, so the literal placeholder no longer surfaces as a bearer token when OIDC fallback is the intended auth source #11513.Fix
devEngines.packageManager(singular form, withoutonFail) defaulting toonFail: "error"instead of the documentedpmOnFail: "download". As a result, a project that pinned a different pnpm version viadevEngines.packageManagerand ranpnpm installfrom a mismatched pnpm version failed with a hard error, even though the migration table frommanagePackageManagerVersions: truetopmOnFail: download (default)promises the install would auto-download the wanted version #11676.The array form of
devEngines.packageManagerkeeps its existing per-element defaults (errorfor the last entry,ignorefor the rest), since those reflect explicit prioritization by the user. ExplicitonFailvalues continue to win.Fix
devEngines.packageManagernot writingpackageManagerDependenciestopnpm-lock.yamlwhen the lockfile lacks an env-doc entry. Previously the lockfile sync skipped resolution unless an existingpackageManagerDependencies.pnpmentry needed refreshing, so a fresh install withoutonFail: "download"left the resolved pnpm version unrecorded — contradicting the documented behavior that the resolved version is stored inpnpm-lock.yaml#11674.Warn when
package.jsoncontains a legacypnpmfield with settings pnpm no longer reads frompackage.json(e.g.pnpm.overrides,pnpm.patchedDependencies). Previously these were silently ignored after the upgrade from v10, leaving users unaware that their overrides/patched dependencies had stopped taking effect #11677.Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.