Commit Graph
132 Commits
Author SHA1 Message Date
Zoltan Kochanandgithub-actions[bot] 925c33d780 chore(release): 11.18.0, pacquet 12.0.0-beta.0 (#13481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 09:29:53 +02:00
Zoltan Kochan 2b03ceaba5 perf: reuse lockfile entries for safe override updates (#13464)
An overrides mismatch previously forced a complete dependency graph
resolution even when the lockfile could be updated safely in place.

Add conservative fast paths to both pnpm implementations for exact registry
version replacements and dependency-removal overrides. Replacements resolve
only their package metadata, retain satisfying child resolutions, prune dropped
edges, and attach added edges only when one safe compatible snapshot is already
locked. Independent replacements and removals can be processed together, and
parent-scoped removals match only the selected locked parent snapshots.

Fall back to the full resolver for peer-sensitive, ambiguous, patched, aliased,
exotic, policy-violating, or custom-hook cases. Remove obsolete virtual-store child links
while materializing the rewritten lockfile.
2026-07-28 23:51:22 +02:00
Zoltan Kochan c59e2a4c7b fix(pacquet): read legacy license fields (#13446)
TypeScript pnpm resolves package licenses from both the modern `license` field and the deprecated `licenses` field. Pacquet only accepted a string `license`, causing packages such as `exit`, `prettysize`, and `seq-queue` to be reported as `Unknown`.

Add a shared package-manifest extractor matching TypeScript precedence and legacy-field handling, and use it in `pnpm licenses list`. Cover the parser directly and verify the CLI result with a registry fixture.

Closes pnpm/pnpm#13438.
2026-07-27 23:26:13 +02:00
Zoltan Kochan 06a1ac32b9 fix(package-manager): validate pnpmfile checksum (#13418)
Compare the pnpmfile loaded for an install with the checksum recorded in the lockfile before using frozen resolution. Evaluate the module only when no checksum is recorded, which distinguishes a pnpmfile that exports hooks from one that does not without adding Node startup to the common matching-checksum case.

Keep optimistic repeat installs worker-free because workspace-state validation has already established that the pnpmfile is unchanged. Skip the comparison in pnpr because the resolver service has no access to the client-local pnpmfile.

Fixes pnpm/pnpm#13385.
2026-07-27 12:45:19 +02:00
Zoltan Kochan 7befc73efb fix(resolving-local-resolver): install local file: tarball dependencies (#13409)
A `file:*.tgz` dependency's name, version, and dependencies live only in
the archive's own `package.json`. The local resolver left `manifest`
unset for the tarball branch, so `build_pkg_id_with_patch_hash` had no
name to prefix and emitted the bare `file:<path>` as the dep path. That
parses as no lockfile key, and `build_packages_and_snapshots` dropped it
with a silent `continue` — the install reported success while writing no
`packages:` / `snapshots:` row and linking a dangling symlink into a
virtual-store directory that was never created.

Read the archive during resolution, as the tarball resolver already does
for remote tarballs and the git resolver for git deps: `pacquet-tarball`
grows `read_local_tarball_metadata`, which hashes the file and reads its
bundled `package.json` in one pass, replacing the resolver's hash-only
read. Nothing is written to the store — the install pass addresses a
`file:` tarball's store-index row by its `<name>@file:<path>` dep path,
not by the `<name>@<version>` a resolve-time extraction could key, so
such a row would never be read.

With the name in scope the dep path becomes `<name>@file:<path>`, both
lockfile blocks are emitted, the tarball's own dependencies are walked
(they were dropped entirely before), and the install summary prints the
version instead of the raw specifier. The lockfile is byte-identical to
pnpm 11's for the same manifest.

The manifest is borrowed out of the decompressed archive buffer rather
than read through the tar entry, so an entry header can't size an
allocation; the offset/size arithmetic both call sites need is extracted
into `tar_entry_payload`.

Reading the manifest puts its `name` into the dep path and into a
`node_modules/<name>` directory, so the shapes that used to fail
silently are refused the way pnpm refuses them: a `package.json` that
isn't valid JSON raises `ERR_PNPM_TARBALL_EXTRACT`, one that names no
package raises `ERR_PNPM_MISSING_PACKAGE_NAME`, and one whose name isn't
a valid npm name raises `ERR_PNPM_INVALID_DEPENDENCY_NAME`. Degrading to
"no manifest" is right for the extraction path, whose consumers re-read
from disk, but here the manifest is the package's only source of
identity, so clearing it reproduces the bug above.

An archive with no `package.json` at all stays tolerated, matching pnpm,
which synthesizes a name from the alias. Telling that apart from a
nameless manifest needs more than a `None`, so the reader reports
whether a root `package.json` was present.

pnpm 11 already handles `file:` tarballs correctly (it reads the manifest
from the fetch, and resolves and fetches together), so this needs no
TypeScript counterpart.

Closes pnpm/pnpm#13379.
2026-07-27 10:55:00 +02:00
Zoltan Kochan 208e5af561 fix(package-manager): pick the projects that run their own lifecycle scripts pnpm's way (#13398)
pacquet decided which projects fire their own preinstall / install /
postinstall / prepare from one `is_full_install` flag: every project the
run materialized ran them for a selector-less `install` or `update`, and
no project ran them for `add` or a targeted `update <pkg>`.

pnpm decides per project, from the mutated-importer list its command
layer builds — the projects the command was pointed at, plus the
workspace root, which the recursive dispatch pushes in as a plain
`mutation: 'install'` whenever the selection leaves it out. A project
runs its scripts when that list covers only part of the workspace, or,
when it covers the whole workspace, when its own mutation is a full
install. The rule was read off the TypeScript CLI by running it, not off
its source: 17 invocations across four workspace shapes, each project
stamping a file from `postinstall`.

Replace the flag with `ProjectMutation`, which names pnpm's mutation kind
(`InstallWorkspace`, `InstallSelected`, `InstallSome`, `NoInstall`), and
select the projects from it. `ProjectMutation::is_full_install()` keeps
the old flag's other four uses — the scope log, the optimistic
repeat-install gate, the resolution-skipped message and the
`--no-optional` exclusion — unchanged.

What changes for users: `update <pkg>` and `add <pkg>` in a workspace
stop skipping the workspace root's scripts, `update` at a workspace root
stops running the other members' scripts, and `update --latest` stops
running the project's own scripts (it rewrites named dependency specs,
so pnpm makes it `installSome` like `update <pkg>`).

The TypeScript CLI is unchanged; it gets the same matrix as e2e tests so
the contract is pinned on both sides.

Closes pnpm/pnpm#13358.
2026-07-26 09:20:46 +02:00
Zoltan Kochan 01dc5ac4c8 fix(lockfile): fail a frozen install when a recorded setting drifts (#13386)
`getOutdatedLockfileSetting` names eleven fields whose drift makes the
lockfile unreproducible; pacquet compared seven. A project that flipped
`autoInstallPeers`, `dedupePeers`, or `excludeLinksFromLockfile` after
writing its lockfile got a clean `--frozen-lockfile` install from
pacquet and `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` from pnpm — the install
proceeded against a lockfile whose recorded resolution no longer
matched the settings that produced it.

Each comparison follows pnpm's: `autoInstallPeers` and
`excludeLinksFromLockfile` are checked only when the lockfile records a
`settings` block, since one written before the field says nothing about
it, while `dedupePeers` is written only while it is on, so an absent key
reads as `false`.

The fields are now compared in pnpm's order too. Only the first drifted
field is reported, so the order decides which name a user sees, and
`injectWorkspacePackages` sat where pnpm checks `autoInstallPeers`.

`pnpmfileChecksum`, the eleventh field, is still unchecked: it needs the
pnpmfile's hooks gate, which means loading the pnpmfile through the Node
worker on a path that deliberately avoids it. Filed as pnpm/pnpm#13385.

`check_lockfile_settings` takes the same options struct its catalog-aware
twin took, and the twin is gone — the two had drifted into different
field sets, which is what let the new fields be added to one and missed
at the other's call site.

Closes the error-code half of item 1 of pnpm/pnpm#13315.
2026-07-26 02:00:29 +02:00
Zoltan Kochan 55d9514581 fix(package-manager): install git-hosted tarballs that pin no integrity (#13349)
pnpm refused every tarball resolution without an `integrity`, including
the commit-pinned git-host archives older pnpm versions wrote without
one, so a committed lockfile carrying such an entry could not be
installed at all.

The exemption now follows pnpm's `classifyResolution`: a git-host
archive URL or a `file:` tarball may be fetched unverified, every other
remote tarball still has to pin a hash. The check keys off the URL
rather than the lockfile's `gitHosted` marker, which pnpm treats as a
hint. `DownloadTarballToStore::package_integrity` became
`Option<&Integrity>`; an unverified fetch also claims no `index.db`
row, because the key pnpm addresses such a package by
(`pickStoreIndexKey`'s `pkg_id\tbuilt` fallback) is the one the
git-hosted prepare pass writes the prepared file set to — the raw
archive must not land there. The warm-path key derivation now goes
through `pick_store_index_key`, so it agrees with that fallback.

The prepare + packlist dispatch and the warm-cache key follow the same
classification, from the flag or the URL, so an entry cannot be exempt
from verification while skipping preparation; and a shape the fetch
refuses gets no warm key at all, so a row already at the shared
`pkg_id\tbuilt` key cannot materialize it before the refusal runs.

The refusal that remains carries pnpm's message and a `help:` line
naming the repair (`pnpm clean --lockfile`, then `pnpm install`), which
re-resolves the entry and records the hash.

Closes pnpm/pnpm#13308.
2026-07-25 16:03:55 +02:00
Zoltan Kochan 089ebad918 fix(pnpr): send the full request both clients owe the server (policy, catalogs, resolution mode) (#13279)
pnpr resolves and enforces policy server-side, and neither client runs its own
verifyLockfileResolutions when a pnpr server is configured. The server assigns
each field from the request unconditionally, so a field a client omits is cleared
rather than defaulted, and the server resolves under inputs the user never chose.

The TypeScript client sent only minimumReleaseAge of the verification policy, so
minimumReleaseAgeExclude entries stopped applying and the input-lockfile verifier
could reject a lockfile whose entries the user had explicitly excluded. It also
never sent the resolution mode, so --frozen-lockfile silently resolved and rewrote
the lockfile it promises to leave alone.

pacquet never sent catalogs, so every catalog: specifier failed to resolve — the
bug #13233 fixed for the TypeScript client and the server, leaving the Rust client
behind.

Drop the TRUST_POLICY_INCOMPATIBLE_WITH_PNPR guard, written before the server
gained trust-policy enforcement and refusing an install pacquet runs happily.
2026-07-25 10:56:36 +02:00
Zoltan Kochanandgithub-actions[bot] 454e7d62b3 chore(release): 11.17.0, pacquet 12.0.0-alpha.19, pnpr 0.1.0-alpha.5 (#13237)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 17:07:05 +02:00
Zoltan Kochan c3c3e6b643 fix: resolve catalog references when installing via a pnpr server (#13233)
A `catalog:` specifier in a workspace's dependencies or overrides failed to
resolve when the install was routed through a pnpr server, erroring with
"No catalog entry '<name>' was found for catalog 'default'." even though the
catalog entry existed.

The pnpr server reconstructs the requested workspace in a temp dir from the
resolve request. That reconstruction wrote a pnpm-workspace.yaml with only a
packages: section and no catalog definitions, and passed catalogs_override:
None to the install — so the server had no catalogs and could not resolve any
catalog: specifier, in dependencies or overrides.

Forward the client's workspace catalogs in the resolve request and use them as
the install's catalogs_override, which feeds both dependency and override
catalog resolution. The client now sends its raw overrides (the server resolves
their catalog: references) instead of pre-resolving them locally.

Closes pnpm/pnpm#13232
2026-07-23 16:04:43 +02:00
Zoltan Kochanandgithub-actions[bot] d1edab423e chore(release): 11.16.0, pacquet 12.0.0-alpha.18 (#13216)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-22 21:58:38 +02:00
KhảiandClaude Opus 4.8 e9433b130d chore(rust/dylint): enable perfectionist::unpinned_repo_ref (#13192)
* chore(dylint): re-enable perfectionist::unpinned_repo_ref

The rule flags repository URLs in comments whose ref is a mutable branch
or tag instead of a commit SHA. It was disabled while its citations were
pinned; re-enable it and bring the tree into conformance.

dylint.toml:
- Remove unpinned_repo_ref from the disable list.
- scan_string_literals = false: forge URLs in string literals are runtime
  data, not citations. The `pnpm repo` / `pnpm list` output URLs asserted
  in repo/tests.rs and list.rs are branch-shaped by design.
- allow_version_patterns = true: accept citations to permanent release
  tags (hosted-git-info v4.1.0, object-hash v3.0.0), where the version
  number is the meaningful reference.

SHA-pin the branch-ref citations to their current branch HEADs:
- zkochan/packages main -> e65701a6ae (is-subdir, path-temp,
  rename-overwrite)
- npm/registry -> ae49abf1ba (package-metadata.md)
- ds300/patch-package master -> be4dfd77d9 (applyPatches.ts)

Replace the pnpm/pnpm self-citations with in-repo pnpm11/ path
references: now that pacquet lives inside pnpm/pnpm, a link to pnpm/pnpm
source is no longer an upstream citation.

Verified locally: cargo dylint (clean), cargo doc, cargo fmt --check,
cargo clippy, and typos.

Resolves pnpm/pnpm#12717.

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

* chore(dylint): trim unpinned_repo_ref config comment

Shorten the comment to match the surrounding config style and drop the
file paths and package@version references that would drift as the tree
changes. The config values are unchanged.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-21 13:28:52 +02:00
Zoltan Kochan b8a4d380e0 feat(package-manager): complete install-state repair paths (#13173)
Complete the remaining install-state and repair-path parity work for the Rust pnpm CLI.

Treat a corrupt private lockfile as disposable install state, ensure expired module caches cannot be skipped by the optimistic repeat-install path, and materialize a PnP loader while keeping dependency symlinks disabled. Preserve direct executable linking in PnP mode and make frozen installs repair a missing loader.

Add coverage for corrupt and npm-created node_modules layouts, external lockfile public hoisting, failed-build bookkeeping, ignored build reporting, hoisted side-effects cache restoration, and fixture-scale optional dependency graphs.

Related to pnpm/pnpm#13167.
2026-07-20 15:02:00 +02:00
Zoltan Kochan 45f06ce85e feat(pacquet): complete git dependency parity (#13169)
Resolve alias-less Git selectors before updating the project manifest so pacquet can use the fetched package name. Reuse unchanged Git entries from the wanted lockfile to keep moving refs pinned during unrelated changes, and source direct-dependency log versions from lockfile package metadata.

Add per-run pnpr fixture substitutions for registry packages with local Git dependencies and port the remaining Git-hosted install, build, peer, partial-commit, and prepare-failure coverage.

Related to pnpm/pnpm#13167
2026-07-20 14:27:17 +02:00
Zoltan Kochan ea1d6efcae fix(pacquet): close the Resolution / verification install-parity gaps (#13172)
Close the Resolution / verification section of pnpm/pnpm#13167
(related to pnpm/pnpm#13167; other sections remain open).

- package-is-installable: implement npm-semver includePrerelease
  semantics exactly — per-||-alternative evaluation with pure ordering
  at fully specified comparators, the stripped-prerelease fallback for
  expanded ranges. >=9.0.0 now rejects 9.0.0-alpha.1 (checkEngine.ts:34).
- cli: port the non-strict minimumReleaseAge immature-version fallback
  test (minimumReleaseAge.ts:68); the resolver fallback already existed.
- config/workspace-manifest-writer/package-manager: port
  cleanupUnusedCatalogs — new workspace-yaml setting (default false),
  writer-side unused-entry removal mirroring
  removePackagesFromWorkspaceCatalog (dep + overrides references,
  emptied blocks and files dropped), wired into add/update/remove via a
  shared catalog_cleanup helper that loads the workspace projects and
  substitutes in-memory manifests.
- package-manager: dispatch engineStrict per inbound lockfile edge
  (pnpm/pnpm#13143) — a walk from the importers classifies skip
  candidates by whether a non-optional edge from an installed source
  reaches them; required incompatible snapshots fail under engineStrict
  and warn+materialize without it; optional-only and
  behind-skipped-parent snapshots keep skipping; unreachable snapshots
  keep the propagated-flag dispatch. Ports optionalDependencies.ts:552.
- resolving-deps-resolver: intersect distinct compatible auto-install
  peer ranges via node-semver Range::intersect (mergePkgsDeps policy),
  and port the lockedPeerContext / resolvedPeerProviderPaths machinery
  into resolve_peers (tree-node fields, resolved_peer_provider_paths
  option, paths_by_node_id output, reuse guards incl. must-win) with
  the two-pass locked-provider unit tests. Install-path wiring
  (lockfile capture + gated second pass) is a follow-up.
- pnpr/cli: add local ajv@4.10.4 + ajv-keywords@1.5.0 fixtures, claim
  the names in REGISTRY_MOCK_LOCAL_PATTERNS, and port hoist.ts:320/:327
  (peer-variant private hoist + uninstall).

All eight known-failure stubs of the section are converted into real
ports and TEST_PORTING.md is updated. Four changesets target pacquet.
2026-07-20 14:04:57 +02:00
Zoltan Kochan 62ab9cb4b2 feat(pacquet): reconcile an existing node_modules on repeat installs (Stage 1) (#13151)
Port the removal half of pnpm's modules-cleaner prune as
PruneStaleModules, wired into the frozen and fresh install paths before
linking: stale direct-dep links and their bin shims are removed with
pnpm:root removed events, orphaned snapshots' hoisted aliases are
unlinked via the persisted .modules.yaml map, and the orphan-package
count feeds a single pnpm:stats removed emission per install. Orphan
virtual-store directories remain the modules cache (throttled
prune_virtual_store sweep), matching upstream's pruneVirtualStore gate.

Parity fixes surfaced by the port: add/remove now fail with the
validateModules *_DIFF errors instead of silently recreating a drifted
modules dir (new installs_only flag mirrors upstream installsOnly);
the frozen no-op short-circuit probes the tree it would skip so a
hand-deleted package is repaired with pnpm:_broken_node_modules;
Lockfile::is_empty inspects all three dependency groups so dev-only
installs stop deleting their current lockfile; the fresh path threads
the current lockfile into the hoisted linker, activating its orphan
diff on add/update flows.

Promote 22 known_failures stubs to real passing ports (16 in hoist.rs,
6 in hoisted_node_linker.rs), add the repeat_install.rs suite porting
the existing-node_modules scenarios, tick 24 TEST_PORTING.md entries,
and correct the peer-hoist stubs' blocker to the registry-mock fixture
gap. Every promoted port was verified to fail against a deliberately
broken implementation before landing.

Related to: pnpm/pnpm#13146 (Stage 1), pnpm/pacquet#299, pnpm/pacquet#433
2026-07-19 21:53:56 +02:00
Zoltan Kochanandgithub-actions[bot] 32a30c4d70 chore(release): 11.15.0, pacquet 12.0.0-alpha.15, pnpr 0.1.0-alpha.4 (#13126)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 14:19:57 +02:00
Zoltan Kochan aeaaccba51 fix(resolving-deps-resolver): keep synthesized reuse results out of the wanted-dep cache (#13122)
During a non-frozen re-install, a node reused from the prior lockfile
registered its synthesized ResolveResult in the workspace-shared
resolved_by_wanted cache. That result's manifest deliberately omits
dependencies (a reused node's children come from the snapshot graph), so
when another edge with the identical wanted key was denied reuse - by
the changed-direct-dep gate in a different importer, or by
subtree_fully_reusable's provisional-false cycle guard racing a
concurrent check - its fresh resolve read the dependency-less result
back, classified the package as a leaf, and (when that occurrence was
shallow enough to win children ownership) recorded it with no children:
the package's lockfile snapshot collapsed to an empty map, its peer
suffix was dropped, its dependents were re-pointed at the bare instance,
and none of its dependencies were linked.

This is what broke the Compile & Lint job on the pnpm@12.0.0-alpha.14
bump PR (pnpm/pnpm#13070): the alpha.14 per-importer freshness check
regression (since fixed by pnpm/pnpm#13117) pushed CI's plain install
onto the re-resolution path, which then emptied the snapshots of
`@yarnpkg/shell@4.0.0` and `normalize-package-data@3.0.3`, leaving
`pnpm/dist` bundling unable to resolve their dependencies. The
corruption itself predates alpha.14 (reproducible on alpha.13 by
forcing a re-resolution with any manifest change).

Drop the cache insert: only the fresh-resolve path ever read it, and a
denied edge must see the real registry manifest to walk its children.
The regression test drives the deterministic cross-importer variant
using new reuse-chain-* registry fixtures: an unchanged importer reuses
the target at depth 2 (caching the synthesized result under the
exact-pin wanted key), then a later importer whose changed direct dep
overlaps the target's snapshot is denied reuse at depth 1 and must
re-resolve fresh.

The TypeScript CLI has no equivalent cache of synthesized reuse
results, so the fix is pacquet-only.
2026-07-18 13:22:35 +02:00
Zoltan Kochan f4948525df chore: remove repository changelogs (#13119)
Remove the legacy repository changelog files now that release changelog storage defaults to the registry. The publish path composes and injects CHANGELOG.md into release tarballs, so keeping historical copies in source control duplicates generated release data.

Update adm-zip to the patched 0.6 release and override vulnerable transitive versions after the dependency audit began rejecting versions below 0.6.0.
2026-07-18 13:10:25 +02:00
YES!HYUNGSEOKandZoltan Kochan 9657c883bf fix(pacquet): honor workspace install filters (#13030)
Pacquet's recursive command pipeline previously dispatched selected projects independently or lost the selection before package-manager execution. This could overwrite earlier manifest mutations, truncate workspace lockfile state, or materialize projects outside the requested filter.

Thread one workspace selection through command dispatch and package-manager execution, batch manifest mutations into a single install, retain the complete wanted lockfile, and derive current-lockfile and node_modules state from the selected dependency closure. Apply the same workspace root and selection semantics to query, state, global, deploy, and pnpr-backed paths.

Keep full-workspace recursive installs on the ordinary unfiltered fast and frozen paths, and add regression coverage for filtering, lockfile preservation, dependency closure materialization, pnpr multi-importer responses, and TypeScript parity edge cases.

Depends on pnpm/pnpm#13016.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-18 13:06:37 +02:00
Zoltan Kochan 2bc4dd5819 fix(lockfile): fold auto-installed peers into the frozen-lockfile check (#13117)
pacquet's per-importer frozen-lockfile freshness check compared each
lockfile importer's dependencies against only the manifest's
dependencies/devDependencies/optionalDependencies. With auto-install-peers
enabled (pnpm's default), pnpm materializes every missing non-optional
peer dependency into the importer's `dependencies` in the lockfile, so a
workspace member that declares a dependency only under `peerDependencies`
had that materialized entry misread as a removed dependency, aborting a
`--frozen-lockfile` install with ERR_PNPM_OUTDATED_LOCKFILE.

This surfaced once pnpm/pnpm#13081 extended the check from the root
importer to every workspace importer (v12.0.0-alpha.14): importers such
as `pnpm11/building/after-install` declare `@pnpm/logger` and
`@pnpm/worker` only as peers.

`satisfies_package_manifest` now takes `auto_install_peers` and folds the
peer-only dependencies into both the flat-record diff and the per-field
`dependencies` comparison, mirroring the TypeScript
`satisfiesPackageManifest`. The TypeScript CLI already handled this, so
the fix is pacquet-only.
2026-07-18 10:39:58 +02:00
Zoltan Kochanandgithub-actions[bot] f8b08ea63f chore(release): 11.14.0, pacquet 12.0.0-alpha.14 (#13113)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 00:01:20 +02:00
Zoltan Kochan d8dfa853aa fix(pacquet): validate workspace lockfile importers (#13081)
Check every workspace project manifest against its matching lockfile importer before taking the
frozen or prefer-frozen path. The project list and importer ID helper are shared with the existing
multi-importer resolver, so workspace members use the same lockfile keys during resolution and
freshness validation.

This makes normal installs refresh stale workspace member importers and makes frozen installs fail
instead of silently accepting a lockfile that no longer matches a member's package.json. It also
matches the TypeScript freshness edge cases for aliases, dependency metadata, missing dependency-free
importers, distribution tags, and direct resolutions outside their declared semver ranges.

Persist importer `dependenciesMeta` and `publishDirectory` during fresh resolution so those parity
checks compare against complete lockfile snapshots. Restore the empty intent list in the versioning
ledger as well. The previous YAML spelling parsed
as null and prevented release-plan validation from reading the committed ledger.

Closes pnpm/pnpm#13080.
2026-07-16 21:25:15 +02:00
Zoltan Kochanandgithub-actions[bot] d5b4b7bd5d chore(release): pacquet 12.0.0-alpha.12, pnpr 0.1.0-alpha.3 (#13023)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-15 11:42:38 +02:00
Khải 86e613c2f4 docs(rust/cli): clean up --help texts (#13008)
Make pacquet's --help read like user-facing help rather than developer API
docs, and re-enable the perfectionist::clap_help_markdown dylint rule.

The clap doc comments are dual-purpose: cargo doc API docs and the text
clap prints for --help. They had accumulated developer and parity prose
that leaked into the terminal (mirrors-pnpm notes, config-field and
store-layout mechanics, overrides_with pairing, env-var names, ERR_PNPM_*
codes, internal type references, not-yet-ported caveats). Rewrite them
across the whole CLI into concise user-facing help and drop those notes;
no flag behavior changes, only doc-comment text moved.

Clean out the markdown the rule flags in the same pass: intra-doc links to
internal types become the CLI flag they describe or plain words, one inline
issue citation leaves the help text, and an angle-bracketed path placeholder
that parsed as an HTML tag is wrapped in a code span. Re-enable the rule by
dropping it from the dylint.toml disable list; keep code spans via the
rule's ignore_constructs setting since they read fine in a terminal.

Closes pnpm/pnpm#12718
2026-07-15 11:36:34 +02:00
Zoltan KochanandClaude Opus 4.8 6dcfadd5ae fix(release): sync Rust product versions via the meta-updater (#12988)
The Rust CLI and pnpr embed the versions their release builds report and
verify (`PNPM_VERSION` in pnpm/crates/config/src/defaults.rs, the crate
version in pnpr/crates/pnpr/Cargo.toml, and its Cargo.lock entry). These
were mirrored from the npm wrappers by `syncRustVersions` in bump.ts — a
step separate from the meta-updater, so running `pnpm version -r` without
the full bump flow bumped the wrappers (pacquet 12.0.0-alpha.10, pnpr
0.1.0-alpha.2) while the Rust sources stayed at the previous versions. The
release workflow's "Verify the committed version" step then failed, and
nothing caught the drift before the tag.

Move the sync into the meta-updater as a set of Rust-source file handlers,
so it is written by `pnpm update-manifests` and, crucially, validated by
`meta-updater --test` in pre-push and CI — a missed sync now fails locally
instead of at release time. bump.ts drops the redundant `syncRustVersions`
and runs `pnpm update-manifests` after `pnpm version -r`; the root `bump`
script no longer needs its own trailing `update-manifests`.

Regenerating brings the Rust sources up to the already-bumped wrapper
versions (alpha.10 / alpha.2), unblocking the release.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:24:21 +02:00
Zoltan KochanandClaude Opus 4.8 1c887b2e98 chore(release): don't re-lint the pnpm CLI at publish time (#12987)
* chore(release): don't re-lint the pnpm CLI at publish time

The pnpm package's `prepublishOnly` ran `compile`, which includes
`eslint --fix` over the src and test files. On the macOS release runner
the `import-x/no-extraneous-dependencies` allow-list for test files did
not take effect, so linting reported hundreds of spurious errors and
aborted the publish of the `pnpm` wrapper — the last package the release
job publishes — leaving 11.13.0 unpublished.

Split the artifact build out of `compile` into a new `build` script and
point `prepublishOnly` at it, so publishing no longer re-lints. Lint is
still run by `compile` (used for local development and `pnpm test`) and
enforced by CI on every PR, so a merged, tagged release commit is already
linted.

`build` runs the same tsgo build, bundle, and asset-copy steps `compile`
did; only the lint step is removed from the publish path. The scripts are
generated by the meta-updater, so the change is made there and the
generated `pnpm/package.json` is regenerated to match.

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

* fix: bump pnpm 12 and pnpr

* fix: update pnpm v12

* chore: bump versions

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:05:14 +02:00
Zoltan Kochanandgithub-actions[bot] 682f57e773 chore(release): 11.13.0, pacquet 12.0.0-alpha.9, pnpr 0.1.0-alpha.1 (#12986)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-13 22:19:04 +02:00
Zoltan Kochan 5bd00823c9 fix(publish): send README to the registry as metadata (#12968)
`pnpm publish` again includes the package's README in the metadata sent to
the registry, matching the npm CLI, so registries can render it on the
package page.

Until v11 the actual upload was delegated to `npm publish <tarball>`, and
npm reads the README out of the tarball (pacote's fullReadJson) into the
version metadata regardless of pnpm's embed-readme setting. Once publishing
became fully native and went through libnpmpublish with pnpm's own
publishedManifest, the README only reached the registry when embed-readme
was true — but that setting defaults to false (deliberately, so the tarball's
package.json stays clean). The result was that published packages silently
lost their README metadata.

Decouple the two concerns: the README is now always attached to the manifest
reported for publishing, while embed-readme continues to control only whether
it is written into the package.json inside the tarball. For a pre-built
tarball passed to `pnpm publish <tarball>`, the README is read back out of the
tarball, mirroring npm's fullReadJson. Applied to both the TypeScript CLI and
the Rust pacquet stack (pack + publish; stage delegates to publish).

pnpr additionally hoists the latest version's readme to the packument's
top-level readme/readmeFilename on publish, matching npm and verdaccio, so a
package published to pnpm's own registry exposes a top-level readme.

Closes pnpm/pnpm#12966.
2026-07-13 18:27:50 +02:00
Zoltan Kochan d3e1383ff4 feat(release): unify the TypeScript, Rust and pnpr release flows on pnpm's native versioning (#12949)
Merge the three release flows (TypeScript, Rust CLI + @pnpm/napi, pnpr)
into a single flow driven by pnpm's native workspace versioning
(pnpm/pnpm#12953), dropping the @changesets/cli dependency (Closes
pnpm/pnpm#12947).

The native engine keys package identity on the workspace directory, so
the Rust CLI wrapper is named pnpm (the v12 line at pnpm/npm/pnpm) and
shares the published name with the TypeScript CLI at pnpm11/pnpm.
Release configuration moves from .changeset/config.json to the
versioning key of pnpm-workspace.yaml: versioning.lanes puts the Rust
CLI, @pnpm/napi, and @pnpm/pnpr on an alpha lane (X.Y.Z-alpha.N
prereleases published under next) while the TypeScript CLI releases
stable on the main lane; versioning.fixed keeps the Rust CLI and
@pnpm/napi at one shared version; versioning.ignore freezes
@pnpm/logger, which is consumed as a catalog: peer the engine would
otherwise reject as an internal range.

Lanes replace the hand-rolled prerelease continuation, and the
committed .changeset/ledger.yaml replaces the .changeset-released
directory as the cherry-pick-safe record of consumed intents. bump.ts
drops both and is now just pnpm version -r plus syncRustVersions, which
mirrors the bumped wrapper versions into defaults.rs and the pnpr crate
version.

Because two workspace projects are named pnpm, name-based --filter=pnpm
is qualified by directory (pnpm{pnpm11/pnpm}) across the build and
release scripts, the meta-updater excludes the Rust wrappers by
directory, and changesets targeting the TypeScript CLI reference it as
./pnpm11/pnpm. release.yml's plan job gates per-product publish jobs on
which committed versions are unpublished; everything publishes via
trusted publishing. @changesets/cli, .changeset/config.json, and the
standalone pacquet/pnpr release workflows are removed; their npm
trusted-publisher bindings must be re-pointed at release.yml before the
first unified release.

pnpm-lock.yaml is regenerated from scratch: an incremental
--lockfile-only resolve after the @changesets/cli removal hit a pacquet
incremental-resolver bug (pnpm/pnpm#12958) that emptied a peer-context
snapshot the CLI depends on and broke the bundle build. A from-scratch
resolve is correct; the bug is filed separately.
2026-07-13 11:11:41 +02:00
YES!HYUNGSEOKandZoltan Kochan 806ff49162 fix(pnpr): prevent stale hosted packument writes (#12832)
S3-backed pnpr deployments can run multiple stateless replicas against the same hosted
object store. The in-process package lock only serializes one replica, so the old
read/merge/write path could let a stale packument overwrite a newer merge.

Add a hosted packument read-for-update path that captures object-store update versions and
use conditional S3 writes for hosted packuments. Publish, partial unpublish, and dist-tag
writes now use that conditional write path; dist-tag writes retry after conflicts because
their mutation can be replayed on a fresh packument. The dist-tag request path and journal
roll-forward share one Storage::update_hosted_packument_with_retry helper so their
conflict/backoff handling stays in a single place.

Tarball finalize on the S3 backend is now compare-and-swap: it promotes with
PutMode::Create, tolerates a byte-identical object, and refuses to overwrite a different
object left by a concurrent same-version publisher. commit_publishes surfaces that as an
HTTP 409 before writing its packument, and journal roll-forward keeps the winner's
immutable version, so a losing publish can no longer corrupt the winner's tarball.

Publish commit journal recovery now rereads the current hosted packument, re-merges the
journaled manifest, and retries conditional writes.
Repeated conflicts surface as HTTP 409 rather than silently losing another writer's update.
The local fs backend keeps its existing single-process behavior because the production
shared-store race is specific to S3-backed replicas.

Regression tests verify that a stale S3 packument update is rejected and that a concurrent
tarball finalize with different bytes is refused without overwriting the first writer.

---------

Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
2026-07-13 10:35:26 +02:00
Alessio AttilioandZoltan Kochan 411bbe89ff feat(registry-access): implement team command in both TypeScript and Rust stacks (#12789)
The team command communicates with the registry through the standard npm team API endpoints.  The scope:team format is parsed to separate the organization scope from the team name.  For mutation subcommands (create, destroy, add, rm) the registry URL is resolved per scope from the registries map with an optional --registry override, the auth header is resolved from the configured credentials honoring scoped credentials, and the request is sent with retry support and bounded response reads.  When an OTP is in play, the Rust side restricts redirects to the configured registry origins so the npm-otp header cannot leak to another host; the TypeScript fetch layer already strips it on cross-host redirects.  The ls subcommand dispatches to listing teams within an org when given @scope and to listing members of a specific team when given @scope:team.  Output supports three modes, the default human-readable listing, --parseable which emits newline-delimited names, and --json which emits structured arrays.

On the TypeScript side the command is registered in pnpm/src/cmd/index.ts and removed from the notImplemented list.  On the Rust side it is added as a CliCommand variant, routed in dispatch, and dispatched in dispatch_query.  Both sides include comprehensive tests covering all subcommands, error paths for 401 403 404 and 409 responses, empty results, and the three output formats.

pnpr serves the npm team API from each hosted registry's config-declared teams: GET /-/org/{scope}/team and GET /-/team/{scope}/{team}/user list teams and members, gated by the registry-level access with denials masked as not-found, while team mutations answer an explicit 403 since pnpr teams are config-managed.

@pnpm/cli.parse-cli-args no longer stops option parsing at an escape word (create, exec, test) that appears as another command's parameter, which previously made pnpm team create drop a trailing --registry option.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-07-12 13:17:26 +02:00
Zoltan Kochan 3acb421adf feat(pnpr): pnpr-native access control — strict token grammar and registry-scoped teams (#12790)
pnpr's access-control declaration is now pnpr-native, matching the
already-native routing config.

Token grammar: only the $-sigiled built-ins ($all, $authenticated,
$anonymous) are recognized; verdaccio's @-prefixed and bare alias
spellings are rejected at config load with a did-you-mean error, and
the $ namespace is reserved so a typo'd built-in cannot silently
become a username that admits nobody. Access lists and member lists
no longer whitespace-split: a YAML scalar is one token, multi-token
lists are YAML sequences, and an empty-string value is an error
pointing at [] or omission.

Teams: the global groups: block is replaced by registry-scoped teams.
Each hosted or upstream registry declares its own teams: map and
references it from its access lists as team:<name>; a bare token is a
username only. This closes the escalation where open registration let
anyone claim a username equal to a group name and inherit its grants,
and it makes cross-registry reuse explicit (YAML anchors) instead of a
global namespace. group:<name> gets a pointer at team:<name>, unknown
<type>: prefixes are rejected (htpasswd forbids ':' in usernames), and
an undeclared team reference is a startup error listing the declared
teams.

team: references are resolved to member sets at config load, so
access-list evaluation still needs only the caller's identity:
Identity drops its groups field and route classification, search, and
the resolver are untouched. The removed top-level groups: block is
rejected loudly, like the removed top-level packages: block, because
silently dropping it would change who may reach what on upgrade.
2026-07-12 01:38:00 +02:00
Zoltan Kochanandgithub-actions[bot] 98722fab10 chore(release): 11.12.0 (#12937)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-11 11:34:29 +02:00
Zoltan Kochan 6a85968c12 feat(pacquet): port the stage command and add its -/stage endpoints to pnpr (#12922)
Port `pnpm stage` (publish, list, view, approve, reject, download) to the
Rust CLI, mirroring the TypeScript command's flags, error codes, endpoints,
and output. `stage publish` reuses the publish pipeline (PublishArgs is
split into a reusable PublishFlags, and the pipeline returns summaries
instead of printing); approve/reject drive the shared OTP / web-auth flow;
download summarizes the tarball with the same traversal protections as
the TypeScript implementation. Also fixes the staged-publish route in
pacquet-publish to POST -/stage/package/:pkg (libnpmpublish's stage route)
instead of the regular packument PUT — previously unreachable because the
CLI hardcoded stage: false.

pnpr grows the server half: POST /-/stage/package/:pkg validates and
authorizes like a direct publish and holds the document under a UUID;
list/view/tarball inspect held records; approve replays the document
through the regular validate/stage/commit flow; reject deletes it. Records
persist under a reserved .staged/ namespace on the fs and S3 backends,
with stage-id validation ahead of any path or object key.

Shared plumbing: the capped response-body reader moves from the dist-tag
command into pacquet-network, and pacquet-pack exports its en-locale path
sort for the tarball summary.

Also fixes a broken test on main (semantic conflict between
pnpm/pnpm#12910 and pnpm/pnpm#12914): State::init no longer persists a
scaffolded root package.json when a pnpm-workspace.yaml sits next to the
missing manifest, so a verify-deps-before-run install can't turn the
workspace root into a selectable project with the init template's failing
test script.
2026-07-11 01:15:59 +02:00
Zoltan Kochan 8e17c3d366 refactor: rename the pacquet/ directory to pnpm/ (#12913)
Pure directory move plus path fixups: the Rust port ships as pnpm v12,
so the source tree now lives at pnpm/ (alongside pnpm11/, the frozen
TypeScript line). No identifiers change in this pass — crate names
(pacquet-*), the pacquet bin, PACQUET_VERSION, the @pacquet/* npm
package names v11's runPacquet spawns, the .pacquet virtual-store dir,
the benchmark harness's clone dir, and the pacquet-*.yml workflow
filenames (npm trusted publishing is bound to them) all stay for a
follow-up.

Also removes the root /pnpm/ .gitignore entry (build detritus in the
pre-pnpm11 package location): pnpm/ is real source now and must not be
ignored. Developers with a stale generated pnpm/ dir should delete it
before checking out this change.
2026-07-10 18:06:56 +02:00
Trevor Burnham a897ef728d feat(pacquet): support custom fetchers from pnpmfile (#12846)
Support custom fetchers from pnpmfiles in pacquet, with delegate-envelope parity in the TypeScript CLI (related to pnpm/pnpm#11685)

Pacquet already supported custom resolvers via its Node.js worker IPC but
lacked the fetcher counterpart. This extends the same protocol pattern:

- Define a CustomFetcher trait and get_custom_fetchers() on PnpmfileHooks
- Add fetchers/fetcher target dispatch to the worker's NDJSON protocol
  and JS runner, mirroring the existing resolver path; the hook is
  invoked with the TS-parity args fetch(cafs, resolution, opts, fetchers)
  (cafs/fetchers are null over IPC)
- Implement NodeJsCustomFetcher bridging the trait to the worker
- Create CustomFetcherPicker for consulting fetchers in declared order
- Consult custom fetchers before the built-in dispatch on both the
  frozen-lockfile and fresh-lockfile install paths; hook-load failures
  abort the install (PNPMFILE_FAIL)
- Support delegation: { delegate: <resolution> } rewrites the resolution
  for the standard tarball/git path; non-delegate responses and
  custom-typed delegates fail the install
- lockfile: add LockfileResolution::Custom preserving custom-typed
  resolution objects verbatim, so custom resolvers/fetchers can round-trip
  them; unclaimed custom-typed resolutions fail with the TS-parity
  UNSUPPORTED_RESOLUTION_TYPE error
- TypeScript: pickFetcher now accepts the { delegate: <resolution> }
  envelope from custom fetchers (the portable delegation form), and
  hooks.types exports CustomFetcherDelegation

Full CAS fetch in pacquet (where a fetcher produces file content directly
rather than delegating) requires a streaming protocol extension
(separate follow-up).
2026-07-10 16:06:09 +02:00
Zoltan Kochan 5333a2543d fix(pacquet): restore Bit workspace/capsule install parity with the v11 engine (#12899)
* **New Features**
  * Installations now generate and preserve `file:` injected workspace dependency mappings and carry them through `.modules.yaml`.
  * Manifest `link:` dependencies are now materialized as symlinks in project `modulesDir` (supports absolute, relative, and `link:.`, plus safe `modulesDir` validation).
  * Workspace components missing `package.json` now link to all other sibling root members.
* **Bug Fixes**
  * Empty/whitespace “no range” specifiers no longer break dependency resolution.
  * Peer-suffixed metadata is now resolved via peer-stripped lookups when needed.
  * Hoisted workspace importers are retained even when workspace-hoisting is disabled.
  * Trusted importer IDs prevent incorrect unsafe-path rejection during isolated symlinking.
2026-07-10 16:02:33 +02:00
Zoltan Kochanandgithub-actions[bot] 8e1e4c0aae chore(release): 11.11.0 (#12886)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-09 22:29:10 +02:00
C. Spencer BeggsandZoltan Kochan fecfe8334b fix(resolver): keep hoisted peer providers out of the root peer context (#12847)
With autoInstallPeers, peers resolved inside a dependency subtree are
attached to the root importer's direct dependencies so other subtrees
can reuse them. Those entries alias tree nodes that already have a
position deep in the graph, and the peer-resolution pass walked them a
second time as root children. The two walks raced on the shared
per-node dep-path state, so a package inside a self-contained closure
could get its peers bound to the root project's incompatible version
of a peer instead of the provider next to it in the tree, producing a
lockfile that mixes both versions and a peer mismatch at run time.

The peer-resolution pass now keeps the attached providers visible as
root-level peer providers but resolves their own peers only at their
true tree position, falling back to the root context only when that
position was pruned by the peers cache. All pruned providers are
resolved in a single fallback pass, because a pass only detects peer
cycles among its own children and mutually peer-depending providers
would otherwise await each other's dep path forever. The fix lands in both stacks:
the TypeScript `@pnpm/installing.deps-resolver` and pacquet's
`resolving-deps-resolver` crate.

Fixes https://github.com/pnpm/pnpm/issues/4993

---------

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
2026-07-09 22:00:20 +02:00
Zoltan Kochan de1371daa9 feat(pacquet): add Node API bindings for the Rust engine (#12822)
Add `pacquet-napi`, a napi-rs cdylib crate, plus its `@pnpm/napi`
npm wrapper, exposing pacquet's programmatic engine surface to Node.js so
programmatic pnpm consumers can drive the Rust engine instead of the
TypeScript pnpm packages. Bit is the reference consumer.

Exports: install (in-memory importers, single and multi-importer workspaces,
a synchronous readPackage hook per resolved dependency manifest, build-script
approval, and depsRequiringBuild), rebuild, resolveDependency, pack,
parseBareSpecifier, engineVersion, and auth via authHeaderByUri.

The install runs on a dedicated 32 MiB-stack worker thread with its own
tokio runtime; the napi async fn awaits the result over a oneshot channel,
so pacquet's borrowed State never crosses the FFI boundary. A reporter
bridge forwards the engine's wire-compatible log events to a JS callback,
and errors carry pnpm's ERR_PNPM_* code and hint through a structured envelope.

Engine-core changes are minimal and inert for existing callers:
PackageManifest::from_value, and two Option fields on Install
(pnpmfile_hook_override and workspace_projects_override) defaulted to None
everywhere. A new napi-release profile sets panic = "unwind" since the
workspace release profile uses panic = "abort".

getPeerDependencyIssues is stubbed pending pacquet's own peer-issue renderer.
Distribution follows the @pnpm/exe.* model via scripts/generate-packages.mjs.
2026-07-07 10:45:03 +02:00
Zoltan Kochan ed54cccc6f fix: filter pacquet install summary by prefix (#12824)
Filter pacquet's default reporter summary inputs by the active prefix for non-global installs, matching the TypeScript reporter behavior. Pacquet was folding every importer into the root summary, so workspace installs printed child importer dependencies in the root summary.

Keep summaries unfiltered for commands whose install events intentionally come from another prefix: global add/remove/update/runtime and the cache-backed dlx/create flows.

Preserve package-manifest diff summaries by tracking manifest snapshots per prefix, emitting the pre-mutation manifest for add/remove/update, and rendering a later non-empty manifest diff when lockfile-only flows emit updated manifests after the install summary marker. Keep the public reporter stream to one initial package-manifest event and one install-closing `pnpm:summary` event, and normalize equivalent prefix paths lexically so relative or trailing-separator variants are not dropped.

Add focused reporter tests plus CLI regressions for workspace root summaries, dlx/create append-only summaries, workspace subdirectory add --lockfile-only, single-initial/single-summary NDJSON output, and normalized prefix matching.
2026-07-06 21:20:16 +02:00
Zoltan Kochanandgithub-actions[bot] 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 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
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
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
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