Commit Graph
206 Commits
Author SHA1 Message Date
Zoltan Kochan 53ba59e882 feat(napi): expose readConfig for embedders (#13513)
Bit parses .npmrc through @pnpm/config.reader only to hand the result
back to the engine, and that JS package chain is what keeps Bit's
registry mirror problems alive (its mirror caps @pnpm/config.env-replace
below the version config.reader needs). The engine already resolves the
full config cascade for its own installs; readConfig projects the
subset an embedder consumes: registries with resolved Authorization
headers, authHeaderByUri, proxy, TLS, network limits, directories, and
install behavior settings.
2026-07-31 00:43:23 +02:00
Abdullah Alaqeel 9a4f51c6fa fix(install-summary): suppress '(X is available)' when latest is held back by minimumReleaseAge (#13214)
fix(install-summary): suppress '(X is available)' when latest is held back by minimumReleaseAge

When minimumReleaseAge (default: 24h) left the registry's
dist-tags.latest immature, the install summary still printed
'(X is available)' for it — advertising the exact version the policy
had just refused to install.

The hint only ever names the actual latest tag: the resolvers now
surface dist-tags.latest on the resolve result only when the active
policy would allow installing it (latest_allowed_by_policy /
latestAllowedByPolicy — an O(1) check of the tag's publish timestamp
against the cutoff, honoring publishedByExclude full-name and exact-
version entries). An immature latest suppresses the hint instead of
being rewritten to an older mature version, so the hint never names a
non-latest version as latest. Suppression requires positive evidence
of immaturity: a missing or unparsable timestamp keeps the raw tag,
matching the pick itself, which only enforces the policy when the
packument's time map is usable.

Also fixes a pre-existing divergence in the pacquet reporter: it gated
the hint on 'latest != version' rather than 'latest > version', so it
would suggest downgrading when the installed version was newer than
the latest tag. Now uses node-semver comparison, matching the
TypeScript reporter's semver.lt check.

Closes pnpm/pnpm#11698.
2026-07-30 20:00:23 +02:00
Zoltan Kochan 3eec70fdea perf(resolver): cut large-workspace peer resolution roughly in half (#13506)
An importer whose required-peer round converged skips re-discovery
until its inputs change; later rounds walk only newly added direct
deps (the full direct set still defines the provider context, and
earlier subtrees' scope-filtered missing reports replay from an
accumulator); a children-ownership handover with an unchanged
peer-shadowed context no longer flips sibling occurrences lazy or
bumps the rewrite counter; and resolver-internal maps use rustc-hash.

Peers provided by multiple candidate versions may bind to a different
(still range-valid) provider than before; output stays deterministic.
Measured on a 114-importer workspace: resolution 77s -> 36s, discovery
walks 802 -> ~400, node visits 2.6M -> ~1.0M.
Profile and remaining work: https://github.com/pnpm/pnpm/issues/13505
2026-07-30 19:47:43 +02:00
dependabot[bot] 8a4d60b9a5 chore(cargo): bump bytes from 1.12.0 to 1.12.1 (#13100)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.12.0 to 1.12.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 10:58:33 +02:00
Zoltan Kochan 0f1c46762e fix(napi): deterministic overrides, hook-free install options, and large-workspace install fixes (#13492)
The napi install options deserialized `overrides` into a HashMap, so
the JS object's key order was lost and pnpm-lock.yaml#overrides was
rewritten in a random order on every install driven through the addon
(the freshness comparison is order-insensitive, so this churned the
file without invalidating it). Deserialize into an IndexMap via napi's
object_indexmap feature - matching the order the TypeScript engine and
the pacquet CLI record - and collect the getPeerDependencyIssues JSON
path through the same order-preserving shape.

Also wire pacquet_diagnostics::enable_tracing_by_env into the addon's
module init so the TRACE env var works for napi consumers like it does
for the CLI, and log the staleness reason when a preferred-frozen
install falls through to a fresh resolve - both were needed to diagnose
a Bit workspace that re-resolved on every install.

Batch the readPackage hook dispatch (per-manifest threadsafe calls cost
roughly one event-loop tick each, serializing large resolutions), expose
dedupePeers in the install options (its absence made the freshness gate
treat every Bit lockfile as outdated), and try pick_package's read-only
mirror fast paths before the per-name fetch semaphore so version-pinned
picks stop queueing behind concurrent refreshes of the same package.

Resolve workspace importers concurrently - children-ownership claims
are rank-ordered, so the result is arrival-order independent - which
absorbs the JS hook round-trip latency on large workspaces.

Add projects[].dependencyManifest so hosts can express their readPackage
hook logic engine-side with zero JS round trips (per-manifest deletions
use the existing overrides removal syntax; neverBuiltDependencies stays
rejected in favor of allowBuilds), and apply overrides after the
readPackage hook to match the TypeScript engine's createReadPackageHook
order - a hook that replaced a manifest wholesale (raw-manifest
substitution for injected workspace instances) previously erased the
overrides from that manifest.

Stop repeating the release-age abbreviated->full metadata upgrade fetch
per dependency edge: coalesce it on a per-document permit and remember a
304 Not Modified for the rest of the install, and share the resolver's
workspace-packages map behind an Arc instead of deep-copying every
project manifest on each ResolveOptions clone. Full resolution of a
345-importer workspace: 105 s -> 36 s.

Cut the peer walker's per-node map churn: share ParentRefs behind Arc
copy-on-write, build the collision overlay lazily, and reuse the
per-child parent-package snapshots when the context is unchanged
(peer-heavy 331-importer benchmark: 3.95 s -> 2.78 s).

Share the run-resolved preferred-versions fold across importers: each
importer replayed the whole workspace history into a private map every
hoist round; the fold now lives once on the workspace context and the
hoist call sites materialize merged buckets for just the names they
query (full-workspace benchmark: 886 ms -> 424 ms).
2026-07-30 10:10:39 +02:00
Zoltan Kochan 0ae2001072 fix(napi): respect in-memory manifest changes (#13488)
NAPI consumers provide authoritative project manifests in memory. The optimistic repeat-install path uses package.json mtimes as its freshness signal, so it could report an install as already up to date when only the supplied manifest changed.

Disable that shortcut for NAPI installs so the normal lockfile freshness checks compare the supplied manifests and materialize newly requested dependencies. Rebuilds retain the existing fast-path behavior.
2026-07-29 12:14:57 +02:00
Zoltan Kochan f0d6343cd1 test(benchmark): cover concurrent integrity extraction (#13433)
Add a concurrent cold-store tarball benchmark alongside the existing single-package integrity benchmark.

The workload covers many file-heavy tarballs in flight at once, matching the extraction shape that made the Rust engine slower than the TypeScript CLI on the Next.js workspace in pnpm/pnpm#13305.
2026-07-27 16:32:18 +02:00
Zoltan Kochan ec389f6d12 fix(store-dir): key non-registry store-index rows by the bare resolution id (#13417)
The v11 store index is an on-disk contract shared between the TypeScript
CLI and pacquet, but the two stacks disagreed on `pkgId` for every
non-registry dependency: pnpm writes the bare resolution id (the tarball
URL, the git-host archive URL, the `git+…#<commit>` spec) while pacquet
wrote the lockfile-shaped `<name>@<id>`. Registry entries — the
overwhelming majority — already agreed. The cost was reuse: a store
warmed by one stack was cold for the other for every URL / git
dependency, so switching between pnpm 11 and pnpm 12 re-downloaded,
re-extracted, and re-imported all of them, and `cat-index` in one stack
could not see the other's rows.

`PkgNameVerPeer::pkg_id` now derives that id from a lockfile key, reusing
`pacquet_deps_path::try_get_package_id` (pnpm's `tryGetPackageId`), and
every store-index keying site goes through it — the install dispatcher,
the warm-key prefetch in `create_virtual_store`, the fresh-lockfile
reuse map and graph prefetch, the side-effects upload, `pnpm patch`, and
`pnpm cat-index`.

A remote tarball also occupied two rows rather than one: the resolve-time
fetch keyed its row by the bundled manifest's `name@version` while the
install pass keyed one by `name@<url>`. `FetchTarballForResolution` now
keys by the caller's `package_id`, and the tarball resolver passes the
normalized bare specifier (the id the lockfile records) rather than the
post-redirect URL, so both passes address one row.

The git fetchers hand `prepare_package` a resolution id now, so it
synthesizes the `<name>@<id>` dep path it gates on from the fetched
manifest's name — exactly what pnpm's `preparePackage` does, keeping the
`allowBuilds` identity unchanged while the store key moves.

Closes pnpm/pnpm#13365
2026-07-27 12:48:52 +02:00
Zoltan Kochan f50f0b2032 fix(update-interactive): measure the choice table in terminal columns (#13415)
`pnpm update --interactive` sized and padded its table by counting
characters, which is only the rendered width for the ASCII most package
names and versions are made of. A cell holding wide characters — CJK,
most emoji — measures narrower than it renders, so its row's columns
stopped lining up with its neighbours'.

pacquet's hand-rolled `printable_width` gives way to
`console::measure_text_width`, which strips the SGR escapes
`colorize_target` embeds and measures the rest against the East Asian
width tables. `console` is already in `Cargo.lock` through `dialoguer`;
it is now declared in `[workspace.dependencies]`.

The TypeScript CLI lays the table out with `@zkochan/table`, which
measures with `string-width` and so already placed wide package and
workspace names correctly. Its own `getColumnWidth` did not: it sized
the two version columns with `stripVTControlCharacters(...).length`, and
a version wider than the width it computed made `@zkochan/table` reject
the cell outright — `pnpm update --interactive` aborted with "Subject
parameter value width cannot be greater than the container width"
instead of printing the list. It now measures with `string-width` too.

Closes pnpm/pnpm#13357
2026-07-27 11:39:15 +02:00
Zoltan Kochan 7befc73efb fix(resolving-local-resolver): install local file: tarball dependencies (#13409)
A `file:*.tgz` dependency's name, version, and dependencies live only in
the archive's own `package.json`. The local resolver left `manifest`
unset for the tarball branch, so `build_pkg_id_with_patch_hash` had no
name to prefix and emitted the bare `file:<path>` as the dep path. That
parses as no lockfile key, and `build_packages_and_snapshots` dropped it
with a silent `continue` — the install reported success while writing no
`packages:` / `snapshots:` row and linking a dangling symlink into a
virtual-store directory that was never created.

Read the archive during resolution, as the tarball resolver already does
for remote tarballs and the git resolver for git deps: `pacquet-tarball`
grows `read_local_tarball_metadata`, which hashes the file and reads its
bundled `package.json` in one pass, replacing the resolver's hash-only
read. Nothing is written to the store — the install pass addresses a
`file:` tarball's store-index row by its `<name>@file:<path>` dep path,
not by the `<name>@<version>` a resolve-time extraction could key, so
such a row would never be read.

With the name in scope the dep path becomes `<name>@file:<path>`, both
lockfile blocks are emitted, the tarball's own dependencies are walked
(they were dropped entirely before), and the install summary prints the
version instead of the raw specifier. The lockfile is byte-identical to
pnpm 11's for the same manifest.

The manifest is borrowed out of the decompressed archive buffer rather
than read through the tar entry, so an entry header can't size an
allocation; the offset/size arithmetic both call sites need is extracted
into `tar_entry_payload`.

Reading the manifest puts its `name` into the dep path and into a
`node_modules/<name>` directory, so the shapes that used to fail
silently are refused the way pnpm refuses them: a `package.json` that
isn't valid JSON raises `ERR_PNPM_TARBALL_EXTRACT`, one that names no
package raises `ERR_PNPM_MISSING_PACKAGE_NAME`, and one whose name isn't
a valid npm name raises `ERR_PNPM_INVALID_DEPENDENCY_NAME`. Degrading to
"no manifest" is right for the extraction path, whose consumers re-read
from disk, but here the manifest is the package's only source of
identity, so clearing it reproduces the bug above.

An archive with no `package.json` at all stays tolerated, matching pnpm,
which synthesizes a name from the alias. Telling that apart from a
nameless manifest needs more than a `None`, so the reader reports
whether a root `package.json` was present.

pnpm 11 already handles `file:` tarballs correctly (it reads the manifest
from the fetch, and resolves and fetches together), so this needs no
TypeScript counterpart.

Closes pnpm/pnpm#13379.
2026-07-27 10:55:00 +02:00
Zoltan Kochan 5de55a1f7e fix(config): resolve $dep-name self-references in overrides (#13404)
An `overrides` value of `$foo` means "whatever specifier the root
manifest declares for `foo`". pacquet copied the raw map onto
`Config::overrides` instead, so the reference reached the read-package
hook, `pnpm-lock.yaml#overrides`, and the lockfile freshness check
verbatim. A lockfile written by pnpm 11 — which records the resolved
specifier — then failed a frozen install with ERR_PNPM_OUTDATED_LOCKFILE
(`overrides` in the lockfile `{"is-odd":"3.0.1"}` vs the config's
`{"is-odd":"$is-odd"}`), which is what blocked validating vitejs/vite.

Resolve the references while config is read: `pnpm-workspace.yaml` is
the only source that can set `overrides` (the global config.yaml is
stripped of the key and no `PNPM_CONFIG_*` var carries a map), and the
cascade knows the workspace root at that point, so the root manifest's
`dependencies` / `devDependencies` / `optionalDependencies` are in
reach. Every consumer downstream of `Config` — including the
`.modules.yaml` settings record — therefore sees the concrete
specifier, as it does in pnpm 11.

A reference to a package that is not a direct dependency fails with
ERR_PNPM_CANNOT_RESOLVE_OVERRIDE_VERSION, and the install family warns
about the deprecated syntax and points at catalogs, both matching
pnpm 11's wording.

TypeScript CLI: no change needed, it already resolves the references.

Closes pnpm/pnpm#13314
2026-07-26 16:42:17 +02:00
Zoltan Kochan 760b2ece2c fix(cli): close the remaining pnpm 12 CLI-surface parity gaps (#13376)
Close the four CLI-surface differences left over from pnpm/pnpm#13315
after pnpm/pnpm#13359 and pnpm/pnpm#13375 landed the other two.

Three land in the Rust engine. The `Scope:` line arrives via a new
`pnpm:scope` channel and a port of `reportScope.ts`; pnpm gates it on the
command being one that reports scope *and* the run being workspace-wide,
and since the engine's recursive-by-default set is narrower than pnpm's,
that conjunction is computed at dispatch and seeded into the reporter.
The event is emitted where the counts already exist — the `--filter`
resolution and the installer's workspace walk — so no install pays for a
second directory walk. A `sharedWorkspaceLockfile: false` plan reports
once for the whole selection rather than once per child install, which
would otherwise overwrite the filtered count with the workspace total.

The second is the `allowBuilds` placeholder an install writes for a build
it blocked, which pnpm/pnpm#13375 taught the engine to read but not yet to
write. The third is the elapsed time on a cache-satisfied lockfile
verdict.

The fourth goes the other way. The store block a first install prints is
unreachable in the TypeScript CLI: `reportContext` needs
`pnpm:package-import-method`, which only `@pnpm/fs.indexed-pkg-importer`
emits, and that runs inside a `@pnpm/worker` thread whose logger never
reaches the reporter. The main process already receives the import method
back from the worker, so it is reported from there instead.

Reporter globals are now seeded before the pre-command checks. They are
`OnceLock`s and the pre-command checks emit, so whatever fired first was
locking in unset values.

Closes pnpm/pnpm#13315
2026-07-26 02:24:03 +02:00
Zoltan Kochan 063094d271 fix(resolving-npm-resolver): report missing versions and packages with pnpm's error codes (#13371)
A well-formed semver range that the registry publishes nothing for was
folded into `Ok(None)`, so the resolver chain ran out and reported
`ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER` — "pnpm doesn't understand
this specifier", which is the wrong diagnosis and drops the latest
release, the other dist-tags, and the `pnpm view <pkg> versions` pointer.
The npm, jsr, and named-registry paths now raise `NoMatchingVersionError`
(`ERR_PNPM_NO_MATCHING_VERSION`) with that appendix, the way
`pickFromSimpleRegistry` and `resolveNpm` do upstream.

A non-2xx registry answer reached the user as raw transport text with no
code at all. Metadata responses now become `RegistryResponseError`
(`ERR_PNPM_FETCH_<status>`) carrying pnpm's `GET <url>: <reason> -
<status>` message, the "not in the npm registry, or you have no
permission to fetch it" hint, and — for 401/403/404, since a private
registry often answers a permission failure with a 404 — which
authorization header was sent, masked.

`ResolveError` erases a resolver failure to `Box<dyn Error>`, dropping
the `Diagnostic` facet the codes live on, so both types are recovered by
downcast in the tree walker and re-surfaced through transparent variants.
Optional dependencies keep skipping on either of them.

The report renderer also folds a cause whose message its wrapper already
quotes in full, so "Failed to resolve dependency tree: <msg>" no longer
prints <msg> again on the line below.

Closes pnpm/pnpm#13319
2026-07-25 20:48:12 +02:00
Zoltan Kochan 56907deda0 fix(cli): close the six pnpm-12-on-n8n defects (#13375)
Six independent pacquet-side defects found while installing and building
n8n with pnpm 12. Each is a divergence from the TypeScript CLI, so every
fix moves pacquet onto pnpm's existing behavior rather than inventing new
behavior.

Finding 2 — an `allowBuilds` placeholder written by pnpm made
`WorkspaceSettings` refuse to load the config at all. The value is now an
`AllowBuild` enum; only decided entries reach `Config::allow_builds`,
matching `createAllowBuildFunction`. The raw value stays on
`WorkspaceSettings` so `pnpm config list` and the `updateConfig` hook
still see what the file says.

Finding 3 — `diffy`'s matcher is byte-exact, while `@pnpm/patch-package`
compares lines with trailing whitespace stripped and retries a hunk
within twenty lines of its recorded position. `pacquet-patching` now
applies hunks itself with those tolerances, keeping `diffy` for parsing.
The file is modeled as `split('\n')` throughout, so untouched CRLF lines
keep their `\r` and a file without a final newline keeps that shape.

Finding 4 — `Config::user_agent` is threaded to install lifecycle
scripts, `pnpm run`, `exec`, and `dlx`, which previously saw nothing or
the bare string `pnpm`. It still reports `node/?` rather than the host
Node version; filling that in costs a `node --version` spawn on every
command, which is a separate trade-off to make.

Finding 5 — `/pattern/` script selectors select every matching script in
single-project and recursive runs, mirroring `tryBuildRegExpFromCommand`
+ `getSpecifiedScripts`. This adds `regex` to the workspace
dependencies; it was already in the lock transitively.

Finding 6 — concurrent `packageManager` switches raced on the shared
global-virtual-store slot: the destructive re-stage removed a directory
another process was still writing, and the native-binary relink used
unlink-then-hardlink, pulling the executable out from under a sibling
running it. The install is now serialized by an advisory lock (a new
`pacquet_fs::DirLock`, which gives up rather than failing so a lost lock
is never worse than today), and the relink skips an already-correct
destination and otherwise swaps via rename.

Finding 8 — settings drift under a frozen install reports
`ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` naming the one field instead of
`ERR_PNPM_OUTDATED_LOCKFILE` with the whole map dumped; ignored build
scripts keep their `(patch_hash=…)` suffix; and a deprecated package is
reported once, since pacquet's resolver re-emits one it later meets at a
shallower depth.

All six are pacquet-only bugs; the TypeScript CLI already behaves this
way, so there is nothing to mirror.

Closes pnpm/pnpm#13322
2026-07-25 20:34:26 +02:00
Zoltan Kochan 0754566903 fix(package-manager): derive global-virtual-store slots deterministically (#13368)
A frozen install of an unchanged project re-imported a varying subset of
its packages on every repeat run, instead of reusing the slots the
previous install materialized.

`calc_dep_graph_hash` breaks dependency cycles by hashing a node that
re-enters through one of its own ancestors with its children truncated —
and it memoizes that truncated digest until the outermost visit
overwrites it. The digest a cycle member ends up with therefore depends
on which node the walk entered the cycle from. Upstream is deterministic
because JS objects iterate in insertion order; pacquet drove the same
walk from `HashMap`s, whose iteration order differs per process, so every
install re-derived a different global-virtual-store hash — and a
different slot path — for the packages caught in a cycle. The
current-lockfile skip gate then found no directory at the new path and
re-imported them.

Both iteration orders are now pinned to the ones upstream's objects
carry:

- `DepsGraphNode::children` becomes an `IndexMap`, and the one shared
  builder inserts `dependencies` before `optionalDependencies`, each in
  lockfile key order — upstream's
  `{...dependencies, ...optionalDependencies}` spread.
  `virtual_store_layout`'s private copy of that builder is gone.
- `VirtualStoreLayout::new` walks the snapshots in lockfile key order
  rather than `HashMap` order, so the per-install memo is filled from the
  same entry points upstream uses.

`BuildModules` shares one `DepsStateCache` across concurrently dispatched
chunk members, which has the same entry-point sensitivity, so it primes
that cache in lockfile key order before the chunks run — otherwise the
side-effects-cache key of a cyclic builder moved between runs and the
build re-ran on every install.

The unit test pins the digests against the ones pnpm's own
`@pnpm/deps.graph-hasher` produces for the same graph: the two CLIs share
one global virtual store, so a cyclic package has to land on the same
slot whichever of them installed it.

Closes pnpm/pnpm#13316
2026-07-25 18:29:13 +02:00
Zoltan Kochan a82b30ec6b feat: support the saveWorkspaceProtocol setting in the Rust engine (#13299)
`pnpm add <pkg>@workspace:...` recorded the specifier exactly as typed,
so `workspace:^1.2.3` stayed pinned where pnpm writes `workspace:^` — a
range with no version in it, so bumping the workspace package never has
to touch its dependents' manifests.

`update --workspace` already had a private calculator for this. It moves
to the npm-resolver crate so both commands share it, and it renders
only: whether a dependency should be written under the protocol at all
stays with the caller, because the two differ. `update --workspace` is
an explicit request and writes a `workspace:` specifier even under
`saveWorkspaceProtocol: false`, while `add` falls back to a registry
range.

`add` needs its own entry point because it writes the manifest before
the install runs and so never reaches the resolver's `calc_specifier`
path. The workspace is enumerated only for the shapes that carry a
version; the rolling default reads nothing extra.

Also adds the missing `PNPM_CONFIG_SAVE_WORKSPACE_PROTOCOL` binding.

Related to pnpm/pnpm#12042.
2026-07-25 14:10:06 +02:00
Zoltan Kochan e2b45563cf fix(package-manifest): read package.json files that start with a UTF-8 BOM (#13327)
serde_json rejects a leading byte order mark, so a manifest carrying one
failed the whole read with "expected value at line 1 column 1". Workspace
discovery aborted on it, which made pnpm 12 unusable in repositories that
keep such a manifest on purpose — vitejs/vite ships
playground/resolve/utf8-bom-package/package.json and matches it with its
workspace patterns.

Route every manifest parse through parse_manifest / parse_manifest_bytes,
which drop the BOM first. pnpm decodes manifests through strip-bom and
TextDecoder, so both stacks now accept the same files: project manifests,
dependency manifests read from node_modules, and the package.json bundled
in a tarball. The BOM only disappears from a file once a real change makes
the writer emit the manifest afresh.

A manifest that really is malformed now reports its path, which the
workspace-discovery diagnostic previously omitted.

Closes pnpm/pnpm#13311
2026-07-25 14:07:43 +02:00
Zoltan Kochan d4788708d0 fix(package-manager): stop nesting hoisted deps that won the root slot (#13306)
With nodeLinker=hoisted, every workspace project got a link to each of its
direct dependencies, including the ones already materialized in the
workspace-root node_modules. Node resolution walks up from the project, so
that second entry is redundant — and it is a second copy for the build
pass to run lifecycle scripts in.

pnpm nests a copy only for the versions that lost the root slot. Upstream's
map comes from the hoisting hierarchy, so a dependency the hoister moved to
the root is simply absent from the importer's entry; pacquet derives it from
the importer's lockfile declarations, which keeps the winner in the map, so
the link pass has to recognize and skip it.

The workspace root itself is exempt: it owns the hoisted slot, and its
entries are the real directories rather than links to them.

Two tests asserted the old shape and now pin the upstream rule, and the
known-failure stub registered for this divergence
(hoisted_workspace_duplicate_materialization) becomes a real assertion in
run_pre_and_postinstall_scripts_in_a_workspace_with_hoisted_linker.

Refs pnpm/pnpm#13167
2026-07-25 13:52:57 +02:00
Zoltan Kochan 0ab9a51652 feat(update): support --workspace in pacquet and honor --depth per dependency (#13288)
Two of the gaps tracked in pnpm/pnpm#12101.

`--workspace` (pacquet): a workspace-link update rewrites each matched direct
dependency to a `workspace:` specifier before the install resolves it, so the
existing `workspace:` resolver produces the link and its errors. Which
dependencies are matched follows the TypeScript CLI exactly: with no selectors,
every direct dependency (minus `updateConfig.ignoreDependencies`) that a
workspace project publishes; with selectors, the direct dependencies they
match, where naming one the workspace does not publish fails with
ERR_PNPM_WORKSPACE_PACKAGE_NOT_FOUND. `--latest` and running outside a
workspace are rejected up front with the same codes the TypeScript CLI uses.
Selecting nothing to link is not an error: the run falls through to the
ordinary update branches.

The written specifier reproduces `calcSpecifierForWorkspaceDep`:
`saveWorkspaceProtocol: 'rolling'` (the default) writes the declared range
operator alone, so a sibling's next release does not invalidate the entry;
otherwise the linked version is written under that operator. This adds
`saveWorkspaceProtocol` to pacquet's config, which is why it leaves the
not-ported list in `pnpm_default_parity`. The workspace-packages map the
resolver already builds is reused as the source of link targets.

`--depth` (pacquet): the flag was only consulted as a `> 0` predicate gating
the name matcher, so a compatible bump always behaved as `depth = Infinity`.
`UpdateDepth` now travels with the seed policy into the resolver and gates
reuse per node, matching pnpm's `currentDepth <= updateDepth`. The
subtree-reuse memo is keyed by depth bucket, collapsed to a single bucket for
an unlimited update and to `max_depth + 1` beyond the ceiling, so the common
case costs nothing.

`--workspace` fixes (TypeScript): two cases linked dependencies the user never
named, which pacquet would otherwise have had to reproduce. Both come from
`params` being rewritten into the matched dependency names before the workspace
block reads it, so the block could not tell "the user named nothing" from "the
user's selectors matched nothing". With `updateConfig.ignoreDependencies` set,
`createWorkspaceSpecs` threw ERR_PNPM_WORKSPACE_PACKAGE_NOT_FOUND for the
project's ordinary registry dependencies; those are now skipped, as they
already were when nothing was ignored. Selectors that matched no direct
dependency left `params` empty, which the workspace block answered by linking
every workspace dependency. Both now key off whether the user named any
package.

Related to pnpm/pnpm#12101.
2026-07-25 11:13:51 +02:00
Zoltan Kochan 089ebad918 fix(pnpr): send the full request both clients owe the server (policy, catalogs, resolution mode) (#13279)
pnpr resolves and enforces policy server-side, and neither client runs its own
verifyLockfileResolutions when a pnpr server is configured. The server assigns
each field from the request unconditionally, so a field a client omits is cleared
rather than defaulted, and the server resolves under inputs the user never chose.

The TypeScript client sent only minimumReleaseAge of the verification policy, so
minimumReleaseAgeExclude entries stopped applying and the input-lockfile verifier
could reject a lockfile whose entries the user had explicitly excluded. It also
never sent the resolution mode, so --frozen-lockfile silently resolved and rewrote
the lockfile it promises to leave alone.

pacquet never sent catalogs, so every catalog: specifier failed to resolve — the
bug #13233 fixed for the TypeScript client and the server, leaving the Rust client
behind.

Drop the TRUST_POLICY_INCOMPATIBLE_WITH_PNPR guard, written before the server
gained trust-policy enforcement and refusing an install pacquet runs happily.
2026-07-25 10:56:36 +02:00
Abdullah Alaqeel d47ab916ad fix(self-update): stop the project config from steering the pnpm download (#12813)
Fixes pnpm/pnpm#12803.

`pnpm self-update` resolved the pnpm download through the project's own
registry/auth config, loaded the repo's default `.pnpmfile.(c|m)js`, and took
its `minimumReleaseAge` policy from the active workspace — so the outcome of a
global tooling operation depended on the directory it ran in, and on config a
checkout controls.

Route the fetch through the trusted package-manager bootstrap config — the
channel `switchCliVersion` already uses, which excludes the project `.npmrc`
and workspace manifest — and stop auto-loading the repo pnpmfile, whose
`updateConfig` hook and custom resolvers/fetchers reach the same requests.
`getPackageManagerBootstrapConfig` moves into `@pnpm/config.reader` so the
command package can reuse it.

Stop reading the project's `minimumReleaseAge` and `trustPolicy` settings, and
its `ci` flag, for self-update. Each is dangerous in both directions for a
command that replaces the global binary: a cooldown lowered waives the
protection the user configured, raised it pins the machine to the installed
pnpm, including past a release that fixes a vulnerability in it; a trust policy
turned off accepts a release whose evidence the user meant to reject, turned on
blocks the update the same way; and `ci` decides whether an immature pick may be
confirmed at the keyboard at all. Unlike a blocked dependency upgrade, those
decisions follow the user into every other project. The policies come from the
built-in defaults, the global config, the environment, and CLI flags instead.
Other commands keep reading them from `pnpm-workspace.yaml`, and there is no new
default.

When an immature version is refused, an interactive run offers to update
anyway, matching how a strict install prompts; non-interactive runs still fail
closed.

Mirrored in pacquet: `Config::current_for_self_update` skips the same settings,
and `self-update` gained the matching prompt and error.
2026-07-25 01:34:27 +02:00
dependabot[bot] 1c394b2d2e chore(cargo): bump object_store from 0.13.2 to 0.14.1 (#13269)
Bumps [object_store](https://github.com/apache/arrow-rs-object-store) from 0.13.2 to 0.14.1.
- [Release notes](https://github.com/apache/arrow-rs-object-store/releases)
- [Changelog](https://github.com/apache/arrow-rs-object-store/blob/main/CHANGELOG-old.md)
- [Commits](https://github.com/apache/arrow-rs-object-store/compare/v0.13.2...v0.14.1)

---
updated-dependencies:
- dependency-name: object_store
  dependency-version: 0.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:05:46 +02:00
dependabot[bot] 9b54e55c72 chore(cargo): bump serde-saphyr from 0.0.27 to 0.0.29 (#13268)
Bumps [serde-saphyr](https://github.com/bourumir-wyngs/serde-saphyr) from 0.0.27 to 0.0.29.
- [Release notes](https://github.com/bourumir-wyngs/serde-saphyr/releases)
- [Commits](https://github.com/bourumir-wyngs/serde-saphyr/compare/0.0.27...0.0.29)

---
updated-dependencies:
- dependency-name: serde-saphyr
  dependency-version: 0.0.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:05:22 +02:00
Scarab Systems 4737386533 fix(setup): persist GitHub Actions env files (#12841)
Write PNPM_HOME to the GitHub Actions environment file.

Add the setup bin directory to the workflow path file.

Later workflow steps can use global pnpm commands.

Keep TypeScript setup and pacquet setup behavior aligned.

Refs pnpm/pnpm#9191.
2026-07-24 02:42:40 +02:00
Zoltan Kochan d1161a19f2 fix(package-manager): let the resolvers decide what update --latest writes (#13250)
Replaying the update-lockfile job's update --recursive --latest showed
prepare_manifest treating the virtual `node: runtime:26.5.0` dependency as
a registry dep: it resolved the unrelated npm package `node` and rewrote
the manifest to a plain version whose install script then failed the
strict ignored-builds check.

The cause was that prepare_manifest asked "what is the latest version of
this name" outside the resolver chain, against a hand-maintained list of
protocols the registry does not own (workspace:, link:, file:). Every
protocol the chain grew had to be repeated there, and the ones never added
were silently mis-resolved: git/github: URLs and remote tarballs were all
looked up under their alias name on the npm registry and rewritten to a
plain version.

Ask the resolvers instead. prepare_manifest now resolves each direct
dependency through a DefaultResolver chain with UpdateBehavior::Latest and
takes the normalized_bare_specifier the claiming resolver reports, so each
protocol's own resolver decides what its manifest entry becomes: the npm
picker takes the higher of the declared range and the `latest` tag, the
runtime resolvers re-resolve within the spec the manifest already declares
(as in the TypeScript CLI, which needs no code change and gains a test
pinning the scenario, as does pacquet), and a dependency no resolver in the
chain claims keeps its entry. The chain deliberately omits the git, tarball
and local-path resolvers: they have no notion of a latest, and asking them
would clone or download during manifest preparation only to be told the
specifier stands.

That required implementing ResolveOptions::calc_specifier, which pacquet
declared but never read, so the npm resolver reports a manifest-ready
specifier for the version it picked. `npm_alias_target` moves out of the
update command into the new pacquet-resolving-npm-resolver::calc_specifier
alongside it, since the npm-alias round-trip is the npm resolver's business.
jsr: and named-registry entries are claimed and resolved but report no
specifier yet, so they keep their declared form.

A resolver that reports back what the manifest already says no longer
counts as a rewrite. Recording it marked the manifest dirty and persisted
it, which for a `runtime:` dependency rewrote the entry into
devEngines.runtime — a change the user never asked for.

`workspace:` keeps an explicit skip: preserveWorkspaceProtocol is always on
under --latest, and the npm resolver answers those only against the
install's workspace-package map, which manifest preparation has not built.
2026-07-24 02:07:11 +02:00
Zoltan Kochan cb68cb0552 fix(publish): retry the sigstore signing exchange and cap it with fetch-timeout (#13245)
The v11.17.0 release run aborted with ERR_PNPM_PROVENANCE_SIGN after a
single dropped request to the sigstore timestamp authority, even though
the eight packages published before it signed fine.

The TypeScript CLI signs through sigstore-js, which runs every Fulcio /
TSA / Rekor request under make-fetch-happen with its default retry
policy (2 retries, factor 2, 1 s floor) and under the caller's timeout
option, which pnpm wires to fetch-timeout (default 60 s, sigstore-js
falls back to 5 s when unset). pacquet called sign_raw_statement exactly
once with no retry and no request timeout (the sigstore-rust clients
build bare reqwest clients), turning any single network flake into a
failed release and letting a stalled connection hang until the OS
dropped the socket.

Retry the whole signing exchange in the Host SignProvenance impl with
the same policy, and cap each attempt with fetch-timeout (5 s fallback),
converting an elapsed deadline into a retryable signing error. The
exchange is idempotent (fresh ephemeral key, certificate, timestamp,
and transparency-log entry per attempt), so re-running it is safe. Both
sit below the DI seam, matching where sigstore-js keeps them on the
TypeScript side, so the generate_provenance contract is unchanged apart
from the pass-through timeout parameter.
2026-07-23 19:24:25 +02:00
Zoltan Kochanandgithub-actions[bot] 454e7d62b3 chore(release): 11.17.0, pacquet 12.0.0-alpha.19, pnpr 0.1.0-alpha.5 (#13237)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 17:07:05 +02:00
Zoltan Kochan c3c3e6b643 fix: resolve catalog references when installing via a pnpr server (#13233)
A `catalog:` specifier in a workspace's dependencies or overrides failed to
resolve when the install was routed through a pnpr server, erroring with
"No catalog entry '<name>' was found for catalog 'default'." even though the
catalog entry existed.

The pnpr server reconstructs the requested workspace in a temp dir from the
resolve request. That reconstruction wrote a pnpm-workspace.yaml with only a
packages: section and no catalog definitions, and passed catalogs_override:
None to the install — so the server had no catalogs and could not resolve any
catalog: specifier, in dependencies or overrides.

Forward the client's workspace catalogs in the resolve request and use them as
the install's catalogs_override, which feeds both dependency and override
catalog resolution. The client now sends its raw overrides (the server resolves
their catalog: references) instead of pre-resolving them locally.

Closes pnpm/pnpm#13232
2026-07-23 16:04:43 +02:00
Zoltan Kochan 10382e0d46 fix(pacquet-cli): align outdated table borders when output is colorized (#13227)
pacquet renders the `pnpm outdated` report with the `tabled` crate, whose
ANSI-handling (`ansi`) feature is not enabled by default. On a terminal the
cells are colorized (blue headers, green/yellow/red `Latest` version segments,
dim `(dev)`/`(github action)` labels), and without the feature `tabled` counts
the invisible color-escape bytes toward each cell's width. Cells carry
different amounts of escape codes, so columns were padded inconsistently and
the box-drawing borders drifted out of alignment.

Enable the `tabled` `ansi` feature so widths are measured by display columns,
ignoring SGR escapes. This also corrects the `outdated --long`, recursive,
`licenses`, and `audit` tables, which share the crate.

The TypeScript CLI is unaffected: it renders with `@zkochan/table`, which is
already ANSI-width-aware, so this is a Rust-only fix.
2026-07-23 11:46:54 +02:00
Khải 62236cb424 feat(network/web-auth): bound the token-poll body and make QR failure non-fatal (#13191)
Harden the web-based authentication flow against a malicious or
compromised registry, in both @pnpm/network.web-auth and its pacquet
port.

Bound the token poll: cap the token response body read from the
registry-supplied "done" URL at 64 KiB, and do not read the body of
non-OK or still-pending (HTTP 202) responses at all, so a registry
cannot grow pnpm's memory through the poll loop.

Make a failed QR code non-fatal: when a QR code cannot be generated
(for example when the authentication URL exceeds the maximum QR data
capacity), warn and display the URL on its own instead of aborting
authentication.

Resolves pnpm/pnpm#12721.
2026-07-23 07:55:55 +02:00
Zoltan Kochan fca656586c feat(update): update GitHub Actions dependencies (#13198)
Teach `outdated`, interactive `update`, and opt-in non-interactive updates to discover GitHub Actions dependencies in workflow files and referenced local action definitions.

Model actions as development dependencies so the existing production/development filters, compatible/latest behavior, explicit selectors, interactive selection, recursive workspace handling, and no-save/lockfile-only semantics remain consistent with package and runtime updates. Keep non-interactive updates package-only unless `--include-github-actions` is passed or `update.githubActions` is enabled in `pnpm-workspace.yaml`.

Resolve semantic release tags through Git refs, but never persist a tag as the executable reference. Every changed action is pinned to the resolved commit SHA, with the semantic tag retained in an adjacent comment for readability and future version comparison. Non-semver and Docker references are left untouched.

Implement the behavior in both the TypeScript CLI and pacquet and cover discovery, version selection, exact-SHA pinning, comment preservation, selector handling, and formatting preservation.
2026-07-22 00:41:30 +02:00
Zoltan Kochan 15c21aaddb feat(update): generate changesets for dependency updates (#13195)
`pnpm update` gains a `--changeset` flag, an `update.changeset` default in
pnpm-workspace.yaml, and a `--no-changeset` override. After the update, both the
TypeScript CLI and pacquet write one .changeset/pnpm-update-<suffix>.md declaring
a release for every workspace package whose published dependency contract changed.

Rationale: the scheduled-update pnpm/update action generates changesets by
git-diffing package.json files, which cannot see catalog-driven updates -- when
`pnpm update` bumps a catalog entry in pnpm-workspace.yaml, the consuming manifests
only say `catalog:` and never change. Only pnpm can map a changed catalog entry to
its consumers, so the generation lives here.

Semantics:
- changed dependencies / optionalDependencies specs -> patch;
- changed peerDependencies specs -> major (they can invalidate consumers);
- changed catalog specs are attributed to every production or peer consumer,
  including packages outside the --filter selection;
- resolution-only and devDependencies-only movement produces nothing;
- private, unnamed, and changeset-config-ignored packages are skipped;
- missing .changeset/config.json warns and skips; malformed config fails with a
  stable ERR_PNPM_INVALID_CHANGESET_CONFIG error; a symlinked .changeset directory
  is refused and generated files use exclusive creation.

Design notes:
- Both engines snapshot dependency specs and workspace catalogs before their shared
  update pipeline and diff the on-disk state afterward, rather than plumbing
  updatedCatalogs out of the installer -- the disk diff covers the single-project,
  recursive, and pacquet-delegated paths without changing any return contract, and
  structurally cannot observe resolution-only changes, so the specs-only rule holds
  by construction.
- The default is exposed as update.changeset on the canonical `update` section
  (#13197); the never-released updateConfig.changeset spelling needs no back-compat
  alias. It resolves onto the internal updateConfig.changeset the command reads.
2026-07-21 23:48:46 +02:00
Zoltan Kochan d62795ae56 feat(pacquet): close the CLI / config surface install-parity gaps (#13177)
Implement the four CLI / config surface parity gaps from
pnpm/pnpm#13167 in pacquet, un-stubbing every corresponding
allow_known_failure! test.

install/add --force (Closes pnpm/pnpm#13142): the flag lives on the
flattened InstallArgs (deploy reads it from there) and merges into
Config::force at the dispatch. The frozen path now drops the
per-snapshot unchanged-skip and the up-to-date short-circuits under
force, mirroring pnpm's lockfileToDepGraph(…, force ? null :
currentLockfile), so already-materialized packages are relinked.

enableModulesDir: false (pnpm/pnpm#12042): the config field is now
honored — with the global virtual store off it rides the
--lockfile-only pipeline (resolve and write the lockfile, materialize
nothing), gated off for rebuilds and exempt from the lockfile:false
conflict, matching pnpm. The NAPI binding keeps its aliasing.

extendNodePath: command shims carry pnpm's NODE_PATH blocks (target's
own node_modules dirs from a getBinNodePaths port, then the hidden
hoisted modules dir) under the isolated linker with a hoist pattern,
unless extendNodePath: false. The sh/cmd/pwsh templates replicate
zkochan/cmd-shim byte for byte; extra_node_paths threads through every
install bin-link pass; extend-node-path joins the pnpm-default parity
contract. Global bins stay NODE_PATH-free as a follow-up.

sharedWorkspaceLockfile: false (pnpm/pnpm#12042): a workspace install
runs one dedicated single-project install per project, each with its
own pnpm-lock.yaml, node_modules, and virtual store; a custom
virtualStoreDir re-resolves per project via
Config::explicit_settings. The engine anchors workspace_root — the
wanted lockfile, importer ids, prefixes, the workspace-state file —
at the active project (pnpm's lockfileDir = sharedWorkspaceLockfile ?
workspaceDir : projectDir) and records a single "." importer, while
catalogs and the workspace:-spec package map stay anchored at the
real workspace dir. add/update/remove anchor at the active project.
Recursive and filtered install-family commands still reject dedicated
lockfiles.

Related to pnpm/pnpm#13167.
2026-07-20 21:12:03 +02:00
Zoltan Kochan 45f06ce85e feat(pacquet): complete git dependency parity (#13169)
Resolve alias-less Git selectors before updating the project manifest so pacquet can use the fetched package name. Reuse unchanged Git entries from the wanted lockfile to keep moving refs pinned during unrelated changes, and source direct-dependency log versions from lockfile package metadata.

Add per-run pnpr fixture substitutions for registry packages with local Git dependencies and port the remaining Git-hosted install, build, peer, partial-commit, and prepare-failure coverage.

Related to pnpm/pnpm#13167
2026-07-20 14:27:17 +02:00
Zoltan Kochan ea1d6efcae fix(pacquet): close the Resolution / verification install-parity gaps (#13172)
Close the Resolution / verification section of pnpm/pnpm#13167
(related to pnpm/pnpm#13167; other sections remain open).

- package-is-installable: implement npm-semver includePrerelease
  semantics exactly — per-||-alternative evaluation with pure ordering
  at fully specified comparators, the stripped-prerelease fallback for
  expanded ranges. >=9.0.0 now rejects 9.0.0-alpha.1 (checkEngine.ts:34).
- cli: port the non-strict minimumReleaseAge immature-version fallback
  test (minimumReleaseAge.ts:68); the resolver fallback already existed.
- config/workspace-manifest-writer/package-manager: port
  cleanupUnusedCatalogs — new workspace-yaml setting (default false),
  writer-side unused-entry removal mirroring
  removePackagesFromWorkspaceCatalog (dep + overrides references,
  emptied blocks and files dropped), wired into add/update/remove via a
  shared catalog_cleanup helper that loads the workspace projects and
  substitutes in-memory manifests.
- package-manager: dispatch engineStrict per inbound lockfile edge
  (pnpm/pnpm#13143) — a walk from the importers classifies skip
  candidates by whether a non-optional edge from an installed source
  reaches them; required incompatible snapshots fail under engineStrict
  and warn+materialize without it; optional-only and
  behind-skipped-parent snapshots keep skipping; unreachable snapshots
  keep the propagated-flag dispatch. Ports optionalDependencies.ts:552.
- resolving-deps-resolver: intersect distinct compatible auto-install
  peer ranges via node-semver Range::intersect (mergePkgsDeps policy),
  and port the lockedPeerContext / resolvedPeerProviderPaths machinery
  into resolve_peers (tree-node fields, resolved_peer_provider_paths
  option, paths_by_node_id output, reuse guards incl. must-win) with
  the two-pass locked-provider unit tests. Install-path wiring
  (lockfile capture + gated second pass) is a follow-up.
- pnpr/cli: add local ajv@4.10.4 + ajv-keywords@1.5.0 fixtures, claim
  the names in REGISTRY_MOCK_LOCAL_PATTERNS, and port hoist.ts:320/:327
  (peer-variant private hoist + uninstall).

All eight known-failure stubs of the section are converted into real
ports and TEST_PORTING.md is updated. Four changesets target pacquet.
2026-07-20 14:04:57 +02:00
Zoltan Kochan 51c623ff8f test(pacquet): port the git-hosted install suite (stage 9) (#13160)
Port the install half of `fromRepo.ts`, the git-hosted `prepare` case,
and the three open git-resolver unit tests, per stage 9 of
pnpm/pnpm#13146.

The install-level tests build a local repo per test and install it over
`git+file://` (new `GitRepoFixture` in `pacquet-testing-utils`), so the
whole git install path runs without network access. That trades away the
host archive identity (`gitHosted: true` tarball), which the resolver
tests pin separately; a `file:` repo resolves to `type: git`.

Two cross-stack divergences surfaced and are fixed in pacquet (the
TypeScript CLI is already correct, verified byte-for-byte against pnpm
11.13.1):

- A non-host git dep whose package name matches its alias recorded
  `<name>@git+<repo>#<commit>` in the importer entry instead of the bare
  `git+<repo>#<commit>` ref. `real_name` now reads the manifest name for
  a `Git` resolution the same way it does for a remote tarball.
- An auth-bearing private HTTPS repo resolved to the host's public
  `codeload` archive URL, which carries none of the URL's credentials.
  The private-repo branch of `from_hosted_git` now drops the host
  archive option, matching upstream's `tarball: undefined`.

Three behaviors are tracked as known_failures stubs: alias-less
`pnpm add <git-spec>`, the `pnpm:root` added.version for a git dep, and
reuse of a git dep's locked commit across resolutions.
2026-07-19 23:23:16 +02:00
Zoltan Kochan 5e0788af7b fix(pacquet): complete auth and proxy parity (#13158)
Port the remaining Stage 6 auth and proxy scenarios to pacquet with hermetic
registry, HTTPS proxy, and SOCKS5 fixtures.

Apply proxy settings from global and workspace YAML, PNPM_CONFIG environment
variables, npmrc, and CLI flags with pnpm-compatible precedence. Ensure proxy
CLI overrides also reach bootstrap requests, including optimistic installs.

Share minimal tarball fixtures across CLI integration tests and record the
completed coverage in the test-porting plan.

Related to pnpm/pnpm#13146.
2026-07-19 22:53:01 +02:00
Zoltan Kochan 9843b4faa8 feat(pacquet): complete runtime installation parity (#13157)
Complete pacquet runtime installation parity for Node.js, Deno, and Bun.

Thread runtime failure policy, Node download mirrors, and runtime-derived Node versions through configuration and installation. Select runtime archive variants from supportedArchitectures and resolve engines.runtime declarations on dependency manifests without cloning full manifests in the resolver hot path.

Preserve explicit runtime dependencies for unmatched engine entries in both the TypeScript and Rust implementations. Add hermetic local-HTTP coverage for cold and offline reinstall, integrity failures, release channels, target architectures, dependency runtimes, and runtimeOnFail flows.

Completes Stage 8 of pnpm/pnpm#13146.
2026-07-19 22:35:28 +02:00
Mark Xian 0dbf7e6aa6 fix(resolving-npm-resolver): send If-Modified-Since as an HTTP-date (#13121)
The mirror's modified value is the packument's ISO-8601 time.modified,
but pacquet set it on the wire verbatim. If-Modified-Since must carry an
HTTP-date (RFC 9110 section 8.8.3), so recipients ignored the header and
answered 200 with the full multi-megabyte packument where the TypeScript
CLI (which converts via toUTCString) gets a 304. Convert before sending,
drop a value that parses as neither ISO-8601 nor an HTTP-date so it does
not count as a validator, and pin the wire format on both stacks with
matching tests.

Fixes https://github.com/pnpm/pnpm/issues/13104
2026-07-18 22:45:28 +02:00
Zoltan Kochanandgithub-actions[bot] 32a30c4d70 chore(release): 11.15.0, pacquet 12.0.0-alpha.15, pnpr 0.1.0-alpha.4 (#13126)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 14:19:57 +02:00
Zoltan Kochan 76f5886c39 feat(peers): accept scheme-carrying specifiers in peerDependencies (#13110)
peerDependencies previously rejected any value that was not a semver range or a
`workspace:`/`catalog:` spec with ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION.
This blocked named-registry specifiers (`<registry>:<version>`), which are
otherwise usable in dependencies/devDependencies (pnpm/pnpm#13095).

Accept any specifier that carries a scheme (named-registry, `npm:`, `file:`,
git/URL). A shared getPeerVersionRange helper extracts the comparable range
everywhere a peer is matched — the satisfaction check, the hoistPeers
auto-install selection, and the peers checker — matching `work:5.x.x` as
`5.x.x` and `npm:bar@^5` as `^5`, or `*` when no version is carried, while the
original specifier still selects the package to auto-install. isAcceptablePeerSpec
relaxes validation while still rejecting bare `name@version` typos.

The same change lands in the Rust port: a shared `peer_range` module in
resolving-resolver-base (which also absorbs the two duplicated
`is_valid_peer_range` copies), resolve_peers, hoist_peers, and the `pnpm peers`
command. MissingPeer carries the extracted range for matching/display and the
original specifier for the hoist source, so both stacks show the same warning
text and auto-install from the right source.

Closes pnpm/pnpm#13095.
2026-07-17 23:47:34 +02:00
debadityaandZoltan Kochan 8e3e5e7b19 fix(lockfile): compare equivalent git specifiers (#13056)
Lockfile freshness compared importer and catalog Git specifiers as raw strings
in both the Rust (pacquet) and TypeScript stacks, so a canonical git+https
lockfile entry was rejected when a manifest or catalog used git:// or a hosted
shortcut.

Add a Git specifier equivalence check and apply it at every freshness
comparison gate — importer specifiers, per-dependency checks, and catalog
entries — in both stacks. Preserve host, repository, and ref differences while
accepting supported protocol, shortcut, and .git suffix representations.

Closes pnpm/pnpm#13039.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-17 23:34:05 +02:00
dependabot[bot] 5434882fba chore(cargo): bump yamlpatch from 1.25.2 to 1.26.1 (#13099)
Bumps [yamlpatch](https://github.com/zizmorcore/zizmor) from 1.25.2 to 1.26.1.
- [Release notes](https://github.com/zizmorcore/zizmor/releases)
- [Changelog](https://github.com/zizmorcore/zizmor/blob/main/docs/release-notes.md)
- [Commits](https://github.com/zizmorcore/zizmor/compare/v1.25.2...v1.26.1)

---
updated-dependencies:
- dependency-name: yamlpatch
  dependency-version: 1.26.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-17 15:26:01 +02:00
debadityaandZoltan Kochan 7794d31823 fix(resolving-git-resolver): read git package names during resolution (#13059)
A git dep's specifier names a repo, not a package, so its name is only
readable from the package.json in the host's archive. The git resolver
left `manifest` unset, so `build_pkg_id_with_patch_hash` had no name to
prefix and emitted a bare archive URL as the dep path, which no lockfile
key parses — the graph-to-lockfile conversion then panicked on it.

Read the manifest during resolution, as the tarball resolver already
does for remote tarballs and as upstream's package requester does when a
resolver returns no manifest. Dep paths then carry the `<name>@` prefix
and parse as ordinary lockfile keys, so the graph-to-lockfile adapter
needs no git-specific key reconstruction.

The fetch stops at the raw archive: the prepare/prepublish pass and its
packlist filtering stay in the install pass, so no package script runs
during resolution. The install pass re-fetches the archive to run them,
so a git dep costs one extra archive download per install.

`real_name` now covers git-hosted tarballs, matching upstream's
`depPathToRef`: an unaliased git dep records `version: <url>`, while a
renamed one keeps the `<name>@<ref>` alias form that composes back to
its snapshot key.

Record the archive's integrity from the bytes that fetch already
hashes. The install pass refuses a tarball resolution without one, so a
git dep otherwise resolved into a lockfile it could never install from.
With it, a pacquet lockfile for a git dep is byte-identical to pnpm
11's for the same manifest.

Replace the `.expect()` on the importer dep path with a propagated,
contextual error so an unexpected shape degrades gracefully instead of
aborting the process.

A repo with no archive endpoint (self-hosted, `file:`, or any ssh URL)
has its name read from a throwaway checkout instead; the clone is
extracted out of `GitFetcher` so both passes share one implementation.
Without it such a dep resolved with no name, failed `PackageKey`
parsing, and was silently dropped from `packages:` / `snapshots:` while
the importer still pointed at it — a frozen install off that lockfile
exited 0 and left a dangling symlink.

Also strip the leading slash from a resolution's `path` before joining
it onto the checkout: `Path::join` discards the root on an absolute
argument, unlike upstream's `path.join`, so every sub-directory git dep
failed to install.

Guard the two git arguments this newly reaches from resolution. `repo`
was passed to `git clone` / `git remote add` as a positional, and git
reads a `-`-leading value as an option, so `--upload-pack=<cmd>` ran
`<cmd>` on a local or SSH transport; `commit` was already guarded for
this, `repo` was not. Reject the shape and pass `--` before the
positional. The resolve-time manifest read joined its sub-path with no
containment check, letting `#path:/../..` read an arbitrary
`package.json` off the host and stamp its name onto the dep; it reuses
the install pass's `safe_join_path` instead of a second weaker copy.

Closes pnpm/pnpm#13040.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-17 11:36:22 +02:00
Zoltan Kochan 73707d2ecb ci(release): gate releases on a publishable manifest, a real upgrade, and pnpm doctor (#13072)
Two published v11 releases were broken in ways nothing in the pipeline
checked, and both only surfaced once users upgraded onto them.

11.12.0 was packed by a pnpm that ignores the .pnpmfile.cjs beforePacking
hook, so the bundled dependency fields survived into the published
manifest. Resolving node-gyp then pulled a peer-suffixed snapshot into the
env lockfile, and every upgrade onto 11.12.0 died in
buildLockfileFromEnvLockfile (pnpm/pnpm#12955, pnpm/pnpm#12959).
11.12.0 and 11.13.0 also shipped `@pnpm/exe` platform packages with no
native binary; setup.js exits 0 when the binary is missing, so the
placeholder bin survived and only a real invocation caught it.

Assert on the packed tarball that the published pnpm manifest declares no
dependency fields, rather than trusting either stripper, since npm
publishes are immutable. Then, in the Tag workflow, upgrade from the
release line's current version onto the new one for both the `pnpm` and
`@pnpm/exe` wrappers and run the resulting binary. The release workflow has
already published under next-<major> at that point, so this reads the real
registry artifact, and it runs before the dist-tags move — the last gate
before a version reaches everyone.

Verified against the registry: the assert rejects pnpm@11.12.0 and accepts
11.11.0/11.13.1; the upgrade gate passes 11.13.0 -> 11.13.1 and catches
both `pnpm@11.12.0` and `@pnpm/exe@11.12.0`/11.13.0.

Related to pnpm/pnpm#12959.
2026-07-17 09:54:10 +02:00
Yashas Gunderia 1f5a097b77 fix(pacquet): prompt for strict minimum release age approval (#13062)
Strict minimumReleaseAge resolution already collects every immature pick, but pacquet could only abort. Add the interactive approval path used by the TypeScript CLI, persist approved exclusions before install writes begin, and keep noninteractive installs fail-closed with the complete violation list.

Run the blocking confirmation off the async worker and bracket it with pnpm:prompt events. The default reporter holds append-only lines or the latest live frame while the question is active, then replays that output when prompting ends. Reject update --no-save up front because approval requires a durable workspace exclusion.

Closes pnpm/pnpm#13047.
2026-07-16 22:37:35 +02:00
Zoltan Kochanandgithub-actions[bot] d5b4b7bd5d chore(release): pacquet 12.0.0-alpha.12, pnpr 0.1.0-alpha.3 (#13023)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-15 11:42:38 +02:00
Zoltan KochanandClaude Opus 4.8 6dcfadd5ae fix(release): sync Rust product versions via the meta-updater (#12988)
The Rust CLI and pnpr embed the versions their release builds report and
verify (`PNPM_VERSION` in pnpm/crates/config/src/defaults.rs, the crate
version in pnpr/crates/pnpr/Cargo.toml, and its Cargo.lock entry). These
were mirrored from the npm wrappers by `syncRustVersions` in bump.ts — a
step separate from the meta-updater, so running `pnpm version -r` without
the full bump flow bumped the wrappers (pacquet 12.0.0-alpha.10, pnpr
0.1.0-alpha.2) while the Rust sources stayed at the previous versions. The
release workflow's "Verify the committed version" step then failed, and
nothing caught the drift before the tag.

Move the sync into the meta-updater as a set of Rust-source file handlers,
so it is written by `pnpm update-manifests` and, crucially, validated by
`meta-updater --test` in pre-push and CI — a missed sync now fails locally
instead of at release time. bump.ts drops the redundant `syncRustVersions`
and runs `pnpm update-manifests` after `pnpm version -r`; the root `bump`
script no longer needs its own trailing `update-manifests`.

Regenerating brings the Rust sources up to the already-bumped wrapper
versions (alpha.10 / alpha.2), unblocking the release.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:24:21 +02:00
Zoltan Kochanandgithub-actions[bot] 682f57e773 chore(release): 11.13.0, pacquet 12.0.0-alpha.9, pnpr 0.1.0-alpha.1 (#12986)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-13 22:19:04 +02:00
Zoltan Kochan 5bd00823c9 fix(publish): send README to the registry as metadata (#12968)
`pnpm publish` again includes the package's README in the metadata sent to
the registry, matching the npm CLI, so registries can render it on the
package page.

Until v11 the actual upload was delegated to `npm publish <tarball>`, and
npm reads the README out of the tarball (pacote's fullReadJson) into the
version metadata regardless of pnpm's embed-readme setting. Once publishing
became fully native and went through libnpmpublish with pnpm's own
publishedManifest, the README only reached the registry when embed-readme
was true — but that setting defaults to false (deliberately, so the tarball's
package.json stays clean). The result was that published packages silently
lost their README metadata.

Decouple the two concerns: the README is now always attached to the manifest
reported for publishing, while embed-readme continues to control only whether
it is written into the package.json inside the tarball. For a pre-built
tarball passed to `pnpm publish <tarball>`, the README is read back out of the
tarball, mirroring npm's fullReadJson. Applied to both the TypeScript CLI and
the Rust pacquet stack (pack + publish; stage delegates to publish).

pnpr additionally hoists the latest version's readme to the packument's
top-level readme/readmeFilename on publish, matching npm and verdaccio, so a
package published to pnpm's own registry exposes a top-level readme.

Closes pnpm/pnpm#12966.
2026-07-13 18:27:50 +02:00