Commit Graph
13013 Commits
Author SHA1 Message Date
Zoltan Kochan 17bb3a12e8 fix(cli): support --fix-lockfile in Rust pnpm (#14253)
Teach the Rust pnpm CLI to accept --fix-lockfile and force a full metadata refresh while retaining the versions represented by the current lockfile.

Repair mode parses package and snapshot entries through a tolerant, security-budgeted loader so malformed derived fields do not prevent the repair itself. It keeps only resolution and dependency data used to seed the fresh resolve while retaining a separately sanitized, unprepared view for filtered merges. Normal, repair-seed, and repair-merge loads use isolated caches, generated top-level fields are normalized when malformed, and the exact merged result passes lockfile verification before materialization or save. The same intent is carried over the pnpr protocol, where repair support is negotiated before the request and supported servers bypass frozen and resolution-cache shortcuts.

Closes pnpm/pnpm#14250.
2026-08-28 00:30:26 +02:00
Zoltan Kochan d819fc9c7f fix(pacquet): prefer latest package bin version (#14252)
When aliases install multiple versions of one package, every candidate reports the same package name. Pacquet therefore kept the first discovered bin because its conflict resolver stopped at the package-name tie.

Compare parsed package versions after direct-dependency, ownership, and package-name precedence so the highest version wins, matching the TypeScript CLI. Keep the existing candidate when a manifest has no parseable version.

Fixes pnpm/pnpm#14249.
2026-08-27 23:17:01 +02:00
Zoltan Kochan cae85a28b7 fix(cli): cancel recursive work on bail (#14251)
Pacquet's scoped scheduler could stop dispatching after a failure, but its
workers still waited for every command already in flight. Watch-style scripts
therefore kept a bailing recursive run alive indefinitely, unlike the
TypeScript CLI whose exit cleanup terminates running children.

Track children launched by recursive run and exec commands. On Unix, launch
tracked external commands in process groups and terminate their descendants;
on Windows, terminate each tracked process tree with the system taskkill
utility. Thread cancellation through the shell emulator as well, so emulated
scripts and commands reached through bin shims do not survive a bail.

Represent interrupted scheduler work separately from natural failures. This
preserves the first failure as the command error and leaves interrupted and
undispatched tasks as running and queued in execution summaries, respectively.

Related to pnpm/pnpm#14211.
2026-08-27 23:11:35 +02:00
Zoltan Kochan 5dd145394b feat: pin shared build artifacts in the lockfile (#14243)
Persist verified shared build artifact envelope digests in the package snapshot
that consumed them. Scope each pin by dependency input key, artifact owner, and
consumer platform so compatible variants remain independently reproducible.

Apply pins only after the existing trust and compatibility checks. A missing
pinned artifact is a remote-cache miss and falls back to a local build; frozen
installs enforce pins without mutating the wanted lockfile.

Add `pnpm update --build-artifacts` as an explicit update mode that clears
existing pins, forces materialization, and records only artifacts that complete
verification and hydration without changing package versions. Reject command
combinations that cannot safely write or verify the lockfile.

Implement the lockfile format, install plumbing, update mode, and tests in both
TypeScript pnpm and pacquet.

Related to pnpm/pnpm#13771.
2026-08-27 23:08:19 +02:00
Zoltan Kochan aaaca889de perf(deps-restorer): clone whole package directories on macOS installs (#14248)
APFS serializes metadata-write syscalls volume-wide: measured against a
warm store, clonefile(2) throughput never exceeds its single-thread rate
no matter how many threads issue it, and link(2) scales negatively.
Importing packages one CAS file at a time from a rayon-wide fan-out
therefore sits at a per-volume ceiling, and the kernel lock convoy
behind that ceiling deepens with core count — which is how a warm-store
install that rebuilds node_modules got several times slower than pnpm
11's libuv-4-thread importer on many-core Macs.

Instead of tuning thread counts against a machine-dependent contention
curve, cut the syscall count: materialize each package once into the
canonical global-virtual-store slot under <store>/v11/links (the same
layout, completion-marker protocol, and concurrent-writer healing that
enableGlobalVirtualStore installs already use), then project it into
the project-local virtual store with one directory clonefile(2) per
package. A hot install whose canonical slots already exist pays one
stat and one clonefile per package instead of one syscall per file: on
a 60-project / 1354-package / 44k-file workspace fixture, rebuilding
node_modules from a warm store drops from 10-14s to ~4.4s (pnpm 11:
~14.7s), while the canonical-populating first install stays at the old
per-file cost. The resulting node_modules is byte-identical to the
per-file path's.

The cache engages only on macOS, under the isolated linker, with the
local virtual store, when the import method may clone (auto, clone,
clone-or-copy). Slots that need a build or patch marker, come from
mutable local sources, are force-re-imported, or resolve without a
checkable integrity (git dependencies, whose slot hash cannot see
whether their fetch-time prepare ran) keep the per-file path, so
canonical slots always hold plain pre-build CAS content and stay
interchangeable with what a GVS-enabled install materializes before
its build phase. Slots are addressed only through the content-addressed
graph-hash suffix, and the engine name is resolved synchronously for
eligible installs so those hashes agree with GVS installs sharing the
store. Every cache failure falls back to the per-file import; failures
that condemn the volume (EXDEV, ENOTSUP) disable the cache for the rest
of the process.

The pnpm 11 TypeScript CLI keeps its per-file importer: it cannot issue
a directory clonefile through libuv, its 4-thread pool sits near the
APFS ceiling already, and materialization strategy is not part of the
cross-stack contract. Like the Linux hardlink-first Auto ladder, this
is a v12-only materialization difference shipped behind the major.

Closes pnpm/pnpm#14231.
2026-08-27 22:57:58 +02:00
Zoltan Kochan 27c2c113d6 test(pacquet): pin what a remote side-effects restore transfers (#14246)
`apply_shared_side_effects` had no test of its own. Everything under it was
covered — the digest-to-CAS-path agreement, the store's refusal to offer
corrupt or non-regular content — but nothing exercised the restore itself, so
the property those pieces exist to produce went unstated: a restore fetches a
manifest's file only when the store does not already hold that content.

Two tests now run the same restore against a mockito pnpr twice, differing
only in whether the store was seeded with the artifact's one file. One expects
a blob download and asserts the overlay points at what the store wrote; the
other expects none and asserts the overlay points at the seeded file. The blob
endpoint's hit count is the whole difference between them, which is exactly
the claim.

The server is mocked rather than run: pulling the pnpr server into
`pnpm-deps-restorer` as a dev-dependency would cost the crate's test build far
more than three routes are worth. The resolve route echoes the candidate's
input key and source integrity back inside a freshly signed envelope, because
both are derived from the host's node major and the lockfile, and the client
discards any variant that does not match what it asked about.

Linux-gated: `linux_glibc_platform` refuses every other target, so elsewhere
there is no restore to observe.

The TypeScript client has covered this since pnpm/pnpm#14189; this closes the
matching gap on the pacquet side.

Follow-up to pnpm/pnpm#14189.
2026-08-27 21:45:59 +02:00
Zoltan Kochan adbeaab3c6 fix(engine-pm): record a satisfied range pin the same way in both stacks (#14245)
The TypeScript side asked the registry to resolve a range pin even when the
running pnpm already satisfied it, so it recorded a version nobody was running
and switched away from a satisfying one. It also passed the resolved version as
the specifier, overwriting the range the project had asked for with an exact
version.

It now prefers the running version when that satisfies the pin, matching
package_manager_to_sync, and records the range as the specifier the way
resolve_package_manager_integrities already does through its wanted_specifier
argument.

Related to pnpm/pnpm#13932, which made the Rust CLI record a satisfied range pin
this way.
2026-08-27 21:41:53 +02:00
Zoltan Kochan 2f2bdaf37f fix(fs.hard-link-dir): fill the destination in place (#14242)
Copying a built package to its other hoisted locations staged a fresh copy
of the source in a temp directory and renamed it over the destination. The
destination directory that went away took its own node_modules with it —
under the hoisted node linker that is where the dependencies that could not
be hoisted any higher live, so those were silently deleted from every
location the copy landed on.

The copies of one build chunk run concurrently, so a sibling package could
also be staging its own copy inside that node_modules at the time. Its
staging directory was carried off with the replaced directory and its
commit then failed with ERR_PNPM_ENOENT, aborting the install:

  ENOENT: no such file or directory,
    rename '<dest>/node_modules/_tmp_<pid>_<hash>' -> '<dest>/node_modules/<pkg>'

Link the source into the destination entry by entry instead. A file the
destination already has is replaced through a temp sibling and a rename, so
a reader sees either the whole old file or the whole new one; a destination
entry whose kind does not match the source's is removed first, which also
keeps a symlinked directory from redirecting the writes out of the
destination; and a file that is already the source file is skipped. Nothing
removes the destination directory any more, so its node_modules and everyone
working inside it are left alone.

The rename retry that leaves the destination in place was private to
`@pnpm/fs.indexed-pkg-importer`, which needed it for the same reason. It now
lives in `@pnpm/fs.graceful-fs` and both call sites use it.

Closes pnpm/pnpm#12880
2026-08-27 21:37:29 +02:00
Zoltan Kochan 6f787c19c6 fix(tarball): match npm path stripping (#14247)
Pacquet normalized away a leading dot component before dropping the archive's top-level path segment. As a result, `./package/package.json` became `package.json`, while pnpm and npm's `tar.x({ strip: 1 })` count `.` as the stripped component and retain `package/package.json`.

Preserve a leading dot in shared archive segmentation long enough for the tar extractor to remove exactly one component. Absolute paths and parent traversal remain rejected. Add matching Rust and TypeScript regression coverage.

Closes pnpm/pnpm#14165
2026-08-27 21:28:51 +02:00
Zoltan Kochan a22206394e fix(run): clean up lifecycle process trees on failure (#14244)
On Windows, pnpm's error handler cannot rely on descendant enumeration because modern systems may lack wmic and the PowerShell fallback can exceed its time budget. Recursive exec children already register their PIDs for the taskkill fallback, but lifecycle children spawned by recursive run did not.

Use the spawn observer added in `@pnpm/npm-lifecycle` 1100.1.0 to register lifecycle children with the existing tracker. Reuse the process-tree fixture to verify recursive run cleanup with the process-enumeration paths removed from PATH.

Also disable Git checks in the publish error test so a dirty contributor worktree cannot mask the missing-version error that test exercises.

Related to pnpm/pnpm#14211.
2026-08-27 21:27:50 +02:00
Zoltan Kochan ea4b11db73 fix(cli): resolve the modules dir per project in clean (#14240)
`clean` derived its per-project modules leaf by stripping `ctx.dir` off
`config.modules_dir`. That path is anchored at the workspace root (or at a
configured `lockfileDir`), so from a workspace member the strip failed, the
leaf stayed absolute, and `dir.join(leaf)` sent every discovered project
back to the anchor: `pnpm clean --dir packages/a` emptied the workspace
root's `node_modules` and left the members' in place.

Take the configured `modulesDir` leaf instead — defaulting to
`node_modules`, as pnpm's `clean` does — so it resolves against each
project directory the way `pathAbsolute(modulesDir, dir)` does upstream. An
absolute setting still wins, since `join` leaves it absolute.

Closes pnpm/pnpm#14239.
2026-08-27 19:00:47 +02:00
Zoltan KochanandClaude Opus 5 12ea6ca0bb fix(engine-pm): record a satisfied range pin in the lockfile (#13932)
* fix(engine-pm): record a satisfied range pin in the lockfile

The comment in pre_command says a pin the running pnpm already satisfies
still has to reach the lockfile — the switch would have written it on the
way to the wanted version — but package_manager_to_sync could only produce
a version for an exact pin (or a source checkout), so a range pin like
`^12.0.0-rc.6` written by hand, or by any tool other than `pnpm add` /
`pnpm self-update`, never landed in packageManagerDependencies and the
project went without the shared resolution the pin exists to provide.

When the running pnpm satisfies the range, its version is the one the
project actually uses, so that is what gets recorded. An exact pin still
records itself, and a source checkout still answers first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: keep the range-pin comments to what the code cannot say

The comment recorded how the code used to behave and the test's doc
repeated its own name. What is left is the part neither the name nor the
body carries: why the range is built from PNPM_VERSION.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 18:58:13 +02:00
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 123737055c fix(run): filter hidden regexp script matches (#14238) 2026-08-27 18:12:53 +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 1f59dadd49 fix(cli): accept --shamefully-hoist option (#14236)
pnpm 12's Rust CLI knew how to read shamefullyHoist from configuration files, but let argument parsing reject the equivalent CLI option before configuration was loaded. Extract the boolean CLI spellings as configuration overrides, record them as explicit settings, and reuse the existing public-hoist derivation while preserving virtualStoreOnly precedence. Leave invalid bare values for clap to reject instead of silently consuming them.

Add parser and end-to-end regressions for the reported version command, boolean spellings, configuration precedence, and actual public hoisting.

Fixes pnpm/pnpm#14235.
2026-08-27 18:07:23 +02:00
Zoltan Kochan 0e4436fe9f fix: keep a declared range an override repeats verbatim (#14234)
An override governs a direct dependency's specifier whether or not it
rewrites the text. When it repeats the declared range, the overrides hook
leaves the manifest byte-identical, so comparing the hooked manifest with
the one on disk — the test the previous fix used — sees nothing. `pnpm
update` then moved the declaration onto the version it resolved while the
override held the resolver's view at the old range, and the lockfile
recorded a specifier the manifest never shows. The next
`--frozen-lockfile` install rejected the pair.

The overrides hook now also answers which of a manifest's dependencies it
claims, so the manifest writer can recognise a declaration it does not
own even where no text changed. pacquet reaches the same entries through
`ManifestSpecBumps`, whose apply step consults the run's own
`VersionsOverrider` before moving a range.

Closes pnpm/pnpm#14224
2026-08-27 17:55:12 +02:00
Zoltan Kochan dcf3657187 fix(run): treat empty regexp scripts as missing (#14237)
RegExp script selectors included matching names even when their script bodies
were empty, unlike exact-name selection. Recursive task orchestration could
therefore dispatch tasks pulled in through dependsOn before eventually
reporting that the requested script did not exist.

Filter empty bodies at the shared script-selection boundary in both the
TypeScript CLI and pacquet, so pre-dispatch validation observes the same
missing-script semantics for exact and RegExp selectors.

Related to pnpm/pnpm#14211.
2026-08-27 17:53:10 +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 0f2ad88da2 feat: add per-task concurrency limits (#14229)
Add an optional positive-integer concurrency limit to workspace task settings and carry it onto resolved task nodes.

Reserve permits in the dependency-ready scheduler rather than inside workers. This lets unrelated task names use available workspace capacity, while tasks waiting on their own limit remain undispatched and do not start after a bailed failure.

Implement matching configuration validation, scheduling behavior, and integration coverage in the TypeScript CLI and pacquet.

Implements the per-task concurrency extension from pnpm/pnpm#14211.
2026-08-27 16:51:15 +02:00
Zoltan Kochan 42a72b0b73 fix(cli): recognize the pm command prefix (#14230)
`pnpm pm <command>` forces pnpm's built-in command over a `package.json`
script of the same name (https://pnpm.io/cli/pm). The Rust CLI had no
handling for the prefix, so clap took `pm` for an unknown subcommand and
the run-script fallback claimed it, failing with
`ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL` / `Command "pm" not found`.

Strip a leading `pm` token before clap parses argv, as the TypeScript CLI
does in `parseCliArgs`, and carry the resulting "built-in forced" signal
through `RunCtx` to `clean` / `purge` — the commands that step aside for a
script of their own name today. The prefix is only recognized as the first
token, matching pnpm, and is kept in the argv passed to a dispatched pnpm.

Closes pnpm/pnpm#14226.
2026-08-27 16:07:51 +02:00
Lazizbek Ergashev 0246958058 fix(cli): report the released pnpm version in doctor (#14227)
The versions check in `pnpm doctor` reported `env!("CARGO_PKG_VERSION")`. That
resolves to the `cli` crate's own manifest version, which is the `0.0.1`
placeholder every crate in the workspace carries, not the version the binary
ships as. Users saw `pnpm 0.0.1` from `doctor` and `12.0.0` from `pnpm -v` on
the same install, which makes the first line of a diagnostics report useless
for the thing diagnostics are usually pasted into: a bug report.

`pnpm_config::PNPM_VERSION` is the single source of truth the `--version` flag
and the default reporter already read, so the check now reads it too.

Fixes pnpm/pnpm#14225.
2026-08-27 15:34:41 +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 301a6db1cd fix: keep the declared specifier of an overridden dependency on update (#14223)
`pnpm update` rewrote every direct dependency's `package.json` entry to the
range resolution settled on. For a dependency that `overrides` also claims,
the resolver never read that entry: the overrides hook replaced it in the
manifest handed to resolution, so the version that came back answers the
override. Writing it back destroyed what the project actually declared —
most visibly a `catalog:` reference, which became a pinned version and
dropped the project out of the catalog.

The manifest writer now keeps the declaration on disk whenever the hooked
manifest's entry differs from it and the resolver followed the hooked one.
A dependency the run names with a specifier of its own (`pnpm add foo@2`)
still writes that request.

pacquet reaches the same entries through `ManifestSpecBumps`, whose targets
now carry the specifier `package.json` declares; a bump whose lockfile entry
no longer matches it is skipped, leaving the lockfile and the manifest
agreeing.

Closes pnpm/pnpm#12115
Closes pnpm/pnpm#11658
2026-08-27 15:27:25 +02:00
Zoltan Kochan 9f3f18905e perf: retire chunk-based workspace execution loops (#14221)
Move the remaining workspace command pipelines from topological chunks to the shared dependency-ready scheduler. Recursive rebuild, per-project install-family commands, pack, publish, stage approval, dependency builds, and project lifecycle scripts can now dispatch each node as soon as its own prerequisites settle instead of waiting at a workspace-wide layer barrier.

Keep concurrency inside the scheduler so ready work cannot over-dispatch, retain the existing command-specific bail and no-bail policies, and wait for already-dispatched command work where the old Promise.all and scoped-thread paths did. Ignored cycles remain deterministic by dropping backward edges in the graph sequencer's flat order.

With all execution consumers migrated, simplify graph sequencing to order plus cycles, replace grouped project sorting with dependency maps, and remove the obsolete run-groups dependency and chunk helpers.

Implements item 2 of pnpm/pnpm#14211.
2026-08-27 15:03:46 +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 Kochanandgithub-actions[bot] c4fdd5fbb4 chore: update dependencies, Node.js, pnpm, and GitHub Actions (#14218)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 13:24:50 +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 a83886e47f refactor: extract workspace task scheduler (#14215)
Move workspace task graph construction and scheduling out of the recursive command implementations into dedicated TypeScript and Rust packages.

The Rust crate depends on the lower-level dependency graph sequencer so package-manager code can consume it without creating a dependency cycle. Existing recursive run and exec consumers now import the shared implementations, preserving their current behavior while enabling the follow-up scheduler migrations tracked by pnpm/pnpm#14211.

Related to pnpm/pnpm#14211.
2026-08-27 11:58:01 +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 e7f3bccfff feat: workspace task orchestration (#14209)
Replace the chunked topological scheduler of recursive run/exec with
per-task scheduling in both stacks: a task — a (project, script) pair —
becomes runnable when every task it depends on has completed
successfully, and runnable tasks are dispatched under the
workspace-concurrency limit with no barrier between
dependency-independent tasks.

A new "tasks" section in pnpm-workspace.yaml declares task dependencies
with the caret convention ("^build" = the task in each workspace
dependency, "build" = the task in the same project). A task with no
entry behaves as depending on its own name in the workspace
dependencies, which is exactly what chunking implied. A project without
the script becomes a pass-through node that is reported skipped and
keeps the chain intact.

Also per the RFC: task-graph cycles are ERR_PNPM_TASK_CYCLE naming the
participating tasks, scoped to the invocation's selected graph, with
ignoreWorkspaceCycles: true downgrading the error to a warning;
--resume-from excludes exactly the anchor's transitive dependencies;
--reverse runs the reverse graph; under --no-bail dependents of a
failed task are reported skipped and do not add to the exit code; with
--bail, the first failure ends the run at once and nothing new is
dispatched; output is inherited only when at most one script can ever
be in flight; and pnpm -r run --dry-run [--json] prints the resolved
task graph without running anything (the verify-deps check included).

Implementation: the projects sorter exposes the tunneled dependency-edge
map (filteredProjectsDependencies / filtered_projects_dependencies)
instead of only its flattened chunks; the recursive summary is
task-keyed (dependsOn-pulled tasks get "<dir>#<task>" keys); pacquet's
inert --sequential now means concurrency 1 and its --no-sort
resume/reverse handling is aligned with the TypeScript CLI; the dead
chunk helpers are removed.

Related to https://github.com/pnpm/rfcs/pull/23
2026-08-27 02:41:03 +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 21df2c4a46 fix(jest-config): update pnpr config path (#14208) 2026-08-27 01:14:41 +02:00
Zoltan Kochan 03436d6880 fix: repair TS CI compile and lint after the dependency and Node.js update (#14206)
The dependency update (pnpm/pnpm#14205) broke TS CI / Compile & Lint in
two independent ways.

streamx 2.28.1 declares the 'data' event payload as unknown, so the five
tar-stream data handlers in the releasing commands that annotated the
chunk as Buffer no longer typechecked. They now take the chunk as
unknown and cast at the use site; tar-stream emits Buffers at runtime,
so the emitted JavaScript is unchanged.

The Node.js v26.8.0 binaries on nodejs.org were built with a bogus
version string and self-report as 26.8.0-alpha.0.0.0 (Node.js shipped
26.8.1 the next day to correct it). That prerelease-looking version made
cspell's engine guard reject the runtime during lint and made node-gyp
request a nonexistent v26.8.0-alpha.0.0.0 headers tarball for native
builds. The pinned runtime is bumped to 26.8.1 in the root devEngines,
the compile-only script, the CI test matrix, and the benchmark and
release workflows.
2026-08-27 00:49:41 +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 Kochanandgithub-actions[bot] 2bd58ac4d4 chore: update dependencies, Node.js, pnpm, and GitHub Actions (#14205)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 00:16:37 +02:00
Zoltan Kochan 45b66df643 fix(deploy): inject transitive workspace dependencies (#14201)
Apply the legacy deploy manifest transform to importer and resolved dependency
manifests in the Rust install pipeline.

Carry dependenciesMeta[alias].injected through recursive child specs and
speculative resolution so nested workspace edges use file-based injection.
This keeps legacy deployments self-contained instead of linking transitive
workspace packages back to the source workspace.

Add an end-to-end regression test for a transitive workspace dependency chain.

Closes pnpm/pnpm#14193
2026-08-27 00:08:17 +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 f656f02eed fix(cli): keep verifier install output off stdout (#14200)
Route the dependency-verification install reporter to stderr so commands retain ownership of stdout. Preserve the effective reporter format through single-project and recursive execution paths.

Add end-to-end coverage for the workspace scope leak, NDJSON diagnostics, and every affected silent execution path.

Fixes pnpm/pnpm#14197.
2026-08-26 23:49:39 +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) pnpr@0.1.0-alpha.8 v12.0.0 2026-08-26 16:36:14 +02:00
Zoltan Kochan 86c74539db feat: make pnpm v12 stable (#14187) 2026-08-26 16:01:14 +02:00
Zoltan Kochan 83350dea56 feat: integrate the remote side-effects cache with install (#14171)
Integrate the remote side-effects cache proof of concept with dependency installation in both TypeScript pnpm and pacquet.

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

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

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

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

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

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

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

Follow-up to https://github.com/pnpm/pnpm/pull/14180 and
https://github.com/pnpm/rfcs/pull/19.
2026-08-26 15:28:20 +02:00