**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
* 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>
* 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>
Allow consumers (e.g. Bit CLI) to provide a nameFormatter callback that
reads the package manifest and returns a custom display name. The resolved
displayName is carried through the DependentsTree/DependentNode data model
and used by all render functions (tree, JSON, parseable).
- **`pnpm why` now shows a reverse dependency tree.** The searched package appears at the root with its dependants as branches, walking back to workspace roots. This replaces the previous forward-tree output which was noisy and hard to read for deeply nested dependencies.
- **Replaced `archy` with a new `@pnpm/text.tree-renderer` package** that renders trees using box-drawing characters (├──, └──, │) and supports grouped sections, dim connectors, and deduplication markers.
- **Show peer dependency hash suffixes** in `pnpm list` and `pnpm why` output to distinguish between different peer-dep variants of the same package.
- **Improved `pnpm list` visual output:** bold importer nodes, dimmed workspace paths, dependency grouping, package count summary, and deterministic sort order.
- **Added `--long` support to `pnpm why`** and the ability to read package manifests from the CAS store.
- **Deduplicated shared code** between `list` and `why` commands into a common module, and reused `getPkgInfo` in the why tree builder.
Instead of manually iterating over top-level dependencies, calling
getPkgInfo/getTreeNodeChildId/getTree per dependency, and handling
dedup/search logic in parallel with materializeChildren, delegate
entirely to a single getTree call with the importer as root.
The returned PackageNode[] are then post-categorized into their
dependency fields (dependencies, devDependencies, optionalDependencies)
using a fieldMap built from the lockfile importer snapshot.
This eliminates the duplicated dedup/search handling between
dependenciesHierarchyForPackage and materializeChildren, and removes
the GetTreeResult wrapper type from getTree (now returns PackageNode[]
directly). The materializeChildren cache is now the sole mechanism for
cross-importer deduplication.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* test: use `import type` in more places
Several tests are failing because a module isn't being mocked. This is
due to the mocked module being imported before the mock being set up.
Switching to `import type` should elide the import fully.
* build: replace ts-jest with simple transformer
* chore: remove `ts-jest`
* chore: remove babel dependencies from root project
* ci: use Node.js 22.13.0 (instead of 22.12.0)
Node.js 22.13.0 introduces the `stripTypeScriptTypes` function
* fix: copilot feedback
* fix(list): correct dependency paths with global virtual store
* fix: global virtual store path
* fix: test
* fix: symlink resolution should only apply to top-level dependencies
* fix: resolve subdependency paths with global virtual store
* fix(list): resolve correct paths with global virtual store
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* chore: configure `erasableSyntaxOnly`
* refactor: remove class property access modifiers in constructor
```
../dedupe/check/src/DedupeCheckIssuesError.ts:5:16 - error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
5 constructor (public dedupeCheckIssues: DedupeCheckIssues) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../reviewing/dependencies-hierarchy/src/getTree.ts:243:24 - error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
243 private constructor (private readonly keypath: readonly string[]) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```
* chore: migrate from enum types
```
../pkg-manager/plugin-commands-installation/src/import/index.ts:66:6 - error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
66 enum YarnLockType {
~~~~~~~~~~~~
../lockfile/detect-dep-types/src/index.ts:5:13 - error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
5 export enum DepType {
~~~~~~~
```
* chore: add changelog
* chore: remove `ts-node` dev dependency (#10371)
* chore: remove `ts-node` dev dependency
* chore: remove `ts-node` usages from `package.json` scripts
* fix: support alias resolution in pnpm why with npm:
protocol
* refactor: make alias required instead of optional
* refactor: reorder field to put alias first