* fix(patch): prevent git config path errors in patch-commit
Replace HOME='' with GIT_CONFIG_GLOBAL to bypass user config
without breaking home directory resolution in restricted environments.
Fixes#6537
* fix(patch): prevent git config path errors in patch-commit
Use GIT_CONFIG_NOSYSTEM and GIT_CONFIG_GLOBAL to bypass git config
without breaking HOME path resolution in restricted environments.
Fixes#6537
* fix: skip re-importing packages when global virtual store is warm
When node_modules is deleted but the global virtual store directories
survive, pnpm previously re-fetched every package because the skip
logic required currentLockfile to be present. Add a fast-path that
checks pathExists(dir) for GVS directories even when currentLockfile
is missing, since the GVS directory hash encodes engine, integrity,
and full dependency subgraph.
* fix: remove includeUnchangedDeps guard from GVS fast-path
The includeUnchangedDeps flag is true whenever currentHoistPattern
differs from the desired hoistPattern. After deleting node_modules,
currentHoistPattern is always undefined (read from .modules.yaml),
so the flag is always true when hoisting is configured — defeating
the optimization in the exact scenario it targets.
The guard is unnecessary because the fast-path only skips fetch/import
(fetchResponse = {}), not graph inclusion. The package is still added
to the graph with children populated, so hoisting recalculation works.
* perf: add GVS warm reinstall benchmark scenario
Adds benchmark 6: frozen lockfile reinstall with a warm global virtual
store after deleting node_modules. This measures the reattach fast-path
where all packages are skipped (no fetch/import) because their GVS
hash directories already exist.
* fix: use proper types in fetchPackage spy to pass tsgo strict checks
* fix(dependencies-hierarchy): handle undefined pkgSnapshot in pnpm why -r
Running pnpm why -r crashes with TypeError due to undefined pkgSnapshot during
invokation of pkgSnapshotToResolution.
This commit wraps resolution and integrity steps with if (pkgSnapshot).
Also, added unit test for the same.
close#10700
* fix: handle undefined pkgSnapshot in pnpm why -r
* Apply suggestions from code review
#10700
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
When pnpm is installed as a standalone executable in environments without
a system Node.js (e.g. Docker containers), the `@pnpm/exe` preinstall
script (`node setup.js`) fails because `node` is not on PATH. This broke
version switching via the `packageManager` field in package.json since
v10.30.2, which changed `getCurrentPackageName()` to return `@pnpm/exe`
instead of platform-specific package names like `@pnpm/linux-x64`.
Install with `--ignore-scripts` and link the platform-specific binary
in-process instead. The setup logic is inlined because setup.js can't
be loaded at runtime: `require()` fails on ESM (pnpm v11+) and
`import()` is intercepted by pkg's virtual filesystem in standalone
executables.
Closes#10687
* fix(shell-completion): give correct suggestions when command line ends with a space
fixes an issue where if you tried to complete any part of any subcommand
with a space before <TAB> (eg: `pnpm run <TAB>`, `pnpm rm react <TAB>`),
pnpm would give you suggestions for the root command, as if you had
typed `pnpm <TAB>`
close#7964close#5426
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Allow approving all pending build dependencies at once without
interactive selection, useful for CI/CD pipelines and project
bootstrapping scenarios where interactive prompts are not feasible.
close#10136
When lockfile-include-tarball-url is explicitly set to false, tarball URLs
are now always excluded from the lockfile. Previously, packages hosted under
non-standard tarball URLs would still have their tarball field written to the
lockfile even when the setting was false, causing flaky and inconsistent
behavior across environments.
The fix makes the option tri-state internally:
- true: always include tarball URLs
- false: never include tarball URLs
- undefined (not set): use the existing heuristic that includes tarball URLs
only for packages with non-standard registry URLs
close#6667
* feat(sbom): add `pnpm sbom` command (#9088)
new command that generates SBOMs from the lockfile + store metadata.
supports CycloneDX 1.6 JSON and SPDX 2.3 JSON via `--sbom-format`.
two new packages following the existing `pnpm licenses` architecture:
- `@pnpm/sbom` — core library (lockfile walking, store reading, serializers)
- `@pnpm/plugin-commands-sbom` — CLI plugin wiring
uses the lockfile walker for dependency traversal and reads package.json
from the CAFS store for license/author/description metadata. `--lockfile-only`
skips the store entirely for faster CI runs where metadata isn't needed.
validated against official CycloneDX 1.6 and SPDX 2.3 JSON schemas.
* chore: add sbom-related words to cspell dictionary
* fix(sbom): address CycloneDX review feedback and bump to 1.7
Implements all 5 items from the CycloneDX maintainer review:
split scoped names into group/name, move hashes to
externalReferences distribution, use license.id for known SPDX
identifiers, switch to modern tools.components structure with
pnpm version, and bump specVersion to 1.7.
Also adds spdx-license-ids for proper license classification and
improves SPDX serializer test coverage.
* fix(sbom): fix CI bundle failure for spdx-license-ids
createRequire doesn't work in the esbuild bundle since it's a runtime
resolve, switched back to regular import which esbuild can inline.
* fix(sbom): use tarball URL for distribution externalReferences
Use actual tarball download URL instead of PURL for CycloneDX
distribution externalReferences, per review feedback.
* feat(sbom): add CycloneDX metadata and improve SBOM quality scores
adds $schema, timestamp, lifecycles (build/pre-build) to CycloneDX output
to match what npm does. also enriches both CycloneDX and SPDX with
metadata.authors, metadata.supplier, component supplier from author,
vcs externalReferences from repository, and root component details
(purl, license, description, author, vcs). SPDX now uses tarball URL
for downloadLocation instead of NOASSERTION.
renames CycloneDxToolInfo to CycloneDxOptions, passes lockfileOnly
through to the serializer for lifecycle phase selection. adds store-dir
to accepted CLI options.
* fix(sbom): address CycloneDX review feedback round 2
switches license classification from spdx-license-ids to
@cyclonedx/cyclonedx-library (SPDX.isSupportedSpdxId) for accurate
CycloneDX license ID validation per jkowalleck's feedback.
removes hardcoded metadata.authors and metadata.supplier — these are
not appropriate for a tool to set. adds --sbom-authors and
--sbom-supplier CLI flags so the SBOM consumer (e.g. ACME Corp) can
declare who they are.
removes supplier from components — supplier is the registry/distributor,
not the package author. also fixes distribution externalReference to
only emit when a real tarball URL exists, no PURL fallback.
* fix(sbom): use sub-path import for CycloneDX library to fix bundle
top-level import from @cyclonedx/cyclonedx-library drags in
validation/serialize layers with optional deps (ajv-formats, libxmljs2,
xmlbuilder2) that esbuild can't resolve during pnpm CLI bundling.
switch to @cyclonedx/cyclonedx-library/SPDX which only pulls in the
SPDX module we actually use — pure JS, no optional deps.
* chore: update manifests
* refactor: extract shared store-reading logic into @pnpm/store.pkg-finder
Both @pnpm/license-scanner and @pnpm/sbom independently implemented
nearly identical logic to read a package's file index from the
content-addressable store. This extracts that into a new shared package
that returns a uniform Map<string, string> (filename → absolute path),
simplifying both consumers.
Close#9088
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
On Windows, node.exe is hardlinked into .bin/ so process.execPath reports
the hardlink path rather than the original store location. The version
check already validates the correct node runtime is being used.
fs.realpathSync uses a JS-only implementation that only resolves symlinks,
not Windows 8.3 short names (e.g., RUNNER~1). Switch to fs.promises.realpath
which uses the native uv_fs_realpath (GetFinalPathNameByHandleW on Windows)
to properly resolve 8.3 short paths to their long form.
On Windows, temporaryDirectory() may return 8.3 short paths (e.g.,
RUNNER~1) but getBinNodePaths resolves via fs.realpath, returning long
paths (e.g., runneradmin). Use realpathSync to normalize expected paths.
Third-party cmd shims (e.g., npm's rimraf.cmd) call node.exe from
within IF/ELSE blocks in batch files. When node resolves to node.cmd
instead of node.exe, Windows batch file chaining breaks with
"The system cannot find the path specified."
On Windows, hardlink node.exe directly into the bin directory.
On non-Windows, symlink the node binary directly.
* fix(config): respect lockfile: false setting from pnpm-workspace.yaml
* fix(config): derive lockfile settings after all config sources are applied
* fix(config): use lockfile instead of useLockfile in integration tests
Fixed "input line too long" error on Windows when running lifecycle scripts with the global virtual store enabled. The `NODE_PATH` in command shims no longer includes all paths from `Module._nodeModulePaths()`. Instead, it includes only the package's bundled dependencies directory (e.g., `.pnpm/pkg@version/node_modules/pkg/node_modules`), the package's sibling dependencies directory (e.g., `.pnpm/pkg@version/node_modules`), and the hoisted `node_modules` directory. These paths are needed so that tools like `import-local` (used by jest, eslint, etc.) which resolve from CWD can find the correct dependency versions.
process.cwd() is not guaranteed to be the package directory when the
package manager runs lifecycle scripts. import.meta.dirname always
resolves to the directory containing setup.js itself.
The SEA binary recorded the CI build-time absolute path for pnpm.cjs,
causing `import('./dist/pnpm.mjs')` to resolve to a non-existent path
on the user's machine. Use process.execPath to resolve at runtime instead.
* fix: respect peer dep range in hoistPeers when preferred versions exist
Previously, hoistPeers used semver.maxSatisfying(versions, '*') which
picked the highest preferred version from the lockfile regardless of the
peer dep range. This caused overrides that narrow a peer dep range to be
ignored when a stale version existed in the lockfile.
Now hoistPeers first tries semver.maxSatisfying(versions, range) to find
a preferred version that satisfies the actual peer dep range. If none
satisfies it and autoInstallPeers is enabled, it falls back to the range
itself so pnpm resolves a matching version from the registry.
* fix: only fall back to exact-version range for overrides, handle workspace: protocol
- When no preferred version satisfies the peer dep range, only use the
range directly if it is an exact version (e.g. "4.3.0" from an override).
For semver ranges (e.g. "1", "^2.0.0"), fall back to the old behavior
of picking the highest preferred version for deduplication.
- Guard against workspace: protocol ranges that would cause
semver.maxSatisfying to throw.
- Add unit tests for hoisting deduplication and workspace: ranges.
* fix: only apply range-constrained peer selection for exact versions
The previous approach used semver.maxSatisfying(versions, range) for all
peer dep ranges, which broke aliased-dependency deduplication — e.g. when
three aliases of @pnpm.e2e/peer-c existed at 1.0.0, 1.0.1, and 2.0.0,
range ^1.0.0 would pick 1.0.1 instead of 2.0.0.
Now the range-aware logic only activates when the range is an exact
version (semver.valid), which is the override case (e.g. "4.3.0").
Regular semver ranges fall back to picking the highest preferred version.
* refactor(env): unify node version specifier parsing into parseNodeSpecifier in node.resolver
Move parseNodeSpecifier from @pnpm/plugin-commands-env to @pnpm/node.resolver and
replace the simpler parseEnvSpecifier with an enhanced version that supports all
Node.js version specifier formats: standalone release channels (nightly, rc, test,
v8-canary, release), well-known aliases (lts, latest), LTS codenames (argon, iron),
semver ranges (18, ^18), and channel/version combos (rc/18, nightly/latest).
* fix(env): address parseNodeSpecifier review feedback
- Remove overly strict release/X.Y.Z-only validation; release/latest,
release/lts, and release/<range> are now accepted
- Validate unknown release channels (e.g. foo/18) with a clear error
instead of letting them fall through to a confusing network failure
- Add test cases for release/latest, release/lts, and release/18
* feat: switch from pkg to Node.js SEA for creating standalone executables
Replace @yao-pkg/pkg with Node.js native Single Executable Applications
(--build-sea, Node.js 25.5+). The SEA binary embeds only pnpm.cjs (CJS
bootstrap), while pnpm.mjs and all assets live in a dist/ directory
shipped alongside the binary in platform-specific tarballs.
* refactor: move dist/ from platform packages to @pnpm/exe
The dist/ directory (pnpm.mjs, worker.js, templates, etc.) is identical
across all platforms, so ship it once in @pnpm/exe instead of duplicating
it in each platform package. Platform packages now only contain the
binary. The self-updater installs @pnpm/exe (not the platform package)
so it gets both dist/ and the binary via optionalDependencies.
* refactor: externalize @reflink/reflink in esbuild bundle
Make @reflink/reflink external in both the main and worker esbuild
bundles so the require() calls resolve at runtime from dist/node_modules
instead of being inlined. Add @reflink/reflink as a production dependency
of both pnpm (bundled into dist/node_modules by bundle-deps.ts) and
@pnpm/exe (installed by npm alongside the binary).
For GitHub release tarballs, only the target platform's reflink package
is kept. For @pnpm/exe npm publishing, all reflink platform packages
are stripped from dist/ since npm installs the right one automatically.
* chore: update cspell list
* test: update system-node-version tests for SEA detection
Mock @pnpm/cli-meta's detectIfCurrentPkgIsExecutable instead of
setting process.pkg, which is no longer used for SEA detection.
* test: improve cli-meta test coverage for SEA migration
Add tests for detectIfCurrentPkgIsExecutable() (non-SEA path) and
isExecutedByCorepack() which were previously untested. The SEA=true
path of detectIfCurrentPkgIsExecutable() cannot be unit tested since
node:sea is unavailable in an ESM test environment.
* refactor: move GitHub tarball assembly to copy-artifacts.ts
build-artifacts.ts (prepublishOnly of @pnpm/exe) now only builds the
SEA executables and prepares the exe npm dist/. The per-target dist/
assembly for GitHub release tarballs moves to copy-artifacts.ts, which
is the natural owner of that concern.
Other changes:
- Extract getReflinkKeepPackages/stripReflinkPackages to reflink-utils.ts
with tests using node:test
- Move --force from top-level pnpm install in release.yml to the pnpm
deploy in bundle-deps.ts, where it is actually needed to install all
@reflink/reflink-* platform packages into dist/node_modules
- Change @pnpm/exe prepublishOnly to run pnpm's full prepublishOnly
(compile + bundle-deps) so dist/node_modules is populated before
build-artifacts.ts and copy-artifacts.ts read from pnpm/dist
* fix: copy dist/ alongside binary when running pnpm setup for SEA
When the pnpm CLI is a Node.js SEA binary, it requires a dist/ directory
adjacent to the executable at runtime (containing pnpm.mjs and bundled
node_modules). The copyCli function in plugin-commands-setup now copies
dist/ from alongside the current binary into the tools directory so that
the installed pnpm works correctly after `pnpm setup`.
* fix: avoid argument list too long when creating Windows zip archives
* fix: propagate errors in copy-artifacts script
Previously errors in createArtifactTarball were swallowed, causing the
script to exit 0 even when artifact creation failed. Now errors are
re-thrown with a descriptive message, and the top-level IIFE has a
.catch() handler that sets a non-zero exit code.
* refactor: remove reflink-utils.ts from @pnpm/exe
The stripReflinkPackages call in build-artifacts.ts stripped all platform
packages while keeping @reflink/reflink. Instead, just remove the entire
@reflink directory from dist/ — @pnpm/exe already declares @reflink/reflink
as a runtime dependency, so npm installs it (along with the right platform
package via optionalDependencies) automatically.
This eliminates reflink-utils.ts, its tests, and the code duplication with
copy-artifacts.ts.
This PR overhauls `pnpm env` use to route through pnpm's own install machinery instead of maintaining a parallel code path with manual symlink/shim/hardlink logic.
```
pnpm env use -g <version>
```
now runs:
```
pnpm add --global node@runtime:<version>
```
via `@pnpm/exec.pnpm-cli-runner`. All manual symlink, hardlink, and cmd-shim code in `envUse.ts` is gone (~1000 lines removed across the package).
### Changes
**npm and npx shims on all platforms**
Added `getNodeBinsForCurrentOS(platform)` to `@pnpm/constants`, returning a `Record<string, string>` with the correct relative paths for `node`, `npm`, and `npx` inside a Node.js distribution. `BinaryResolution.bin` is widened from `string` to `string | Record<string, string>` in `@pnpm/resolver-base` and `@pnpm/lockfile.types`, so the node resolver can set all three entries and pnpm's bin-linker creates shims for each automatically.
**Windows npm/npx fix**
`addFilesFromDir` was skipping root-level `node_modules/` (to avoid storing a package's own dependencies), which stripped the bundled `npm` from Node.js Windows zip archives. Added an `includeNodeModules` option and enabled it from the binary fetcher so Windows distributions keep their full contents.
**Removed subcommands**
`pnpm env add` and `pnpm env remove` are removed. `pnpm env use` handles both installing and activating a version. `pnpm env list` now always lists remote versions (the `--remote` flag is no longer required, though it is kept for backwards compatibility).
**musl support**
On Alpine Linux and other musl-based systems, the musl variant of Node.js is automatically downloaded from [unofficial-builds.nodejs.org](https://unofficial-builds.nodejs.org).
The lockfile now includes musl Linux builds (sourced from
unofficial-builds.nodejs.org) alongside the standard glibc variants,
so that `node@runtime:` works out of the box on Alpine Linux and other
musl-based distributions.
`env use` can download node.js artifacts for systems that use musl.
* test: fix flaky tests & add retries for failed tests during CI testing
* fix: import jest in setupFilesAfterEnv & reduce retries to 2
* test: remove global retries
Replaces separate conditional steps with a unified "Determine test scope"
step that selects the test script and a descriptive label. The step name
now shows the scope (all, all — pnpm-workspace.yaml modified, or
affected packages) in the GitHub Actions UI.