* feat: add `pnpm runtime set` command and deprecate `pnpm env use`
Add a new `pnpm runtime set <name> <version> [-g]` command (alias: `rt`)
in a new `@pnpm/runtime.commands` package. It delegates to
`pnpm add <name>@runtime:<version>` supporting node, deno, and bun.
`pnpm env use` now prints a deprecation warning pointing users to the
new command.
* fix: address PR review feedback for runtime command
- Use opts.dir as cwd when --global is not passed (instead of always
using pnpmHomeDir)
- Only pass --global-bin-dir when in global mode
- Keep deprecated env command in help output for discoverability
- Add Jest tests for the runtime command (7 tests)
* feat: use global virtual store for global packages and dlx
* fix(config): remove unnecessary virtualStoreDir override for global installs
When the global virtual store is disabled, the default `node_modules/.pnpm`
path works fine — no need to explicitly override it to `.pnpm`.
* fix: prefer .pnpmfile.mjs by default
* fix: handle ERR_MODULE_NOT_FOUND for missing optional .pnpmfile.mjs
Node's dynamic import() throws ERR_MODULE_NOT_FOUND (not MODULE_NOT_FOUND
like require()) when a file doesn't exist. This caused a hard error when
tryLoadDefaultPnpmfile was enabled and .pnpmfile.mjs was absent.
* fix: load only .pnpmfile.mjs when it exists, not both .mjs and .cjs
When both .pnpmfile.mjs and .pnpmfile.cjs exist, only the .mjs file
is now loaded. Previously both were loaded and their hooks combined.
Also adds .mjs support for config dependency plugins.
**TLDR:** Global packages in pnpm v10 are annoying and slow because they all are installed to a single global package. Instead, we will now use a system that is similar to the one used by "pnpm dlx" (aka "pnpx").
Each globally installed package (or group of packages installed together) now gets its own isolated installation directory with its own `package.json`, `node_modules`, and lockfile. This prevents global packages from interfering with each other through peer dependency conflicts or version resolution shifts.
## Changes
- Add `@pnpm/global-packages` shared utilities package for scanning, hashing, and managing isolated global installs
- `pnpm add -g` creates isolated installs in `{pnpmHomeDir}/global/v11/{hash}/`
- `pnpm remove -g` removes the entire installation group containing the package
- `pnpm update -g` re-installs into new isolated directories and swaps symlinks
- `pnpm list -g` scans isolated directories to show installed global packages
- `pnpm outdated -g` checks each isolated installation for outdated dependencies
- `pnpm store prune` cleans up orphaned global installation directories
## Breaking changes
- `pnpm install -g` (no args) is no longer supported — use `pnpm add -g <pkg>`
- `pnpm link <pkg-name>` no longer resolves packages from the global store — only relative or absolute paths are accepted
- `pnpm link --global` is removed — use `pnpm add -g .` to register a local package's bins globally
* feat: add `pnpm clean` command for safe node_modules removal
Adds a new `pnpm clean` command that safely removes node_modules contents
from all workspace projects. Uses Node.js fs.rm() which correctly handles
NTFS junctions on Windows without following them into their targets,
preventing catastrophic data loss. Preserves non-pnpm hidden files
(e.g. .cache) and lockfiles by default; use --lockfile/-l to also
remove pnpm-lock.yaml files. Also cleans custom virtual-store-dir
when configured inside the project root.
Closes#10707
* fix: use is-subdir package instead of custom implementation, check existence before printing
- Replace custom isSubdir function with the existing is-subdir package
- Only print "Removing" and remove lockfile/virtualStoreDir when they exist
- Rethrow non-ENOENT errors instead of swallowing them
* refactor: use path-exists package, handle TOCTOU race in removeModulesDirContents
- Replace manual fs.access + try/catch with pathExists for cleaner existence checks
- Add ENOENT handling to removeModulesDirContents readdir to handle the race
where the directory is removed between hasContentsToRemove and readdir
* refactor: use opts object with .bind for cleanProjectDir, rename to removeLockfile
On Windows, os.devNull is '\\.\nul', which git cannot open as a config
file path (fatal: unable to access '\\.\nul': Invalid argument).
Git for Windows translates the literal '/dev/null' correctly via its
MSYS2 layer, fixing patch-commit on Windows.
* fix(dlx): print help message on calling pnpm dlx without arguments
Running `pnpm dlx` with no arguments would crash Node.js with a
TypeError as it attempted to call `.indexOf()` on an undefined variable.
This commit adds a guard clause and displays the help message instead
and exits gracefully.
Fixes#10633
* refactor: dlx
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* fix(npm-resolver): respect version constraints when falling back to workspace packages
When link-workspace-packages=true, the fallback resolution paths (registry 404
and no matching registry version) pass update: Boolean(opts.update) to
tryResolveFromWorkspacePackages. On fresh installs without a lockfile entry,
opts.update is 'compatible' (truthy), which overrides the version spec to '*'
and matches any workspace package regardless of version.
Change both fallback call sites to pass update: false so version constraints
are always respected for non-workspace-protocol dependencies. The workspace:
protocol path returns before these blocks and correctly continues to use
opts.update.
Close#10173
* test: clarify npm-resolver test names for workspace version mismatch scenarios
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
When a package has an injected self-referencing dependency (e.g.
"pkg": "file:" with dependenciesMeta injected: true), the lockfile
resolves it as "link:". The linkedPackagesAreUpToDate() function
incorrectly reported these projects as not up-to-date because
refToRelative() returns null for link: refs, causing pnpm to skip
the fast headless install path and do full resolution instead.
* 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