Commit Graph

12136 Commits

Author SHA1 Message Date
Khải
39a030d47e style: formatting 2026-07-06 11:52:21 +07:00
Khải
10a800ade0 chore(rust): strip aggressive optimization to improve compile time 2026-07-06 11:41:28 +07:00
Zoltan Kochan
23996e9504 feat(pacquet): support global runtime install (#12814)
`pnpm runtime set <name> <version> -g` now installs the runtime
(Node/Deno/Bun) into the global packages directory and links its binary
into the global bin directory, matching the TypeScript pnpm CLI. It
previously errored with a "not supported yet" stub.

The `<name>@runtime:<version>` selector is routed through the existing
global-add pipeline. The wrinkle: pacquet's manifest writer folds a
`runtime:<version>` dependency into `engines.runtime` on save, so a
globally-installed runtime lands under `engines.runtime` with an empty
`dependencies` map. The global scanner and bin-linker read `dependencies`,
so without a fix they would link no binary. The global crate now reifies
`engines.runtime` / `devEngines.runtime` back into dependencies when
reading a group manifest — the same conversion the package manifest
reader already applies — so an installed runtime is treated as the direct
dependency it is. `list -g`, `remove -g`, and `update -g` therefore see
it too.

The TypeScript pnpm CLI already ships this feature, so this brings pacquet
to parity and needs no TypeScript change; pacquet crates are unpublished,
so no changeset is required.
2026-07-05 22:33:06 +02:00
Zoltan Kochan
1a3406d82d fix(cli): accept a --no-<flag> negation for every boolean flag (#12812)
pnpm parses its CLI with nopt, which auto-accepts a `--no-<name>`
negation for every option typed as Boolean. Pacquet's flags are plain
`#[clap(long)] bool`s that only accept the positive spelling, so a
forwarded `--no-frozen-lockfile` (or `--no-lockfile-only`, `--no-dry-run`,
and so on) aborted the parser with "unexpected argument" -- which broke
`pnpm install --no-frozen-lockfile` when the v12 binary ran the
pnpm.io benchmarks.

Add a generic augmentation layer instead of a per-flag paired negation:
`with_boolean_negations` walks the built clap Command tree and, for every
boolean flag that lacks a `--no-` sibling, adds a hidden negation that
`overrides_with` the positive flag. Last-one-wins semantics reproduce
nopt exactly (`--no-foo` leaves the default `false`, `--foo --no-foo`
resolves to `false`). Explicit hand-written negations are detected and
left untouched; global flags get a global negation so the override
reference propagates into subcommands. Because the negations are real
clap args, the grammar stays intact -- trailing pass-through args
(`run`, `exec`, `dlx`) and value-taking flags are unaffected, and
genuinely unknown flags still error.

Give the config-OR-merged booleans real force-off semantics. For
`trust-lockfile`, `offline`, `prefer-offline`, `frozen-store`, and
`ignore-scripts`, pacquet resolved the effective value as
`config_value || cli_flag`, so it could never turn a yaml `true` back
off from the CLI. Left to the generic negation alone, `--no-<flag>`
would parse but silently no-op against a config `true` -- a footgun for
the security-sensitive `trustLockfile`, where a repo-controlled
`pnpm-workspace.yaml` could keep verification disabled past a user's
explicit `--no-trust-lockfile`. Each now exposes an explicit `--no-`
flag, resolved via the shared
`resolve_bool_override(force_on, force_off, config)`: force-on wins,
force-off wins over a config `true`, an unset pair falls through to
config -- matching pnpm, where a CLI boolean overrides the
workspace/`.npmrc` value in either direction.

Wire every explicit `--flag` / `--no-flag` pair (the five above plus the
pre-existing `prefer-frozen-lockfile`) with mutual `overrides_with`
rather than `conflicts_with`, so both spellings in one argv resolve
last-one-wins instead of aborting the parser -- pnpm forwards raw argv
tokens, so a valid `--offline --no-offline` must not fail at the pacquet
boundary. The override collapses the pair to the last-specified, so
`resolve_bool_override` still sees at most one side set.

Also refresh the stale rationale in runPacquet.ts for why pnpm strips
the user's frozen-lockfile flags before delegating: pnpm resolves the
frozen-lockfile setting itself and encodes it in the mode it hands
pacquet (a resolving install, or a frozen materialization with an
injected `--frozen-lockfile`), so forwarding the user's own token would
contradict that choice via pacquet's last-wins negation override.
2026-07-05 10:58:33 +02:00
Zoltan Kochan
2dabb31dce fix(pacquet): skip unsupported optional deps on fresh install (#12809)
Rust pnpm was resolving optional dependencies and starting prefetch downloads before it computed installability on the fresh-lockfile path. That meant platform-specific optional packages for other OS/CPU combinations could be downloaded even though they would never be linked or built.

Run the same installability skip computation for fresh lockfiles before virtual-store materialization, thread those skips back to the modules manifest writer, and make the prefetch resolver avoid platform-incompatible optional tarballs. The prefetch shortcut only checks platform compatibility; the full installability pass still handles engine checks with the real host Node version.

This matches the TypeScript requester behavior, which skips fetching unsupported optional packages after resolution.
2026-07-05 08:42:49 +02:00
Zoltan Kochan
e0616d3550 fix(cli): support top-level command fallback (#12810)
Pacquet did not implement pnpm's top-level fallback command behavior. The TypeScript CLI parses an unknown top-level token as `run`, then falls through to `exec` when no matching script exists. That lets commands like `pnpm commitlint --edit ...` resolve local bins from `node_modules/.bin`.

Add an external clap subcommand for pacquet, route it through the existing run dispatcher, and fall through to the existing exec implementation only when no script matches. Scripts still take precedence over local bins, matching pnpm.
2026-07-05 08:40:33 +02:00
Zoltan Kochan
2b5e9bc54c fix(cli): print the bare version for --version and -v (#12808)
Clap's built-in version flag rendered `pnpm 12.0.0-alpha.0` and only
accepted `-V`, while pnpm prints the bare version (`12.0.0-alpha.0`)
and accepts `-v` — scripts that parse `pnpm --version` output would
break on the prefix. Replace the built-in flag with a `-v`/`--version`
arg using the Version action and catch clap's DisplayVersion
short-circuit in main to print PACQUET_VERSION without clap's
"name version" rendering.

The TypeScript CLI already prints the bare version (main.ts logs
packageManager.version), so no TS-side change is needed. Not covered
yet: pnpm's short-circuit also honors `--version` after a known
command name (e.g. `pnpm install --version`), which pacquet still
rejects as an unknown argument.
2026-07-05 01:15:17 +02:00
Zoltan Kochan
fbf78bd7d2 chore: fix lockfile (#12807) 2026-07-05 00:13:34 +02:00
Zoltan Kochan
e36ae78f7d fix(release): declare the MIT license in the generated native packages (#12806)
The generated `@pnpm/exe.<target>` manifests carried no license field,
so npm showed the natives as unlicensed. The v11 platform artifact
packages all declare MIT; the two wrappers already inherit it from the
committed manifest.
2026-07-04 23:48:56 +02:00
Zoltan Kochan
8d1d7590c8 chore: remove the redundant dependency-field validation from the pnpmfile pack hook (#12805)
The beforePacking guard threw for any package named 'pnpm' that
declares peerDependencies or optionalDependencies. From v12 the 'pnpm'
name is the Rust wrapper, whose generated manifest intentionally
declares optionalDependencies on the @pnpm/exe.<target> natives, so
the guard broke the Rust release.

The throw is redundant validation: the meta-updater config already
deletes those fields from the CLI manifest, so meta-updater --test
(run by lint in CI) rejects them before anything is packed. Keep only
the genuinely pack-time part - stripping dependencies / devDependencies
from the published manifest of the bundled TS CLI. The strip never
touches optionalDependencies, so it behaves identically for every pnpm
version and the v12 wrapper manifests pass through with their natives
intact.

Unblocks the staged wrapper publish in pnpm/pnpm#12804.
2026-07-04 23:48:38 +02:00
Zoltan Kochan
b7908f96e7 chore(release): stage the wrapper packages in the Rust release workflow (#12804)
The Rust release workflow authenticated every publish via npm trusted
publishing, but npm allows a single trusted publisher per package and
`pnpm` / `@pnpm/exe` have theirs bound to release.yml (the v11 TS
releases). The OIDC token exchange therefore returned 404, pnpm skipped
OIDC, and the unauthenticated PUT failed with E404.

Split the publish into two steps:

1. The `@pnpm/exe.<target>` natives are published only from this
   workflow, so they keep trusted publishing and go live directly.
2. The `pnpm` and `@pnpm/exe` wrappers are staged with
   `pnpm stage publish` using the static NPM_TOKEN. Nothing goes live
   from CI for those two names; a maintainer promotes them with
   `pnpm stage approve <stage-id> --otp`, which is where the
   proof-of-presence is consumed. Stage ids are teed into the job
   summary for the approver.

The steps must stay separate because `pnpm publish` skips OIDC as soon
as a static _authToken is configured (pnpm/pnpm#11495). `--provenance`
keeps working in both steps: it needs only the `id-token: write` OIDC
token, not trusted publishing. `set -o pipefail` guards the tee pipe
because the runner's default bash is -e without pipefail.
2026-07-04 22:51:50 +02:00
Zoltan Kochan
276d634ab1 fix(release): keep the pack-app config inside the packed project (#12801)
The Release job for 11.10.0 failed at the Publish `@pnpm/exe` step with
ERR_PNPM_PACK_APP_ENTRY_OUTSIDE_PROJECT: the pack-app hardening from
pnpm/pnpm#12669 rejects entry/output paths that escape the project
directory, and the `@pnpm/exe` build config used exactly that shape
(entry "../../pnpm.cjs", outputDir ".." relative to
pnpm11/pnpm/artifacts/exe).

Move the pnpm.app config to pnpm11/pnpm/package.json (entry "pnpm.cjs",
outputDir "artifacts") and run pack-app from the pnpm package root in
build-artifacts.ts. Output paths are unchanged
(pnpm11/pnpm/artifacts/<target>/pnpm), and the guard stays strict
instead of being loosened to a workspace-root anchor, which would
re-allow embedding files from outside the packed project.
v11.10.0
2026-07-04 21:42:11 +02:00
Zoltan Kochan
7cd1e4f4f6 chore(release): 11.10.0 (#12799)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-04 21:05:03 +02:00
Zoltan Kochan
2f389d6cf7 feat(release): auto-refresh embedded signing keys in the release PR (#12798)
* feat(release): auto-refresh embedded signing keys in the release PR

The create-release-pr workflow previously failed when the embedded npm
signing keys or Node.js release keys drifted from their canonical
sources, requiring a manual update PR before a release could proceed.
Now the workflow runs the update scripts in --update mode and commits
the refreshed keys together with the version bumps, so the new trust
roots are reviewed as part of the release PR diff. A synthesized
changeset records the refresh in the changelogs of the affected
packages.

Also embeds the Node.js release team's new signing key
655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD (Stewart X Addison), which the
last release run reported as missing, and fixes the Rust renderer in
update-node-release-keys.mjs so its output is lint-clean (angle-bracket
URL for dylint's bare-url lint, raw-string hashes only when the key
contains a quote for clippy's needless_raw_string_hashes) and matches
the committed file byte-for-byte on a no-drift run.

* fix(release): canonicalize npm key order and flag trust-root refreshes

Review follow-ups: sort the merged npm signing keys by keyid so a
reordering of npm's response cannot churn the generated file or
synthesize a spurious changeset, and leave an explicit warning comment
on the release PR whenever embedded trust roots were refreshed, so a
key change cannot slip through buried in a large version-bump diff.
2026-07-04 20:47:45 +02:00
Hichem Taboukouyout
d53917264f fix(pack): compute packed summary before postpack cleanup (#12777)
---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-04 19:43:41 +02:00
Abdullah Alaqeel
dcabb78620 fix(resolver): targeted updates resolve exactly like a fresh install (#12558)
Contract: `pnpm up <pkg>` produces the same lockfile a fresh install
of the same manifests would (equivalently: delete the target's
lockfile entries and run `pnpm install`).

pnpm (TypeScript):
- Thread a per-wanted-dependency `updateRequested` flag (true only for
  packages matching the user's update target) from the deps-resolver
  through requestPackage to the npm picker.
- At the picker, `stripLockfileVersionPins` subtracts
  EXISTING_VERSION_SELECTOR_WEIGHT from the target's version-type
  selectors (the lockfile seed's contribution, which is added onto
  manifest entries pinning the same version) and drops a selector only
  when nothing remains. Manifest pins, down-chain propagated versions,
  and range/tag selectors (audit-fix penalties) keep applying.
- Always seed `preferredVersions` from the lockfile, also during
  update mutations (pnpm/pnpm#10662); caller-supplied preferred
  versions (audit-fix penalties) merge over the seed per package name
  on a null-prototype target.
- New held-back-update warning at both npm picker entry points: when
  `updateRequested` and a kept preferred selector steers the pick
  below what the non-pin selectors alone would choose, warn once per
  (name, picked, preferred) recommending a pnpm.overrides entry.
- `pnpm update <dep>@<version>` for a transitive-only <dep> warns that
  the version part is ignored and recommends an override
  (pnpm/pnpm#12744).

pacquet (same behavior):
- The picker strips nothing: UpdateSeedPolicy::DropOnly already
  withholds the target's lockfile pins from the preferred-versions
  seed. `update_requested` (derived per wanted dependency via
  `is_update_target`, matching npm-alias and jsr real names) gates the
  held-back-update warning emitted by `warn_once_on_held_back_update`,
  with the same message as the TypeScript side.
- `pacquet update <pkg>@<version>` on a transitive-only package warns
  that the version part is ignored, mirroring the TypeScript wording.

Coverage: resolver-level tests in both stacks assert that the
lockfile-pin strip lets the target move, that manifest pins,
propagated versions, combined manifest+lockfile entries (manifest
weight preserved), and audit penalties keep steering, and e2e tests
assert the same-chain and importer-pin topologies dedupe under update
exactly as a fresh install does (fresh-install behavior verified
empirically against the registry mock). The transitive-version update
resolves to highest-in-range with the override recommendation, and
crafted package names (`__proto__@1.0.0`) cannot pollute
Object.prototype.

Closes pnpm/pnpm#10662.
Closes pnpm/pnpm#12744.

Co-authored-by: Minha Kang <118591632+m2na7@users.noreply.github.com>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Minha Kang <118591632+m2na7@users.noreply.github.com>
2026-07-04 19:22:14 +02:00
dependabot[bot]
07c9787a48 chore(cargo): bump reflink-copy from 0.1.29 to 0.1.30 (#12783)
Bumps [reflink-copy](https://github.com/cargo-bins/reflink-copy) from 0.1.29 to 0.1.30.
- [Release notes](https://github.com/cargo-bins/reflink-copy/releases)
- [Changelog](https://github.com/cargo-bins/reflink-copy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/cargo-bins/reflink-copy/compare/v0.1.29...v0.1.30)

---
updated-dependencies:
- dependency-name: reflink-copy
  dependency-version: 0.1.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 17:57:26 +02:00
dependabot[bot]
c09a1c5765 chore(cargo): bump pgp from 0.19.0 to 0.20.0 (#12782)
Bumps [pgp](https://github.com/rpgp/rpgp) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/rpgp/rpgp/releases)
- [Changelog](https://github.com/rpgp/rpgp/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rpgp/rpgp/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: pgp
  dependency-version: 0.20.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-07-04 17:56:56 +02:00
dependabot[bot]
be363b1b1c chore(cargo): bump bcrypt from 0.19.1 to 0.19.2 (#12781)
Bumps [bcrypt](https://github.com/Keats/rust-bcrypt) from 0.19.1 to 0.19.2.
- [Commits](https://github.com/Keats/rust-bcrypt/compare/v0.19.1...v0.19.2)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-version: 0.19.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 17:56:32 +02:00
Andrew Stegmaier
11a7fddb74 perf(npm-resolver): cache disk-loaded metadata in memory on offline resolution (#12760)
When resolving with --offline or --prefer-offline, pickPackage loaded a
package's metadata from the on-disk cache mirror but never stored it in
the in-memory metaCache, so every subsequent resolution of the same
package (once per dependent that references it) re-read and re-parsed
the full packument from disk. Store the parsed metadata in the
in-memory cache on those disk-return paths, so each package's metadata
is parsed at most once per command. On a large monorepo this collapses
23,746 metadata loads to 5,124 (66.8 GB of JSON parsed to 8.8 GB) and
takes pnpm dedupe --offline from ~140s to ~75s with a byte-identical
lockfile. The cache is the existing bounded LRU; online and network
paths are untouched.

Promotion alone would have made the top-of-function cache hit terminal:
a disk-promoted packument that predates a later-requested range would
turn a previously-recoverable network fallback into
ERR_PNPM_NO_MATCHING_VERSION. Track registry-unverified disk promotions
(a WeakSet keyed by object identity in TypeScript; a registry_verified
flag carried inside the cache value in pacquet, so meta and state are
read atomically) and let a cache hit whose pick fails on such an entry
fall through to the regular network-validating flow unless offline. The
revalidating fetch stores a verified entry, so each package revalidates
at most once. The pre-existing exact-version fast paths on both stacks
had the same latent failure mode and are covered by the same marking.
Pacquet's PackageMetaCache::set_unverified is a required trait method
so custom cache implementations cannot silently inherit the terminal
behavior, and the TypeScript PackageMetaCache documents the
object-identity contract its WeakSet relies on.

Also registry-qualify pacquet's verifier shared-meta-cache reads
(read_shared_meta queried bare name keys while the resolver stores
registry-qualified ones, so the fast path could never hit), mirroring
the TypeScript readSharedMeta lookup order.

Add an isolated-linker.fresh-resolve.hot-cache.offline benchmark
scenario (install --offline --lockfile-only against a warm mirror, with
an untimed online pre-warm pass and per-iteration node_modules +
lockfile wipes so the up-to-date short-circuit cannot skip the measured
resolution) to guard offline resolution on all Bencher testbeds. Fixing
it to measure real work also revived the pnpm benchmark pipeline,
silently dead since the pnpm11/ move: bench.sh resolved REPO_ROOT to
pnpm11/ where no Cargo.toml exists, the orchestrator probed the
pre-move pnpm/dist bundle path, and continue-on-error masked every
failure as a green run. The scenario measures this change directly:
pnpm@HEAD resolves the fixture offline in 2.41s vs 3.17s for pnpm@main
(1.31x), with metadata-mirror reads dropping from 2625 to 1297.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-04 17:50:02 +02:00
Cosimo Matteini
acbdb94853 fix(completion): complete workspaces with -F alias as for --filter (#12788) 2026-07-04 17:05:36 +02:00
Andrew Stegmaier
99982b9f00 perf(npm-resolver): normalize non-abbreviated registry metadata (#12772)
pnpm requests abbreviated package metadata (Accept:
application/vnd.npm.install-v1+json). Some registries (e.g. Azure DevOps
Artifacts) ignore that header and return the full CouchDB document, including
per-version fields the installer never reads (scripts, exports, devDependencies,
readme, custom package.json fields). pnpm stored that response verbatim in the
abbreviated metadata cache, so every later resolution re-read and re-parsed
those unused fields.

Fix the response at the boundary where it enters pnpm:
fetchMetadataFromFromRegistry inspects the response Content-Type. When an
abbreviated request comes back with anything other than
application/vnd.npm.install-v1+json, the registry ignored the header and served
the full document, so the fetch layer strips it to the abbreviated field set
(via the existing clearMeta helper) and re-serializes. Registries that honor the
header (the npm registry echoes the abbreviated Content-Type) take an early
return: no clearMeta, no re-serialization.

clearMeta is extracted from pickPackage.ts into its own module so the network
layer can use it without a circular import, and is made null-safe on versions.
pickPackage's abbreviated slot no longer needs special-casing.

Alternative to pnpm/pnpm#12766, which applied the same normalization in
pickPackage; both produce a byte-identical cache, this variant skips the work
for spec-compliant registries and removes resolver special-casing.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-04 17:01:09 +02:00
YES!HYUNGSEOK
84cf84c62e fix(package-manager): don't wipe node_modules on an included-deps change (#12676)
When .modules.yaml recorded a different `included` set than the current
install wants (pnpm install --prod <-> full, or --no-optional),
modules_consistent_with reported the directory inconsistent, driving the
purge that empties node_modules of everything except pnpm's own and
dotfile entries. A user's non-pnpm contents were deleted silently.

pnpm never purges the root project's node_modules for an included
mismatch (validateModules's lockfileDir !== rootDir guard); an included
change is satisfied by relinking plus a targeted prune of the removed
deps.

Split the check: the purge gate uses modules_layout_consistent_with
(every layout setting except included); the up-to-date fast path keeps
modules_consistent_with (still compares included), so a --prod<->full
switch still forces a reinstall. A new prune pass
(prune_direct_deps_excluded_by_groups) runs when the purge is skipped
for an included drift and removes the now-excluded direct-dep links and
their .bin shims — mirroring pnpm's removeDirectDependency — so excluded
deps don't stay resolvable. Only symlinks/junctions are removed, so the
user's own node_modules contents are never touched.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-04 15:18:48 +02:00
Alessio Attilio
ebb4096646 feat(pacquet-cli): implement pnpm peers command (#12706)
Add the peers command to pacquet-cli, which checks for unmet or missing
peer dependency issues by walking the lockfile, mirroring the TypeScript
CLI: one shared visited set across importers so each package is
evaluated once, missing-peer ranges merged via interval intersection to
detect conflicts, peerDependencyRules filtering (ignoreMissing,
allowAny, allowedVersions with parent-scoped selectors), deterministic
BTreeMap-ordered output, and a PeersOutcome mapped to exit code 1 in
the dispatch layer. Importer link: targets resolve relative to the
importer directory and are rejected when they escape the lockfile
directory.

Also fix the TypeScript peers-checker to drop conflicts of missing
peers removed by peerDependencyRules.ignoreMissing, so the JSON output
no longer reports a conflict while the text output reports no issues.

Related to: pnpm/pnpm#11633

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-04 13:29:45 +02:00
Alessio Attilio
fdfe172f6c feat(pacquet): route preResolution hook info/warn logging to pnpm:hook channel (#12681)
Route preResolution hook info/warn logging to the pnpm:hook channel

The preResolution hook's logger.info() and logger.warn() calls now emit
pnpm:hook events through the install reporter, matching the TypeScript
implementation in requireHooks.ts's createPreResolutionHookLogger.

- node_runtime.rs: pipe stdout/stderr in the one-shot node process and
  parse JSON logger lines from stdout, forwarding them to the Rust-side
  info/warn closures
- install_with_fresh_lockfile.rs: replace no-op info/warn closures with
  proper Reporter::emit(&LogEvent::Hook(HookLog { ... })) calls using
  LogLevel::Info/LogLevel::Warn and hardcoded from="pnpmfile"
- install/tests.rs: add pre_resolution_hook_log_is_forwarded_to_pnpm_hook_channel
  test verifying both info and warn events have the correct hook name,
  message, from field, and prefix

Related to pnpm/pnpm#11633

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-04 13:29:05 +02:00
Zoltan Kochan
c3e9fddf9e chore: update lockfile, Node.js, pnpm, and pacquet versions (#12751)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-04 11:19:10 +02:00
luo2430
ce5d5a57cb fix(pnpm): resolve relative patchedDependencies against lockfileDir (#12762)
Ensure patchedDependencies paths are resolved relative to the lockfile directory before computing patch hashes. This fixes cases where relative patch paths were incorrectly resolved against the workspace root instead of the lockfile location.
2026-07-04 10:59:29 +02:00
Ankit Pradhan
806c3ecd9f fix: suppress auth warning when PNPM_CONFIG_NPMRC_AUTH_FILE points at project .npmrc (#12679)
When npmrcAuthFile resolves to the same path as the project .npmrc, the user
has explicitly opted in to trusting that file for auth. Before this fix, the
warning was unconditionally fired for any auth env var in the project .npmrc,
even when PNPM_CONFIG_NPMRC_AUTH_FILE pointed at it.

Fix: compare the resolved npmrcAuthFile path against the workspace .npmrc path.
If they match, allow env var expansion and skip the warning.

Closes #12480
2026-07-04 10:39:11 +02:00
JSap0914
25c738883d fix(resolving): reject malformed jsr: and named-registry package names (empty scope/name, path separators) (#12677)
The jsr: specifier parser accepted scoped names with an empty scope or
name and with path separators inside the name; the malformed name was
folded into the @jsr/... npm name and flowed into registry URLs and
metadata cache file paths. It now throws INVALID_JSR_PACKAGE_NAME for
any name rejected by npm's package-name rules.

The named-registry specifier parser (e.g. gh:) had the same gap: the
scoped branch only rejected a missing '/' or trailing slash, and the
unscoped branch validated nothing, while the downstream pick-package
validator only rejects '/' in unscoped names. The parser now validates
the final package name in every branch (including alias-derived names)
and throws INVALID_NAMED_REGISTRY_PACKAGE_NAME for the same shapes.

Rather than hand-rolling the shape checks, both parsers delegate to the
validator the repo already ships: validate-npm-package-name
(validForOldPackages) in TypeScript and its existing pacquet port
is_valid_old_npm_package_name. Both fixes land in the TypeScript CLI
and pacquet, with matching tests.

---------

Co-authored-by: JSap0914 <JSap0914@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-04 00:50:41 +02:00
Alessio Attilio
e2e3c818f2 feat: add issues alias to bugs command (#12734)
Add the `issues` alias to the `bugs` command in the TypeScript CLI, so that `pnpm issues <pkg>` behaves identically to `pnpm bugs <pkg>`. The alias is registered via the bugs command's commandNames, removed from the not-implemented command list, shown in the help output, and covered by a CLI-level alias-resolution test. The pacquet CLI already ships the same alias.
2026-07-04 00:16:08 +02:00
Mateusz Burzyński
1e81761409 Expose web authentication authUrl and doneUrl in JSON error output when OTP is required in a non-interactive terminal (#12786)
Preserve web-auth OTP challenge URLs when OTP handling fails because pnpm is not running in an interactive terminal.

The web-auth layer now carries authUrl and doneUrl from the registry challenge onto OtpNonInteractiveError. The pnpm error handler includes those fields in parseable JSON error output when present.

The same URL preservation was added to pacquet's web-auth error type to keep the two implementations aligned. Tests cover the TS and Rust web-auth layers and the pnpm JSON error output path.
2026-07-03 22:37:15 +02:00
Zoltan Kochan
90dd34672b feat(pnpr): merge the namespace and the ACL into per-registry packages: maps (RFC pnpm/rfcs#17) (#12787)
* feat(pnpr): merge the namespace and the ACL into per-registry packages: maps

Implements the revised model from pnpm/rfcs#17, replacing the interim
patterns:-plus-global-ACL shape outright (pre-1.0, no compatibility
mode).

Every concrete registry now declares one packages: map whose keys are
its namespace (the former patterns: list) and whose values are the
per-package access/publish/unpublish rules (the former top-level
packages: block, scoped to the one registry that serves the name). One
declaration routes, filters, and authorizes. The registry-level access:
is the default an entry's omitted fields fall back to; publish defaults
to $authenticated and unpublish to nobody.

Selection is by specificity, not key order: an exact name beats
@scope/* beats @*/* beats **. YAML mappings are formally unordered, so
a formatter or yq round-trip must not change which rule applies; the
restricted pattern language makes the winner unique (at most one
matching key per tier), no entry can be dead, and a duplicate key is
the only within-registry error. Router sources: stay an ordered list
with their unreachable/shadowed-claim validation intact.

The removed top-level packages: block is a startup error naming the
per-registry replacement — it used to enforce access, so dropping it
like an unknown verdaccio key would silently open previously gated
packages on upgrade.

public: true keeps meaning the upstream fetch (no credential, no
headers, no registry-level access default), but per-package access
rules are now permitted on a public upstream — they gate who may read
the name through pnpr, not how pnpr fetches it. publish/unpublish
values on any upstream are rejected: no write can land there.

Hosted denials answer by tier, preserving both prior behaviors: a
caller the registry-level default denies is masked with 404 for every
name (a blanket-private registry never reveals which names exist, even
explicitly ruled ones), while an explicit entry denying a caller the
registry itself admits rejects loudly — 401 anonymous / 403
authenticated — so clients can prompt for credentials (the
registry-mock needs-auth contract).

The resolver's route classification resolves path-less fetches through
the registry graph — the same dispatch serving uses — and hosted
private-access descriptors are registry-qualified (registry NUL
package), so the same name@version on two hosted registries can never
share a cache key; unqualified descriptors from older builds fail
closed and re-resolve.

The bundled config.yaml moves the fixture namespace and the old ACL
into the local registry's packages: map (YAML anchors keep it
readable), and Config::proxy / Config::static_serve carry the
registry-mock rules programmatically.

Implements the pnpr side of pnpm/rfcs#17 only; no client or lockfile
changes.

* fixup: review + CI — missed consumer, indexed rule lookup, lint/typos

- pacquet-pnpr-client's integration test builds an UpstreamConfig
  literal; add the new rules field (this compile error was failing the
  CI test jobs and coverage).
- Index PackageRules::for_package by specificity tier (exact/scope
  maps + any-scoped/all slots) instead of scanning every rule: the
  lookup runs on every read, write, search hit, and route
  classification, and the tier chain makes the winner a map lookup.
- Sharpen the hosted_gate and classify_hosted docs: the effective
  per-package access decides — an explicit entry fully decides its
  names, including opening one name on an otherwise-private registry —
  and only the denial's *shape* varies by tier (default denial masks,
  explicit-entry denial is loud). Classification admits with the same
  lookup serving does, so the two cannot diverge.
- Appease dylint (single-letter closure params, intra-doc link),
  clippy (trailing comma), and typos (mis-order).

* fixup: gate alias selection by upstream per-package rules; search fast path

Route classification now consults the upstream registry's packages:
rules per name: a caller the effective access denies is never handed
the server-owned credential, so a fresh resolve fails closed exactly
where the serving endpoint would deny the read. Cache replay stays
registry-scoped by design (the alias descriptor names no package) —
documented at the descriptor gate.

Search skips a hosted registry outright when no rule of it could admit
the caller, restoring the pre-merge fast path: a blanket mask must not
become an enumeration or scan-timing primitive.

* fixup: drop the pre-mount shape from the benchmark cold-mock config

The cold-mock config carried three routing shapes so one file could
drive any benchmarked pnpr revision, on the premise that every server
ignores the blocks it doesn't recognize. HEAD broke that premise on
purpose: a top-level packages: block is now a startup error, so the
pnpr@HEAD revision mock refused to start and the benchmark job failed.

Keep the two shapes that still coexist (registries:/defaultRegistry:
and mounts:/defaultTarget:) and document that a pre-mount pnpr can no
longer share a config file with current ones.

* fixup: reject a bare top-level packages: key too

Option<IgnoredAny> maps a present-but-null packages: (a bare key, or
~) to None, slipping past the loud rejection. Detect presence through
a custom deserializer that consumes any value — including null — so
every spelling of the removed key fails startup identically.

* fixup: package-qualified alias descriptors for explicitly refined names

Cache replay was registry-scoped for alias descriptors, so a caller
passing the registry-level gate but denied by a per-package upstream
access refinement could replay a cached resolution a fresh resolve
would refuse them. The alias descriptor now carries the package name —
only when the upstream's rules explicitly refine that name's access —
and replay re-checks the refinement through the same per-package-aware
alias selection a fresh resolve uses. Unrefined names keep the plain
registry-scoped descriptor, so the common footprint stays one
descriptor per alias. The refined metadata mirror namespaces per
package for the same reason.

Also document why resolves_to_private_source treats every name on an
access-bearing upstream as caller-gated: unlike hosted registries, the
upstream registry-level gate is enforced independently at serving
(authorized_upstream runs before per-package rules), so a per-package
'access: $all' entry cannot open a name on a private upstream.
2026-07-03 21:49:06 +02:00
Hardik Dholariya
a6c4d5f1e8 fix: show available workspace versions on version mismatch (#12615)
When registry resolution fails (404 or no matching version) and a
workspace project with the same name exists only at non-matching
versions, surface ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE with
the available workspace versions instead of the raw registry failure.

- pacquet: reuse ResolveFromWorkspaceError::NoMatchingVersionInsideWorkspace
  rather than introducing a parallel error type; try_workspace_fallback
  returns the workspace error and the caller decides which failures to
  surface. Non-404 fetch errors propagate unchanged.
- TypeScript (pnpm11): the npm resolver's workspace fallback no longer
  swallows the mismatch error; on the fetch-error path it is gated on
  ERR_PNPM_FETCH_404 so auth, network, and server errors propagate
  unchanged.
- Tests cover: 404 + exact/range mismatch, registry-200 without a
  matching version, package absent from the workspace (404 propagates),
  non-404 errors not masked, and the still-working fallback match.

Closes pnpm/pnpm#1379
2026-07-03 16:07:33 +02:00
Zoltan Kochan
02ef9b57a5 feat(pnpr): declare patterns on registries, reduce routers to ordered sources (#12778)
Implements the revised model from pnpm/rfcs#16, replacing the
route-level-pattern shape outright (pre-1.0, no compatibility mode).

The config surface is renamed per the RFC: `mounts:` -> `registries:`,
`defaultTarget:` -> `defaultRegistry:`, and the Rust types follow
(`MountKind` -> `Registry`, `Mounts` -> `Registries`, the `mount` module
-> `registry`). The vocabulary is now: registry — a named surface pnpr
serves; origin — the external URL an upstream registry fetches from.

Hosted and upstream registries take an optional `patterns:` list — their
declared namespace (omitted = every name) — and a router collapses to an
ordered `sources:` list: a package resolves to the first listed source
whose patterns claim it. The namespace is enforced at the registry, on
every path to it: an off-pattern read is a definitive 404 answered before
storage or the upstream is consulted, and an off-pattern publish is
rejected with a clear reason — through a router and at the registry's own
`/~<name>/` URL alike. This closes the open hosted namespace (no dormant
stored state that a later source edit would surface as authoritative) and
stops an authorized caller from pulling arbitrary public names through a
private upstream's server-owned credential.

Validation translates to the same `covers()` machinery: unreachable
sources (all claims covered by earlier sources' union, including a
non-last pattern-less source), per-pattern shadowing across sources
(identical claims by two sources rejected, so bidirectionally-overlapping
namespaces fail in either order), duplicate sources per router, duplicate
patterns per registry, plus the carried-over unknown/self-referential/
router-as-source/empty-router checks. A registry's own internally
redundant patterns are allowed and do not count as self-shadowing.

Patterns live only in the registry graph (`Config::registries`), not
duplicated into the hosted/uplink tables: enforcement happens in
`Registries::resolve`, which every read, write, search, and cache-header
decision flows through, so the namespace is one declaration. The bundled
config.yaml moves the registry-mock fixture list onto the `local`
registry, and `Config::proxy` / `Config::static_serve` build the
equivalent graphs programmatically.
2026-07-03 13:38:24 +02:00
Zoltan Kochan
9ef4c01c98 feat(pnpr): derive the registry surface from declared mounts (#12773)
The top-level `registry:` config key duplicated information the config
already carries and could contradict it: `mounts:` together with
`registry: {enabled: false}` declared structure and then disclaimed it.
The npm-registry surface is now served iff at least one mount is
declared under `mounts:`, minus the per-tier `--disable-registry`
override. The `registry:` key is gone; a leftover one is ignored like
any other unknown key by the verdaccio-lenient parser. The `resolver:`
toggle stays: it is the only expression of a genuinely non-derivable
operator choice and duplicates nothing.

The account endpoints (adduser/login, whoami, profile, token listing
and revocation, logout) move out of the registry surface onto
dedicated always-mounted routes: they are pnpr account management, not
package-registry functionality, and a resolver-only tier must be able
to mint the tokens its own resolver surface demands
(`pnpm login --registry https://<resolver-host>/`).

The at-least-one-surface startup check becomes a nothing-to-serve
error: no mounts (or the registry disabled by flag) and the resolver
disabled. The mount graph is still built and validated on every tier,
and a registry surface disabled by flag still skips strict upstream
credential resolution.

Closes pnpm/pnpm#12767
2026-07-03 00:34:46 +02:00
Abdullah Alaqeel
991405eaff fix(default-reporter): restore ansi-diff to stop duplicated output lines (#12698)
* fix(default-reporter): restore ansi-diff to stop duplicated output lines

PR pnpm/pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

TypeScript CLI: restore the `ansi-diff` npm package for differential
rendering, wrapping its output with \r (column reset) + \x1b[0J
(erase below frame) to preserve the external-process-output fix
from pnpm/pnpm#12351.

Rust pacquet port: implement a zero-dependency differential renderer
(`diff.rs`) with the same algorithm, replacing the full-frame reprint
in `Sink::write_output`.

Closes pnpm/pnpm#12634.

* fix(default-reporter): pass dylint, add trailing newline, harden reporter test

Builds on the restored ansi-diff differential renderer:

- pacquet: guarantee a trailing newline before diffing a frame, matching the
  TypeScript reporter. This keeps an interactive prompt on a fresh line below
  the frame instead of joined onto the last progress line, and leaves the
  differ's tracked cursor column at 0 so it stays in sync with the \r
  prepended on the next update (otherwise the inline diff computes relative
  moves from a stale column and corrupts in-place rendering).

- pacquet: move the differ's inline test module into src/diff/tests.rs with
  explicit imports and drop a stray trailing comma, so cargo dylint passes.

- TypeScript: locate the sticky lockfile verdict's first render with findIndex
  instead of assuming writes[0], and wrap each reporter test body in
  try/finally so the subscription is always stopped.

* fix(default-reporter): align rust diff with ansi-diff edge cases

- Add col !== width guard before inline diff, matching the JS
  ansi-diff edgecase check that prevents mispositioning at exact-width
  wrap boundaries.
- Fix clearDown to start from the cursor's current row (self.row)
  instead of new_last_row, matching the JS _clearDown pattern. The
  previous code could clear frame content when trailing lines were
  unchanged and the frame shrank.
- Document the visible_width vs wcwidth simplification.

* test(default-reporter): expand diff coverage to 14 tests

Add 8 new tests covering:
- inline diff skipped for ANSI-containing lines
- inline diff threshold edge case
- full line rewrite with clear
- clear_down starting from cursor row (not new_last_row)
- soft-wrapped line height tracking
- empty frame clears all previous content
- multiple progress ticks (simulated install sequence)
- frame without trailing newline

* fix(default-reporter): tighten test assertions and avoid hot-path allocation

- clear_down_from_cursor_row: replace weak || assertion with direct
  checks that Line A is absent, Line B changed is present, and clear
  sequence is emitted.
- TS reporterRenderer: capture writes.length before the 'fetched'
  event and wait for an increase, so the test can't pass early.
- pacquet Sink::write_output: write \r, diff output, and erase codes
  as separate write_all calls instead of format!() which allocated a
  new String on every frame.

* fix(default-reporter): make frame redraw a single write and fix diff test

Compose the \r + diff + erase sequence into a reusable per-sink buffer and
emit it with one write_all, keeping each frame update atomic against
interleaved writers without allocating on the hot progress path.

Rework inline_diff_skipped_for_small_change (which actually exercised the
inline-diff happy path already covered by inline_diff_writes_only_changed_chars)
into inline_diff_skipped_for_short_common_affix, covering the
left + right <= 4 full-rewrite branch its name promised.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-02 22:56:44 +02:00
Zoltan Kochan
a3153604b3 fix(pnpr): repair registry-mock routing and migrate tests off real-npm writes (#12769)
* fix(pnpr): route the registry mock by exact name and restore its write ACL

The full-purity registry-mock config (pnpm/pnpm#12747) broke the TypeScript
test suite in ways TS CI never caught (it was path-filter-skipped on that
pnpr-only merge):

- The @zkochan/* and @pnpm/* routes claimed those entire REAL npm scopes
  with no fall-through, 404ing real packages that proxied dependency trees
  need (@zkochan/async-regex-replace, @pnpm/error). The fixture packages in
  those scopes are now routed individually; the rest of each scope proxies
  npm again.
- Unscoped names tests publish to the mock (test-publish-*, batch-*,
  project-100, ...) routed to the npmjs upstream, where a write is
  rejected. They are enumerated exactly; @pnpmtest/* covers
  dynamically-suffixed publish tests. Deliberately no unscoped prefix
  wildcards: a test-* route would swallow real packages like test-exclude
  (istanbul's dependency tree).
- The migration dropped the '**' ACL entry, and the built-in default
  admits no one to unpublish, so unpublish tests got 403. Restored:
  $all access, $authenticated publish and unpublish.

* test(pnpm11): stop dist-tagging and publishing over real npm packages

Under the mounts model a write to an upstream-routed name is rejected, and
the old materialize-on-write overlay is gone on purpose — so tests may only
write to packages the mock hosts. Migrate every real-npm write target to a
dedicated fixture:

- @pnpm.e2e/multi-version-{a,b,c} replace is-negative/is-positive/micromatch
  in the update, overwrite, and interactive-update tests.
- @pnpm.e2e/circular-{iterator,ext,symbol} replace the
  es6-iterator/es5-ext/es6-symbol circular trio; circular-ext requires
  ^2.0.1 so both circular-iterator versions land in the tree, which is the
  point of the concurrency test.
- @pnpm.e2e/function-with-clone replaces lodash where the test executes the
  installed code (module and module.clone are functions).
- @scoped/exports-function replaces @rstacruz/tap-spec in the scoped
  devDependencies-save test.
- @pnpm.e2e/has-build-metadata{,-dep} replace @monorepolint/{core,cli}: the
  dependency range carries build metadata (^0.5.0-alpha.51+f10fea0), which
  is what pnpm/pnpm#2928 is about; the hardcoded real-npm integrity becomes
  getIntegrity().
- The search tests query a hosted fixture (search scans hosted stores only).
- Dynamically-suffixed publish names move into the @pnpmtest scope, since
  exact routes cannot cover generated names.
- The vestigial addDistTag('foo') calls in the workspace-protocol tests are
  dropped; those resolve via workspace:, never the registry.

Read-only usages of real npm packages are untouched — they keep proxying.
getIntegrity() in the registry-mock helper also learns the proxy cache's
post-mounts layout (.pnpr-cache/~public/<digest>/), which the patch tests
depend on for proxied is-positive.

* fix(registry-mock): re-enumerate proxy-cache namespaces on every getIntegrity retry

The ~public namespace directory is created lazily together with the first
cached packument, so a candidate list built once before the retry loop could
never discover a namespace that appears while the retries are running.

* fix(pnpr): align Config::proxy routing with the bundled registry-mock config

Route the @pnpm and @zkochan fixture packages by exact name in
REGISTRY_MOCK_LOCAL_PATTERNS too, so pacquet's in-process test registry
proxies the rest of those real npm scopes exactly like the bundled
config.yaml does. Also filter the getIntegrity() proxy-cache namespace
enumeration to directories, so a stray file under ~public/ cannot turn a
retryable miss into an ENOTDIR error.
2026-07-02 22:51:48 +02:00
Allan Kimmer Jensen
9375797b46 feat(pacquet-cli): add pacquet sbom command (#12732)
Port of pnpm's sbom command to pacquet. Generates CycloneDX (1.5-1.7) and SPDX 2.3 SBOMs from the lockfile.

Ref: #11633
2026-07-02 17:04:43 +02:00
Adrian Niculescu
c1212355bf fix: respect transitive dependencies when sorting filtered projects (#12688)
The recursive command runners sorted opts.selectedProjectsGraph, which keeps
only the selected projects as keys. Edges to unselected projects were dropped,
so a transitive dependency between two selected projects (a -> b -> c with only
a and c selected) was lost and the two ran out of order.

sortFilteredProjects resolves the order through the full workspace graph: for
each sorted project it walks dependencies, tunneling past unselected projects
and stopping at selected ones, so a transitive relationship becomes a direct
edge. Projects without a real dependency stay in the same chunk, preserving
concurrency.

Prod-only filters (--filter-prod) prune dev edges. Their selected projects are
sorted through the prod-pruned full graph, so transitive prod deps are honored
without reintroducing the dropped dev edges. In a mixed selection each project
is sorted through the graph that matches how it was selected: regularly filtered
projects through the full graph, prod-only ones through the prod-pruned graph.
Carrying prod-only projects on the full graph would let a dev-only reverse edge
form a cycle and collapse a real prod dependency and its dependent into one
chunk.

Fixes pnpm/pnpm#8335

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-02 17:03:35 +02:00
Alessio Attilio
8491f8e26f feat(cli): implement prefix command (#12728)
Adds the `prefix` command that prints the local prefix path (the nearest
ancestor directory that contains a package.json, node_modules,
pnpm-workspace.yaml, package.json5, or package.yaml). With --global it
prints the global prefix.

TypeScript: new cmd/prefix.ts module, registered in cmd/index.ts and
removed from notImplemented.ts. Full integration test suite in
test/prefix.ts.

Rust (pacquet): PrefixArgs struct and find_local_prefix walk-up in
cli_args/prefix.rs; subcommand wired through cli_command.rs,
dispatch.rs, and dispatch_query.rs. Integration tests in
crates/cli/tests/prefix.rs.
2026-07-02 17:02:32 +02:00
Zoltan Kochan
72b1927856 feat(pnpr): registry mounts as the only routing model (RFC pnpm/rfcs#13) (#12747)
Model every addressable registry origin in pnpr as a registry mount at
/~<mount>/: a pnpr-hosted organization registry, a single-origin upstream, and
a router mapping package-name patterns to one concrete source. Provenance is
declared, never inferred — no configuration can express a cross-origin
fall-through. Full replacement of the legacy Verdaccio-shaped model.

New `mount` module: a decidable PackagePattern language with a covers()
superset relation; first-match authoritative resolution; and Mounts::validate,
which rejects shadowed/unreachable routes (including a non-last catch-all),
duplicate patterns, and unknown/self/non-concrete sources at config load.

mounts:/defaultTarget: is the only routing surface; uplinks:, packages: proxy:
fallback chains, hosted-first serving, and multi-uplink tarball fallback are
removed. Path-less and write requests route through the mount graph; a router
no-route is a 404 with no fall-through and a down source errors rather than
404s. Served tarball URLs stay canonical for the client's base. Per-package
ACLs apply on every mount-served read.

Each hosted-org mount has its own storage namespace (local dir and S3/R2) so two
orgs hosting the same name@version cannot collide; the org is threaded through
staging, commit, and the publish journal so crash recovery lands in the right
org. Public upstream mounts use a stable, secret-free cache namespace.

The bundled config.yaml and the integrated-benchmark mock config are converted
to the mount model; registry-mock keeps working with no task or seed changes.

Implements the pnpr side of RFC pnpm/rfcs#13 only; lockfile registry-identity
changes for the TypeScript CLI and pacquet are out of scope.
2026-07-02 11:45:23 +02:00
Alessio Attilio
ddbb4899c2 feat(pacquet): implement bugs command (#12687)
Port the `pnpm bugs` command to pacquet, following the structure of the TypeScript handler at `pnpm11/deps/inspection/commands/src/bugs/index.ts` and matching its error codes (`ERR_PNPM_NO_BUGS_URL`, `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND`). The command supports local manifest lookup and registry lookup by package name, with repository URL normalization for GitHub/GitLab/Bitbucket shorthand, hosted git URLs, and self-hosted git servers. Unit tests cover all URL derivation branches (36 tests). Integration tests cover the CLI entry points with local manifests and a mocked registry (9 tests).

Related to pnpm/pnpm#11633.
2026-07-01 18:09:02 +02:00
Zoltan Kochan
397f9783f4 fix: avoid passing infinite width to cli-truncate when streaming lifecycle output (#12750)
* fix: avoid passing infinite width to cli-truncate when streaming lifecycle output

cli-truncate 6.1.0 rejects a non-finite width, which crashed the reporter
when streaming lifecycle script output (streamLifecycleOutput passes Infinity
to mean "do not truncate").

* chore: drop changeset

The broken build this fixes was never released, so no release note is needed.
2026-07-01 09:15:29 +02:00
Zoltan Kochan
b6d29d212d chore: update lockfile, Node.js, pnpm, and pacquet versions (#12692)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-30 20:56:39 +02:00
Khải
f57f5af8bd docs(pacquet): remove TypeScript-code references from comments and document the parity paradigm (#12737)
pacquet and the TypeScript pnpm CLI are co-developed parallel
implementations at near-complete feature parity, with neither stack
downstream of the other. Remove the GitHub permalinks and the "mirrors
upstream's X" framing that pointed pacquet's comments at the pnpm
TypeScript source, and rewrite the affected prose to describe pacquet's
own behavior. This also resolves the dead permalinks the PR originally
set out to repair: the links are gone rather than re-pinned, so they
cannot rot again.

Removed every /blob/ and /tree/ permalink to the pnpm-org TypeScript
repos (over 1,700 links across ~330 files) plus the cited TS symbols and
"Mirrors/Ports/matches upstream's X" prose. Kept issue/PR/CI references,
pnpm/pacquet self-references, third-party attributions, Rust intra-doc
links, shared-contract identifiers (ERR_PNPM_* codes, the pnpm-prefixed
reporter wire format), and generic pnpm-product mentions.

Updated the contributor guides to state the new paradigm: root and
pacquet AGENTS.md (cardinal rule rewritten, permalink-citation guidance
removed), pacquet CODE_STYLE_GUIDE.md (reporter section reframed around
the shared wire contract), pacquet README, and a few plan/task docs.

The Rust changes are comment-only except for three #[expect(reason =
"...")] attribute strings that themselves cited a TypeScript symbol.
2026-06-30 20:53:14 +02:00
Khải
a6f303c2ff chore(rust/dylint): upgrade perfectionist 0.0.0-rc.22 (#12719)
* chore(dylint): upgrade perfectionist to 0.0.0-rc.21

Bump the perfectionist dylint library from 0.0.0-rc.17 to 0.0.0-rc.21 and
migrate the configuration to the rc.21 rule names and semantics.

Renamed config keys (perfectionist renamed every lint to name the
anti-pattern it flags):
- macro_argument_binding -> impure_macro_arguments
- derive_ordering        -> unordered_derives
- import_granularity     -> import_granularity_mismatch
- unit_test_file_layout  -> excessive_inline_tests

Remove three lint suppressions that upstream bug fixes made unnecessary
(each now reported an unfulfilled #[expect]):
- two bare_url suppressions on include_str!-ed fixtures (comment scanners
  now ignore include_str!-ed files)
- the import_granularity suppression in resolve_importer tests (the rule
  no longer folds a named item into self; the both-module-and-item case
  is left unflagged by default)

flat_module_pattern was removed from perfectionist in favor of
clippy::mod_module_files, which this workspace already enables; update
CODE_STYLE_GUIDE.md to match.

Tune the heavy new active-by-default rules for this change:
- needless_borrowed_parameters: disabled pending a parity-reviewed pass
  (https://github.com/pnpm/pnpm/issues/12715)
- bare_identifier_reference: narrowed to own-module, multi-word names
  (https://github.com/pnpm/pnpm/issues/12716)
- clap_help_markdown: code spans ignored; link leaks still flagged

Simplify the executor test gate now that excessive_inline_tests accepts a
compound #[cfg(all(test, unix))] on an external test module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* fix(lint): comply with new perfectionist rc.21 rules

Apply the rc.21 rule changes across the Rust workspace and tune the
heaviest new active-by-default rules in dylint.toml.

Code compliance:
- bare_identifier_reference: wrap backticked identifiers that resolve in
  the documenting item's own module as intra-doc links (~197 sites). The
  rule is narrowed to own-module, multi-word names; widening is tracked in
  https://github.com/pnpm/pnpm/issues/12716.
- avoidable_string_escapes: convert escape-only string literals to raw
  strings (74 sites).
- allow_attributes: convert #[allow] to #[expect] where the lint
  deterministically fires (8 sites). Drop three suppressions whose lint
  does not actually fire — a too_many_arguments on a 2-arg fn and two
  large_stack_frames blocks already neutralised by Box::pin (the rule
  flagged dead #[allow]s under its driver; #[expect] is unfulfilled under
  stable clippy).
- wildcard_imports: expand `use super::*` to explicit imports in six test
  modules (pacquet-cli, pnpr).

Config (dylint.toml): disable needless_borrowed_parameters
(https://github.com/pnpm/pnpm/issues/12715), unpinned_repo_ref
(https://github.com/pnpm/pnpm/issues/12717), and clap_help_markdown
(https://github.com/pnpm/pnpm/issues/12718) pending dedicated follow-up
passes.

Edits are limited to doc comments, raw-string conversions, lint
attributes, and import lists — no runtime behaviour changes. Verified
locally: cargo dylint, cargo doc, cargo clippy (all -D warnings), taplo
format --check, and the affected crates' unit tests all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* test(executor): restore the original Unix-only test gate

The rc.21 upgrade moved the gate from a plain `#[cfg(test)] mod tests;`
(with `#![cfg(unix)]` inside the test file) to `#[cfg(all(test, unix))]`
on the module declaration. No lint required that move: rc.21's
`excessive_inline_tests` recognises the external test module regardless
of where the Unix gate sits, so the original form was already clean
(verified with `cargo dylint`). Revert it and trim the now-obsolete
workaround rationale from the comment — the rc.17 `unit_test_file_layout`
compound-`cfg` misclassification it dodged is fixed in rc.21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(dylint): trim stale-prone comments in dylint.toml

Remove the verbose rationale comment above the `disable` list — its
per-rule counts and narrative would drift out of date; the disabled rules
are tracked in their follow-up issues and the commit history. Shorten the
`bare_identifier_reference` comment to the configured intent plus its
tracking issue, dropping the firing-count and default-behaviour narrative.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(dylint): disable perfectionist bare_identifier_reference

Its autofix blindly rewrites every doc code span into an intra-doc link,
which silently produces false intra-links when the span names an upstream
(TypeScript) type that only shares a name with a local Rust item. The rule
itself is sound, so disable it rather than keep narrowing it until the
autofix is removed or downgraded to MaybeIncorrect.

Keep the one site corrected by hand (graph-hasher dep_state) as a reference
for the right pattern: an explicit reference link to the upstream source
instead of a self-resolving intra-doc link.

Re-enable tracked in https://github.com/pnpm/pnpm/issues/12722.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(executor): drop the doc-comment churn in the test-module gate

The earlier revert rewrote the gate comment in tests.rs instead of
restoring it, leaving a documentation-only diff against the base. Restore
the original comment verbatim so this file is unchanged by the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(pacquet): link false intra-doc links to their upstream source

Several doc comments wrapped an upstream (pnpm TypeScript / yarn) type or
const name in [`Name`], which resolved to a same-named local Rust item — a
false intra-link that reads as "upstream's Name" but points at the port.

Convert each to an explicit reference link to the upstream definition
([`Name`][ts-Name] plus a commit-pinned [ts-Name]: <url> permalink),
sourcing the URL from the doc block's existing citation, the file's
module-level reference, or (for the handful with no in-repo citation) the
upstream definition located at pnpm main 6fadd7def9. SINGLE_LINE_KEYS lives
in the out-of-scope zkochan/js-yaml fork, so it becomes a plain code span
rather than a fabricated permalink.

Doc-comment-only change. Verified with cargo doc, cargo dylint, and cargo
fmt (all -D warnings / --check), plus a rendered-HTML check that every
reference link resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(lockfile): permalink SINGLE_LINE_KEYS to the js-yaml fork source

github.com/zkochan/js-yaml returns 404 (the fork is not a public repo),
verified against known-good controls (nodeca/js-yaml, pnpm/pnpm, and the
zkochan user page all 200). A repo permalink therefore cannot be formed,
so point the reference at the immutable, version-pinned npm CDN copy of
the fork's dumper.js, where SINGLE_LINE_KEYS (cpu/engines/os/libc) is
defined at lines 63-68.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(pacquet): pin upstream permalinks to their definition line ranges

Every upstream-source permalink added in this PR now carries a line anchor
(#Ln for a one-line definition, #Ln-Lm for a multi-line one) pointing at the
exact definition it cites, resolved against each link's pinned commit.

Three links pointed at a module that only imports or uses the symbol rather
than defining it; repoint them to the defining file: DepPath to
core/types/src/misc.ts, EnvLockfile to the lockfile.types index, and
InvalidWorkspaceManifestError to its dedicated error module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(pacquet): reword doc links that named non-existent upstream identifiers

Several doc comments linked a bracketed identifier that does not exist in the
upstream source. Replace each with plain English describing the code it
mirrors, keeping a permalink only where one resolves:

- render_specifier: upstream inlines the specifier string in its no-resolver
  error rather than naming a function; link the error block instead.
- HoistingLimits: yarn exposes only the lowercase `hoistingLimits` option
  (no named type); link that option's definition.
- ConfigFileChangeType / ConfigReport / EnvVariableChange: pacquet's own
  names for report shapes from pnpm's os.env.path-extender package, which
  lives outside the pnpm/pnpm monorepo (the old links 404'd). Name the
  package without a dead link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs: share reference

* docs(lockfile): point js-yaml fork links at pnpm/js-yaml

The `@zkochan/js-yaml` source repo is pnpm/js-yaml, not zkochan/js-yaml
(a hard 404). Repoint the module-level fork link there.

Also drop the `#L63-L68` anchor from the SINGLE_LINE_KEYS jsDelivr
reference: 0.0.11 has no tag in pnpm/js-yaml and the anchor is inert on
the raw CDN file, so it resolved to nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* fix(lint): resolve dylint findings surfaced by the main merge

Merging main brought two pre-existing items into the linted tree:

- pnpr storage tests imported `CachedTarballIntegrity` and
  `TARBALL_INTEGRITY_SUFFIX`, but main removed their uses; drop the now
  dead imports.
- integrated-benchmark's per-path `rm` retry command is escape-heavy as a
  normal string literal; make it a raw string (avoidable_string_escapes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(rust/dylint): upgrade perfectionist to 0.0.0-rc.22 and re-enable bare_identifier_reference

rc.22 downgrades `bare_identifier_reference`'s autofix from
`MachineApplicable` to `MaybeIncorrect` and rewords its diagnostic to warn
that a doc code span may name a foreign symbol of the same name — the exact
fix that justified disabling the rule (pnpm/pnpm#12722). Re-enable it at the
previously narrowed scope (`reference_scope = "own_module"`, `min_words = 2`;
widening still tracked in pnpm/pnpm#12716).

Comply with the 8 sites it flags in the merged tree — all foreign/upstream
references, so each gets an explicit reference link to its upstream source
(or a reword), never a self-resolving intra-doc link:

- `DependencyManifest` (resolving-resolver-base): reword; the upstream type
  is already linked earlier in the same doc.
- `WebAuthFetchOptions`, `WebAuthFetchResponse`, `WebAuthTimeoutError`,
  `OtpErrorBody`, `SyntheticOtpError`, `OtpNonInteractiveError`,
  `OtpSecondChallengeError` (the ported `@pnpm/network.web-auth` crate):
  link to their definitions in the upstream TypeScript package.

No other rc.22 change affects the workspace: no rules added, removed, or
renamed; no rule config-schema change; same nightly toolchain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* fix(lint): resolve rc.22 lint findings from the latest main merge

main's newly merged code tripped two rc.22 rules:

- The `repo` command's tests used `use super::*` (wildcard_imports);
  expand it to the four items the tests actually use.
- The integrated-benchmark `BenchmarkScenario` enum carried an
  `#[expect(clippy::enum_variant_names)]`. main's new
  `IsolatedFreshRestoreColdCacheColdStoreColdPnpr` variant ends in
  `Pnpr`, breaking the shared `...Store` suffix the lint had fired on, so
  the lint no longer fires and the expectation is unfulfilled. The
  suppression is dead (its own comment anticipated this once another
  linker bucket landed) — drop it; `#[allow]` would instead trip the
  re-enabled allow_attributes rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-30 00:27:17 +02:00
Zoltan Kochan
c4acfadd86 fix(pnpr): stream proxied tarballs instead of buffering-then-verifying (and benchmark the cold pnpr cache) (#12729)
On a cache miss the proxy buffered the whole tarball into the cache, verified
its SRI, and only then streamed the finished file — a head-of-line stall on
every cold fetch: the client waited for the full upstream fetch plus a verify
pass before the first byte, with no overlap. Buffer-then-verify is the wrong
model for a streaming proxy.

Stream the upstream response to the client while teeing it into the cache and
hashing it (stream_verified_to_cache); promote the cache entry only once the
full body matches dist.integrity. A mismatched/truncated/oversize body is never
cached and the client re-verifies what it receives, so a bad upstream tarball
can't poison a later client — it just can't be turned into a BAD_GATEWAY, since
SRI is only known after the body is already streamed.

Converge the namespaced /~<uplink>/ route onto the same model (the integrity is
known from the packument either way), removing the now-dead uplink integrity
sidecar machinery. Stop forwarding the upstream Content-Length on streamed
responses (attacker-controlled, unverifiable before streaming; kept only for
the internal early oversize check). Add a cold-pnpr-cache benchmark scenario
that actually exercises the cold download/serve path (every other scenario
warms the mock); it runs frozen so resolution variance doesn't mask the
serve-path delta.

The measured speedup on that scenario is modest and within noise (this fixture
is small tarballs over a latency-bound link); the change is a correctness fix,
and the benchmark confirms no regression.

Out of scope: packuments with no usable dist.integrity (pnpm/pnpm#12727).
2026-06-29 23:42:49 +02:00
Nikita Zhukovskii
cc04eb174e feat(cli): port the bin command to pacquet (#12703)
Port pnpm's `bin` command, which prints the directory where executables are
installed.

Locally it prints `<dir>/node_modules/.bin`: the `node_modules/.bin` leaf is
hardcoded, so a configured `modules-dir` is ignored, and the path is anchored
on the already-canonicalized `--dir`, mirroring pnpm's config reader.

`--global` resolves `global-bin-dir ?? <pnpm-home>/bin`, creates it, and runs
`check_global_bin_dir` (PATH membership + writability, since
`globalDirShouldAllowWrite` is true for every command except `root`) before
printing — the same `mkdir` + `checkGlobalBinDir` pnpm's config reader performs
for every `--global` command. Errors mirror pnpm:
`ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH`, `ERR_PNPM_NO_PATH_ENV`,
`ERR_PNPM_PNPM_DIR_NOT_WRITABLE`, and `ERR_PNPM_NO_GLOBAL_BIN_DIR`.

Ported from pnpm's bin.ts and config reader.

Related to pnpm/pnpm#11633.
2026-06-29 22:53:32 +02:00
Alessio Attilio
da9f834e34 feat(pacquet-cli): implement pnpm repo command (#12702)
Implement the pnpm repo command in pacquet-cli, which opens a package's
repository URL in the browser. Supports local manifest lookup and
registry-based lookup with version selection via dist-tags and semver
ranges. Handles SSH, git://, git+https, and shorthand repository URL
formats, strips .git suffixes, and expands shorthands to proper web
URLs. Uses the structured Reporter trait for browser-open error logging.
Includes 32 unit tests and 3 integration tests covering all URL formats,
error paths, and registry interaction.
2026-06-29 22:52:42 +02:00
Khải
d22f277613 feat(pacquet/cli): promote --filter/--filter-prod to recursive mode (#12726)
* feat(pacquet/cli): promote `--filter`/`--filter-prod` to recursive mode

A bare `--filter` / `--filter-prod` (without `-r`/`--recursive`) now puts
the command into recursive mode CLI-wide, matching pnpm's parse-cli-args
promotion. This is the prerequisite for driving release.yml, which runs
`pn publish --filter=<pkg>` without `-r`.

Also ports the `!{<workspace-root>}` augmentation from pnpm's main
dispatch: for run/exec, an all-exclusion (or unfiltered) selection
auto-excludes the workspace root unless the workspace is root-only
(patterns === ['.']). The recursive selection now filters with glob
directory matching (pnpm's useGlobDirFiltering default), which is
load-bearing for that augmentation: it excludes only the project at the
workspace root, not every nested package. pack stays out of the
auto-exclusion command set, matching pnpm.

Refs https://github.com/pnpm/pnpm/issues/12723

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBAmoo35Kr6K96ie7vG8tF

* test(pacquet/cli): cover pack root-inclusion and subdir root-exclusion

Strengthens coverage of the recursive root auto-exclusion, surfaced by review:

- `recursive_pack_includes_workspace_root` pins the
  `AutoExcludeRoot::Disabled` contract — a recursive `pack` keeps the
  workspace root. This was previously indistinguishable from `Enabled`
  because no test set up a root package.
- `recursive_run_from_subdirectory_still_excludes_root` exercises the
  non-trivial relative-path branch of the `!{<workspace-root>}`
  augmentation (running `-r run` with `--dir packages/<pkg>`), which had
  no coverage.

Also documents that pnpm's `--include-workspace-root` / `--workspace-root`
gates are not ported because pacquet surfaces neither flag yet.

Refs https://github.com/pnpm/pnpm/issues/12723

* test(pacquet/cli): cover --filter-prod all-exclusion root drop

Pins the load-bearing `follow_prod_deps_only` flag on the
`!{<workspace-root>}` augmentation, surfaced by review. A
`--filter-prod`-only all-exclusion must route the root exclusion into the
same production-only selection pass as the user's exclusion; otherwise the
union of the prod and non-prod passes re-adds both the workspace root and
the excluded package, silently running the script everywhere.

Refs https://github.com/pnpm/pnpm/issues/12723

* test(pacquet/cli): cover the select_recursive_projects error path

The `select_recursive_projects(...)?` in the recursive `run` / `exec`
runners propagates the filter engine's error, which a coverage report
flagged as uncovered. A `[<since>]` changed-packages selector is rejected
with `UnsupportedDiffSelector` (pacquet has not ported git-diff project
selection), and that error travels out through the `?`. Add integration
tests that pass `--filter [main]` to recursive `run` / `exec` and assert
the error surfaces (non-zero exit plus the explanatory message) rather
than being swallowed.

A real fixture (a `[<since>]` selector string) reaches this branch, so per
pacquet/AGENTS.md these are plain integration tests, not the dependency-
injection seam (which is reserved for branches a real fixture cannot
reach, e.g. filesystem error kinds or time).

Refs https://github.com/pnpm/pnpm/issues/12723

* test(pacquet/cli): assert recursive run/exec don't dispatch on a rejected selector

Strengthen the `[<since>]` diff-selector regression tests so they prove the
command short-circuits before dispatch, not merely that stderr names the
error. `run` asserts the build script's `ran.txt` marker is absent, and
`exec` now runs a marker-writing command (`touch ran.txt`) and asserts the
marker is absent. Per REVIEW_GUIDE.md, regression assertions must observe
the changed behavior rather than only the error message.

Refs https://github.com/pnpm/pnpm/issues/12723

* style(pacquet/cli): drop call-site comment duplicating the callee doc

The comment above `promote_recursive_for_filter()` in main restated the
function's own doc comment clause-for-clause (the recursive promotion, the
`parse-cli-args` parity, and the place-once-before-dispatch rationale all
already live on the `///`). Per the repo comment convention, a call site
must not re-explain what the callee documents; the method name carries the
intent and the doc comment carries the why.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-29 17:20:05 +00:00