Commit Graph
179 Commits
Author SHA1 Message Date
Zoltan Kochan d278429ba9 refactor: give the artifact blob digest one name and one guard (#14241)
Two leftovers from pnpm/pnpm#14189, both behaviour-neutral.

`artifactBlobDigest` was a pass-through around a private `blobId` that did
the actual work, so the digest had two names and the doc comment sat on
neither the definition nor every call site. There is now one exported
function; the three call sites that only wanted the integrity validated
call it and discard the result, as they did before under the other name.

The restore's store lookup tested `!downloaded.contains_key` and then the
download below tested it again, so a reader had to hold both in mind to see
that the lookup only runs when the bytes are not already in memory. Folding
the lookup into the download's own branch says it once. `blob_id`'s error is
propagated rather than swallowed by `if let Ok(..)`: the manifest validator
rejects a malformed integrity long before hydration, so this is unreachable,
and a silent fall-through to downloading is a worse way to express that than
a `?`.

Follow-up to pnpm/pnpm#14189.
2026-08-27 18:47:06 +02:00
Zoltan Kochan a83487c2f8 perf: reuse store content when restoring a remote build (#14189)
Reuse store content when restoring a remote build.

A remote side-effects restore downloaded every file of the artifact, then handed the bytes to the store, which addresses content by the very digest the artifact manifest already carries. A built package's files are mostly its own, and artifacts share files with one another, so most of what was transferred was content the store already had.

Ask the store first. `locateFileInStore` answers with the path it holds for a digest and mode, or nothing, and the restore only fetches what is missing. The store keeps executable and non-executable content apart, so the mode is part of the question.

Nothing is trusted that was not trusted before: the store addresses content by its hash, so a file it holds under an artifact's digest is that artifact's bytes.

The pacquet side already had the store helper this needs; its test pins the part that could silently drift — a digest or executable-bit rule diverging between the write side and the artifact side would not fail anything, it would just make the lookup always miss.

Follow-up to pnpm/pnpm#14171.
2026-08-27 18:11:52 +02:00
Zoltan Kochan e322d08690 fix(pnpr): reclaim orphaned shared artifact blobs (#14232)
Ambiguous object-store failures can commit a shared-artifact blob while returning an error. Conservatively retaining the reserved quota prevents limit bypasses, but can permanently charge storage that no committed envelope references.

Register each publication in the conditionally written quota document before it reads or creates immutable objects. When an ambiguous failure marks reclamation as necessary, let one replica acquire a token after all active publications drain. The token blocks new publications while the collector derives reachability from stored envelopes, removes unreferenced blobs, rescans physical usage, and atomically replaces the quota state. Preserve the retry marker when collection fails, retry transient cleanup failures from live requests, jitter conditional-write backoff, and retain the documented quiesced quota-reset procedure for registrations or tokens left by terminated processes.

Also synchronize the recursive-run bail summary regression with bounded marker waits so it deterministically exercises recording a task that was in flight when another task failed without risking a hung test.

On Windows, a competing `create_dir` can report `AccessDenied` while the prior lock directory is delete-pending. Retry that platform-specific transient error within the caller's wait and a one-second release budget, while continuing to report persistent permission failures.

Related to pnpm/pnpm#14220.
2026-08-27 16:55:59 +02:00
Zoltan Kochan bff7a17261 fix(pnpr): retain quota after ambiguous artifact writes (#14228)
Object-store write errors do not prove that a create-only write failed. A
backend can commit an artifact and then return a transport error, so releasing
that object reservation can make the quota counter smaller than physical
storage.

Retain quota for every attempted object on non-conflict errors while still
releasing the reservation for later, unattempted objects. This deliberately
prefers conservative overcounting to allowing the storage limits to be
exceeded. Document the recovery procedure and cover both pre-commit and
post-commit failure paths.

Follow-up to pnpm/pnpm#14222.
2026-08-27 15:29:54 +02:00
Zoltan Kochan 6632c9acc1 feat(pnpr): scale shared artifact storage horizontally (#14222)
Promote the signed shared-artifact service from a resolver sub-feature to an independent pnpr surface, allowing artifact-only deployments.

Use the configured object store for immutable artifact blobs and envelopes. Coordinate the durable quota counter with conditional writes so concurrent replicas cannot lose updates, while retaining the existing local cache layout and a small local quota lock for filesystem deployments. Apply the protocol's variant bound while resolving instead of serializing publishers.

Closes pnpm/pnpm#14220.
2026-08-27 15:01:52 +02:00
Zoltan Kochan 0605218abb refactor(pnpr): keep the parsed config free of a live object store (#14214)
`HostedStoreConfig::S3` held a constructed `Arc<dyn ObjectStore>`, built during
`Config::from_yaml_str`. It now holds the parsed `S3Settings`, and
`Storage::new` builds the client.

A parsed config stays plain data. Nothing in
`Config` holds a live object-store handle a reader has to reason about, which
matches `BackendConfig` immediately below it in the same file. And an
embedder-supplied store gets its own variant instead of being disguised as a
parsed one.
2026-08-27 12:13:22 +02:00
Zoltan Kochan 3d83d1c601 refactor(pnpr): give the tilde-registry rule one definition (#14213)
`~<name>` parsing appeared sixteen times: fifteen inline in the package
dispatch functions and once inside the `TargetRegistry` extractor, each
spelling out `strip_prefix('~').filter(|r| !r.is_empty())`. The rule has two
edges worth stating deliberately — a bare `~` names no registry, and a `~`
inside a segment is part of a package name — and sixteen copies is sixteen
chances to state one of them differently.

Name it once and route every site through it, including the extractor that
already documented the same contract in prose. The follow-up pnpm/pnpm#14198
noted these sites but left them: they parse a captured segment inside
multi-segment dispatch rather than being handler twins, so the extractor could
not absorb them — but the underlying rule is the same one.

A unit test pins both edges, which nothing did before: the extractor's test
covers the route-level 404 for a malformed prefix, not what the dispatch
functions do with a bare `~`.
2026-08-27 03:11:06 +02:00
Zoltan Kochan 13d32215d4 refactor(pnpr): extract pnpr-osv, pnpr-search, and pnpr-shared-artifacts (#14212)
## Summary

The last structural step of the crate split from pnpm/pnpm#14191.

Three modules now depend on nothing else inside `pnpr` — everything they touch
is already its own crate — so each becomes one:

| crate | contents | tests |
| --- | --- | --- |
| `pnpr-osv` | the OSV advisory index pnpr consults to refuse vulnerable versions | 14 |
| `pnpr-search` | the local `/-/v1/search` scan over hosted packages | 7 |
| `pnpr-shared-artifacts` | the shared build-artifact store: reservations, blobs, usage accounting | 23 |

That leaves **`server` and `resolver`** in `pnpr` itself, which is where they
belong — the HTTP surface and the install accelerator behind it, and the only
pair left that genuinely references each other.

Twelve items the boundaries caught as reaching across become `pub`; seven gained
`#[must_use]` as newly public API.

### The whole split, end to end

`pnpr` went from one 42k-line crate to **fourteen**, each named for what it
holds:

```
pnpr-error → pnpr-package-name → pnpr-registry → pnpr-policy
           → pnpr-config → { pnpr-auth, pnpr-route, pnpr-upstream, pnpr-osv }
           → pnpr-storage → { pnpr-search, pnpr-shared-artifacts }
           → pnpr  (server + resolver)
```

Layering is now checked by cargo rather than by convention.
2026-08-27 02:40:31 +02:00
Zoltan Kochan 82e9ae5eb0 refactor(pnpr): extract pnpr-storage (#14210)
## Summary

Continues the crate split from pnpm/pnpm#14191, after pnpm/pnpm#14202,
pnpm/pnpm#14204 and pnpm/pnpm#14207.

`storage`, `s3`, `streaming`, `journal` and `publish` move together as **one**
crate. That is not a shortcut — the graph says they are one subsystem:

```
storage → journal → publish → streaming → storage      (a 4-cycle)
s3 ↔ storage                                            (another)
```

Splitting them further would mean inventing seams the code does not have. The
crate they form is coherent on its own terms: where packages live, how bytes
get into that store, and the journal that makes a publish survive a crash.

The two thin edges are `Storage::publish_journal()` (a convenience constructor)
and `streaming`'s use of `TarballWrite` — either could be cut, but doing so to
manufacture crate boundaries would make the code worse, not better.

### What the boundary caught

Fifteen items become `pub`, plus the `HostedPackumentForUpdate` fields the
server reads — I had initially guessed that type was crate-internal and the
compiler corrected me.

`HostedBackend` deliberately stays `pub(crate)`: the trait is implemented and
dispatched only inside this crate, so it is the one part of the backend
contract that is nobody else's business. The re-export is split by visibility to
say so.

One over-publish caught on review of my own diff: `revision_ref_owner` exists
twice — a method and a free function — and the compiler only needed the method.
The free function is back to private.

Four doc links pointed at `crate::serve`, `crate::s3` or private items that no
longer resolve from the new crate root; those are plain code spans now.

`just test-pnpr` covers eleven crates at **733 tests**, unchanged. All feature
combinations build and `cargo deny check` passes.
2026-08-27 02:05:02 +02:00
Zoltan Kochan 54957d73c5 refactor(pnpr): extract pnpr-auth, pnpr-route, and pnpr-upstream (#14207)
## Summary

Continues the crate split from pnpm/pnpm#14191, after pnpm/pnpm#14202
(the bottom layer) and pnpm/pnpm#14204 (`pnpr-config`).

The three modules that depended only on `pnpr-config` become crates. They are
independent of each other, so this is one *tier* rather than a chain:

| crate | contents | tests |
| --- | --- | --- |
| `pnpr-auth` | the user and token stores: htpasswd, SQLite, networked SQL backends | 43 |
| `pnpr-route` | classifies a fetch route public or private, so a shared resolution never leaks private data | 25 |
| `pnpr-upstream` | the upstream registry proxy client: packument/tarball fetches, cache validators, circuit breaker | 31 |

Nine items the boundaries caught as reaching across become `pub`, and seven
gained `#[must_use]` as newly public API. Three doc links pointed at items that
are private or at a crate no longer in scope; they are now plain code spans
rather than links.

## Two things the boundaries exposed, both silent

**`pnpr-auth`'s backend features did not forward.** They enable its own
`libsql`/`sqlx`, but the matching `RegistryError` variants are gated in
`pnpr-error`, so `cargo check -p pnpr-auth --features backend-libsql` failed to
compile with 57 errors. It only ever worked *through* `pnpr`, which forwards to
both. Now `pnpr-auth`'s features forward to `pnpr-error` as well.

**The justfile's two test recipes had drifted.** `test-pacquet` runs the
workspace minus `pnpr` and `pnpr-fixtures`; `test-pnpr` runs the rest. The
intent is clear — pacquet crates in one job, pnpr crates in the other — but the
excludes were written when `pnpr` was one crate. Every new `pnpr-*` crate has
been landing in the *pacquet* job, and building bare there: `pnpr-auth` would
have reported 28 tests instead of 43, **dropping its 14 libsql backend tests
from CI without failing anything.**

Both recipes now name every `pnpr-*` crate: excluded from `test-pacquet`,
selected together in `test-pnpr` so cargo's feature unification gives them the
backends `pnpr` itself defaults to. `just test-pnpr` covers all ten crates at
**732 tests**, and `just test-pacquet` is unchanged at 8606.

That drift is worth knowing for the remaining extractions — it is the kind of
coverage loss that shows up as a green build.
2026-08-27 01:31:10 +02:00
Zoltan Kochan 7237adea49 refactor(pnpr): extract pnpr-config (#14204)
Extracting config was blocked by a cycle: config reached into s3 for
S3Settings and build_s3_store, s3 into storage, and storage back into config.

Neither of those items belonged in s3. S3Settings is the deserialized YAML
s3: block — config's own data, filed under the backend that consumes it. And
build_s3_store builds an AmazonS3Builder from that block; it touches
object_store but nothing in our s3 module, so it sits just as well next to
the settings it reads. Moving both leaves config with no dependency on s3 at
all and the cycle gone, with no behavior change: the store is still built
once at config-load time, which is what makes constructing the storage layer
infallible.

config is then a leaf, so it becomes pnpr-config, carrying config.yaml (the
bundled default it include_str!s) with it.

Two items the boundary caught as reaching across: RedactedHeaders and
UpstreamConfig::with_defaults, both now pub. with_defaults also gained
#[must_use], the same public-API lint consequence the last extraction saw.

The suite is unchanged at 714, of which 144 now run under pnpr-config.

Related to pnpm/pnpm#14191.
2026-08-27 00:32:48 +02:00
Zoltan Kochan 49ad901d9e refactor(pnpr): extract the bottom layer into four crates (#14202)
pnpr was one 42k-line crate, though pnpr/AGENTS.md has always specified the
pnpr-<short-name> convention for registry-only crates and none existed.
Start at the bottom of the module graph, where the boundary is already
closed: error depends on nothing, package_name on error, registry on
package_name, policy on registry, and no member reaches back up.

Those four become pnpr-error, pnpr-package-name, pnpr-registry, and
pnpr-policy — one crate each, named for what it holds. The workspace has 77
Rust crates and not one named core, common, shared, or utils; such a name
describes a position in the graph rather than a subject, which is what lets
it collect anything. AGENTS.md now says so, so the next extraction doesn't
have to relearn it.

Consumers name pnpr_error:: and friends instead of crate::, so the layering
is checked by cargo rather than by convention. The backend features forward
to pnpr-error, which is the crate that names those backends.

Three things the crate boundary surfaced. AccessList::new becomes pub — the
one item reaching across. Three methods gained #[must_use], since pedantic
public-API lints apply where pub(crate) had exempted them. And every
first-party pnpr* crate needs a deny.toml exception plus a licenses.clarify
block, because their PolyForm license text scores below cargo-deny's
confidence threshold; without both, Cargo Deny fails the crate as unlicensed.

All four feature combinations build, cargo deny check passes, and the suite
is unchanged at 714 — 65 of which now run under the four new crates.

Related to pnpm/pnpm#14191.
2026-08-26 23:51:05 +02:00
Zoltan Kochan 03b8525306 refactor(pnpr): read the registry prefix with an extractor (#14198)
Every account and staging route answers both bare and under /~<name>/, and
each pair was two handlers: the prefixed twin validated the segment, then
called the same body. Twelve twins, each repeating is_tilde_prefix or
tilde_registry and its own not_found fallback.

Add a TargetRegistry extractor that reports the addressed registry, or None
on the path-less base, and rejects a {prefix} segment that is not a
well-formed ~<name>. Both registrations now point at one handler, which says
it is prefix-aware by taking the extractor.

RawPathParams is what makes this work: it reports only what the matched
route captured, so an absent prefix distinguishes the bare registration from
a prefixed request rather than being guessed from the URI. The handlers that
also capture a parameter gain a named path struct — the prefixed
registration captures two segments, which a single-value Path refuses.

The 404-on-a-non-tilde-segment half of the contract had no test; the guard
existed for it, and centralizing it is the moment to pin it. Added one
covering a plain segment and a bare ~.

Related to pnpm/pnpm#14191.
2026-08-26 22:59:25 +02:00
Zoltan Kochan 195c6974f6 refactor(pnpr): lift osv out of the resolver (#14196)
The OSV advisory index sat at resolver/osv.rs, but it is not a resolver
concern: server consults it independently to block a vulnerable publish and
to filter dist-tags, reaching it as crate::resolver::OsvIndex through a
module it otherwise has no business addressing.

The module is also a leaf — it imports crate::Config and crate::error and
nothing else from the crate — so nesting it bought no cohesion.

Move it to the top level. Both files move byte-for-byte; the rest is import
rewiring, and server now names crate::osv for what it was already using.

Sequenced before the crate split so that split has an obvious owner for the
module.

Related to pnpm/pnpm#14191.
2026-08-26 22:32:26 +02:00
Zoltan Kochan 958a6aa14e refactor(pnpr): dispatch the hosted store through a trait (#14195)
The hosted store was a two-variant enum whose impl block was 193 lines of
match { Fs => …, S3 => … } across 36 arms — a hand-rolled vtable for a
choice the rest of the registry never needs to see. Adding a third backend
meant editing eighteen methods.

Replace it with a HostedBackend trait, implemented by the filesystem Store
and by S3Store, held as Arc<dyn HostedBackend>. This is the shape the auth
layer already uses for UserBackend and TokenBackend, so it makes the two
consistent rather than introducing a pattern.

Each impl now owns the adaptation the dispatch used to hold: the fs side
wraps its file handle into a stream and reports an unconditional Written,
the S3 side maps its object version onto the CAS token and keeps the staged
tmp file on a conflict.

Two things stop naming the backend. HostedPackumentVersion::Fs becomes
Unversioned — it describes a store with no compare-and-set, which is what
callers actually branch on. And publish_journal picked its root by matching
on the variant; that becomes local_scratch_root, so S3Store now carries the
cache root it already derived staging_dir from. The journal path is
unchanged on both backends.

The trait and the types in its signatures live in storage::backend, so what
a backend must provide is stated in one place instead of being implied by an
enum's arms.

Related to pnpm/pnpm#14191.
2026-08-26 22:04:56 +02:00
Zoltan Kochan a5e479132a refactor(pnpr): render errors through IntoResponse (#14192)
RegistryError had no NotFound variant, so every path answering a bare 404
built the response itself. That forced twelve helpers to carry HTTP
responses as their error type — Result<T, Box<Response>> — and pushed 27
Box::new(error_response(&err)) wraps onto their callers. It also split
validate_publish_doc in two, purely so the routing half could `?` on
Box<Response> while the attachment half kept a plain RegistryError.

Add the variant and move response rendering into
impl IntoResponse for RegistryError, which absorbs the tracing that the
free error_response function did. Every Box<Response> signature becomes
Result<T, RegistryError>, the wraps become plain `?`, 120 error_response
call sites become err.into_response(), error_response is deleted, and the
two publish-validation halves rejoin.

NotFound renders as the bare 404 Not Found the old helper emitted, so the
wire is unchanged; not_found() now builds it from the variant to keep one
source of truth, and a new test pins the rendering. 404s newly log at
debug, alongside 401 and 403 — below the default level, and the statuses a
probing client provokes routinely are not worth a warn each.

Related to pnpm/pnpm#14191.
2026-08-26 21:42:51 +02:00
Zoltan Kochan 959be082fe refactor(pnpr): split oversized modules (#14190)
Split four oversized pnpr modules along the seams already implicit in
them, with no behavior change.

config.rs sheds its upstream-registry half into config/upstream.rs.
resolver.rs sheds resolution-cache storage (resolver/cache.rs), the
pre-resolve reject guards (resolver/request_validation.rs), and ndjson
frame construction with its OSV/tarball routing (resolver/wire.rs).
server.rs sheds router assembly (server/routing.rs), authorization
(server/authentication.rs), publish and staging (server/publishing.rs),
and unpublish/dist-tag/packument mutation (server/package_mutation.rs).

shared_artifacts.rs is restructured rather than moved: reconcile_pending_usage
was one ~130-line body whose nested blob-lock loop escaped via a labelled
continue. It is now lock acquisition, commit detection, per-file
reconciliation, and adoption as separate functions, with the labelled
continue replaced by an Option-returning lock acquirer and three copies of
the same metadata match collapsed into stored_file_has_size.

Two consequences of the moves are fixed on top: intra-doc links whose
targets left the module are qualified against their new home (the Doc CI
gate documents private items, so it sees these), and the #[cfg(test)] use
re-exports the parent modules kept for their test files are dropped in
favor of test imports that name the owning submodule, which the dylint
perfectionist rule requires.

The pnpr suite passes unchanged at 710 tests.
2026-08-26 16:51:17 +02:00
Zoltan Kochan 677a3e11d0 chore(release): pacquet 12.0.0, pnpr 0.1.0-alpha.8 (#14188) 2026-08-26 16:36: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 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 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 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 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
Zoltan Kochanandgithub-actions[bot] 726d6b4a04 chore(release): 11.23.0 (#14111)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 15:47:36 +02:00
Zoltan Kochan 5f88187f5e fix(update): apply updateConfig.ignoreDependencies to what is reported (#14091)
Finishing the test porting tracked in pnpm/pnpm#12101 turned up two places
where pacquet's `update` still differed from pnpm's.

`updateConfig.ignoreDependencies` reached the update itself but neither of
the two places that report what could be updated: `pnpm outdated` listed an
ignored dependency, and `pnpm update --interactive` offered it for
selection. Both now drop the names the setting matches, the way pnpm hands
`ignoreDependencies` to `outdatedDepsOfProjects` from its `outdated` and
interactive-update paths alike.

A recursive `pnpm update --latest --depth 0 <selector>` that matched no
project's dependencies exited 0 having done nothing. pnpm lets `--latest`
return quietly only on the single-project path; the recursive one throws
NO_PACKAGE_IN_DEPENDENCIES once no project is left to mutate, whatever
`--latest` says, and so does pacquet now.

The harness gained the two seams the remaining upstream tests needed:

- `pnpr_fixtures::set_dist_tag` moves a dist tag in a built storage tree,
  and `CommandTempCwd::add_mocked_registry_with_own_storage` gives a test a
  registry of its own to move it in. The fixture registry otherwise serves
  `latest` as the highest published version, which cannot express "the
  newer version was published after the install" — the setup behind every
  upstream `--latest` case. The packument the last command cached is
  dropped with the tag, so the next one resolves against the move.
- `UpdatePrompt` decides how `--interactive` asks the user. The tests
  answer it from a script instead of a terminal, the way the upstream suite
  mocks `@inquirer/prompts`; `dialoguer` wants a TTY and offered no seam.

Ported with them: the five remaining `recursive.ts` cases, five `update.ts`
cases, three from `interactive.ts`, the `issue-7415.ts` regression at the
level the seam reaches (the outdated collector walking past a resolution
that names no version), and the `outdated` ignore-dependencies case.
`@pnpm.e2e/multi-version-b` stands in for `@zkochan/async-regex-replace`,
which the fixture registry does not carry.

Closes pnpm/pnpm#12101
2026-08-23 03:25:36 +02:00
Ngo Quoc VietandZoltan Kochan 9e49bbf7b6 fix(update): preserve prerelease range operators (#13900)
When update resolves a prerelease dependency, preserve the range style already
declared in the manifest in both the TypeScript and Rust implementations.
Newly added prerelease dependencies remain pinned exactly.

The TypeScript calcVersionRange serves plain update, re-add of a declared
dependency, and update --latest, so all three preserve the style. pacquet
routes those paths through a new calc_version_range helper mirroring it,
replacing the duplicated decision (PackageVersion::serialize is removed).

Add shared-fixture unit and end-to-end coverage for the prerelease update path.

Fixes pnpm/pnpm#7002.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-08-21 02:10:07 +02:00
a8ff38dde6 chore(release): pacquet 12.0.0-rc.7, pnpr 0.1.0-alpha.7 (#13971)
* chore(release): pacquet 12.0.0-rc.7, pnpr 0.1.0-alpha.7

* chore(cargo): bump h2 to 0.4.16 for RUSTSEC-2026-0258

RUSTSEC-2026-0258 (h2 unbounded empty DATA frames, low-severity DoS) was
published to the advisory database on 2026-08-17 and fails `cargo deny
check` on every branch that touches Cargo.lock.

The workspace locks two h2 majors. h2 0.4.13, which backs pnpr's own
HTTP/2 server through axum 0.8 -> hyper 1.x and pacquet's outbound
requests through reqwest, moves to the patched 0.4.16; its dependency
set is unchanged, so this is a lockfile-only edit.

h2 0.3.27 has no fix: the advisory patches only `>= 0.4.16`, 0.3.27 is
the final 0.3 release, and the copy is pulled by libsql's gRPC client
stack (libsql -> tonic 0.11 -> axum 0.6 -> hyper 0.14) that the newest
stable libsql, 0.9.30, is still pinned to. That client only talks to the
operator-configured libsql/sqld backend, never to untrusted registry
clients, so it is ignored in deny.toml until libsql 0.10 leaves
prerelease.

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

* docs(changeset): correct the registries release notes

Two review findings on the release PR:

The registry-rename note claimed `node_modules/.modules.yaml` keeps its
`registries` field, which contradicts another entry in the same release
saying the field is dropped. The removal is the true behaviour: the
writer deletes the key on every rewrite and the Rust `modules-yaml`
crate has no such field. The rename genuinely changes nothing on disk,
so the two `.modules.yaml` mentions are dropped from that paragraph
rather than restated -- the removal already has its own entry.

The catalog `outdated` note wrote a bare `outdated --compatible`; release
prose spells out the published executable, so it is now
`pnpm outdated --compatible`.

Both the pending changesets and the generated pacquet changelog carry
the prose, so both are corrected; fixing only the changelog would let
the release tooling reintroduce the old text on the next regeneration.

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:32:51 +02:00
Zoltan Kochan 6261b7f388 fix(pnpr): resolve in the client's resolutionMode (#13958)
A resolve request carried the client's `minimumReleaseAge` and
`trustPolicy` but not its `resolutionMode`, and `intern_config` never
set `config.resolution_mode`, so every pnpr-delegated resolve ran on the
server's `highest` default. A project configured for `time-based` or
`lowest-direct` got a lockfile pinning the highest satisfying version of
everything, with nothing to show that the setting had been dropped.

The mode now travels with the request and reaches the interned config,
which is what `PickPolicy::from_config` reads on the server side. It
also joins both cache keys — the interned-config key and the resolution
cache key — so two modes can neither share a config nor replay each
other's resolution.

`VerifyLockfileOptions` deliberately does not carry it: verification
judges an existing lockfile against the client's policy and picks no
version.
2026-08-17 13:31:24 +02:00
Edward LongandZoltan Kochan c5fadf15d6 fix(pnpr): forward client resolver settings (#13730)
Source the fallback for an omitted setting from the config's own
defaults rather than a hardcoded copy of them, and restore the
rationale for gating the input-lockfile fallback on a frozen request.

Send the three settings inline like every other optional field of the
resolve request: the server reads an explicit null the same as an
absent key, so the manual inserts bought nothing.

Give the resolver settings their own forwarding test instead of
folding them into the verification-policy one.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-08-17 10:45:33 +02:00
Zoltan Kochan c9b8632ea6 feat(config): declare each registry once in the registries setting (#13942)
Registries do not all lay out tarball URLs the way the npm registry does.
JFrog Artifactory repeats the scope in a scoped package's tarball filename
(`@acme/widget/-/@acme/widget-1.0.0.tgz`) where npm strips it. pnpm cannot
rebuild such a URL, so it writes it out for every scoped package instead of
omitting it, and the lockfile carries a host-specific URL per dependency.

pnpm had no place to record such a fact, because it had no place to
describe a registry at all — only three ways to name one. `registries`
mapped a scope to a URL, `namedRegistries` mapped a bare-specifier prefix
to a URL, and neither could carry anything else.

`registries` now declares a registry once, keyed by its URL, with every
fact about it in the entry: a `serverType`, the `scopes` routed to it,
and the `prefix` it answers to. `serverType` has three states:

  undeclared  strict; only the exact canonical URL is reconstructible
  npm         also serves the percent-encoded scoped path
  artifactory repeats the scope in the tarball filename

registry.npmjs.org resolves to `npm` as a built-in, so its behavior is
unchanged and the old hostname check becomes that one default rather than a
special case in the predicate. `npm` cannot be the default: asserting
npmjs-compatibility is a claim only the operator can make.

The URL is the key because every fact in an entry is a fact about that
server. Keying the layout by scope would bind it to whoever the scope
currently points at, so two developers whose scope resolves differently
would write lockfiles that disagree about which URLs may be omitted.
`scopes` and `prefix` are routes to the registry and are inverted at
config-read time into the two lookups the rest of pnpm already queries,
leaving the resolver, installer, and lockfile layers untouched and the
precedence chain (builtin < .npmrc < yaml < `_auth` < CLI) unchanged.

The layout is declared, never inferred. Sniffing the registry URL cannot work:
a virtual repository serves both layouts at once, depending on whether each
package was synced from upstream or published locally, so no registry-level
signal — route, response header, or probe — decides it. Declaring it also
keeps a wrong guess a fixable misconfiguration instead of a silent breakage.

`serverType` feeds a single URL builder that both sides of the lockfile use:
the writer omits a tarball URL only when the builder reproduces it, and the
reader rebuilds it with the same call. They therefore agree by construction,
so pnpm never has to assume a registry serves some second URL as well.

The setting lives in pnpm-workspace.yaml rather than .npmrc because the
lockfile depends on it: one developer omitting URLs that another reconstructs
differently would break a frozen install. A `serverType` in the global
config.yaml is ignored for the same reason, while the routes declared
alongside it are kept. Credentials are rejected there — the file is
committed — and still belong in .npmrc. The registry URL is the map key, so
the request-destination env gate applies to keys as well as values.
Credentials and unknown fields are refused after parsing, since a parse
error renders the offending source line verbatim.

A map whose values are all strings is the older `<scope>: <url>` shape and
is still read as one. Mixing the two shapes in one map is refused, and so is
a URL-keyed entry written as a string. `namedRegistries` is deprecated in
favor of `prefix` and is read only for prefixes `registries` does not
declare; a prefix stays singular because it is the registry's identity in a
lockfile dep path.

An entry that routes nothing to itself and matches no configured registry is
reported as a warning rather than silently ignored; it is a warning and not
an error because a shared config dependency can legitimately describe
registries a given project does not use.

Config dependencies and pnpr-server-mode resolution pass no server type; in
both, the writer and the reader share that default, so they stay consistent.

Closes pnpm/get-npm-tarball-url#16. Supersedes pnpm/pnpm#13920.
2026-08-17 01:28:21 +02:00
Zoltan Kochan fe8ec8fbec refactor(pacquet): rename the Rust crates from pacquet- to pnpm- (#13948)
The Rust CLI ships as `pnpm`; `pacquet` survives only as the in-repo
package identifier that keeps the npm wrapper from colliding with the
TypeScript CLI's name. Carrying that identifier into all 75 Cargo crate
names made every `use` statement and every `cargo nextest run -p ...`
spell a name users never see. This renames the crates to the product
they belong to.

Mechanical, name-only: package/lib/bin names, the root
`[workspace.dependencies]` keys, every dependency entry, and every Rust
path reference. All the crates are `publish = false`, so nothing about
the released artifacts changes and no changeset is needed.

Also updated the tooling and docs that spell crate names: the `codecov`
cargo alias, the `registry-mock` just recipe, the release workflow's
`-p` targets and `libpnpm_napi.*` artifact paths, the `libpacquet` ->
`libpnpm` cspell dictionary entry, and the crate names quoted across
`AGENTS.md`, `CONTRIBUTING.md`, `CODE_STYLE_GUIDE.md`, and the plans.

Two spots needed more than a token swap. The five insta snapshot files
carry the crate name in their filename, since insta derives it from the
module path, so they are renamed too — otherwise the tests fail with
"snapshot not found" rather than a diff. And the private module in
`pnpm-lockfile-verification` that mirrors the npm verifier's violation
codes is named after the crate it shadows, so it moved with it.

Two knock-on fixes: the shorter names let rustfmt fit three macro
invocations onto one line, where `perfectionist::macro_trailing_comma`
rejects the trailing comma that was legal while they were wrapped; and
the `libpacquet` cspell entry became `libpnpm` for the NAPI addon's
shared library.

Left alone deliberately, none of which are crate names: the product name
in prose and in the changeset package identifier, the `--pacquet`
benchmark flag and testbed IDs, test helpers and temp-dir prefixes
(`pacquet-test-*`, `../pacquet-store`), the released changesets that
quote error codes shipped versions actually emitted, the
`.github/workflows/pacquet-*.yml` filenames, and the
`@pnpm-private/pacquet-registry-mock-launcher` npm package.
2026-08-16 23:18:23 +02:00
Zoltan Kochan 494f21d39b perf(pacquet): overlap and shrink cold-install verification, linking, and metadata (#13934)
Cold-cache installs spent most of their non-download time on
serialized verification, a post-download link pass, and re-fetched
metadata. This change overlaps or removes each:

- The fresh-resolve path's pre-resolve verification of the existing
  lockfile runs as a spawned gate (LockfileVerificationGate)
  concurrent with resolution and materialization; its verdict still
  gates bin linking, dependency builds, and the lockfile save.
- Verification metadata fetches queue in a new BACKGROUND permit
  class served only when no resolution-gating fetch waits.
- Cold virtual-store slots link in chunks of 32 between download
  completions instead of one serial pass after the last download.
- For entries whose canonical tarball the frozen path downloads
  anyway (existence fail-closed by the fetch), the minimumReleaseAge
  gate accepts a package-level Last-Modified HEAD probe in place of
  the abbreviated-packument body; pnpr now serves Last-Modified on
  packuments to answer it. Probe failure falls back to the metadata
  chain.
- The current lockfile parses on a blocking task while the wanted
  lockfile parses on the install task; both emit TRACE phase logs.

On the pnpm/benchmarks harness (emulated 50ms RTT / 200 Mbit link),
the cold CI install drops 4.46s -> 3.42s, the update row
2.69s -> 2.08s, and the warm-cache restore rows roughly halve, with
no change to the up-to-date fast paths.
2026-08-16 12:18:17 +02:00
Zoltan Kochanandgithub-actions[bot] 93fcba4224 chore(release): 11.22.0, pacquet 12.0.0-rc.6 (#13926)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 18:50:30 +02:00
Zoltan Kochanandgithub-actions[bot] e6f2249aba chore(release): pacquet 12.0.0-rc.4, pnpr 0.1.0-alpha.6 (#13868)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-12 16:12:36 +02:00
Zoltan Kochan d96974d914 fix(catalogs): honor a wanted version the catalog entry already resolves past (pacquet) (#13853)
A version named on the command line reached the lockfile only through the specifier written to
the manifest. A cataloged dependency writes `catalog:` there and keeps its version in the catalog
entry, so when that entry was a range covering the wanted version but resolved to another one,
the request was dropped in silence — `add <pkg>@<version>` wrote nothing at all, and `update
<pkg>@<version>` re-resolved to the highest version in range rather than to the one asked for.

Install grows a preferred-versions overlay, layered onto the seed built from the lockfile and
manifests. `add` and `update` put a version named on the command line there, weighted above the
lockfile's own pin so the re-resolve lands on it. `add` also withholds that package's pin and
skips the frozen shortcut, which would otherwise hold: its manifest specifier doesn't change when
the version behind a `catalog:` reference does, so nothing looked stale enough to resolve.

Every other install passes no overlay and is unaffected.

Related to pnpm/pnpm#13715.
2026-08-12 13:26:47 +02:00
bankiandZoltan Kochan 14d7ac7b57 fix(sbom): exclude platform-incompatible optional packages (#13710)
`pnpm sbom` walked every package in the lockfile, so optional platform-specific
dependencies that cannot be installed on the current platform (e.g. the native
`@rolldown/binding-*` variants for other operating systems) were emitted as
components even though they are never fetched and their metadata cannot be
resolved from the store. Those components appeared without a license, while
`pnpm licenses` — which filters such packages with the installability check
before reading metadata — reported them correctly.

Apply the same platform filter in the SBOM component walk of both stacks. It
runs only when metadata is read from the store; `--lockfile-only` keeps
describing the whole lockfile graph, which is platform-independent by design.
Only an optional dependency is filtered: an incompatible non-optional package
is still installed, so it still has metadata and still belongs in the document.

The verdict is read through `checkPackageInstallability`, the side-effect-free
variant, rather than `packageIsInstallable`: the latter emits a
`pnpm:install-check` warn and a skipped-optional-dependency event per filtered
package, and a command that only reads a lockfile must not report skips as if
an install were happening. pacquet's filter is silent for the same reason.

On the Rust side the check lives in `pacquet-package-is-installable` as
`platform_is_supported_with_inference`, shared by `pnpm sbom` and
`pnpm licenses` instead of being written out twice.

The license resolution itself was already shared between the two commands
(`@pnpm/deps.compliance.license-resolver`); the divergence was in which packages
are walked, which is why this adds a filter to the SBOM walk rather than reusing
the whole `licenses ls` implementation. That keeps SBOM generation free of extra
disk or network I/O and covers all optional native bindings generically.

Closes pnpm/pnpm#13683

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-08-11 14:34:42 +02:00
YES!HYUNGSEOKandZoltan Kochan a30a1fa60d fix(pnpr): validate and key forwarded catalogs (#13632)
Include forwarded catalogs in pnpr's resolution cache identity so requests with different catalog mappings cannot reuse each other's resolver output.

Route catalog URL values through the existing forwarded-input validation, applying the same destination allowlist and inline-credential rules as dependency specs, overrides, and lockfile tarballs.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-08-10 22:02:58 +02:00
Zoltan Kochan 9463e98c1e fix(pnpr-fixtures): replace a storage tree that lost its completion marker (#13749)
The generated registry-fixture storage is published by renaming a freshly
built temp tree onto `target/pnpr-fixtures/storage/<fingerprint>`. A rename
onto a non-empty directory fails with ENOTEMPTY, which the publisher treated
as a lost race and forgave only when the target carried the `.complete`
marker; anything else aborted the process.

A marker-less tree at that path is not a lost race, and it does happen: a
half-finished publish, or a CI build cache that restores `target/` without
the marker. Nothing may read such a tree, and nothing could ever repair it,
so every test process that touched the fixtures died with "publish generated
registry fixture storage: Directory not empty" until the cache rolled over.

Move the unusable tree aside instead — atomically, so exactly one publisher
claims it — and publish over the space it frees.
2026-08-10 10:57:48 +02:00
Zoltan Kochanandgithub-actions[bot] 8adb97ed05 chore(release): 11.21.0, pacquet 12.0.0-rc.2 (#13742)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 16:02:53 +02:00
Zoltan Kochan 7e189d6941 fix(package-manager): persist loose-mode minimumReleaseAge picks to the exclude list (#13708)
A fresh resolve that lets immature versions through under
minimumReleaseAgeStrict: false now appends them to
minimumReleaseAgeExclude in pnpm-workspace.yaml, matching the TypeScript
CLI's loose-mode auto-persist. Without the entries, the committed
lockfile failed verification on the next frozen install with
ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION (e.g. in CI).

The new Install.persist_policy_excludes flag gates the write: it is set
for the user-facing resolving commands (install, add, update with
--save, dedupe) and off for embedder-driven installs (napi, pnpr
server) and commands that must not touch the workspace manifest,
mirroring which TypeScript commands route through the policy handlers.

Closes pnpm/pnpm#13687
2026-08-07 15:47:11 +02:00
Ayush SinghandZoltan Kochan e18a6ec699 fix(pacquet): resolve symlinks in POSIX shell shims before computing … (#13411)
When a POSIX shell shim is reached via an additional symlink layer (e.g. placing the shim on PATH via an external symlink), the POSIX shell sets `$0` to the path of the symlink itself. 

The previous logic `basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")` would thus resolve to the directory containing the symlink, breaking the relative traversal path into the `.pnpm` store.

This PR updates `SH_SHIM_HEADER` in the Rust `pacquet-cmd-shim` crate to follow symlinks recursively via a shell loop using `readlink` before deriving `basedir`. 

Upstream TypeScript package `@zkochan/cmd-shim` needs a matching change.

Closes pnpm/pnpm#13405

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-08-07 13:48:51 +02:00
Zoltan Kochan f31c7ca00a perf: prune newly ignored optional dependencies without resolution (#13482)
Changing ignoredOptionalDependencies previously forced dependency resolution
even when the change only added ignore patterns. Detect monotonic additions,
remove matching optional importer edges, and prune package snapshots that are
no longer reachable. Preserve packages that remain reachable through another
dependency path.

Use the same optimization in the TypeScript CLI and pacquet. Keep removals and
new exclusion patterns on the safe resolution path, and prevent pacquet from
using a pruned lockfile as the seed when ignored-optional settings differ.

Make current-version package-manager pin tests use a static local registry so
release commits can be tested before their version is published.

Related to pnpm/pnpm#13474.
2026-08-06 12:40:27 +02:00
Zoltan Kochanandgithub-actions[bot] ebc48abdc5 chore(release): 11.20.0, pacquet 12.0.0-beta.4 (#13608)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-03 15:48:54 +02:00
Zoltan Kochan c2b7cfb930 feat(napi): add returnListOfDepsRequiringBuild install option (#13532)
The napi install result populated depsRequiringBuild from the ignored-
builds accumulation only, so an embedder that allows dependency builds
to run (Bit) always received an empty list and wiped its recorded
bit.depsRequiringBuild lockfile block on every install.

Mirror the TypeScript engine's returnListOfDepsRequiringBuild option:
when set, the fresh-resolve path reports every non-skipped package
whose files carry install scripts, regardless of the allow-build
policy, through a sink threaded into the install pipeline. Installs
served from the frozen-lockfile path leave the field undefined so the
embedder keeps its previously recorded list, matching the TS engine,
which only computes the list from a fresh resolve's fetch results.
2026-07-31 16:29:23 +02:00
Zoltan Kochan 536b7a2c8a chore(release): 11.19.0 (#13524) 2026-07-31 10:46:26 +02:00
Zoltan Kochanandgithub-actions[bot] 925c33d780 chore(release): 11.18.0, pacquet 12.0.0-beta.0 (#13481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 09:29:53 +02:00
Zoltan Kochan 2b03ceaba5 perf: reuse lockfile entries for safe override updates (#13464)
An overrides mismatch previously forced a complete dependency graph
resolution even when the lockfile could be updated safely in place.

Add conservative fast paths to both pnpm implementations for exact registry
version replacements and dependency-removal overrides. Replacements resolve
only their package metadata, retain satisfying child resolutions, prune dropped
edges, and attach added edges only when one safe compatible snapshot is already
locked. Independent replacements and removals can be processed together, and
parent-scoped removals match only the selected locked parent snapshots.

Fall back to the full resolver for peer-sensitive, ambiguous, patched, aliased,
exotic, policy-violating, or custom-hook cases. Remove obsolete virtual-store child links
while materializing the rewritten lockfile.
2026-07-28 23:51:22 +02:00
Zoltan Kochan c59e2a4c7b fix(pacquet): read legacy license fields (#13446)
TypeScript pnpm resolves package licenses from both the modern `license` field and the deprecated `licenses` field. Pacquet only accepted a string `license`, causing packages such as `exit`, `prettysize`, and `seq-queue` to be reported as `Unknown`.

Add a shared package-manifest extractor matching TypeScript precedence and legacy-field handling, and use it in `pnpm licenses list`. Cover the parser directly and verify the CLI result with a registry fixture.

Closes pnpm/pnpm#13438.
2026-07-27 23:26:13 +02:00