Skip to content

install: apply both "overrides" and "resolutions" when package.json has both - #38811

Open
robobun wants to merge 1 commit into
mainfrom
farm/2918be2f/merge-overrides-resolutions
Open

robobun wants to merge 1 commit into
mainfrom
farm/2918be2f/merge-overrides-resolutions

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A root package.json with both an "overrides" map and a "resolutions" map silently ignores the whole "resolutions" map, even when the two pin different packages. No warning, exit 0. Same on 1.3.14 and on both linkers.
  • Repro: dependencies: {"two-range-deps": "1.0.0"} (declares no-deps ^1.0.0 and @types/is-number >=1.0.0), overrides: {"no-deps": "1.0.0"}, resolutions: {"@types/is-number": "1.0.0"} installs no-deps@1.0.0 but @types/is-number@2.0.0. Deleting the overrides key makes the same resolutions entry apply.
  • Cause: OverrideMap::parse_append and parse_count (src/install/lockfile/OverrideMap.rs) are an if overrides ... else if resolutions ...: exactly one field is ever parsed. The root package.json, yarn.lock migration (yarn.rs) and package-lock.json migration (migration/npm_lock.rs) all go through these two functions.
  • Projects migrated from Yarn that keep their resolutions pins and later gain one overrides entry lose every resolutions pin.

Fix

  • parse_append parses "overrides", records how many flat and scoped rules it produced, then parses "resolutions". parse_count counts both fields.
  • Precedence when both fields define the same selector: the "overrides" rule wins. This keeps today's behavior for conflicting keys and matches pnpm, which reads resolutions and lets pnpm.overrides win.
  • A losing "resolutions" rule is skipped before its value is parsed (put_rule). This matters because a flat npm: value also registers the alias in known_npm_aliases, which PackageManagerEnqueue.rs consults before the override map; merging by overwriting the map entry would have left that alias behind and redirected edges the override was supposed to pin.
  • Rules owned by "overrides" are identified by position (the first N flat entries / first M scoped entries), so within "resolutions" itself the existing last-spelling-wins behavior is unchanged. push_scoped keeps its semantics for the lockfile loaders; the parse path uses the new put_scoped with a keep count.
  • The --frozen-lockfile note says overrides or resolutions in package.json changed when the root has both fields (install_with_manager.rs), and docs/pm/overrides.mdx documents the merge and the precedence.
  • Behavior change to be aware of: a project that already has both fields will re-save its lockfile on the next install (the resolutions rules now get recorded and applied), so a --frozen-lockfile install there fails until the lockfile is updated. That is the fix taking effect; those projects currently have unpinned packages.
  • Verified with test/cli/install/nested-overrides.test.ts:
    • new overrides and resolutions in the same package.json block (7 tests): both fields apply, flat and scoped precedence, the losing npm: rule registers nothing, last spelling within resolutions still wins, editing resolutions is a frozen-lockfile change with the new note
    • new yarn.lock migration carries both overrides and resolutions test, frozen-clean after bun pm migrate
    • the both-fields-apply, scoped, last-spelling, frozen and migration tests fail without the src/ change (confirmed against the released binary for the flat cases), all 152 tests in the file pass with it
    • bun-lock.test.ts, bun-workspaces.test.ts, migration/migrate.test.ts, migration/yarn-lock-migration.test.ts and bun-audit.test.ts override/resolution tests pass; test/internal/source-lints/byte-search.test.ts passes; cargo clippy -p bun_install is clean

Background

  • OverrideMap is the in-memory form of the root's override rules. map holds flat rules (one per package name); scoped holds rules limited to a parent package and/or a target version range ("one-dep>no-deps", "no-deps@1", npm object form, yarn path form). PackageManagerEnqueue.rs consults it for every dependency edge, and bun.lock serializes it as the "overrides" section regardless of which field the rule came from.
  • Parsing into the lockfile string buffer is two-pass: parse_count sizes the buffer by counting every string the rules will need, then parse_append appends and builds the rules. Over-counting is harmless (the builder is clamped afterwards), which is why skipped rules can still be counted.
  • known_npm_aliases is a side table filled while parsing npm: specifiers (dependencies, catalogs, flat override rules). When a plain dependency's name matches a registered alias and its range overlaps, the edge is redirected to the alias target before the override map is consulted, so registering an alias for a rule that is then discarded would change resolution.
  • ArrayHashMap preserves insertion order and put on an existing key replaces the value in place, which is what makes "the first N entries came from overrides" stable while resolutions is being parsed.

…as both

OverrideMap::parse_append (and parse_count) read "overrides" and fell back
to "resolutions" only when "overrides" was absent, so a package.json that
carried both silently dropped every "resolutions" rule. The same code path
serves the root package.json, yarn.lock migration and package-lock.json
migration.

