Files
pnpm/installing
shiminshen e7e99f04e4 fix: do not crash when a catalog specifier is a range (#11706)
## Summary

`pnpm update --recursive --lockfile-only <pkg>@<version>` crashed with
`Invalid Version: <range>` when the catalog entry for `<pkg>` was a range
(e.g. `^21.2.10`) and `catalogMode` was `strict` or `prefer`. This is the
exact command Renovate's pnpm artifact updater runs; monorepos using
`catalog:` with any range specifier were blocked from Renovate-driven
lockfile updates.

**Root cause:** in `installSome`, the catalog-match short-circuit guards
`semver.eq(wantedDep.bareSpecifier, catalogDepSpecifier)` with
`semver.validRange` on both sides. `validRange` returns truthy for ranges,
but `semver.eq` constructs `new SemVer(...)` internally and throws on a
range.

**Fix:** use `semver.valid` instead of `semver.validRange` on both sides of
the equality guard. Range specifiers now fall through to the existing
mismatch handling (`CatalogVersionMismatchError` in `strict` mode,
warn-and-use-direct in `prefer` mode) instead of crashing. Behavior for
concrete-on-both-sides is unchanged.

Closes #11570

## Behavior after the fix

This turns a crash into pnpm's normal catalog-mismatch handling; it does
**not** make a strict-mode update succeed when the catalog is a range:

- **`catalogMode: strict`** — rejects with `ERR_PNPM_CATALOG_VERSION_MISMATCH`
  (clean, actionable error instead of a stack trace).
- **`catalogMode: prefer`** — warns and uses the direct version.
- **concrete-vs-concrete** — unchanged (`semver.eq` still runs).

## pacquet parity

The TypeScript fix patches a crash inside pnpm's `catalogMode` mismatch
gate — a feature pacquet had not ported at all (`catalog-mode` was in the
config parity test's `NOT_PORTED` list). Rather than just the one-liner,
this PR ports that gate to pacquet so the two stacks match:

- **config:** new `CatalogMode { Manual, Strict, Prefer }` enum (default
  `manual`), `Config.catalog_mode`, wired through `pnpm-workspace.yaml`
  (`catalogMode:`) and the env overlay; `catalog-mode` moved from
  `NOT_PORTED` to a mapped row in the `pnpm_default_parity` contract test.
- **package-manager:** `check_catalog_mode` + `CatalogVersionMismatchError`
  (`ERR_PNPM_CATALOG_VERSION_MISMATCH`), invoked from `update` before the
  manifest is mutated. The comparison only treats both sides as equal when
  each parses as a concrete semver version, so a ranged catalog entry falls
  through to the mismatch path instead of reaching an exact-version
  comparison — the Rust analogue of the `semver.valid` guard above.

The crash itself can't occur in pacquet (Rust's `node-semver` returns a
`Result` rather than throwing); the port is the *feature* with the
range-correct comparison built in, so pacquet behaves like fixed pnpm.

**Not ported** (the surrounding pieces pacquet still lacks, so wiring them
would diverge from pnpm rather than match it): the `add`-path cataloging
that relies on `defaultCatalog` rewriting, and the `saveCatalogName` →
`pnpm-workspace.yaml` auto-cataloging half. The gate is therefore wired
into `update <pkg>@<version>` / `--latest` (the Renovate scenario), not
`add`.
2026-06-04 21:20:01 +02:00
..
2026-05-29 17:26:13 +02:00
2026-05-29 17:26:13 +02:00
2026-05-27 15:15:01 +02:00
2026-05-29 17:26:13 +02:00