This is consistent with #9358, but implements support for the GitHub Packages npm registry and, more broadly, for vlt-style https://docs.vlt.sh/cli/registries for any registry.
This PR adds a built-in gh: specifier that resolves against the GitHub Packages npm registry, plus a namedRegistries config key so a project can map its own aliases to arbitrary registries. A project can mix public npm packages and private GitHub Packages (or self-hosted) ones without applying a scope-wide registry override to every @scope/* package.
- pnpm add gh:@acme/private writes "@acme/private": "gh:^1.0.0" and resolves from https://npm.pkg.github.com/.
- pnpm add gh:@acme/private@^1.0.0 (with or without an alias) is also supported. Aliased form writes "my-alias": "gh:@acme/private@^1.0.0".
- Auth comes from the existing per-URL .npmrc mechanism, e.g. //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}. No new auth surface.
- @github is intentionally not defaulted to https://npm.pkg.github.com/ - hardcoding that would hijack installs of the public @github/* packages on npmjs.org (e.g. @github/relative-time-element) for users without a scope-wide override. Use gh: to install from GitHub Packages, or configure @github:registry=... yourself if that's really what you want.
- Additional named registries (a self-hosted proxy, GitHub Enterprise Server, etc.) can be configured in pnpm-workspace.yaml:
```yml
namedRegistries:
gh: https://npm.pkg.github.example.com/ # optional: overrides the built-in `gh` alias for GHES
work: https://npm.work.example.com/
```
- Then work:@corp/lib@^2.0.0 resolves against https://npm.work.example.com/, and the built-in gh alias can be redirected to a GHES host.
- Env-var substitution (${VAR}) is supported in namedRegistries values, mirroring the .npmrc convention.
- Reserved alias names (npm, jsr, github, workspace, catalog, file, git, http, https, link, patch, and related git host shorthands) cannot be redefined as user-named registries - the resolver throws ERR_PNPM_RESERVED_NAMED_REGISTRY_ALIAS at startup rather than silently shadowing another protocol. Malformed URLs throw ERR_PNPM_INVALID_NAMED_REGISTRY_URL at startup too, instead of failing as a confusing 404 during resolution.
- On publish, createExportableManifest strips any named-registry prefix (both the built-in gh: and any user-configured alias) so npm and yarn consumers can still resolve the dependency via their own scope-registry configuration - mirroring the user-facing requirement when installing such a dep without the prefix.
The prefix is gh: rather than github: because github: is reserved by npm-package-arg / hosted-git-info as a git host shorthand (e.g. github:owner/repo) - reusing it would be a deviation from the specs used by the npm CLI. gh: is shorter, matches vlt's convention, and cannot collide with any existing npm scheme.
Unlike jsr:, gh: (and any other named-registry alias) does not rewrite the package name - gh:@acme/foo resolves @acme/foo from the GitHub Packages registry as-is. This also means npm/yarn consumers see the original name after the prefix is stripped on publish.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
## Summary
Adds an opt-in **pnpm agent** server that resolves dependencies server-side and streams only the files missing from the client's content-addressable store.
- **`@pnpm/agent.server`** — multi-process HTTP server (Node.js `cluster`) with SQLite-backed metadata and file caches
- **`@pnpm/agent.client`** — streams an NDJSON response, dispatches worker threads to fetch files while the server is still resolving
- **New config**: `agent` in `pnpm-workspace.yaml` (opt-in)
## How it works
1. Client reads integrity hashes from its local store index
2. Sends `POST /v1/install` with dependencies + store integrities
3. Server resolves the dependency tree using pnpm's `install({ lockfileOnly: true })`, with a SQLite-backed `PackageMetaCache` for fast repeat resolution
4. As each package resolves, a wrapped `storeController.requestPackage` looks up its files and immediately streams digests the client is missing (NDJSON `D` lines)
5. Client reads the stream line by line; digest batches fill up and dispatch worker threads to `POST /v1/files` — file downloads overlap with server-side resolution
6. After resolution, server sends index entries (`I` lines) and lockfile (`L` line)
7. Client writes index entries to store, then runs headless install with a wrapped `fetchPackage` that calls `readPkgFromCafs` with `verifyStoreIntegrity: false` (files are trusted from the agent)
8. `/v1/files` response is gzip-streamed (274MB → ~80MB) — server pipes through `createGzip`, worker pipes through `createGunzip`, parsing and writing files to CAFS as data arrives
## Performance
1351-package project, cold local store, warm server (localhost):
| Scenario | Time |
|----------|------|
| Vanilla pnpm install (cold OS cache) | ~48s |
| Vanilla pnpm install (warm OS cache) | ~34s |
| With pnpm agent (consistent) | **~33s** |
### Key optimizations
1. **SQLite metadata cache** — server-side resolution drops from ~3.4s to ~0.9s
2. **SQLite file store** — consistent read performance regardless of OS file cache state
3. **Streaming `/v1/install`** — file digests stream during resolution, downloads start before resolution finishes
4. **Gzip-streamed `/v1/files`** — whole-stream gzip (274MB → ~80MB), significant savings on remote servers
5. **Worker-thread streaming HTTP** — workers pipe gzip → parse → write to CAFS as data arrives, no buffering
6. **No rehashing** — server-provided digests used directly, skipping 33K SHA-512 computations
7. **No re-verification** — wrapped `fetchPackage` calls `readPkgFromCafs` with `verifyStoreIntegrity: false`
8. **Direct `writeFileSync` with `wx`** — no stat + temp + rename
9. **Pre-packed msgpack** — server sends raw store index buffers, client writes directly to SQLite
10. **WAL checkpoint** — ensures store index entries written by agent are visible to headless install's worker threads
## Usage
Start the server:
```bash
node agent/server/lib/bin.js
```
Configure in `pnpm-workspace.yaml`:
```yaml
agent: http://localhost:4873
```
Skips the minimumReleaseAge maturity check when the registry metadata
lacks the "time" field, instead of throwing ERR_PNPM_MISSING_TIME.
Defaults to true, and prints a warning once per affected package.
* feat: skip lockfile writes for legacy packageManager field
When pnpm is pinned via the `packageManager` field in `package.json`, the
resolved pnpm integrity info is no longer written to `pnpm-lock.yaml`
unless the pinned version is pnpm v12 or newer. `devEngines.packageManager`
still populates and reuses `packageManagerDependencies` as before. This
keeps the v10 -> v11 transition quiet by avoiding unrelated lockfile
churn for projects that pin pnpm the legacy way.
* fix: address Copilot review and CI failure
- Update `configurationalDependencies.test.ts` to assert the new behavior:
the `packageManager` field no longer writes pnpm resolution info to the
env lockfile while config dependencies still are.
- Fast-path in `switchCliVersion`: when the lockfile is not persisted and
the running CLI already matches `pm.version`, skip store access and
integrity resolution entirely.
- Clarify the `resolvePackageManagerIntegrities` docstring to describe
the conditional `save` behavior.
* test: add unit tests for shouldPersistLockfile
Extract the decision logic for persisting pnpm resolution info to the env
lockfile into a dedicated helper so the branches — devEngines source,
legacy `packageManager` field with v11 or older, v12+, and invalid/missing
version — can all be covered without needing an actual pnpm v12 tarball
on the registry.
* feat: add runtimeOnFail setting
Adds a `runtimeOnFail` config setting ('ignore' | 'warn' | 'error' |
'download') that overrides the `onFail` field on `devEngines.runtime`
and `engines.runtime` in the root project's package.json. This makes
it possible to opt into (or out of) runtime auto-download without
changing the project manifest.
* fix: skip runtime download when version is missing
Without a version, convertEnginesRuntimeToDependencies would write
`runtime:undefined` into the manifest. Warn and skip instead.
* feat: apply runtimeOnFail override during install
The config reader override only mutates the context's rootProjectManifest,
but installDeps reads the manifest fresh via tryReadProjectManifest and
findWorkspaceProjects. Apply the override there too so `runtimeOnFail`
actually affects what gets installed. Adds an e2e test covering both
download and ignore overrides through the real CLI bundle.
* feat!: remove managePackageManagerVersions / packageManagerStrict / packageManagerStrictVersion
These three settings existed only to derive the `onFail` behavior for
the legacy `packageManager` field. The `pmOnFail` setting introduced
in #11275 subsumes all three — it directly sets `onFail` for both
`packageManager` and `devEngines.packageManager`.
Legacy `packageManager` now defaults to `onFail: 'download'` when no
override is set. `COREPACK_ENABLE_STRICT` is no longer read (it only
gated `packageManagerStrict`); `pmOnFail` is the replacement.
Also drops pass-through `packageManagerStrict*` option fields from
cli.utils / workspace.projects-reader (they were unused) and the
unused `managePackageManagerVersions` Pick in engine.pm.commands'
`SelfUpdateCommandOptions`.
* fix: use kebab-case setting name in BAD_PM_VERSION hint
Copilot review feedback: user-facing error hints for configuration keys
conventionally use the kebab-case form that matches both the CLI flag
(`--pm-on-fail`) and the `.npmrc` key, consistent with the prior hint
text that referenced `package-manager-strict`. The `pnpm-workspace.yaml`
field (`pmOnFail`) is camelCase but that mapping is documented
elsewhere.
* Revert "fix: use kebab-case setting name in BAD_PM_VERSION hint"
This reverts commit e03c29b17. pnpm-workspace.yaml uses camelCase
(`pmOnFail`) — the primary config location for pnpm 11 — so the
hint keeps the camelCase form. The CLI flag is already shown
alongside.
## Summary
- **New command `pnpm with <version|current> <args...>`** — runs pnpm at a specific version (or the currently active one) for a single invocation, bypassing the project's `packageManager` and `devEngines.packageManager` pins. Uses the same install mechanism as `pnpm self-update`, caching the downloaded pnpm in the global virtual store for reuse.
- **New config setting `pmOnFail`** — overrides the `onFail` behavior of both `packageManager` and `devEngines.packageManager`. Accepted values: `download`, `error`, `warn`, `ignore`. Readable from CLI flag, env var, `pnpm-workspace.yaml`, or `.npmrc` — useful when version management is handled by an external tool (asdf, mise, Volta, etc.) and the project wants pnpm itself to skip the check.
```
pnpm with current install # one-shot, use running pnpm
pnpm with 11.0.0-rc.1 install # one-shot, use specific version
pnpm install --pm-on-fail=ignore # direct CLI flag
pnpm install --config.pm-on-fail=ignore # equivalent via --config.* sugar
pnpm_config_pm_on_fail=ignore pnpm install # env var
# or in pnpm-workspace.yaml: pmOnFail: ignore
```
## Implementation notes
- Command handler lives in `@pnpm/engine.pm.commands` (next to `self-update` and `setup`).
- `'with'` added to `SPECIALLY_ESCAPED_CMDS` in `cli/parse-cli-args` so args after `<spec>` pass through opaquely like `dlx`/`run`.
- `pnpm with current <cmd> [args]` is rewritten in `pnpm/src/parseCliArgs.ts` to an in-process dispatch — argv is rebuilt in place so any global flags the user put before `with` (e.g. `--dir`, `--filter`) are preserved. `process.env.pnpm_config_pm_on_fail=ignore` is set so the override survives `parseCliArgsLib`'s `-v` / `--help` short-circuits (which discard other parsed options).
- `main.ts` treats `skipPackageManagerCheck: true` as bypassing both the auto-download and the warn/error check (previously only the check). Also skips when `cmd='help'` and the help target is itself a skip-check command, so `pnpm with -h` works in pinned projects without downloading the pinned version first.
- Errors reported to stderr for `with` (aligned with `dlx`/`create`/`sbom`).
- `pmOnFail` wired in `config/reader/src/index.ts`: added to `types`, `Config`, and `pnpmConfigFileKeys`; applied as an override in the `onFail` resolution block.
- The `with <version>` child process sets both `COREPACK_ROOT` (honored by every pnpm release via `isExecutedByCorepack()`) and `pnpm_config_pm_on_fail=ignore` (principled override on new releases that ship the setting). This gives graceful behavior when `pnpm with 9.3.0 install` spawns an older pnpm that predates the new setting.
- Store controller lifecycle in the handler wrapped in `try/finally` to prevent leaks on install errors. Signal-induced child exits return a non-zero exit code so interrupted runs aren't masked as success.
The legacy `/-/npm/v1/security/audits{,/quick}` endpoints have been retired by npmjs.org. This PR rewires the audit client to the replacement `/-/npm/v1/security/advisories/bulk` endpoint.
The new endpoint is not a drop-in rename — the request and response contracts are both different:
- **Request**: a flat `{ pkgName: [versions] }` map. `lockfileToAuditRequest` walks the lockfile once and builds the POST body directly; there is no more nested `AuditTree`.
- **Response**: only `id`, `url`, `title`, `severity`, `vulnerable_versions`, and `cwe` per advisory. Everything else the old endpoint returned is computed locally:
- `findings[].paths` are walked from the lockfile (skipped entirely when the response is empty; the second walk intentionally avoids `@pnpm/lockfile.walker`'s global dedup so alternate install chains to the same shared dep aren't dropped).
- `metadata.vulnerabilities` counts advisories per severity.
- `metadata.dependencies` / `devDependencies` / `optionalDependencies` / `totalDependencies` come from a classified lockfile walk; the classifier respects `--prod`/`--dev` include flags when deciding whether a subgraph is reachable non-optionally.
- `patched_versions` is inferred from the vulnerable range for common `<X.Y.Z` / `<=X.Y.Z` shapes so `audit --fix` can still produce usable overrides; left `undefined` when inference fails.
- `github_advisory_id` is parsed from the advisory URL and canonicalized to the github.com form (uppercase `GHSA-` prefix, lowercase suffix).
- `info` severity is now supported end-to-end (severity type, `--audit-level`, filters, colors).
## Breaking changes (v11)
- Private registries that do not implement `/advisories/bulk` now fail with `AuditEndpointNotExistsError`.
- CVE-based filtering is replaced with GHSA-based filtering, since the bulk endpoint does not return CVE identifiers:
- `auditConfig.ignoreCves` → `auditConfig.ignoreGhsas` (the old key is no longer recognized).
- `pnpm audit --ignore <id>` and `--ignore-unfixable` now read and write GHSAs.
- Migration: replace each `CVE-YYYY-NNNNN` in `auditConfig.ignoreCves` with the matching `GHSA-xxxx-xxxx-xxxx` (visible in the `More info` column of `pnpm audit` output) under `auditConfig.ignoreGhsas`.
- `--ignore-unfixable` now only targets advisories whose patched range couldn't be inferred — the only "no fix available" signal the bulk endpoint provides.
- `AuditReport` and `AuditAdvisory` are trimmed to just the fields the audit client actually populates:
- `AuditReport`: `advisories` + `metadata` only (`actions` and `muted` removed).
- `AuditAdvisory`: `findings`, `id`, `title`, `module_name`, `vulnerable_versions`, `patched_versions?`, `severity`, `cwe`, `github_advisory_id`, `url`. Dropped: `cves`, `created`, `updated`, `deleted`, `access`, `overview`, `recommendation`, `references`, `found_by`, `reported_by`, `metadata`.
- `AuditAction`, `AuditResolution`, `AuditActionRecommendation` removed (no consumers).
## Hardening
- Response body validated: non-object / malformed JSON / non-array package buckets all surface as `ERR_PNPM_AUDIT_BAD_RESPONSE` with a body excerpt. Advisory `id` must be a finite number and `severity` must be a known value before being indexed.
- Name-keyed records use `Object.create(null)` so a hostile/unusual package name can't trigger prototype pollution.
- GHSA ids canonicalized on both read and write so casing drift between config and registry doesn't mask ignores.
- `findings[].paths` are deduped and capped per (name, version) to keep pathologically shared graphs from blowing up memory.
## Internals
- `AuditTree` / `AuditNode` / `lockfileToAuditTree` removed. `lockfileToAuditIndex.ts` exports `lockfileToAuditRequest` (flat POST body + counts) and `buildAuditPathIndex` (only invoked when the response has advisories).
- `AuditAdvisory.findings` is now `AuditFinding[]` (was an unintended 1-tuple).
- Top-level test fixtures regenerated from real `registry.npmjs.org` responses; synthetic `update-*` fixtures converted in place to bulk shape.
---------
Co-authored-by: John van Leeuwen <john.van.leeuwen@priva.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
- Adds a new `minimumReleaseAgeStrict` setting (default: `false`)
- When `false` (default), pnpm falls back to versions that don't meet the `minimumReleaseAge` constraint if no mature versions satisfy the range being resolved
- Set to `true` to preserve the previous strict behavior (error when no mature version matches)
* refactor: remove ignoreDepScripts and neverBuiltDependencies settings
These settings are redundant in v11:
- `ignore-dep-scripts` is superseded by the default behavior of `allowBuilds`
- `neverBuiltDependencies` was already dead code, replaced by `allowBuilds`
* chore: add changeset for removed ignore-dep-scripts setting
* refactor(config): split Config interface into settings + runtime context
Create ConfigContext for runtime state (hooks, finders, workspace graph,
CLI metadata) and keep Config for user-facing settings only. Functions
use Pick<Config, ...> & Pick<ConfigContext, ...> to express which fields
they need from each interface.
getConfig() now returns { config, context, warnings }. The CLI wrapper
returns { config, context } and spreads both when calling command
handlers (to be refactored to separate params in follow-up PRs).
Closes#11195
* fix: address review feedback
- Initialize cliOptions on pnpmConfig so context.cliOptions is never undefined
- Move rootProjectManifestDir assignment before ignoreLocalSettings guard
- Add allProjectsGraph to INTERNAL_CONFIG_KEYS
* refactor: remove INTERNAL_CONFIG_KEYS from configToRecord
configToRecord now accepts Config and ConfigContext separately, so
context fields are never in scope. Only auth-related Config fields
(authConfig, authInfos, sslConfigs) need filtering.
* refactor: eliminate INTERNAL_CONFIG_KEYS from configToRecord
configToRecord now receives the clean Config object and explicitlySetKeys
separately (via opts.config and opts.context), so context fields are
never in scope. main.ts passes the original split objects alongside
the spread for command handlers that need them.
* fix: spelling
* fix: import sorting
* fix: --config.xxx nconf overrides conflicting with --config CLI flag
When `pnpm add` registers `config: Boolean`, nopt captures
--config.xxx=yyy as the --config flag value instead of treating it
as a nconf-style config override. Fix by extracting --config.xxx args
before nopt parsing and re-parsing them separately.
Also rename the split config/context properties on the command opts
object to _config/_context to avoid clashing with the --config CLI option.
Major cleanup of the config system after migrating settings from `.npmrc` to `pnpm-workspace.yaml`.
### Config reader simplification
- Remove `checkUnknownSetting` (dead code, always `false`)
- Trim `npmConfigTypes` from ~127 to ~67 keys (remove unused npm config keys)
- Replace `rcOptions` iteration over all type keys with direct construction from defaults + auth overlay
- Remove `rcOptionsTypes` parameter from `getConfig()` and its assembly chain
### Rename `rawConfig` to `authConfig`
- `rawConfig` was a confusing mix of auth data and general settings
- Non-auth settings are already on the typed `Config` object — stop duplicating them in `rawConfig`
- Rename `rawConfig` → `authConfig` across the codebase to clarify it only contains auth/registry data from `.npmrc`
### Remove `rawConfig` from non-auth consumers
- **Lifecycle hooks**: replace `rawConfig: object` with `userAgent?: string` — only user-agent was read
- **Fetchers**: remove unused `rawConfig` from git fetcher, binary fetcher, tarball fetcher, prepare-package
- **Update command**: use `opts.production/dev/optional` instead of `rawConfig.*`
- **`pnpm init`**: accept typed init properties instead of parsing `rawConfig`
### Add `nodeDownloadMirrors` setting
- New `nodeDownloadMirrors?: Record<string, string>` on `PnpmSettings` and `Config`
- Replaces the `node-mirror:<channel>` pattern that was stored in `rawConfig`
- Configured in `pnpm-workspace.yaml`:
```yaml
nodeDownloadMirrors:
release: https://my-mirror.example.com/download/release/
```
- Remove unused `rawConfig` from deno-resolver and bun-resolver
### Refactor `pnpm config get/list`
- New `configToRecord()` builds display data from typed Config properties on the fly
- Excludes sensitive internals (`authInfos`, `sslConfigs`, etc.)
- Non-types keys (e.g., `package-extensions`) resolve through `configToRecord` instead of direct property access
- Delete `processConfig.ts` (replaced by `configToRecord.ts`)
### Pre-push hook improvement
- Add `compile-only` (`tsgo --build`) to pre-push hook to catch type errors before push
Replace the unmaintained @pnpm/npm-conf package with a purpose-built
module that reads only auth/registry-related settings from .npmrc files
using read-ini-file + @pnpm/config.env-replace (both already deps).
All non-registry settings (hoist-pattern, node-linker, etc.) are now
only read from pnpm-workspace.yaml, CLI options, or environment
variables. Registry-related settings (auth tokens, registry URLs,
SSL certs, proxy settings) continue to be read from .npmrc for
migration compatibility, and can also be set in pnpm-workspace.yaml.
New modules:
- loadNpmrcFiles.ts: reads .npmrc from standard locations, filters to
auth/registry keys, returns structured layers
- npmConfigTypes.ts: inlined npm config type definitions
- npmDefaults.ts: inlined npm defaults (registry, unsafe-perm, etc.)
* feat: load default trusted deps list from @pnpm/plugin-trusted-deps
Add a new `use-default-trusted-deps` setting (default: true) that
automatically loads a curated list of known-good packages into
`allowBuilds` from @pnpm/plugin-trusted-deps. User-configured
allowBuilds entries take precedence over the defaults. Set
`use-default-trusted-deps=false` to disable.
* fix: use catalog reference for @pnpm/plugin-trusted-deps
* fix: use default import for @pnpm/plugin-trusted-deps CJS compat
The package uses Object.defineProperty for DEFAULT_ALLOW_BUILDS,
which Node.js/Jest ESM interop can't detect as a named export.
Switch to a default import to fix test failures.
* fix: use named ESM import from @pnpm/plugin-trusted-deps@0.3.0-1
The package now ships an ESM entry point with proper named exports,
so we can use a clean named import instead of the default import
workaround.
* fix: update @pnpm/plugin-trusted-deps to 0.3.0-2
Uses static JSON import attributes in ESM entry, fixing the bundle
issue where createRequire resolved paths relative to the bundle
output instead of the original package.
* refactor: rename setting to allow-builds-for-trusted-deps
* test: disable default trusted deps in approveBuilds tests
The tests assert exact allowBuilds contents, so the default trusted
list must be disabled to avoid polluting the expected values.
* fix: don't persist default trusted deps list to pnpm-workspace.yaml
Track the user's original allowBuilds separately as userAllowBuilds
before merging the default trusted list. Use userAllowBuilds when
writing back to pnpm-workspace.yaml to avoid persisting the ~370
default entries from @pnpm/plugin-trusted-deps.
* refactor: rename setting to allow-builds-of-trusted-deps
* docs: use camelCase for setting name in changeset
* fix: include userAllowBuilds in install command opts types
Without this, userAllowBuilds wasn't passed through to
handleIgnoredBuilds, causing the default trusted list to be
written to pnpm-workspace.yaml during e2e tests.
* fix: set userAllowBuilds to empty object when user has no config
When the user has no allowBuilds configured, userAllowBuilds was
undefined, causing handleIgnoredBuilds to fall back to the merged
allowBuilds (with defaults). Use empty object instead so the
fallback doesn't trigger.
* fix: read allowBuilds from workspace manifest when writing back
Instead of tracking userAllowBuilds separately (which gets stale
when other code writes to pnpm-workspace.yaml mid-install), read
the current allowBuilds directly from pnpm-workspace.yaml before
writing. This avoids persisting the default trusted list and
preserves entries written by --allow-build earlier in the flow.
Also update e2e test expectation: esbuild is now in the default
trusted list, so it builds instead of being ignored.
* chore: update tsconfig references for new dependencies
* test: disable default trusted deps in approveBuilds e2e install
The execPnpmInstall helper runs the bundled CLI which picks up
the default allowBuildsOfTrustedDeps=true. This causes extra
placeholder entries in pnpm-workspace.yaml that break assertions.
* fix: revert approveBuilds to use config-based allowBuilds
approveBuilds.handler should use opts.allowBuilds from getConfig()
(which excludes trusted deps defaults when disabled) rather than
reading the workspace manifest. The handler's job is to write
approve/deny decisions, not merge with auto-populated placeholders.
* test: add config reader tests for allowBuildsOfTrustedDeps
Cover: (1) default enabled with trusted defaults merged,
(2) user allowBuilds overrides defaults, (3) setting
allow-builds-of-trusted-deps=false disables the merge.
* feat: add `dedupePeers` option to reduce peer dependency duplication
When enabled, this option applies two optimizations to peer dependency resolution:
1. Version-only peer suffixes: Uses name@version instead of full dep paths
(including nested peer suffixes) when building peer identity hashes.
This eliminates deeply nested suffixes like (foo@1.0.0(bar@2.0.0)).
2. Transitive peer pruning: Only directly declared peer dependencies are
included in a package's suffix. Transitive peers from children are not
propagated upward, preventing combinatorial explosion while maintaining
correct node_modules layout.
The option is scoped per-project: each workspace project defines a peer
resolution environment, and all packages within that project's tree share
that environment. Projects with different peer versions correctly produce
different instances.
Closes#11070
* fix: pass dedupePeers to getOutdatedLockfileSetting and use spread for lockfile write
The frozen install path (used by approve-builds) calls getOutdatedLockfileSetting
but was missing the dedupePeers parameter. This caused a false LOCKFILE_CONFIG_MISMATCH
error because the lockfile had the key written (as undefined/null via YAML serialization)
while the check function received undefined for the config value.
Fix: pass dedupePeers to the settings check call, and use spread syntax to only write
the dedupePeers key to lockfile settings when it's truthy (avoiding undefined keys).
* fix: write dedupePeers to lockfile like other settings
Write the value directly instead of spread syntax, and use the same
!= null guard pattern as autoInstallPeers in the settings checker.
* test: add integration test for dedupePeers in peerDependencies.ts
* fix: only write dedupePeers to lockfile when enabled
When dedupePeers is false (default), don't write it to lockfile settings.
This avoids adding a new key to every lockfile.
* test: simplify dedupePeers test assertions
* test: check exact snapshot keys in dedupePeers integration test
* test: add workspace test for dedupePeers with different peer versions
* fix: keep transitive peers in suffix with version-only IDs
Instead of pruning transitive peers entirely (which prevented per-project
differentiation), keep them but use version-only identifiers. This way:
- Packages like abc-grand-parent still get a peer suffix when different
projects provide different peer versions (correct per-project isolation)
- But the suffixes use name@version instead of full dep paths, eliminating
the nested parentheses that cause combinatorial explosion
* refactor: extract peerNodeIdToPeerId helper in resolvePeers
* refactor: simplify peerNodeIdToPeerId return
* fix: pin peer-a dist tag in dedupePeers tests for CI stability
* fix: address review comments
- Register dedupe-peers in config schema, types, and defaults so
.npmrc/pnpm-workspace.yaml settings are parsed correctly
- Use Boolean() comparison in settings checker so enabling dedupePeers
on a pre-existing lockfile triggers re-resolution
- Fix changeset text and test names: transitive peers are still
propagated, just with version-only IDs (no nested dep paths)
Remove the @pnpm/fs.msgpack-file package which was never imported in
source code (only in its own tests). Also remove the deprecated
lockfile-directory CLI option alias — users should use lockfile-dir.