Parse both fields. "overrides" is parsed first; a "resolutions" rule whose
selector "overrides" already defined is skipped before its value is parsed,
so a losing flat "npm:" rule does not register an alias either. Rules that
only "resolutions" defines are added. Within each field the last spelling
of a rule still wins.

The frozen-lockfile note names "overrides or resolutions" when the root
package.json has both.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd0d5a3f-867b-4433-bfff-3d7eb5e2813f

📥 Commits

Reviewing files that changed from the base of the PR and between a5c86ae and 62972e8.

📒 Files selected for processing (4)
  • docs/pm/overrides.mdx
  • src/install/PackageManager/install_with_manager.rs
  • src/install/lockfile/OverrideMap.rs
  • test/cli/install/nested-overrides.test.ts

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

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 PM PT - Aug 14th, 2026

❌ @robobun, your commit 62972e8 has 1 failures in Build #97101 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 38811

That installs a local version of the PR into your bun-38811 executable, so you can run:

bun-38811 --bun

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced against the Verdaccio fixtures: two-range-deps@1.0.0 with overrides: {"no-deps": "1.0.0"} and resolutions: {"@types/is-number": "1.0.0"} installed @types/is-number@2.0.0; removing the overrides key made the same resolutions entry apply. Cause is the if overrides / else if resolutions in OverrideMap::parse_append and parse_count, which the root package.json, yarn.lock migration and package-lock.json migration all share.

Coverage is in test/cli/install/nested-overrides.test.ts (the new overrides and resolutions in the same package.json block plus a yarn.lock migration case); the both-fields cases fail on main and pass with this change.

CI: 178 of 179 jobs passed. The one failed job is Windows x64, on test/bake/deinitialization.test.ts, a dev server crash at exit that fails the same way on other current PR builds and does not touch this change (reported separately). Nothing in this PR is waiting on CI.

Two decisions for the reviewer, both described in the PR body: overrides wins when both fields define the same selector (pnpm's behavior, and bun's current behavior for those keys), and a project that already has both fields re-saves its lockfile on the next install because its resolutions rules now apply.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it changes user-facing package-manager semantics (merging resolutions into overrides, with a precedence rule and an intentional lockfile re-save for projects that currently have both fields), a human sign-off on the design choice would still be worthwhile.

What was reviewed

  • parse_append/parse_count now iterate both fields; verified ArrayHashMap::put replaces in place so the position-based RuleCount precedence is stable and last-spelling-wins within each field is preserved.
  • push_scoped → put_scoped(_, _, 0) refactor is behavior-preserving (the redundant scoped_names.insert on the name-known-but-no-match path is a no-op on a ()-valued set).
  • The losing-npm:-alias skip is correctly placed before parse_override_value, and parse_count over-counting the skipped rule is harmless per the builder-clamp contract.
Extended reasoning...

Overview

This PR changes bun install so that a root package.json with both "overrides" and "resolutions" applies rules from both fields instead of silently dropping "resolutions". Touches src/install/lockfile/OverrideMap.rs (parse loop, RuleCount, put_scoped/scoped_position refactor, hoisting name_hash out of parse_override_value), src/install/PackageManager/install_with_manager.rs (overrides_field_name returns "overrides or resolutions" when both exist), one doc sentence, and 8 new tests in test/cli/install/nested-overrides.test.ts.

Security risks

None identified. This is package.json parsing into an in-memory rule map; no new untrusted-input surface, no path handling, no network. The npm: alias side-table concern is handled defensively (losing rules are skipped before their value is parsed).

Level of scrutiny

Medium-high. The implementation is clean and the position-based precedence relies on a verified invariant (ArrayHashMap::put replaces at the existing index, never reorders). But this is a user-visible behavior change with an explicit design decision — overrides wins over resolutions on conflict (matching pnpm), and projects that today have both fields will re-save their lockfile on next install and fail --frozen-lockfile until updated. That's the fix taking effect, but it's the kind of semantic/UX choice a maintainer should confirm rather than have auto-approved.

Other factors

No CODEOWNERS cover the changed paths. Test coverage is thorough: both-apply, flat/scoped precedence, losing npm: rule registers no alias, last-spelling-within-resolutions preserved, frozen-lockfile note wording, and yarn.lock migration. The push_scoped → put_scoped refactor was checked line-by-line against the old body and is behavior-preserving for keep = 0 (the extra scoped_names.insert in the None arm when the name was already present is idempotent on a ()-valued map). ensure_unused_capacity in parse_from_resolutions correctly reserves additional capacity on top of what parse_from_overrides already used.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants