nodejs/release-keys added a new release key (Stewart X Addison,
655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD), which made the
check:node-release-keys release gate fail. Sync the embedded keys
via update-node-release-keys.mjs --update.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backport of 5a4daec4bd from main.
Project-level pnpm-workspace.yaml is repository-controlled and therefore
untrusted, so getOptionsFromPnpmSettings refuses to expand ${...}
environment-variable placeholders in request-destination fields. The guarded
set only covered registry; the httpProxy, httpsProxy, noProxy, proxy, and
noproxy fields were missing, so a malicious repository could route requests
through an attacker-controlled proxy whose URL embedded a secret and
exfiltrate install-time secrets before any lifecycle script runs.
Add the proxy fields to REQUEST_DESTINATION_SCALAR_KEYS so they receive the
same protection already applied to the registry channel.
Reported by YESHYUNGSEOK as flaw F01 of GHSA-vx52-2968-3vc6.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Backports the TypeScript portion of 51300fd41c (#12872) to release/10.
Two advisories:
- GHSA-c59q-g84q-2gj5: lockfile depPath name -> isolated linker + PnP.
- GHSA-vq4v-j7r6-jq4m: dependency manifest `name` from the fetched
tarball (e.g. @x/../../../path) -> resolver/linker.
Apply safeJoinModulesDir at lockfileToDepGraph (deps/graph-builder) and
the PnP packageLocation (lockfile-to-pnp); validate manifest names in
resolve-dependencies (extendGraph + resolvePeers); contain the
global-virtual-store slot dir by rejecting a traversal version segment
in iterateHashedGraphNodes (calc-dep-state), the single point every GVS
slot path funnels through.
The pacquet and env-installer changes from the original commit do not
apply to v10 (no pacquet stack; installConfigDeps has no GVS subdep
installation and config-dependency names are already validated by
assertValidConfigDepName from #12504).
All rejections surface ERR_PNPM_INVALID_DEPENDENCY_NAME.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: allow pnpm v10 to switch/self-update to pnpm v12
pnpm v12 (the Rust port) publishes the `pnpm` and `@pnpm/exe` packages
with placeholder bins that a preinstall script replaces with the host's
native binary from a `@pnpm/exe.<platform>-<arch>[-musl]` optional
dependency. Because pnpm installs its own engine with --ignore-scripts,
that relinking never ran, leaving a non-executable placeholder and a
broken switch/self-update.
installPnpmToTools now:
- converges on the native `pnpm` package for v12+ (equal content to
`@pnpm/exe`, ships every target incl. darwin-x64);
- relinks the native binary for any wrapper that ships one (`@pnpm/exe`
for all majors and, from v12, `pnpm`), recognizing both the legacy
`@pnpm/<os>-<arch>` and the new `@pnpm/exe.<platform>-<arch>[-musl]`
platform-package naming; and
verifyPnpmEngineIdentity now verifies the native platform package's npm
registry signature for v12's `pnpm` wrapper, not only `@pnpm/exe".
* fix(security): verify pnpm engine before relinking and sanitize wrapper bin names
Addresses a Windows-only pre-verification write primitive: linkExePlatformBinary
read bin *keys* from the not-yet-verified wrapper package.json and used them as
path.join destinations. A crafted bin map (e.g. '../../evil') could unlink/hardlink
outside the staged dir before verifyPnpmEngineIdentity ran.
- Run verifyPnpmEngineIdentity before linkExePlatformBinary so no file whose name
derives from the wrapper's own manifest is mutated until the wrapper is proven
to be a genuine, signed pnpm release.
- Add safeWrapperBinNames() as defense in depth: relink only bin names that
resolve to a direct child of the wrapper dir (no '..', separators, or absolute
paths).
Bump tmp (>=0.2.7), tar (>=7.5.16), and @babel/core (>=7.29.6) to patched
versions, and ignore two dev-only advisories with no compatible patch
(js-yaml GHSA-h67p-54hq-rp68, http-proxy-middleware GHSA-64mm-vxmg-q3vj).
Backports three merged security fixes from main to release/10:
- Contain hoisted dependency aliases so a crafted lockfile alias cannot
escape node_modules or overwrite pnpm-owned layout. The hoisted graph
builder now validates each alias at the safeJoinModulesDir sink.
(GHSA-fr4h-3cph-29xv, #12343)
- Contain pnpm patch-remove deletions to the configured patches
directory. (#12341)
- Reject path-traversal config dependency names and versions from
pnpm-workspace.yaml before they are used to build filesystem paths.
(GHSA-qrv3-253h-g69c, #12470)
Written by an agent (Claude Code, claude-opus-4-8).
The key embedded in the warning's suggested 'pnpm config set' command comes
from a repository-controlled .npmrc. The previous guard only suppressed the
example for keys containing a ${...} placeholder, but a shell also expands
$(...), backticks and $VAR inside double quotes — so a crafted key could turn
the suggested copy-paste command into command execution. The example is now
emitted only for keys made up entirely of shell-inert characters.
The previous warning only said the setting was ignored. It now explains why
(the project .npmrc is committed to the repository and must not expand secrets
into request destinations or credentials) and how to fix it: move the value to a
trusted source such as the user-level ~/.npmrc or via pnpm config set, with a
link to the docs.
The suggested 'pnpm config set' example is only shown when the key has no
${...} placeholder, so the snippet is always safe to copy-paste (a shell would
otherwise expand a placeholder embedded in the key). The wording no longer
claims a specific destination file, since pnpm config set delegates registry
and auth keys to npm.
A repository-controlled project or workspace .npmrc could set userconfig,
globalconfig, or prefix to redirect pnpm into loading an attacker-supplied,
repo-shipped file as a trusted user/global config source. Such sources are not
subject to the untrusted-env-expansion filtering added for GHSA-3qhv-2rgh-x77r
and may set tokenHelper, so this was a bypass of that fix that could exfiltrate
environment secrets to an attacker-selected registry.
@pnpm/npm-conf@3.0.3 resolves the user/global config destinations from trusted
layers (cli, env, builtin, defaults) before the project/workspace .npmrc files
are added to the config chain.
* fix(security): harden build-approval artifact identities on v10
Port of pnpm/pnpm#12294 (commit bf1b731ee6) to release/10.
Package-name entries in onlyBuiltDependencies (and allowBuilds) no longer
approve lifecycle scripts for artifacts whose identity a name cannot pin:
git, git-hosted tarball, direct tarball, and local directory dependencies.
To approve such an artifact explicitly, use its peer-suffix-free lockfile
depPath as the key — the GIT_DEP_PREPARE_NOT_ALLOWED hint, pnpm
ignored-builds, and pnpm approve-builds print exactly that key.
- AllowBuild policy functions identify packages by DepPath instead of
caller-supplied name/version. Identity trust is derived from the depPath
shape: a registry-style depPath (name@semver) is a trusted identity.
- The trust is sound because lockfile entries are structurally checked
wherever they are materialized into fetchable resolutions
(pkgSnapshotToResolution) and before rebuild runs scripts: a
registry-style key backed by a git, directory, or git-hosted tarball
resolution is rejected with ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
- preparePackage requires a pkgResolutionId and gates on the synthesized
name@<resolution id> depPath; scp-style git URLs are normalized to
ssh:// form in resolution ids and the git fetcher reuses
createGitHostedPkgId from the resolver.
- isGitHostedTarballUrl matches case-insensitively and is shared from
@pnpm/lockfile.utils; new removePeersSuffix() in @pnpm/dependency-path
and allowBuildKeyFromIgnoredBuild() in @pnpm/builder.policy.
- pnpm rebuild and approve-builds accept depPath specs for selecting and
approving artifact builds; installs rebuild ignored builds approved by
depPath keys.
- shell-quote is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p /
CVE-2026-9277).
Differences from main: v10 has no lockfile resolution verifier, so the
structural pass lives in pkgSnapshotToResolution and the rebuild loop; the
AllowBuild policy keeps v10's boolean return; the revoked-approval
detection and global-virtual-store hash changes have no v10 counterpart.
* test: serve the direct-tarball fixture from an off-registry origin
The registry mock binds only localhost, so a hard-coded 127.0.0.1 URL is
refused in CI, and a tarball URL on the registry's own origin resolves as
a registry package, which defeats the direct-tarball identity the test
needs. An in-test HTTP server redirecting to the mock provides a separate
origin whose binding the test controls.
* test: approve the git-protocol licenses fixture by its depPath
A git-hosted artifact has an untrusted package identity, so the
ajv-keywords build has to be approved by its depPath. Pin the fixture to
the commit the depPath names.
* fix(package-bins): reject reserved manifest bin names
Manifest bin keys "", ".", "..", and scoped forms such as "@scope/.."
passed the bin-name guard because encodeURIComponent leaves them
unchanged. When joined to the global bin directory during global
remove/update/add operations, "." resolves to the bin directory itself
and ".." to its parent, which removeBin then recursively deletes.
Reject empty, ".", and ".." bin names after scope stripping.
Backport of pnpm/pnpm#12289 to v10.
* fix: block untrusted request destination env expansion
Makes environment expansion trust-aware for registry/auth config and
request destinations:
- Stops project and workspace .npmrc files from expanding ${...}
placeholders in registry/proxy request destinations, URL-scoped keys,
and registry credential values.
- Stops repository-controlled pnpm-workspace.yaml from expanding
${...} placeholders in the registry setting.
- Preserves env expansion for trusted user/global/CLI/env config so
existing token and registry setup flows continue to work.
Backport of pnpm/pnpm#12291 (CAND-PNPM-122 / GHSA-3qhv-2rgh-x77r) to v10.
* fix(security): verify npm registry signature before spawning a package-manager binary
The packageManager field (and pnpm self-update) makes pnpm download and
run a specific pnpm version. The staged install's bytes were trusted
based on lockfile integrity alone, which proves nothing when the inputs
are repository-controlled.
pnpm now verifies the npm registry signature of the engine it is about
to spawn, over the installed integrity, against npm's public signing
keys embedded in the pnpm CLI (exactly as corepack does):
- verifyPnpmEngineIdentity() checks pnpm/@pnpm/exe and the materialized
platform binaries of the staged install before it is linked into the
tools directory.
- Fails closed: any verification failure, including an unreachable
registry, refuses the version switch rather than running an unverified
binary. Runs only on a tools-directory cache miss (an actual
download).
- The embedded keys live in a generated file kept in sync with npm's
keys endpoint by scripts/update-npm-signing-keys.mjs; the release
workflow runs the check as a gate so a key rotation cannot silently
break verification.
Backport of pnpm/pnpm#12292 (CAND-PNPM-097) to v10.
* fix: harden package-manager bootstrap metadata
Resolve package-manager bootstrap traffic through trusted user/CLI
registries and trusted network config, defaulting to the public npm
registry instead of project/workspace registry settings:
- getConfig() now computes packageManagerRegistries and
packageManagerNetworkConfig from trusted config sources only (CLI
options, env config, user and global .npmrc) — never the repository's
project/workspace .npmrc or pnpm-workspace.yaml.
- switchCliVersion() applies that bootstrap config when installing and
verifying the wanted pnpm version, so repository .npmrc
proxy/TLS/registry values cannot steer package-manager bootstrap
traffic.
Backport of pnpm/pnpm#12296 to v10. The v11 env-lockfile validation
parts do not apply: v10 bootstraps the wanted version through a staged
child install instead of an env lockfile.
* fix(security): verify Node.js runtime SHASUMS OpenPGP signature
When a repository requests a Node.js runtime (useNodeVersion or an
execution env), pnpm downloads and then executes a Node binary. The
download mirror is repository-configurable via node-mirror:<channel> in
project .npmrc, and the integrity came from SHASUMS256.txt fetched from
that same mirror — a circular check a malicious mirror can satisfy with
a tampered binary and matching hashes.
pnpm now fetches SHASUMS256.txt.sig and verifies its detached OpenPGP
signature against the Node.js release team's public keys, embedded in
the pnpm CLI, before trusting the hashes:
- @pnpm/crypto.shasums-file: new fetchVerifiedNodeShasums /
fetchVerifiedNodeShasumsFile verify the signature via openpgp against
the embedded keys (generated src/nodeReleaseKeys.ts, mirrored from
the canonical nodejs/release-keys list).
- @pnpm/node.fetcher verifies the configurable-mirror SHASUMS for the
release channel; pre-release channels (rc, nightly, ...) are unsigned
by Node and remain unverified.
- scripts/update-node-release-keys.mjs keeps the keys current
(pnpm run check:node-release-keys / update:node-release-keys), and
the release workflow runs the check as a gate.
Backport of pnpm/pnpm#12295 to v10 (without the pacquet Rust port,
which does not exist on this branch).
* test(env): sign the SHASUMS fixture for Node.js download tests
The Node.js download tests exercise the release channel, whose
SHASUMS256.txt is now signature-verified. Sign the fixture with a
generated OpenPGP key and trust it through the new
trustedNodeReleaseKeys test seam (threaded from plugin-commands-env via
@pnpm/node.fetcher to fetchVerifiedNodeShasums), so the tests keep
exercising the verification path instead of bypassing it.
* fix(self-updater): redact registry credentials from engine identity errors
Registry URLs may legally embed basic-auth credentials
(https://user:pass@host/). verifyPnpmEngineIdentity() interpolated the
packument URL and registry URL into PnpmError messages, and the
unreachable-registry path surfaced fetch-layer error messages that embed
the request URL — all of which land in terminal output and CI logs.
Strip URL credentials from every error message and truncate the non-200
response body.
* fix: update vulnerable transitive dependencies
Override shell-quote to >=1.8.4 (GHSA-w7jw-789q-3m8p, critical, pulled
in via concurrently) so the audit workflow passes again. The advisory
was published after the last release/10 audit run; it is unrelated to
the security backports on this branch.
A tampered `pnpm-lock.yaml` that strips the `integrity` field from a
tarball resolution let the worker download the URL contents and mint a
fresh integrity from the unverified bytes. An attacker who could both
alter the lockfile (e.g. via a pull request that drops `integrity:`)
and serve modified content at the referenced tarball URL could install
a tampered package without any error — including under
`--frozen-lockfile`.
`pkgSnapshotToResolution` now fails closed at lockfile-read time with
`ERR_PNPM_MISSING_TARBALL_INTEGRITY` whenever a tarball-shaped
resolution (no `type` field — covers plain remote, registry-derived,
`file:`, and `gitHosted` entries) lacks integrity. Git-hosted
tarballs and `file:` tarballs remain exempt: the commit SHA in a
git-host URL and the user-controlled local path already anchor the
bytes.
The fix sits at the lockfile-read chokepoint every install path flows
through (deps-resolver, deps-restorer, graph-builder), so both
isolated and hoisted node-linkers are covered.
Credit to AutoFyn for finding and reporting the issue.
The `pnpm/bundle` step runs `ts-node bundle.ts`, which transitively
loads `esm@3.2.25` (via `publish-packed` → `all-module-paths` → `esm`).
That package monkey-patches Node internals removed in Node 22+. The
release workflow runs on `ubuntu-latest` and was relying on the
runner's default Node; GitHub bumping that default between 10.33.4
and 10.34.0 silently broke the publish phase.
Pin Node 20 — the last release line `esm@3.2.25` still works on — and
drop `continue-on-error` from the Publish step so future failures fail
the workflow instead of hiding behind a green check.
The upstream `https://gitlab.com/opensource-saurik/ldid.git` is no
longer publicly accessible (returns 401 / redirects to sign-in), so
`git clone` prompts for a username and fails non-interactively with
`could not read Username for 'https://gitlab.com'`. This blocks every
v10 release.
`ProcursusTeam/ldid` is a maintained GitHub fork that contains the
exact commit the workflow pins (c2f8abf), and the libplist submodule
already points at github.com, so a one-line URL swap is the full fix.
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(config): pin unscoped per-registry settings to their source's registry at load time
Each .npmrc / auth.ini / CLI source's unscoped per-registry settings
(_authToken, _auth, username/_password, tokenHelper, inline cert/key)
are rewritten to their URL-scoped equivalent during load, using the
same source's registry= value (or the npmjs default if none). After
this rewrite the merged config contains only URL-scoped settings, so a
later layer overriding registry= (workspace .npmrc, pnpm-workspace.yaml,
CLI --registry) cannot rebind a credential or client certificate to a
different host.
Each rescope emits a deprecation warning naming the source and the URL
the setting was pinned to. ca/cafile are intentionally not rescoped —
they're trust anchors, not credentials, and corporate MITM-proxy setups
rely on them applying globally.
Ported from #11953. Reported by JUNYI LIU.
---
Written by an agent (Claude Code, claude-opus-4-7).
* chore(changeset): bump @pnpm/network.auth-header to major
Removing the unscoped credential fallback in getAuthHeadersFromConfig
is a breaking change to the package's contract — callers passing
unscoped _authToken/_auth/username+_password/tokenHelper alongside a
default registry no longer get a header back. Matches the major bump
in #11953.
---
Written by an agent (Claude Code, claude-opus-4-7).
Backport of #11968 to release/10. Treats tarball-integrity mismatches
against the lockfile as a hard failure by default; `--update-checksums`
is the only opt-in. `--force` and `pnpm update` deliberately do not
bypass the integrity check.
🤖 Written by an agent (Claude Code, claude-opus-4-7).
The git fetcher passed the lockfile-controlled `resolution.commit` value
to `git fetch --depth 1 origin <commit>` and `git checkout <commit>`
without a `--` separator and without validating that the value is a
SHA. A malicious lockfile could smuggle a value such as
`--upload-pack=touch /tmp/pwned`, which `git` parses as an option. On
SSH and local-file transports `--upload-pack` runs the supplied
command. HTTPS ignores it, but the SSH/file paths are enough to
execute code as the user running `pnpm install`.
Validate `resolution.commit` against `/^[0-9a-f]{40}$/i` at the entry
of the fetcher and throw `INVALID_GIT_COMMIT` otherwise. The check is
strictly stronger than adding a `--` separator: a validated value
cannot start with `-` or contain shell-significant characters at all.
Backport of #11967 to release/10.
---
Written by an agent (Claude Code, claude-opus-4-7).
A transitive registry package can use a dependency-alias key like
`@x/../../../../../.git/hooks` to make `pnpm install` create a symlink
outside the intended `node_modules` directory, since pnpm passes the
alias straight into `path.join(modulesDir, alias)` without checking
that the joined path stays inside `modulesDir`.
Reject aliases that aren't valid npm package names at manifest-read
time (both the importer's manifest and every transitive package
manifest) and re-check at the symlink layer as defense in depth.
Backport of #11954 to the release/10 line.
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(patching/apply-patch): reject patch paths that escape the patched directory
A malicious .patch file with `diff --git a/../../X` headers could otherwise
write, delete, or rename files outside the patched package as the user
running `pnpm install`.
* refactor(patching/apply-patch): narrow caught errors via util.types.isNativeError
Drops the `any`-typed catch + eslint-disable in favor of the cross-realm-safe
narrowing pattern documented in CLAUDE.md.
* refactor(patching/apply-patch): replace error helper with PatchPathEscapesError class
* chore(patching/apply-patch): reword comment to satisfy cspell
Backport of #11622 to release/10. The npm registry returns abbreviated
package metadata (without per-version `time`) by default, which made the
maturity check throw ERR_PNPM_MISSING_TIME whenever cached abbreviated
metadata was reused under `publishedBy`/`minimumReleaseAge`. pnpm now
upgrades cached abbreviated metadata to the full document via a follow-up
fetch, persists the upgrade to the on-disk cache so subsequent installs
skip the extra fetch, and lets ERR_PNPM_MISSING_TIME from the cache fast
path fall through to the network fetch even under strict mode.
Adapted to release/10's simpler pickPackage shape (no 304/etag plumbing,
no `pickMatchingVersionFast`/`pickMatchingVersionFinal` split, no
`ignoreMissingTimeField`), so the unconditional helper triggers the
upgrade whenever the cached meta lacks `time`.
The `--prefix` option (renamed to `dir`) was applied to options after
`getWorkspaceDir` had already run, so workspace detection fell back to
`process.cwd()` and missed the manifest at the prefix dir. As a result,
running pnpm from outside the project (e.g. `pnpm --prefix=child install`
from the parent dir) silently skipped settings declared in
`child/pnpm-workspace.yaml`.
Backport of #11549 to release/10. Closes#11535.
Cherry-pick of #11481 from main, adapted to the v10 layout.
For git-hosted tarballs (codeload.github.com / gitlab.com / bitbucket.org)
the fetcher dropped the integrity it computed while downloading, so the
lockfile only stored the URL. A compromised git host or man-in-the-middle
could serve a substituted tarball on subsequent installs and pnpm would
install it without lockfile changes.
This pins the SHA-512 SRI of the raw tarball in the lockfile in the same
sha512-<base64> form npm-registry tarballs use; subsequent installs verify
the download against that integrity in the worker.
A new optional gitHosted: boolean field is recorded on TarballResolution
so every store-key consumer can route by a single typed read instead of
re-deriving the routing from the URL. Lockfiles written by older pnpm
versions are enriched on load (URL fallback) so the field can be relied
on uniformly.
🤖 Cherry-picked by Claude (claude-opus-4-7) on behalf of @zkochan
* fix: include workspace root only when --filter is positive
PR #10465 stopped the implicit workspace-root exclusion for `pnpm -r
run/exec/test/add` whenever any --filter was provided. That gate was
too broad: with a negative-only filter set (e.g. `--filter '!a'`) the
workspace root started showing up in the matched projects, contradicting
the documented default behavior.
Only suppress the implicit root exclusion when the user provided at
least one positive (non-`!`) filter that could have been intended to
select the root. Negative-only filter sets keep the documented default,
and `--include-workspace-root` continues to opt the root in explicitly.
Close#11341.
* test: fix root-exclusion assertion to match --stream prefix
The --stream reporter prefixes output lines with the project's relative
directory, so the workspace root appears as `. which$`, not
`root which$`. The original assertion would have passed even if the
regression were still present. Verified the corrected assertion fails
against the buggy code and passes against the fix.
`pnpm self-update` defaults to the `latest` dist-tag, but `latest` on the
registry can lag the installed version when a new major has shipped
without being tagged. Refuse to downgrade in that case. Users can still
run `pnpm self-update latest` (explicit) to force the downgrade.
Closes#11418
When self-updating from a v10 @pnpm/exe install on Intel macOS
(darwin-x64) to a v11+ version, transparently install the JS-only
"pnpm" npm package instead of @pnpm/exe.
@pnpm/exe v11+ no longer ships a darwin-x64 binary because Node.js
SEA injection produces a binary that segfaults on Intel Macs (upstream
nodejs/node#62893; pnpm/pnpm#11423). Without this swap, self-update
would resolve @pnpm/exe@11.x, install a manifest with no platform
binary, and leave the user with a broken bin shim.
The new install requires Node.js on PATH; a globalWarn explains the
swap when it happens. All other host/version combinations are
unchanged. The targetPkgName is also threaded through getToolDirPath,
the `pnpm add` command, and the linkExePlatformBinary guard so the
new install lives under the correct tool directory and the
@pnpm/exe-specific platform-binary linking is skipped for the JS path.
* feat(self-update): point users at the migration guide across major bumps
When `pnpm self-update <version>` crosses a pnpm major (upward) from
the version being upgraded from, print a one-line pointer to the
versioned migration guide on pnpm.io.
The "from" version is the project's `packageManager` pin when present
(via the existing `managePackageManagerVersions` gate), falling back
to the running binary's version otherwise. No-op updates
(target === previous) are silent.
v11 points at https://pnpm.io/11.x/migration. Future majors register
an entry in the in-file `MAJOR_UPGRADE_HINTS` table.
This is the v10-line backport of the corresponding main-branch change
in #11354.
* fix: update lockfile
* fix: prevent fork-bomb during packageManager-driven version switching
When pnpm was installed via one method (e.g. `npm install -g pnpm@A`)
and run in a project whose package.json's packageManager field selected
a different pnpm version (pnpm@B), and a pnpm-workspace.yaml existed at
the project root, the install-child spawned by `installPnpmToTools` to
fetch pnpm@B inherited a cwd under the pnpm home directory. pnpm's
workspace walk-up from there discovered the ancestor pnpm-workspace.yaml,
adopted the root package.json, and re-triggered switchCliVersion inside
the child. Because the target tool dir had not yet been symlinked in, the
recursive installPnpmToTools call saw alreadyExisted === false and kicked
off another nested install, recursing forever at 100% CPU.
Force the install-child's environment to disable its own version handling:
- `npm_config_manage_package_manager_versions=false` (v10 setting name)
- `pnpm_config_pm_on_fail=ignore` (v11+ setting name)
Also set the v11 setting on the final spawn at the end of switchCliVersion,
so when v10 hands off to a v11 target the child's check/download paths stay
disabled regardless of which env-var convention the child reads.
Closes#11337.
* test: add v11-switch and same-version regression tests for #11337
- v11 switch with a root pnpm-workspace.yaml: covers the primary #11337
reproducer (target major differs from running major). Before the fix this
fork-bombed via the install-child's workspace walk-up; now it reaches the
terminal spawn and `installPnpmToTools` completes.
- Same-version short-circuit: with a root pnpm-workspace.yaml and
`packageManager: pnpm@<current>`, `switchCliVersion` must return at the
`pm.version === packageManager.version` guard, and the tool dir must not
be created. Guards against a future regression where the ancestor
pnpm-workspace.yaml alone accidentally triggers an install.
* fix(installPnpmToTools): isolate the install-child from the caller's workspace
Pass `--ignore-workspace` to the child pnpm so it doesn't walk up from the
stage directory and adopt the caller's pnpm-workspace.yaml as its own root.
That walk-up was both (a) the mechanism that caused the #11337 fork-bomb
(the child would rediscover the caller's packageManager field and re-enter
switchCliVersion) and (b) a correctness problem in its own right: once the
child treats the caller's project as its workspace, `pnpm add` runs with
semantics that don't match an isolated tool-dir install. The env-var guards
from the previous commit stay in place as a defense-in-depth measure in
case any future code path surfaces a wantedPackageManager without going
through workspace discovery.
Also fold the new v11-switch regression into the existing v11 test rather
than adding a second v11 install, so CI doesn't fetch pnpm@11.0.0-rc.5
from the real npmjs registry twice. The tool-dir assertion in that test
now doubles as a fork-bomb regression check for the v11-target path.
* fix(installation): skip pnpm exe when no Node.js is on PATH
When pnpm is installed as @pnpm/exe (a Single Executable Application
that bundles Node.js into the pnpm binary) and the user has no separate
Node.js on PATH, `which('node')` fails and `getNodeExecPath` used to
fall back to `process.execPath` - which in @pnpm/exe is the pnpm binary
itself, not a Node binary. That path got baked into generated global
bin shims via nodeExecPath, so running any globally-installed CLI
invoked pnpm with the target script as its first positional arg,
producing `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` from the current
working directory.
Detect the @pnpm/exe case via `detectIfCurrentPkgIsExecutable` and
return undefined from the fallback so the shim emits a plain
`exec node <target>` instead.
Closes#11291
Refs #4645
* Update pkg-manager/plugin-commands-installation/src/nodeExecPath.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
sveltejs/action-deploy-docs was deleted/privated, causing git clone to
prompt for credentials and fail in non-interactive CI. Matches the repo
already used on the v11 branch.
The previous overrides `minimatch@<3.1.4: '>=3.1.4'` and
`minimatch@>=9.0.0 <9.0.7: '>=9.0.7'` were unbounded on the upper end
and resolved to minimatch@10.x, which breaks consumers built against
v3's CJS export shape (e.g. @verdaccio/utils fails with
`Cannot read properties of undefined (reading 'makeRe')`).
* fix: defer npm-passthrough commands to main() when packageManager wants pnpm v11+
The argv[0] passthrough in pnpm.ts runs before main() loads config and calls
switchCliVersion, so users with packageManager=pnpm@11 on a v10 host hit npm
directly for commands like `version`, bypassing v11's native implementations.
Walk up from cwd to the nearest package.json; if its packageManager field
selects pnpm v11 or newer, skip the passthrough and let main() switch.
Fixes#11328.
* test: add e2e test for v11 deferral, and only read package.json on passthrough
Move the shouldSkipNpmPassthrough check inside the passthrough switch case
so non-passthrough commands (install, add, run, etc.) don't pay for the
cwd-to-/ walk when their own config loading will discover packageManager
anyway.
Add an e2e test that pairs packageManager=pnpm@11.0.0-rc.3 with
manage-package-manager-versions=false so the assertion verifies main()
took over (command falls through to the `run` fallback and fails with
'Command "version" not found') without actually downloading pnpm v11 —
that keeps the test fast and network-independent.
* fix: walk up past workspace leaf manifests when detecting wanted pnpm major
- Continue walking up when package.json lacks a string packageManager field,
so workspace leaves (which typically omit it) correctly pick up the root's
pnpm@11+ declaration.
- Only treat ENOENT/ENOTDIR as "keep walking" — other fs errors now surface
as null instead of silently consulting an ancestor manifest.
- Lazy-load readWantedPnpmMajor in pnpm.ts to keep cold start unaffected for
non-passthrough commands.
* fix: honor manage-package-manager-versions when deciding to skip npm passthrough
When a user pinned pnpm v11 in packageManager but disabled version switching
(via .npmrc or an explicit `npm_config_manage_package_manager_versions` env
var), pnpm v10's main() won't switch to the wanted pnpm — so bypassing the
legacy argv[0] npm passthrough would leave commands like `version` stranded
with "Command not found". Fall back to the passthrough in that case.
Also:
- Treat a non-string `packageManager` field as malformed (return null) rather
than conflating it with "missing" and walking up to an ancestor manifest.
- Extract the decision into a testable `shouldSkipNpmPassthrough(env, cwd)`
helper and unit-test the full matrix.
* test: add e2e coverage for the v11+ switching-enabled passthrough skip
Covers the primary #11328 fix scenario end-to-end: when packageManager pins
pnpm v11+ and switching is enabled, the argv[0] passthrough is skipped and
main() hands off to switchCliVersion, which installs pnpm v11 and runs its
native `version` command (not npm's). Also updates the stale comment on the
v10 test that referenced a now-different adjacent test.
The test uses the real npmjs registry for this one install; the registry
mock's proxy is slow and flaky for pnpm's full dep tree, and the
end-to-end assertion only needs a real v11 tarball to prove the handoff.
* test: assert pnpm v11's version header in the switch-e2e test
* test: use v11's ERR_PNPM_INVALID_VERSION_BUMP marker and assert on combined streams
The --help-based assertion worked locally but failed in CI with stdout="", which
obscured the cause. Switching to `pnpm version` (no args) triggers v11's native
ERR_PNPM_INVALID_VERSION_BUMP error — a marker that only pnpm v11 can produce —
and asserting on stdout+stderr combined makes any install/switch failure
visible in the diff instead of a silent "expected substring not found".
* test: assert tool-dir presence instead of v11 executing in CI
pnpm v11 requires Node.js >= 22.13 but the GH Actions workflow runs on 22.12,
so v11 errors out at its own Node-version check before reaching the `version`
command. The tool dir's presence is a lower-level but direct proof that
installPnpmToTools ran — which is reached only from main() → switchCliVersion,
so the argv[0] passthrough must have been skipped. Combined with asserting
the absence of npm's `Bump a package version` help line, the test covers
the #11328 fix without depending on v11 actually executing a command.