mirror of
https://github.com/pnpm/pnpm.git
synced 2026-10-06 13:11:44 -04:00
212315de1663a5d210c359866981368fc7e97ddb
11575
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
212315de16 |
fix: cap lockfile verification memory and add trustLockfile opt-out (#11878)
* fix: cap lockfile verification memory and add trustLockfile opt-out Verifying a multi-thousand-entry lockfile against `minimumReleaseAge` or `trustPolicy: no-downgrade` retained every fetched packument in a per-install cache for the entire install. On large workspaces this OOM'd CI runners with a 2GB heap cap. Project both caches down to just the fields each check reads (per-version trust evidence + the `time` map for trust; package-level `modified` + version-name set for the abbreviated shortcut) so the bulk packument is GC'd as soon as the fetch returns. Also adds a `trustLockfile` setting (default `false`) that skips the verification pass entirely for environments where the lockfile is already part of the trusted base. Mirrored in pacquet. Closes #11860. * perf: share resolver packument cache with the lockfile verifier The verifier kept its own per-install dedup Maps and re-fetched every packument the resolver had already pulled during the same install. Plumb the resolver's per-install `PackageMetaCache` through to the verifier (via `createNpmResolutionVerifier` / `build_resolution_verifiers`) so a name already in the resolver's LRU short-circuits the verifier's disk/network round-trip — fast path only, the cached document is projected for the trust check so the verifier's memory footprint stays bounded. In pnpm, `installing/client` now constructs one LRU and hands it to both `createResolver` and `createResolutionVerifiers`. In pacquet, the `InMemoryPackageMetaCache` is lifted to `Install::dispatch` and passed to both `build_resolution_verifiers` and `InstallWithFreshLockfile`. |
||
|
|
a5b1ac783f |
chore(deps): bump the github-actions group with 4 updates (#11854)
Bumps the github-actions group with 4 updates: [github/codeql-action](https://github.com/github/codeql-action), [taiki-e/install-action](https://github.com/taiki-e/install-action), [garnet-org/action](https://github.com/garnet-org/action) and [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action). Updates `github/codeql-action` from 4.35.4 to 4.35.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba) Updates `taiki-e/install-action` from 2.78.0 to 2.78.1 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/e1c4cd42111751368541a7cb5db3522bd1f846a4...184183c2401be73c3bf42c2e61268aa5855379c1) Updates `garnet-org/action` from 2.0.1 to 2.0.2 - [Release notes](https://github.com/garnet-org/action/releases) - [Commits](https://github.com/garnet-org/action/compare/9e819143e63d6dda04bca2e90ac85e3cf0e5289d...2b7fc9d79b54f551b43358c27424a36064b3e078) Updates `zizmorcore/zizmor-action` from 0.5.3 to 0.5.5 - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/b1d7e1fb5de872772f31590499237e7cce841e8e...a16621b09c6db4281f81a93cb393b05dcd7b7165) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.78.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: garnet-org/action dependency-version: 2.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: zizmorcore/zizmor-action dependency-version: 0.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
bf581bb0a5 |
ci(bencher): enforce PR thresholds and grant checks: write (#11883)
- Add `--start-point-clone-thresholds` to the non-main upload arms so PR/feature branches inherit thresholds configured on main; pair it with `--err` so a sample over the upper boundary fails the job. - Add `checks: write` to the three workflows that call `bencher run`. On main pushes (no `--ci-number`, not a PR event) Bencher falls back to creating a GitHub Check on the commit; without the permission it exits 1 with "Failed to create GitHub Check". |
||
|
|
4088de0433 |
ci(bencher): record benchmark results to Bencher (#11875)
* ci(bencher): record benchmark results to Bencher Tracks both stacks in one Bencher project (`pnpm`), under separate testbeds (`pnpm`, `pacquet`). - `benchmarks/bench.sh` emits a hyperfine-shaped `bencher-results.json` combining the six pnpm scenarios. - `benchmark.yml` adds `push: branches: [main]` so each merge updates the `pnpm` baseline, then uploads via the Bencher CLI. - `pacquet-integrated-benchmark.yml` adds the same main-push baseline for `pacquet`, combines the four scenario JSONs into a Bencher report, and stages it into the existing artifact. - `pacquet-integrated-benchmark-comment.yml` uploads PR results from the trusted `workflow_run` context so fork PRs are covered too. Requires `BENCHER_API_TOKEN` in repo secrets; workflows no-op with a `::notice::` if it's missing. * ci(bencher): allow workflow_dispatch to upload pacquet results The inline Bencher upload was gated to `event_name == 'push'`, which meant manual dispatch from a feature branch ran the bench but skipped the upload. Both push and workflow_dispatch execute in the base-repo privilege context, so it's safe to upload from both — the fork-safety gate only needs to keep `pull_request` runs out. On dispatch from a non-main branch we record into the ref name with `--start-point main --start-point-reset`, matching the pnpm bench's branch policy. * ci(bencher): treat workflow_dispatch on main as the main baseline Two small fixes from PR review: - `benchmark.yml`: manual dispatch from `main` was falling into the non-main branch arm, recording `--branch main --start-point main --start-point-reset` — equivalent to forking main from itself. Treat it like a `push` event so a manual run on main updates the baseline directly. Matches the pacquet workflow's branch policy. - `bench.sh`: emit a stderr warning when `jq` is missing instead of silently skipping `bencher-results.json`. Keeps behaviour optional for local users but makes the skip discoverable. * bench: rename scenarios with explicit state axes Replaces the hyperfine-leaking names (`clean-install`, `frozen-lockfile`, `peek`, `gvs-warm`, …) with a consistent grid that spells out every state the benchmark depends on: - "Fresh" — node_modules wiped at start (future variants will start with a populated node_modules). - "Install" vs "Restore" vs "Add new dep" — the work being measured. - "hot/cold cache + hot/cold store" — both pnpm directories, spelled out separately because they're distinct on disk. - "isolated linker" — nodeLinker mode (future variants will cover `hoisted` and `pnp`). The slugs map directly from the clap-derived kebab-case names, so `--scenario=fresh-restore-cold-cache-cold-store-isolated` is the new CLI surface. Updates land across the Rust orchestrator (`BenchmarkScenario`), `benchmarks/bench.sh`, the pacquet workflow, `benchmarks/README.md`, and `pacquet/CONTRIBUTING.md` so the names agree end-to-end. Adds a justified `#[allow(clippy::enum_variant_names)]` on the enum because every variant currently shares the `Fresh` prefix; the lint will stop firing once `Filled*`/`Resynced*` counterparts land. Bencher's stored history for the old benchmark names will become orphaned and can be archived in the UI. * bench: linker-first slug shape with dot-separated axes Reshapes the scenario identifiers so the linker mode is the leading group: `<linker>.<action>.<cache state>.<store state>`. Dots separate the four axes the bench varies, and `isolated-linker.*` / `gvs-linker.*` sort together in any dashboard that groups by prefix. Future buckets (`hoisted-linker.*`, `pnp-linker.*`) will slot in without disturbing the existing names. GVS is its own top-level bucket rather than a sub-variant of isolated — its perf profile differs enough to chart separately. Renames: - `clean-install` → `isolated-linker.fresh-install.cold-cache.cold-store` - `full-resolution` → `isolated-linker.fresh-install.hot-cache.hot-store` - `frozen-lockfile` → `isolated-linker.fresh-restore.cold-cache.cold-store` - `frozen-lockfile-hot-cache` → `isolated-linker.fresh-restore.hot-cache.hot-store` - `peek` → `isolated-linker.fresh-add-dep.hot-cache.hot-store` - `gvs-warm` → `gvs-linker.fresh-restore.hot-cache.hot-store` Each Rust variant now carries `#[value(name = "…")]` so clap accepts the dotted CLI form (`--scenario=isolated-linker.fresh-install.cold-cache.cold-store`). Display labels follow the slug structure: `Isolated linker: fresh install, cold cache + cold store` and `GVS linker: fresh restore, hot cache + hot store`. The `#[allow(clippy::enum_variant_names)]` is renewed; 5 of 6 variants share the `Isolated` prefix today. Once `Hoisted*` / `Pnp*` buckets land the lint will stop firing on its own. * style: apply rustfmt after scenario rename The longer match-arm pattern produced by the linker-first rename exceeded the rustfmt width budget. Auto-format breaks the `&["install", "--frozen-lockfile"]` body onto its own line so the arm stays within the limit. |
||
|
|
8c68c20211 |
fix(pacquet): drop synthetic transitive-peer entries from the importer lockfile (#11877)
Aligns `dependencies_graph_to_lockfile` with pnpm's `addDirectDependenciesToLockfile`, which only writes manifest-declared aliases into `importers["."].dependencies` / `importers["."].specifiers`. The earlier synthetic-specifier branch (added in #11867 to keep the top-level symlinks the old recursive walker happened to produce) wrote the resolver's transitive auto-installed peers into the importer block with the resolved version as their specifier, so `pnpm-lock.yaml` ended up listing entries the on-disk `package.json` never declared. `satisfies_package_manifest` then read those entries on the next install, flagged 17 "removed" specifiers against the alotta-files fixture, and the auto-frozen fast path (`preferFrozenLockfile: true`) fell through to the full fresh-resolve pipeline — turning the bench's `withWarmCacheAndLockfile` / `withWarmModulesAndLockfile` / `repeatInstall` scenarios from sub-second no-ops into ~4.5 s re-resolutions in pacquet@0.2.5. Verified against pnpm itself (`pnpm install react-dom@18.2.0` → lockfile lists only `react-dom` in `importers["."].dependencies`, no top-level `react` symlink). Pacquet now produces the same shape: transitive auto-installed peers live in `snapshots:` / `packages:` only, consumers reach them through their parent's slot's `node_modules`, and the freshness gate stays clean across installs. Local reproduction on the alotta-files fixture: - importer.dependencies count: 126 → 109 (matches the manifest exactly) - `pacquet install --frozen-lockfile`: was failing with "17 dependencies were removed", now succeeds - `repeatInstall`: ~22 s → ~0.5 s Test updates: - `auto_install_peers_hoists_missing_peers_at_importer` keeps the `.pnpm/` virtual-store assertions (the actual hoist-loop contract) and drops the top-level `node_modules/<peer>` symlink loop, which was asserting behavior pnpm itself doesn't produce. - `should_install_dependencies` snapshot drops the three `node_modules/@pnpm/{x,y,z}` entries for the same reason — those are `@pnpm/xyz`'s transitive peers and never reach the importer's `node_modules` under pnpm. Refs #11867. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
e7820bf650 |
perf(pacquet): cache per-wanted resolves in resolve_dependency_tree (#11874)
* perf(pacquet): cache per-wanted resolves in resolve_dependency_tree Memoise `resolver.resolve()` and `extract_children()` on the tree-walk context. The first visit for a given `(alias, bare_specifier, optional)` wanted dep runs the resolver chain; later visits clone the cached `Arc<ResolveResult>` and skip `pick_package`'s cache-key formatting, HashMap probing, and semver matching. The first visit for a given `pkgIdWithPatchHash` walks the manifest for child specs; later visits clone the cached `Arc<Vec<ChildSpec>>` and skip the JSON traversal. Per-occurrence `NodeId`s and the cycle-break gate are unchanged, so peer-suffix variants for non-leaf packages stay intact. Mirrors pnpm's `resolveDependencies.ts:1584` `isNew` gate at the wanted-dep edge, which is the layer pacquet's recursion shape can short-circuit without restructuring the tree builder. Refs #11869. * docs(pacquet): unlink cross-crate pick_package references `pick_package` lives in `resolving-npm-resolver`, not in the deps-resolver crate, so the `[`pick_package`]` intra-doc links broke `cargo doc --document-private-items` under `-D warnings`. Drop the link form and keep the bare backticks. |
||
|
|
6ae4b5d5ba |
chore(lint): more clippy rules (#11855)
* chore(lint): more clippy rules * fix: address clippy needless_collect warnings * fix: silence clippy or_fun_call warnings * fix: remove unnecessary clone in link_bins tests * chore(lint): reorder clippy warnings for clarity * fix: address clippy needless_collect warning in run_workers function * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix cargo fmt * fix: address review feedback on needless_collect lint suppressions Switch `#[allow]` to `#[expect]` and revert the `.any()` rewrite in `resolve_importer/tests.rs` back to the more readable `Vec<&str>` + `.contains()` form, suppressing the lint locally with a reason. --- Written by an agent (Claude Code, claude-opus-4-7). --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
d4a2b0364c | chore: update dependencies (#11802) | ||
|
|
c5a1d0842a |
perf(package-manager): route fresh-lockfile install through CreateVirtualStore (#11867)
Replace `install_with_fresh_lockfile`'s recursive per-package `install_subtree` walk with the phased warm/cold-batch pipeline `install_frozen_lockfile` already uses (`CreateVirtualStore` + `SymlinkDirectDependencies` + `LinkVirtualStoreBins`). The recursive shape pinned one tokio worker per in-flight package on its own rayon `par_iter` for the per-package link step; replacing it with a single phased `par_iter` over every snapshot closes most of the gap to pnpm on the no-lockfile install paths. 5-run hyperfine against the verdaccio mock, 10-core M-series Mac: | scenario | before | after | pnpm | who wins | |---------------------------|------:|------:|------:|----------| | clean-install, cold cache | 25.5s | 13.7s | 24.6s | **pacquet 1.79×** | | full-resolution, warm | 22.1s | ~10s | 9.4s | pnpm 1.07× (was 1.94×) | | frozen, cold cache | 21.1s | 19.6s | 21.7s | pacquet (unchanged) | | frozen, warm cache | 7.5s | 7.6s | 9.6s | pacquet (unchanged) | The `install_with_fresh_lockfile` path already builds the v9 lockfile shape from the resolved graph (`built_lockfile`) and constructs `VirtualStoreLayout::new` — `CreateVirtualStore` consumes exactly that shape, so the refactor is mostly plumbing. `install_subtree` and `InstallCtx` go away with it. `dependencies_graph_to_lockfile` is also updated so auto-installed peers (resolved under `autoInstallPeers: true` but not present in the manifest) make it into the importer's `dependencies` map with a synthetic specifier matching the resolved version. The recursive walker surfaced these as top-level symlinks by reading `direct_dependencies_by_alias` directly; `SymlinkDirectDependencies` reads `built_lockfile.importers["."]`, so without recording the hoisted peers there, the `auto_install_peers_hoists_missing_peers_at_importer` regression test fails. The `specifiers:` map skips the synthetic entry — only manifest-authored specifiers belong there. Refs #11866, #11857. |
||
|
|
976504fd04 |
perf(pacquet): close the clean-install gap to pnpm CLI (#11856)
Cuts pacquet's clean-install and full-resolution wallclock by roughly 3-4× on Linux (and roughly 2× on macOS), and brings user-CPU below pnpm's, primarily by: - pipelining tarball fetches with resolution (re-application of a prior draft); - caching disk-loaded packuments in `pick_package`'s in-memory `meta_cache` so repeated resolves of the same `(registry, name)` don't re-pay `spawn_blocking` + multi-MB `serde_json::from_str` (biggest single win); - de-duplicating the resolve-time tarball prefetch via an atomic `DashSet` claim (one spawn per unique URL, not per `(parent, child)` edge), routing the prefetch download through `SilentReporter` so it doesn't fire `pnpm:progress` ahead of `resolved`, and dropping a write-lock that was used purely to read a `CacheValue` variant; - shaving redundant pre-flight `stat` syscalls in `link_file` (~130k saved per alotta-files install) and `symlink_package` (~3-5k saved), and simplifying the EEXIST recovery to a single `return Ok(())` matching pnpm's `linkOrCopy`. ## Bench (alotta-files fixture, verdaccio mock) Linux CI numbers from the auto-bench on this PR: | Scenario | pacquet@main | pacquet@HEAD | pnpm | vs pnpm before → after | |--------------------------------|-------------:|-------------:|--------:|-----------------------:| | Clean Install | 19.7 s | **5.91 s** | 4.60 s | 4.29× → **1.29×** slower | | Full Resolution | 20.4 s | **5.12 s** | 3.34 s | 6.09× → **1.53×** slower | | Frozen Lockfile | 2.02 s | 2.17 s | 3.57 s | already 1.77× **faster** (unchanged) | | Frozen Lockfile (Hot Cache) | 756 ms | 752 ms | 2125 ms | already 2.83× **faster** (unchanged) | User CPU on clean-install drops from 23.9 s (pacquet@main) to 6.19 s (pacquet@HEAD), comparable to pnpm's 6.48 s — meaning the resolver no longer over-uses CPU. The frozen-lockfile path doesn't touch the resolver, so it's unchanged (the small shift is within the stddev bands). Local macOS sees a smaller win (Clean Install ~1.5× pnpm) — `sys` time is dominated by APFS metadata-journal contention that bites pacquet harder than pnpm because of pacquet's default `2 × available_parallelism` rayon pool. Tracked separately as #11851 / #11857. ## Commits 1. `perf(pacquet): pipeline tarball fetches with resolution` — wraps the resolver chain in a `PrefetchingResolver` that `tokio::spawn`s a `DownloadTarballToStore::run_with_mem_cache` after every successful resolve. Mirrors pnpm's `packageRequester.requestPackage` shape (the `fetching` promise is already running by the time the resolver returns). Re-applies an earlier draft (commit `f375c9161e` on the `pacquet-perf` branch). Also tightens the bench harness's git-repo check to accept worktree `.git` files via a `gitdir:` content check. 2. `perf(pacquet/resolving-npm-resolver): cache disk-loaded packuments in pick_package` — the version-spec disk fast path and the `publishedBy` mtime shortcut loaded the packument mirror from disk but never populated `ctx.meta_cache`, so every later resolve of the same `(registry, name)` re-ran `spawn_blocking(load_meta)` + a multi-MB `serde_json::from_str`. Promote the disk-loaded packument into the install-scoped cache. **Biggest single win** — resolve phase 25 s → 3 s, user CPU 4× lower. 3. `perf(pacquet/tarball): dedup prefetch spawns and read-lock cache state` — multiple improvements to the prefetcher's tarball download path: - Atomic per-URL dedup via a `DashSet<String>` on the `PrefetchingResolver`. The previous `mem_cache.contains_key()` gate was a TOCTOU pair (two concurrent resolvers could both pass it and both `tokio::spawn`); `DashSet::insert` is the atomic claim and only the winner spawns. The `MemCache` still backstops correctness. - Route the prefetch's `DownloadTarballToStore::run_with_mem_cache` through `SilentReporter` so `pnpm:progress fetched` / `found_in_store` don't fire ahead of the install pass's `resolved`. The install pass's later call to `run_with_mem_cache` now emits `found_in_store` on the `Available` short-circuit, so consumers still see the documented `resolved → fetched|found_in_store → imported` triple once per URL. - Switch the waiter's `cache_lock.write().await` to `read().await` — the variant inspection is read-only, and the owner branch is the only writer. 4. `perf(pacquet/package-manager): drop pre-flight stats in link_file` — first half of the link_file simplification: the original two `fs::metadata` + `fs::symlink_metadata` calls per file collapsed to a single `fs::metadata` short-circuit, deferring dangling-symlink detection to the EEXIST recovery path. 5. `perf(pacquet/package-manager): drop redundant create_dir_all in symlink_package` — `force_symlink_dir` already handles a missing parent via its own `NotFound` → `create_dir_all` retry; the explicit pre-create was paying ~3-5k unneeded `stat`s on a typical install. 6. `fix(pacquet/package-manager): keep one stat in link_file, simplify EEXIST` — second half of the link_file simplification. Keeps the single `fs::metadata` pre-check (needed because `fs::copy` overwrites silently rather than surfacing EEXIST, so `Copy` callers need the no-op short-circuit) and simplifies the EEXIST recovery to a single `return Ok(())` matching pnpm's `linkOrCopy`. Drops the dangling-symlink scrub for the same reason — pnpm doesn't scrub either, and CAFS contents are content-addressed so a "stale" dangling symlink from a crashed prior install is a corruption case the install layer is not the right place to repair. Test `link_file::tests::dangling_symlink_is_replaced` → `dangling_symlink_is_preserved` locks in the new (pnpm-aligned) contract. ## Where the remaining gap lives pacquet@HEAD user-CPU on clean-install is now **lower** than pnpm's (6.19 s vs 6.48 s on the Linux CI). The remaining ~1.3-1.5× wallclock gap is `sys` time — file-system syscalls during the link phase (verify-files-cache stats, per-file `linkat`/`clonefile`/`copy_file_range`, store-index lookups). The macOS-only multiplier on top of that is APFS metadata-journal contention from pacquet's over-subscribed rayon pool; see #11851 and the follow-up tracker #11857. |
||
|
|
d4136eb6f6 |
chore(pacquet/lint): more clippy (#11839)
Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
815e507d67 |
ci(integrated-benchmark): scenarios without lockfiles (#11838)
* ci(benchmark): run all six integrated-benchmark scenarios Wires `clean-install`, `full-resolution`, `peek`, and `gvs-warm` into `pacquet-integrated-benchmark.yml` so per-PR runs cover the same scenario set the manual `benchmark.yml` workflow already exercises via `benchmarks/bench.sh`. Requested for #11837, where the perf delta affects the resolution-bound scenarios (`firstInstall`, `withWarmCache`, `withWarmModules`, `updatedDependencies`) that the prior two-scenario set did not measure. Each scenario gets its own step with a 10 min hyperfine timeout (same rationale as the existing steps) and writes per-scenario report copies that the summary step concatenates into `SUMMARY.md`. * ci(benchmark): drop peek and gvs-warm scenarios Keep only the two new no-lockfile scenarios (`clean-install`, `full-resolution`) on top of the existing `frozen-lockfile` and `frozen-lockfile-hot-cache`. #11837's perf change is in the fresh-lockfile install path, which only runs when resolution runs — i.e., exactly the no-lockfile scenarios. `peek` mutates an existing lockfile and `gvs-warm` is a frozen-lockfile variant; neither exercises the affected path, and including them only costs per-PR CI wall time. * fix(bench): pin packages: ['.'] in synthesized pnpm-workspace.yaml The integrated-benchmark clones each pacquet revision's source tree into `<bench_dir>/pacquet/`, which on the pnpm/pnpm monorepo includes upstream test fixtures like `workspace/project-manifest-reader/__fixtures__/invalid-package-json/package.json` — intentionally malformed JSON used to exercise pnpm's manifest reader. Without a `packages:` field, both pnpm's `findPackages.ts:28` and pacquet's `crates/workspace/src/projects.rs:128` default to `[".", "**"]`, so the fresh-resolve install path's `find_workspace_projects` walk descends into the cloned source tree and trips on the bad fixture: Error: pacquet_package_manifest::serialization_error × installing dependencies ╰─▶ expected `,` or `}` at line 3 column 3 The walk only runs on the fresh-lockfile branch (`install.rs:628-630`), which is why frozen-lockfile and frozen-lockfile-hot-cache stay green while clean-install and full-resolution fail every time. Pin `packages: ['.']` in the synthesized manifest so enumeration stays at the workspace root. The benchmark's installs are single-project, so this doesn't narrow anything the install actually needed to see. Fixtures supplied via `--fixture-dir` that already declare `packages:` keep their own value. * ci(benchmark): bump no-lockfile scenarios to 20 min Clean-install and full-resolution go through pacquet's fresh-resolve install path, which is currently ~3-5x slower than pnpm on the `alotta-files` fixture (pnpm/pnpm#11832). Hyperfine's default 1 warmup + 10 timed runs across three benchmark targets (pacquet@HEAD, pacquet@main, system pnpm) projects to ~13 min wallclock for these two scenarios, putting the previous 10 min cap right on the edge. Doubling to 20 min keeps the per-step timeout meaningful as a stuck- install detector without losing CI time when the bench is healthy. The frozen-lockfile steps stay at 10 min — they don't traverse the slower fresh-resolve path. * fix(bench): drop --no-frozen-lockfile from full-resolution scenario Pacquet doesn't expose `--no-frozen-lockfile` (only `--frozen-lockfile`, `--prefer-frozen-lockfile`, and `--no-prefer-frozen-lockfile`). Passing it makes clap reject the install: error: unexpected argument '--no-frozen-lockfile' found tip: a similar argument exists: '--frozen-lockfile' The flag was redundant for this scenario anyway: full-resolution starts every iteration with no lockfile on disk (init() skips the lockfile when `lockfile_enabled()` is false; cleanup removes it; `lockfile=false` in the synthesized npmrc/workspace prevents writing one). With no lockfile present the frozen path is unreachable regardless of the flag, so both tools take fresh resolution by definition. Fold full-resolution into clean-install's bare `install` arm. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
54ff453268 |
perf(pacquet): collapse leaf-package occurrences in the dependencies tree (#11847)
Mirrors pnpm's `pkgIsLeaf` reuse: a package with no `dependencies`, `optionalDependencies`, `peerDependencies`, or `peerDependenciesMeta` collapses every parent reference onto one tree node keyed by the package id, instead of allocating a fresh `NodeId` per occurrence. Peer resolution and every downstream `HashMap<NodeId, _>` see the reduced node set. Closes #11844. |
||
|
|
3422cecfd3 |
fix(installing.deps-resolver): deterministically order cyclic peer suffixes (#11826)
* fix(installing.deps-resolver): deterministically order cyclic peer suffixes (#8155)
`resolveDependencies` was pushing onto `pkgAddresses`, `postponedResolutionsQueue`,
and `postponedPeersResolutionQueue` from inside `Promise.all`-spawned callbacks,
so the order of items in those arrays reflected completion timing rather than
the order of `extendedWantedDeps`. That ordering then flowed downstream into
`resolvePeers` and the cyclic-peer suffix assignment, so two packages with
transitive peer dependencies on each other (e.g. `@aws-sdk/client-sts` and
`@aws-sdk/client-sso-oidc`) flipped between two equally-valid lockfile forms
across consecutive installs.
The fix awaits `Promise.all` to a temporary array and drains it with `for…of`
so the per-edge results land in input order. This matches the existing pattern
200 lines earlier in `resolveDependenciesOfImporters`.
End-to-end repro from the issue (`pnpm add @aws-sdk/client-s3@3.588.0` then
loop `pnpm dedupe --check`): 33/50 failures without the fix → 0/100 with it.
---
Written by an agent (Claude Code, claude-opus-4-7).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(installing.deps-installer): replace all slashes in mock metadata path
Addresses CodeQL incomplete-string-escaping finding: `replace('/', '%2F')`
only swaps the first occurrence. Scoped names in this test only have one
slash so the behavior is unchanged, but switching to `replaceAll` clears
the warning and is more defensible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(installing.deps-installer): assert raw snapshot key order
Removed the .sort() applied to the lockfile snapshot keys in the cyclic
peer determinism test so the comparison reflects the actual order
emitted by the lockfile writer. The deterministic ordering guaranteed
by
|
||
|
|
5353fcbf01 |
perf(pacquet): close the warm-cache resolve gap to pnpm CLI (#11837)
Closes #11832. On the `alotta-files` benchmark (1362 nodes, warm cache, GVS on), pacquet was 3-5× behind the TypeScript pnpm CLI whenever resolution ran (`firstInstall`, `withWarmCache`, `withWarmModules`, `updatedDependencies`). Wall-clock dropped from ~11.83s to ~5.03s on this branch; pnpm sits at ~4.16s, and the remaining gap is concentrated in the resolver's per-node `pick_package` walk (3.1s of the 5.03s — see #11843 for the `peekManifestFromStore` follow-up that would close it). The branch is a series of small wins rather than one big rewrite. The original `PrefetchingResolver` (commit |
||
|
|
9eb632bfbd |
fix(registry): tolerate non-string entries in dependency maps (#11833)
Historical npm registry entries sometimes carry object-valued
`dependencies` / `devDependencies` / `peerDependencies` (e.g.
`deep-diff@0.1.0`'s nested `devDependencies: { vows: { version, ... } }`).
pnpm's JS path silently ignores these via later `typeof spec === 'string'`
checks; pacquet's strict serde failed the whole packument with
`invalid type: map, expected a string`, surfacing as a spurious
`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` for an unrelated version pinned
in the lockfile.
Custom deserializer drops non-string entries while keeping the string
ones, matching pnpm's tolerance. Issue #11829.
|
||
|
|
fd564a43b9 |
ci(benchmark): consolidate (#11741)
Unify the two install-benchmark stacks (`benchmarks/bench.sh` and `pacquet/tasks/integrated-benchmark/`) into one shared Rust orchestrator. Scenario, fixture, workspace-manifest, install-script, and report generation live in one place so changes propagate to both stacks. Scenario sets per workflow are unchanged: `benchmark.yml` keeps measuring the same 6 scenarios; `pacquet-integrated-benchmark.yml` keeps measuring the same 2. Both verdaccio and live-npm registry modes are preserved; neither is removed in favor of the other. All six scenarios accept both `pacquet@<rev>` and `pnpm@<rev>` targets. |
||
|
|
fecaee0b35 | chore: update pnpm and add pacquet to config deps (#11765) | ||
|
|
a5a2c2482e |
fix(pacquet/store-dir): gate verify_file's destructive branch on cas_write_lock (#11825)
The verifier was racing in-flight writers: while `ensure_file` held `cas_write_lock(path)` and was partway through `write_all`, a sibling snapshot's `check_pkg_files_integrity` would stat the same CAS path (no lock), see a partial size, and call `remove_stale_cafs_entry(path)`. The writer's `write_all` then continued against an orphan inode, the writer's `cas_paths` was populated with the now-deleted path, and `link_file` later hit ENOENT — the CI failure shape on #11816 / 0.2.2-7. Fix (Option C): keep `verify_file`'s lock-free fast path (the common case: file unchanged since prior install, `is_modified` false), but acquire `cas_write_lock(path)` before any branch that could call `remove_stale_cafs_entry`. Re-`check_file` under the lock so a writer's full `write_all` lands before we evaluate. Performance: the fast path adds zero overhead. The slow path — files whose mtime is > 100 ms past the recorded `checked_at` — takes one uncontended Mutex acquire per file, sub-microsecond on uncontended locks. Contention only fires when a writer + a verifier hit the same blob simultaneously; the wait is bounded by one `write_all` and trades a millisecond-scale wait for avoiding a network re-fetch. The new integration test in `pacquet-store-dir/tests/` is a deterministic reproducer: it acquires `cas_write_lock` from the test thread (standing in for an in-flight writer), pre-seeds a partial CAS file at the matching path, and runs the verifier in the background. Pre-fix, the verifier unlinks the file while the "writer" is still simulated as in-progress; post-fix, the verifier blocks on the lock until released. To make `cas_write_lock` reachable from the store-dir crate the function was promoted from `fn` to `pub fn` in pacquet-fs. |
||
|
|
f9a0abe02d |
test(pacquet/fs): port upstream multi-process CAS stress tests (#11823)
Adds the three cross-process scenarios upstream pnpm covers in
store/cafs/test/writeBufferToCafs.test.ts but pacquet only covered
intra-process (one 32-thread test): N workers racing on the same
target with a corrupt pre-seed, with a truncated pre-seed, and on a
clean target.
Pacquet's `cas_write_lock` is process-local (`OnceLock<DashMap<...>>`)
just like upstream's `locker: Map<string, number>`, so the cross-
process safety contract lives entirely in `O_CREAT | O_EXCL` +
atomic-rename. The existing 32-thread test in `ensure_file::tests`
exercises the lock; the new suite exercises the unprotected
filesystem-only path so a regression in the `verify_or_rewrite` +
`write_atomic` recovery would surface as a test failure instead of a
production install failing to import a CAS file at link time.
Approach:
- New `[[bin]]` `cafs_stress_worker` under `pacquet-fs/src/bin/`.
Reads a content fixture from argv[1] and a target path from
argv[2], calls `ensure_file`, exits 0/1. Tiny and test-only;
`pacquet-fs` is `publish = false` so an extra bin target is free.
- New integration test `pacquet-fs/tests/ensure_file_stress.rs`.
Uses `env!("CARGO_BIN_EXE_cafs_stress_worker")` to find the bin
Cargo builds alongside the test, then spawns 8 instances per
scenario via `std::process::Command`.
- Each scenario then asserts every worker exited 0 and the final
on-disk content sha-512-matches the expected payload.
Each recovery test was verified to catch a regression: temporarily
bypassing `verify_or_rewrite` flips both recovery tests red while
the clean-target test (which doesn't pre-seed anything) stays
green, matching upstream's coverage shape.
|
||
|
|
1386b5987d |
feat(pacquet): honor preferFrozenLockfile in the install dispatch (#11824)
`pacquet install` (no flag) didn't consult `preferFrozenLockfile`. A fresh lockfile got re-resolved from the registry instead of taking the cheap frozen path, and a stale lockfile was silently overwritten without seeding the resolver from the existing pins. Closes pnpm/pnpm#11815. The install dispatch now has four ordered states: 1. `--frozen-lockfile` flag → frozen path (lockfile required, freshness check fatal). 2. No flag + lockfile present + effective `preferFrozenLockfile == true` + freshness check passes → frozen path (same code as state 1). 3. No flag + lockfile present + opt-out or stale → fresh-resolve, seeded from the existing lockfile's snapshots so unrelated pins survive the rewrite (mirrors upstream's `update: false` resolver mode). 4. No lockfile → fresh-resolve with no seed. `check_lockfile_freshness` is the shared helper: it runs `pnpm.overrides` parsing, `check_lockfile_settings`, the overrides-aware manifest re-apply, and `satisfies_package_manifest`. State 1 surfaces its `Err` as `InstallError`; state 2 treats a stale-lockfile `Err` as fall-through and surfaces `InvalidOverrides` as fatal. CLI exposes `--prefer-frozen-lockfile` / `--no-prefer-frozen-lockfile` mirroring pnpm so users can override per invocation; `pacquet add` opts out of the fast path explicitly since the manifest is necessarily stale by the time the install dispatch runs. |
||
|
|
5881b57115 |
feat(pacquet): honor enableGlobalVirtualStore in the fresh-resolve install path (#11819)
`pacquet install` (no flag, fresh project) was hardcoded to `VirtualStoreLayout::legacy`, so it materialized packages under the project-local `node_modules/.pnpm/` even when `enableGlobalVirtualStore: true` was configured. Closes pnpm/pnpm#11814. A new `build_lockfile_view_from_resolver_graph` adapter converts the resolver's `DependenciesGraph` into the `snapshots:` / `packages:` shape `VirtualStoreLayout::new` already consumes, so all hashing, slot-dir, and bin-linker code is shared with the frozen-lockfile path. The without-lockfile flow now also calls `register_project` against the shared store when GVS is on, mirroring the frozen-lockfile branch. Side effect: aligns the without-lockfile path's per-package save directory with the peer-suffixed slot the children-recursion was already using, so peer-suffixed snapshots no longer split between two unreachable slot directories. |
||
|
|
8695496f58 |
feat(pacquet): write pnpm-lock.yaml and <vsd>/lock.yaml on fresh install (#11816)
* feat(pacquet): write pnpm-lock.yaml on fresh install Adds a `dependencies_graph_to_lockfile` adapter that converts the resolver's `DependenciesGraph` into a v9 `Lockfile`, hooks it into the install path so a fresh `pacquet install` produces a wanted lockfile, renames `InstallWithoutLockfile` to `InstallWithFreshLockfile` to match the new behavior, drops the `UnsupportedLockfileMode` branch from the dispatch, and flips the `config.lockfile` default from `false` to `true` to match pnpm. Closes pnpm/pnpm#11813. * chore(pacquet): fix rustdoc + rustfmt CI failures - Drop the broken `[ResolvedPackage.optional]` intra-doc link (no such item in scope; the reference is upstream's, and the prose is enough without the link). - Disambiguate two `dependencies_graph_to_lockfile` intra-doc links to the function form so rustdoc doesn't error on the function/module collision. - Flatten an `assert!(prod.contains_key(...))` so rustfmt + dylint agree on the body (rustfmt wanted the call wrapped, dylint wanted a trailing comma; extracting the key into a local resolves both). * feat(pacquet): write <virtual_store_dir>/lock.yaml in the fresh path After a fresh install, also persist the current-lockfile alongside `pnpm-lock.yaml` so the next install can diff each snapshot against it and skip the unchanged slots — the same optimization the frozen path already enables. `InstallWithFreshLockfile::run` now returns an `InstallWithFreshLockfileResult` that surfaces the freshly-built `Lockfile` to the caller. `install.rs` saves it as `<virtual_store_dir>/lock.yaml` after `.modules.yaml` succeeds, mirroring the frozen path's safety property: a manifest-write failure can't leave a current-lockfile pointing at incomplete install state. The wanted lockfile and the current lockfile describe the same resolved graph here — the resolver only walked what the install requested, so no `filter_lockfile_for_current` step is needed. Both writes are gated on `config.lockfile`, matching upstream pnpm's `useLockfile` opt-out. * feat(pacquet): propagate optional flag from ResolvedPackage to SnapshotEntry `SnapshotEntry.optional` was hard-coded to `false`, so every snapshot looked "non-optional" and `BuildModules` would treat any build failure as fatal even for packages reachable only via `optionalDependencies`. Port upstream pnpm's `ResolvedPackage.optional` propagation: 1. `ResolvedPackage` (the dedup envelope) gains an `optional: bool` field. The walker seeds it from `wanted.optional || parent.optional` on the first visit and AND-folds it with `current_is_optional` on every subsequent visit, so a single non-optional path flips it back to `false` and keeps it there. Mirrors resolveDependencies.ts:1625-1630. 2. `extract_children` now emits a per-child `is_optional` flag — `true` when the entry came from the package's `optionalDependencies` map. 3. `resolve_dependency_tree` and `resolve_importer` look up the importer's `optionalDependencies` set and tag each direct dep, then propagate the flag down the recursion. 4. `DependenciesGraphNode` carries the resolved package's `optional` field so peer-variants of the same `pkgIdWithPatchHash` share it. 5. The lockfile adapter writes `SnapshotEntry.optional = node.optional` instead of hard-coding `false`. Tests: - 4 unit tests on the resolver (direct optional seed, transitive inheritance, AND-fold when a non-optional path exists, transitive via a parent's `optionalDependencies` edge). - 1 unit test on the adapter (`SnapshotEntry.optional` round-trips). - 1 integration test through `Install` asserting a top-level `optionalDependencies` entry produces `optional: true` in the written `pnpm-lock.yaml`. Each test was verified to catch a regression by temporarily breaking the implementation before re-checking it green. * fix(pacquet): fail fast when fresh install gets node_linker: hoisted or --no-runtime The fresh-lockfile dispatch silently dropped `skip_runtimes` and `node_linker` — neither was forwarded into `InstallWithFreshLockfile`, so a fresh `pacquet install` with `--node-linker=hoisted` produced an isolated `node_modules` layout, and `--no-runtime` materialized runtime archives anyway. Pacquet's hoist pass and runtime-snapshot filter both run only against a loaded lockfile (frozen-lockfile path); honoring the flags on a fresh install needs upstream's per-snapshot filter and a hoist pass over the freshly-built graph. Both ports are out of scope for this PR. Refuse the unsupported combinations up front instead, before any reporter event fires or any state file is written, so a follow-up retry under `--frozen-lockfile` against an existing lockfile lands on the supported path. Adds `UnsupportedFreshInstallNodeLinker` and `UnsupportedFreshInstallSkipRuntimes` error variants plus integration tests asserting both that the error fires and that no `pnpm-lock.yaml`, `lock.yaml`, or `.modules.yaml` ends up on disk. * chore(pacquet): broaden fresh-install-flag error messages to non-frozen installs The `UnsupportedFreshInstall*` errors fire on any `!frozen_lockfile` run, not just first installs — pacquet doesn't honor an existing lockfile without `--frozen-lockfile` yet (stale-lockfile rewrite is a follow-up). So a re-install with a pinned `pnpm-lock.yaml` but no `--frozen-lockfile` would hit the same gate, and the "on a fresh install yet" wording pointed users at the wrong condition. Reword the display strings to "without --frozen-lockfile yet" so the prompt matches the actual gate. Variant names and diagnostic codes stay put — the next round will rename them along with the fresh-install-path semantics. Surfaced by coderabbitai review on #11816. |
||
|
|
22d6742960 |
fix(pacquet): resolve catalog: in pnpm.overrides before freshness check (#11820)
The frozen-lockfile freshness check compared the lockfile's overrides map (with `catalog:` already expanded by pnpm) against the raw config map (still containing `catalog:` strings), so every catalog-backed override surfaced as `ERR_PNPM_OUTDATED_LOCKFILE` on every install. Mirror pnpm's `parseOverrides(overrides, catalogs)` → `createOverridesMapFromParsed` pipeline: thread `&Catalogs` through `parse_overrides[_iter]`, resolve each value via `resolve_from_catalog`, and flatten the resolved entries into the map handed to `check_lockfile_settings`. |
||
|
|
501681044e | chore(release): 11.2.2 (#11817) v11.2.2 | ||
|
|
f279d77d60 |
fix(pacquet/cli): patch --version literal during release build (#11812)
The `pacquet --version` string is a hardcoded clap attribute in `cli_args.rs`. It didn't get bumped for the 0.2.2 release, so the published binary still reports 0.2.1. Bump the literal to 0.2.2 and add a release-workflow step that rewrites the attribute from `inputs.version` before `cross build`, so future releases stay correct automatically. A trailing `grep -F` fails the job loudly if the regex stops matching after a future refactor of the attribute. |
||
|
|
881a86541b |
fix(installing.commands): forward pnpm install flags to pacquet (#11781)
* fix(installing.commands): forward `pnpm install` flags to pacquet When the install engine is delegated to pacquet via configDependencies, pnpm hard-coded the args to `install --frozen-lockfile --reporter=ndjson` and silently dropped the user's other CLI flags. `pnpm install --no-runtime` therefore still installed the workspace's runtime devDependency, clobbering the Node version the surrounding tooling had set up — visible as the `Verify Node version` failure on PR #11765 where setup-pnpm provisions Node 24.0.0 but pacquet then materializes node 24.6.0. Pacquet's `install` subcommand already mirrors pnpm's surface for the common flags (`--no-runtime`, `--prod`, `--dev`, `--no-optional`, `--node-linker`, `--offline`, `--prefer-offline`, `--cpu`/`--os`/`--libc`). Forward the user's argv verbatim when the command is `install`/`i`; `add`/`update`/`dedupe` still don't forward — their flag surfaces don't line up with pacquet's `install`. * fix(installing.commands): pass --ignore-manifest-check to pacquet `pnpm up` / `add` / `remove` were aborting with `pacquet_package_manager::outdated_lockfile` whenever pacquet was declared in `configDependencies`. After resolving and writing the updated lockfile, pnpm hands materialization off to pacquet but hasn't yet written the post-mutation `package.json` — that write happens after `mutateModules` returns. Pacquet's frozen-lockfile freshness gate then saw the new lockfile paired with the pre-mutation manifest and refused to install. Pass pacquet's new `--ignore-manifest-check` flag (pacquet PR #11811) on every delegation. The flag is narrow: it only skips `satisfies_package_manifest`. Settings drift like `overrides` is still enforced, and pnpm already re-validated the lockfile before delegating, so re-checking the manifest here was redundant work that only ever fired false positives on the mutate-then-materialize path. Requires a pacquet release that ships the flag; bump `PACQUET_VERSION` in `pnpm/test/install/pacquet.ts` once it does, or the existing e2e tests will fail against pacquet 0.2.2-9 (which doesn't recognize the flag and clap would reject). Closes #11797. --- Written by an agent (Claude Code, claude-opus-4-7). * fix: update pacquet in tests * fix(installing.commands): strip positionals + always-injected flags when forwarding to pacquet `collectForwardedFlags` checked `argv[0] === 'install'` to find the command token to strip. Any global flag the user typed before `install` (e.g. `--config.registry=...` in the e2e test) shifted the token out of position, so the function returned the full argv and pacquet saw `install` twice — `error: unexpected argument 'install' found`. Use the parsed argv that `@pnpm/cli.parse-cli-args` already produced: `remain` lists positionals (the `install`/`i` token and nothing else on this code path, since `isInstallCommand` is only true when no package params are present), and `original` preserves the user's exact tokens. Drop positionals + the flags we always inject (`--reporter=ndjson`, `--frozen-lockfile`, `--ignore-manifest-check`) so clap doesn't reject duplicates either. `original` over `cooked` deliberately: nopt's `cooked` splits `--key=value` into two tokens, which would break pacquet's `--config.<key>=<value>` parser (it requires the `=` form). * fix(installing.commands): make argv.cooked/remain optional on InstallCommandOptions Widening these to required broke test fixtures elsewhere (publish/pack/ deprecate/dist-tag/deploy) that construct minimal `argv: { original }` options for code paths that never reach pacquet. Only the pacquet delegation actually reads `remain`, so make the two new fields optional on the shared options type and supply a default at the runPacquet call site. The runtime path through main.ts already populates all three. * fix(installing.commands): strip any user-supplied --reporter when forwarding to pacquet Pacquet's `--reporter` is a clap value option with last-value-wins semantics, so `pnpm install --reporter=silent` (or `--reporter silent` two-token form) reached pacquet and overrode the `--reporter=ndjson` pnpm injects, breaking the NDJSON-to- streamParser plumbing the default reporter depends on. The previous filter only matched the exact `--reporter=ndjson` token. Walk argv with a lookahead so both `--reporter=<value>` and `--reporter <value>` are dropped without consuming an adjacent flag. * fix(installing.commands): drop negated/value forms of always-injected flags `collectForwardedFlags` only matched the exact positive tokens `--frozen-lockfile` and `--ignore-manifest-check`, so a user typing `pnpm install --no-frozen-lockfile` (or `--frozen-lockfile=false`) forwarded the negation to pacquet, which then saw both our injected `--frozen-lockfile` and the user's `--no-frozen-lockfile` and crashed clap with "unexpected argument". Match every shape the user can write the same flag in: positive, `--no-` negated, and any `=value` form. Can't blindly strip `--no-` either way — pacquet has flags whose literal name starts with `no-` (`--no-runtime`, `--no-optional`); those must still forward. The user's `--no-frozen-lockfile` intent is honored upstream — pnpm did a fresh resolve before delegating; pacquet's role here is just lockfile-driven materialization, which is always frozen. * fix(installing.commands): match positionals by index, hide reporter from dropped-flags warning `collectForwardedFlags` matched positionals via `new Set(argv.remain)`, which strips by value: a flag value that happened to equal a positional token (e.g. `pnpm install --node-linker install`) was wrongly dropped from the forwarded list, costing pacquet the value of `--node-linker`. Walk `argv.original` with a subsequence pointer into `argv.remain` so only the actual positional indexes get skipped. `collectDroppedFlags` still surfaced `--reporter foo` / `--reporter=foo` in the "may not be honored" warning on `add`/`update`/`dedupe`, but pnpm honors reporter selection itself before delegation — so the warning was misleading. Route both helpers through the same `isAlwaysInjected` check and consume `--reporter` and its value the same way `collectForwardedFlags` already does. |
||
|
|
0dd1ec445c |
feat(pacquet): add --ignore-manifest-check to skip frozen-lockfile manifest gate (#11811)
Surfaces a narrow CLI flag on `pacquet install` that gates only `satisfies_package_manifest`. Settings-drift checks (`overrides`, `ignoredOptionalDependencies`, …) still fire, and the broader `--ignore-package-manifest` name is reserved for a future port of pnpm's `pnpm fetch` semantics (which skip linking / hoisting / pruning too). Intended for the pnpm CLI's `configDependencies` delegation path (issue #11797): pnpm resolves and writes the lockfile, then hands materialization to pacquet but hasn't yet written the post-mutation `package.json`. With the flag set, the freshness gate skips the per-importer manifest check that would otherwise reject every `pnpm up` / `add` / `remove` with `ERR_PNPM_OUTDATED_LOCKFILE`. The matching pnpm-side change to forward the flag lands separately. Refs #11797. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
3a6392828c |
fix(pacquet/config): pick the store on the project's volume (#11804)
* fix(pacquet/config): pick the store on the project's volume Port pnpm's `getStorePath` / `storePathRelativeToHome` cross-volume detection to pacquet's `default_store_dir`. When no `storeDir` is explicitly set (global config.yaml, pnpm-workspace.yaml, or `PNPM_CONFIG_STORE_DIR`), `Config::current` now probes whether the project root can be hardlinked into the user's pnpm home dir. If not, it walks from the filesystem root toward the project to find the volume mount point and falls back to `<mountpoint>/.pnpm-store` — matching pnpm's behaviour at https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L14-L78. Before this fix, a workspace on a separate volume (e.g. `/Volumes/src/` on macOS) installed into the home-volume store (`~/Library/pnpm/store`). When the home volume is case-insensitive and the workspace volume is case-sensitive, typescript-eslint's path-cache canonicalises against the home store and then can't locate the same files in TypeScript's case-sensitive program loaded from the workspace, so `eslint --fix` fails with "TSConfig does not include this file" on every project file. The hardlink probe goes through a new `LinkProbe` capability in `pacquet-config::api`, threaded into `Config::current`'s `Sys` bound so tests can pin the linkability answer without touching disk. The production `Host` impl performs real link attempts via `store_path::host_can_link_between_dirs`. Test fakes get an inert `LinkProbe` impl via the `inert_link_probe!` macro added to the `tests` module — every probe returns `false`, the algorithm walks without finding a mount, and the SmartDefault home store survives unchanged, so existing cascade assertions stay valid. * fix(pacquet/config): satisfy rustdoc + Windows clippy - Drop the redundant explicit `[LinkProbe][crate::api::LinkProbe]` link target and fully qualify `[StoreDir]` as `[pacquet_store_dir::StoreDir]` so the `Doc` job passes under `-D rustdoc::broken-intra-doc-links` and `-D rustdoc::redundant-explicit-links`. - Gate the `PrefixProbe` fake, its `ALLOW_PREFIXES` static, and the related imports with `#[cfg(unix)]`. Every consumer is already `#[cfg(unix)]`, so on Windows the items were unreferenced and the `Lint and Test (windows-latest)` job's clippy run failed under `-D dead-code`. * test(pacquet/config): serialize PrefixProbe scenarios with a scenario lock `ALLOW_PREFIXES` was only locked for the read/write of its `Vec`, not across "set allowlist then probe" — under nextest's default parallel execution two scenarios could race: scenario A would set its prefixes, scenario B would overwrite them, and A's `resolve_store_dir` would observe B's allowlist. Add `PREFIX_PROBE_SCENARIO_LOCK` and a `PrefixProbe::with_allow(prefixes, body)` helper that holds it across the entire set-and-probe so scenarios serialise cleanly. Per CodeRabbit review on pnpm/pnpm#11804. * test(pacquet/config): rename single-letter `R` to `Output` for Perfectionist Dylint's `perfectionist::single-letter-generic` flagged the `R` return-type parameter on `PrefixProbe::with_allow`. Rename to `Output`. |
||
|
|
09b7e9d354 |
docs: widen pacquet parity scope to all dep-management commands (#11810)
Update the scope caveat in AGENTS.md to state that pacquet's parity surface now covers `install`, `add`, `update`, and `remove`, rather than `install` alone. |
||
|
|
86bf5db996 |
feat(pacquet/cli): accept --config.<key>=<value> overrides (#11806)
When pnpm delegates `install` to the pacquet binary it forwards the user's `pnpm install` flags verbatim, including pnpm's `--config.<key>=<value>` universal syntax (handled by npm-conf upstream). Pacquet's clap parser previously rejected those tokens with `unexpected argument '--config.registry'`, causing the delegated install to fail. Strip every `--config.*` token out of argv before clap parses it and layer the values onto `Config` after `.npmrc`/yaml have been applied, mirroring pnpm 11's `CLI > yaml > .npmrc > defaults` precedence. Only `registry` is wired in for now (matches the immediate test gap); unknown keys are accepted silently so a pnpm-side key pacquet hasn't ported yet doesn't break the delegation. Malformed tokens (`--config.foo`, `--config.=value`) are dropped on the same path so clap never sees them either. |
||
|
|
69f8ea8de4 |
feat(pacquet): port blockExoticSubdeps to reject exotic subdeps (#11792)
Rejects git/tarball/file resolutions reached transitively from the
importer when `blockExoticSubdeps` is on. Direct deps remain allowed.
Mirrors pnpm's gate at
|
||
|
|
213136dec3 |
feat(pacquet/resolving-npm-resolver): port abbreviated metadata fast path (#11794)
* feat(pacquet/resolving-npm-resolver): port abbreviated metadata fast path Default the resolver-time fetch to the abbreviated install-v1 packument (`application/vnd.npm.install-v1+json`) and only request full metadata when the call explicitly needs it. Mirrors pnpm's `fullMetadata = opts.optional || ctx.fullMetadata` derivation in [pickPackage.ts L201](https://github.com/pnpm/pnpm/blob/2a9bd897bf/resolving/npm-resolver/src/pickPackage.ts#L201). - Adds `full_metadata` to the cached/non-cached fetchers and routes the Accept header + mirror dir (`ABBREVIATED_META_DIR` vs `FULL_META_DIR`) off it. - `PickPackageContext.full_metadata` is the install-wide bias; `PickPackageOptions.optional` forces full per-call (pnpm#9950). In-memory cache key gains a `:full` suffix when full so the two modes can coexist without contamination. - Ports `maybeUpgradeAbbreviatedMetaForReleaseAge`: when `published_by` is active, the picker ends up with an abbreviated packument that lacks per-version `time`, and the package's top-level `modified` falls past the cutoff, the orchestrator re-fetches full metadata so the maturity check runs on real timestamps. The upgraded full meta is persisted back to the abbreviated mirror so the next install skips the upgrade fetch. - Verifier and resolver pass the appropriate flag explicitly (`true` for the verifier, `false` for the resolver and named registry resolver). Tests cover the abbreviated default, the optional override, the cache-key separation, the boundary case `modified == cutoff`, and the upgrade trigger when `modified > cutoff`. --- Written by an agent (Claude Code, claude-opus-4-7). * docs(pacquet/resolving-npm-resolver): disambiguate fetch_full_metadata intra-doc link `fetch_full_metadata` is both a function and a module, so the bare `[fetch_full_metadata]` link triggers `rustdoc::broken_intra_doc_links` under `--deny warnings`. Add parentheses so it resolves to the function. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
400b21a90f |
feat(pacquet): port pnpm-workspace.yaml overrides support to the install chain (#11793)
* feat(pacquet): port pnpm.overrides support to the install chain
Adds a new `pacquet-config-parse-overrides` crate (port of
`@pnpm/config.parse-overrides`), threads `overrides` through
`Config`/`WorkspaceSettings`, surfaces lockfile-side drift as
`StalenessReason::OverridesChanged` (matching upstream's
`getOutdatedLockfileSetting` overrides branch), and applies the parsed
overrides to a cloned root manifest before the frozen-lockfile
freshness check so post-override lockfile specifiers line up with the
on-disk manifest. The read-package-hook port (`VersionsOverrider`)
mirrors upstream's `createVersionsOverrider` minus the peer-arm
promotion, which is deferred until peer install lands. Catalog refs in
override values surface as `INVALID_OVERRIDES` until catalogs are
ported.
* chore(pacquet): satisfy Dylint Perfectionist lints and fix stale doc link
Renames single-letter closure / function / generic params introduced
by the overrides port to descriptive names, fixes trailing-comma
policy in test macro invocations, swaps the Windows path literal to
a raw string, and removes a stale `[`Self::root_dir`]` rustdoc link
left behind when the `root_dir` field was dropped from
`VersionsOverrider`.
* style(pacquet): apply rustfmt to install.rs overrides_map binding
* fix(pacquet/overrides): address review feedback
- `parse_overrides` doc no longer claims insertion-order behavior; it
accurately states that `HashMap` iteration is unordered and points
ordered-output callers at `parse_overrides_iter`.
- `WorkspaceSettings::apply_to` now collapses `overrides: {}` from a
later layer (env overlay, repeat `apply_to`) to `None` on `Config`,
so an explicit empty map clears an earlier non-empty assignment
instead of silently being skipped. Adds a regression test for the
env-overlay-clears-yaml shape.
- `sort_by_specificity` widens its comparator to a 3-way result so
Rust's `sort_by` total-order precondition holds. The strict
Less/Greater arms keep the sort outcome identical to upstream's
first-match choice; the `Equal` arm covers mutually-intersecting
ranges.
- `resolve_local_override_spec` routes the absolute-path and
diff-paths-fallback branches through `normalize_path` too, so
Windows `\` separators get rewritten to `/` for every `link:` /
`file:` shape (not just the diff-paths success branch).
|
||
|
|
667e587392 |
feat(pacquet): attach patch hashes to resolved pkg ids (#11791)
* feat(pacquet): attach patch hashes to resolved pkg ids Thread `patchedDependencies` into the tree walker so each matched package's `pkgIdWithPatchHash` gains the `(patch_hash=<hash>)` suffix upstream's `resolveDependencies.ts` produces. The peer resolver concatenates the peer suffix onto the patched id, so the install layer's depPath-keyed lookups land on the patched virtual-store slot without further changes. Surfaces `ResolvedTree::applied_patches` for the post-walk `ERR_PNPM_UNUSED_PATCH` check, and propagates `ERR_PNPM_PATCH_KEY_CONFLICT` from `get_patch_info` through the resolver error surface. * docs(pacquet): drop private-item intra-doc links The two `pub` items pointed at `resolve_node`, which is private — fine under `--document-private-items` but rejected when CI runs `cargo doc` with `-D rustdoc::private-intra-doc-links`. Rephrase to plain prose; the call site is obvious from the surrounding context. * style(pacquet/resolving-deps-resolver): drop trailing comma in assert! Dylint's `perfectionist::macro_trailing_comma` rejects a trailing comma on a single-line macro invocation. |
||
|
|
71dfccce9a |
feat(pacquet): port workspace: protocol resolution and publish-time rewrite (#11789)
* feat(pacquet): port workspace: protocol resolution and publish-time rewrite
Ports the `workspace:` family of bare specifiers end-to-end:
- `pacquet-workspace-spec` (new) ports `workspace/spec-parser`'s
`WorkspaceSpec` parser + `toString`.
- `pacquet-workspace-range-resolver` (new) ports
`workspace/range-resolver`'s `resolveWorkspaceRange` — `*`/`^`/`~`/`""`
pick the highest version with prereleases included; other inputs
follow standard semver range rules.
- `pacquet-resolving-npm-resolver` grows two helpers from the upstream
npm-resolver: `workspace_pref_to_npm` (port of `workspacePrefToNpm.ts`)
and `try_resolve_from_workspace` (port of `tryResolveFromWorkspace` +
`tryResolveFromWorkspacePackages` + `pickMatchingLocalVersionOrNull` +
`resolveFromLocalPackage`). `NpmResolver::resolve_impl` now intercepts
`workspace:` specs before the npm pick, deferring `workspace:./` /
`workspace:../` to the local resolver. Emits `link:` / `file:`
(injected) lockfile resolutions, with the matching
`WORKSPACE_PKG_NOT_FOUND` / `NO_MATCHING_VERSION_INSIDE_WORKSPACE`
/ `CANNOT_RESOLVE_WORKSPACE_PROTOCOL` error codes preserved.
- `pacquet-exportable-manifest` (new) ports the publish-time
`replaceWorkspaceProtocol` and `replaceWorkspaceProtocolPeerDependency`
helpers from `releasing/exportable-manifest`. The full
`createExportableManifest` (catalog rewrite, jsr rewrite, pre-pack
hooks, publishConfig overrides) lands as pacquet ports the surrounding
commands.
- `Install::run` builds a workspace-packages map via
`find_workspace_projects` when a `pnpm-workspace.yaml` is present and
threads it through `ResolveOptions::workspace_packages` so the
resolver chain can satisfy `workspace:` specs from local projects in
the no-lockfile install path.
Test ports:
- `workspace/spec-parser/test/workspace-spec.test.ts`
- `workspace/range-resolver/test/index.test.ts`
- `resolving/npm-resolver/test/workspacePrefToNpm.test.ts`
- `releasing/exportable-manifest/test/index.test.ts` (workspace cases)
- plus new unit tests for `try_resolve_from_workspace` covering
`WORKSPACE_PKG_NOT_FOUND`, `NO_MATCHING_VERSION_INSIDE_WORKSPACE`,
the inject branch, and the `publishConfig.directory` /
`linkDirectory` handling.
Frozen-lockfile installs already record `link:` entries directly; the
new resolution path matters for the no-lockfile install path.
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet): dylint + doc-link nits in workspace-protocol port
- Rename single-letter params (perfectionist::single-letter-*).
- Add trailing commas in multi-line macro invocations.
- Avoid the ambiguous `crate::parse_bare_specifier` doc link and the
cross-crate `pacquet_workspace_spec::WorkspaceSpec` /
`pacquet_workspace_range_resolver::resolve_workspace_range` doc
links (the crates don't have a Cargo dependency on each other, so
rustdoc can't resolve them).
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet): address coderabbit review comments
- replace_workspace_protocol_peer_dependency: use replacen("workspace:", "", 1)
so compound peer specs match upstream JS String.replace's first-only
semantics; locked with a new test
peer_workspace_strip_only_removes_first_occurrence.
- npm-resolver module docs: drop the stale "workspace: returns
Ok(None)" bullet and explain that non-path workspace specs now route
through try_resolve_from_workspace while path-relative forms still
fall through to the local resolver.
The third coderabbit nit (read_workspace_manifest error swallowing in
install.rs) was already addressed by the rebase: the workspace manifest
is now read once at the top of Install::run with proper error
propagation, and build_workspace_packages_map takes the pre-loaded
Option<&WorkspaceManifest> instead of re-reading the file.
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet/resolving-npm-resolver): surface WorkspacePkgNotFound.hint
Upstream pnpm's WORKSPACE_PKG_NOT_FOUND error carries a 'hint' field
that PnpmError prints as guidance after the message ('Packages found in
the workspace: ...'). The Rust port was populating the field but the
miette diagnostic didn't reference it, so the help text never reached
the user. Add help("{hint}") to the diagnostic attribute so miette
renders it under the message — matching pnpm's output verbatim.
---
Written by an agent (Claude Code, claude-opus-4-7).
|
||
|
|
b2a95fa1f7 |
feat(pacquet): port catalogs (types, protocol-parser, resolver, config) (#11787)
* feat(pacquet): port catalogs (types, protocol-parser, resolver, config)
Adds four new crates mirroring upstream's `catalogs/*` packages:
- pacquet-catalogs-types: `Catalog`/`Catalogs` map aliases plus
`DEFAULT_CATALOG_NAME`.
- pacquet-catalogs-protocol-parser: `parse_catalog_protocol`; folds
`catalog:` shorthand into `"default"` to match upstream.
- pacquet-catalogs-resolver: `resolve_from_catalog` with the four
upstream `PnpmError` codes (`CATALOG_ENTRY_NOT_FOUND_FOR_SPEC`,
`CATALOG_ENTRY_INVALID_RECURSIVE_DEFINITION`,
`CATALOG_ENTRY_INVALID_WORKSPACE_SPEC`,
`CATALOG_ENTRY_INVALID_SPEC`). Rust callers `match` on the result
enum directly instead of porting the TS-ergonomic
`matchCatalogResolveResult` visitor.
- pacquet-catalogs-config: `get_catalogs_from_workspace_manifest` plus
the `INVALID_CATALOGS_CONFIGURATION` mutual-exclusion check.
`pacquet-workspace`'s `WorkspaceManifest` now actually deserializes
the `catalog` and `catalogs` fields (previously dropped). The
resolver is not yet wired into the install path — `deps-installer`
hasn't been ported — but the crates are ready for that next step.
Tests are 1:1 ports of the upstream Jest suites.
* fix(pacquet): satisfy rustdoc and perfectionist lints in catalogs port
- catalogs-resolver: drop the intra-doc-link form on the
`pacquet_resolving_resolver_base::WantedDependency` reference; the
crate isn't a dependency, so rustdoc failed to resolve it under
`-D rustdoc::broken-intra-doc-links`.
- catalogs-resolver, catalogs-config: reorder the `CatalogResolutionError`
/ `InvalidCatalogsConfigurationError` derive lists to
`Debug, Display, Error, Diagnostic, Clone, PartialEq, Eq` so they
match the `prefix_then_alphabetical` rule that the CI-only
Perfectionist dylint enforces. `just ready` doesn't surface this lint
locally.
* feat(pacquet): resolve catalog: specifiers during install
Wires the catalogs port into the install path:
- `install.rs`: read `pnpm-workspace.yaml` after `find_workspace_dir`
and normalize via `get_catalogs_from_workspace_manifest` into a
`Catalogs` map. Adds `InstallError::{ReadWorkspaceManifest,
InvalidCatalogsConfiguration}` so the upstream
`ERR_PNPM_INVALID_CATALOGS_CONFIGURATION` propagates verbatim.
- `install_without_lockfile.rs`: threads the `Catalogs` map into
`ResolveDependencyTreeOptions`. (Frozen-lockfile catalog handling
needs a lockfile-snapshot pass and is a separate slice.)
- `resolve_dependency_tree`: replaces direct (importer-level)
`catalog:` bare specifiers with the catalog's recorded version
before the resolver chain dispatches. Catalog resolution does NOT
run on transitive deps, matching upstream's importer-only scope.
Misconfigured entries surface as `CatalogMisconfiguration` with
the upstream `ERR_PNPM_CATALOG_ENTRY_*` code instead of leaking
through to `SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER`.
Tests:
- deps-resolver: two new unit tests prove direct-dep rewriting and
the misconfiguration code path.
- cli (e2e): `install_resolves_catalog_protocol` runs the binary
against a workspace with a `catalog:` entry and checks the
virtual-store layout; `install_surfaces_catalog_misconfiguration`
asserts the upstream message is surfaced when the catalog has no
matching alias.
* style(pacquet): apply rustfmt to catalogs test after rebase
* fix(pacquet): rename single-letter closure param in catalogs e2e test
Perfectionist's `single_letter_closure_param` lint (CI-only via
Dylint) flagged `|c|` in the box-drawing-strip filter.
|
||
|
|
df990fdb51 |
feat(pacquet): port node/deno/bun runtime resolvers (#11783)
* feat(pacquet): port node/deno/bun runtime resolvers and wire them into the install chain
Ports the three `@pnpm/engine.runtime.*-resolver` packages and the shared
`@pnpm/crypto.shasums-file` helper into pacquet, and slots them into the
default-resolver chain so `node@runtime:<spec>`, `deno@runtime:<spec>`,
and `bun@runtime:<spec>` resolve through pacquet as they do in pnpm.
New crates under `pacquet/crates/`:
- `crypto-shasums-file` — downloads and decodes `SHASUMS256.txt`,
shared by node and bun. Mirrors `FAILED_DOWNLOAD_SHASUM_FILE`,
`NODE_INTEGRITY_HASH_NOT_FOUND`, `NODE_MALFORMED_INTEGRITY_HASH`.
- `engine-runtime-node-resolver` — `parse_node_specifier`,
`get_node_mirror`, `get_node_artifact_address`, `normalize_arch`,
`resolve_node_version[s]`, and the `Resolver`-impl entry point.
Handles the unofficial-musl mirror fan-out, the `lts` / LTS-codename
/ channel / range selectors, and the `darwin/arm64 <16 → x64`,
`win32/ia32 → x86`, `arm → armv7l` arch quirks. Error codes
`NO_OFFLINE_NODEJS_RESOLUTION`, `NODEJS_VERSION_NOT_FOUND`,
`INVALID_NODE_RELEASE_CHANNEL` match upstream.
- `engine-runtime-deno-resolver` — version selection delegates to the
npm resolver; assets come from the GitHub Releases API + per-asset
SHA256 sidecars. Windows x64 covers arm64 under emulation.
Errors: `DENO_RESOLUTION_FAILURE`, `DENO_MISSING_ASSETS`,
`DENO_GITHUB_FAILURE`, `DENO_PARSE_HASH`.
- `engine-runtime-bun-resolver` — version selection delegates to npm;
assets come from the GitHub-release `SHASUMS256.txt`. `windows` /
`aarch64` are normalised to `win32` / `arm64`. Error:
`BUN_RESOLUTION_FAILURE`.
Wiring (`install_without_lockfile.rs`): chain order is now
`npm → git → node → deno → bun`, matching upstream's
`resolving/default-resolver/src/index.ts` at
|
||
|
|
d08e9abfb9 |
fix(pacquet/lockfile): accept link: refs in snapshot dependencies (#11788)
* fix(lockfile): accept link: refs in snapshot dependencies An injected workspace package's snapshot can carry a `link:<path>` dependency value when the dep is a workspace sibling. Pnpm accepts the shape — `refToRelative` short-circuits to `null` for `link:` — but pacquet's `SnapshotDepRef` only handled the plain/alias shapes and rejected `link:` at parse time. Add a `SnapshotDepRef::Link(String)` variant mirroring `ImporterDepVersion::Link`, return `None` from `resolve` / `ver_peer` for it, and skip it at every consumer (matching upstream's `if (childDepPath)` guards in `getChildren` / `lockfileDepsToGraphChildren`). Fixes https://github.com/pnpm/pnpm/issues/11775. * test(lockfile): trim redundant doc comments on link snapshot dep tests Drop test doc-comments that restated the assertion. The test name + asserts already convey the intent; the upstream-parity reason lives at the `SnapshotDepRef::resolve` / `Link` variant docs. |
||
|
|
ee8fd0d4cf |
feat(pacquet): port auto-install-peers algorithm (#11784)
* feat(pacquet): port auto-install-peers algorithm
Replaces the placeholder peer-folding behavior with a faithful port of
pnpm's `hoistPeers` algorithm. Missing required peers are hoisted to
the importer's direct deps (shared across consumers, not nested), with
a multi-pass loop that re-resolves until no required peer remains and
the optional-peer pass picks already-available versions from the
preferred-versions map.
- New `pacquet-lockfile-preferred-versions` crate seeds the version-
picker tie-break table from manifest + lockfile snapshots.
- New `hoist_peers` module ports `hoistPeers` and
`getHoistableOptionalPeers` with the full upstream test suite.
- New `resolve_importer` orchestrator drives the multi-pass hoist
loop and threads `parent_pkg_aliases` / `all_preferred_versions`
across iterations.
- `resolve_dependency_tree` exposes `TreeCtx` / `extend_tree` /
`snapshot` so the orchestrator can extend the tree incrementally
without re-walking already-resolved subtrees.
- `auto-install-peers-from-highest-match` config setting added,
mirroring upstream's flag for range-merge behavior.
Ported from pnpm's installing/deps-resolver `resolveRootDependencies`
and hoistPeers.ts at commit
|
||
|
|
e2791ab6fe |
feat(pacquet): port named registries to the install chain (#11785)
* feat(pacquet): port named registries to the install chain Adds the user-facing `<alias>:` resolver surface so a manifest entry like `"@acme/private": "gh:^1.0.0"` resolves against GitHub Packages (or any other registry the user configures under `namedRegistries:` in `pnpm-workspace.yaml`). Mirrors upstream pnpm/pnpm@b61e268d57: - `parse_named_registry_specifier_to_registry_package_spec` parses `<alias>:[@<owner>/]<name>[@<version>]` and `<alias>:<version>` bodies. Rejects scope-without-name with `ERR_PNPM_INVALID_NAMED_REGISTRY_PACKAGE_NAME`. - `merge_named_registries` folds the user map onto pacquet's built-in aliases (`gh:` -> GitHub Packages) and validates URLs at resolver construction (`ERR_PNPM_INVALID_NAMED_REGISTRY_URL`). - `NamedRegistryResolver` is a third `Resolver` alongside the npm / jsr paths, emitting `resolved_via: "named-registry"`. Auth headers flow through the existing per-URL `.npmrc` lookup. - `Config::named_registries` reads `namedRegistries:` from `pnpm-workspace.yaml`, with `${VAR}` substitution on the values (matches upstream's `replaceEnvInStringValues`). - `install_without_lockfile` constructs the merged map once and threads it through both the resolver chain (after npm/git, so configured aliases cannot hijack built-in schemes) and `build_resolution_verifiers` (so tarball-URL prefix routing honours user aliases). * fix(pacquet): rustdoc broken links + spell-check typo - `resolving-local-resolver/src/chain.rs`: drop the redundant explicit `(crate::...)` target on `[resolve_latest_from_local]` (clippy's `redundant_explicit_links` lint flagged it under `RUSTDOCFLAGS=-D warnings`) and demote the `[contains_path_sep]` intra-doc reference to backticks since the helper is private. - `resolving-npm-resolver/src/parse_bare_specifier.rs`: rename `unparseable` to `unparsable` in a doc comment so the workspace typos check passes. * test(pacquet): cover the remaining named-registry test cases Ports upstream's [`resolving/default-resolver/test/namedRegistry.ts`](https://github.com/pnpm/pnpm/blob/b61e268d57/resolving/default-resolver/test/namedRegistry.ts) and the two `resolveNamedRegistry.test.ts` cases the previous commit hadn't covered. - New `tests/chain.rs` integration tests assert that the explicit `link:` / `workspace:` / `file:` schemes win over a colliding named-registry alias. These pin the local-scheme / local-path split: with combined `LocalResolver`, named-registry would slot *after* both halves and a `link` alias could hijack `link:./pkg`. - `resolves_via_builtin_gh_alias` covers the `gh:` happy path that was previously only exercised via a user-supplied `work:` alias. - `preserves_scoped_pkg_name_when_alias_differs` verifies that the resolver records the dependency under the registry's name, not the local manifest alias. - `user_config_overrides_builtin_gh_alias` covers the GHES override scenario where a user points `gh` at their enterprise host. 11 resolver tests pass (5 unit + 3 chain integration + 3 happy-path scenarios) plus the 12 parser cases already in place. |
||
|
|
998ea4cd37 |
fix(pacquet/lockfile): accept non-semver version slots (URLs, git refs) (#11786)
* fix(pacquet/lockfile): accept non-semver version slots (URLs, git refs) Pnpm's `parse` in `deps/path/src/index.ts` falls back to the `nonSemverVersion` branch whenever `semver.valid` rejects the version slot, so a `https://codeload.github.com/...` tarball URL or `git+...` URL parses cleanly. Pacquet's `PkgVerPeer` parser was strict and errored on anything that wasn't a semver or a `file:` path, which made the lockfile reader fail with `Failed to parse importer dependency version "https://codeload..."` on the install path. Add `VersionPart::NonSemver(String)` and route the parser's bare-version fallback through it, mirroring upstream byte-for-byte. Closes the parse failure reported in pnpm/pnpm#11776. * fix(pacquet/lockfile): add trailing commas to satisfy dylint perfectionist |
||
|
|
4b25d60f62 |
fix(pacquet/patching): apply patches without mutating the store (#11782)
Reported against pacquet's configDependencies preview engine for `msw@2.12.14`: install fails with `ERR_PNPM_PATCH_FAILED ... error applying hunk #1` even though the patch is perfectly valid. Original report (patch file + error log): [Stanzilla/21648ae644068c3aef3d18693c7c32c9](https://gist.github.com/Stanzilla/21648ae644068c3aef3d18693c7c32c9). **Root cause.** When two snapshots of the same patched package hardlink (or reflink, where the filesystem doesn't break-on-write) from the same store file, pacquet's `apply_patch_to_dir` did a plain `fs::write` — which truncates and writes through the shared inode, **corrupting the on-disk store copy** and leaking patched content into every other snapshot that linked the same file. The next worker then read already-patched content and `diffy::apply` failed on the missing `-` lines. The patched output already lives in the side-effects cache after apply returns, so nothing requires the store copy to carry it. The fix is three layered changes to `apply_patch_to_dir`: - **Atomic temp + rename for Modify** ([apply.rs](pacquet/crates/patching/src/apply.rs)). Stage the patched bytes in a sibling `.{name}.{pid}.{counter}.pacquet-tmp` opened with `create_new(true)` (no symlink-follow, no truncation of an attacker-pre-seeded path), chmod the temp to match the original *before* the rename so the final mode lands atomically, then `fs::rename` over the target. `rename` is atomic on Unix and replaces in-place on Windows, so an IO failure mid-write leaves either the original or the rewritten file — never an empty dirent. As a side effect of creating a new dirent → inode mapping, it also breaks any hardlink the path previously shared with the store. Matches the [`pacquet_lockfile::save_lockfile::write_atomic`](pacquet/crates/lockfile/src/save_lockfile.rs) pattern. - **Idempotency via reverse-dry-run** (Modify/Create/Delete). When forward apply fails, try reverse-apply against the on-disk content; if it succeeds the file is already in the post-patch state and we no-op. Mirrors upstream [`@pnpm/patch-package`'s `applyPatch`](https://github.com/ds300/patch-package/blob/master/src/applyPatches.ts) which retries `executeEffects(reversePatch, { dryRun: true })` on failure. Defense in depth for re-runs that find the directory pre-patched (side-effects-cache hit that fell through, manual edits, partial install recovery) — the hardlink-break above prevents fresh installs from ever producing that state. - **Idempotency for Create and Delete**. Create skips when the existing file already contains the post-patch content; Delete treats `NotFound` on the target as already-deleted. Equivalent fix for the TS pnpm CLI side is in [`pnpm/patch-package@c97a62e`](https://github.com/pnpm/patch-package/commit/c97a62e) (`@pnpm/patch-package` is the apply implementation pnpm imports); pnpm will pick it up once that package is released and the dep gets bumped. |
||
|
|
a8a8cbce6d |
feat(pacquet): port resolving.local-resolver (file:/link:/workspace:) (#11778)
* feat(pacquet/resolving-local-resolver): port file:/link:/workspace: resolver
Ports pnpm's @pnpm/resolving.local-resolver:
- parse_bare_specifier mirrors parseLocalScheme/parseLocalPath/fromLocal
(link:/workspace:/file: prefixes, bare path shapes, tarball-filename
detection, tilde/drive-letter handling, preserveAbsolutePaths,
injected directories get file: not link:).
- local_resolver provides resolve_from_local_scheme / _path /
resolve_latest_from_local matching upstream's three exports;
ssri-based tarball integrity for file: tarballs;
safe_read_package_json_from_dir for directories with the upstream
fallback (warn + name=basename / version='0.0.0' for missing link:
targets, LINKED_PKG_DIR_NOT_FOUND for missing file: targets,
NOT_PACKAGE_DIRECTORY for ENOTDIR + Windows stat-check) and
PATH_IS_UNSUPPORTED_PROTOCOL for path:.
- chain.rs wraps the free functions behind the Resolver trait so the
default-resolver dispatcher can compose this in alongside the npm
resolver. ResolveResult.name_ver is None for local resolutions —
the canonical name lives in the fetched manifest, not the
resolver-time signal.
17 ported tests mirror resolving/local-resolver/test/index.ts plus
3 chain-dispatch tests verifying the trait wiring. The missing-link:
target warn is emitted via tracing::warn! because pacquet's reporter
doesn't yet have a generic pnpm:logger channel.
Install-side wiring is left for a follow-up alongside the Stage-1
directory-fetcher integration: surfacing Directory resolutions to
install_without_lockfile today would only swap the
SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER error for an UnsupportedResolution
one in install_package_from_registry.
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet): satisfy doc + dylint CI
Doc:
- pacquet_directory_fetcher intra-doc link was unresolved
(resolving-local-resolver has no such dep — it's a sibling).
- LocalSpecError doc linked to crate-private parse_local_scheme /
parse_local_path.
Dylint (perfectionist):
- PkgResolutionId / WantedLocalDependency / ParseOptions /
PathProtocolNotSupportedError derive lists reordered to
prefix_then_alphabetical.
- Single-letter closure params (|p|, |s|, |v|) renamed.
- Impure expression passed to tracing::warn! bound to a let first.
- Multi-line format!/assert_eq! macro invocations gained trailing
commas; the single-line assert! shed its stray trailing comma.
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet/resolving-local-resolver): surface NOT_PACKAGE_DIRECTORY on Windows
`safe_read_package_json_from_dir` opens `<spec>/package.json` and lets
the OS error surface. On Unix that's `ENOTDIR` for a file path; on
Windows it's `NotFound`, so the resolver fell through to the
fallback-manifest branch instead of returning `NOT_PACKAGE_DIRECTORY`.
Upstream pnpm has the same gap on Windows and patches around it inside
`readProjectManifestOnly` (workspace/project-manifest-reader/src/index.ts#L100-L114
at
|
||
|
|
606ff8f648 |
feat(pacquet): port resolving/tarball-resolver (#11773)
* feat(pacquet): port resolving/tarball-resolver Adds pacquet-resolving-tarball-resolver, the Rust port of resolving/tarball-resolver/src/index.ts. The new crate claims any WantedDependency whose bare specifier starts with `http://` or `https://`, normalizes the URL through `reqwest::Url::parse`, runs a HEAD pre-flight that follows redirects, and stores the post-redirect URL in the resolution when the response carries `cache-control: immutable`. Mutable responses keep the normalized request URL. Integrity stays None at resolve time, matching upstream (integrity is stamped later in package-requester). To make the seam fit, ResolveResult.id is now an opaque `PkgResolutionId(String)` newtype in resolver-base mirroring upstream's branded string at core/types/misc.ts:59. PkgNameVer was a poor fit because tarball ids are URLs and git ids are `repo#commit` — not name@version. NpmResolver round-trips the existing PkgNameVer through PkgResolutionId::from(string); the npm-only install paths in package-manager parse the id back to PkgNameVer at their boundary (safe because the npm resolver stamps that shape). The deps-resolver alias fallback drops its `.id.name` access since the id is opaque now. Test coverage in the new crate (7 tests, mockito): http(s) claim vs decline, mutable vs immutable response, immutable-after-redirect follow, resolve_latest for http(s) vs non-http(s). * fix(pacquet/resolving-tarball-resolver): rename single-letter closure params Dylint's perfectionist::single-letter-closure-param rejects |v|; rename to |header| in the cache-control header check. * feat(pacquet/package-manager): wire TarballResolver into the install chain Insert TarballResolver after the GitResolver in install_without_lockfile's DefaultResolver chain so http(s):// bare specifiers actually route through the new resolver. Order mirrors upstream's chain (npm → git → tarball → local/...). * fix(pacquet/resolving-tarball-resolver): forward wanted alias and resync lockfile - Echo `wanted_dependency.alias` on the `ResolveResult` so a spec like `"foo": "https://.../bar.tgz"` preserves `foo` as the install name downstream. Matches the npm and git resolvers' convention even though upstream TS doesn't surface alias on `ResolveResult` (downstream consumers fall back to `wantedDependency.alias` over there). - Drop a stray blank line in resolve.rs that cargo fmt rejected. - Record the new `package-manager -> resolving-tarball-resolver` edge in Cargo.lock so `cargo build --locked` succeeds on CI. |
||
|
|
35d5440ce1 |
feat(pacquet): port resolving/git-resolver and wire it into the install chain (#11779)
* feat(pacquet): port resolving/git-resolver and wire it into the install chain Adds `pacquet-resolving-git-resolver`, the Rust port of pnpm's `@pnpm/resolving.git-resolver`. Recognises GitHub / GitLab / Bitbucket shortcut forms and full `git+ssh:` / `git+https:` / `ssh:` / plain `https://…/repo.git` URLs, runs `git ls-remote` to pin the commit (partial commit search, annotated-tag dereference, semver-range matching), and emits either a git-hosted tarball resolution or a `Git{repo,commit}` resolution. Production runners shell out to the system `git` binary via `tokio::task::spawn_blocking` and use the install-wide `ThrottledClient` for the HEAD probe. Widens the resolver-base contract so URL-shaped IDs fit: adds a `PkgResolutionId` newtype (rule-3 branded string, infallible `From<String>`/`From<&str>`/`From<&PkgNameVer>`), changes `ResolveResult.id` to that type, and adds `name_ver: Option<PkgNameVer>` so callers that need the structured `name@version` form keep working. npm-resolver fills both fields; git-resolver leaves `name_ver` `None` (the install path that consumes git resolutions hasn't landed yet, so those call sites panic with a TODO message until then). `DefaultResolver` now implements `Resolver` too (returns `Ok(None)` when no resolver in the chain claims), letting `resolve_dependency_tree` accept the chain directly. The install-side wiring in `install_without_lockfile.rs` constructs `DefaultResolver::new(vec![Box::new(npm_resolver), Box::new(git_resolver)])` with `RealGitProbe` / `RealGitRunner`, mirroring upstream's `createResolver` chain order. Test coverage: 51 unit tests in the new crate, including the full SCP-style URL repair matrix ported from `parsePref.test.ts` and the GitLab `/-/archive/` tarball regression for pnpm #11533. Full workspace `cargo nextest run` is green at 1635 tests. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet): satisfy dylint perfectionist + rustdoc on git-resolver port * Reorder `#[derive(...)]` on `PkgResolutionId` to match the `prefix_then_alphabetical` rule the dylint Perfectionist lint enforces (`From` last after `Serialize`/`Deserialize`). * Add `()` to function intra-doc links that collide with same-named modules (`create_git_hosted_pkg_id`, `parse_bare_specifier`) so rustdoc's `broken-intra-doc-links` lint stops treating them as ambiguous. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet): satisfy Perfectionist dylint lints on git-resolver port CI's `just ready` doesn't surface Perfectionist (it runs only as a dedicated dylint job on a nightly toolchain). Fixes: * Rename single-letter generics `P`/`R` → `Probe`/`Runner` on `GitResolver`, `PartialSpec::finalize`, `from_hosted_git`, and `resolve_ref`. * Rename single-letter closure / function / let-binding params (`s`/`h`/`c`/`p`/`i`/`g`/...) to descriptive names. * Replace Unicode ellipsis (`…`, U+2026) with ASCII `...` in comments. * Add trailing commas to multi-line `assert_eq!` / `assert!` invocations, and remove the stray trailing comma on a single-line one. Also fix follow-on JSR-resolver test cases that still read `result.id.{name,suffix}`: switch them to `result.name_ver.as_ref()...` to match the post-widening `ResolveResult` shape. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet): address PR review on git-resolver port * Replace the two `.expect()` calls on `ResolveResult.name_ver` in the install path with `.ok_or_else()` that surfaces a typed error: `InstallPackageFromRegistryError::UnsupportedResolution` and a new `InstallWithoutLockfileError::UnsupportedInstallResolution`. Now that the git resolver is in the chain, a git/tarball/local resolution reaching the without-lockfile install path returns an error end-to-end instead of panicking. Add a regression test pinning the contract. * Make `percent_decode` (in `hosted_git.rs`) and `percent_decode_str` (in `parse_bare_specifier.rs`) UTF-8 aware: collect decoded bytes into a `Vec<u8>` and reassemble via `String::from_utf8`, falling back to the original input on malformed UTF-8 (matches Node's `decodeURIComponent` throwing a `URIError` that upstream's `try/catch` swallows). The byte→`char` cast was corrupting any multi-byte sequence (e.g., `%E2%80%A6` → ellipsis); regression test added. --- Written by an agent (Claude Code, claude-opus-4-7). * chore(pacquet): drop unused UnsupportedInstallResolution after rebase Main's `feat(pacquet): peer-dependency resolution stage` reworked `install_without_lockfile.rs` to derive the virtual-store name from the resolved depPath via `pacquet_deps_path::dep_path_to_filename` instead of reading `result.name_ver`. That removed the `.expect()` / `.ok_or_else()` site this error variant was added for; with no remaining callers, drop the dead variant. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
a1f91e1770 |
feat(pacquet): peer-dependency resolution stage (#11774)
* feat(pacquet): peer-dependency resolution stage Ports pnpm's `resolvePeers` algorithm to pacquet's `resolving-deps-resolver` crate and wires it into `install_without_lockfile`. The depPath-keyed `DependenciesGraph` produced by the new stage replaces the flat `(name, version)` keying the install pass used to drive virtual-store slot names from. New `pacquet-deps-path` crate ports the `@pnpm/deps.path` helpers (`createPeerDepGraphHash`, `depPathToFilename`, balanced-paren suffix scan). `ResolvedTree` now carries both the flat dedup map (`packages`) and the per-occurrence tree (`dependencies_tree`, keyed by a new `NodeId`) so two parents sharing the same package can compute different peer suffixes — the whole point of the stage. Cycles in the tree pass are broken by skipping the cycled edge entirely (matches upstream's `parentIdsContainSequence` gate in `buildTree`), and peer-resolution falls back to `name@version` peer-ids when re-entering an in-progress node. Three upstream optimisations are intentionally not ported in this slice: `peersCache`, the `purePkgs` fast path, and `graph-cycles`-driven async deferment. Each is correctness-preserving — the algorithm produces the same depPaths, just without the short-circuits. `autoInstallPeers` keeps its existing "fold peers into the regular walk" behavior until the full `hoistPeers` algorithm lands. * fix(pacquet/deps-path): satisfy Doc / Dylint / Spell Check CI checks - Dylint perfectionist::single-letter-let-binding: rename `i` → `cursor` in `suffix_index.rs`. - Dylint perfectionist::macro-trailing-comma: drop trailing comma in the `dep_path_to_filename` `file:` test. - rustdoc broken-intra-doc-links: link `[`DependenciesTree`]` to the newly-exported `pacquet_resolving_deps_resolver::DependenciesTree` type alias and downgrade the `pacquet_lockfile::PkgNameVerPeer` mention in `pacquet-deps-path`'s module doc to plain text (the crate deliberately doesn't depend on `pacquet-lockfile`). - rustdoc fn-vs-mod ambiguity: prefix [`resolve_dependency_tree`], [`resolve_peers`], and [`create_peer_dep_graph_hash`] doc-links with `fn@` so they bind to the function rather than the same-named module. - typos: rename `unparseable` → `unparsable` in a test name. * fix(pacquet/resolving-deps-resolver): satisfy Dylint perfectionist lints - `derive_ordering`: reorder `#[derive(...)]` on `DepPath` and `NodeId` to match the configured `prefix_then_alphabetical` style — `PartialOrd, Ord, Hash` rather than `Hash, PartialOrd, Ord`. See `dylint.toml`'s `[perfectionist::derive_ordering]` prefix list for the canonical order. - `single_letter_function_param`: rename `s` → `value` on `impl From<String> for DepPath`. * fix(pacquet): address PR review feedback Round of correctness fixes flagged by CodeRabbit and qodo-code-review on #11774. - **`dep_path_to_filename_unescaped`**: guard against empty / single-byte input. The previous `trimmed.as_bytes()[1..]` slice panicked when `trimmed.len() < 2`; the function now short-circuits to `trimmed.to_string()` in that case, mirroring upstream's `indexOf` with `fromIndex = 1` returning -1 on out-of-range scan. - **`extract_children` / `extract_peer_dependencies` mismatch**: the child walker added every `peerDependencies` entry when `auto_install_peers` was on, but the peer-resolution stage's `peerDependenciesWithoutOwn` filter skips peer names also in `dependencies` / `optionalDependencies`. `BTreeMap` collection of the result by alias could silently drop the optional edge for a name that appeared in both. Fix: collect `optionalDependencies` into children too, and dedupe peer entries against own deps before appending. - **Dropped peer edges in `graph_children`**: when a peer pointed at a later sibling direct dep (e.g., manifest order `{ react-dom: …, react: … }`), the peer's `DepPath` wasn't in `node_dep_paths` yet at the time the parent's `graph_children` was built, so the symlink edge was silently dropped. Install would then walk react-dom's slot without finding react. Add `pending_peer_edges` + a `patch_pending_peer_edges` post-pass that runs after every direct dep is walked, with regression test. - **Aliased child peer misfilter**: the "is this peer one of my own children?" check compared the peer alias against `tree_node.children` keys, but `children` is keyed by install alias while peers can match by real name via the dual-keyed `ParentRefs`. Switch both filter sites to compare by `NodeId` so an npm-aliased child satisfying a peer is correctly classified as internal. - **`DepPath` newtype consolidation**: `PeerId::DepPath(String)` collapsed the depPath brand. Move the `DepPath` newtype from `resolving-deps-resolver::dependencies_graph` into the lower-level `pacquet-deps-path` crate so the `PeerId` enum carries the branded type instead of `String`. `resolving-deps-resolver` re-exports `DepPath` from `pacquet_deps_path` to keep existing imports working. - **Prerelease-tolerant semver match**: `node-semver`'s `Range::satisfies` rejects prereleases against non-prerelease comparators (`18.0.0-rc.1` vs `^18.0.0` returns `false`). Add a fallback in `satisfies_with_prereleases`: if the straight check fails and the candidate is a prerelease, retry against the stripped `MAJOR.MINOR.PATCH` base. Approximates Yarn's `satisfiesWithPrereleases` for the cases pnpm cares about without importing the full per-comparator algorithm. The `tracing::warn!` peer-issue emission flagged by qodo and the deeper prerelease semantics gap (per-comparator) remain documented follow-ups; the slice's `resolve_peers.rs` module doc lists what's intentionally not ported in this PR. * fix(pacquet): apply cargo fmt to peer-resolution slice Three sites where the previous edits left non-canonical wrapping: - `create_peer_dep_graph_hash.rs` test calls now fit one line. - `lib.rs` re-export ordering (alphabetical). - `tests.rs` whitespace. CI Format check is `cargo fmt --all -- --check`; the local pre-push hook didn't catch these because the edits landed via `Edit` without a follow-up `cargo fmt`. * fix(pacquet/resolving-deps-resolver): drop trailing comma rustfmt re-added `cargo fmt` collapsed the regression test's `assert_eq!` onto one line and kept the trailing comma, which tripped the same `perfectionist::macro_trailing_comma` rule that #11774's earlier commit fixed elsewhere. The lint forbids a trailing comma on single-line macro calls; rustfmt leaves it alone. Drop the comma to satisfy both. |
||
|
|
f807f6d402 |
feat(pacquet): port JSR specifier parser and wire resolve_jsr (#11772)
* feat(pacquet/resolving-npm-resolver): port JSR specifier parser and wire resolve_jsr Adds a new `pacquet-resolving-jsr-specifier-parser` crate that ports `@pnpm/resolving.jsr-specifier-parser` (`parseJsrSpecifier` + `JsrSpec`), mirroring upstream's `ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE` / `ERR_PNPM_INVALID_JSR_PACKAGE_NAME` / `ERR_PNPM_INVALID_JSR_SPECIFIER` error codes 1:1. Wires the parser into the npm resolver: - `parse_jsr_specifier_to_registry_package_spec` adapter alongside `parse_bare_specifier`, matching upstream's same-file shape in `resolving/npm-resolver/src/parseBareSpecifier.ts`. - `NpmResolver::resolve_impl` detects `jsr:` early and routes to a new `resolve_jsr_impl` that picks against `registries['@jsr']` (with the `https://npm.jsr.io/` `DEFAULT_REGISTRIES` fallback) and stamps `resolved_via = "jsr-registry"` plus `alias = spec.jsr_pkg_name`. - Extracts a shared `pick_from_registry` helper so npm and JSR paths share the picker invocation; `build_resolve_result` now takes a `resolved_via` parameter. Ports the 6 upstream parser test cases and adds adapter + resolver integration tests covering the JSR happy path, default-tag fallback, and parser-error propagation. Ports https://github.com/pnpm/pnpm/blob/05dd45ea82/resolving/jsr-specifier-parser/src/index.ts and https://github.com/pnpm/pnpm/blob/1627943d2a/resolving/npm-resolver/src/index.ts. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet/resolving-jsr-specifier-parser): treat empty alias as missing in version-only specs Upstream's `parseJsrSpecifier` guards the version-only branch with the truthy check `if (!alias)`, so an empty string falls into the `INVALID_JSR_SPECIFIER` arm. Pacquet's `let Some(alias) = alias else` form only triggers on `None`, so `Some("")` was instead carried into `jsr_to_npm_package_name` and surfaced `MISSING_JSR_PACKAGE_SCOPE`. Filter empties out before the destructure so both sides agree on the error code. Also tighten the resolver-level integration test for the unscoped-name case to assert the upstream-defined error message instead of a generic "JSR" substring. Reported by CodeRabbit on https://github.com/pnpm/pnpm/pull/11772. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
11a43b15da | chore(release): 11.2.1 (#11777) v11.2.1 | ||
|
|
2061c55b2a |
fix(env-installer): mark optional config subdep snapshots with optional: true (#11770)
Match how optional packages are recorded elsewhere in pnpm-lock.yaml so non-host platform variants pulled in via a config dep's optionalDependencies aren't treated as required. |