`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.
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.
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.
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.
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.
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.
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.
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.
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.
* 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.
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.
Closespnpm/pnpm#10662.
Closespnpm/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>
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>
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>
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>
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>
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>
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.
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
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>
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.
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.
* 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.
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.
Closespnpm/pnpm#1379
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.
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.
Closespnpm/pnpm#12767
* 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`.
Closespnpm/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>
* 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.
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.
Fixespnpm/pnpm#8335
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
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.
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.
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.
* 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.
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.
* 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>
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).
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.
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.
* 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>