mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-02 13:13:17 -04:00
a662de44dd8e76f4e4e67bb5b2cd193f84ce4e73
11624 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a662de44dd |
fix: pnpm runtime set defaults to devEngines (#11951)
* fix: pnpm runtime set defaults to devEngines Previously `pnpm runtime set <name> <version>` wrote to `engines.runtime` because it ran `pnpm add` with the default `--save-prod`. Default to `--save-dev` so the runtime lands in `devEngines.runtime`; pass `--save-prod` (or `-P`) to opt back into `engines.runtime`. Closes #11948 * fix: honor --save-dev precedence in pnpm runtime set When both `--save-dev` and `--save-prod` are passed, prefer `--save-dev` to match `getSaveType`'s precedence elsewhere in pnpm. Also makes the explicit `--save-dev` flag actually consulted instead of relying solely on the default branch. * ci: trigger |
||
|
|
26a7d633bf |
fix(patching/apply-patch): reject patch paths that escape the patched directory (#11952)
* fix(patching/apply-patch): reject patch paths that escape the patched directory A malicious .patch file with `diff --git a/../../X` headers could otherwise write, delete, or rename files outside the patched package as the user running `pnpm install`. * refactor(patching/apply-patch): narrow caught errors via util.types.isNativeError Drops the `any`-typed catch + eslint-disable in favor of the cross-realm-safe narrowing pattern documented in CLAUDE.md. * refactor(patching/apply-patch): replace error helper with PatchPathEscapesError class * chore(patching/apply-patch): reword comment to satisfy cspell |
||
|
|
35d235542e |
fix: validate devEngines runtime onFail (#11822)
Fixes #11818 ## Summary `devEngines.runtime` / `engines.runtime` entries with `onFail: error` or `warn` silently did nothing — only `onFail: download` had any effect. This PR wires up validation for all three supported runtimes (node, deno, bun). - Add `getSystemDenoVersion` / `getSystemBunVersion` and a generic `getSystemRuntimeVersion(name)` dispatcher in the runtime-version helper package. - Walk each runtime entry in the manifest during pnpm startup, compare to the live system runtime, and throw `ERR_PNPM_BAD_RUNTIME_VERSION` (or warn) on a mismatch. Invalid ranges (e.g. `"invalid range"`) are reported instead of crashing `semver.minVersion`. Missing runtimes ("no Node.js on the system") get the same error path. - The shell-out for deno/bun only runs when the manifest configures them AND `onFail` is `error`/`warn`. `download`/`ignore` short-circuit, and projects with no runtime pin pay nothing. Memoized per runtime. - `pnpm --version`, `pnpm --help`, and `pnpm <cmd> --global` are exempt from the check. - Rename `@pnpm/engine.runtime.system-node-version` → `@pnpm/engine.runtime.system-version` to match its broader scope; hoist `RuntimeName` / `RUNTIME_NAMES` / `isRuntimeAlias` to `@pnpm/types` so callers don't need to depend on `pkg-manifest.utils` just for the alias check. ## Tests - `pnpm --filter pnpm run compile` - `pnpm --filter pnpm exec jest packageManagerCheck.test` — 42 passing. New coverage: node/deno/bun version mismatch, invalid range, missing range, multi-entry runtime arrays, `engines.runtime` path (not just `devEngines.runtime`), and the `pnpm --version` exemption. - `pnpm --filter @pnpm/engine.runtime.system-version test` — 10 passing, 100% statement coverage; unit tests for each helper and the dispatcher. - Manual end-to-end smoke tests against the rebuilt bundle for deno and bun version mismatch. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added runtime version validation for Node.js, Deno, and Bun. The system now enforces `devEngines.runtime` and `engines.runtime` declarations with configurable failure behavior (`error`, `warn`, or `ignore`). * Enhanced error messages for runtime version mismatches with helpful suggestions for overrides. * **Improvements** * Improved system runtime detection and version checking across multiple runtime environments. --------- Co-authored-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
a1f6f32996 |
fix(pacquet/package-manager): build workspace state from project list, not lockfile (#11946)
`build_projects_map` derived workspace projects from `lockfile.importers.keys()`. On the fresh-install path the wanted lockfile is `None` (no `pnpm-lock.yaml` on disk yet), so the function fell into its no-lockfile arm and recorded **only the root importer** — even for an 87-project workspace like babylon. That broke the [`optimistic_repeat_install`](https://github.com/pnpm/pnpm/blob/6b3ba4d337/pacquet/crates/package-manager/src/optimistic_repeat_install.rs) fast path on the *next* install: `project_structure_matches` compared the recorded project list (`len = 1`) against the workspace projects the resolver discovered (`len = 87`), returned `false`, and the install fell through to the full resolve + verify path even though the on-disk state was already a no-op. ## Fix Upstream pnpm's [`createWorkspaceState`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/workspace/state/src/createWorkspaceState.ts#L14-L27) takes `allProjects: ProjectsList` as input and emits one entry per project — the lockfile isn't involved. Pacquet already builds the matching `project_manifests: &[(PathBuf, &PackageManifest)]` at the top of `Install::run` (for the optimistic-repeat fast path); thread it through to `build_workspace_state` instead of re-deriving from the lockfile. The new `build_projects_map` is half the size of the old one and can't fall into a "lockfile missing" arm — the project list always comes from the same scan the rest of the install uses. ## Impact Re-bench against the vlt `babylon` fixture (87-project monorepo) shows every `*+node_modules` cell collapsing from "very slow" to "faster than pnpm": | Variation | Before | After | |---|---:|---:| | `node_modules` | 37.87× | **0.09×** (11× faster than pnpm) | | `lockfile+node_modules` | 25.52× | **0.07×** (14× faster) | | `cache+node_modules` | 11.55× | **0.15×** (6.7× faster) | | `cache+lockfile+node_modules` | 11.35× | **0.17×** (5.9× faster) | These were the four worst cells in the vlt benchmark chart for babylon (all flagged DNF before #11944 fixed the underlying panic). After #11944 babylon stopped DNF'ing but stayed 11-37× slower because of this workspace-state writing bug. The fast path only failed for workspace installs whose wanted lockfile was absent on the first install of the iteration — i.e. exactly the vlt benchmark's `node_modules` and `*+node_modules` shape. Single-project installs always recorded the root correctly because the no-lockfile fallback already emitted the root entry. Found while validating #11944's claim that the babylon DNF cells would collapse on the next pacquet release (#11902). |
||
|
|
198c661b99 |
fix(pacquet): require pnpm-lock.yaml for single-project optimisticRepeatInstall fast path (#11945)
* fix(pacquet): require pnpm-lock.yaml for single-project optimisticRepeatInstall fast path The port of pnpm's `optimisticRepeatInstall` short-circuit in #11943 applied the workspace branch's mtime-only exit (`checkDepsStatus.ts:263-271`) to every install, including single-project ones. Pnpm's single-project branch (`checkDepsStatus.ts:387-462`) additionally throws `RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND` when `pnpm-lock.yaml` is absent, which the outer `try` converts into `upToDate: false`. Without that gate, pacquet treated a single-project install with `node_modules` present but no lockfile as "Already up to date" — the pnpm.io `node_modules`-only and `cache+node_modules` benchmark cells finished in ~35 ms instead of running the install (pnpm ~5–7 s on the same fixtures). Add an `is_workspace_install: bool` parameter; in single-project mode, require `<workspace_root>/pnpm-lock.yaml` to exist before declaring the install up to date. Workspace installs continue to skip the lockfile probe — pnpm's workspace branch's only lockfile check (`findConflictedLockfileDir`) silently `continue`s on ENOENT (`checkDepsStatus.ts:593-596`). Tests: - `returns_skipped_when_lockfile_missing_in_single_project_mode` - `returns_up_to_date_in_workspace_mode_without_lockfile` - `optimistic_repeat_install_does_not_short_circuit_when_lockfile_missing` (install-level integration test) - Existing happy-path tests now seed `pnpm-lock.yaml` via a new `write_empty_lockfile` helper in `setup_fresh_install`. * test(pacquet): prove single-project optimisticRepeatInstall round-trips end-to-end Add `optimistic_repeat_install_round_trips_on_single_project_install`: two real `Install::run` calls back-to-back on a non-workspace project (no `pnpm-workspace.yaml`). The first install resolves through the registry mock and writes `pnpm-lock.yaml` + `.pnpm-workspace-state-v1.json` to disk. The second install must hit the optimistic fast path — emit `Already up to date` and skip every install-setup event. Pairs with the negative `..._does_not_short_circuit_when_lockfile_missing` test so the gate's polarity is pinned in both directions. * style(pacquet): cargo fmt |
||
|
|
6b3ba4d337 |
fix(pacquet): port pnpm's workspace-link short-circuit and depPath helpers (#11944)
`pacquet install` was panicking on the babylon vlt fixture under the `node_modules` and `lockfile+node_modules` variations. The crash surfaced in [`PkgNameVerPeer::without_peer`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/pacquet/crates/lockfile/src/pkg_name_ver_peer.rs#L55-L62), but the root cause was an unported piece of pnpm's resolver: workspace `link:` nodes weren't short-circuited and flowed through peer resolution, producing depPaths of the shape `link:<rel-path>(<peers>)` that downstream code wasn't prepared for. This PR ports the missing short-circuit, fixes the panic, and closes the parity gaps the audit surfaced. ### Workspace-link short-circuit (the root fix) Ported upstream's [`isLinkedDependency`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/installing/deps-resolver/src/resolveDependencies.ts#L926-L937) arm plus the [`depth === -1`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/installing/deps-resolver/src/resolvePeers.ts#L396) short-circuit in `resolvePeers.ts`. Workspace `link:` deps now: - Skip child recursion in the tree walker (`TreeChildren::Realized(empty)`). - Carry `depth = -1` on the tree node. - Carry empty `peer_dependencies` on the `ResolvedPackage` — peer matching is the linked importer's concern. - Use a leaf [`NodeId`] so every reference to the same workspace path shares one id. `resolve_peers::resolve_node` early-returns for `depth == -1` nodes with `dep_path = pkg.id` (just `link:<rel-path>`). The link node never enters the graph, so `packages:` / `snapshots:` stay clean and the importer's `version:` cell carries `link:<rel-path>` exactly. ### `PkgNameVerPeer::without_peer` no longer panics Construct the bare key through the typed fields rather than reformatting `{prefix}{version}` and re-parsing under `.expect(...)`. The new `PkgVerPeer::without_peer` clones the existing `prefix`/`version` slots and returns a `PkgVerPeer` with an empty peer string. No round-trip, no `.expect(...)`. Defensive even after the upstream fix lands — `without_peer` is called on values from the lockfile, which can be hand-edited. ### `pkgIdWithPatchHash` is now strip-peer-only at every site Four call sites built a [`PkgIdWithPatchHash`] from the wrong baseline: - `virtual_store_layout::lockfile_to_dep_graph` stripped both segments (`PkgNameVerPeer::without_peer().to_string()`) — patched variants of the same `name@version` collided in the dep-graph hash input. - `create_virtual_store`'s two `cas_paths_by_pkg_id` inserts and `hoisted_dep_graph`'s `pkg_id_with_patch_hash` initialiser stripped nothing (raw `snapshot_key.to_string()`) — peer variants of the same patched package got separate CAS-paths entries instead of sharing one. All four now go through `pacquet_deps_path::get_pkg_id_with_patch_hash` (the balanced-paren scan upstream's [`getPkgIdWithPatchHash`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/path/src/index.ts#L63-L70) uses): strip the peer-graph suffix, keep `(patch_hash=…)`. Non-patched packages are unaffected. ### New helpers in `pacquet-deps-path` - `is_runtime_dep_path` — matches `^(?:node|bun|deno)@runtime:` byte-level. Pnpm filters the runtime-only install pass with this. - `try_get_package_id` — strips peer-graph + patch-hash suffix, then drops the `<name>@` prefix on URL-shaped resolution ids while keeping `runtime:` entries intact. ### Ported `deps/path/test/index.ts` cases | Suite | Cases ported | |---|---| | `pacquet-deps-path::suffix_index` | runtime, scoped, scoped + patch-hash, scoped + peer, scoped + both, leading-slash-legacy nested-peer, scope-with-parens | | `pacquet-deps-path::is_runtime_dep_path` | pnpm's `isRuntimeDepPath` test + carve-outs | | `pacquet-deps-path::try_get_package_id` | pnpm's `tryGetPackageId` test + URL-shape, bare, runtime carve-outs | | `pacquet-lockfile::PkgVerPeer` | patch-hash + peer round-trip | | `pacquet-lockfile::PkgNameVerPeer` | file-protocol tarball, patch-hash + peer, scope-with-parens, babylon regression | | `pacquet-resolving-deps-resolver::tests` | babylon-shape: workspace dep with peers → `depth = -1`, empty children, no peer_dependencies | | `pacquet-package-manager::virtual_store_layout::tests` | patched snapshot → `full_pkg_id` retains `(patch_hash=…)` | Resolves #11939. |
||
|
|
c8c50caeca |
perf(pacquet): port optimisticRepeatInstall fast path for repeat installs (#11943)
Closes #11940. ## Summary Ports upstream pnpm's `optimisticRepeatInstall` + [`checkDepsStatus`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/status/src/checkDepsStatus.ts) dispatch ([`installing/commands/src/installDeps.ts:179-194`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/installing/commands/src/installDeps.ts#L179-L194)). When nothing has changed since the previous successful install, `Install::run` now logs `Already up to date` and returns **before**: - loading the wanted or current lockfile, - building the lockfile-verifier list, - the `verify_lockfile_resolutions` fan-out (and the `<cache_dir>/lockfile-verified.jsonl` lookup it performs internally), - `getContext`, project registration, `validateModules`, - the no-op short-circuit that fires *after* all of the above. That's the missing earlier shortcut from the original investigation. It's what lets pnpm finish the vlt `lockfile+node_modules` cells in ~580 ms regardless of `~/.cache/pnpm` state — the verifier-cache file the bench wipes is irrelevant because pnpm never reaches the verifier on a repeat install. ## How it works The fast path keys off `<workspace_root>/node_modules/.pnpm-workspace-state-v1.json`'s `lastValidatedTimestamp` against each project's `package.json` mtime, plus a settings-drift check and a workspace-project structure check. Wire shape and field-by-field comparison match upstream's [`WorkspaceStateSettings`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/workspace/state/src/types.ts) so a previous-install state file written by pnpm is honored by pacquet and vice versa. Settings construction is shared between `build_workspace_state` (writer) and `check_optimistic_repeat_install` (reader) via `optimistic_repeat_install::current_settings`, so the two can't drift on a new field. ## Scope This PR ports the mtime-vs-`lastValidatedTimestamp` exit only — upstream's `modifiedProjects.length === 0` branch at [`checkDepsStatus.ts:263-271`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/status/src/checkDepsStatus.ts#L263-L271). Branches that detect a modified project and then re-verify the lockfile (`assertWantedLockfileUpToDate`, `patchesOrHooksAreModified`) aren't ported here — when any manifest is newer than the last validation, this function returns `Skipped` and the install falls through to the regular path, which still has its own freshness guards (`check_lockfile_freshness`, the existing no-op short-circuit). Disabled under `--frozen-lockfile` so a headless install still fails loudly on missing / stale lockfiles, matching upstream not calling `checkDepsStatus` in that mode. ## Config New `optimistic_repeat_install: bool` field on `pacquet_config::Config`, default `true` — matches [`config/reader/src/index.ts:169`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/config/reader/src/index.ts#L169). Wired through `pnpm-workspace.yaml` via `WorkspaceSettings.optimistic_repeat_install: Option<bool>`. Yaml `optimisticRepeatInstall: false` opts out per-project; the value also lives in the global config file's allowlist so users can opt out at the user level. |
||
|
|
cc4ff817aa |
fix(pacquet/registry): deserialize optionalDependencies and peerDependenciesMeta (#11934)
`PackageVersion` (the per-version registry manifest the npm resolver parses) was missing the `optionalDependencies` and `peerDependenciesMeta` fields. The resolver builds `ResolveResult.manifest` via `serde_json::to_value(picked)` and downstream walks it with [`extract_children`](https://github.com/pnpm/pnpm/blob/1fb8a2d5d8/pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs#L752-L759) (reads `optionalDependencies`) and [`extract_peer_dependencies`](https://github.com/pnpm/pnpm/blob/1fb8a2d5d8/pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs#L776-L824) (reads `peerDependenciesMeta`). Without those fields on the struct, both reads always saw nothing — so `optionalDependencies` edges were silently dropped, and every optional peer was treated as required, then auto-installed via the `autoInstallPeers` fallback in [`hoist_peers`](https://github.com/pnpm/pnpm/blob/1fb8a2d5d8/pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs#L134-L136). ## Astro cascade On the vlt [`astro`](https://github.com/vltpkg/benchmarks/tree/main/fixtures/astro) fixture, `unstorage` (a transitive of astro) declares 19 optional peers via `peerDependenciesMeta` (`@azure/*`, `@vercel/*`, `@netlify/blobs`, `@upstash/redis`, `@deno/kv`, `ioredis`, `uploadthing`, …). Pacquet's resolver auto-installed every one of them and walked their transitive trees; astro's own `optionalDependencies` (`sharp`) went missing entirely. The supposed "5.5× astro deep-tree slowdown" tracked in #11902 was almost all wasted work, not a real perf bug. None of the candidate hypotheses listed there (`async_recursion` Box-pinning, per-node `lock_recoverable` mutex acquires, manifest `serde_json::to_value` cost, tarball extraction) were the bottleneck. ## Before / after on vlt astro | Metric | Before | After | pnpm 11.3.0 | |---|---:|---:|---:| | `pacquet install` wall time (warm store) | 39.6 s | 8.5 s | 7.0 s | | Lockfile lines | 13,364 | 3,037 | 3,444 | | `resolution:` entries | 1,535 | 377 | 377 | | Astro root peer suffixes | 30 (`@azure/...`, `@vercel/...`, ...) | `(rollup@4.60.4)(typescript@5.9.3)` | `(rollup@4.60.4)(typescript@5.9.3)` | | `sharp` (`optionalDependencies`) refs in lockfile | 0 | 110 | 85 | Warm-cache hyperfine (3 runs, fresh `node_modules` + lockfile each time): ``` pacquet (patched): 670 ms ± 72 ms pnpm 11.3.0: 1270 ms ± 7 ms pacquet is 1.89 ± 0.20 times faster than pnpm ``` Closes the astro column in #11902. ## Implementation - Add `optional_dependencies: Option<HashMap<String, String>>` and `peer_dependencies_meta: Option<HashMap<String, PeerDependencyMeta>>` to `PackageVersion`. The existing `#[serde(rename_all = "camelCase")]` handles wire format. - Add a `PeerDependencyMeta` newtype with just the `optional` field (the only field the resolver consumes). - Fix up the four struct-literal construction sites in tests + the trust-evidence projection. - Add a regression test that deserializes a fixture with both fields populated and asserts they round-trip through `serde_json::to_value` — which is what the resolver consumes. |
||
|
|
7120ac0813 |
fix(pacquet/resolving-npm-resolver): singleflight verifier lookup caches (#11933)
* fix(pacquet/resolving-npm-resolver): singleflight the verifier lookup caches Convert PublishedAtLookupContext's five per-key dedup caches from Mutex<HashMap<String, T>> to Mutex<HashMap<String, Arc<OnceCell<T>>>> so two verifier tasks that race for the same key share one in-flight fetch instead of both performing the work. Mirrors upstream's `Map<string, Promise<T>>` singleflight pattern (see #11932). The outer mutex is dropped before awaiting the init future so unrelated keys stay unblocked. Add a fan-out regression test that asserts 16 concurrent verifications of the same (registry, name, version) issue exactly one abbreviated GET; without the singleflight property mockito's `.expect(1)` fails with 16 requests received. Refs #11932. * fix(pacquet/resolving-npm-resolver): rename SingleflightMap generic to satisfy dylint `perfectionist::single-letter-generic` flagged the `T` parameter on the new `SingleflightMap` type alias. Rename to `Value` — the slot contents are the values cached behind the singleflight cells. |
||
|
|
1fb8a2d5d8 |
perf(pacquet): unlock no-op short-circuit + port abbreviated-modified verifier shortcut (#11931)
Two fixes that together unlock pnpm-parity on the `benchmarks.vlt.sh` `lockfile+node_modules` shape — the row where pacquet was 2-12× slower than pnpm on every fixture. ### 1. `fix(modules-yaml)`: normalise joined `virtualStoreDir` `read_modules_manifest` joins a stored relative `virtualStoreDir` with `modules_dir` to recover an absolute path, mirroring upstream's `path.join(modulesDir, modules.virtualStoreDir)`. Node's `path.join` normalises interior `..` segments; Rust's `PathBuf::join` does not. Stored values like `../../../Users/.../store/v11/links` came back as `<modules_dir>/../../../Users/.../links` — never byte-matched `Config::effective_virtual_store_dir()`, so the no-op short-circuit added in #11904 silently missed every install whose store sits outside the project (the default macOS / Linux setup). The accompanying refactor lifts `lexical_normalize` (already duplicated in `cmd-shim` and `store-dir`) into `pacquet-fs` so `modules-yaml` doesn't make it a third copy. ### 2. `perf(resolving-npm-resolver)`: port the missing verifier layers The npm resolution verifier walked a 4-layer fallback chain in upstream pnpm (abbreviated-modified shortcut → on-disk full-meta mirror → npm attestation endpoint → full packument fetch); pacquet only had the last two. The module's doc-comment explicitly noted "Phase 4 stubs the abbreviated-shortcut and on-disk-mirror layers (no cached fetcher / no mirror yet); Phase 5 ports `fetchFullMetadataCached.ts`…" — this is Phase 5. Result: a cold lockfile-verification pass now pays at most one *abbreviated* GET per name (small payload, decided by package-level `modified`) instead of a full-meta GET per name (hundreds of KB each). ## Bench 5-iteration cold-cache measured pass on `vltpkg/benchmarks/fixtures/svelte` (`pnpm-lock.yaml` + `node_modules` present, `~/.cache/pnpm` and store wiped before each run), 10-core M-series Mac: | | pnpm | pacquet@main | this PR | |-------------|------:|-------------:|--------:| | wall time | 0.54 s | 2.16 s | 0.71 s | 3.0× faster on the `lockfile+node_modules` row. |
||
|
|
b7229f8571 |
fix(pacquet/resolving-npm-resolver): honor linkWorkspacePackages for bare-semver deps (#11930)
* fix(pacquet/resolving-npm-resolver): honor linkWorkspacePackages for bare-semver deps Pacquet's npm resolver only consulted the workspace map for `workspace:`-prefixed wanted deps; bare-semver ranges always went straight to the registry. When the workspace package isn't on npm (e.g. babylon's `@dev/build-tools`), the install errored out with 404; when a same-named package existed on the registry, pacquet silently linked the wrong copy. Mirror pnpm's three workspace branches around `pickPackage`: * registry pick succeeded + workspace shadow (exact `name@version` match, higher local version, or `preferWorkspacePackages`), * registry pick returned `null` → workspace fallback, * registry fetch errored → workspace fallback (swallow workspace errors and re-raise the registry error). Gated by `link-workspace-packages` (true / false / "deep") which is now parsed from `pnpm-workspace.yaml`, flows through `Config`, and is encoded into `ResolveOptions::always_try_workspace_packages` at the install layer. Tri-state semantics are preserved on the config side; pacquet's single-`base_opts` deps-resolver collapses `true` and `"deep"` onto the same per-call flag until depth threading lands. Closes #11929. * fix(pacquet/resolving-npm-resolver): swap unicode ellipsis for ascii triple-dot to satisfy Dylint * test(pacquet/resolving-npm-resolver): port remaining linkWorkspacePackages tests from pnpm Cover the six pnpm tests the initial port skipped: * `injected_workspace_match_emits_file_resolution` — workspace shadow branch with `injected: true` emits a `file:` resolution. * `workspace_fallback_picks_highest_version_for_latest_tag` — 404 fallback into a multi-version workspace via the Tag branch of `pick_matching_local_version_or_null`. * `workspace_fallback_picks_local_prerelease_for_latest_tag` — 404 fallback into a prerelease-only workspace, exercising `resolve_workspace_range`'s `includePrerelease` arm. * `workspace_fallback_resolves_specific_version_request` — 404 fallback against a pinned version-spec lookup. * `workspace_fallback_kicks_in_when_registry_lacks_requested_version` — `Ok(None)` fallback path (registry serves the packument but no version matches), distinct from the `Err` 404 path. * `registry_error_propagates_when_workspace_has_no_matching_version` — negative test verifying the original 404 surfaces when the workspace can't satisfy the request. * `registry_pick_wins_when_workspace_version_does_not_match` — workspace shadow no-op when the workspace carries a different version than the registry pick. * fix(pacquet/resolving-npm-resolver): drop trailing comma in single-line assert! to satisfy Dylint * test(pacquet/resolving-npm-resolver): trim redundant doc-prose on linkWorkspacePackages tests Per pacquet/CLAUDE.md "tests are documentation" — the new test docblocks restated what the test name plus body already say. Keep only the upstream-link citation (required by the porting rule) and drop the trailing narrative. Two ports keep one extra sentence because the distinction they exercise (the `Ok(None)` vs `Err` fallback split, the `includePrerelease` arm) is not recoverable from the test body alone. Also drop the inline `lockfile_dir` / "Registry returns a packument..." comments that narrate setup the helper code already encodes; keep the `latest`-back-stamp comment because it explains why a workspace-resolved result still carries that field. |
||
|
|
e52e4fce63 |
feat(pacquet): port detect-libc to Rust and replace ad-hoc libc detection in graph-hasher (#11921)
* refactor(graph-hasher): replace ad-hoc libc detection with pacquet-detect-libc Extract libc detection into a new `pacquet-detect-libc` crate ported from the upstream `detect-libc` JS package, replacing the limited ad-hoc `detect_host_libc()` in graph-hasher. Detection uses a three-tier fallback (ELF header → filesystem → command) that avoids spawning processes in the common case and works in slim containers where getconf or ldd may not be present. The command step runs getconf and ldd --version as separate subprocesses to avoid stream pollution between the two, with ldd only invoked when getconf fails. * fix(detect-libc): harden ELF parser, UTF-8 decoding, test cfg, and imports - Use checked arithmetic (checked_add/checked_mul) in elf_interpreter to return None on overflow instead of panicking on malformed headers - Use from_utf8_lossy for /usr/bin/ldd content so non-UTF-8 bytes don't skip the filesystem detection path - Gate detect_integration_host test with #[cfg(target_os = "linux")] so it doesn't fail on non-Linux platforms - Replace use super::* with explicit imports in command tests * fix(detect-libc): use from_utf8_lossy for command output, fix lints and tests |
||
|
|
97391bf341 |
test(pacquet/package-manager): make side-effects write test umask-agnostic (#11922)
* fix(pacquet/package-manager): make side-effects write test umask-agnostic The test hardcoded mode 0o644 in the pre-seeded PackageFilesIndex row, but fs::write() assigns mode according to the process umask (e.g., 0o664 with pam_umask usergroups, Debian's default). calculate_diff() compares both digest and mode, so a mode mismatch caused a false-positive assertion failure for unchanged index.js on systems with umask 0002. Read the actual file mode from the written fixture and use it in the pre-seeded row instead of a hardcoded value. * fix(pacquet/package-manager): factorize umask-agnostic mode into fixture Move the actual-mode reading into create_postinstall_modifies_source_fixture so both write_path_populates_side_effects_row and write_path_cache_key_includes_patch_hash share it, instead of each test duplicating the metadata inspection. |
||
|
|
d579e6cbb5 |
perf(pacquet): trim install-phase syscalls and allocations (#11864)
* perf(fs,package-manager): striped CAS lock + skip pre-flight stat on fresh-target imports Two install-phase syscall trims: - `cas_write_lock` swaps the per-path `DashMap<PathBuf, Arc<Mutex<()>>>` for 256 static `Mutex<()>` stripes keyed by hashed path. Every CAFS write previously paid one `PathBuf::to_path_buf` allocation, a `DashMap` shard write lock, plus an `Arc<Mutex<()>>` slot allocation even though contention was vanishingly rare. Striping keeps the writer/verifier coordination the per-path mutex provided while removing those per-call costs. With 256 stripes and ~10 rayon workers the false-sharing probability per pair is ~4%, and the guarded body (one `O_CREAT|O_EXCL` open + `write_all` of a tar entry) is microseconds long. - `import_indexed_dir::populate_dir` now calls a new `import_into_fresh_target` instead of `link_file`. `populate_dir` only ever runs against a directory it just `mkdir`'d, so the `fs::metadata` pre-flight `link_file` performs to protect the Copy-method overwrite contract is wasted — every call is `NotFound` in practice and the EEXIST surface from the import syscall is the only collision signal we need. Saves ~170k `stat` syscalls per clean install on the alotta-files fixture. `link_file` still exists with the original semantics for any caller that genuinely doesn't know whether the target is fresh. On the 3343-package alotta-files fixture against the verdaccio mock, clean-install wall time goes from ~28s to ~19-22s on the local 10-core machine — roughly closing the gap to pnpm (~20s) for that scenario. Refs #11857, #11851. * perf(store-dir): trim per-CAS-file allocations on the hot write path Two micro-optimisations in `cas_file_path`, the helper every CAFS write goes through: - `cas_file_path` no longer `format!`s the sha-512 digest into a fresh `String`. Sha-512 is always 64 bytes / 128 hex chars, so render the hex into a stack buffer and slice it into the `file_path_by_hex_str` call instead. One heap allocation per file shaved off — ~170k on the alotta-files clean install. - The repeated `self.v11().join("files")` rebuild used to walk two `PathBuf::join`s per call. Memoise the result behind a `OnceLock` on `StoreDir` (`cached_files_dir`) so `file_path_by_head_tail` borrows it without re-joining. Race-free initialisation across rayon workers, one allocation per process instead of one per file. Refs #11857. * docs(pacquet): address CodeRabbit nits - Refresh `import_indexed_dir` doc comments so they name `import_into_fresh_target()` (the actual materialization helper after the fresh-target split) instead of `link_file()`. - Add a const assertion that `NUM_CAS_LOCK_STRIPES` stays a power of two, since `cas_write_lock` uses `& (NUM_CAS_LOCK_STRIPES - 1)` as the stripe selector. * docs: forbid past-implementation history in comments - Extend AGENTS.md Comments rules: comments must describe the current contract, not what the code replaced. Phrasings like "used to", "previously", "the original X", or parentheticals naming a removed type belong in `git log`. - Apply the rule to `cas_write_lock`'s doc, which previously framed itself in terms of the removed `DashMap<PathBuf, Arc<Mutex<()>>>` shape. |
||
|
|
440e15586d | fix: summarize all global update groups (#11920) | ||
|
|
ae2175829a |
feat(registry-access): extract dist-tag + adduser helpers, dogfood from tests (#11926)
* feat(registry-access): extract setDistTag and dogfood from tests
Add `@pnpm/registry-access.commands#setDistTag` — the low-level PUT to
`/-/package/:pkg/dist-tags/:tag`. The CLI `dist-tag add` handler now
calls it instead of issuing the fetch inline.
Tests in this monorepo now use a thin new package
`@pnpm/testing.registry-mock` (REGISTRY_MOCK_PORT + REGISTRY_MOCK_CREDENTIALS
baked in) that delegates to `setDistTag`, replacing `addDistTag` from
`@pnpm/registry-mock`. That dropped helper relied on
`anonymous-npm-registry-client` and a verdaccio-era
fetch-then-DELETE-then-PUT dance that is no longer needed against
pnpm-registry.
39 test files swapped from `@pnpm/registry-mock` to
`@pnpm/testing.registry-mock`.
* fix: move setDistTag to its own package to break tsconfig project-reference cycle
testing/registry-mock → registry-access.commands → releasing/commands
→ installing/commands → installing/deps-installer → testing/registry-mock.
Extract setDistTag into @pnpm/registry-access.set-dist-tag (only depends
on @pnpm/error, @pnpm/network.fetch, @pnpm/npm-package-arg). Both
@pnpm/registry-access.commands and @pnpm/testing.registry-mock import
from it. Cycle gone.
* feat(registry-access): extract addUser helper, dogfood from login + tests
Add @pnpm/registry-access.add-user — a small helper that PUTs to
/-/user/org.couchdb.user:<name> and returns { token }. The CLI's
classicLogin (pnpm login fallback path) now calls it, and tests
use it via @pnpm/testing.registry-mock instead of the legacy
addUser from @pnpm/registry-mock.
Swapped 3 call sites: globalSetup.js, installing/deps-installer's
auth.ts, and pnpm/test/dlx.ts. AddUserHttpError exposes status +
text + parsed-json-if-applicable + headers so the CLI can still
do its OTP detection. One webauth-OTP login test mock had to be
adjusted to provide its body via `text` (JSON-stringified) rather
than `json` only, since the helper consumes the body via `text()`.
* refactor: consolidate set-dist-tag + add-user helpers into one @pnpm/registry-access.client package
One shared package is better than splitting per endpoint. Future endpoints
(publish, deprecate, etc.) can land here without another wrapper.
No behavioral change — same setDistTag and addUser exports as before,
just under one roof. Callers updated: registry-access.commands,
auth.commands, testing.registry-mock.
* fix(registry-access): sort imports
|
||
|
|
ac299aa0e5 |
fix(pacquet,package-manager): walk every workspace project in fresh-resolve install (#11905)
* fix(package-manager): walk every workspace project in fresh-resolve install The fresh-resolve install path (no `--frozen-lockfile`, no usable lockfile) only resolved the workspace root manifest, so sibling workspace projects' own dependencies never landed in the lockfile or on disk. Re-run `resolve_importer` per importer with shared install caches (`meta_cache`, `fetch_locker`, `picked_manifest_cache`), merge the per-importer graphs, and emit one `importers[<id>]` entry per project. Mirrors upstream's [`resolveRootDependencies`](https://github.com/pnpm/pnpm/blob/3422cecfd3/installing/deps-resolver/src/resolveDependencies.ts#L327-L437) iteration shape — one shared resolution context, per-importer direct-deps slices. Per-importer `link_bins` so each project gets its own `node_modules/.bin`. GVS `register_project` now loops every importer key the freshly-built lockfile carries, mirroring the frozen path. `importer_dep_version` and `snapshot_dep_ref` learned a `link:` short-circuit so workspace-sibling edges emit `ImporterDepVersion::Link` / `SnapshotDepRef::Link` instead of falling through to the `name@version` parser. Cross-importer `TreeCtx` sharing (full upstream parity: one resolution context with per-importer hoist loops) is deferred — each `resolve_importer` call still has its own context. Network-side caches still amortize packument fetches and JSON parsing across importers; only per-resolve semver matching duplicates. Closes #11901. * fix(workspace): drop trailing comma on single-line assert_eq! for Perfectionist lint * fix(package-manager): register only the workspace root with the store, matching pnpm Pacquet was looping `register_project` over every importer in both the frozen-lockfile and fresh-lockfile branches, but upstream pnpm calls `registerProject(opts.storeDir, opts.lockfileDir)` exactly once per install against the workspace root — store prune walks the workspace's `node_modules/.pnpm/` to find every installed package, so one registry entry per workspace is enough. Consolidate to a single call near the start of `Install::run`, matching pnpm's `getContext` ordering at <https://github.com/pnpm/pnpm/blob/d8a79a9c30/installing/context/src/index.ts#L128>. Also port two upstream-derived tests that the multi-importer rewrite of `compute_corrected_optional` and the per-importer link rendering were previously missing direct coverage for: - `multi_importer_pruner_marks_shared_dep_non_optional_when_any_importer_reaches_via_prod` ports the spirit of pnpm's [`pruneSharedLockfile`](https://github.com/pnpm/pnpm/blob/d8a79a9c30/lockfile/pruner/src/index.ts#L17) cross-importer pooling: a depPath reached via a non-optional path from any importer ends up `optional: false` even when another importer reaches it only via an optional path. - `workspace_sibling_link_renders_per_importer_with_link_ref` exercises the multi-importer `workspace:`-link case — importer A depends on importer B via a `link:`-resolved depPath, both render their own `importers[<id>]` entries, and the link node stays out of `packages:` / `snapshots:`. * fix(package-manager): skip undeclared aliases from pruner BFS seeds Addresses CodeRabbit's review on PR #11905. Pacquet's resolver hoists auto-installed peers into `direct_dependencies_by_alias` even when they aren't in the importer's manifest (see `resolve_importer::direct.extend(...)` after each `hoist_peers` call). `build_importer` correctly excludes those undeclared aliases from the importer's lockfile entry, but `compute_corrected_optional` was seeding the pruner BFS from the full `direct_dependencies_by_alias` and defaulting unknown aliases to `DependencyGroup::Prod`. That diverges from upstream's [`pruneSharedLockfile`](https://github.com/pnpm/pnpm/blob/d8a79a9c30/lockfile/pruner/src/index.ts#L27-L29), which seeds purely from `lockfile.importers[*].{dev,optional,}dependencies` — i.e., from the same set `build_importer` writes. The mismatch forced auto-peers reachable only via an optional parent's chain to `optional: false`, leaking them into non-optional installs. Skip aliases not in the manifest when seeding. The new test `auto_installed_peer_not_declared_in_manifest_is_skipped_from_pruner_seeds` pins the corrected behavior — `peer-x` (auto-installed for an `optionalDependencies` parent) stays `optional: true`, matching pnpm. Verified the test fails against the pre-fix code. Also tightens the multi-importer integration test's lockfile assertion: scope the `hello-world-js-bin-parent` check to the `packages/a:` importer section instead of a global substring match, so the test proves the direct-dep entry — not just any mention in `packages:`. * fix(package-manager,store-dir): ensure store root exists before registering project CI failure: `fresh_install_honors_enable_global_virtual_store` started failing after the previous register_project consolidation. Two compounding bugs: 1. `register_project` now runs early in `Install::run`, before any install phase has materialized the store. With the test's relative `storeDir: ../pacquet-store` in `pnpm-workspace.yaml`, `config.store_dir.root()` ends up shaped like `<workspace>/../pacquet-store/v11` — a path that doesn't yet exist on disk. 2. `path_contains`'s "lexical fallback" wasn't actually lexical — it called `dunce::canonicalize`, and on failure (path doesn't exist) it kept the path verbatim and ran `starts_with`. So `<workspace>/../pacquet-store/v11`.starts_with(`<workspace>`) returned true, the early-return guard fired, and the call silently skipped without writing the registry entry. Two-part fix matching upstream: - `Install::run` now calls `fs::create_dir_all(store_dir.root())` before `register_project`, mirroring pnpm's [`fs.mkdir(opts.storeDir, { recursive: true })`](https://github.com/pnpm/pnpm/blob/d8a79a9c30/installing/context/src/index.ts#L125) call right before `registerProject`. Once the store exists, `canonicalize` succeeds and `path_contains` resolves both sides correctly. - `path_contains` now lexically normalizes `.` / `..` components when canonicalize fails. Matches upstream's `is-subdir` semantics (which uses `path.relative`, purely lexical). New test `path_contains_resolves_parent_components_when_paths_do_not_exist` pins the behavior; verified it fails against the pre-fix code. * style: cargo fmt * fix(package-manager,store-dir): satisfy Perfectionist lint and harden lexical_normalize Two issues: 1. The multi-line `assert!` in `multi_importer_pruner_marks_shared_dep_non_optional_when_any_importer_reaches_via_prod` was missing its trailing comma after `cargo fmt` reformatted it from one-line to multi-line. Perfectionist's `macro-trailing-comma` rule (which CI enforces via Dylint) flagged it. Added the comma. 2. CodeRabbit pointed out that `lexical_normalize` silently dropped leading `..` components because `PathBuf::pop()` is a no-op when the path is empty. For the current `path_contains` callers (both inputs are absolute paths) this doesn't matter, but the helper is now a general-purpose utility and the bug would bite any future caller passing a relative path. Replaced the naive `out.pop()` with a match on the trailing component: - `Component::Normal(_)` → pop (real segment collapses with `..`) - `Component::RootDir | Prefix(_)` → drop the `..` (`/..` is `/` per POSIX) - else → push `..` (preserve leading `..` chain in relative paths) Matches Go's `path.Clean` semantics. New test `lexical_normalize_handles_parent_dir_corner_cases` pins all four corner cases. |
||
|
|
0721d64188 |
fix: require provenance for trusted publisher evidence (#11911)
* fix: require provenance for trusted publisher evidence
* test: align provenance fixtures with registry types
* chore: include pnpm CLI in changeset
The repo guideline requires every changeset that touches a published
package to list the pnpm CLI explicitly so the fix appears in the CLI's
release notes.
* fix(resolving-npm-resolver): require provenance for trusted publisher evidence
Ports pnpm's
|
||
|
|
e8b3ae132e | fix: clarify non-root resolutions warning (#11912) | ||
|
|
494cdcaa01 |
chore: drop verdaccio from the repo (#11925)
The TS test harness (`__utils__/jest-config/with-registry/globalSetup.js`) already launches `pnpm-registry`, and pacquet's `RegistryMode::Verdaccio` spawns `pnpm-registry` too (the enum variant is a misnamed leftover). The verdaccio dependency was only there to satisfy `@pnpm/registry-mock`'s peerDependency declaration — nothing in this repo invokes verdaccio at runtime. Remove the catalog entry, the stranded `verdaccio.yaml` config, and the `@verdaccio/auth` packageExtensions block. Mark `@pnpm/registry-mock`'s verdaccio peer optional so pnpm doesn't auto-install it (and the entire `@verdaccio/*` tree) across the workspace. Lockfile drops ~1100 lines. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
d8a79a9c30 |
feat(registry): add auth/dist-tag/publish endpoints + wire TS tests onto pnpm-registry (#11914)
Lands the pieces of the npm registry protocol that pnpm-registry was missing, and switches the TypeScript test harness off verdaccio onto pnpm-registry. `@pnpm/registry-mock` (the npm package) is untouched. ### Server-side additions (`registry/crates/pnpm-registry`) - `PUT /-/user/org.couchdb.user:<name>` — adduser / login, returns a Bearer token. In-memory user + token stores. - `PUT /:pkg` — publish (scoped + unscoped). Base64-decodes `_attachments`, merges into the existing packument, writes manifest + tarball atomically. 100 MiB body limit. - `GET /-/package/:pkg/dist-tags` + `PUT/DELETE /-/package/:pkg/dist-tags/:tag` — rewrites the on-disk packument so tag changes survive a restart. - `Authorization: Bearer` and `Authorization: Basic` both identify the caller. - Per-package access policy (wax glob patterns). Defaults mirror `@pnpm/registry-mock`'s `config.yaml`: `@private/*` and `@pnpm.e2e/needs-auth` require auth; everything else is anonymous read, authenticated write. Enforced on every packument / version-manifest / tarball GET and every write endpoint. ### TypeScript-test migration - `__utils__/jest-config/with-registry/globalSetup.js` keeps `prepare()` from `@pnpm/registry-mock` (still needed for the tempy storage path written into the runtime-config yaml — `getIntegrity` reads it from there) but spawns `pnpm-registry` instead of verdaccio. `addUser`, `addDistTag`, `getIntegrity`, `REGISTRY_MOCK_*` from registry-mock work as-is — they're plain npm-wire-protocol HTTP calls. - Binary lookup follows pacquet's pattern: `PNPM_REGISTRY_BIN` env override, then `target/release/pnpm-registry`, then `target/debug/pnpm-registry`. - CI test job (`.github/workflows/test.yml`) installs the Rust toolchain via the existing `./.github/actions/rustup` composite action and builds `pnpm-registry --release` before tests run. Per-platform — Linux and Windows in the matrix each build their own. |
||
|
|
058f5f2f8b |
fix(package-manager): port pnpm's lockfile-pruner BFS to re-derive transitive optional (#11919)
The resolver's per-node AND-fold updates only the directly-revisited package, so descendants walked first via an `optionalDependencies` edge stay stuck at `optional: true` even when a later non-optional path reaches them transitively. Upstream hides this from users by re-deriving the flag in `copyDependencySubGraph`; pacquet had no equivalent pass, so a fetch or build failure on a transitively-required package was silently tolerated as if it were optional. Port the BFS into `dependencies_graph_to_lockfile`: walk from the importer's direct deps (classified by manifest dep-group), recurse through each node's children with parent-inherited optional for regular edges and forced `optional: true` for `optionalDependencies` edges, then override `SnapshotEntry.optional` to `false` for any package reached by an all-non-optional path. Refs https://github.com/pnpm/pnpm/issues/11916. |
||
|
|
3788a8b0e6 |
perf(pacquet): lazy children realization in dependency tree (#11915)
* perf(resolving-deps-resolver): defer per-occurrence child realization until peer resolution
Mirrors upstream pnpm's lazy `children` thunk on `DependenciesTreeNode`:
revisits of a `pkgIdWithPatchHash` no longer recurse to fan out a fresh
NodeId subtree eagerly. Instead the tree node records
`TreeChildren::Lazy { parent_ids }` and the peer resolver allocates
per-occurrence child NodeIds on first descent via `realize_children`,
applying the same `parentIdsContainSequence` cycle break upstream uses
in `buildTree`.
Pure subtrees that the peer resolver already short-circuits through
`purePkgs` (ported in #11906) now skip realization entirely — the tree
never gets walked past the first occurrence of those packages.
Bench (astro deep tree, cold store, single resolver phase):
- tree nodes: 74,940 → 4,069 (~18× smaller)
- `resolve_importer`: 11.6s → 8.2s (~1.42× faster)
Refs #11907.
* fix(resolving-deps-resolver): fix doc + dylint failures from #11915
- Re-export `TreeChildren` and `ChildEdge` from the crate root so the
intra-doc links from public docs (`resolve_peers`, `ResolvedTree`)
resolve. They were `pub` on the enum/struct but unreachable because
`resolved_tree` is a private module.
- Drop the `[Walker::realize_children]` / `[Walker::pure_pkgs]`
intra-doc references from `resolve_peers`'s public doc — `Walker`
is private, so the links failed under `--document-private-items`
with `-D rustdoc::private_intra_doc_links`. The prose still names
the items in plain backticks.
- Rename closure params `p` and let binding `v` in `realize_children`
to satisfy `perfectionist::single-letter-{closure-param,let-binding}`.
* fix(resolving-deps-resolver): persist first-walk is_leaf for lazy realization
Mirrors upstream's
[`ResolvedPackage.isLeaf`](https://github.com/pnpm/pnpm/blob/b9de85dcb6/installing/deps-resolver/src/resolveDependencies.ts#L250)
field: `pkg_is_leaf(&result)` is computed once on the first walk and
stored on `ResolvedPackage::is_leaf`; the peer-resolver's
`realize_children` reads it back instead of inferring leaf-ness from
`children_by_id.is_empty() + peer_dependencies.is_empty()`.
The inferred check was a weaker approximation — a package with a
missing manifest (e.g. a git/tarball/local resolution where `result.
manifest` is None) lands on `pkg_is_leaf == false` in the eager walk
but on `is_leaf == true` in the realize path, which would collapse
distinct per-occurrence `NodeId`s onto a shared `NodeId::leaf` and
break the peer resolver's per-call-site state.
Matches upstream's
[`buildTree` consumption](https://github.com/pnpm/pnpm/blob/b9de85dcb6/installing/deps-resolver/src/resolveDependencyTree.ts#L381):
`ctx.resolvedPkgsById[child.id].isLeaf` is the source of truth, not
recomputed per realization.
* style(resolving-deps-resolver): apply cargo fmt for is_leaf binding
* test(resolving-deps-resolver): cover lazy children edge cases
Adds two regression tests for the lazy-children mechanism introduced
in #11915 that the existing coverage didn't hit:
1. `revisit_with_no_manifest_child_keeps_per_occurrence_node_id` —
a child whose first walk produced `result.manifest == None`
(the shape git / tarball / local resolvers return) must keep the
non-leaf classification on every lazy realisation. Without the
`ResolvedPackage::is_leaf` persistence the realizer would mis-
classify it as a leaf and collapse distinct occurrences onto a
shared `NodeId::Leaf`, breaking per-call-site state.
2. `pure_revisit_leaves_lazy_children_unrealized` — a pure pkg
reached through multiple parents only realises its children for
the occurrence the peer resolver walks first. Subsequent
occurrences hit the `purePkgs` short-circuit before
`realize_children` runs, so their `TreeChildren::Lazy` stays
Lazy. Regression guard against accidentally moving the realise
call above the short-circuit.
Both tests were validated by breaking the relevant subject (swapping
`is_leaf` back to the inferred check; moving `realize_children`
above the `purePkgs` gate) and confirming they fail cleanly.
* style(resolving-deps-resolver): drop hyphenated mis- in test docs
CI's typos pass at .typos.toml flags `mis-` (suggests "miss" /
"mist"). Use "misclassify" instead of "mis-classify" — same
word, no hyphen, no typo hit.
|
||
|
|
b9de85dcb6 |
ci(pacquet): drop pnpm comparison and self-compare on main from integrated-benchmark (#11913)
The pnpm-CLI baseline was useful while pacquet was slower than pnpm; now that pacquet is the perf target itself, comparing against pnpm on every run is noise. Drop --with-pnpm from every scenario step. When the workflow runs on main, HEAD and main point at the same commit, so a HEAD-vs-main comparison is wasted work. Resolve the target list at job level: pacquet@HEAD on main, pacquet@HEAD pacquet@main everywhere else (PRs, workflow_dispatch from non-main). The Bencher upload already filters to pacquet@HEAD, so the single-target result still lands on the main baseline as before. |
||
|
|
f5d7723f3a |
perf(pacquet): port pnpm's purePkgs + peersCache for peer resolution (#11906)
* test(resolving-deps-resolver): port four peer-resolution cases from pnpm Pacquet's `mod peers` test block had five tests, all of which exercise single-occurrence happy paths. None covered the harder branches the upstream resolver is designed to handle — cycles, packages reached twice with divergent peer scope, parallel peer chains, transitive peer issues. That gap left the peer resolver under-tested even for the current algorithm, and would have made it dangerous to land the `peersCache` + `purePkgs` ports tracked in #11907 because the new cache lookup short-circuits exactly the branches no existing test exercises. Port four resolver-layer cases from [`installing/deps-resolver/test/resolvePeers.ts`](https://github.com/pnpm/pnpm/blob/c86c423bdc/installing/deps-resolver/test/resolvePeers.ts): - `cyclic_peer_dependencies_resolve_cleanly` — four-way cycle (foo ↔ bar ↔ qar ↔ zoo), every node lands in the graph without the walker panicking on cycle re-entry. Upstream `:14`. - `revisit_resolves_peer_in_one_occurrence_misses_in_other` — same package reached via two parent chains, one where the peer resolves and one where it's missing; both occurrences must surface with distinct depPaths. Upstream `:128`. - `two_peer_chains_resolve_against_their_own_sibling` — two parallel pkg-with-peer chains in the same importer; each picks its own sibling, no cross-pollination. Substitutes for upstream's `'resolve peer dependencies with npm aliases'` (`:573`) since npm-alias plumbing isn't yet wired through the test stub resolver — the TODO captures the gap so a follow-up can swap in the alias form once it lands. - `bad_peer_inside_subtree_records_resolved_from_parent` — a peer reachable through a subdependency but at the wrong version surfaces as a *bad* peer, not a missing one. Stands in for upstream's `'unmet peer dependency issue resolved from subdependency'` describe-block (`:502`); the `resolvedFrom` field upstream tracks isn't exposed on pacquet's `PeerDependencyIssue` yet, so the test asserts the bad/missing classification only. All four pass on `main`. Together they exercise the parent-context matching that future cache optimizations (#11907) need to get right — the second test in particular drives a shared-subtree shape where a naive NodeId-keyed cache returns stale depPaths. Refs #11907. * perf(resolving-deps-resolver): port pnpm's purePkgs + peersCache for peer resolution The peer resolver was rewalking every `NodeId` in the tree from scratch and recomputing the full per-package peer set on each hoist-loop iteration. On the `astro@^5` install (~1.6k unique packages, deep transitive shape) that pushed `resolve_peers` to 3.8 s of an 8.5 s `resolve_importer` phase — pacquet had already recorded `is_pure` per graph node but wasn't using it as a cache key, and `peersCache` was deferred in the original slice. Port both upstream optimizations and remove the unsafe `node_dep_paths` shortcut at the top of `resolve_node` that silently returned stale `depPath`s when the same shared `NodeId` got walked under two different parent peer contexts (an inevitable shape post-isNew-gate, and the bug `revisit_resolves_peer_in_one_occurrence_misses_in_other` catches). ## `purePkgs` fast path A `HashSet<String>` of `pkgIdWithPatchHash` values whose full subtree resolved with zero external peers and zero missing peers. Populated bottom-up: a node is added when `is_pure` is true after its own walk completes. A revisit of any pure pkg whose own `peerDependencies` is empty short-circuits with `depPath = pkgIdWithPatchHash` — no recursion, no peersCache lookup. Mirrors upstream's `purePkgs` early-return (resolvePeers.ts:398-406 at |
||
|
|
add6c794f1 |
feat(registry): implement pnpm-registry server and adopt it in pacquet's test mock (#11898)
Creates a working pnpm-compatible npm registry server (verdaccio analogue, in Rust) — and replaces `@pnpm/registry-mock`'s Node + Verdaccio launcher in pacquet's test setup with the new binary, against `@pnpm/registry-mock`'s shipped storage.
### What `pnpm-registry` does
- **HTTP server** (axum + tower-http) with the three endpoints pnpm/npm clients need:
- `GET /<pkg>` — packument (`/{name}` and `/{scope}/{name}`)
- `GET /<pkg>/<version-or-tag>` — single-version manifest, resolves `dist-tags` and rewrites `dist.tarball` to point at this server
- `GET /<pkg>/-/<tarball>` — tarball, streamed
- **Two modes:**
- **Proxy** — fetches missing packuments/tarballs from a configurable upstream (defaults to `https://registry.npmjs.org`), caches to disk
- **Static** (`--static`) — serves the storage directory verbatim, 404s on cache miss
- **Verdaccio-shaped on-disk storage** (`<root>/<pkg>/package.json` + flat tarballs) — drop-in compatible with the storage `@pnpm/registry-mock` publishes
- **Tarball streaming** — cache hits stream off disk; cache misses tee upstream chunks into a temp file via an mpsc channel and forward them to the client at the same time, atomically renaming on success and abandoning on upstream error or client disconnect
- **Tuned HTTP client** — wraps `pacquet_network::ThrottledClient::new_for_installs()`, inheriting pnpm's tuned defaults (`User-Agent: pnpm`, HTTP/1.1, hickory DNS, connection-pool tuning, concurrency semaphore)
- **Gateway-style status mapping** — `is_timeout()` → 504, `is_connect()` → 503, everything else (incl. upstream 5xx) → 502. No proxy-side retry (the pnpm client already has `fetch-retries`; stacking retries would only multiply latency on real failures).
### What changed in pacquet
- `pacquet/tasks/registry-mock` now spawns `pnpm-registry` against `node_modules/@pnpm/registry-mock/registry/storage-cache` (proxy mode with `npmjs.org` upstream and a 1-year packument TTL — matching `@pnpm/registry-mock`'s `'**': proxy: npmjs` verdaccio config). No more Node, no more Verdaccio, no more `launch.mjs`, no more process-tree walk to kill child verdaccios.
- `@pnpm/registry-mock` stays as a devDep — only for the storage data it ships, not the launcher.
### Tests
- **36 pnpm-registry tests** (12 unit + 7 against `@pnpm/registry-mock` storage in static mode + 17 mockito-based proxy/cache/streaming): packument rewrite, version-manifest resolution, tarball streaming (large body, cache finalize, mid-stream upstream error, client disconnect mid-stream, concurrent fetches → one cache file), gateway status mapping (504/503/502), stale-cache fallback on upstream failure, TTL refresh, invalid-package-name 400, scoped vs unscoped routing.
- **Full pacquet test suite** (2043 tests) runs green against `pnpm-registry`-backed mock.
### CI
- `pacquet-ci.yml` and `pacquet-codecov.yml` path filters now include `registry/**` (so registry-only PRs trigger the workspace CI); typos checker covers `registry` too. The workflow name stays "Pacquet CI" but a header comment explains the intentional cross-stack scope.
- `just registry-mock launch` pre-builds with `cargo nextest run --no-run` (workspace-wide) so its fingerprint matches what `just test` will later need — without this, Windows MSVC fails with `os error 5` trying to re-link the running `pnpm-registry.exe`.
### Crates.io name reservations (from the original scaffold commit)
- [`pnpm-registry`](https://crates.io/crates/pnpm-registry) — published from this repo
- [`pnpm-registry-cli`](https://crates.io/crates/pnpm-registry-cli) / [`pnpm-registry-server`](https://crates.io/crates/pnpm-registry-server) — placeholder stubs, name reservation only
|
||
|
|
e549cd1cf1 |
perf(pacquet): no-op short-circuit when node_modules is up to date (#11904)
* perf(pacquet): no-op short-circuit when node_modules is up to date Adds a fast-path gate in `Install::run` that mirrors upstream pnpm's `validateModules` + `allProjectsAreUpToDate` shortcut: when the frozen-lockfile dispatch is eligible, `.modules.yaml` agrees with the current config, and `<virtual_store_dir>/lock.yaml` is byte-equal to the wanted lockfile, skip materialization entirely. The install emits the `name: "pnpm" / level: "info"` "Lockfile is up to date, resolution step is skipped" log, refreshes the workspace-state timestamp so `pnpm run`'s `verifyDepsBeforeRun` doesn't fire spuriously, and returns. Closes #11899. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet): tighten short-circuit test assertions Address review feedback: - Use `r#"..."#` raw-string for the up-to-date log assertion message so dylint's `perfectionist::prefer-raw-string` lint stops flagging the escaped quotes inside it. - Loosen the "up-to-date log fires" check to `any(|e| matches!(...))` so unrelated future `LogEvent::Pnpm` emits don't make the test brittle. - Swap `Path::exists()` for `std::fs::symlink_metadata().is_err()` on the "link: dep not materialized" assertion so a dangling symlink (which `exists()` reports as `false`) wouldn't sneak past. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
a456dc78fb | fix(list): limit manifest reads for large workspaces (#11692) | ||
|
|
572842a039 |
fix(installing.commands): clarify "loose mode" wording in minimumReleaseAge log (#11763)
* fix(installing.commands): clarify "loose mode" wording in minimumReleaseAge log The log line printed when pnpm auto-adds entries to `minimumReleaseAgeExclude` referred to internal "loose mode" terminology, which doesn't appear in the docs and isn't discoverable. Point users at the actual setting name they need to flip. Closes #11747 * Update installing/commands/src/policyHandlers.ts Co-authored-by: Zoltan Kochan <z@kochan.io> * fix(installing.commands): name the value in minimumReleaseAgeStrict log hint Change "set minimumReleaseAgeStrict to gate these updates with a prompt" to "set minimumReleaseAgeStrict to true to ..." so the value is explicit. --------- Co-authored-by: shiminshen <16914659+shiminshen@users.noreply.github.com> Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
9a3207367d | chore: update pnpm and pacquet | ||
|
|
bcbc008f2d | fix: temporarily disabling pacquet for release v11.3.0 | ||
|
|
6316e7b275 |
fix(deploy): skip configDependencies in the nested install (#11895)
* fix(deploy): skip configDependencies in the nested install The deploy directory never installs configDependencies, so the install engine they designate (e.g. pacquet) isn't on disk to invoke. Without this override, `pnpm deploy` crashes with `ENOENT: ... lstat '<deployDir>/node_modules'` when the workspace declares pacquet under `configDependencies`. * test(deploy): cover deploy with pacquet in configDependencies Reproduces the ENOENT crash that happens when `deployFromSharedLockfile` forwards the workspace's `configDependencies` (e.g. pacquet) into its nested install and the install engine tries to spawn from `<deployDir>/node_modules/.pnpm-config/`. * test(deploy): clarify the public-registry comment in the pacquet deploy test |
||
|
|
74a219eac6 |
fix(pacquet/modules-yaml): write GVS-aware virtualStoreDir to match pnpm (#11896)
* fix(pacquet/modules-yaml): write GVS-aware virtualStoreDir to match pnpm
Under `enableGlobalVirtualStore: true`, upstream pnpm mutates
`virtualStoreDir` in place at `extendInstallOptions.ts:419-422` so
every consumer that reads `ctx.virtualStoreDir` — including
`writeModulesManifest` and the `pnpm:context` debug log — sees the
GVS-derived `<storeDir>/v11/links` path.
Pacquet kept `Config::virtual_store_dir` at its project-local value
(deliberately, see `apply_global_virtual_store_derivation`'s rationale)
and wrote that field straight into `.modules.yaml` and the context
log. With `pnpm install` delegating to pacquet via `configDependencies`,
every run came back through pnpm's `checkCompatibility`, the recorded
project-local `virtualStoreDir` didn't match the GVS-mutated value
pnpm computed, and per-importer purges fired the
"modules directories will be reinstalled from scratch" prompt on
every install.
Route both externally-visible consumers through a new
`Config::effective_virtual_store_dir` helper that returns
`global_virtual_store_dir` when GVS is on (which already encodes
"user pinned or fall back to `<storeDir>/links`" via
`apply_global_virtual_store_derivation`) and the project-local
`virtual_store_dir` otherwise. Pacquet's internal layout consumers
still read the field directly — the divergence the helper bridges
is only at the parity boundary.
Test pins both halves: `.modules.yaml` round-trips to
`<storeDir>/v11/links` under GVS, and the `pnpm:context` event
reports the same path.
* fix(pacquet/store-dir): build modules_yaml expected path with Path::join so the test passes on Windows
`modules_yaml_serialized_store_dir_carries_store_version` (added in
|
||
|
|
f2a4d2caef | chore(release): 11.3.0 (#11894) | ||
|
|
3b62f9da31 |
feat(publish): add --skip-manifest-obfuscation flag for pack/publish (#11393)
* feat(publish): add preserve-manifest-fields option * fix(publish): omit pnpm field when preserveManifestFields is enabled The preserve-manifest-fields option was deep-cloning the entire manifest, which leaked the pnpm-specific `pnpm` field into packed/published manifests. The PR description explicitly calls for this field to remain stripped; align the implementation, tests, help text, and changeset accordingly. * refactor(publish): rename preserve-manifest-fields to skip-manifest-obfuscation The original name implied the flag preserves *all* manifest fields, which isn't true — the pnpm-specific `pnpm` field is still stripped, and `publishConfig` / workspace-protocol / catalog rewriting still happen. The flag is really an escape hatch from pnpm's manifest mangling, so name it that way. Help text and changeset updated to match. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
cdceebc2ab | chore: update pacquet to v0.2.7 (#11893) | ||
|
|
155af87585 |
fix(env-installer): prune env lockfile when updating a config dep (#11892)
`pnpm add --config <pkg>` (via `resolveConfigDeps`) wrote the env lockfile without pruning, so optional subdependencies from the previously resolved version remained as orphans. Mirror the prune call from `resolveAndInstallConfigDeps`. |
||
|
|
3209c2510c |
fix(pacquet/store-dir): append STORE_VERSION to storeDir so pnpm and pacquet stay interchangeable (#11891)
* fix(store-dir): append STORE_VERSION to storeDir so pnpm and pacquet stay interchangeable pnpm's `getStorePath` appends `STORE_VERSION` (`"v11"`) to whatever the user configured, so the `.modules.yaml` it writes records the v11-suffixed path. pacquet stored the suffix only as an internal sub-path accessor (`StoreDir::v11`), which meant `config.store_dir.display()` — the value pacquet writes to `.modules.yaml`, prints from `pacquet store path`, and emits in the NDJSON `context` log — yielded the un-suffixed parent. Switching between the two CLIs in the same project tripped pnpm's `checkCompatibility` with `ERR_PNPM_UNEXPECTED_STORE`. Fix is centralised in `From<PathBuf> for StoreDir`, mirroring pnpm's `if (endsWith(v11)) return; else append(v11)` branch at store/path/src/index.ts:39-42. Every consumer reading from `StoreDir` (`display()`, `root()`, `files()`, `tmp()`, `links()`, `projects()`) now sees the v11-suffixed path through one source of truth, so the on-disk layout is unchanged and the externally-reported `storeDir` matches pnpm's exactly. Ref: https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L39-L42 * fix(store-dir): satisfy Perfectionist macro-trailing-comma on remaining multi-line assert * fix(store-dir): enforce STORE_VERSION suffix on deserialize via #[serde(from)] CodeRabbit flagged that the previous `#[serde(transparent)]` derive on `StoreDir` deserialised straight into `StoreDir::root`, bypassing the auto-append in `impl From<PathBuf> for StoreDir`. A persisted unsuffixed path would therefore violate the [`STORE_VERSION`] invariant on the live struct until the next reconstruction. Pacquet doesn't currently deserialize `StoreDir` from any disk shape, but the type-level guarantee is part of the public contract — future serialised state must round-trip through the suffix logic. Route both directions through `PathBuf` with `#[serde(from = "PathBuf", into = "PathBuf")]`. Deserialize now flows through `From<PathBuf>` (which applies the suffix); serialize converts to `PathBuf` and back to the same wire shape `transparent` produced, so no on-disk format change. `Clone` is required by `into` and was added. Also fix CodeRabbit's doc-comment nit at project_registry::register_skips_when_store_is_inside_project — the comment referenced `StoreDir::from` while the test calls `StoreDir::new`; clarified that `new` routes through `From<PathBuf>`. Added round-trip tests in `store_dir::tests`: - `deserialize_applies_store_version_to_unsuffixed_path` - `deserialize_preserves_already_suffixed_path` |
||
|
|
e0bd879dea |
fix(deps-resolver): restore index-based pairing so git/tarball deps aren't dropped (#11890)
PR #11711 switched updateProjectManifest and the catalog-update loop in resolveDependencies to look up wantedDependencies by alias, but parseWantedDependency returns `{ alias: undefined, bareSpecifier }` for inputs like `pnpm/foo#sha` or tarball URLs whose alias is only known after fetching the package's package.json. Those entries collided under the `undefined` Map key, so the alias-keyed lookup of the resolved dep returned undefined, the filter dropped them from specsToUpsert, and they silently disappeared from the manifest update and pendingBuilds. This restored the index-based pairing the code used before #11711. catalog: preservation isn't affected: it's driven by rdd.catalogLookup.userSpecifiedBareSpecifier in the spec object, not by how wantedDep is looked up. The premise in the removed comment ("linked deps like workspace:* are excluded from directDependencies") was also wrong — linked deps stay in directDependencies with isLinkedDependency: true, they're not dropped. Restores building/commands/test/build/index.ts: rebuilds dependencies, rebuilds specific dependencies, rebuild with pending option. |
||
|
|
4bcc268be8 | chore: update node.js used for local development (#11889) | ||
|
|
ae42a7adc1 |
fix: preserve catalog: protocol references on upgrade (#11711)
* fix: preserve catalog: protocol references on upgrade (issue #11658) * refactor: address review feedback on catalog: preservation fix - Fix typo in 3 test assertions (`@pnpm.e2e.foo` → `@pnpm.e2e/foo`) that made `.toBeFalsy()` pass vacuously - Use `Map` for alias→wantedDependency lookup in `updateProjectManifest` to match the pattern in `index.ts` --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
22cb743672 |
feat: implement native 'pnpm repo' command (#11505)
* feat: implement native 'pnpm repo' command * fix(deps.inspection.commands): preserve repository.directory in fetchPackageInfo `fetchPackageInfo` flattened `repository` to its URL string, dropping `directory`. `pnpm repo <pkg>` therefore couldn't append the monorepo subdirectory for registry packages even though `pickRepoUrl` supported it. Keep the original repository value so the URL builder receives both `url` and `directory`. Also add the missing changeset for the `pnpm repo` command. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
d55263fff5 |
feat(pkg-manifest): add native set-script command with ss alias (#11504)
* feat: add native set-script command with ss alias * refactor(pkg-manifest): host set-script and wire it into the CLI - Move set-script into @pnpm/pkg-manifest.commands (drops the orphan @pnpm/pkg.commands package; pkg/* is not in the workspace). - Use readProjectManifest from @pnpm/cli.utils so package.json5 and package.yaml are updated in place instead of growing a stray package.json. - Remove set-script from notImplemented and register the command in pnpm/src/cmd/index.ts. - Cover the ss alias and the multi-word command path in tests. * refactor(set-script): share the pkg-set primitive Replace direct manifest.scripts mutation with setObjectValueByPropertyPath - the same primitive pkg-set uses. Reuses the prototype-pollution rejection for free and keeps the two commands on the same write path. Avoids the pkg-set string-CLI's first-equals key/value split, so script names containing '=' work too. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
d7da112eea |
feat(pkg): implement native pnpm pkg command (#11512)
Implements `pnpm pkg` natively with `get`, `set`, `delete`, and `fix` subcommands. Workspace usage follows pnpm conventions: use `-r` / `--recursive` for all selected workspace projects, and `--filter` to narrow the selected project graph. This does not add npm-style `--workspace` or `--workspaces` flags. The PR also extends `@pnpm/object.property-path` with safe set/delete helpers used by the command. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
389dae8382 |
ci: fix zizmor ref-version-mismatch on action-gh-release (#11888)
The dependabot bump to v3.0.0 updated the pinned commit hash but left the trailing version comment as v2.5.0. |
||
|
|
3d143854c0 |
fix(exec.commands): fall back to alias as bin name when dlx slot lacks package.json (#11886)
`getBinName` reads the installed package's `package.json` out of the GVS slot to discover the bin name. On CI this read has been failing intermittently for `node@runtime:24.6.0` with `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` — the dlx install reports `added 1, done`, but the slot the symlink points at has no `package.json`. The bin link itself is fine (pnpm creates it from the resolution's `bin` info, not from the slot's manifest), so the only casualty is `getBinName`. The slot can end up without `package.json` when something populated it without going through pnpm's `appendManifest` synthesis (or pacquet's runtime-manifest synthesis equivalent) — runtime archives don't ship their own `package.json`, so the synthesized one is the only way it gets there. Pacquet's `import_indexed_dir` short-circuits on existing slots without checking which files are present, so a slot populated by an older code path stays incomplete. Catch the manifest-not-found error and fall back to the scopeless package name. For single-bin packages that match `manifest.bin` (the common case for `pnpm dlx <pkg>`, including every `runtime:` spec), this gives the same answer the manifest would. Multi-bin packages already require `--package=<spec> <bin>` to disambiguate, which short-circuits `getBinName` upstream and never enters this branch. |
||
|
|
7a5cb92f80 |
chore(cargo): bump assert_cmd from 2.2.1 to 2.2.2 (#11853)
Bumps [assert_cmd](https://github.com/assert-rs/assert_cmd) from 2.2.1 to 2.2.2. - [Changelog](https://github.com/assert-rs/assert_cmd/blob/master/CHANGELOG.md) - [Commits](https://github.com/assert-rs/assert_cmd/compare/v2.2.1...v2.2.2) --- updated-dependencies: - dependency-name: assert_cmd dependency-version: 2.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
43e06bb2ae |
fix(pacquet): accept string libc in PackageMetadata (#11880)
|
||
|
|
508e6d800b |
feat: add pnpm stage command (#11863)
* feat: add npm stage command
* fix: correct stage command edge cases
* fix: handle stage error paths
* refactor: address stage command review feedback
- Type stageId via OtpPublishResponse so publishPackedPkg no longer needs a cast.
- Hoist fetchFromRegistry + auth header into a per-subcommand StageContext.
- Send npm-auth-type: web on all stage requests, not just approve/reject.
- Consolidate stageRequest / stageRequestWithOtp / stageJsonRequest into (context, params) form.
* fix: trigger stage OTP flow on web-auth challenges
The registry responds to stage approve/reject with 401 and a body of
`{ authUrl, doneUrl }` when the user must complete a browser-based
authentication, but `www-authenticate` does not contain "otp" in that
case. The previous check missed this and surfaced the response as a
generic STAGE_REGISTRY_ERROR. Detect web-auth responses by body shape so
withOtpHandling can drive the polling flow.
* test: cover stage approve web-auth detection paths
Add tests that lock in the OTP-trigger detection on the stage command:
- 401 with `{authUrl, doneUrl}` enters the web-auth flow, exercised here
via the full polling-completion path (registry returns a token, the
retry request carries it as npm-otp).
- 401 with web-auth body but no TTY surfaces as OTP_NON_INTERACTIVE.
- 401 without any OTP signals stays a STAGE_REGISTRY_ERROR so we don't
over-trigger the OTP flow on unrelated unauthorized responses.
* fix: keep web-auth and OTP wrapper on stage publish
Calling `context.publish()` directly for staged publishes bypassed
`publishWithOtpHandling`, so users without a preconfigured token had no
path through the browser-based authentication flow on `pnpm stage
publish`. Route the staged publish through the same wrapper as the
regular publish; `OtpPublishResponse.stageId` carries the registry's
identifier when set.
* refactor: split stage into per-subcommand files and lift tarball helpers
The 631-line `stage.ts` carried six subcommands, tarball parsing,
auth/request plumbing, error class, OTP detection, and rendering helpers
in one file. Reorganized to match the existing `publish/` folder layout:
- `stage/{index,help,publish,list,view,approve,reject,download,context,
request,parsing,rendering,errors,types}.ts` — one concept per file.
- `tarball/{publishSummary,summarizeTarball}.ts` — shared between
`publish` and `stage` instead of duplicated. `PublishSummary` and
`extractBundledDependencies` now live with the tarball helper rather
than inside the publish subfolder, so other commands can reuse them
without reaching into `publish/`.
Behavior unchanged. Also dropped `StageRegistryError`'s redundant
`statusCode` field (was identical to `status`) to bring it in line with
`FailedToPublishError`.
* chore: add TOTP and unparseable to cspell dictionary
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
|
||
|
|
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`. |