Skip to content

fix: preserve catalog version range policy on update#12416

Merged
zkochan merged 1 commit into
mainfrom
fix/10321
Jun 15, 2026
Merged

fix: preserve catalog version range policy on update#12416
zkochan merged 1 commit into
mainfrom
fix/10321

Conversation

@zkochan

@zkochan zkochan commented Jun 15, 2026

Copy link
Copy Markdown
Member

What

Fixes #10321: pnpm update overriding the version range policy of a catalog entry.

When a dependency points at a named catalog whose name parses as a version — e.g. "express": "catalog:express4-21" referencing a catalog entry express: ~4.21.2 — running pnpm update widened the catalog entry's ~ range to ^.

Root cause

During update, the dependency's previous specifier is the catalog reference catalog:express4-21. whichVersionIsPinned strips everything before the first colon, leaving express4-21, which semver-utils' parseRange interprets as a version with major 4. It therefore returned 'major' (=> ^) and overrode the real catalog specifier's ~ pin.

This only reproduced with named catalogs whose names look like versions; catalog: (default) and non-numeric names like catalog:foo parse to [] and were unaffected — which matches the reporter's "I could only reproduce this behavior in catalogs".

Fix

A catalog: reference carries no version pinning of its own — the pinning lives in the catalog entry, which is already substituted into the bare specifier before the prefix is computed. whichVersionIsPinned now returns undefined for any catalog: spec, so the catalog entry's own prefix wins.

Tests

  • Unit cases in whichVersionIsPinned.test.ts for catalog:, catalog:default, catalog:foo, and catalog:express4-21.
  • An e2e regression test in deps-installer/test/catalogs.ts that runs pnpm update over a version-like named catalog and asserts the ~ prefix is preserved. It fails on main (~1.0.0^1.0.0) and passes with the fix.

pacquet parity

Not applicable: pacquet doesn't implement pin-preservation on update (it always writes ^ unless --save-exact), so it doesn't share this code path. No pacquet-side change is needed for this fix.


Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed pnpm update incorrectly overriding version-range policies for named catalogs with version-like names (e.g., express4-21). Catalog pinning prefixes are now correctly preserved during installation and updates.

A named catalog whose name parses as a version (e.g. catalog:express4-21)
had its range policy overridden by pnpm update because whichVersionIsPinned
misread the catalog: reference in the previous specifier as a pinned
version. The catalog reference carries no pinning of its own, so the prefix
from the catalog entry is now preserved.

Closes #10321
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4534d0be-3900-45a9-b8aa-a89897bc03dd

📥 Commits

Reviewing files that changed from the base of the PR and between 1e82e00 and 4fd2267.

📒 Files selected for processing (4)
  • .changeset/catalog-update-policy.md
  • installing/deps-installer/test/catalogs.ts
  • resolving/npm-resolver/src/whichVersionIsPinned.ts
  • resolving/npm-resolver/test/whichVersionIsPinned.test.ts

📝 Walkthrough

Walkthrough

whichVersionIsPinned gains an early-return guard that returns undefined for any specifier starting with catalog:, preventing named catalogs whose names look like versions from being misidentified as pinned ranges. A unit test and an integration regression test validate the fix. A patch changeset entry is added.

Changes

Catalog version-range policy fix

