mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-20 04:32:26 -04:00
d08e9abfb9dc1d3c8f8a5104f7e56b46db94c337
11537 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
c068720dec |
fix(pacquet): shorten long virtual store dirnames to avoid ENAMETOOLONG (#11768)
* fix(pacquet): shorten long virtual store dirnames to avoid ENAMETOOLONG Peer-heavy snapshot keys (e.g. vitest with a dozen browser / coverage / DOM peers) produced flat-name directories that overflowed macOS's 255- byte filename limit, so `install` aborted with errno 63 before unpacking any tarballs. Port the trailing length / case-shortening branch of upstream's `depPathToFilename` (deps/path/src/index.ts:169) so the name becomes `<prefix>_<32-hex-sha256>` capped at `virtualStoreDirMaxLength` bytes (default 120). Extract `create_short_hash` and `shorten_virtual_store_name` into a new `pacquet-crypto-hash` crate mirroring upstream `@pnpm/crypto.hash`; `pacquet-lockfile`, `pacquet-registry`, and `pacquet-store-dir` all consume it instead of duplicating the sha2 + truncate logic. Reported via pnpm/pacquet issue triage (vitest@4.1.6 peer suffix). * fix(pacquet): taplo format and remove broken intra-doc link Format `pacquet/crates/crypto-hash/Cargo.toml` per the workspace `.taplo.toml` (aligns the `[package]` keys) and downgrade the `pacquet_modules_yaml::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH` reference in `PkgNameVerPeer::to_virtual_store_name` to plain text, since `pacquet-lockfile` deliberately does not depend on `pacquet-modules-yaml` and `RUSTDOCFLAGS=-D warnings` rejected the unresolved intra-doc link. * feat(pacquet/config): expose virtualStoreDirMaxLength Add `virtual_store_dir_max_length: u64` to `Config` with default 120 (matching `pacquet_modules_yaml::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH`). Wire it through `WorkspaceSettings.virtualStoreDirMaxLength` and the `PNPM_CONFIG_VIRTUAL_STORE_DIR_MAX_LENGTH` env-overlay so users can override the threshold via `pnpm-workspace.yaml`, global `config.yaml`, or environment variables — mirroring upstream `Config.virtualStoreDirMaxLength`. The three flat-name call sites (`install_without_lockfile.rs`, `install_package_from_registry.rs`, `virtual_store_layout.rs`) and the `.modules.yaml` writer now read the configured value instead of the hardcoded constant. `VirtualStoreLayout::legacy` takes the value as an explicit second arg so test fixtures don't silently inherit a default. |
||
|
|
e5e7b7241d |
fix(env-installer): suppress 'Installing config dependencies...' on no-op installs (#11766)
* fix(env-installer): only print "Installing config dependencies..." when work is actually being done Previously the message was emitted unconditionally for every config dependency, before any of the "do we need to fetch / re-symlink?" checks. As a result the banner printed on every install even when everything was already cached and correctly linked. Emit the started event lazily — at most once per install, and only when an orphan is being removed, a parent or subdep needs fetching, a parent symlink needs (re)creating, or orphan subdep siblings are being pruned. --- Written by an agent (Claude Code, claude-opus-4-7). * test(env-installer): assert installing-config-deps events fire only when work happens Captures `streamParser` events around `resolveAndInstallConfigDeps` to verify the lazy emission introduced in the previous commit: - fresh install emits both `started` and `done`, - a follow-up no-op install emits neither, - removing a config dep still emits `started` (orphan cleanup work). --- Written by an agent (Claude Code, claude-opus-4-7). * test(env-installer): subscribe to streamParser once at module load `streamParser` is a `split2` Transform stream that buffers writes until the first 'data' listener attaches and then drains the whole buffer into it. Subscribing per-test made the new install-config-deps test capture events from every earlier test in the file. Move the subscription to module load and have each test drain the accumulated events around its own call. Also drop the "removal" assertion: `resolveAndInstallConfigDeps` does not prune entries that disappear from the configDeps argument (lockfile pruning happens at a higher layer), so the scenario it claimed to test never actually fired the orphan-cleanup path. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(env-installer): emit started when only the sibling symlink needs relinking If a config dep's optional subdep is already cached in the global virtual store but the sibling symlink under the parent's node_modules is missing or points at a stale target, symlinkDir() does real work without reportStarted ever firing. Check whether the link already points at the expected target and only fire reportStarted + symlinkDir when it doesn't, mirroring the parentSymlinkAlreadyCorrect path. Also clean up the test-level streamParser listener in afterAll so the subscription doesn't outlive the test file. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
ef87f3ccff |
test(pnpm): group release-brittle tests under a shared describe block (#11767)
Three tests resolve the running pnpm version's integrity from registry-mock,
which proxies pnpm to npmjs. They fail every release between the version
bump commit and the matching npm publish ("No matching version found for
pnpm@<version>"), then pass again once the version lands on npmjs. Group
them under a 'release-brittle' describe in each file so the failure mode
is obvious from the test name and only stated once.
|
||
|
|
097983fbca |
feat(pacquet): wire NpmResolver into install; fix(pick-registry) unscoped npm-alias routing (#11760)
Two changes ship together: the bulk is the pacquet refactor described in #11756, plus a TypeScript-side fix to `@pnpm/config.pick-registry-for-package` that surfaced during review. ### Pacquet — wire NpmResolver into install (Phases A/B/C of #11756) - **Phase A.** New `parse_bare_specifier.rs` and `npm_resolver.rs` in `pacquet-resolving-npm-resolver`. `NpmResolver` implements the `Resolver` trait: parses the bare specifier (including npm-alias `npm:@scope/name@<spec>` and tarball-URL forms — with prefix-anchored name validation), picks a version via `pick_package`, surfaces `minimumReleaseAge` violations inline via `detect_min_release_age_violation`. `workspace:` specs decline so the chain falls through. `published_by` / `published_by_exclude` / `dry_run` added to `ResolveOptions`. - **Phase B.** `install_without_lockfile.rs` constructs an `NpmResolver` at install entry from the config-derived registries map and an `InMemoryPackageMetaCache` that's shared across the resolve pass and dropped before the install pass. - **Phase C.** New `pacquet-resolving-deps-resolver` crate exposes `resolve_dependency_tree`: a flat `name@version`-keyed package map with parent-child edges, concurrent sibling resolution via `try_join_all`, per-id dedup gate. `install_package_from_registry.rs` no longer calls `Package::fetch_from_registry` / `Package::pinned_version`; it takes a pre-resolved `ResolveResult` and reads tarball URL + integrity off `LockfileResolution::Tarball`. Additional behaviors landed during review: - **`minimumReleaseAge` policy in the resolve pass.** Previously only enforced by the lockfile-verification gate; the no-lockfile resolve pass now derives `published_by` and the exclude policy from `Config` so resolver-time picks match the configured policy. - **`SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER` surfaces correctly.** `resolve_dependency_tree` now returns a typed error when the chain returns `Ok(None)` — silently dropping the edge would leave installs missing transitive deps. Mirrors upstream's `default-resolver` error shape. - **Per-package progress events.** `InstallPackageFromRegistry` takes a `first_visit: bool`; `pnpm:progress resolved` / `pnpm:progress imported` plus the tarball download fire once per `(name, version)`, while the per-parent `symlink_package` runs on every edge. Matches upstream's per-package (not per-edge) reporter contract. - **Windows symlink race fix.** `ResolvedPackages` is now `DashMap<String, watch::Sender<bool>>`; the first writer signals completion after `import_indexed_dir`, so a second visitor's `symlink_package` (which may fall back to a Windows junction requiring an existing target) doesn't race ahead of the materialization. A dropped first-writer task surfaces as a typed `FirstWriterAborted` error. - **Scope routing.** `pick_registry_for_package` is now bareSpecifier-aware so an entry like `"foo": "npm:@acme/bar@^1"` routes through `registries[@acme]`. ### TS — `@pnpm/config.pick-registry-for-package` unscoped-target fix A separate bug surfaced during the scope-routing port: `pickRegistryForPackage('@private/foo', 'npm:lodash@^1')` was routing through `registries['@private']`, even though `lodash` is unscoped and doesn't live on the `@private` registry. `getScope` now returns `null` in the npm-alias branch when the alias target is unscoped (instead of falling through to the local pkgName's scope). Changeset is in `.changeset/pick-registry-unscoped-npm-alias.md` (patch bump for `@pnpm/config.pick-registry-for-package` and `pnpm`). Added matching tests on both the TS and pacquet sides. ### Out of scope (left as #11756 follow-ups) - Preferred-versions harvesting from the lockfile (Phase D). - Install-side aggregation of `policy_violation` from the tree (Phase E) — the resolver attaches them per-pick already, but the install layer doesn't yet collect or fail on them. - Other-protocol resolvers (git, tarball, workspace, jsr, named-registry, …) — `NpmResolver` is the only chain entry today; once a second resolver lands, `DefaultResolver` will get wired in too. - Full `parseBareSpecifier.test.ts` corpus port — the parser tests pacquet ships cover the cases the install path exercises; remaining corpus items land alongside Phase F. Closes part of #11756. |
||
|
|
0fb723323f | chore(release): 11.2.0 (#11764) v11.2.0 | ||
|
|
4f56179d48 |
chore: update pnpm-lock.yaml (#11761)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
df77f649ee |
fix(pacquet/fs): serialize concurrent CAS writes to the same path (#11758)
* fix(pacquet/fs): serialize concurrent CAS writes to the same path
Two install snapshots whose tarballs ship identical file content
(e.g. a shared LICENSE across sibling packages like
`@pnpm.e2e/hello-world-js-bin` and `@pnpm.e2e/hello-world-js-bin-parent`)
hash to the same CAS path and call `ensure_file` concurrently. Without
serialization the second writer's `O_CREAT|O_EXCL` hits `AlreadyExists`
while the first writer is mid-`write_all`, falls into
`verify_or_rewrite`'s `meta.len() != content.len()` arm, and runs
`write_atomic` — which renames a temp file over the live source.
On Linux/ext4 the partial-size observation is fast enough that this
window opens often, surfacing as flaky CI failures of the form:
failed to import "<store>/v11/files/65/<hash>" to ".../LICENSE":
No such file or directory (os error 2)
emitted by `link_file` whose `reflink`/`fs::hard_link` raced the
rename. macOS/APFS and Windows tests pass because their `stat` cadence
and CI runner parallelism don't reliably open the window.
Port pnpm v11's `locker: Map<string, number>` semantics — slightly
stronger in pacquet, as a per-path `Mutex` rather than a dedup cache —
so two writers of the same CAS path serialize through it. The second
caller acquires the lock after the first writer's `write_all` has
finished, then takes the byte-match fast path inside `verify_or_rewrite`
and never has to rewrite. The previous docstring at the bottom of the
"Differences from pnpm" list explicitly acknowledged the locker
omission and predicted it would matter; this change closes that gap.
Add a regression test (`concurrent_writers_of_same_path_do_not_swap_the_inode`)
that fires 32 threads at one path with identical content and asserts
the inode never swaps — the observable signal that no writer ever took
the `write_atomic` rename path.
* fix(pacquet/fs): unlink intra-doc references to private items
`ensure_file` is public; references to private `verify_or_rewrite`,
`write_atomic`, and `cas_write_lock` only resolve under
`--document-private-items`, which trips `rustdoc::private-intra-doc-links`
under `-D warnings`. Drop the link form and keep the names as plain
backticked identifiers — the docstring still reads correctly and
`cargo doc` no longer fails.
* style(pacquet/fs): rename single-letter N to WRITER_COUNT in test
Address review feedback on #11758 — single-letter constants are
opaque; `WRITER_COUNT` reads as the loop count of concurrent
writers the test fires at one CAS path.
* docs(pacquet/fs): tighten ensure_file / cas_write_lock / test docs
Address review feedback on #11758: drop body-narrating prose, keep
only the contract and the non-obvious why. The test docstring no
longer references "pre-fix code" so it reads independent of this
PR's history.
* test(pacquet/fs): pre-create + per-thread inode capture in concurrent test
Address CodeRabbit's and Copilot's review feedback on #11758. The
previous assert compared two metadata reads taken at the same moment
after join — tautological. Pre-creating the file gives an
`original_ino` reference taken before the contended run, and each
writer also captures the path's inode immediately after its own
`ensure_file` returns so a mid-run rename swap from another writer
is visible to at least one of those observations.
* style(pacquet/fs): add trailing comma in multi-line assert! macro
Address `perfectionist::macro_trailing_comma` warning surfaced by
the Dylint CI on #11758 — multi-line macro invocations must end with
a trailing comma. (My earlier local `dylint --all` run missed this
because the perfectionist library hadn't fully recompiled against the
new test code.)
* test(pacquet/fs): drop pre-create from concurrent writer test
Address Copilot review feedback on #11758. The pre-create made every
contender take the byte-match fast path against a complete file, so
the lock made no observable difference and the test couldn't even
weakly distinguish lock from no-lock. Removing it leaves the
fresh-dirent shape (`O_CREAT|O_EXCL` race + verify_or_rewrite for
the rest), and the per-thread inode observations can now diverge
under a multi-rename race without the lock.
Honest about the limitation in the docstring: any observation taken
after `ensure_file` returns has already missed the rename window, so
a single-rename race converges on one inode and slips past. The
test catches the multi-rename case and validates the "no deadlock,
all writers see correct content" baseline.
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
3a54205178 | feat(pacquet): resolver scaffold + npm version picking (#11755) | ||
|
|
ced20cbe71 |
fix(config/reader): only sync registries.default to registry when workspace contributes it (#11754)
The sync introduced in #11744 unconditionally overwrote the unnormalized registry value parsed from .npmrc with the normalized registries.default, causing a trailing slash to appear in config.registry when the user only configured a registry in .npmrc. Restrict the sync to cases where pnpm-workspace.yaml actually contributes a default registry different from what .npmrc provided. |
||
|
|
04a2c9c610 |
test(outdated): add regression test for minimumReleaseAge (#11699)
Adds a regression test for #11698. The underlying fix already shipped as part of #11705 (which removed `strictPublishedByCheck` entirely and routed maturity decisions through `policyViolation`), so this PR now lands only the dedicated test that locks in the behavior. ## What the test covers `deps/inspection/commands/test/outdated/minimumReleaseAge.test.ts`: - **Baseline** — without an age policy, `pnpm outdated` reports `is-negative@2.1.0` as an available upgrade (sanity check that the fixture actually has outdated deps). - **Regression** — with `minimumReleaseAge` set to a cutoff so far in the past that every published version is immature, `pnpm outdated` reports nothing: `exitCode === 0` and `2.1.0` does not appear. Before #11705 this test went red because the non-strict resolver fallback re-picked the immature `latest` ignoring `publishedBy`. The `allImmatureMinimumReleaseAge = Date.now() / (60 * 1000)` trick (cutoff = epoch in minutes) is date-independent and matches the technique already used in the install-side `minimumReleaseAge` suite. ## Why a test-only PR The original PR proposed flipping `strictPublishedByCheck` in `createManifestGetter`, but #11705 deleted that option entirely and replaced it with an always-defer model (`policyViolation` flows through `ResolveResult` → `getManifest` returns `null` on `MINIMUM_RELEASE_AGE_VIOLATION`). The test was the durable contribution; preserving it as a regression gate is worth keeping. |
||
|
|
3687b0e180 |
fix(config/reader): resolve relative cafile path against the .npmrc directory (#11726)
* fix(config/reader): resolve relative cafile path against the .npmrc directory `cafile=<relative-path>` in `.npmrc` was being read via `fs.readFileSync`, which resolves relative paths against `process.cwd()`. When pnpm is invoked from a different cwd than the project (e.g. `pnpm --dir <project> install` in CI wrappers and monorepo scripts), the CA file silently failed to load: the `try/catch` in the loader dropped the CA list, the install proceeded without the configured CA, and the user only saw TLS errors against a private registry — with no log line tying back to the wrong path. Resolve relative `cafile` values in `readAndFilterNpmrc` against `path.dirname(filePath)` of the .npmrc that declared the key, before `loadCAFile` reads the file. Absolute paths (the dominant CI shape) and CLI `--cafile` are unchanged. Ref: #11624 * refactor(config/reader): tighten cafile-fix comments The test name and the linked issue already describe the failure mode, so the 4-line preamble on the test and the 5-line in-line comment on the implementation were re-narrating what the tests document. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet/config): resolve relative cafile path against the .npmrc directory Pacquet's `NpmrcAuth::from_ini` used to store the `cafile=` value verbatim and pass it to `std::fs::read_to_string` at apply time. A relative path therefore resolved against the process cwd, so a project `.npmrc` containing `cafile=certs/ca.pem` reached via `pacquet --dir <proj>` from a different cwd silently failed to load the CA — same failure mode as pnpm/pnpm#11624 on the TypeScript side, which the parent commit fixed by resolving against `path.dirname` of the `.npmrc`. Mirrors the parent commit on the pacquet side: - `NpmrcAuth::from_ini` now takes the directory the `.npmrc` was loaded from. A relative non-empty `cafile=` value is resolved against that directory via `npmrc_dir.join(...)`; empty and absolute values pass through unchanged. - `Config::current` tracks which of `start_dir` / home dir actually provided the `.npmrc` text and passes that path through. - The `load_cafile` doc comment that documented "matches pnpm's surprising cwd-resolution behavior" is gone; that caveat was current only as long as pnpm itself had the bug. - Existing tests updated mechanically to pass `Path::new("")` for the new parameter; four new tests cover the resolution branches (relative resolves, absolute passes through, empty passes through, end-to-end load via `apply_to` with a real tempdir-based fixture). --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet/config): add trailing commas inside multi-line assert_eq! macros `perfectionist::macro-trailing-comma` is enforced via dylint at CI and ran clean before the cafile port. Rustfmt reflowed two `assert_eq!` calls in `parses_strict_ssl_true_and_false` onto multiple lines when the `Path::new("")` argument made the line too long, but did not add the trailing comma the dylint rule wants on the last macro argument. --- Written by an agent (Claude Code, claude-opus-4-7). --------- Co-authored-by: shiminshen <16914659+shiminshen@users.noreply.github.com> Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
a62055786b |
fix: handle minimumReleaseAge policy violations in global installs (#11753)
* fix: handle release-age policy in global installs * refactor: dedupe global policy-callback wiring Collapse setupPolicyHandlers + createResolutionPolicyManifestUpdater into one createGlobalPolicyCallbacks helper used by both global add and global update entry points. --- Written by an agent (Claude Code, claude-opus-4-7). --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
9cb48bb27b |
fix: injectWorkspacePackages crashes — lean resolution defense-in-depth + lifecycle re-import (#11662)
Fixes two **independent** crashes hitting `pnpm install --frozen-lockfile` on workspaces with `injectWorkspacePackages: true` (or `dependenciesMeta.*.injected`), surfaced via `turbo prune --docker` pipelines. ## Bug 1 — peer-variant snapshot missing `resolution` (lean, defense-in-depth) A peer-variant injected workspace snapshot (`@scope/pkg@file:packages/pkg(peerA@1)(peerB@2)`) inherits its `resolution` from the base `packages:` entry (`@scope/pkg@file:packages/pkg`). When a tool prunes the lockfile and drops that base entry, readers that deref `pkgSnapshot.resolution` crash with the cryptic: ``` Cannot use 'in' operator to search for 'directory' in undefined ``` **The root cause is upstream of pnpm**: the pruner (e.g. `turbo prune`) emits an internally inconsistent lockfile. Fixed at the source in **vercel/turborepo#12825** (retain the base entry for peer-variant injected deps; minimal repro in **vercel/turborepo#12824**) — empirically verified to produce a correct pruned lockfile for a real multi-service workspace. **pnpm side (this PR): one lean normalization at the read layer** — in `convertToLockfileObject`, where base→variant inheritance already happens via `Object.assign`. When the base entry is absent, reconstruct the directory resolution from the `file:` depPath. This is *reconstruction, not guessing*: for a workspace `file:` dep the directory **is** the depPath suffix — exactly what pnpm's own writer emits. It is **defense-in-depth, not load-bearing**: with a well-formed lockfile (turbo#12825 or any correct input) the branch never fires. Because the normalization sits at the single shared read layer, it also covers the sibling `Cannot use 'in' operator … 'integrity' in undefined` on the `pnpm deploy` path (same `resolution === undefined` root, different deref site). Per review feedback: the earlier per-reader `inheritOrSynthesizeResolution` helper across 5 call sites is **removed**; normalization lives in exactly one place (`convertToLockfileObject`), and the readers are back to `main`. ## Bug 2 — lifecycle re-import wipes `.bin/<tool>` (pure pnpm; the substantive fix) `runLifecycleHooksConcurrently` re-imports an injected workspace package into its targets after `prepare`/`postinstall`. The 2022 `scanDir`-into-`filesMap` workaround (#4299) fed target-internal paths to `importPackage`; once #11088 made `importIndexedDir`'s `makeEmptyDir` fast path the default, that path wipes the target's `node_modules` before copying, so the re-import dies with `ERR_PNPM_ENOENT` on `node_modules/.bin/<tool>`. Fix: drop the `scanDir` workaround and pass `keepModulesDir: true` so `importIndexedDir` skips the destructive fast path and preserves the target's existing `node_modules` (bin symlinks + transitive deps) via its staging/move path. Stays on `storeController.importPackage`, so source files keep their **hardlinks** (no copy-loop regression). Net reduction vs `main`: the `scanDir` helper and the `node:fs` / `FilesMap` imports are removed. ## Tests - The `deps-restorer` regression fixture `peer-variant-missing-resolution` **omits the base `packages:` entry**, so it encodes the actual pruned shape and reproduces the crash on `main`: reverting the `convertToLockfileObject` change yields `resolution: undefined` for the peer-variant (→ the `lockfileToDepGraph` crash); with this PR it is reconstructed as `{ type: 'directory', directory: … }`. - A `lockfile.fs` unit test pins the heuristic boundary: a directory resolution is synthesized for a pruned `file:` peer-variant but **never** for a `file:` tarball. - A `deps-installer` regression test covers the Bug 2 re-import (injected dep with a `prepare` script + a bin-having dependency). ## Validation End-to-end on a real `injectWorkspacePackages` monorepo (`turbo prune --docker` → `pnpm install --frozen-lockfile`), on services that crash on **both** bugs with stock pnpm: - pnpm with both fixes: the crashing services build. - **vercel/turborepo#12825 + pnpm with only Bug 2** (Bug 1 fully reverted): the crashing services still **build** → confirms Bug 1 here is genuine defense-in-depth and turbo#12825 owns the root cause. - Bug 2 reproduces on stock pnpm regardless of turbo (it is purely pnpm's importer fast-path). Pairs with **vercel/turborepo#12825** (Bug 1 root cause; minimal repro **vercel/turborepo#12824**). Tracks #11663. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> Co-authored-by: Eyalm321 <eyal@sunsationsusa.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: UApply Developer <developer@uapply.ai> |
||
|
|
d1b340f3fe |
fix: synchronize default registry from pnpm-workspace.yaml for login/logout commands (#11744)
Closes #10099 |
||
|
|
56f3851904 |
feat(auth): implement pnpm login --scope (#11727)
The online documentation has long listed `pnpm login [--registry <url>] [--scope <scope>]`,
but the CLI only ever accepted `--registry`. `pnpm login --scope foo` errored with
`Unknown option: 'scope'` and there was no way to associate a scope with a registry
through the login command — users had to edit `.npmrc` by hand.
This change wires `--scope` through:
- `cliOptionsTypes` / `rcOptionsTypes` declare the option.
- `help()` documents it.
- After the token is written, `@<scope>:registry=<registry>` is written into the same
auth file (`~/.config/pnpm/auth.ini`), which `config.reader` already accepts as a
source of `@scope:registry=` mappings.
- Scope normalization: `--scope foo` and `--scope @foo` both produce `@foo`. Blank
values (`""`, `" "`, `"@"`) are treated as unset rather than writing a broken
`@:registry=` entry.
Ref: #11716
Co-authored-by: shiminshen <16914659+shiminshen@users.noreply.github.com>
|
||
|
|
64afc9233e |
fix(publish): honor publishConfig access (#11746)
* fix(publish): honor publishConfig access * test(publish): cover publishConfig access * test(publish): handle invalid metadata json * test(publish): replace verdaccio-dependent access tests with unit test Verdaccio strips the top-level `access` field from publish payloads, so the metadata-fetching integration tests could never pass. Replace them with a direct unit test of the access-resolution logic in createPublishOptions. --- Written by an agent (Claude Code, claude-opus-4-7). --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
0a40f64b54 |
fix(pacquet/config): read global config.yaml and PNPM_CONFIG_* env vars (#11752)
* fix(pacquet/config): read global config.yaml and PNPM_CONFIG_* env vars Pacquet's config loader only read `pnpm-workspace.yaml` (plus the auth/network subset of `.npmrc`). Two sources pnpm v11 reads were missing: - `<configDir>/config.yaml` (e.g. `~/.config/pnpm/config.yaml`, `~/Library/Preferences/pnpm/config.yaml`, `%LOCALAPPDATA%/pnpm/config/config.yaml`). Now loaded between `.npmrc` and `pnpm-workspace.yaml`, with workspace-only keys (`nodeLinker`, `hoist`, `lockfile`, …) filtered out to mirror upstream's `isConfigFileKey` allowlist at `index.ts:299-309`. - `PNPM_CONFIG_*` / `pnpm_config_*` env vars across the whole schema (pacquet previously only honored `NPM_CONFIG_WORKSPACE_DIR`). Now read for every `WorkspaceSettings` field and applied after `pnpm-workspace.yaml` so env vars win — matching upstream's `parseEnvVars` loop at `index.ts:471-488`. Closes the scenario in pnpm/pnpm#11738: a user with `enableGlobalVirtualStore: true` in `~/.config/pnpm/config.yaml` running an install in a project whose `pnpm-workspace.yaml` doesn't repeat the setting now lands in the global virtual store, same as pnpm. Note: the original issue proposed reading the full setting surface from `.npmrc`, `~/.pnpmrc`, `~/.config/pnpm/rc`, and `npm_config_*` env vars. Those proposals are anti-parity — pnpm v11 reads none of them for non-auth/non-network settings (see `isNpmrcReadableKey` at `localConfig.ts:197-198` and the `pnpm_config_*`-only prefix filter in `env.ts:185-195`). Implementing them would diverge from pnpm. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet/config): address PR review + perfectionist dylint - Don't clobber a `virtualStoreDir` set in global `config.yaml` when a `pnpm-workspace.yaml` is present but doesn't redeclare the field. Guard the workspace-root re-anchor on `virtual_store_dir_explicit`. Adds a regression test (CodeRabbit review on #11752). - Drop `parse_tri_array`'s `"null"` shortcut. Pnpm's `Array` schema feeds the env value through `JSON.parse` and then requires `Array.isArray`, so `PNPM_CONFIG_HOIST_PATTERN=null` is silently rejected upstream — accepting it here would diverge from pnpm. Test updated to assert rejection (Copilot review on #11752, but the suggestion direction had to be inverted for parity). - Rename single-letter parameter / generic names (`s`, `T`, `p`) to satisfy perfectionist dylint's `single-letter-*` rules; add trailing comma to a multi-line macro invocation. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
036b64e1c3 |
fix(pacquet/config): default publicHoistPattern to [] to match pnpm v11 (#11751)
Pacquet's default `public_hoist_pattern` was `["*eslint*", "*prettier*"]`, but pnpm v11 defaults to `[]`. Any project that ran `pacquet install` ended up with a `.modules.yaml` whose `publicHoistPattern` differed from what pnpm computes on the next invocation, and the follow-up `pnpm add` / `pnpm update` / bare `pnpm install` aborted with `ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF` from `validateModules`. Mirror pnpm v11's default exactly so `.modules.yaml` round-trips cleanly across the two CLIs. Closes #11750. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
b206a15395 |
feat(installing): delegate fetch / import / link to pacquet when configured (#11734)
When `configDependencies` declares pacquet (under either the unscoped `pacquet` or the scoped `@pnpm/pacquet` alias), pnpm delegates the fetch / import / link / build phases of an install to the pacquet Rust binary. Pnpm keeps owning dependency resolution — pacquet's resolver isn't ready yet — and hands pacquet a freshly-written lockfile to materialize. Covered install shapes: - frozen install (`tryFrozenInstall` → pacquet, no resolve needed) - default isolated `nodeLinker` (`installInContext`: lockfileOnly resolve via JS, then pacquet) - hoisted `nodeLinker` (same resolve-then-materialize shape) - workspace partial install (subset of workspace projects mutated) - agent-server install (`@pnpm/agent.client` resolves, pacquet materializes) ```yaml # pnpm-workspace.yaml configDependencies: "@pnpm/pacquet": "^0.2.0" # or unscoped `pacquet` ``` ## How it works - `installing/commands/src/runPacquet.ts` resolves the platform binary via `createRequire(realpath(.pnpm-config/<name>/package.json))` — same algorithm the upstream wrapper uses, but skipping the JS shim's extra Node startup. - Pacquet's NDJSON stderr is forwarded through `@pnpm/logger`'s global `streamParser` so `@pnpm/cli.default-reporter` renders its events the same way it renders pnpm's own. Non-JSON stderr lines pass through verbatim. - A few pnpm-side log emits (`importing_done` placeholder, `pnpm:summary`) are suppressed when pacquet will take over so the reporter doesn't close streams or lock in empty diffs before pacquet's real events arrive. Pacquet's duplicate `pnpm:progress status:resolved` events are filtered on the resolve-then-materialize paths so the reporter doesn't double-count. - `installing/deps-installer/src/install/index.ts` gates the delegation on a `runPacquet?: () => Promise<void>` callback in `StrictInstallOptions`. The CLI layer in `installing/commands/src/installDeps.ts` constructs the callback, threaded through both the single-project and workspace-recursive paths. - The `pacquet` and `@pnpm/pacquet` npm packages ship the same JS shim from `pacquet/npm/pacquet/scripts/generate-packages.mjs`; per-platform binaries stay under the existing `@pacquet/<plat>-<arch>` scope and aren't duplicated. |
||
|
|
1627943d2a |
feat(outdated): include node, deno, and bun runtimes (#11739)
`pnpm outdated` and `pnpm update --interactive` previously skipped runtime dependencies (`node`/`deno`/`bun` installed via the `runtime:` protocol). Both commands go through `outdatedDepsOfProjects` → `outdated()`, and the inner loop bailed out for anything `parseBareSpecifier` couldn't parse — which is everything `runtime:`-shaped. A runtime was only ever reported if the current install differed from the wanted lockfile entry, so the latest available version was never surfaced. The same gap silently affected `jsr:` and named-registry deps too. Commits, smallest fix first → progressively cleaner architecture: 1. **`feat(outdated)`** — minimal fix: special-case runtime deps in `outdated.ts` so they appear in the table and the interactive update picker. 2. **`refactor(outdated)`** — per-resolver dispatch. Each protocol resolver gets its own "what's the latest?" function; `@pnpm/resolving.default-resolver` composes them. 3. **`refactor(outdated)`** — rename to `resolveLatest` (the function returns info regardless of whether the dep is outdated; "outdated" described a state, not an action). 4. **`refactor(outdated)`** — let the local-resolver own the `link:`/`file:` skip, drop the matching short-circuit in `outdated.ts`. 5. **`refactor(outdated)`** — slim `LatestQuery` / `LatestInfo` to the bare essentials; move `pickRegistryForPackage` into the npm-resolver where it belongs; derive `current`/`wanted` display from `pkgSnapshot.version` in `outdated.ts`. 6. **`chore(outdated)`** — drop stale tsconfig project reference left behind by #5. 7. **`refactor(outdated)`** — drop `wantedRef` from the query; resolvers detect protocol from `bareSpecifier` alone. ## Final architecture `@pnpm/resolving.resolver-base` defines a single tiny protocol: ```ts interface LatestQuery { wantedDependency: WantedDependency compatible?: boolean } interface LatestInfo { latestManifest?: PackageManifest } type ResolveLatestFunction = (query: LatestQuery, opts: ResolveOptions) => Promise<LatestInfo | undefined> ``` - `undefined` from a resolver means "I don't claim this dep — try the next one." - `{}` means "I claim it, but I can't tell you what's latest" (policy-blocked, network unavailable, or a protocol with no concept of latest — git/tarball). - `{ latestManifest }` is the happy path. Each protocol resolver (npm/jsr/named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single first-match dispatcher, surfaced through `@pnpm/installing.client` as `createResolver(...).resolveLatest`. `outdated.ts` is protocol-agnostic: dispatches, then derives `current`/`wanted` display from `pkgSnapshot.version` (falling back to the raw ref for URL-shaped refs where the URL is the only diff signal between commits), uses raw `wantedRef !== currentRef` for the lockfile-shifted check, and pulls `packageName` from `dp.parse(relativeDepPath).name` so aliased deps still report under the real package name. Per-resolver responsibilities: - **npm-resolver** (`resolveLatestFromNpm` / `resolveLatestFromJsr` / `resolveLatestFromNamedRegistry`): match their respective spec shapes, call the matching `resolveFromX` with `'latest'` (or the original spec under `--compatible`), handle `MINIMUM_RELEASE_AGE_VIOLATION` and `ERR_PNPM_NO_MATCHING_VERSION` so policy-blocked deps don't surface as available updates. Picks the per-package registry internally via its ctx. - **node/bun/deno runtime resolvers**: claim deps via `bareSpecifier.startsWith('runtime:')` + alias match, query their release sources for the latest version (only the version — no asset-hash fetches), return `{ latestManifest }`. - **git / tarball resolvers**: claim deps via spec shape, return `{}` (no concept of "latest"); the caller still surfaces a ref-mismatch report if the lockfile shifted to a different commit/URL. - **local-resolver**: returns `undefined` so `link:`/`file:`/`workspace:` deps fall through and get silently skipped. |
||
|
|
9a8675d3f1 |
chore: update pnpm-lock.yaml (#11740)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
f77244feb7 |
refactor(pacquet/config): replace default callback with self (#11736)
Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
61e724cef3 |
ci(pacquet): pass --workspace --all-targets to clippy and check (#11735)
Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c83b002a7b |
refactor(pacquet): replace callback-DI with capability traits (#11730)
https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c8d8fde6ca |
feat(config-deps): support optionalDependencies with platform filtering (#11725)
Extends `configDependencies` to resolve and install one level of `optionalDependencies`, with `os` / `cpu` / `libc` platform filtering applied at install time. Closes the prerequisite called out in #11723: this is what makes the esbuild/swc-style platform-binary pattern viable for config dependencies (e.g. shipping pacquet as a config dep with native binaries via `optionalDependencies`). ### What lands - **Resolution** (`resolveOptionalSubdeps.ts`, wired into `resolveConfigDeps` and `resolveAndInstallConfigDeps`): after each top-level config dep resolves, walks one level of `optionalDependencies`, resolves each, and records them in the env lockfile with `os`/`cpu`/`libc` preserved. The parent's snapshot gets `optionalDependencies: { … }`. All variants are recorded regardless of host platform, so the env lockfile stays portable across machines. - **Install** (`installConfigDeps.ts`): after the parent is installed into its GVS leaf, fetches each platform-compatible subdep into its own GVS leaf and creates a sibling symlink inside the parent leaf's `node_modules/`. Node's `realpath`-based resolution then makes `require('pkg-platform-arch')` from inside the parent resolve correctly. Stale siblings are pruned, so platform changes between runs produce a clean layout. - **GVS hash** (new `calcGlobalVirtualStorePathWithSubdeps` in `graph-hasher`): the parent's GVS leaf hash now folds in the optional subdeps' full pkg ids. Without this, changing a subdep version while keeping the parent pinned would land in the same leaf and silently overwrite the sibling symlinks. The leaf function keeps its original "no children" contract; the new function is a separate entry point that pacquet can mirror cleanly. - **Re-install detection**: the "skip if already installed" check compares the existing `.pnpm-config/{name}` symlink's `realpath` against the expected GVS leaf, not the package.json's name/version. With subdep versions now feeding the leaf hash, name/version alone isn't sufficient. The check only short-circuits the parent's re-import and re-symlink — `installOptionalSubdeps` always runs so platform-specific siblings get pruned and relinked when the host's effective platform changes (Rosetta x64 ↔ arm64, etc.). - **Exact versions only**: subdep specifiers must be valid semver exact versions (e.g. `"1.2.3"`). Ranges (`"^1.0.0"`) and tags (`"latest"`) are rejected up-front with a `CONFIG_DEP_OPTIONAL_NOT_EXACT` error. With the parent pinned by integrity, the subdep's resolved version mustn't drift between machines. - **Error handling**: optional-subdep resolution failures are logged via `skippedOptionalDependencyLogger` with `reason: 'resolution_failure'` (same shape as `installing/deps-resolver`) and the install continues — except for `ERR_PNPM_TRUST_DOWNGRADE`, which is a security signal that must still abort the install. ### Scope Only one level deep. Transitive `dependencies` and lifecycle scripts remain unsupported — pacquet doesn't need them yet, and they carry meaningful security and complexity tradeoffs that deserve a separate discussion. The env lockfile schema needs no changes: `LockfilePackageInfo` already carries `os`/`cpu`/`libc`, and `LockfilePackageSnapshot.optionalDependencies` already exists for recording the parent→child edge. ## Known limitation If a workspace already had a resolved config dep in the env lockfile (`snapshots[pkgKey] = {}`) before this PR, optional subdeps won't be retroactively discovered on subsequent installs. Workaround: `pnpm update <pkg>` (or remove + re-add). In practice no published package today relies on `optionalDependencies` in a config dep — they couldn't, since the feature didn't exist — so the practical exposure is narrow. See the inline review thread for the design rationale. |
||
|
|
cddf522cfa |
feat(pacquet): port lockfile verification (minimumReleaseAge + trustPolicy) (#11729)
Ports pnpm's lockfile-verification gate to pacquet, tracking #11722. The gate re-applies the resolver's policy checks (`minimumReleaseAge`, `trustPolicy='no-downgrade'`) to every entry in `pnpm-lock.yaml` immediately after the lockfile is loaded, before any resolution or fetch happens — so a lockfile resolved elsewhere (committed to the repo, restored from CI cache, or generated by a tool that bypassed local policy) can't reach the filesystem under a weaker policy. Three new crates and four extended ones: - **`pacquet-resolving-resolver-base`** — `ResolutionVerifier` trait + types. - **`pacquet-resolving-npm-resolver`** — `NpmResolutionVerifier`, trust-rank logic, attestation fetcher, cached full-metadata fetcher, mirror helpers (path / IO). - **`pacquet-lockfile-verification`** — fan-out runner, in-memory lockfile hasher, JSONL stat-and-skip cache + `recordLockfileVerified` wrapper. - `pacquet-config` — new `cacheDir`, `minimumReleaseAge*`, `trustPolicy*` fields + `TrustPolicy` enum + `createPackageVersionPolicy`. Defaults match upstream byte-for-byte: `minimumReleaseAge` defaults to `1440` minutes (24 h) and `minimumReleaseAgeIgnoreMissingTime` defaults to `true`. - `pacquet-reporter` — `pnpm:lockfile-verification` channel. - `pacquet-registry` — enriches `Package` / `PackageVersion` with `time`, `_npmUser.trustedPublisher`, `dist.attestations`, `modified`. - `pacquet-package-manager::install` — wires the gate between lockfile load and the frozen-lockfile dispatch. Phase-by-phase landing order matches the plan in #11722. Ports against upstream pnpm at commit `2a9bd897bf`. |
||
|
|
dfd8fbf709 |
chore: update pnpm-lock.yaml (#11559)
* chore: update pnpm-lock.yaml * chore: update lockfile --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
e01d2bc0a1 |
fix(pacquet/fs): fall back to absolute target across Windows drives (#11721)
* fix(pacquet/fs): fall back to absolute target across Windows drives
`pathdiff::diff_paths` does not return `None` when the two paths sit on
different Windows drive roots; it produces a path that climbs out with
`..` and then re-anchors with the target's drive letter (for example,
`..\..\C:\Users\...`). Windows rejects such a symlink target with
`ERROR_INVALID_PARAMETER` (os error 87), which is the failure mode CI
on Windows hits when the workspace lives on `D:` and the global store
installed by `setup-pnpm` lives on `C:`.
Node.js's `path.win32.relative` returns the absolute `to` argument
when the two paths share no common root, which is the behavior
upstream `symlink-dir` relies on. Mirror that by detecting unequal
`Component::Prefix` values up front and returning the absolute target
unchanged. Drive-letter comparison is case-insensitive to match
NTFS and Node.js semantics.
* fix(pacquet/fs): normalize verbatim Windows paths before relative diff
`Prefix::Disk('C')` and `Prefix::VerbatimDisk('C')` name the same
physical drive, but compare unequal as `Component::Prefix` values, so
the cross-root guard added in the previous commit would unnecessarily
fall back to an absolute target whenever one input was a verbatim
`\\?\C:\...` path. Worse, `pathdiff::diff_paths` itself would emit a
re-anchored garbage path on the variant-mismatched case if the guard
ever let it through.
Run both inputs through `dunce::simplified` on Windows, which strips
the `\\?\` prefix when the non-verbatim form is still valid, so
mixed verbatim and non-verbatim views of the same drive collapse
onto a common `Prefix::Disk` before the comparison and the diff.
Tighten `case_normalize` to map `VerbatimDisk` onto `Disk` for the
residual long-path case that `dunce` declines to simplify.
* style(pacquet/fs): split relative_target_inner per platform
`clippy::needless_return` rejects the explicit `return` in the
Windows `{ ... }` block because the block itself is the function's
tail expression on Windows. The block-with-cfg-attribute layout
also obscured that the function body shrinks to a single tail
expression on non-Windows. Extracting two `#[cfg]`-gated
`relative_target_inner` helpers makes both targets read as a
straight tail expression and drops the clippy violation.
* fix(pacquet/fs): make same_path_root variant-strict
Collapsing `Disk` and `VerbatimDisk` (and the UNC analogues) onto a
common variant in `same_path_root` is unsafe when one input is a
verbatim path that `dunce::simplified` declined to strip (e.g., a
path long enough that the non-verbatim form would exceed the legacy
limit). `same_path_root` would declare the two paths same-root, but
`pathdiff::diff_paths` would still walk `Component::Prefix` for
equality and emit a re-anchored garbage path on the variant
mismatch — the same `..\\..\\C:\\Users\\...` shape that triggers
`ERROR_INVALID_PARAMETER` (os error 87) on Windows.
Restore the variant-strict comparison so mismatched variants fall
back to an absolute target, which is the correct outcome on the
edge case. The common short-path mixed-form case (`\\?\C:\foo`
paired with `C:\bar`) still produces a relative target because
`dunce::simplified` normalizes the variants upstream of this check.
* docs(pacquet): tests are documentation; do not narrate them in code
Add an explicit rule to AGENTS.md: when a behavioral scenario or
edge case is already captured by a test (name, setup, assertions),
do not re-narrate it in a doc comment on the implementation. The
doc comment should state the contract; the test demonstrates the
behavior.
Apply the rule to the recently added Windows symlink fix. The doc
comments on `relative_target_for` and `same_path_root` had grown
into mini essays restating the `ERROR_INVALID_PARAMETER` failure
mode, the CI scenario, and the `dunce::simplified` rationale,
all of which is covered by the three Windows tests. Trim both to
the contract plus one short note on the only non-obvious invariant
(why `same_path_root` is variant-strict). The tests in turn shed
their multi-paragraph preambles in favor of self-explanatory names
and direct assertions, keeping only a one-line regression pointer
on the CI-failure test.
* docs(pacquet/fs): describe same_path_root match shape accurately
The previous doc claimed "same UNC share" as a match condition, but
the implementation requires identical `Prefix` after `dunce::simplified`
with only drive-letter case folding. UNC server/share differences in
case or `UNC`/`VerbatimUNC` variant fall back to absolute.
|
||
|
|
bfa861fe04 |
refactor(pacquet/config): replace env mutations with DI pattern (#11718)
* refactor(pacquet/config): thread default_store_dir through the EnvVar DI seam Replaces process-environment mutation in the `default_store_dir` tests with the dependency-injection pattern established in pnpm/pacquet#339 and consolidated for the in-tree pacquet subtree in #11708. Tracks pnpm/pacquet#343 — the original issue still applied after pacquet was merged into this repo because the env-mutating tests came with it. The four affected unit tests (`test_default_store_dir_with_pnpm_home_env` and `test_default_store_dir_with_xdg_env` in `defaults::tests`, plus `should_use_pnpm_home_env_var` and `should_use_xdg_data_home_env_var` in `lib::tests`) now drive each branch with per-test unit structs that satisfy `EnvVar`. No `EnvGuard` snapshot, no `unsafe` block, no process-environment write. The `home_dir` and `current_dir` closures call `unreachable!` for branches the early `PNPM_HOME` / `XDG_DATA_HOME` returns short-circuit before consulting them, documenting the precondition the way the style guide's worked example does. `default_store_dir` is now generic over `Sys: EnvVar` and takes the `home_dir` and `current_dir` lookups as `FnOnce` closures, mirroring the shape of `Config::current`. A thin args-less wrapper `default_store_dir_host` wires the production `Host` provider together with `home::home_dir` and `env::current_dir` so the SmartDefault expression on `Config::store_dir` stays short. The `have_default_values` wiring assertion now compares against `default_store_dir::<Host>(home::home_dir, env::current_dir)` instead of the old args-less helper. `npmrc_auth::tests::ignores_non_auth_keys` no longer needs to hold the EnvGuard global lock against the two `Config::new()` snapshots it compares — nothing in the crate mutates `PNPM_HOME` or `XDG_DATA_HOME` anymore, so the env-derived `store_dir` is observed identically by both calls even under nextest's in-process parallelism. `EnvGuard` itself stays in `pacquet-testing-utils` because the proxy cascade and `NPM_CONFIG_WORKSPACE_DIR` tests in `lib::tests`, as well as out-of-crate users in `git-fetcher` and `executor`, still rely on it. Retiring it entirely is out of scope for this issue. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet/config): add Debug bound on default_store_dir's Error parameter The Windows branch of `default_store_dir` calls `current_dir().expect("current directory is unavailable")`, which requires `Error: Debug` on the `Result<PathBuf, Error>` returned by the `CurrentDir` closure. The original `where` clause didn't declare that bound, so the function compiled on Linux/macOS (where the Windows branch is `#[cfg(windows)]`-gated out) but failed on `Lint and Test (windows-latest)` once the cfg fired. All current callers pass `env::current_dir` (Error = `io::Error`) or pin `Error = std::io::Error` via turbofish on test fakes — both already satisfy `Debug` — so this is a pure type-bound fix with no behaviour or call-site change. --- Written by an agent (Claude Code, claude-opus-4-7). * docs(pacquet): add shared-process-state DI exception to the style guide The "Dependency injection for tests" gating list in `pacquet/CODE_STYLE_GUIDE.md` enumerated four reasons to reach past real fixtures for the DI seam: filesystem error kinds, deterministic time, external-service happy paths, and unreachable-by-design preconditions. It did not cover the case this PR retired from `default_store_dir`: tests that mutate a single per-process slot (env vars, cwd, umask, signal handlers, …) and so race with every other test in the same process. The pre-existing `EnvGuard` workaround restored correctness only by holding a binary-wide mutex around `unsafe { env::set_var(...) }`, which forecloses parallelism inside the affected tests and leaves `unsafe` in the test source. A capability-trait fake keeps the read deterministic and the mutation contained to the test that needs it. Adds that bullet to the style guide between "Deterministic time" and "External-service happy paths," and updates rule 7 in `pacquet/AGENTS.md` (and its `CLAUDE.md` / `GEMINI.md` symlink targets) so the summary mirrors the longer list. --- Written by an agent (Claude Code, claude-opus-4-7). * docs(pacquet): drop unfounded set_current_dir example from DI guidance The previous commit cited `set_current_dir` and "the umask" as illustrative examples in both the style guide and the AGENTS rule 7 summary. Neither is actually used anywhere in the pacquet codebase today — only `env::set_var` is. Speculative examples in a style guide invite contributors to chase patterns that don't exist, so narrow the wording to the one slot we have a concrete example for while keeping the principle generalisable (the bullet still says "any future analogue"). --- Written by an agent (Claude Code, claude-opus-4-7). * docs(pacquet): revert example narrowing on the shared-process-state bullet Reviewer feedback: keep the broader `set_current_dir` / umask / signal-handler / global-allocator examples in the DI gating bullet and rule 7 summary. They're not used in the codebase today, but the analogues are useful as forward-looking illustrations so the next contributor recognises the same shape when it comes up. Restores the wording introduced in 96040d7; reverses the narrowing in |
||
|
|
471b59025d |
chore: update pnpm to v11.1.3 (#11719)
* chore: update pnpm to v11.1.3 * fix: lock pacquet version |
||
|
|
cd80b2c8ae | chore(release): 11.1.3 (#11717) v11.1.3 | ||
|
|
2a9bd897bf |
perf: record locally-resolved lockfile in verification cache (#11714)
The lockfile verification cache currently only records the lockfile that exists at the **start** of an install. So a flow like:
```
pnpm install <pkg>
rm -rf node_modules
pnpm install
```
re-runs the per-package registry round-trip against the newly written lockfile, even though the local resolver already enforced the policy when picking those versions. The fresh lockfile is now recorded immediately after each install-time write, so the second install takes the cache fast path.
## Implementation
### Recording the post-resolution lockfile
- New helper `recordLockfileVerified` (in `installing/deps-installer/src/install/`). Gated on `cacheDir` + non-empty `resolutionVerifiers` — same gate the pre-resolution verifier uses.
- Two thin combiners over the lockfile writers: `writeWantedLockfileAndRecordVerified` and `writeLockfilesAndRecordVerified`. The install paths use these so the record always runs alongside the write.
### Hash stability: writer returns the canonical lockfile
The cache stores `hashObject(LockfileObject)` and the next install computes the same hash off the file it loads from disk. For the hashes to match, both ends must compute over structurally identical objects. They don't, naïvely: the in-memory write object can carry `undefined` optional fields (e.g. `settings.dedupePeers = undefined` from `opts.dedupePeers || undefined` in install code) that YAML drops on serialize — `object-hash` treats undefined vs missing as distinct values.
- `writeWantedLockfile` / `writeLockfiles` (in `@pnpm/lockfile.fs`) now return the canonical post-write `LockfileObject`: `convertToLockfileObject(stripUndefinedDeep(lockfileFile))`. The strip walks the existing object graph in memory rather than going through a `yaml.load` round-trip, so non-cache callers (deploy, deps-restorer, make-dedicated-lockfile, agent server) pay near-zero cost.
- Install hooks hash the writer's returned value, not the raw in-memory input. Guaranteed by construction to match what the next reader produces.
### `useGitBranchLockfile` correctness
The pre-resolution verification gate and the new post-write recorder were both keying cache records on a hard-coded `pnpm-lock.yaml`. Under `useGitBranchLockfile` the actual file is `pnpm-lock.<branch>.yaml`, so the stat shortcut hit `ENOENT` and the cache effectively never engaged for git-branch users. Both sites now resolve the real filename via `getWantedLockfileName`. The wrappers compute it once and pass it to the writer via a new optional `lockfileName` opt so `useGitBranchLockfile` installs don't fork `getCurrentBranch` twice per write.
### Bug fix unrelated to the cache, found during review
`writeLockfiles`' differs branch was deciding whether to remove or keep `node_modules/.pnpm/lock.yaml` based on `isEmptyLockfile(wantedLockfile)`. Filtered-current callers (deps-restorer) pass an empty current against a non-empty wanted, so this could leave a stale current lockfile on disk. Fixed to key off the current.
### Comments policy
`AGENTS.md` (and `pacquet/AGENTS.md`) now spell out the comment defaults: write self-documenting code, do not restate at call sites what the callee's JSDoc / doc comment already says, comments are reserved for the non-obvious *why*. The pruning pass in this PR brings the changed code in line.
## API surface
- `@pnpm/lockfile.fs` (minor):
- `writeWantedLockfile`: return widened from `Promise<void>` to `Promise<LockfileObject>`. New optional `lockfileName` opt.
- `writeCurrentLockfile`: return widened to `Promise<LockfileObject | undefined>` (undefined when the empty-lockfile branch unlinks).
- `writeLockfiles`: return widened from `Promise<void>` to `Promise<{ wantedLockfile, currentLockfile }>`. New optional `wantedLockfileName` opt. New exported `WriteLockfilesResult` type.
- New export: `getWantedLockfileName`.
- `@pnpm/installing.deps-installer` (patch): internal-only wrappers; no external API change.
|
||
|
|
e2a3c68309 |
chore(pacquet/dylint): upgrade perfectionist to 0.0.0-rc.15 (#11709)
Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
4a79336473 |
feat: report lockfile verification progress (#11712)
* feat: report lockfile verification progress The lockfile resolution verifier introduced in #11705 runs an unbounded registry round-trip on cache miss and was previously silent — on a cold registry cache users saw nothing for several seconds. Emit pnpm:lockfile-verification log events (started/done) around the actual verification pass and render them in the default reporter as a transient progress line that collapses into a final "verified" summary with entry count and elapsed time. The cached short-circuit stays silent. * feat: include lockfile path in verification log and render when non-standard Add `lockfilePath` to the `pnpm:lockfile-verification` event payload so consumers always know which lockfile a `started`/`done` pair refers to. In the default reporter, render the path in the message only when the lockfile lives outside the workspace root (or, for non-workspace installs, outside cwd) — the common case stays uncluttered, while custom `lockfileDir` setups now surface in the verification line. * feat: name what the lockfile verification actually checks in the rendered message "Verifying lockfile" was opaque about *what* was being verified. Reword the rendered messages to explicitly name the check ("supply-chain policies"), so users on a cold-cache pause understand what's happening instead of just seeing the pause. * fix: skip lockfile verification emission for empty candidate set A non-empty lockfile.packages whose snapshots all fail name/version extraction would still emit a "Verifying lockfile (0 entries)" line even though no verifier work runs. Bail before emission when the candidate map is empty so the no-op branch stays silent, matching the contract for the other no-op branches (empty verifiers, no lockfile.packages). * fix(reporter): always close out the verifying-lockfile frame Address two Copilot review points on #11712: 1. The verifier emitted `started` but no terminal event when violations were found or when the registry fan-out threw, leaving "Verifying lockfile…" as the last frame for that block in ansi-diff mode (and an unmatched line in CI logs). Add a `failed` status to the logger, wrap the fan-out in try/finally so a terminal event is emitted on every exit path that emitted `started`, and render a brief failure line so the spinner-style frame is replaced before the PnpmError block prints. 2. The path-suppression heuristic used strict `===` between path.dirname(lockfilePath) and expectedDir, which broke on trailing separators and slash-direction differences. Switch to a path.relative-based check so a workspaceDir like `/repo/` or a Windows path with mixed slashes still correctly suppresses the redundant "at <path>" suffix. * docs: update lockfile verification logging behavior The lockfile verifier now emits log events during the registry round-trip pass, improving user visibility into the process. |
||
|
|
a671dda173 |
test(pacquet): do not tolerate the lacking of tools (#11713)
* test(git-fetcher): drop skip_if_no_{git,npm} guards
Replace runtime probe-and-skip helpers with the existing `.unwrap()`
calls — if `git`, `node`, or `npm` is missing the test will fail
loudly rather than silently return. Tolerance defeats the purpose of
testing: an under-provisioned environment is a bug in the environment,
not something the suite should paper over. Git is ubiquitous and
Node.js is a documented prerequisite for building pnpm.
Codify the rule in pacquet/AGENTS.md so future ports don't reintroduce
the pattern. Platform-locked tools are still allowed to gate via
`#[cfg(...)]` or `#[cfg_attr(..., ignore = "...")]`, which surface in
the test report instead of disappearing.
---
Written by an agent (Claude Code, claude-opus-4-7).
* docs(test-porting): drop stale skip_if_no_npm note
The TEST_PORTING.md entry for fetcher_runs_prepare_script_when_allowed
still advertised the deleted `skip_if_no_npm` helper as the recommended
pattern. Reword it to match the new AGENTS.md rule: rely on the existing
`.unwrap()` calls and let missing tools fail the test loudly.
Spotted by Copilot review on #11713.
---
Written by an agent (Claude Code, claude-opus-4-7).
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
e74a6b7e48 |
test(pacquet): coverage (#11710)
* test: fill coverage holes across lockfile, package-manifest, fs, and friends
Adds ~44 unit and integration tests to close the easier targets in the
coverage analysis at #339-style boundaries. Touches:
- `lockfile::resolved_dependency`: cover `as_alias` / `ver_peer` for
non-matching variants, alias / parse error variants, `TryFrom<Cow>`,
serialize-alias / serialize-link, `From<PkgVerPeer> / From<PkgNameVerPeer>`,
and a Display vs. `String` round-trip.
- `lockfile::freshness`: cover the plural arms of `SpecDiff::Display`
and the comma separator inside the removed/modified loops.
- `lockfile::pkg_name`: cover `TryFrom<String>` and `TryFrom<Cow>` happy
and empty-input paths.
- `lockfile::snapshot_dep_ref`: cover `ver_peer` and `From<PkgVerPeer>`.
- `lockfile::save_lockfile`: cover the `CreateDir` / `RemoveFile` /
`RenameFile` error classifications by planting a regular file or
directory where the writer expects a file or a writable path.
- `registry::package`: cover `PartialEq`, `latest`, and `pinned_version`
happy and no-match paths.
- `graph-hasher::engine_name`: cover `detect_node_version` /
`detect_node_major` when `node` is on PATH; skip cleanly otherwise.
- `graph-hasher::object_hasher`: cover the null / bool / array arms of
the bytestream serializer.
- `patching::apply`: cover non-NotFound read-patch error, partial-delete
non-empty result, unsupported rename/copy operation, and `Create`
with an unwritable parent.
- `workspace-state`: cover the `CreateDir` / `ReadFile` / `ParseJson`
error variants.
- `package-manifest`: cover `from_path` ENOENT, `add_dependency` on a
non-object field, `safe_read_package_json_from_dir` non-NotFound IO
error, and `convert_engines_runtime_to_dependencies` for unsupported
shapes.
- `fs::file_mode`: cover `EXEC_MASK` / `EXEC_MODE` constants,
`is_executable` for all positions, and `make_file_executable` on Unix.
- `executor`: cover `execute_shell` happy and non-zero-exit paths.
- `cli/tests/run.rs`: new integration tests for `pacquet run`,
including argument forwarding and `--if-present`.
Also bumps `hex_decode` in `graph-hasher::tests` to use
`is_multiple_of` so test-time clippy stays clean under Rust 1.95.
https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL
* style(package-manifest): drop trailing comma in single-line `assert!`
Dylint's perfectionist::macro-trailing-comma rule rejected the single-
line `assert!` formatting I'd accidentally inherited from the multi-
line shape — remove the trailing comma so the lint clears.
https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL
* test(graph-hasher): make `node` a hard prerequisite for detect tests
The two `detect_node_*` tests previously skipped silently when `node`
was missing from `PATH`. `node` is a documented prerequisite of the
test suite (see `pacquet/CONTRIBUTING.md`'s setup section), so a
missing binary is a test-env bug to surface, not a condition to paper
over. Switch the `let Some(...) = ... else { return }` skips to
`expect` so a missing `node` fails loudly with a clear message.
https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL
* test(cli,patching): switch fixtures to `json!` and `text_block_fnl!`
Addresses six review comments on #11710:
- `cli/tests/run.rs`: the three `package.json` fixtures used
`format!("{{ ... }}", marker = …)` with literal-brace escapes,
which is awkward and brittle when path strings contain
JSON-special characters. Switched to
`json!({...}).to_string()` so serde handles escaping and the
shape reads as JSON, not as a brace-escaped template.
- `patching/src/apply/tests.rs`: the three patch fixtures I
added used `"\<newline>...\n"` raw strings. Converted them to
`text_block_fnl!`, matching the convention the rest of the
workspace (lockfile, package-manager, testing-utils) uses for
multi-line fixture text. The `_fnl` variant keeps the trailing
newline that git-diff parsers expect.
Pre-existing fixtures in the same file (`IS_POSITIVE_PATCH`,
etc.) were not touched — keeping the scope to the lines
reviewers flagged.
https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL
* style(registry): use `assert_ne!` over `assert!(lhs != rhs)`
Idiomatic Rust and produces a better failure message when the
assertion trips. Caught during a self-review pass over #11710.
https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL
* test: address zkochan + Copilot review feedback on #11710
- Move executor + fs::file_mode unit tests from inline modules
to the project's standard `src/<parent>/tests.rs` layout. The
convention is documented in `CODE_STYLE_GUIDE.md` under "Unit
test file layout"; zkochan flagged the violation in the
executor file.
- Gate `pacquet-executor::tests` on `cfg(all(test, unix))` at
the declaration site so the `use super::execute_shell` import
isn't dead on Windows. Copilot flagged that
`clippy --tests --deny warnings` would fail there.
- Drop `execute_shell_propagates_nonzero_exit_as_ok`. Copilot
flagged that it regression-pinned a behavior that *should*
change: pnpm/npm `run` exits with the script's status, but
pacquet's wrapper currently returns `Ok(())` regardless. A
fix belongs in its own PR; until then we should not add tests
that block that fix.
- Quote the temp-path redirect targets in the `cli/tests/run.rs`
shell fixtures so a tempdir path containing a space
(`/var/folders/...` on macOS) doesn't split the `touch` /
redirect argument. Copilot flagged the unquoted paths.
The `fs::file_mode::make_file_executable_sets_exec_bits` test
keeps `#[cfg(unix)]` at the test site (and moves its
`make_file_executable` import inside) rather than gating the
whole `tests` module — the other two tests
(`exec_constants_pin_pnpm_layout`,
`is_executable_matches_any_exec_bit`) are platform-neutral and
should still run on Windows.
Copilot's concern about `detect_node_version`'s `expect` (in
`engine_name.rs`) is intentionally not addressed: KSXGitHub
asked for the hard-fail in
|
||
|
|
4195766f10 |
feat: tighten minimumReleaseAge — auto-exclude, lockfile verification, and interactive prompt (#11705)
Three coordinated changes that close the silent-bypass gap in loose `minimumReleaseAge` mode AND the discover-by-loop UX problem in strict mode (#10488), plus a parallel hardening of the lockfile verifier: 1. **Auto-collect into `minimumReleaseAgeExclude` (loose mode)** — fresh resolutions that fall back to a version newer than the cutoff are auto-recorded into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists what was persisted. The workspace manifest writer dedupes against existing entries. 2. **Lockfile verifier runs in loose mode too** — `createNpmResolutionVerifier` no longer gates on `minimumReleaseAgeStrict`. With auto-collect keeping the exclude list explicit, every accepted-immature pin must be on the list — same contract strict mode enforces. Lockfiles produced under a weaker (or absent) policy that still hold immature entries are rejected the same way strict mode would. 3. **Strict mode prompts on the aggregate set instead of throwing on the first** — the resolver always collects every immature direct and transitive in one pass; the install command's `handleResolutionPolicyViolations` checkpoint decides what to do with the set. Interactive (TTY) prompts the user once with the full list (default = No) and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues, persisted at the end. Decline → resolution aborts before the lockfile, package.json, or modules dir is touched. Non-interactive (CI) keeps `ERR_PNPM_NO_MATURE_MATCHING_VERSION` as the exit code but lists every offending entry instead of just the first one the resolver happened to hit. 4. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). The steady-state flows: - **Loose mode, `pnpm add foo@immature`**: lockfile clean, verifier no-op, resolver picks via lowest-version fallback, `foo@immature` lands in `minimumReleaseAgeExclude`, install succeeds. Subsequent `pnpm install --frozen-lockfile` in CI verifies against the populated list and succeeds. - **Strict mode (interactive), security bump to `next@15.5.9`**: resolver collects `next@15.5.9` AND every immature `@next/swc-*@15.5.9` shim. pnpm prompts once with the full list. User approves → install completes, all entries persisted in `pnpm-workspace.yaml`. CI then runs the populated config cleanly. - **Strict mode (non-interactive / CI)**: aborts with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing every immature entry's `name@version` and publish time — no more discover-by-loop dance. - **Teammate commits a poisoned lockfile**: single-policy batches reject with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` (or `ERR_PNPM_TRUST_DOWNGRADE`); a batch that trips both policies escalates to the generic `ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION` and lists each entry's per-policy code in the breakdown. ### Implementation - The npm resolver always falls back to the lowest matching version when no mature version satisfies the range, and flags the result with `ResolveResult.policyViolation` instead of throwing `NO_MATURE_MATCHING_VERSION`. `deferImmatureDecision` and `strictPublishedByCheck` are gone — every caller (install, dlx, outdated, self-update) inspects the violation and decides what to do. - `policyViolation` flows from `ResolveResult` → `PackageResponse.body.policyViolation` → a shared accumulator in `ResolutionContext` → the `resolutionPolicyViolations` field on `resolveDependencyTree`'s return → out through `mutateModules` / `addDependenciesToPackage` to the install command. - The violation type lives in `@pnpm/resolving.resolver-base` as `ResolutionPolicyViolation`; the npm resolver exports the two built-in codes (`MINIMUM_RELEASE_AGE_VIOLATION_CODE`, `TRUST_DOWNGRADE_VIOLATION_CODE`) as constants so consumers reference one source of truth. - `handleResolutionPolicyViolations` runs between `resolveDependencyTree` and `resolvePeers` — the resolver-agnostic checkpoint where the install command's plan prompts (TTY) or aborts (no-TTY) with the full violation list. - `setupPolicyHandlers` (in `installing/commands/src/policyHandlers.ts`) composes per-policy handlers behind a uniform plan interface: each handler has its own `handleResolutionPolicyViolations` (filter by code, decide what to do) and `pickManifestUpdates` (return a typed `WorkspaceManifestPolicyUpdates` patch the install command spreads into `updateWorkspaceManifest`). Today the only registered handler is `createMinimumReleaseAgeHandler` — strict + TTY prompts via `enquirer`, strict no-TTY throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` with every entry listed, loose mode auto-persists at the tail. Strict + `--no-save` is rejected up-front via `ERR_PNPM_STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE`. Future policies plug in via a sibling factory + push into the handlers list, with no changes to `installDeps.ts` / `recursive.ts`. - `installDeps` / `recursive` drain `pickManifestUpdates` after install and spread the patch into `updateWorkspaceManifest`. Plain `pnpm install` (no `--update`, no params) now still updates the workspace manifest when any handler contributes a patch. The `install` command's CLI schema gained `save: Boolean` so `--no-save` actually flows through to `opts.save = false` instead of being silently dropped by nopt. - `makeResolutionStrict` (in `installing/client`) wraps a `ResolveFunction` and rethrows any `policyViolation` as a `PnpmError`. Used by `dlx` and `self-update` under strict `minimumReleaseAge` OR `trustPolicy: 'no-downgrade'`, since one-shot callers have nowhere to defer a violation to. Violation-code → error-code mapping lives in one place so future violation kinds get consistent UX. - `createNpmResolutionVerifier` extends its check to `trustPolicy: 'no-downgrade'` — same per-entry fan-out, same cache key, sharing the full-metadata fetch with the maturity check. Trust-fetch errors now propagate up so the violation reason carries the underlying message (network code, 404 detail) instead of a generic "metadata is unavailable". - `verifyLockfileResolutions`'s aggregate throw uses the per-policy code when every violation in the batch shares it, and escalates to a generic `LOCKFILE_RESOLUTION_VERIFICATION` (with per-entry codes in the breakdown) for mixed batches. - The pnpm agent path refuses installs under `trustPolicy: 'no-downgrade'` (`ERR_PNPM_TRUST_POLICY_INCOMPATIBLE_WITH_AGENT`) — the agent has no server-side counterpart to that check yet, so silently allowing it would land a lockfile the local verifier would later reject. `minimumReleaseAge` is forwarded to the agent and enforced server-side, so that combination is fine. ### Pacquet parity Pacquet only carries a stub reference to `minimumReleaseAgeExclude` (see `pacquet/crates/package-manager/src/version_policy.rs`); the broader `minimumReleaseAge` and `trustPolicy` policies aren't ported yet, so this feature is outside pacquet's current surface area. It'll come along when pacquet ports the policies. ### Closes - Closes #10488 (resolves the discover-by-loop dance for security bumps without needing `withTransitives`). |
||
|
|
02f8138f13 |
refactor(pacquet): optional DI pattern + documentation (#11708)
Co-authored-by: Claude <noreply@anthropic.com> |