Commit Graph

11871 Commits

Author SHA1 Message Date
Zoltan Kochan
d91e2edbec chore: update lockfile, Node.js, pnpm, and pacquet versions (#12421) 2026-06-15 13:12:51 +02:00
Zoltan Kochan
41b74e792e fix(ci): refresh update-lockfile install state (#12420) 2026-06-15 12:25:06 +02:00
MCMXC
2f44bc93e9 test(pnpm): enforce minimumReleaseAge during --fix-lockfile (#12398)
* test(pnpm): add --fix-lockfile + minimumReleaseAge enforcement tests

Confirm that pnpm/pnpm#10361 is handled correctly: the lockfile
verifier runs on the existing lockfile even when --fix-lockfile is
passed, so immature packages not covered by minimumReleaseAgeExclude
cause the install to fail with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION.
A companion test confirms that packages explicitly listed in
minimumReleaseAgeExclude are still allowed through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(eslint): exclude .claude worktree directories from linting

Claude Code creates git worktrees under .claude/worktrees/ for agent
tasks. These directories are not part of any tsconfig project, so
the meta-updater linter fails when it picks them up. Adding .claude/**
to the ESLint ignores list prevents this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 12:04:45 +02:00
Khải
826f2133be feat(pacquet/add): --save-prefix (#12413)
* feat(pacquet/add): `--save-prefix`

Mirror `pnpm add --save-prefix` in pacquet. The flag configures the
semver range operator written to the manifest for a freshly added
dependency: `^` (the default) allows same-major updates, `~` allows
patch-level updates, and an empty string pins the exact version.
`--save-exact` still takes precedence.

The collapse of `--save-exact` / `--save-prefix` into a single pinned
version mirrors pnpm's `getPinnedVersion`
(installing/commands/src/getPinnedVersion.ts), and the range operator
that pinned version maps to mirrors pnpm's
`createVersionSpecFromResolvedVersion`
(pkg-manifest/utils/src/updateProjectManifestObject.ts).

Introduce a `PinnedVersion` enum in `pacquet-registry` (a port of
pnpm's `PinnedVersion` union) carrying both the flag interpreter
(`from_save_options`) and the range-operator mapping (`range_prefix`),
and change `PackageVersion::serialize` to take it instead of a bare
`save_exact` bool. `pacquet update` keeps its existing behavior by
deriving the pinned version from its `save_exact` flag with no prefix.

https://claude.ai/code/session_017HsNkaxX4bVZJN5dKBK3tR

* refactor(pacquet/add): tidy `--save-prefix` help and test

Address review feedback on PR 12413:

- Shorten the `--save-prefix` doc comment, which clap also renders as the
  `--help` text, to a single line listing the accepted prefixes.
- Read the manifest in the `prod_spec` test helper through
  `pipe-trait` (`dir.join(..).pipe(PackageManifest::from_path)`), and drop
  the now-redundant inner `pipe_trait::Pipe` import in
  `should_symlink_correctly`.

https://claude.ai/code/session_017HsNkaxX4bVZJN5dKBK3tR

* style(registry): reorder `PinnedVersion` derives for perfectionist

`perfectionist::derive_ordering` (prefix_then_alphabetical) wants the
`#[derive(...)]` list as `Debug, Default, Clone, Copy, PartialEq, Eq`.

https://claude.ai/code/session_017HsNkaxX4bVZJN5dKBK3tR

* test(pacquet/add): lock `--save-prefix` caret fallback

Add an integration test asserting that an arbitrary non-empty
`--save-prefix` value (`foo`) falls back to the default caret range
(`^1.0.0`), matching pnpm's `getPinnedVersion` which buckets any value
other than `''` and `'~'` into the caret pin. Keeps the permissive,
pnpm-compatible `String` behavior locked by tests.

https://claude.ai/code/session_017HsNkaxX4bVZJN5dKBK3tR

* fix(registry): write prerelease versions without a range prefix

`PackageVersion::serialize` always prepended the range operator, so an
added or `--latest`-updated dependency whose resolved version is a
prerelease (e.g. `2.1.0-rc.1`) was written as `^2.1.0-rc.1` / `~2.1.0-rc.1`
instead of the bare `2.1.0-rc.1` pnpm writes.

pnpm's `createVersionSpecFromResolvedVersion` returns the resolved
version verbatim when it carries a prerelease tag, regardless of the
pinned version
(pkg-manifest/utils/src/updateProjectManifestObject.ts). Mirror that:
short-circuit on a non-empty `pre_release` before applying the prefix.

Found while self-reviewing the `--save-prefix` port, which introduced
the `PinnedVersion`-based serializer in the first place. Adds a registry
unit test (prerelease ignores every `PinnedVersion`) and a CLI
integration test for `--save-exact` alone, completing the
save-exact/save-prefix matrix.

https://claude.ai/code/session_017HsNkaxX4bVZJN5dKBK3tR

* test(pacquet/add): cover prerelease add end-to-end

Add an integration test that `pacquet add @pnpm.e2e/beta-version`
(whose only published version is the prerelease `1.0.0-beta.0`, so
`latest` resolves to it) writes the spec verbatim with no `^` prefix.
This locks the prerelease branch of `PackageVersion::serialize` through
the full resolve-and-write path, complementing the registry unit test.

https://claude.ai/code/session_017HsNkaxX4bVZJN5dKBK3tR

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-15 12:03:36 +02:00
Zoltan Kochan
29ab905c21 fix: preserve catalog version range policy on update (#12416)
A named catalog whose name parses as a version (e.g. catalog:express4-21)
had its range policy overridden by pnpm update because whichVersionIsPinned
misread the catalog: reference in the previous specifier as a pinned
version. The catalog reference carries no pinning of its own, so the prefix
from the catalog entry is now preserved.

Closes https://github.com/pnpm/pnpm/issues/10321
2026-06-15 11:58:29 +02:00
Zoltan Kochan
a8c4704ac0 ci: structure CI check names (#12417) 2026-06-15 11:57:14 +02:00
Zoltan Kochan
bc13e49f96 ci: keep Dylint required check reportable (#12415) 2026-06-15 09:03:21 +02:00
Zoltan Kochan
1e82e001cd chore(release): 11.7.0 (#12414) v11.7.0 2026-06-15 08:37:08 +02:00
Zoltan Kochan
cd8348c6e9 fix(pacquet): support WSL Windows binaries in sh shims (#12409)
- Port the shell shim behavior from pnpm/cmd-shim#56 to pacquet.
- Generate `basedir_win` with Cygwin/MSYS/WSL2 handling and use it only when invoking `.exe` runtime branches.
- Preserve POSIX target paths for non-`.exe` runtime branches and add the `.cmd`/`.bat` `/C` runtime fallback.
- Gate MSYS-specific cmd switch escaping behind an `$msys` runtime flag, so MSYS gets `//C` while WSL2 and other shells keep `/C`.
- Bump `@zkochan/cmd-shim` to 9.0.6.
2026-06-15 07:21:16 +02:00
Zoltan Kochan
a6d485abca fix: stabilize Windows pacquet install tests (#12410)
- Replace lockfile env-document stream scanning with a FileHandle read loop that closes deterministically, including split-BOM handling.
- Align pacquet's default `virtualStoreDirMaxLength` with pnpm's Windows default.
- Forward pnpm's effective virtual store max length to delegated pacquet installs through `PNPM_CONFIG_VIRTUAL_STORE_DIR_MAX_LENGTH`, so currently published pacquet versions do not write mismatched `.modules.yaml` on Windows.
2026-06-15 01:53:56 +02:00
David Barratt
3a271413d8 fix: prevent a pinned locked peer provider from leaking to sibling nodes (#12320)
* fix: prevent a pinned locked peer provider from leaking to sibling nodes

When the locked-peer-context pinning introduced in pnpm/pnpm#12083 runs for
a node that has no child dependencies, parentPkgs aliases the parent's
object, so writing the pinned provider into it exposed the provider to every
sibling resolved afterwards. Sibling order follows resolution completion
order, so optional peers of siblings resolved nondeterministically and
"pnpm dedupe --check" failed intermittently in CI.

Copy parentPkgs before pinning so the pin stays scoped to the node and its
own subtree.

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

* perf: copy parentPkgs only before the first pin write

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-15 01:50:22 +02:00
Zoltan Kochan
05e82c945e ci: upload partial CI performance results (#12411) 2026-06-15 01:09:45 +02:00
Zoltan Kochan
d50d691e5a fix(lockfile): wait for stream close before returning (#12408) 2026-06-14 23:37:22 +02:00
Zoltan Kochan
18cba025ed ci: resolve merged PRs for Bencher uploads (#12407) 2026-06-14 23:20:15 +02:00
Zoltan Kochan
5f63458644 fix: bound descendant-process lookup on error exit to avoid a Windows hang (#12403)
## Problem

On Windows, **any failed `pnpm` command hangs 20–46 seconds before exiting.** The error handler (`pnpm/src/errorHandler.ts`) enumerates descendant processes via `pidtree` to terminate them on every error exit. On Windows `pidtree` shells out to `wmic` and, where wmic has been removed, a PowerShell `Get-CimInstance Win32_Process` fallback — a process listing that takes tens of seconds on busy CI runners.

This also broke Windows CI: the `verifyDepsBeforeRun/*` e2e suites are full of intentional-failure assertions (e.g. `pnpm start` with `--config.verify-deps-before-run=error` when deps aren't installed). Each failure paid the ~23 s error-handler tax, so the suite blew past the 70-minute cap. `pnpm install` and success paths never hit the error handler, which is why only failures were slow.

Diagnosed by sampling `process.getActiveResourcesInfo()` during the hang: it showed a lingering `ProcessWrap` (a spawned child), and hooking `child_process.spawn` named it (`wmic` → `powershell … Get-CimInstance Win32_Process`, exiting after ~23–46 s).

## Fix

Race the descendant-process lookup against a 2 s timeout. If it doesn't return in time, skip the kill and exit — `exit()` calls `process.exit`, which abandons the still-running (harmless, read-only) process query instead of blocking on it. The fast path (Unix, fast Windows) is unchanged.

Confirmed on Windows CI: the failing `start` invocations dropped from **~23 s to ~2.7 s**, and `multiProjectWorkspace.ts` went from **716 s to 124 s**.

## Also included

The CI pnpr-binary cache is split into `restore` + an explicit `save` step that runs right after the build, so a failing test step no longer discards the ~20-minute Rust build (the combined `actions/cache` only saved in a post-job step that gets skipped on failure).
2026-06-14 18:45:49 +02:00
Zoltan Kochan
9ddc86b635 ci: track test suite durations in bencher (#12404)
## Summary

Adds CI duration tracking for the `pnpm-ci-performance` Bencher project.

Tracked Rust testbeds and benchmarks:

- `pacquet.ubuntu`, `pacquet.windows`, `pacquet.macos` -> `tests.all`
- `pnpr.ubuntu`, `pnpr.windows`, `pnpr.macos` -> `tests.all`

Tracked pnpm testbeds and benchmarks for full test runs:

- `pnpm.ubuntu.node22`, `pnpm.ubuntu.node24`, `pnpm.ubuntu.node26` -> `tests.all`, `tests.cli`
- `pnpm.windows.node22`, `pnpm.windows.node24`, `pnpm.windows.node26` -> `tests.all`, `tests.cli`

The test workflows produce Bencher-compatible JSON artifacts without receiving `BENCHER_API_TOKEN`. A separate `workflow_run` workflow downloads those artifacts only for same-repository runs, validates their metadata, and uploads from trusted workflow code using the existing `BENCHER_API_TOKEN` secret. The pnpm CLI e2e duration is extracted from `pnpm run --report-summary` output during the same full-test execution, so the CLI e2e suite is not run a second time.
2026-06-14 18:44:17 +02:00
Zoltan Kochan
98745562b0 docs: configure AI review security and performance guidance (#12405)
* chore: tune ai review guidance

* docs: centralize ai review guidance
2026-06-14 17:39:28 +02:00
Zoltan Kochan
74a2dc9027 feat(installing): delegate resolution to pacquet >= 0.11.7 when configured (#12210)
* feat(installing): delegate resolution to pacquet >= 0.11 when configured

When pacquet is declared in configDependencies, pnpm previously always
ran it with --frozen-lockfile (pnpm resolved, pacquet materialized). If
the installed pacquet is >= 0.11 it ships its own resolver, so a
non-frozen plain install on the default isolated linker is now delegated
end-to-end: pacquet resolves, writes pnpm-lock.yaml, and materializes in
a single pass. Older pacquet keeps the resolve-then-materialize split,
and add/update/remove still resolve in pnpm.

* test(pnpm): cover pacquet 0.11 resolution delegation end-to-end

Bump the e2e pacquet pin to 0.11.0 (published under both pacquet and
@pnpm/pacquet) and add tests for the resolve path, the materialize-only
fallback (pinned to 0.2.14), and the scoped alias. Un-skip the add/update
tests now that pacquet 0.11 writes a compatible .modules.yaml, and fix
the update test (is-positive has no v4; update from 1.0.0 instead).

Also preserve the configDependencies env document when pacquet resolves:
pacquet rewrites pnpm-lock.yaml without the leading env YAML doc, which
dropped configDependencies and broke the next --frozen-lockfile install.
Capture it before delegating and restore it after.

* fix(installing): restore configDependencies env document even when pacquet fails

On the pacquet resolve-delegation path the configDependencies env document
was only restored after a successful pacquet run. A non-zero exit could
leave a rewritten pnpm-lock.yaml without it, breaking the next
--frozen-lockfile install's config-deps freshness gate. Restore it in a
finally block, swallowing (and warning on) any restore error so it cannot
mask the original pacquet failure.

* fix(installing): require pacquet 0.11.7 for resolving installs

* fix(installing): skip pacquet in lockfile check mode

* fix(installing): harden pacquet lockfile handoff

* fix(installing): preserve policy handling with pacquet

* fix(installing): skip pacquet when lockfile is disabled

* fix(installing): skip pacquet with branch lockfiles
2026-06-14 16:51:25 +02:00
Dasa Paddock
da248c3eef fix(view): update dist tarball unpackedSize to compute value as power of 10 (#12342) 2026-06-14 15:19:50 +02:00
tsushanth
86e70d2896 fix(installing.commands): key selectProjectByDir graph by project.rootDir (#12380)
* fix(installing.commands): key selectProjectByDir graph by project.rootDir

`selectProjectByDir` constructs a single-entry `ProjectsGraph` for the
non-workspace install path. It was using `searchedDir` (`opts.dir`) as
the key, but downstream `recursive()` builds `manifestsByPath` from the
projects array (keyed by `project.rootDir`) and then looks up entries
via `manifestsByPath[rootDir]` where `rootDir` is drawn from
`Object.keys(selectedProjectsGraph)`. When `opts.dir` and
`project.rootDir` differ in platform-normalized form (most often on
Windows due to drive-letter casing), the lookup falls through as
`undefined` and `pnpm add <pkg>` crashes with:

  Cannot destructure property 'manifest' of 'manifestsByPath[rootDir]' as it is undefined

Pin the graph key to `project.rootDir` in both `installing/commands/src/installDeps.ts`
and `installing/commands/src/import/index.ts`, so the keys stay in sync
with `manifestsByPath`. Closes https://github.com/pnpm/pnpm/issues/12379

Written by an agent (Claude Code, claude-opus-4-7).

* docs: remove redundant comments

* test(installing.commands): cover project graph keying

* Revert "test(installing.commands): cover project graph keying"

This reverts commit 426fae9434.

* test(installing.commands): cover add with mismatched project dir

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-14 15:08:42 +02:00
Felipe Santos
23716ed9b0 fix: preserve user-defined npm_config_* env vars in lifecycle scripts (#12400)
* fix: preserve user-defined npm_config_* env vars in lifecycle scripts

* fix: use released `@pnpm/npm-lifecycle` and port npm_config_* filter to pacquet

Pin the catalog to the released `@pnpm/npm-lifecycle` ^1100.0.0 instead of a
mutable PR-head ref, regenerating the lockfile to the immutable registry
tarball.

Port the upstream env filter to pacquet's make_env so user-defined
npm_config_* vars (e.g. npm_config_platform_arch) survive lifecycle scripts
while (npm|pnpm)_config_* auth keys are still stripped, matching
`@pnpm/npm-lifecycle` 9e2ac78148.

Harden the new TS test to save/restore npm_config_platform_arch.

* test(executor): restore env vars to pre-test value in lifecycle EnvGuard

The guard removed the seeded var unconditionally on drop, which would
discard any value the process env already had. Capture the original via
var_os and restore it (or remove only when originally absent).

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-14 14:35:44 +02:00
Zoltan Kochan
0fe6ea9a8a fix(setup): skip @pnpm/exe build scripts during global self-install (#12402)
When running from the standalone executable, `pnpm setup` installs pnpm
via `pnpm add -g file:<dir>`. The shipped `@pnpm/exe` package.json carries
preinstall/prepare scripts, which triggered a build-approval prompt for
pnpm's own install. pnpm links the platform-specific binary itself, so
these scripts are unnecessary (and unrunnable on a Node-less host); pass
--ignore-scripts to skip them.

Closes https://github.com/pnpm/pnpm/issues/12377
2026-06-14 11:59:47 +02:00
Zoltan Kochan
09f0bab194 test(deps-installer): make the parallel custom-resolver check deterministic (#12401)
The 'runs checks in parallel' test raced three real setTimeout delays
(10/20/30ms) and asserted their exact completion order. Timer scheduling
jitter on Windows CI runners reordered the sub-50ms timers, so the suite
flaked and failed every Windows test job.

Replace the timing race with a start-barrier: each hook blocks until all
hooks have started. This only completes if the checks run concurrently --
were they awaited one at a time, the first hook would wait forever for
siblings that never start. No timers, no ordering assumptions.
2026-06-14 11:59:34 +02:00
tsushanth
8dcd9a055c fix(installing.commands): show only names in checkbox summary (#12393)
Closes pnpm/pnpm#12386
2026-06-14 11:50:27 +02:00
MCMXC
7cdf9f82a6 fix(publish): forward strictSsl to libnpmpublish so self-signed registries work (#12396)
`pnpm publish` was ignoring `strictSsl: false` from `.npmrc` /
`pnpm-workspace.yaml` because `createPublishOptions` never included
`strictSSL` in the options object passed to `libnpmpublish`.
`npm-registry-fetch` (used internally by `libnpmpublish`) defaults
`strictSSL` to `true`, so the flag had to be forwarded explicitly.

Fixes pnpm/pnpm#12012.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 11:48:27 +02:00
Zoltan Kochan
681b593eb2 fix: support scope-specific registry auth tokens (#12392)
pnpm can now use different auth tokens for different package scopes, even when those scopes use the same registry URL.

Previously, auth was selected only by registry URL. If `@org-a` and `@org-b` both used `https://npm.pkg.github.com/`, they had to share the same token. This caused problems for registries that issue tokens per organization or per scope.

Configure a scope-specific token by adding the package scope after the registry URL in the auth key:

```ini
@org-a:registry=https://npm.pkg.github.com/
@org-b:registry=https://npm.pkg.github.com/

//npm.pkg.github.com/:@org-a:_authToken=${ORG_A_TOKEN}
//npm.pkg.github.com/:@org-b:_authToken=${ORG_B_TOKEN}

//npm.pkg.github.com/:_authToken=${FALLBACK_TOKEN}
```

`pnpm login --registry=https://npm.pkg.github.com --scope=@org-a` writes the token to the same scope-specific auth key.

When installing or publishing `@org-a/*`, pnpm uses `ORG_A_TOKEN`. For `@org-b/*`, pnpm uses `ORG_B_TOKEN`. Packages without a matching scope continue to use the registry-wide fallback token.
2026-06-14 11:43:30 +02:00
kimulaco
2da044434e fix(pacquet): preserve exec bit on copy fallback (#12385)
## Summary

Fixes #12171.

pacquet stores executable CAFS entries under paths ending in `-exec`, but the copy fallback tier in `link_file.rs` materialized them without the executable bit. When hardlink/reflink isn't available and the install falls back to `fs::copy` (e.g. overlayfs on CI), a native binary such as `@esbuild/linux-x64/bin/esbuild` lands at `0o644` and fails to spawn with `EACCES`.

This routes all copy-tier imports through one `copy_file` helper. On Unix, when the CAS source path ends with `-exec`, it OR-s the exec bits onto the copied file, mirroring `git-fetcher`'s existing `materialize_into`. Non-executable files are left exactly as `fs::copy` produced them.

#12177 (closed) took the same direction but re-read and re-applied the source's full mode to every copied file. This version only touches files the store marked executable (`-exec` suffix), so it never widens a restrictive mode (e.g. `0o600` → `0o711`) and adds no `set_permissions` syscall on the non-exec majority. Rather than duplicate that logic, the `-exec` check now lives in a shared `pacquet_fs::file_mode::cas_path_is_executable` that both copy paths call, replacing the private copy in `cas_io.rs`.

This is a pacquet-only bug. pnpm preserves the exec bit on its own copy path, so no pnpm-side change is needed.
2026-06-14 11:40:52 +02:00
Ahmed Abdelbaset
ab0b7d1847 feat(link): support --trust-lockfile flag on pnpm link to match other commands (#12374) 2026-06-14 11:30:30 +02:00
Zoltan Kochan
baf15021ec fix: sync pacquet lockfile output with pnpm (#12394)
* **New Features**
  * During installation, Pacquet can now optionally synchronize pnpm “package manager dependencies” from the workspace `package.json` policy, including when frozen-lockfile mode is enabled.
* **Improvements**
  * More accurate lockfile generation and resolution for optional dependencies, including platform metadata (such as `libc`) and package-manager metadata.
  * Enhanced resolver mirror support, including pnpm-style JSONL mirrors and filtered full-metadata caching/persistence.
* **Bug Fixes**
  * Improved lockfile pruning/traversal to generate smaller lockfiles without missing optional dependency reachability.
2026-06-14 10:56:14 +02:00
Khải
8dfe43857d chore(husky): add git hook rejecting bare @mentions in commit messages (#12387)
* chore: add git hook rejecting bare `@mentions` in commit messages

* fix(husky): align bare-mention boundary with GitHub linkification rules

* fix(husky): keep mention boundaries intact when stripping code spans

* fix(husky): ignore the verbose-commit diff below the scissors line

* refactor(husky): split bare-mention hook into small single-purpose functions

* fix(husky): exempt tilde-fenced code blocks from mention scanning

* fix(husky): trim trailing punctuation from the reported scoped handle

* refactor(husky): detect bare mentions by scanning, not regex

Replace the single packed detection regex with a plain character scan and
small predicate functions: for each `@`, exempt it when it sits inside
backticks (odd backtick count before it), when it is not followed by an
ASCII letter/digit, or when it is preceded by one (email-like). This is
easier to read and reason about than the regex.

Side effect: the `~~~` tilde-fence special case is gone, so a mention inside
a `~~~` block is now flagged. Backtick code spans and fences are still
exempt.

* docs(husky): note readHandle precondition that the char after @ is alphanumeric

* refactor(husky): spell out abbreviated identifiers in the mention hook

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-13 22:24:37 +02:00
Zoltan Kochan
5b402ea22b test(deps-installer): isolate the heavy deepRecursive test in its own process (#12388)
test/install/deepRecursive.ts resolves @teambit/bit's enormous circular and
peer-dependency graph. Measured in a CI-faithful container (Linux, Node
22.13.0, amd64, default ~4 GB heap) it peaks at ~3.6 GB — it fits the default
heap on its own, but not with the memory the other deps-installer test files
leave behind in the same jest process (the --experimental-vm-modules module
registry is not reclaimed between files). That overflow is the
"FATAL ERROR: Reached heap limit" the CI suite hit.

Run deepRecursive in a dedicated jest process (.test:heavy) so it gets the
whole default heap to itself, and run the rest (.test:rest) in a separate
process with it excluded via a negative-lookahead path pattern. The two runs
cover every test file exactly once.

This makes the earlier OOM workarounds unnecessary, so they are reverted:
- the 5-way sharding of the suite (deepRecursive was the sole culprit; the
  remaining files are a subset of what historically ran in one process), and
- the workerIdleMemoryLimit in the with-registry jest preset.

No global heap bump: every run stays within Node's default ~4 GB, matching
the budget pnpm has in production.
2026-06-13 22:21:25 +02:00
Zoltan Kochan
94c13cc068 ci: run clippy as a single-OS job and add it to the pre-push hook (#12389)
* fix(pnpr): pass batch_publish test request bodies by reference

The put_json/put_json_with_token test helpers took the JSON body by
value but only borrowed it for serde_json::to_vec, tripping clippy's
needless_pass_by_value under --all-targets. Take &Value instead, which
also drops an unnecessary body.clone() at one call site.

* ci: run clippy as a single-OS job and add it to the pre-push hook

Clippy was a step inside the three-OS Lint-and-Test matrix, so it ran
once per OS even though it lints the same platform-independent source
each time. Move it to its own ubuntu-only job, mirroring the existing
single-OS doc, format, and dylint jobs (platform-gated cfg blocks are
still type-checked per-OS by the test build).

It was also missing from pacquet/scripts/pre-push-rust.sh, so a clippy
lint that only fires under --all-targets — like the one that just
reached main — slipped past local pushes and surfaced only in CI. Add
the same --all-targets workspace clippy gate to the hook.
2026-06-13 20:23:16 +02:00
Zoltan Kochan
f1521cfc8c feat(publish): add --batch flag to publish all packages in a single request (#12299)
* feat(publish): add --batch flag to publish all packages in a single request

When publishing recursively, the new opt-in --batch flag packs every
selected package first and sends them all to the registry in one
"PUT /-/v1/multi-publish" request per target registry, instead of one
request per package. The endpoint is not part of the standard npm
registry API; registries that lack it are reported with
ERR_PNPM_MULTI_PUBLISH_UNSUPPORTED.

pnpr implements the endpoint with all-or-nothing semantics: every
document is validated (name, publish policy, attachment integrity) and
every tarball is fully written to a tmp slot before anything becomes
visible, so a batch that fails midway leaves no new versions behind.
The single-package publish handler was refactored into shared
validate/stage/commit steps, and package-lock stripes are acquired in
sorted order so overlapping batches cannot deadlock.

* refactor(publish): namespace the multi-publish endpoint under /-/pnpm/v1/

Vendor-namespace the batch publish endpoint as /-/pnpm/v1/multi-publish,
mirroring how the npm client keeps its registry extensions under
/-/npm/v1/. The unprefixed /-/v1/ namespace is effectively owned by the
npm registry API (login, search, tokens), so a future npm endpoint at
the same path could collide with different semantics. The pnpm prefix
makes the path unambiguously a pnpm-client extension while staying
server-implementation-neutral.

* refactor(publish): rename the batch endpoint to /-/pnpm/v1/publish

The endpoint is not inherently about multiple packages — a single
package is just a batch of one — so name it after the operation rather
than the cardinality. The unsupported-registry error is renamed to
ERR_PNPM_BATCH_PUBLISH_UNSUPPORTED to match the other BATCH_PUBLISH_*
error codes.

* feat(pnpr): crash-atomic publish commits via a journal

A publish (single-package or batch) is applied in several non-atomic
steps — one rename/upload per tarball, one packument write per package
— so a crash mid-apply could leave a batch partially published. Before
anything is promoted, the full intent (merged packument bytes + staged
tmp-file locations) is now persisted under .pnpr-journal/<txn>/ and
sealed with a single atomic rename of the commit marker. Startup
recovery rolls sealed transactions forward (every apply step is
idempotent, and the packument is re-merged into the current on-disk
state so versions published between a failed apply and the restart
survive) and rolls unsealed ones back, so a publish is either fully
visible or fully absent.

* test(pnpr): pin canonicalization of scoped dist.tarball URLs on serve

libnpmpublish-based clients submit dist.tarball with the scoped
filename (.../-/@scope/name-1.0.0.tgz). The registry never serves that
string verbatim: rewrite_dist_tarball rebuilds the URL from the
basename, so consumers always see the routable 4-segment form. The
batch publish scoped test now submits the real client wire form and
asserts the served packument exposes the canonical URL.

* test: align getConfig env-var warning test with request destination blocking

Project-level .npmrc request destinations (registry=, proxy keys) no
longer go through env expansion at all — they are dropped with a
dedicated warning — so registry=${VAR} can't produce the generic
'Failed to replace env in config' warning anymore. Exercise the
env-replace warning through cafile= (still expanded) and pin the
request-destination ignore warning in its own test.

* fix(pnpr): harden crash-atomic publish commit against partial states

Two reliability fixes from PR review:

- Journal recovery treated any I/O error probing the commit marker as
  "unsealed" (`try_exists(...).unwrap_or(false)`), so a transient error
  could roll back an already-committed transaction and delete its staged
  tarballs. Propagate the error instead, so startup fails loudly rather
  than risk losing a sealed publish.

- After sealing, the commit loop applied packages with `?`; a mid-loop
  failure returned an error while earlier packages were already visible,
  leaving a running server partially published until the next restart's
  recovery. On apply failure, complete the sealed transaction immediately
  via the same idempotent roll-forward, falling back to the original
  error with startup recovery as the final backstop.

* fix(pnpr): fail recovery loudly when a staged tarball can't be probed

roll_forward treated any fs::try_exists error on a staged tarball as
"missing", so a transient I/O error would skip promotion, still write the
packument, and delete the journal entry — advertising a tarball with
nothing on disk and no journal state left to retry from. Propagate the
error instead, mirroring the commit-marker probe, so recovery aborts and
the transaction survives for a later attempt.

* test: fix env-var warning assertion in the cafile getConfig test

cafile=${ENV_VAR_123} asserted the registry request-destination warning,
which is only emitted for registry/proxy URLs; cafile is still
env-expanded, so an unresolved placeholder surfaces the generic
"Failed to replace env in config" warning. Assert that instead and
retitle the test (the request-destination case has its own test).

* fix(pnpr): serialize package deletes with the same package lock

delete_package and delete_tarball mutated package storage without taking
the per-package lock that every publish and dist-tag path holds, so a
same-package DELETE could race a stage-and-commit and remove the package
or a tarball mid-write, leaving the on-disk state dependent on filesystem
timing. Acquire the lock before the removal, completing the same-package
serialization guarantee.

Also bind the batch-publish test stub URL to 127.0.0.1 to match the
listener, avoiding intermittent IPv6 connection failures.
2026-06-13 19:16:40 +02:00
Zoltan Kochan
29981f663b fix(pacquet): refresh lockfile when catalogs change (#12382)
## Summary

- detect catalog drift between `pnpm-workspace.yaml` and the lockfile `catalogs:` snapshot
- record and compare catalogs in pacquet's workspace-state fast path so plain `pacquet install` does not skip after catalog edits
- add lockfile, repeat-install, and CLI regression coverage for catalog changes
2026-06-13 18:44:29 +02:00
Zoltan Kochan
eb2dba3d7c test: recycle the registry-test worker to fix Jest OOM (#12384)
The `with-registry` Jest preset sets `maxWorkers: 1` to avoid the dist-tag
races that concurrent registry tests would cause. With no
`workerIdleMemoryLimit`, Jest's `shouldRunInBand` collapses `maxWorkers: 1`
to in-band execution, running every test file in the main process. Under
`--experimental-vm-modules` the VM module registry is never released
between files, so the process climbs to Node's default ~4 GB old-space
ceiling and dies with a heap out-of-memory FATAL ERROR (sharding the file
selection doesn't help — each shard still runs all its files in one
long-lived in-band process).

Setting `workerIdleMemoryLimit` both forces Jest to use a (single, serial)
worker instead of running in-band and recycles that worker once its heap
crosses the limit, reclaiming the leaked memory between files.
2026-06-13 16:28:58 +02:00
Zoltan Kochan
a98767a693 fix(resolving-deps-resolver): preserve deprecated metadata on reuse (#12383) 2026-06-13 16:28:30 +02:00
Zoltan Kochan
03143cad22 test: shard deps-installer integration tests (#12376) 2026-06-13 15:09:27 +02:00
Zoltan Kochan
4819fb4e66 fix(pacquet): match pnpm lockfile resolution (#12372)
## Summary
- Match pacquet peer-resolution and lockfile output to pnpm for transitive optional peer variants.
- Apply pnpm's Yarn compatibility package extensions in pacquet, with `ignoreCompatibilityDb` support.
- Add regression coverage on both the pnpm resolver test and pacquet resolver/install paths.

Fixes pnpm/pnpm#12330.
2026-06-13 15:07:59 +02:00
Truffle
f20ad8f1d2 fix: keep the path of git-hosted tarball resolutions in the lockfile (#12344)
* fix: keep the path of git-hosted tarball resolutions in the lockfile

* test(lockfile): cover git-hosted tarball path preservation in to_lockfile_form

Adds regression tests guarding that to_lockfile_form keeps the
TarballResolution.path of git-hosted subdirectory tarballs in both the
default and include_tarball_url branches, mirroring the TypeScript
toLockfileResolution tests added for pnpm/pnpm#12304.

* refactor(lockfile): simplify toLockfileResolution to a single kept-URL return

Invert the branching so the canonical-registry case is the early return and
the kept-URL resolution (integrity + tarball + optional gitHosted/path) is the
single fall-through. This removes the keepTarballUrl closure and the
preservingGitHosted helper; the URL-reconstruction check moves into
isCanonicalRegistryTarballUrl. No behavior change.

* refactor(lockfile): mirror the single kept-URL return in to_lockfile_form

Apply the same inversion as the TypeScript toLockfileResolution: the
canonical-registry case becomes the early return and the kept-URL resolution
is the single fall-through, removing the keep_url closure. The URL-match check
moves into is_canonical_registry_tarball_url. No behavior change; the three
to_lockfile_form tests still pass.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-13 12:35:12 +02:00
Zoltan Kochan
3d1a980036 test: fix CI resource usage (#12373) 2026-06-13 12:34:29 +02:00
Zoltan Kochan
7f2ac5ddc0 chore: allow selecting git-wt PR agent (#12371) 2026-06-13 01:44:47 +02:00
Zoltan Kochan
1310ab53c4 perf: close repeat-install and warm-resolve gaps (lazy lockfile, pre-runtime fast path, 304 freshness renewal) (#12364)
* perf(cli): keep the repeat-install fast path off the lockfile parse and thread spawns

The "Already up to date" short-circuit decides from manifest mtimes
alone (mirroring upstream checkDepsStatus, which never reads the wanted
lockfile), yet pacquet parsed pnpm-lock.yaml eagerly in State::init
before the check ran — a multi-millisecond YAML parse on every no-op
install, scaling with lockfile size (babylon's 720 KB lockfile dominated
its repeat-install wall time).

pnpm-lock.yaml now loads through a LazyLockfile (OnceLock-backed) that
install forces only after the fast path has passed on the run; add /
update / remove / outdated / the pnpr path force it up front, keeping
their behavior unchanged. The repeat-install regenerate branch probes
for the file's existence instead of its parsed contents, so the
fast path stays mtime-cheap.

The rayon global pool is likewise no longer built eagerly at startup:
the worker count is published via RAYON_NUM_THREADS (set in fn main
while the process is still single-threaded) so the pool spawns lazily
on first parallel use — commands that never reach a parallel phase no
longer pay 2×CPUs thread spawns.

A corrupt lockfile now surfaces its parse error when the install
actually reads the file; an up-to-date project with an unreadable
lockfile reports "Already up to date" exactly as pnpm does.

* perf(cli): finish up-to-date installs before building the async runtime

A plain `pacquet install` that is already up to date now completes on
the main thread, before the tokio runtime, the rayon pool, the HTTP
client, or any install state exists. The new
`install_already_up_to_date` twin of the repeat-install short-circuit
reuses the exact same workspace discovery and
`check_optimistic_repeat_install` inputs as `Install::run`, and the
CLI invokes it from the (now synchronous) `main` after clap parsing.

Gates mirror everything that would make the full path behave
differently: `--frozen-lockfile` / `--lockfile-only`, a configured
pnpr server (that path never runs the optimistic check), `--recursive`
/ `--filter`, config dependencies, and pnpmfile updateConfig hooks
(both can mutate the config the check compares against). Any gate or
error falls through to the full install path, which re-runs the check
and reproduces failures with their established error shape; the
"Already up to date" + summary emissions are byte-identical.

Repeat-install instruction count on the vue fixture drops from ~203M
to ~41M retired instructions — within a rounding error of the
`pacquet --version` floor (~39M).

* perf(resolving): renew metadata-mirror freshness on 304 Not Modified

The minimumReleaseAge freshness shortcut treats a metadata mirror
younger than the cutoff as authoritative and resolves without touching
the network. But a 304 revalidation never rewrote the mirror file, so
its mtime froze at the last 200 response: once a cached packument grew
older than minimumReleaseAge (24h by default), every subsequent install
re-validated every package against the registry, forever.

A 304 proves the cached packument equals the registry's current
document, so the validation clock legitimately restarts at the
response: bump the mirror's mtime to now (fire-and-forget — a
read-only cache dir only costs the next install another conditional
request). Applied to both stacks: pnpm's pickPackage notModified
branch and pacquet's fetch_full_metadata_cached 304 path.

On a vue-fixture install with a stale cache, the second warm resolve
drops from ~2s (520 conditional requests) to ~250ms (zero requests).

* style(resolving): use clippy's preferred Duration units in the 304 mtime test

CI clippy denies duration_suboptimal_units; from_hours / from_mins
replace the hand-multiplied from_secs values.

* style(package-manager): use clippy's preferred Duration unit in the sync fast-path test

Same duration_suboptimal_units deny as the previous commit, one site
CI's clippy surfaced after it stopped at the first failing crate.

* fix(lockfile): address review — dir-addressed LazyLockfile, read-open 304 touch

LazyLockfile resolved pnpm-lock.yaml against the process cwd while the
CLI honours a canonicalized --dir without chdir, so the deferred load
and the existence probe could consult a different lockfile than the
rest of the install (which derives lockfile_path from the manifest's
directory). The lazy handle now carries the manifest's directory and
loads via load_wanted_from_dir; lockfile-disabled config gets an
explicit disabled() constructor. The pre-runtime fast path builds its
handle from the same directory, keeping verdict parity with
Install::run.

renew_mirror_freshness opened the mirror with append just to bump the
mtime; set_modified only needs a file handle plus ownership (futimens
semantics), so a plain read-open also covers mirrors whose mode
dropped write permission.

* test(integrated-benchmark): compare best-of-N samples in the slow-start proxy test

The test raced two single wall-clock samples, and a loaded CI runner
can inflate the ramped-vs-flat comparison in either direction (macOS
runner measured flat 318ms vs ramped 304ms against a ~66ms model).
Scheduler stalls only ever inflate a sample, while slow start's ramp
overhead is structural and survives in every sample — so the minimum
of several runs per side is the noise-resistant estimator.

* chore(deps): bump esbuild to 0.28.1 to clear GHSA-gv7w-rqvm-qjhr

The new advisory (install-module RCE via NPM_CONFIG_REGISTRY,
patched in 0.28.1) fails the audit gate. 0.28.1 was published within
the minimumReleaseAge window, so the patched version is excluded from
the age gate — the same mechanism pnpm audit --fix uses — including
its '@esbuild/*' platform packages, whose versions move in lockstep
with the root package.

* fix(lockfile): make the unloaded presence probe match the loader's absence rules

The loader treats an empty file and an env-only combined document as
an absent wanted lockfile (Ok(None)), but is_loaded_or_on_disk probed
bare file existence, so the repeat-install path could skip restoring a
semantically-missing pnpm-lock.yaml. The probe now reads the file and
checks the main document is non-empty (Lockfile::wanted_exists_in_dir)
— the loader's exact absence rules, still without the YAML parse.

* fix(cli): build the rayon pool after the fast-path gate instead of injecting env

Publishing the worker count through RAYON_NUM_THREADS leaked the
variable into every child process the install spawns — lifecycle
scripts, node probes, git — and pnpm exposes no such variable to
scripts. Build the global pool with ThreadPoolBuilder again, but only
once the repeat-install fast path has declined: real installs pay
exactly the cost they always did, the no-op path still spawns no
workers, and the process environment stays untouched (which also
drops the unsafe set_var and its single-threaded contract).

* fix(lockfile): treat only NotFound as absence in the presence probe

A permission or I/O failure reading pnpm-lock.yaml reported the file
as absent, which would send the repeat-install path into the
regenerate-on-missing branch — overwriting an existing lockfile it
merely could not read. Only NotFound counts as absent now; any other
read failure reports presence, and the real load surfaces the
underlying error when the contents are actually needed.

* fix(resolving): open the mirror write-capable for the 304 touch, read-only as fallback

Windows' set_modified requires write-attributes access on the handle,
so the read-only open silently failed there (caught by the Windows CI
run of a_304_renews_the_mirror_mtime). Append-mode open carries that
access; the read-only fallback still covers Unix mirrors whose mode
dropped write permission, where timestamp syscalls need ownership
rather than write access.

* fix(package-manager): never short-circuit partial installs as already up to date

add and remove mutate the manifest in memory and persist it only after
Install::run returns, so the on-disk mtimes the optimistic
repeat-install check reads still describe the pre-mutation project.
With a fresh workspace state, `pacquet add X` right after a clean
install reported "Already up to date", skipped the entire install,
and then saved a package.json declaring a dependency that was never
resolved, lockfiled, or materialized (self-healing on the next run,
which sees the newer manifest mtime).

Gate the short-circuit on is_full_install, mirroring upstream
installDeps calling checkDepsStatus only for the plain-install
mutation, never for installSome / uninstallSome. The new
partial_install_disables_optimistic_short_circuit test fails without
the gate.

The bug predates this PR (the KeepAll gate has carried add since the
optimistic path landed) — surfaced by CodeRabbit review on
pnpm/pnpm#12364.
2026-06-13 00:10:06 +02:00
Khải
4759e0559a chore(rust/clippy): enable some lints (#12363)
* chore(rust/clippy): enable undocumented_unsafe_blocks

Move the SAFETY rationale out of the EnvGuard doc comment and onto the
two env-var unsafe blocks it actually justifies, so every unsafe block
in the workspace now carries its own // SAFETY: comment.

* chore(rust/clippy): enable unnecessary_safety_comment

Reword a comment in local_tracing that used the // SAFETY: prefix to
justify an expect() on safe code; the prefix is reserved for unsafe
blocks.

* chore(rust/clippy): enable todo

No occurrences in the workspace; the lint keeps stray todo!() markers
out of committed code from now on.

* chore(rust/clippy): enable unimplemented

No occurrences in the workspace; the lint keeps stray unimplemented!()
markers out of committed code from now on.

* chore(rust/clippy): enable exit

The one deliberate process exit — dlx propagating the spawned command's
exit status, matching pnpm — now carries an #[expect] stating that;
any new std::process::exit call site has to justify itself the same
way.

* chore(rust/clippy): enable infinite_loop

No occurrences in the workspace; a loop that can never terminate must
return ! from now on.

* chore(rust/clippy): enable mem_forget

The overrides test helper leaked its TempDir via mem::forget to keep
the fixture on disk; TempDir::keep() is the purpose-built API for
that. The two registry-mock forget(mock_instance) calls stay — the
leak is how the shared mock-registry process survives the spawning
test process — but now carry #[expect] so the intent is
machine-checked.

* chore(rust/clippy): enable get_unwrap

Replace .get(...).unwrap() / .get_mut(...).unwrap() with indexing
across the test suites (machine-applicable clippy fixes): the panic is
the same, but the intent is clearer and the panic message names the
missing key instead of 'unwrap on None'.

* chore(rust/clippy): enable unused_result_ok

Two best-effort calls in tests discarded their Result with .ok();
spell the discard out with let _ = instead, so .ok() is reserved for
actually consuming the Option.

* chore(rust/clippy): enable pathbuf_init_then_push

resolve_path built its joined path with to_path_buf + push; Path::join
expresses the same thing in one allocation-aware call.

* chore(rust/clippy): enable string_add

The phase-event fixture concatenated string literals at runtime with
String + &str; concat! assembles the same fixture at compile time.

* chore(rust/clippy): enable verbose_file_reads

No occurrences in the workspace; whole-file reads go through fs::read
/ fs::read_to_string rather than manual open + read_to_end loops from
now on.

* chore(rust/clippy): enable allow_attributes

Convert every #[allow] in the workspace to #[expect]. The conversion
immediately paid for itself by surfacing four suppressions whose lint
no longer fires:

- resolve_peers: a too_many_arguments expect on a three-argument
  method (stale since a refactor) - removed.
- resolve_peers: two dead_code expects on MissingPeerInfo fields the
  compiler considers used - removed.
- tarball: fetch_and_extract_zip_once stacked two copies of the same
  too_many_arguments suppression - deduplicated.
- ensure_file: a cfg_attr(windows, allow(unused)) on a mode parameter
  that IS used on Windows (via verify_or_rewrite) - removed.

The remaining windows-side cfg_attr suppressions become expect with a
reason, and fire correctly there (the parameters are genuinely unused
on that cfg).

* chore(rust/clippy): enable allow_attributes_without_reason

Every #[expect] in the workspace already carries a reason; the lint
locks the convention in.

* chore(rust/clippy): enable negative_feature_names

No occurrences in the workspace; cargo features stay additive
(no no-std / not-x style names) from now on.

* chore(rust/clippy): enable redundant_feature_names

No occurrences in the workspace; feature names won't restate that they
are features (use-x / with-x prefixes and suffixes).

* chore(rust/clippy): enable wildcard_dependencies

No occurrences in the workspace; every dependency keeps a real version
requirement instead of "*".

* chore(rust/clippy): drop allow_attributes and allow_attributes_without_reason

perfectionist is adding better-targeted replacements for both, so leave
the clippy versions off to avoid duplicate diagnostics. The #[expect]
conversions they prompted stay — they are valid without the lints and
already removed four stale suppressions.

* chore(rust/clippy): trim redundant lint comments

The enabled lints' names already say what they enforce, so the inline
notes and per-theme section headers restated the obvious. Drop them to
match the convention of the surrounding activations, keeping only the
two category headers that carry non-obvious context.

* chore(rust/clippy): drop get_unwrap

The lint only rewrites `x.get(i).unwrap()` to `x[i]`, erasing the word
"unwrap" from panicking call sites and making them harder to grep for.
No safety benefit, so revert the enablement and all of its rewrites.

This reverts commit f274ad8.

* chore(rust/clippy): revert allow_attributes enablement and its code changes

Undo the #[allow] -> #[expect] conversions from bde9865 and restore the
suppressions that conversion dropped. perfectionist is adding a
better-targeted replacement; notably it exempts `allow` inside
`cfg_attr`, so the #[cfg_attr(windows, allow(unused))] attributes are
restored as-is. The clippy lints themselves were already removed.

This reverts the code changes of commit bde9865.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-13 00:03:08 +02:00
Zoltan Kochan
93e974a7ce test(deps.compliance): use synthetic package in audit update test (#12368) 2026-06-12 23:57:11 +02:00
Zoltan Kochan
6648b53119 fix(pacquet): byte-identical lockfile — generation-once owner records + cycle-closing edges (#12365)
* fix(deps-resolver): write the owner's missing-peer record once per ownership generation

The owner-keyed first-walk record was overwritten on every owner pass,
so once the owning importer hoisted a peer into its own scope (e.g.
typanion under @yarnpkg/core, owned by hooks/read-package-hook), every
other importer's hoist of that peer was suppressed — upstream's
once-per-generation missingPeersOfChildren promise keeps the owner
walk's initial misses visible instead, and each importer hoists its
own copy. The record now stores the recording owner: the owning
importer writes once per ownership generation (its later post-hoist
passes never refresh it), an ownership change records afresh, and an
owner's report replaces a non-owner's provisional one.

Restores the (typanion@3.14.0) / (@babel/types@7.29.7) suffixes that
regressed with the deterministic-owner port; whole-monorepo lockfile
diff vs fresh pnpm 11.6.0: 11 -> 3 changed lines.
Part of https://github.com/pnpm/pnpm/issues/12266.

* fix(deps-resolver): keep a cycle's closing edge through the first re-entry

pacquet's cycle break dropped the edge at the first re-entry of an
ancestor package, so a snapshot like arraybuffer.prototype.slice's
lost its cycle-closing es-abstract dependency line. pnpm's buildTree
gate (parentIdsContainSequence) only drops a direct self-edge and the
second lap of the full parent…child sequence: the first re-entry's
node exists with its own back-edge pruned, and the
previously-resolved-children merge in the peer pass — already ported,
but dead code until now — restores the pruned children on the
repeated node's graph entry. Ported the gate to the seed walk, the
lockfile-reuse walk, and the lazy realization in the peer pass.

Whole-monorepo lockfile diff vs fresh pnpm 11.6.0: 3 → 0 — the real
lockfile document is now byte-identical, deterministic across runs.
Part of https://github.com/pnpm/pnpm/issues/12266.
2026-06-12 23:03:23 +02:00
dependabot[bot]
80af95a6f2 chore(cargo): bump chrono from 0.4.44 to 0.4.45 (#12353)
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.44 to 0.4.45.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.44...v0.4.45)

---
updated-dependencies:
- dependency-name: chrono
  dependency-version: 0.4.45
  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-06-12 22:53:59 +02:00
Zoltan Kochan
d2b42c2dfc fix(pacquet): per-level preferred-version fold + all-importers hoist rounds (#12357)
## Summary

Two parity changes for pacquet's resolver, continuing https://github.com/pnpm/pnpm/issues/12266. Measured on the monorepo (fresh state, `install --lockfile-only`, back-to-back vs **pnpm 11.6.0**), the real-lockfile document diff drops from **128 to 5 changed lines** (re-measured after rebasing over #12361/#12362: **132 → 11**, where 8 of the 11 are a divergence the pacquet side of #12362 itself introduced — see the analysis on pnpm/pnpm#12266 — and 3 are the known cycle-closing-edge gap).

### 1. Per-level preferred-version fold

pnpm extends the preferred-versions map per resolution level: after a package's direct dependencies settle, their `(name, version)` pairs join the map the *children's* subtree resolutions pick against ([resolveDependencies.ts#L717-L746](https://github.com/pnpm/pnpm/blob/ce9c096e8e/installing/deps-resolver/src/resolveDependencies.ts#L717-L746)). So `signed-varint`'s `varint@~5.0.0` dedupes to the `varint@5.0.0` its parent pinned as a sibling instead of drifting to `5.0.2`. pacquet picked against a static seed only; besides `varint`/`es-abstract`, this turned out to drive the remaining `jest`/`@types/node` duplicate variants too.

- The walk resolves a whole sibling level before any child subtree starts (upstream's postponed-resolution barrier): `resolve_node` splits into `resolve_node_seed` + `walk_node_children`.
- Each level layers its versions onto a new `PreferredVersionsOverlay` (O(1) `Arc`-chained layers in `resolver-base`); the npm picker folds the per-name view in as plain `version` selectors at both registry seams.
- The overlay's per-name view joins the per-wanted dedup cache key; lockfile-reuse subtrees keep the no-overlay path (exact pins).

### 2. Hoist rounds across all importers (deterministic barrier, same logic as pnpm)

pnpm resolves **every importer's initial wave before any peer hoist**, then repeats global hoist rounds (per round: each importer's required-peer loop to a fixpoint, then one optional-peer hoist) until no importer hoists ([resolveDependencies.ts#L335-L445](https://github.com/pnpm/pnpm/blob/ce9c096e8e/installing/deps-resolver/src/resolveDependencies.ts#L335-L445)). pacquet ran each importer's whole hoist loop before the next importer's initial wave, so an early importer's optional-peer pick couldn't see versions a later importer resolves — `@cyclonedx`'s `spdx-expression-parse` hoisted `3.0.1` where pnpm's barrier-visible map picks `4.0.0`. `resolve_importer_with_workspace` is now an `ImporterHoistState` (`init` / `run_required_round` / `hoist_optional_round`) driven by `resolve_workspace` in upstream's exact phase order. Both implementations are deterministic here; the rule is identical.

## Verification

- New regression test `child_resolution_prefers_parent_level_sibling_versions` (fails with the fold disabled) + full `resolving-*`, `package-manager`, `cli` suites: 1,242 tests pass; clippy `--deny warnings`, rustfmt, typos clean.
- Whole-monorepo diff vs fresh pnpm 11.6.0: 128 → 5 changed lines; consecutive pacquet runs byte-identical.
2026-06-12 22:00:59 +02:00
Zoltan Kochan
9b35a6004e fix(deps-resolver): make shared children resolution deterministic (#12362)
## Summary

- Make shared package child resolution deterministic by choosing the owner by depth, importer order, and parent path instead of async completion timing.
- Keep non-owner and stale occurrences lazy while reusing the current owner children and missing-peer context.
- Port the same behavior to pacquet and add TypeScript plus Rust regression coverage.

## Verification

- `pnpm --filter @pnpm/installing.deps-resolver test test/resolveDependencyTree.test.ts`
- `cargo test -p pacquet-resolving-deps-resolver`
- Pre-push hook: TypeScript typecheck, pnpm bundle, lint, spellcheck, meta lint, cargo fmt, cargo doc, cargo dylint, taplo format check

Fixes pnpm/pnpm#12358
2026-06-12 21:12:58 +02:00
Zoltan Kochan
43b5bf7520 perf(pacquet): cache build metadata during installs (#12360)
* perf(pacquet): cache build metadata during installs

* fix(pacquet): satisfy clippy for build helpers

* fix(pacquet): reduce prefetch row type complexity
2026-06-12 17:27:13 +02:00
Zoltan Kochan
2aa2eaa6ff feat(pacquet): hand custom resolvers the currentPkg of a re-resolving edge (#12361)
* feat(pacquet): hand custom resolvers the currentPkg of a re-resolving edge

Custom resolvers received currentPkg: null on every call because nothing
populated ResolveOptions.current_pkg. Mirror pnpm's hand-off
(currentPkg: extendedWantedDep.infoFromLockfile in resolveDependencies):

- the tree walker now derives each edge's prior lockfile snapshot key
  (importer refs satisfies-gated like pnpm's referenceSatisfiesWantedSpec;
  transitive keys from the parent's snapshot) and, when the edge
  re-resolves anyway, threads the recorded entry into the per-call
  ResolveOptions as currentPkg
- a Registry ({integrity}-only) entry regains its derived tarball URL,
  like pnpm's pkgSnapshotToResolution; pick_registry_for_package moved
  from pacquet-resolving-npm-resolver to pacquet-lockfile (next to
  npm_tarball_url) so the deps-resolver can route scoped packages, with
  a re-export keeping existing callers unchanged
- children of a freshly resolved parent that landed back on its
  recorded version keep their prior refs for currentPkg purposes
  (pnpm's non-updated parentPkg arm) via the new ReuseSource::PriorOnly;
  subtree reuse semantics are unchanged
- the per-wanted memo key includes the prior key so two edges sharing a
  specifier but recording different versions never share a
  currentPkg-dependent result

The e2e test ports upstream's 'custom resolver receives currentPkg on
subsequent installs': a forced re-resolve receives the prior entry with
the re-derived tarball URL, and echoing it back keeps the pinned
version.

* fix(pacquet): gate prior child refs on the canonical dep-path id

The 'parent landed back on its recorded entry' check compared the
recorded snapshot key against the raw resolver id, which is not the
canonical dep-path form: build_pkg_id_with_patch_hash may append a
(patch_hash=...) suffix and name@-prefix file:/git/tarball ids. Extract
the comparison into landed_on_prior_entry, which strips suffixes from
both sides (the recorded key's peers/patch hash via without_peer, the
resolved id via remove_suffix) so the gate keys on which package
version the parent is, like pnpm's parentPkg.updated flag.
2026-06-12 17:26:44 +02:00