Layer / File(s) Summary
whichVersionIsPinned guard and unit tests
resolving/npm-resolver/src/whichVersionIsPinned.ts, resolving/npm-resolver/test/whichVersionIsPinned.test.ts
Adds an early-return for catalog:-prefixed specifiers in whichVersionIsPinned and extends the test.each suite with catalog:, catalog:default, catalog:foo, and catalog:express4-21, all asserted to return undefined.
Integration regression test and changeset
installing/deps-installer/test/catalogs.ts, .changeset/catalog-update-policy.md
Adds a full install-then-update regression test (issue #10321) verifying the ~1.0.0 specifier is preserved in updatedCatalogs and the lockfile catalogs section. Includes a patch-level changeset for @pnpm/resolving.npm-resolver and pnpm.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • pnpm/pnpm#11567: Shares coverage in installing/deps-installer/test/catalogs.ts and addresses related catalog:* specifier handling in install/upgrade flows.

Suggested labels

area: catalogs

🐇 A catalog named like a version, oh my!
The tilde would widen — pnpm asked why.
Now catalog: returns undefined with grace,
And ~1.0.0 stays locked in its place.
No more sneaky ^ where ~ should reside! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main fix—preserving catalog version range policy during updates—which aligns with all changed files and PR objectives.
Linked Issues check ✅ Passed The PR fully addresses the coding requirements from issue #10321 by fixing the whichVersionIsPinned function, adding unit and regression tests, and preserving catalog version range policies during updates.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the catalog version range policy override issue; no unrelated or out-of-scope modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/10321

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix pnpm update to preserve catalog version range policy
🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

Walkthroughs

Description
• Prevent pnpm update from widening named catalog specifiers from ~ to ^
• Treat all catalog:* references as unpinned so catalog entry policy wins
• Add unit + e2e regression coverage for version-like catalog names
Diagram
graph TD
  A["pnpm update"] --> B["deps-installer (mutateModules)"] --> C["whichVersionIsPinned()"] --> D{"spec starts with catalog:?"}
  D -->|"yes"| E["return undefined"] --> F["preserve catalog prefix (~)"] --> G[("lockfile catalogs")]
  D -->|"no"| H["semver-utils parseRange"] --> I["derive pin (major/minor/patch)"] --> G

  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _dec{"Decision"} ~~~ _db[("Data store")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Skip pin-detection earlier in the update pipeline
  • ➕ Keeps whichVersionIsPinned purely semver-focused
  • ➕ Avoids special-casing protocol handling in resolver utilities
  • ➖ Requires threading catalog context through higher-level update code paths
  • ➖ Higher risk of missing other call sites that use whichVersionIsPinned
2. Tighten parsing logic to ignore non-semver tokens after protocol stripping
  • ➕ More general protection against future protocol/name collisions
  • ➕ Reduces reliance on prefix checks for specific protocols
  • ➖ More complex and potentially error-prone than a protocol guard
  • ➖ Could change behavior for other edge-case spec strings

Recommendation: Current approach (early-return undefined for catalog:*) is the lowest-risk fix: a catalog reference is not a version range, so treating it as unpinned is semantically correct and prevents parseRange from misclassifying version-like catalog names. The added unit and e2e regressions make this targeted change safe.

Grey Divider

File Changes

Bug fix (1)
whichVersionIsPinned.ts Treat 'catalog:*' specifiers as unpinned to avoid range widening +5/-0

Treat 'catalog:*' specifiers as unpinned to avoid range widening

• Adds an early guard so any 'catalog:' reference returns 'undefined' pinning. This prevents mis-parsing version-like catalog names as pinned versions and preserves the catalog entry’s own range operator.

resolving/npm-resolver/src/whichVersionIsPinned.ts


Tests (2)
catalogs.ts Add e2e regression for preserving '~' in version-like named catalogs +49/-0

Add e2e regression for preserving '~' in version-like named catalogs

• Introduces an update regression test that uses a named catalog whose name looks like a version (e.g. 'foo1-0'). Verifies 'pnpm update'-equivalent mutation preserves the catalog entry's '~' prefix in 'updatedCatalogs' and the lockfile.

installing/deps-installer/test/catalogs.ts


whichVersionIsPinned.test.ts Add unit coverage for 'catalog:*' specifiers in pin detection +5/-0

Add unit coverage for 'catalog:*' specifiers in pin detection

• Extends parameterized tests to assert 'whichVersionIsPinned' returns 'undefined' for 'catalog:' (default), 'catalog:default', arbitrary names, and version-like catalog names.

resolving/npm-resolver/test/whichVersionIsPinned.test.ts


Documentation (1)
catalog-update-policy.md Add changeset for catalog update pinning regression fix +6/-0

Add changeset for catalog update pinning regression fix

• Adds a patch changeset for 'pnpm' and '@pnpm/resolving.npm-resolver' documenting the fix for catalog range prefix widening during 'pnpm update'.

.changeset/catalog-update-policy.md


Grey Divider

Qodo Logo

@zkochan zkochan merged commit 29ab905 into main Jun 15, 2026
15 checks passed
@zkochan zkochan deleted the fix/10321 branch June 15, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pnpm update overrides catalogs version update policy

1 participant