Commit Graph
12967 Commits
Author SHA1 Message Date
Zoltan Kochan 677a3e11d0 chore(release): pacquet 12.0.0, pnpr 0.1.0-alpha.8 (#14188) pnpr@0.1.0-alpha.8 v12.0.0 2026-08-26 16:36:14 +02:00
Zoltan Kochan 86c74539db feat: make pnpm v12 stable (#14187) 2026-08-26 16:01:14 +02:00
Zoltan Kochan 83350dea56 feat: integrate the remote side-effects cache with install (#14171)
Integrate the remote side-effects cache proof of concept with dependency installation in both TypeScript pnpm and pacquet.

Plan eligible build candidates before contacting pnpr, batch lookup requests, verify the configured P-256 trust keys and artifact integrity, hydrate verified files into CAFS, and select the restored side-effects map before lifecycle scripts run. Fall back to the ordinary local build on all cache failures.

Configure the feature through the remoteSideEffectsCache setting, assembled by the config reader from the workspace file, the global config file and the environment. A repository declares which organization and packages are eligible and nothing else: everything describing the act of signing stays with the machine holding the key, so a cloned repository cannot turn that key into a signing oracle.

Capture and publish the actual post-build diff from trusted builders. Bound artifact hydration and blob downloads, retain CAFS paths instead of downloaded buffers, and withhold direct store writes from a store opened read-only. Add transactional owner and global storage accounting to pnpr alongside request, response, timeout, streaming-memory, manifest, variant, descriptor, lock-namespace, concurrency, and crash-residue bounds.

Keep the proof of concept organization-scoped and Linux glibc-only. Defer lockfile pinning, persistent quarantine, publisher ownership, and key lifecycle policy to the RFC.

Related to pnpm/rfcs#20.
2026-08-26 15:49:39 +02:00
Zoltan Kochan ceed82fb11 feat(pnpr): serve hosted originals by digest (#14183)
Hosted pnpr registries need to resolve integrity-addressed original artifacts
without scanning every packument or object on each request.

Seal a durable package/version reference under the artifact SHA-512 digest
with each hosted publish. Apply and recover that reference through the same
crash-atomic journal as the tarball and packument. Resolve default,
named-registry, and router digest routes from those references, then
revalidate the current packument, revision history, caller access, registry
routing, and OSV policy before serving the artifact.

Prefer public candidates without making policy-sensitive responses shared
cacheable. Maintain a bounded per-digest index that embeds reference records together
with provisional journal ownership claims. Apply filesystem updates atomically
and S3 updates with compare-and-swap, finalize claims after packument
visibility, and roll back only claims owned by the failing transaction. This keeps
concurrent add/remove safe, resolves a digest with one bounded index read, and
enforces its 32-entry quota atomically. Reject overflow as a publish conflict
while journal recovery drops the uncommitted version, so reads never truncate
candidates or enter a persistent retry loop. Support the same reference layout
in filesystem and S3 storage. Add storage,
metadata-validation, access-control, restart-persistence, journal-recovery,
routing, response-header, and fanout-bound coverage.

Follow-up to https://github.com/pnpm/pnpm/pull/14180 and
https://github.com/pnpm/rfcs/pull/19.
2026-08-26 15:28:20 +02:00
Zoltan Kochan 327e6afa7b fix(cli): switch to the pnpm version the lockfile resolved the pin to (#14184)
A project pinning a range in `devEngines.packageManager` resolves it once,
and `pnpm-lock.yaml` then names the one pnpm every contributor runs. The
pre-command switch read that resolution and threw it away, deciding whether
to switch by testing the running pnpm against the range instead: a project
pinning `^12.0.0-rc.3` with `12.0.0-rc.11` recorded went on running whatever
pnpm was on PATH, as long as the range admitted it too. The recorded
resolution now decides, which is what `switchCliVersion` does in the
TypeScript CLI. Only when there is none does the range decide, so a running
pnpm the range already accepts is still spared the round trip; a resolution
that has to be repaired is always carried out, because the repairing write
happens in the switch.

The range test itself was wrong in the same direction. It emulated npm's
`includePrerelease` by stripping the prerelease off the running version and
re-testing, which raises it -- a prerelease sorts below its own release --
so `12.0.0-rc.7` passed for `>=12.0.0-rc.9`, `^12.0.0-rc.11` and `12.0.0`
alike. That silenced the pin check as well as the switch. The resolver
already carried a faithful port of those semantics, comparator by
comparator; it moves to `pnpm-semver-include-prerelease` and both the pin
check and `check_engine` -- which carried a third, coarser copy -- now share
it.

`engines` checks change with it: a prerelease no longer satisfies a caret
range spelling out the release it is a prerelease of, matching
`semver.satisfies(version, range, { includePrerelease: true })` upstream,
and a bound that omits a component is read as npm reads it, so `<=22`
accepts 22.5.0.
2026-08-26 14:06:22 +02:00
Zoltan Kochan 0ec67d0fcf feat: refresh registry revisions through pnpr (#14180)
Allow revision-only updates to delegate dependency resolution to pnpr.

Carry updatePatches across both pnpr clients and map it server-side to pacquet's RefreshRevisions seed policy. Explicit refreshes bypass frozen reuse and pnpr's whole-resolution cache, while the registry resolver revalidates metadata even when its in-memory or disk cache is warm.

Route both pnpm implementations through their existing pnpr install and materialization paths. The Rust update command accepts --pnpr-server, and partial TypeScript workspace refreshes continue locally so unselected importers are not refreshed.

Follow-up to https://github.com/pnpm/pnpm/pull/14175.
Related to https://github.com/pnpm/rfcs/pull/19.
2026-08-26 12:29:51 +02:00
Zoltan Kochanandgithub-actions[bot] c6187b8fae chore: update dependencies, Node.js, pnpm, and GitHub Actions (#14182)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-26 12:27:44 +02:00
Zoltan Kochan 6b521cdc32 fix: preserve patches and package extensions with pnpr (#14181)
The pnpr resolution protocol omitted patchedDependencies and packageExtensions. Its fast path therefore returned an untransformed lockfile, while bypassing pnpr for these projects would sacrifice the remote-resolution performance the setting exists to provide.

Compute patch hashes on the client and send only the ordered selector-to-hash map to pnpr. The server uses those hashes to key patched snapshots in the lockfile, while the client retains the local paths and applies the patch files during materialization. Send package extensions and unused-patch policy alongside them so pnpr resolves the same transformed dependency graph as local resolution.

Key pnpr's interned configuration and resolution cache by the forwarded transforms, validate patch digests at the protocol boundary, and scan package-extension dependency URLs through the existing fetch allowlist and inline-credential checks. Implement the protocol in both TypeScript and Rust clients and cover it with live server-to-client tests. Both clients validate the returned transform metadata so older servers that ignore the new request fields fail with a stable protocol error instead of silently returning an untransformed lockfile. pnpr advertises transform support in the HTTP response headers, allowing the Rust client to validate compatibility before consuming any package frames. Current servers retain streaming prefetch performance; older servers fail before they can trigger downloads or unbounded hint buffering.

Fixes pnpm/pnpm#14177.
2026-08-26 12:23:10 +02:00
Zoltan Kochan 4e88dd1635 fix(fetch): link virtual-store bins so dependency build scripts resolve siblings (#14178)
`run_link_phase` returned early under `virtual_store_only` before reaching
`LinkVirtualStoreBins`, while `run_build_phase` still ran dependency
lifecycle scripts under that mode. A `postinstall` that invokes a sibling
dependency's bin — `unrs-resolver` calling `napi-postinstall`, for
instance — therefore died with `command not found` (exit 127) under
`pnpm fetch`, because `pnpm_executor::extend_path`'s ancestor-`.bin` walk
had no `.bin` to find. `pnpm install --frozen-lockfile` against the same
lockfile worked: it does not set `virtual_store_only`.

The per-slot bin pass now runs under `virtual_store_only` too. Its links
live inside the virtual store, which is exactly what that mode
materializes; only the importer-facing passes (prune, direct-dependency
symlinks, root components, hoisting, sidecars) stay skipped.

The TypeScript CLI is unaffected — its `buildModules` links a dep node's
bins itself right before running the node's scripts — so it gets the
matching regression test only, plus the `allowBuilds` widening that test
needs on `FetchCommandOptions`.

Closes pnpm/pnpm#14174
2026-08-26 02:32:29 +02:00
Zoltan Kochan bb09b5084b feat: support selecting and refreshing registry revisions (#14175)
Implement explicit registry revision selection and revision-only updates from the current revisions RFC.

Parse exact +rN selectors across npm, JSR, aliases, and named registries. Validate historical artifacts against strict integrity-addressed metadata, overlay only immutable manifest fields, and avoid metadata requests during frozen installs unless an active policy requires them.

Add update --patches to refresh artifacts at the currently locked package versions while preserving explicit revision pins and manifest ranges. Mirror the behavior in the Rust CLI, force reimport when the artifact changes under the same package identity, and make pnpr expose only safe rewritten revision histories.

Follow-up to https://github.com/pnpm/pnpm/pull/13475.
Related to https://github.com/pnpm/rfcs/pull/19.
2026-08-26 01:58:44 +02:00
Zoltan Kochan 74f9236b67 feat: support integrity-addressed registry revisions (#13475)
Add registry replacement-tarball support to the TypeScript and Rust pnpm implementations. Validate explicit positive safe-integer revision metadata against complete SHA-512 integrity-addressed registry URLs, compact and hydrate revision resolutions in the lockfile, and enforce a single authenticated non-redirecting fetch with no retry or fallback.

Route revision URLs through the normalized scope and prefix maps derived from the registries setting, preserving registry path prefixes. Let pnpr proxy immutable revision artifacts for concrete upstream registries with forwarded authentication, integrity verification, and digest caching. Carry revision metadata through pnpr's resolver protocol so pacquet uses the same strict fetch behavior for direct and proxied resolution paths.

Leave revision selection and history, patch refresh commands, pnpr hosted publishing, and the RFC's unresolved lockfile compatibility gate for follow-up work.
2026-08-25 22:13:27 +02:00
Zoltan Kochan b87c7cc5d2 fix(config): stop pnpm config delete leaving a trailing blank line (#14170)
A top-level block's span reaches the next top-level key, so it carries that
block's trailing blank separator. The last block in a document has no such
successor: its separator is the blank line before it. Take that one instead
when the removal runs to the end of the document, and skip the separator on
insert when the text already ends in a blank line.

Judge the document's blank-line style once, at parse time, rather than
re-deriving it from the mutated text. `config set` on an already-present
last key removes and reinserts that key, and the style probe then saw a
document one key short of the original, concluded its blocks were not
blank-line separated, and glued the rewritten setting to its predecessor.
The TypeScript writer likewise judges the style from the original key list.

Also renames this repo's own `enableGlobalVirtualStore: true` to the
`virtualStoreType: global` spelling, and drops the stray blank line the bug
left behind when that setting was rewritten.
2026-08-25 20:03:34 +02:00
Zoltan Kochan c22eac25ab feat(init): pin the latest pnpm version, not the running one (#14167)
`pnpm init` wrote its `devEngines.packageManager` / `packageManager` pin
from the version of pnpm that happened to run the command. That makes
staleness self-perpetuating: a developer whose global pnpm is a year old
scaffolds a project pinned to that year-old pnpm, and the pin then holds
every collaborator there. It is also the friction behind the request to
have pnpm auto-update itself (pnpm/pnpm#7490) — a brand new project is
the one place where jumping to the current release can break nothing.

Both stacks now resolve pnpm's `latest` tag when they are about to write
a pin, and fall back to the running version whenever that cannot be
answered: no `cacheDir`, `offline` / `preferOffline`, an unreachable or
slow registry, a `latest` the `minimumReleaseAge` / `trustPolicy`
settings reject, or a `latest` older than the running version (the tag
lags whenever a new major ships untagged). The lookup makes one attempt
with no retry schedule and is capped at ten seconds, so scaffolding a
manifest can never fail or appear to hang on it, and it is skipped
entirely when a `package.json` is already there.

The lookup runs through the trusted package-manager registries, the
channel `self-update` provisions the engine from, rather than the
project registries: the value becomes a durable pin that drives an
engine download, so a repo-controlled `.npmrc` must not choose it.

On the TypeScript side the version resolution `self-update` already had
— bootstrap config, release-age cutoff excluding the running version,
trust policy — moves to `resolvePnpmVersion`, which `init` now shares;
`self-update`'s `resolution.manifest.version` reads become the
`targetVersion` it already declared. On the pacquet side `init` gains a
real command future, since it now awaits the registry.

Related to pnpm/pnpm#7490.
2026-08-25 18:25:07 +02:00
Zoltan Kochan d3ebbedc4a fix: enforce git prepare approvals on store reuse (#14160)
Git preparation was gated only while fetching a package. Once an approved project placed prepared files in the shared store, a different project could link those files without consulting its own allowBuilds policy.

Persist whether preparation was required in each git package's store-index row and check the active project policy before warm-store reuse. Legacy rows without the marker are reused only with explicit approval; otherwise they go through the existing cold fetch and preparation gate. Revalidate in-process git fetch results as well so a long-lived store controller cannot carry approval across policy changes.

Pass the canonical lockfile resolution ID into both git fetch paths so the allowBuilds identity and suggested key stay byte-for-byte exact. Mirror the marker and warm-reuse gate in pacquet using the shared msgpackr-compatible store format.

Closes pnpm/pnpm#13965.
2026-08-25 18:15:59 +02:00
Zoltan Kochan 369a9d6c93 fix(tarball): bound the memory an untrusted archive decode can claim (#14163)
Four decode paths grew to whatever the archive or the server produced,
and nothing upstream of them bounded it. The lockfile's integrity does
not: a gzip bomb is a legitimately published package whose hash matches.
The registry's `dist.unpackedSize` does not either — it is the
publisher's claim, and on a frozen install there is none at all, since a
lockfile records no unpacked size.

- The whole-archive gzip decode now stops at `MAX_UNTRUSTED_PREALLOC_BYTES`,
  the same ceiling `should_stream_extract` routes on, instead of relying
  on zune-inflate's 1 GiB default that every concurrent extraction may
  claim. Reaching it is not a refusal: the archive is re-run through the
  streaming extractor and decoded in full. `read_local_tarball_metadata`
  gets the same fallback, so a large `file:` tarball still resolves.
- A response body that keeps arriving is handed to the streaming
  extractor once it reaches the size at which it would be extracted as a
  stream anyway, so a chunked response — which advertises no length —
  and a body that outruns the length it advertised both stop growing the
  buffer. The buffer is also pre-sized only as far as it can fill, so an
  advertised `Content-Length` no longer decides the size of an
  allocation it will never occupy. A body that is not a gzip stream is
  drained and dropped rather than accumulated, after being hashed, so a
  pinned integrity still produces the verdict that outranks the decode
  failure.
- The queue joining the download loop to the streaming extractor is
  bounded. An unbounded channel between an async producer and a blocking
  consumer holds whatever a server sends faster than the extractor
  consumes it, which would hand back the very buffer this path exists to
  avoid; it was only ever bounded by the buffered alternative holding
  the whole body too. A bounded tokio channel with a blocking receive on
  the extractor's thread applies backpressure to the download instead.
- A zip entry is read only as far as the size its central directory
  records, and one above the entry buffer ceiling streams straight into
  the store. Nothing in a zip bounds an entry's decompressed size on its
  own — unlike a tar entry, whose header size genuinely delimits it.

No archive is refused for being large, so nothing that installed before
stops installing. Routing now consults the gzip trailer's unpacked size,
which the frozen-install path had no equivalent of, so a large archive
goes to the streaming extractor on the first pass rather than being
decoded twice to discover how large it was. It is read only behind a
well-formed deflate header, so a corrupt body keeps reporting the decode
error rather than being routed by four bytes of whatever it ends with.

One error code moves: a non-gzip response body larger than 16 MiB
reported `ERR_PNPM_TARBALL_IO_ERROR`, having been routed to the
streaming extractor by its compressed size and failed in the tar reader,
and now reports `ERR_PNPM_TARBALL_DECODE_GZIP` — what the same body
under 16 MiB already reported. Both are retryable, so retry behavior is
unchanged.

The TypeScript CLI has the same exposure on all three of the issue's
paths — it buffers the whole body, `gunzipSync`es the whole archive, and
inflates each zip entry through adm-zip, with no ceiling anywhere.
Bounding pacquet's decode changes no observable behavior, so there is
nothing to mirror for parity; giving the TypeScript stack the same
property means replacing `parseTarball`'s index-scan-over-one-buffer
design, and is tracked separately.

Closes https://github.com/pnpm/pnpm/issues/13656
2026-08-25 17:06:28 +02:00
Zoltan Kochan a10a6c3bef feat(napi): honor ignorePackageManifest and pnpmHomeDir (#14162)
Close the last two install options the `@pnpm/napi` binding accepted without
acting on (Closes pnpm/pnpm#12823, follow-up to pnpm/pnpm#12826).

ignorePackageManifest now carries pnpm's full `pnpm fetch` shape rather than
only skipping the manifest/lockfile freshness gate: the frozen path against
the lockfile alone, virtualStoreOnly (no importer symlinks, .bin entries,
hoisting, or project lifecycle scripts), a forced-on modules dir so an
ambient enableModulesDir:false cannot leave the virtual store nowhere to go,
and ProjectMutation::NoInstall. It outranks both update (which would
re-resolve what the lockfile already pins) and the lockfile-only
short-circuit (which would skip the materialization the mode exists to
perform) — the same precedence the TypeScript handler applies.

pnpmHomeDir now places the default store at <pnpmHomeDir>/store via a new
Config::resolve_store_dir_from_home, mirroring the pnpmHomeDir input of
pnpm's getStorePath including the same-volume probe and the mountpoint
fallback. An explicit storeDir, passed alongside it or set by a config
source, still wins.

The TypeScript engine already implements both options; this is the Rust
binding reaching parity with it, so there is no TS-side counterpart.
2026-08-25 16:30:14 +02:00
Zoltan Kochan 1405c7c4e1 fix(cli): install workspace root under filters (#14158)
Keep manifest-mutation selection separate from importer materialization selection.
Filtered shared-lockfile install-family commands now install the workspace root without mutating its
manifest. Preserve filtered validation and cleanup semantics while including the root
in lifecycle execution and pnpr requests.

Closes pnpm/pnpm#13397.
2026-08-25 16:04:17 +02:00
Zoltan Kochan 6d1176f8e5 fix: validate unused patches during incremental installs (#14156)
Derive applied patch keys from the final pruned lockfile graph. This lets reused lockfile subtrees and unchanged workspace importers count toward validation on full-workspace resolutions.

Closes pnpm/pnpm#13692
2026-08-25 15:21:25 +02:00
Zoltan Kochan 95e23ffb23 feat(pacquet): deprecate pnpmfile filterLog (#14157)
The Rust v12 CLI would have to cross the persistent Node worker IPC boundary
for every eligible reportable event to implement filterLog. That cost is a bad
fit for the reporter hot path.

Detect a callable filterLog export once while the pnpmfile worker is already
active, warn that v12 ignores the deprecated hook, and leave emitted logs
unchanged.

Closes pnpm/pnpm#12118.
2026-08-25 15:05:16 +02:00
Zoltan Kochan f4d196afd1 docs: warn against installing pnpm with Corepack (#14159)
The repository's scripts invoke pnpm through the pn and pnx aliases,
which the official installation methods create but Corepack does not,
so a Corepack-managed pnpm fails with errors like pn: Permission denied.

Related to https://github.com/pnpm/pnpm/issues/12448
2026-08-25 15:03:50 +02:00
Nicolas Le CamandZoltan Kochan fb0f338cdf feat(audit): add audit.ignorePrune to remove stale ignored GHSAs (#11384)
Add audit.ignorePrune (default false). When enabled, `pnpm audit --fix`
diffs the ignored-GHSA list against the current audit report and drops
entries whose GHSA id no longer appears in it, persisting the retained
list back to pnpm-workspace.yaml. GHSA ids are compared after
normalization, and retained entries are rewritten in canonical spelling
and deduplicated.

Implemented in both stacks: the TypeScript CLI reads the canonical
audit.ignorePrune setting into the reader-derived auditIgnorePrune
config field, and pacquet mirrors it with Config::audit_ignore_prune
plus a prune_ignored_ghsas port of the same split, wired into the --fix
path ahead of the existing advisory filter.

Closes pnpm/pnpm#11237

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-08-25 14:39:20 +02:00
Zoltan Kochan e630a0492a fix(pacquet): stop recording corepack's integrity in the packageManager pin (#14146)
`pnpm add yarn@<version>` wrote the Yarn Classic pin the way `corepack use`
does, with the sha512 of the npm tarball as semver build metadata. Drop it
and record the resolved version alone.

The hash is not pnpm's to keep. Corepack is what installs a release from
this pin, and for the line it fetches from npm it derives the same hash
from the registry's signed metadata anyway — so a copy in the manifest
only gave the same fact a second home, in a field pnpm itself never
verifies, for the two to drift apart. Integrity that pnpm does verify
belongs in the lockfile, which is where pnpm records it.

The doc comment on the removed helper also claimed that a Yarn Berry pin
without a hash is still verified "through npm's signature". It is not:
corepack derives a missing hash, and checks the npm registry signature,
only for a package manager whose spec has an npm-typed registry, and
Yarn `>=2` is fetched from a plain `repo.yarnpkg.com` URL.
2026-08-25 14:20:30 +02:00
Lazizbek Ergashev 08a082f730 fix(lockfile): keep bundledDependencies and the libc shape on a reused package entry (#14154)
`synthesize_manifest` in the lockfile reuse gate rebuilds a manifest fragment
from a reused `packages:` entry, carrying peerDependencies, engines, cpu, os,
libc, deprecated and bin. bundledDependencies was missing, so
`BundledDependencies::from_manifest` in `build_package_metadata` found nothing
and the rewritten entry dropped the field. Any lockfile rewrite that reused a
resolution therefore deleted bundledDependencies from unrelated entries.

Both recorded shapes are synthesized back: the name list as a JSON array and
`true` as a JSON boolean, which is what `from_manifest` reads on the fresh
resolve path.

libc had a narrower case of the same bug. It was synthesized through
`string_array`, which takes `&[String]` and so reached StringOrList::String
through its Deref, always emitting an array; the writer then read that back as
a list. An entry recorded as `libc: musl` was rewritten as `libc: [musl]`. It
is now matched on and the scalar form is preserved.

The issue also reports that `prepare` is dropped. The TypeScript CLI drops it
as well — it is not a field of LockfilePackageInfo and not in the pick list of
convertToLockfileFile — so preserving it here alone would break parity, and it
is left for a change that covers both stacks.

Closes pnpm/pnpm#14153
2026-08-25 14:14:02 +02:00
Zoltan Kochanandgithub-actions[bot] 07527caf1c chore: update dependencies, Node.js, pnpm, and GitHub Actions (#14122)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-25 14:06:01 +02:00
Zoltan Kochan d669c8bf17 perf(deps): sort dependency graphs in linear time (#14152)
graph_sequencer collected each topological chunk by rescanning every
included node, so a graph whose chains produce as many chunks as nodes
sorted in O(V * chunks) — with a full node hash (a PathBuf of the
project directory) paid on every probe. A 6,833-project workspace whose
projects each depend on their predecessors spent ~4 s per sort, and a
plain install sorts twice (the recursive project selection and the
workspace-cycle report), regressing pnpm:scope from 0.15 s to 4 s and
pnpm:package-manifest from 0.5 s to 4.3 s between v12.0.0-rc.8 (which
sorted nothing on a plain install) and rc.9 (where cfg.sort defaults the
recursive sort on and pnpm/pnpm#14074 added the cycle check).

Both implementations now intern the nodes to indices up front and gather
each chunk from the nodes whose degree the previous chunk's removal
dropped to zero, i.e. layered Kahn's in O(V log V + E) with no hashing
in the loop. Chunk membership and order (following `included`), the
cycle list, and the `safe` verdict are unchanged; the one divergence the
rewrite removes is that the Rust port used to emit a node twice into one
chunk when `included` listed it twice, where the TypeScript
implementation deduplicated — both now deduplicate.

The TypeScript graphSequencer had the same shape (its Map probes are
cheaper, so it hid the cost longer) and is rewritten identically to keep
the stacks aligned.

On the pnpm/pnpm#14151 reproduction (6,833 projects), a warm no-op
`pnpm install --lockfile-only` drops from 9.0 s (rc.9) to 1.7 s, and a
one-dependency lockfile update from 14.2 s to 5.9 s — faster than
Yarn 4's 7.2 s on the same change.

The cycle pass is confined to strongly connected components (iterative
Tarjan): nodes that merely lead into a cycle no longer pay a
reachability walk each, and one ring's search cannot walk downstream
rings. Reported cycles, chunk order, and the safe verdict are
unchanged. The TypeScript side also appends cycle ids without a
call-spread (argument-limit overflow on huge cycles) and replaces
queue.shift() with a head index.

A new workspace_sort micro-benchmark group covers the shape every
existing benchmark missed (single-project integrated fixture; the
workspace_resolution group starts downstream of sorting): the projects
graph build plus sequencing over 4,000 chained projects — 538 ms
quadratic, 15.7 ms now.

Closes pnpm/pnpm#14149.
Closes pnpm/pnpm#14151.
2026-08-25 13:51:56 +02:00
Zoltan Kochan 370dfd4bd0 fix(cli): accept --production, and reproduce a prod install as --prod (#14148)
The Rust CLI registers only `--prod` on `install`, so the `pnpm install …`
command the verify-deps-before-run gate reproduces from a production-only
install — built with pnpm's `--production` spelling — was rejected by the
argument parser, aborting every `pnpm run` (Closes pnpm/pnpm#14147).

`--production` is the setting name behind `--prod`, and the TypeScript CLI
accepts it on every command where `--prod` selects dependency groups, so
restore it as an alias there too (`audit`, `licenses` and `outdated` already
had it), and emit the documented `--prod` in the reproduction command in
both stacks.

Closes #14147
2026-08-25 11:27:03 +02:00
Zoltan Kochan b4d19c87a1 fix(release): count only installer processes this step started (#14145)
The check that separates a rejected modify command line from an install
that ran and skipped a component watched for any Visual Studio installer
process by name. One already running for an unrelated reason would pass
for the one this step launches, putting the run back on the generic
"clang-cl was not installed" error it is meant to sharpen.

Snapshot the matching process ids before the launch and count only ones
that appear afterwards.
2026-08-25 02:23:09 +02:00
Zoltan Kochan 6098ce7a74 fix(release): drop the unsupported --wait from the Visual Studio installer call (#14144)
The 12.0.0-rc.11 release failed on both win32-arm64 legs with "clang-cl
was not installed under ...\VC\Tools\Llvm", five minutes after the
Visual Studio installer was asked to add the LLVM and ARM64 components.

`--wait` had been added to that `setup.exe modify` call. Microsoft
documents it as bootstrapper-only: "The --wait parameter can only be
passed into the bootstrapper; the installer (setup.exe) doesn't support
it." We invoke the installer at
`C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe`, so
it rejected the command line and installed nothing. The logs show the
call returning in under a second and no installer process alive for the
whole polling window.

The exit-code check could not catch that. `setup.exe` is a
GUI-subsystem binary, so PowerShell's call operator does not wait for it
and never sets $LASTEXITCODE from it — the value tested was still
vswhere's. Check vswhere's own result instead, where the variable
actually means something, and leave the poll loop as the sole
confirmation that the components landed, which is what it was written
for.

Regressed in pnpm/pnpm#14138; the split into separate CLI and NAPI
matrices is unaffected, it only doubled the number of legs running the
step.
v12.0.0-rc.11
2026-08-25 00:46:28 +02:00
Zoltan Kochanandgithub-actions[bot] f2b50ddf95 chore(release): pacquet 12.0.0-rc.11 (#14143)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-25 00:12:05 +02:00
Zoltan Kochan ea10c041a5 feat(stage): approve staged packages interactively with one OTP (#14140)
`pnpm stage approve` accepted exactly one stage id and wrapped each request in
its own `withOtpHandling`, so releasing a workspace meant one invocation and one
proof of presence per package, in an order the releaser had to work out by hand.

It now takes a list of stage ids, and with none it lists the staged versions and
offers them in a checkbox prompt (the non-interactive case keeps failing with
STAGE_ID_REQUIRED, now with a hint).

The batch runs through a new `OtpSession` in the web-auth package: it keeps the
one-time password a challenge yielded and passes it to every later request,
re-prompting only once the registry stops accepting it — a classic TOTP expires
within a minute, which is exactly the case a whole-workspace approval hits.
`withOtpHandling` is now the single-operation form of that session, so publish
and reject behave as before.

Inside a workspace the selection is sorted through the workspace dependency
graph (the same sequencer the recursive commands use), keyed on the name each
project publishes under, since that is the only name a staged version carries. A
package whose workspace dependency could not be approved is skipped rather than
published against a dependency that never reached the registry. A registry
verdict on one version leaves the rest of the batch running and ends with a
non-zero exit; an authentication failure or a broken connection aborts it.

The registry's description of a staged version decides what a maintainer
publishes, so its id and package name are validated as they came — a hidden
character can neither be stripped into a valid value nor keep a name from
matching a workspace package — and the display-only fields are stripped of the
control characters that could redraw the picker around a selection. The shared
`@pnpm/text.sanitize` package holds that character set for both the staged
picker and `update --interactive`, mirroring the `pnpm-text-sanitize` crate.

Both stacks implement this, down to the partial-batch summary and exit code —
pacquet has no output-with-exit-code channel, so it prints the summary the way
the dispatcher prints command output and exits 1.
2026-08-25 00:10:33 +02:00
Zoltan Kochan 34120fd5e0 fix(pacquet): link a manifest-less root-component member from its snapshot, not the whole root (#14142)
The sibling-linking pass for Bit root components (`link_root_component_members`) falls back to linking **every other member of the root** into a member's slot when the materialized copy carries no `package.json`. A Bit workspace component never carries one at engine link time — Bit writes a manifest into the copy only during its own post-install compile, and that manifest is stripped of sibling edges anyway — so on a real Bit workspace every injected member gets the all-member fallback: one symlink per (member, member) pair per peer context.

### Measured on teambit/bit's own workspace

- **270,840** `@teambit/*` symlinks under `node_modules/.pnpm/`, all inside the **812** injected-component slots
- a `@teambit/compositions` variant slot carries **359** sibling links where its lockfile snapshot declares **42** — the other 317 are the fallback
- the inode cost is paid on every install and every CI cache restore; on teambit/bit's CI the persisted `node_modules` tree grew 2.9 GB / 207k symlinks → 3.8 GB / 356k symlinks across the pnpm v12 migration, and per-node workspace attach time went 0.8 → 2.7 min on all 40 e2e nodes

### The fix

A member's own lockfile snapshot — keyed by its exact peer variant — declares precisely the sibling `file:` edges of that copy. A manifest-less member now links those. The source order is: slot `package.json` → member snapshot → all members of the root (only when neither exists; the reachability guarantee is unchanged).

`link_root_component_members` takes the lockfile's `snapshots` map (the link phase already holds it), and `Member` carries its snapshot key.
2026-08-24 22:57:21 +02:00
Zoltan Kochan 4aa13c1bc6 fix(release): generate the release description when run as a script (#14137)
`pn make-release-description` crashed with "Cannot access 'SPONSORS_FRAGMENT'
before initialization", so pnpm 11.24.0's release notes fell back to the
workflow's diagnostic description.

The script's top-level `await writeReleaseText(repoRoot)` sat above the
`SPONSORS_FRAGMENT` constant it ends up reading. Function declarations hoist,
`const` bindings do not: the entry point ran while the constant was still in
its temporal dead zone. The test suite never saw it because tests import the
module, which leaves the direct-invocation branch unevaluated.

Moves the entry point to the bottom of the module, past every module-level
binding, and covers the script path with a test that spawns it the way the
release job does. The entry point takes an optional workspace directory so
that test can point it at a fixture instead of the repository.
2026-08-24 21:27:35 +02:00
Zoltan Kochan 05129e1fd2 perf(release): build the NAPI addon on its own runners (#14138)
The Rust release built the CLI binary and the NAPI addon back to back in
one matrix leg per target. The two are compiled under different Cargo
profiles — `release` for the CLI, `napi-release` for the addon, which
must keep `panic = "unwind"` so a panic reaching the FFI boundary becomes
a JS exception instead of aborting the host node process. Different
profile means a different target directory, so the two builds shared not
one compiled dependency: every leg paid for the whole graph twice, in
series.

Split the addon into its own `build-rust-napi` matrix over the same eight
targets. Measured on the 12.0.0-rc.9 release run, per-leg build time was:

  target             CLI     addon    leg
  win32-arm64        508s    369s     17m09s
  win32-x64          495s    363s     15m06s
  darwin-arm64       477s    241s     12m30s
  darwin-x64         461s    235s     12m10s
  linux-*        322-351s  217-238s   ~10m

The stage is bounded by its slowest leg, so dropping the addon out of
win32-arm64 takes the critical path from ~17 to ~11 minutes and the whole
release run from ~25m30s to roughly 19 minutes. The addon legs run
alongside and gate nothing.

Downstream needs no changes: the new job uploads to
`binaries-rust-napi-<code-target>`, which the `binaries-rust-*` download
pattern the verify, publish and draft-release jobs already use picks up
and merges into the same directory. It is also free of the node-gyp
payload dependency, since only the CLI archives ship `dist/`.

The toolchain prelude the two jobs share — installing cross, the
clang-cl/ARM64 Visual Studio components, the rustup target, and the guard
that the committed PNPM_VERSION matches the tag — moves into a
`rust-release-target` composite action rather than being duplicated.
zizmor flags that action's writes to GITHUB_ENV/GITHUB_PATH because a
composite action cannot see who calls it; the values come from the
Visual Studio install and `vcvarsall`, and the only caller is a release
job running on a maintainer-signed tag, so the audit is suppressed on
that step with that justification.
2026-08-24 21:16:43 +02:00
Zoltan Kochan c181e90cef fix(release): keep the Rust CLI's bundled bins packable (#14136)
The Rust CLI's `dist/node_modules` is deployed the same way as the TypeScript
CLI's, so the hoisted linker symlinked its bins too and
`dist/node_modules/.bin/get-pnpm` went missing from the `pnpm` tarball:

    tar: package/dist/node_modules/.bin/get-pnpm: Not found in archive

Pin the setting off for that deploy as well, and teach `verifyPayload` to reject
a symlink anywhere in `dist/` — packing drops one silently, which leaves exactly
the hole that function exists to catch.
v12.0.0-rc.10
2026-08-24 16:50:07 +02:00
Zoltan Kochan 4c07abab66 fix: stop pointing users at Corepack for pnpm updates (#14115)
## Summary

The v11 CLI and the Rust engine, matching #14113 (merged) plus its follow-up #14116.

Two places told users to update pnpm with Corepack:

- The **update notification** printed `corepack use pnpm@<version>` when pnpm ran under Corepack. It now prints pnpm's [standalone install script](https://pnpm.io/installation) — `curl -fsSL https://get.pnpm.io/install.sh | sh -`, or the `Invoke-WebRequest` form on Windows.
- **`pnpm self-update`** refused under Corepack with *"You should update pnpm with corepack"*. It now says it cannot update itself under Corepack, and names the standalone install script as the hint.

The notification also stopped naming `pnpm add -g pnpm`, which `add` refuses outright (`ERR_PNPM_GLOBAL_PNPM_INSTALL` in the TypeScript CLI, `GlobalError::GlobalPnpmInstall` in the Rust one). `pnpm self-update` is named only where it can replace the executable in use — an install another package manager owns is resolved from that manager's bin directory, so a self-update would land beside it and report success while the old pnpm keeps announcing the update:

| `PNPM_HOME` set | under Corepack | suggestion |
| --- | --- | --- |
| yes | no | `pnpm self-update` |
| yes | yes | standalone install script |
| no | either | standalone install script |

The command has one definition per stack — `standaloneInstallCommand` in `@pnpm/cli.meta`, `standalone_install_command` in `pnpm-config` (beside `PNPM_VERSION`) — shared by the notification and the self-update refusal so the two cannot drift.
2026-08-24 16:45:42 +02:00
Zoltan Kochan cef4816dfb fix(release): keep the bundled bins packable (#14135)
`dist/node_modules` is built by a `pnpm deploy --config.node-linker=hoisted`,
and the hoisted linker turns `preferSymlinkedExecutables` on, so
`dist/node_modules/.bin` became a directory of symlinks. An npm tarball cannot
carry a symlink, so those bins were dropped from the packed `pnpm` and
`@pnpm/exe` tarballs and the release workflow's artifact verification failed:

    tar: package/dist/node_modules/.bin/node-gyp: Not found in archive

Force the deploy to write shell shims, which pack fine, restoring the layout
every release before 11.24.0 shipped.
v11.24.0
2026-08-24 16:29:13 +02:00
Zoltan Kochanandgithub-actions[bot] e3cb54258c chore(release): 11.24.0, pacquet 12.0.0-rc.10 (#14134)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 15:53:03 +02:00
Lazizbek ErgashevandZoltan Kochan ccff620f66 fix(lockfile): drop removed dependencies when merging branch lockfiles (#13967)
Merging git branch lockfiles unions their keys, so a dependency that the main branch removed after a branch lockfile was written comes back in the merged result. Resolution normally prunes it again, but --frozen-lockfile skips resolution and fails the manifest check with ERR_PNPM_OUTDATED_LOCKFILE.

Once the branch lockfiles are merged, prune each importer back to what its manifest declares. The declared names are derived per dependency group the way satisfiesPackageManifest derives them, so a dependency that moved between groups is reconciled rather than left in both. Only a peer that no other field declares is auto-installed, so a dependency listed in both devDependencies and peerDependencies keeps the field that declares it rather than being dropped from it. Pruning importer edges can strand packages, so the merged lockfile then goes through pruneSharedLockfile.

The Rust CLI merges branch lockfiles the same way and had the same bug. It gets the same reconciliation, hooked into the install pipeline because the lazily loaded wanted lockfile has no access to the project manifests at load time.

Fixes pnpm/pnpm#13966.
---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-08-24 15:49:36 +02:00
Zoltan Kochan c54dba47fd fix(install): accept a package-manager entry pnpm does not install from (#14132)
A pnpm below 11.20.0 pins `@pnpm/exe` beside `pnpm` for a v12 version, because
that is the set its own major is installed from — pnpm/pnpm#13599 is where pnpm
learned that v12 publishes the executable as the unscoped package alone. A
project pinning pnpm 12 whose lockfile such a pnpm last wrote therefore carries
an entry every newer pnpm rejects, and `--frozen-lockfile` fails on it from
either side of the disagreement while a plain install rewrites the block and
hides it again. The two versions take turns.

That entry pins the wanted version through the same integrity and cannot change
which pnpm runs, so the frozen refusal now accepts a block that records every
package this pnpm installs from, whatever else it records. An entry pinning
another version is a lockfile that disagrees with the manifest and is still
refused. The predicate that drives the write keeps the exact-set rule, so a
writable install still rewrites the block to the packages this pnpm installs
from — the trade is that `--frozen-lockfile` no longer guarantees that block is
byte-stable against a later plain install.

Related to pnpm/pnpm#14124.
2026-08-24 15:43:28 +02:00
Zoltan Kochan 9798c9f611 revert: "chore(release): stop publishing @pnpm/exe from v12 (#14121)" (#14133)
This reverts commit afb4071811.
2026-08-24 15:35:45 +02:00
Zoltan Kochan fc0278acd7 fix(install): stop --frozen-lockfile rejecting a package-manager pin it agrees with (#14129)
Three ways a project's `packageManagerDependencies` entry could be rejected by
`--frozen-lockfile` while agreeing with the manifest, each leaving a plain
install reporting success without repairing anything.

A bootstrap repair — entries whose resolutions the bootstrap refuses to read
are discarded and re-resolved before the pinned pnpm is installed — went
through the same save path as a genuine update. It now resolves in memory
under a frozen lockfile and leaves the lockfile untouched, and re-resolves the
version the lockfile records rather than the range around it, so the command
runs the pnpm the lockfile pins. `resolve_package_manager_integrities` returns
the env lockfile it resolved, which `install_engine_to_store` installs from
instead of re-reading the one it just wrote, and the switch plan carries the
locked version. `switchCliVersion` does the same on the TypeScript side by
dropping `save` and passing the locked `pmVersion`.

`exact_version` kept the `+<algorithm>.<hash>` build corepack appends. A
`devEngines.packageManager` version carrying one was recorded as the version
to look for, but the registry resolves that spec without the build, so the
recorded `version` never equalled it: the entry was rewritten byte-for-byte on
every install and rejected on every frozen one. The build is now dropped where
the pinned version is read, as `parse_package_manager` already does for the
`packageManager` field; the `specifier` still records the pin as the manifest
writes it, which is what the TypeScript CLI writes.

The up-to-date fast path returns before the install pipeline, which is where
the install family records the pin — the pre-command block defers to it. A pin
added to a project whose dependencies are already installed therefore never
reached the lockfile. The fast path now gives way when the pin still has to be
recorded, and stays a fast path once it is.

The TypeScript CLI records the pin from its pre-command block and compares
versions with semver, so the last two divergences are Rust-only.

Related to pnpm/pnpm#14124.
2026-08-24 14:16:43 +02:00
Zoltan Kochan 35e31f2b6a fix(pm): drop the statically detected phantom compat entries (#14128)
The compatibility database gained ten dependency additions found by
static analysis of published packages (pnpm/pnpm#13970). The analysis
counted type-only imports, which name no runtime dependency: two of the
ten targeted `estree`, a package that does not exist on the registry
(pnpm/pnpm#13981), and the rest name packages the extended package needs
only to type-check, usually through an already-declared `@types/*`
counterpart.

Installing them is at best dead weight - every `@jest/types` install
pulled in yargs and istanbul-reports - and at worst breaks the
dependent, because each target was requested as `*` and resolved to the
newest release. `@typescript-eslint/types` gained a `typescript`
dependency, putting TypeScript 7 under `@typescript-eslint` 6, where
`ts-api-utils` fails with "Cannot read properties of undefined (reading
'Intrinsic')". `@eslint/eslintrc` gained an `eslint` dependency and
`@types/react-dom` a `react` one, each duplicating a runtime whose
consumers assume a single instance.

Restores `pnpm_compat_package_extensions.json` to the three curated
entries, matching the TypeScript CLI's `pnpmCompatPackageExtensions`
again, and keeps the `@yarnpkg/extensions` sync from the same commit -
that half tracks upstream's curated database and matches the
`@yarnpkg/extensions` version the TypeScript CLI installs. A test pins
that no entry may inject `estree` or a single-instance runtime.
2026-08-24 14:12:02 +02:00
Zoltan Kochan d0b71db3ec fix(config): classify confirmModulesPurge across pnpm versions (#14127)
The pnpm v11 workspace-setting validator classified confirmModulesPurge as unknown because this install-only option intentionally sits outside Config and WorkspaceManifest. Register it in the explicit untyped workspace-setting list so pnpm v11 no longer warns that the working setting was ignored.

Pacquet does not implement confirmModulesPurge, so it should continue warning that the key is ignored. Record the key in pacquet's cross-version setting table so its diagnostic names pnpm v11 instead of suggesting enableModulesDir.

Closes pnpm/pnpm#14125.
2026-08-24 14:10:58 +02:00
Zoltan Kochan 2f703cb8d7 fix(hoisted): keep injected peer variants as separate copies (#14126)
Under `nodeLinker: hoisted`, peer-resolution variants of an **injected directory dependency** (a `file:` snapshot) collapsed onto the first-seen variant, like registry peer variants do since #14039 (and the TypeScript CLI's long-standing `depPathByPkgId` mapping). With #14034, edges declared against a losing variant then resolve to the surviving copy.

For registry packages that dedup is intended: every variant unpacks the same tarball, and peers resolve through the ancestry either way. An injected directory dep is different — each `file:` variant is materialized as its **own copy of the local package with its own peer-resolved dependency set**; that is the point of injecting. Collapsing rewires every dependent of the losing variant onto the survivor's children.

### Symptom

teambit/bit's root-components e2e (`nodeLinker: hoisted`): two Bit root components pin `is-odd@1` and `is-odd@2` across injected copies of the same component (`comp2@file:comp2(is-odd@1.0.0)` / `(is-odd@2.0.0)`). The second root's copy materialized as a symlink into the first root's tree and resolved the wrong peer:

```
AssertionError: expected '1.0.0' to match /^2\./
```

### The fix

Exempt `file:` snapshots from the collapse in the shared identity function — `pkg_id` in pacquet, `getHoisterPkgId` in the TypeScript CLI. Every index over the hoist result (`pkg_locations_by_pkg_id`, `package_ids_by_pkg_id`, `pkgLocationsByPkgId`) derives its keys from that function, so the variants stay apart with no further changes.

The registry collapse stays untouched: it is what stopped the per-path walk from exploding on peer-variant-heavy lockfiles (the OOM #14039 fixed), and `file:` snapshots cannot explode that way — there is one per injected workspace package.
2026-08-24 14:10:32 +02:00
Zoltan Kochan afb4071811 chore(release): stop publishing @pnpm/exe from v12 (#14121)
Up to v11, `@pnpm/exe` was the build of pnpm that bundled a Node.js
runtime, so it ran where the JavaScript `pnpm` package could not. From
v12 the `pnpm` package is itself the native executable and needs no
Node.js, which left `@pnpm/exe` an equal-content copy of it, published
and dist-tagged for nothing.

generate-packages.mjs no longer emits the `pnpm-exe` wrapper dir, and
release.yml no longer packs or stages it. The per-platform
`@pnpm/exe.<target>` native packages are unchanged: they carry the
binaries the `pnpm` wrapper resolves.

update-latest.yml would have failed on the first v12 promotion, since it
unconditionally installs and dist-tags `@pnpm/exe` at the promoted
version. Both sites now pick the wrapper by major. The upgrade check
gates on the *starting* version so upgrading a v11 `@pnpm/exe` onto a
v12 release is still covered, and it gained `--allow-build=pnpm` because
from v12 the preinstall that swaps the placeholder bin for the native
one lives on the `pnpm` package -- without it that check would have
validated a placeholder.

No runtime code changes: both stacks' package-to-install selection
already converges on `pnpm` for major >= 12, the update notifier already
prints `pnpm`, and `@pnpm/exe` stays in the bin-resolver and global-alias
lists so v11-and-earlier installs keep working. Only the comments that
justified those behaviors with "published under both names" needed
correcting.
2026-08-24 02:39:51 +02:00
Zoltan Kochanandgithub-actions[bot] f33a1bdf9c chore: update dependencies, Node.js, pnpm, and GitHub Actions (#14120)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 01:33:19 +02:00
Zoltan Kochanandgithub-actions[bot] 2dad383d2e chore(release): pacquet 12.0.0-rc.9 (#14119)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
v12.0.0-rc.9
2026-08-24 00:45:04 +02:00
Zoltan KochanandClaude Opus 5 a390e363b8 fix: include sponsors in v12 release descriptions (#14118)
* fix: include sponsors in v12 release descriptions

The v11 release description comes from `pn make-release-description`, which
appends the sponsors table. v12 builds its description in the workflow by
dumping the pending changelog, so its release pages never showed sponsors.

Adds .github/release-sponsors.md — a checked-in fragment carrying the same
platinum and gold tables with release_notes attribution — and cats it onto
RELEASE.md in the Rust release job. A missing fragment warns rather than
fails; the sponsors table is not worth losing a release over.

The fragment is regenerated from pnpm.io's sponsors.json alongside the
READMEs and the v11 release text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: read the sponsors table from the shared fragment in v11 too

make-release-description inlined its own copy of the sponsors tables — 109
lines of HTML that had to be regenerated in lockstep with the READMEs. Now
that v12 reads a fragment, v11 can read the same one.

getChangelogEntry goes back to returning just the changelog section, and
writeReleaseText appends the fragment. A missing fragment warns and writes
the description without the table, matching the v12 job: the workflow falls
back to a diagnostic description when this script fails, so throwing here
would trade a missing sponsors table for missing release notes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: assert the sponsors fragment is appended exactly once

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 00:42:22 +02:00
Zoltan KochanandClaude Opus 5 d67254d677 docs: add Notion as a platinum sponsor (#14117)
* docs: add Notion as a platinum sponsor

Regenerated from pnpm.io's sponsors.json. Also picks up Latitude, which was
added to the website earlier but never propagated to the sponsor tables here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: put all three platinum sponsors on one README row

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: put all three platinum sponsors on one release notes row

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:34:23 +02:00
Zoltan Kochan a5effe488d feat(cli): report unmet peer dependencies from a resolving install (#14114)
The Rust CLI computed peer-dependency issues during resolution and then
dropped them on the floor: every call site passed `peer_issues_sink:
None`, and the only other consumer was a `tracing::warn!` the reporter
never shows. `strictPeerDependencies` was parsed and read by nothing.
`pnpm dedupe` was the one command that said anything, via a lockfile
walk of its own after the install.

An install that resolves now ends by applying `peerDependencyRules` to
the issues its resolution left behind and either warning once — the
same line the TypeScript CLI's reporter prints — or failing with
`ERR_PNPM_PEER_DEP_ISSUES` under `strictPeerDependencies`, after the
artifacts are written, the way `ERR_PNPM_IGNORED_BUILDS` already does.
Living at the shared install tail means `add`, `remove`, `update`,
`dedupe`, and `--lockfile-only` all get it, matching where pnpm places
the call in `_installInContext`. Dedupe's own walk goes away as
redundant.

An install that skips resolution — frozen, or an up-to-date lockfile —
still reports nothing, in both stacks. That asymmetry with `dedupe` is
the subject of pnpm/pnpm#14098 and is deliberate: peer issues are a
byproduct of resolution, so whatever ran one reports them, and
`pnpm peers check` is what answers for a tree nobody re-resolved.
Recomputing them on the frozen path would put a second, independent
implementation on the hot path of every CI install and trade an
install-vs-dedupe disagreement for an install-vs-install one. A test in
each stack pins the decision.

The verdict is read off the resolved lockfile rather than off the
resolver's own issue map, because pacquet's `ParentChain` is name-only
and would render every "Wanted:" entry with an empty version. That
makes the lockfile walk shared code, so it moves out of the `peers`
command into a new `pnpm-deps-inspection-peers` crate alongside the
rules filter and the issue renderer — pnpm's `peers-checker` and
`peers-issues-renderer` packages.

Closes pnpm/pnpm#14098.
2026-08-23 20:37:07 +02:00