Commit Graph

11983 Commits

Author SHA1 Message Date
Alessio Attilio
e72b482b6f feat(pacquet): implement find-hash (#12552)
- Added `find-hash` subcommand in `pacquet/crates/cli/src/cli_args/find_hash.rs`.
- Scans `StoreIndex` for `PackageFilesIndex` matching the given `digest`.
- Outputs matched package names and versions.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-22 01:30:23 +00:00
Zoltan Kochan
4d3fe4b495 refactor(pnpr): move resolver endpoints under the /-/pnpr namespace (#12565)
The pnpr resolver's two POST endpoints were the only proprietary routes
served outside npm's reserved /-/ namespace, where they overlapped the
package path space (a package literally named `v1`). Move them under the
/-/pnpr namespace alongside the existing capability handshake:

  POST /v1/resolve         -> POST /-/pnpr/v0/resolve
  POST /v1/verify-lockfile -> POST /-/pnpr/v0/verify-lockfile

The GET /-/pnpr handshake now advertises protocol version 0 to match, and
the Rust client's PROTOCOL_VERSION drops to 0. Keeping every pnpr route in
the reserved namespace removes the package-collision concern and lets the
disabled-resolver branch stop special-casing the old npm-compatible GET
paths.

No backward compatibility is kept: the resolver protocol is not yet
released. Server and both clients (Rust pacquet-pnpr-client and the
TypeScript `@pnpm/pnpr.client`) change together.
2026-06-22 01:13:15 +00:00
Zoltan Kochan
bed6c59f1f feat(pnpr): add registry/resolver feature toggles (#12563)
pnpr now exposes its registry and resolver surfaces as independent,
config-toggleable features (per-surface `registry:` / `resolver:` blocks,
both enabled by default; CLI `--disable-registry` / `--disable-resolver`).
This supports running a stateless resolver-only tier in front of an
existing registry, or a registry with no server-side resolution. At least
one surface must stay enabled, validated at config load and after CLI
overrides.

The resolver paths overlap the registry's catch-all param routes, so when
the resolver is disabled they are registered to an explicit 404 stub —
otherwise a capability probe (GET /-/pnpr) would fall through to the
registry and be proxied upstream, surfacing a 502 where clients expect the
"no resolver" 404.

OSV stays a top-level (cross-cutting) setting rather than nesting under a
surface. Registry-side OSV screening is not yet implemented; tracked in
pnpm/pnpm#12561.
2026-06-22 02:01:41 +02:00
Kirk Holloway
25a829ed1a fix(audit): combine min release age excl entries (#12538)
Updates the minimumReleaseAgeExcludes output to combine entries per module, matching rec in docs. (Closes pnpm/pnpm#12534).

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-22 01:49:23 +02:00
Zoltan Kochan
8928aed6a9 chore: reduce Qodo inline-comment noise on iterated PRs (#12546)
Qodo re-runs its full review on every push (`push_commands`), which on a
long-running PR reposts low-severity perf/style nits as fresh inline
threads each commit. Keep the per-push review (it does catch issues
introduced in later commits) but raise `inline_comments_severity_threshold`
from 2 to 3 so only higher-severity findings get standalone inline
threads. `comments_location_policy = "both"` is unchanged, so every
finding still appears in the review summary — nothing is dropped, it's
just not re-threaded inline on each push.

Written by an agent (Claude Code, claude-opus-4-8).
2026-06-22 01:48:09 +02:00
dependabot[bot]
a34bfc942e chore(cargo): bump insta from 1.47.2 to 1.48.0 (#12524)
Bumps [insta](https://github.com/mitsuhiko/insta) from 1.47.2 to 1.48.0.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.47.2...1.48.0)

---
updated-dependencies:
- dependency-name: insta
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 01:25:06 +02:00
Zoltan Kochan
b4fdfe9b33 fix(package-manager): save hosted-git adds in normalized shortcut form (#12557)
`pacquet add <name>@owner/repo#sha` (and GitHub / GitLab / Bitbucket URL
forms) previously saved the request verbatim. Save the resolver's shortcut
form instead — `github:owner/repo#sha` — matching pnpm's saved
normalizedBareSpecifier. The normalization runs only in the non-registry
fallback (a git shorthand isn't claimed by the npm resolver, so no registry
fetch happens), leaving registry ranges, npm: aliases, file:, link:,
workspace:, and tarball URLs untouched.

Related to #12548.
2026-06-21 16:48:02 +02:00
Zoltan Kochan
bd5390e24e feat(pnpr): add pluggable SQL auth backends (#12547)
Add feature-gated pnpr auth backends so deployments can build with libsql, PostgreSQL, or MySQL support instead of locking the registry to one database driver.

The auth state still resolves to the existing UserBackend and TokenBackend trait objects. Configuration now accepts backend.libsql, backend.postgres/backend.postgresql, or backend.mysql and rejects selecting multiple shared databases.

PostgreSQL and MySQL use a shared SQLx-backed auth implementation with driver-specific placeholder syntax. The shared auth schema uses common column types, and pnpr avoids SQLite-only upsert syntax in auth and verdict-cache writes.
2026-06-21 16:31:03 +02:00
Zoltan Kochan
49971fb5d6 feat(package-manager): port updateProjectManifest manifest writer (#12549)
Port pnpm's post-resolution manifest writer into the pacquet
package-manager crate as two modules mirroring pnpm's crate boundaries:

- update_project_manifest_object: port of @pnpm/pkg-manifest.utils'
  updateProjectManifestObject (field upsert with cross-field delete,
  peer-range derivation, guessDependencyType, empty-spec preservation).
- update_project_manifest: port of updateProjectManifest, matching
  resolved direct deps to wanted deps by alias (with a github: shorthand
  fallback) instead of by position, including the fix from #11373.

Both modules ship with the ported pnpm unit tests (14 total). They are
not yet wired into add/update; that integration (surfacing resolved
direct deps from Install and threading the catalog
userSpecifiedBareSpecifier) is tracked in #12548.

Related to #11267, #11373. Part of #12548.
2026-06-21 14:57:43 +02:00
Alessio Attilio
247201eb19 feat(pacquet): implement cat-file command (#12550)
Adds `pnpm cat-file` functionality to the `pacquet` CLI. This extracts the
base64-encoded file hash from an integrity string (e.g. `sha512-...`),
decodes it, and reads the corresponding file from the CAFS store directory
at `files/XX/YYYY...`.

Includes a new e2e test verifying correct lookup and read operations.
2026-06-21 14:44:59 +02:00
Rayan Salhab
322f88f4f1 fix: avoid mutating unrelated deps on failed optional updates (#11373)
updateProjectManifest previously reconstructed the pairing between each
resolved direct dependency and the wanted dependency it came from — first by
alias, then by specifier shape, then by array position. Every one of those is
fragile: a failed optional dependency drops out of directDependencies and
shifts a positional pairing (#11267), and an aliasless selector that resolves
to an alias already in the manifest matches the stale aliased entry instead of
the request.

Carry the originating wanted dependency on each resolved direct dependency
(PkgAddressOrLinkBase.wantedDependency -> ResolvedDirectDependency.wantedDependency),
set where the resolver builds the result and already has it in scope, and have
updateProjectManifest read rdd.wantedDependency directly. This removes all the
alias/position/specifier heuristics (and the earlier normalizeGitHubBareSpecifier
band-aid) and fixes both correctness gaps structurally.

Adds unit tests for the failed-optional (#11267), alias-collision re-add, and
aliasless-optional-failure cases.

Closes #11267

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes@example.invalid>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-21 13:24:21 +02:00
Alessio Attilio
bdc1b07f63 feat(pacquet): add restart and stop script commands (#12543)
Port the `restart` command from
<https://github.com/pnpm/pnpm/blob/d4a2b0364c/exec/commands/src/restart.ts>
to the Rust pacquet CLI. `restart` delegates to `RunArgs::run()` three
times (stop → restart → start), matching the TypeScript handler that
calls the `run` handler sequentially for each script name.

Add a standalone `stop` command alongside the existing `start` and
`test` shortcuts. `stop` silently succeeds when no script is defined
(like `start` with its `node server.js` fallback), matching npm
convention.

Both commands forward trailing args and the `--if-present` flag to
each script. Lifecycle hooks (`pre<name>`/`post<name>`) apply when
`enablePrePostScripts` is set, because each step runs through the
full `RunArgs` pipeline.

Tests cover:
- Sequential execution order (stop → restart → start)
- Failure propagation (stop fails → restart/start skipped)
- `--if-present` skipping missing scripts
- Args forwarding to each script
- Standalone `stop` with and without a script
- Regression guard for existing `start` command

Related: pnpm/pnpm#11633
2026-06-21 10:08:56 +02:00
Alessio Attilio
6367954be4 feat(pacquet): add create command (port from pnpm create) (#12542)
Ports the create command from pnpm's TypeScript implementation
(@pnpm/exec.commands/create) to Rust. The handler converts the
user-provided name to a create-* package name via
convertToCreateName and delegates to the existing dlx
infrastructure — matching pnpm's exact design where create.ts
calls dlx.handler().

Implements the Stage 3 'Script & process' item from
pnpm/pnpm#11633 (pacquet roadmap).

Changes:
- Add pacquet/crates/cli/src/cli_args/create.rs: command handler,
  name-conversion algorithm (13 unit tests covering all npm naming
  conventions), and error for missing args.
- Wire Create(CreateArgs) into CliCommand dispatch.
- Add 5 integration tests (error handling, happy path, args
  forwarding, --allow-build, -c/--shell-mode).
- Add create-touch-file-one-bin fixture to pnpr mock registry.
2026-06-21 10:06:59 +02:00
Zoltan Kochan
0d24a60236 feat(pnpr): block vulnerable versions with local OSV index (#12506)
Add local OSV npm database support to pnpr. When enabled, pnpr loads an OSV npm dump (an `all.zip` or an extracted JSON directory) from disk at startup and fails before serving requests if the configured database is missing, not a regular file, empty of npm advisories, or otherwise invalid. Resolution then uses the in-memory index rather than querying OSV during package selection. Package names are matched case-insensitively, OSV range events are sorted before evaluation, and per-record reads are size-bounded.

Add a resolver-time package version guard to pacquet's npm resolver. The guard can reject a concrete package version, after which the picker filters that version out of the packument and tries the normal selection flow again. pnpr uses this hook to avoid vulnerable versions while preserving existing semver selection semantics. When every matching version is rejected, the resolver returns a distinct AllVersionsBlockedError rather than reporting the spec as unsupported.

Check frozen, cached, verified, and freshly produced lockfiles against the local OSV index, gating tarball entries on the tarball URL rather than the tamper-prone gitHosted flag. The pnpr lockfile-verdict cache records a content-based OSV database fingerprint in its policy snapshot, so a changed vulnerability database invalidates previous cached verification passes and forces rechecking under the new data.

Add fallible try_router / try_router_with_auth constructors so callers that build the router directly can handle an invalid OSV database recoverably instead of panicking.

Add tests for OSV range matching (including out-of-order events), exact-version and case-insensitive matching, withdrawn handling, zip and directory loading, empty/non-regular-file rejection, pnpr config parsing, guarded re-pick and all-versions-blocked behavior, and tarball OSV-checkability.
2026-06-21 00:46:23 +02:00
Alessio Attilio
9d7627cca6 feat(pacquet): purge node_modules on layout changes matching validateModules (#12536)
Reads `.modules.yaml` unconditionally before dispatching the install path. If the layout settings (nodeLinker, hoist patterns, etc.) differ from the currently active config, it purges `node_modules` completely. This mirrors upstream's `validateModules` prune side-effects and prevents leaving behind stale dependencies or symlinks when layout settings change.
2026-06-21 00:42:21 +02:00
David Barratt
1cbb5f2355 fix(resolver): make shared-children missing-peer reuse independent of resolution order (#12514)
claimChildrenResolution let a non-owner occurrence of a shared package reuse the
owner's missingPeersOfChildren when `existing.owner.depth >= currentDepth ||
existing.missingPeersOfChildren.resolved`. The second clause made reuse depend on
whether the owner's resolution had settled by claim time. Under concurrent
resolution that timing varies run to run, so a deeper consumer inherited the
owner's missing peers on some runs but not others — flipping an optional
transitive peer (e.g. `@babel/core`, reached via styled-jsx) in and out of a
package's resolved peer suffix and churning the lockfile, with intermittent
`pnpm dedupe --check` failures in CI.

Drop the `.resolved` clause: reuse only when the owner is at the same or a deeper
depth, a function of the dependency graph rather than completion order.

The `.resolved` flag was introduced in pnpm/pnpm#5467 (closing pnpm/pnpm#5454) to
avoid a deadlock where, with auto-install-peers in a workspace, a shared package
awaited its own not-yet-settled missingPeersOfChildren promise. Removing the
clause is strictly more conservative — a shallower owner's promise is never
reused, settled or not, so no unsettled promise is ever awaited and the deadlock
cannot return. The pnpm/pnpm#5454 regression test still passes, as do the peer,
dedupe, and cyclic suites. The deterministic owner selection from pnpm/pnpm#12362
made shared-package ownership order-independent but had moved this timing branch
in verbatim without neutralizing it.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:45:57 +02:00
Zoltan Kochan
6379dc1a88 fix(release): write release artifacts to the repo-root dist (#12545)
The TypeScript stack was relocated under pnpm11/ in pnpm/pnpm#12537, which
moved copy-artifacts.ts to pnpm11/__utils__/scripts/src/. Its
`import.meta.dirname` + '../../..' now resolves to the pnpm11/ directory
instead of the repository root, so `pn copy-artifacts` wrote the release
tarballs to pnpm11/dist.

The Release workflow attests (`subject-path: 'dist/*'`) and uploads
(`files: dist/*`) from the repo-root dist, so a tagged release would have
produced a GitHub Release with no binary assets and a failing provenance
attestation, even though the npm publishes (filtered by package name)
still succeeded.

Resolve repoRoot to the actual repository root again and point the
artifact source directories at their new pnpm11/pnpm/ locations.
2026-06-20 20:35:17 +02:00
Zoltan Kochan
258b11274c fix(ci): point pnpm CLI bencher extraction at pnpm11/pnpm (#12544)
The TypeScript pnpm CLI was relocated from `pnpm/` to `pnpm11/pnpm/` in
pnpm/pnpm#12537, but the "Extract pnpm CLI e2e test duration" step still
passed `--package-dir pnpm`. That path no longer exists, so the
exec-summary lookup found no entry and the step exited 1, failing the
full TS CI test job on main.

Point `--package-dir` at the package's new location, `pnpm11/pnpm`.
2026-06-20 20:34:12 +02:00
btea
f6dde6246f ci: skip ecosystem-e2e on forks (#12541) 2026-06-20 20:18:47 +02:00
C. T. Lin
d3f68e2aa4 fix(audit): compute reachable vulnerabilities with Tarjan SCC (#12467)
`pnpm audit` enumerates the install paths to every vulnerable package. The
reachability-based pruning added in 11.5.1 (pnpm/pnpm#12087) lets the walker
skip subtrees that reach no unsaturated finding by precomputing, per node, the
set of vulnerabilities reachable from it.

That getter only memoised acyclic subtrees: a node whose subtree contained a
cycle was `complete === false`, and so was every ancestor up to the importer
roots. None of them were cached, so their reachable set was recomputed on every
query. Real dependency graphs commonly contain cycles, and a single cycle high
in the graph makes a large fraction of nodes non-memoisable, yielding an O(N^2)
walk. This matched the report in pnpm/pnpm#12212 exactly (CPU-bound, identical
audit output across versions).

Reachability is now computed with Tarjan's strongly-connected-components
algorithm. Every node is scanned once; all members of an SCC reach the same set
of vulnerabilities and share one set, finalised in reverse-topological order.
Cyclic graphs are handled in O(N + E).

The reachable set is used only to prune, so it must never under-approximate
(that would hide a real finding). Tarjan yields the exact set for every node,
so no finding can be dropped, and the path-recording logic is unchanged. The
getter returns ReadonlySet<string> so the shared sets cannot be mutated by
callers, and a missing memo entry (an impossible-by-construction state) throws
rather than silently returning an empty set.

A regression test asserts the read-count growth ratio between two cycle sizes
(L=200 and L=400) is sub-quadratic: the fix scales ~2x (linear), the previous
code ~4x (quadratic). Asserting the ratio cancels the per-node constant, so the
test is not brittle to constant-factor changes.

Closes pnpm/pnpm#12212.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-20 13:34:06 +00:00
Zoltan Kochan
8c3a372048 fix(pacquet): read the pnpm CLI manifest from its pnpm11/ location (#12540)
* fix(pacquet): read the pnpm CLI manifest from its pnpm11/ location

pnpm/pnpm#12537 moved the TypeScript pnpm CLI under `pnpm11/`, so
`pnpm_version_from` could no longer find `pnpm/package.json`;
`current_source_pnpm_version` then returned `None` and
`package_manager_to_sync` (and its test) failed.

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

* fix(pacquet): read pnpm's config-reader source from its pnpm11/ location

The `pnpm_default_parity` contract tests read pnpm's `defaultOptions` from
`config/reader/src/index.ts` in the TypeScript tree, which moved under
`pnpm11/` in pnpm/pnpm#12537. Point the path at the new location so the
tests stop failing with a missing-file panic.

Written by an agent (Claude Code, claude-opus-4-8).
2026-06-20 15:27:45 +02:00
Zoltan Kochan
12baab400d chore(coderabbit): scope the pnpm CLI product label to the pnpm11 directory (#12539)
Rename the `product: pnpm` label to `product: pnpm@11` and apply it only
when changes touch the pnpm11/ directory. After the TypeScript CLI moved
under pnpm11/, the old "any code outside pacquet/ and pnpr/" instruction
over-matched shared root files (workspace config, CI, docs), so the label
no longer cleanly tracked the TypeScript product.
2026-06-20 14:47:54 +02:00
Zoltan Kochan
fc2f33912e refactor: move the TypeScript pnpm CLI into a pnpm11/ directory (#12537)
The TypeScript pnpm CLI freezes at v11; pnpm 12 will be the Rust pacquet
port. To make that split legible, all TypeScript source, test, and build
directories move under a new top-level pnpm11/ directory. The name states
the version boundary rather than implying a behavioral fork, since the two
stacks are meant to behave identically.

Scope is source-only: the shared workspace root stays at the repo root.
pnpm-workspace.yaml, package.json, pnpm-lock.yaml, .pnpmfile.cjs,
.meta-updater, __patches__, .changeset, .husky, and the lint/spell configs
remain in place, so one pnpm workspace and one Cargo workspace still span
all three products. pnpr/client and pacquet/tasks/registry-mock stay as
cross-product workspace members.

Rewiring the move required:
- pnpm-workspace.yaml globs prefixed with pnpm11/
- root package.json script paths, eslint.config.mjs, tsconfig.lint.json,
  .gitignore, and CODEOWNERS updated
- .meta-updater/src/index.ts literals repointed (pnpm11/pnpm/package.json,
  pnpm11/__utils__, pnpm11/__typings__, and the main package directory)
- regenerated every moved package's repository/homepage URL via meta-updater
- pnpm11/pnpm/bundle-deps.ts and __utils__/scripts/src/typecheck-only.ts
  climb one more level to reach the repo root

.meta-updater stays at the repo root because @pnpm/meta-updater resolves
its config at <cwd>/.meta-updater/main.mjs.

TS CI (.github/workflows/ci.yml) now only runs when pnpm11/-relevant paths
change, via a dorny/paths-filter changes job plus a TS CI / Success
aggregate gate; branch protection should require only that gate.
2026-06-20 14:36:25 +02:00
Nightt
6545793845 fix: avoid updating allowBuilds when workspace is ignored (#12488)
pnpm install --ignore-workspace auto-populated ignored builds into the
allowBuilds map of pnpm-workspace.yaml, overwriting committed true/false
values with the "set this to true or false" placeholder — even under
--frozen-lockfile, which must stay read-only.

The ignore-workspace CLI flag is now a first-class Config field
(ignoreWorkspace, mirroring ignoreWorkspaceCycles) instead of being read
from the untyped cliOptions bag. handleIgnoredBuilds reads it directly and
skips writing to allowBuilds when the workspace is ignored. The strict
ignored-build failure is unchanged.

Closes pnpm/pnpm#12469

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-20 10:39:15 +00:00
Allan Kimmer Jensen
6c35a432be feat(sbom): add --exclude-peers to omit peer dependencies (#12443)
With `auto-install-peers` (default since v8), peer dependencies resolve into
the lockfile indistinguishably from a package's own deps, so `pnpm sbom` lists
them as the package's components. For a published-library SBOM that's wrong —
peers are supplied by the consumer. `--exclude-peers` drops them, plus any
transitive subtree reachable only through them.

peerDependencies come from the manifest (the lockfile carries no marker). The
collector filters those names at each importer's top level, so a peer's
exclusive subtree is never walked while a package also reached via a real dep
stays. With no `--filter`, every importer is walked, so each importer's own
manifest is resolved (via `safeReadProjectManifestOnly`, which tolerates a
missing manifest) and peers in workspace sub-packages are dropped too.

The flag name matches `pnpm list --exclude-peers`; the SBOM behavior is
stricter, pruning the exclusive subtree rather than only hiding leaf peers.
CycloneDX 1.7 has no scope or relationship for "consumer-provided peer", so
omission is the only spec-clean handling.

Known limitation: an aliased peer (`"x": "npm:real@1"` in `peerDependencies`)
is not excluded, since matching is by resolved package name. Aliased peer deps
are vanishingly rare in practice.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-20 12:11:04 +02:00
YES!HYUNGSEOK
05b95ab100 fix(network.fetch): reject non-retryable errors instead of throwing in the retry callback (#12517)
A non-retryable error code (e.g. SELF_SIGNED_CERT_IN_CHAIN) was thrown inside the
detached op.attempt callback, so the governing promise never settled: the caller
hung and the throw surfaced as an unhandled rejection that crashed the process.
Reject the promise instead, mirroring the retries-exhausted path right below it.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-20 02:18:36 +00:00
Trevor Burnham
a84d2a1157 feat(resolving): vendor get-npm-tarball-url as @pnpm/resolving.tarball-url + canonical-URL helper (#12513)
Add @pnpm/resolving.tarball-url, which builds and recognizes the canonical npm
tarball URL of a package. It vendors getNpmTarballUrl (previously the external
get-npm-tarball-url dependency) and adds isCanonicalRegistryTarballUrl.

@pnpm/lockfile.utils (toLockfileResolution, pkgSnapshotToResolution) and
@pnpm/installing.env-installer now import from the new package; the private copy
of the canonical check in toLockfileResolution is removed, and the external
get-npm-tarball-url dependency and its catalog entry are dropped. The vendored
getNpmTarballUrl is byte-for-byte equivalent to get-npm-tarball-url@2.1.0, so the
fetch paths that use it are unchanged.

Two correctness fixes are folded in while consolidating the logic:
- the scoped-package unescape now handles uppercase %2F as well as %2f
  (percent-encoding is case-insensitive), so canonical scoped URLs are not
  needlessly persisted;
- protocol-insensitive comparison strips only a leading http(s):// scheme via
  regex instead of splitting on the first :// (which could truncate a URL
  containing a later :// and yield a false-positive "canonical" match).

Both fixes are mirrored in the pacquet port (is_canonical_registry_tarball_url
in pacquet/crates/lockfile/src/resolution.rs) so the two stacks omit the same
canonical scoped registry URLs from the lockfile, with matching regression tests.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-20 01:02:30 +00:00
Alessio Attilio
1488db1d0a fix(hoist): replace stale non-GVS symlinks when enable-global-virtual-store is toggled on (#12494)
When a project transitions from non-GVS to GVS mode, hoisted symlinks at
`node_modules/.pnpm/node_modules/<dep>` still pointed into
`node_modules/.pnpm/<depPath>/node_modules/<dep>`. The
`symlinkHoistedDependency` function only checked `isSubdir(virtualStoreDir, ...)`
— with GVS active, `virtualStoreDir` is `storeDir/links`, so these old symlinks
were classified as "external" and silently skipped.

Added `internalPnpmDir` (derived from `path.dirname(privateHoistedModulesDir)`)
as a second check. Symlinks under `node_modules/.pnpm/` are now recognized as
pnpm-internal and correctly replaced, matching the pattern in `safeIsInnerLink`.

The broader TypeScript type inference breakage (#9739) also has an architectural
dimension: when TypeScript follows a symlink to storeDir/links/<hash>/, Node's
module resolution walk-up never reaches node_modules/. This is the same root
cause as #12437 (packages from links store can't resolve transitive deps) and
requires a separate architectural fix.

Closes pnpm/pnpm#9739
---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-20 00:13:50 +00:00
dependabot[bot]
c12a15f766 chore(cargo): bump ignore from 0.4.25 to 0.4.26 (#12525)
Bumps [ignore](https://github.com/BurntSushi/ripgrep) from 0.4.25 to 0.4.26.
- [Release notes](https://github.com/BurntSushi/ripgrep/releases)
- [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/ripgrep/compare/ignore-0.4.25...ignore-0.4.26)

---
updated-dependencies:
- dependency-name: ignore
  dependency-version: 0.4.26
  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-20 00:48:57 +02:00
dependabot[bot]
c59ae0a477 chore(cargo): bump which from 8.0.2 to 8.0.3 (#12523)
Bumps [which](https://github.com/harryfei/which-rs) from 8.0.2 to 8.0.3.
- [Release notes](https://github.com/harryfei/which-rs/releases)
- [Changelog](https://github.com/harryfei/which-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harryfei/which-rs/compare/8.0.2...8.0.3)

---
updated-dependencies:
- dependency-name: which
  dependency-version: 8.0.3
  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-20 00:48:25 +02:00
Zoltan Kochan
d7ba22ef95 docs: add review guide (#12522)
Add REVIEW_GUIDE.md as the repository's canonical code-review guide, distilled
from the maintainer's review history. Centralize the review framework: move the
AI Review Guidance out of AGENTS.md into the guide, move the engineering
conventions into AGENTS.md's Code Style section, and update the CodeRabbit and
Qodo configs to apply the guide and split review focus between the two bots
(CodeRabbit on correctness/conventions, Qodo on security/performance).
2026-06-19 23:33:39 +02:00
Khải
83457b5764 feat(pacquet): cover the gap in nodeLinker: 'hoisted' (#12510)
Port yarn berry's popularity-based ident preference (buildPreferenceMap /
getHoistIdentMap) into the real-hoist crate so that, when multiple versions of
one name compete for the root node_modules slot, the most-used version wins —
with the root's direct dependencies always preferred first. This matches pnpm's
hoisted node-linker layout, which pacquet previously diverged from by hoisting
the first-visited version.

Mechanism:
- build_hoist_ident_map / add_dependent / PreferenceEntry build a per-name list
  of candidate idents ordered most-preferred-first (root deps, then by the
  count of distinct dependents + peer-dependents, stable on ties).
- A new AbsorbDecision::Defer plus is_preferred_ident gates free-slot
  absorption: only the currently-preferred ident may take a free root slot; a
  non-preferred version stays nested.
- hoist_into_root performs a per-pass ident shift (VecDeque::pop_front) that
  promotes the next candidate when the preferred ident still hasn't reached the
  root, so a less-preferred version can land once the preferred one is proven
  unreachable.
- HoistCtx bundles root, border_names, and the ident map to keep hoist_subtree
  within the argument limit.

Tests: unit tests for the preference machinery and most-used-wins precedence, a
dep-graph workspace test, and two frozen-lockfile CLI e2e tests
(single-project and workspace) ported from
installing/deps-restorer/test/index.ts. TEST_PORTING.md is updated accordingly.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-19 16:59:36 +02:00
shiro
fbdc0ebaf9 fix(version-policy): treat multiple exact-version excludes as equivalent to a single disjunction (#12516)
`minimumReleaseAgeExclude` (and `trustPolicyExclude`) ignored every
rule after the first match for a given package, so two separate
exact-version entries like `[form-data@4.0.6, form-data@2.5.6]` could
still trip the policy for the second version while
`[form-data@4.0.6 || 2.5.6]` (a single disjunction entry) worked.
That made list semantics depend on whether the user happened to merge
versions into one `||` selector, which is surprising and unsafe for
supply-chain exclusion lists.

Walk every matching rule and merge consecutive `name@version[...]`
matches in source order with duplicates removed. A bare-name or
wildcard match still terminates the walk, with first-match precedence
between bare and exact rules: a wildcard listed after an
exact-version rule no longer silently widens the exclusion to every
version of the package, while a bare-name rule listed first keeps
its existing `AnyVersion` semantics. Apply the same change to the
pacquet port so both stacks stay in sync.

Closes pnpm/pnpm#12463

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-19 13:20:08 +02:00
Alessio Attilio
ee9fab5476 feat(cli): add pacquet why command (#12497)
Add `pacquet why <pkg>` command that shows the reverse dependency tree
for a given package. Reads the lockfile, inverts the dependency graph,
and renders a tree with cycle detection and deduplication.

Flags: --depth <n>, --exclude-peers (reserved for parity).
2026-06-19 13:07:55 +02:00
Allan Kimmer Jensen
17e7f2ce9e feat(sbom): add issue-tracker external reference from package bugs (#12445)
* feat(sbom): add issue-tracker external reference from package bugs

Emits a CycloneDX `issue-tracker` external reference for components (and
the root) whose package.json declares a `bugs` URL. The value is parsed
with `new URL()` and kept only when the protocol is http(s), so malformed
values and email-only bug contacts are dropped while uppercase schemes
still pass. A single shared helper handles both transitive components and
the root package. Like homepage and repository, this is populated from
package metadata, so it appears only when the store is read (not in
--lockfile-only mode).

* fix(sbom): emit the normalized issue-tracker URL, not the raw bugs value

bugsUrlFromField used new URL() only as a validity gate but returned the
raw input string, so a crafted http(s) `bugs` value containing CR/LF or
whitespace would pass verbatim into the CycloneDX externalReferences url
(format iri-reference). Return parsed.href instead: new URL strips
CR/LF/tab and percent-encodes spaces and control characters, so the
emitted url is always a clean, single-line value.

* fix(sbom): strip credentials from the issue-tracker URL

bugsUrlFromField returned new URL().href, which preserves any embedded
username:password. An SBOM is a shareable, often-published artifact, so a
bugs value like https://user:token@tracker/... would leak the credentials
into externalReferences[].url. Clear username and password before
emitting; the tracker URL itself stays useful.
2026-06-19 12:33:07 +02:00
Ryan Tarpine
fa7004b261 perf(npm-resolver): cache meta in memory for exact version matches. (#12458)
On the exact-version disk fast path in pickPackage(), promote the parsed
packument into the in-memory metaCache so later resolutions of the same package
in one install skip the disk read and parse. In large monorepos this brings
adding a package down from minutes to seconds.

The in-memory cache key is now registry-qualified via a shared
getPkgMetaCacheKey(registry, name, fullMetadata) helper. Previously the key was
only the package name (plus a `:full` suffix) while the on-disk mirror was
registry-qualified, so a package of the same name served by two registries in
one install could share one cache slot and resolve the wrong tarball/integrity.
The registry is threaded through createNpmResolutionVerifier's shared-meta reads
(readSharedMeta / readSharedMetaForTrust), which already noted that a
registry-qualified read required the resolver's metaCache key shape to change
first. Added a regression test that resolves the same package name from two
registries and asserts each gets its own tarball.

The Rust pacquet port already implements both behaviors (registry-scoped cache
key and disk-fast-path cache population), so no port is required.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-19 12:30:31 +02:00
Zoltan Kochan
0f11c4018f chore: update lockfile, Node.js, pnpm, and pacquet versions (#12520)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-19 12:07:54 +02:00
kimulaco
11ebd9d232 fix(pacquet): preserve exec bit on clone/reflink import path (#12502)
On Linux reflink_copy::reflink uses FICLONE, which clones only the data into a
freshly-created 0o644 target, dropping the exec bit pacquet encodes in the
`-exec` CAS suffix. pnpm/pnpm#12385 restored it for the copy tier but left clone
out, so an undeclared executable like `@esbuild/linux-x64/bin/esbuild` failed
with EACCES. hardlink shares the store inode's 0o755 and copy was fixed, so only
clone was affected.

Extract the restoration (already duplicated in copy_file and git-fetcher's
materialize_into) into a shared
pacquet_fs::file_mode::restore_exec_bit_from_cas_suffix and route clone through
it. In auto_link / clone_or_copy_link only a reflink failure downgrades; the
post-reflink restoration error stays terminal, so it can't trigger a hardlink
retry against the just-created file that would mask it behind AlreadyExists.

Fixes pnpm/pnpm#12500.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-19 11:19:32 +02:00
John-David Dalton
2dd79e372c feat(pacquet): honor NODE_EXTRA_CA_CERTS for custom CA trust (#12508)
* feat(pacquet): honor NODE_EXTRA_CA_CERTS for custom CA trust

pacquet's reqwest client trusts only its bundled webpki roots plus the
.npmrc ca/cafile material — it ignores NODE_EXTRA_CA_CERTS. pnpm running
on Node picks that variable up transitively via Node's TLS runtime, so a
native port has to read it explicitly to keep real-world parity for
users behind a corporate MITM proxy or a self-signed registry.

Load the named PEM bundle as additional trust roots in
default_client_builder (the single chokepoint every client routes
through), keeping the .npmrc-derived TlsConfig env-free. Additive and
lowest-priority; a missing, unreadable, or malformed file is silently
ignored, matching pnpm's silent treatment of a missing cafile. Documented
as the one deliberate exception to the TlsConfig no-env-vars parity note.

* perf(pacquet): load NODE_EXTRA_CA_CERTS once per for_installs

Addresses review on the NODE_EXTRA_CA_CERTS change:

- Read and parse the bundle once in for_installs via
  load_node_extra_ca_certs(), then clone the parsed certs into the
  default client and each per-registry client. Previously
  default_client_builder re-read and re-parsed the file on every call,
  i.e. once per per-registry override.
- Use the existing EnvGuard test helper (process-wide lock + restore on
  drop, panic-safe) instead of a hand-rolled lock with manual restore.
  The test now also asserts a valid bundle parses to one root and a
  missing file yields none.

* test(pacquet): align NODE_EXTRA_CA_CERTS test with aube's

Make the pacquet and aube tests mirror each other in form and coverage:
exercise the same four cases in the same order — empty value, valid
bundle (asserting one parsed root plus a successful client build), a
readable non-PEM file, and a missing file — each asserting
load_node_extra_ca_certs() yields the expected roots. Also align the
env-var read in load_node_extra_ca_certs to the same let-else + filter
form aube uses (no behavior change).

* docs(pacquet): fix broken intra-doc links in load_node_extra_ca_certs

The free function's doc used [`Self::for_installs`], but `Self` only
resolves in impl/trait contexts, so rustdoc flagged it as an unresolved
intra-doc link and the Rust CI Doc job (RUSTDOCFLAGS=-D warnings) failed.
Reference [`ThrottledClient::for_installs`] instead.
2026-06-18 21:38:13 +00:00
Zoltan Kochan
511e74200d fix: update vulnerable dependencies (#12503) 2026-06-18 19:42:35 +02:00
Zoltan Kochan
33745b892b fix(ci): grant pull-requests: write so the review label step can label PRs (#12501) 2026-06-18 16:11:04 +02:00
Zoltan Kochan
cb3d8e75bd test(exec): fix Windows failure in PnP require option test (#12499)
The 'exec should merge node options with PnP require option' test (added in
#12430) hardcoded an unquoted '--require=<path>' expectation. On Windows the
.pnp.cjs path contains backslashes, which makeNodeRequireOption quotes and
escapes for Node's NODE_OPTIONS tokenizer, so the assertion mismatched and the
test failed on Windows runners.

Derive the expected NODE_OPTIONS from makeNodeRequireOption itself so the
expectation matches the implementation's quoting on every platform.
2026-06-18 14:02:12 +00:00
Zoltan Kochan
8d17c7b8cd chore: update lockfile, Node.js, pnpm, and pacquet versions (#12441)
* chore: update lockfile, Node.js, pnpm, and pacquet versions

* fix: node range

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-18 14:35:34 +02:00
Zoltan Kochan
6ee484e3c5 fix(ci): run PR review automation for fork PRs via workflow_run (#12493)
Approvals on PRs from forks never got the `reviewed: coderabbit` label or a
Discord announcement. A pull_request_review run triggered by a forked PR is
granted a read-only token and no secrets, so the label step failed with HTTP
403 and the Discord step had no webhook.

Split the automation in two: the pull_request_review workflow now only records
the approval as an artifact (no token, no secrets), and a new workflow_run
companion runs from the default branch in base-repo context — where it has the
write-scoped token and secrets — to add the label and post to Discord.

The privileged half never checks out or executes PR content: it reads inert
data files, validates the PR number is an integer, maps the reviewer to a fixed
label, requests only actions:read + issues:write, surfaces a failed (vs absent)
artifact download instead of passing as a silent no-op, and scopes the Discord
webhook secret to the announce step.

Also documents that agents must open PRs using .github/pull_request_template.md,
since gh pr create does not apply it automatically.
2026-06-18 14:25:52 +02:00
Zoltan Kochan
93458600a8 chore(release): 11.8.0 (#12492)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
v11.8.0
2026-06-18 12:17:52 +02:00
Maël Nison
0474a9c3b1 feat: add support for package maps (#12430)
Generate a Node.js package map at `node_modules/.package-map.json` on every
isolated or hoisted install, including under the global virtual store, so that
third-party tooling can start experimenting with package maps. The file is
serialized compactly.

Two settings control how the map is consumed:

- `node-experimental-package-map` (default: off): inject
  `--experimental-package-map` into `NODE_OPTIONS` for the Node.js scripts pnpm
  runs — dependency lifecycle scripts, `pnpm exec`, and `pnpm run` (including
  recursive runs).
- `node-package-map-type` (`standard` | `loose`): choose between a strict map
  and one that tolerates hoisting-like access.

Covered by both the pnpm CLI and the pacquet (Rust) implementation.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-18 11:51:50 +02:00
Alessio Attilio
c2414e8236 docs: add PR template and clarify changeset purpose (#11290)
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-18 11:47:53 +02:00
Zoltan Kochan
fe66535046 fix: converge incremental install by refreshing stale transitive pins (#12472)
When a package is reused from the lockfile, its child edges are taken
verbatim and bypass the preferred-versions walk, so a transitive
dependency can stay pinned to an older version even after a direct
dependency resolved to a higher version that satisfies the same range —
leaving the lockfile non-convergent (an incremental install keeps a
duplicate that a fresh install would not).

The resolver now refreshes such a stale pin to the higher
direct-dependency version during resolution, via `preferredVersion`
(singular), which overrides the EXISTING_VERSION_SELECTOR_WEIGHT stability
bias. The older version is never resolved or fetched, and the incremental
result converges to what a fresh install produces. The pick is anchored to
direct dependencies (which resolve first), so it restores the automatic
dedupe removed in pnpm/pnpm#11110 without reintroducing its
non-determinism, and unlike the post-pass in pnpm/pnpm#11502 it does not
over-fetch.

pacquet is ported in the same change. Its full-subtree lockfile reuse is
coarser than pnpm's per-edge reuse, so it records per importer which direct
deps changed and their resolved versions, declines full-subtree reuse for a
parent that depends on a changed direct dep, and forces the higher version
in the child walk. Range satisfaction uses plain semver (not
prerelease-inclusive), matching pnpm's semver.satisfies(.., true).
2026-06-18 10:59:15 +02:00
Juan Picado
ff9a1cfa1d feat(pnpr): per-uplink verdaccio settings (maxage, timeout, max_fails, fail_timeout, cache) (#12375)
Add verdaccio's per-uplink tuning knobs to pnpr's proxy uplinks, parsed
from the same config.yaml shape verdaccio uses:

- maxage:       per-uplink packument freshness window; overrides the
                global packument_ttl when set, otherwise defers to it.
- timeout:      per-request deadline applied to every upstream fetch
                (default 30s).
- max_fails /   circuit breaker: after max_fails consecutive failures the
  fail_timeout  uplink short-circuits with a 503 until the fail_timeout
                cooldown lapses, then one probe per window is allowed
                through (defaults 2 / 5m). max_fails: 0 disables it.
- cache:        when false, tarballs stream through without being written
                to the local mirror (default true).

Interval values accept verdaccio's format ("2m", "30s", "1h30m", or a
bare number of seconds, as either a YAML string or a numeric scalar);
an unparsable or out-of-range value is a named InvalidConfig error
rather than a silent fallback or a panic. Defaults match verdaccio
(timeout 30s, max_fails 2, fail_timeout 5m, cache true).

Circuit breaker behavior:
- Only a 5xx or transport error counts as a failure; a non-404 4xx
  (auth / rate-limit / bad request) surfaces verbatim and never opens
  the circuit.
- The half-open probe is gated on the cooldown window, so exactly one
  probe runs per fail_timeout and a cancelled or dropped probe cannot
  stick the breaker open.
- An open breaker reports the configured uplink name, not its upstream
  URL, so the 503 does not disclose internal endpoints.

Part of the verdaccio-parity tracking issue pnpm/pnpm#11973.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-17 23:44:03 +00:00
Zoltan Kochan
bee4bf41ca fix: reject path-traversal config dependency names from the env lockfile (#12470)
Config dependency names and versions are read from the committed env lockfile
(pnpm-lock.yaml) and the legacy inline-integrity format in pnpm-workspace.yaml,
and both become path segments of the directories pnpm creates during install
(node_modules/.pnpm-config/<name> and the global virtual store's
<name>/<version>/<hash>). They were used unvalidated, so a malicious repository
could commit a traversal-shaped name (../../PWNED) or version (../../../PWNED)
and make `pnpm install` create symlinks or write package files outside those
roots — triggered on install, even with --ignore-scripts.

Add verifyEnvLockfile, an offline structural gate that validates every config
dependency and optional-subdependency name (must be a valid npm package name)
and version (must be an exact semver version) before any path is built from it.
It runs at the install boundary and, through a single writeVerifiedEnvLockfile
seam, before the env lockfile is ever persisted, so an invalid entry is rejected
with no write side effect. __proto__ names are rejected too (the validation
accumulators use null-prototype objects so the key can't slip past Object.keys).

The same fix and structure land in pacquet to keep the two stacks in sync.

Fixes GHSA-qrv3-253h-g69c.
2026-06-17 23:03:38 +00:00