Skip to content

install: resolve bun update --latest at the row, so package.json never holds a temporary latest - #44485

Draft
robobun wants to merge 7 commits into
mainfrom
robobun/bc0234d4/update-latest-out-of-band
Draft

robobun wants to merge 7 commits into
mainfrom
robobun/bc0234d4/update-latest-out-of-band

Conversation

@robobun

@robobun robobun commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun update --latest moves a package off the version an override pins. With "overrides": {"kept": "catalog:"} and catalog kept: 1.0.0 it prints ^ kept 1.0.0 -> 2.0.0. package.json, bun.lock's catalog and --frozen-lockfile still say 1.0.0. $name and selector overrides lose the pin too.
  • bun update --latest -r on an up-to-date catalog writes "kept": "latest" into package.json.
  • Cause: the editor wrote a temporary latest into the package.json that the install parses (PackageJSONEditor.rs:516, :740, :1398). Overrides read it.

Fix

  • package.json keeps the declared text. version_pick (PackageManagerEnqueue.rs) makes a targeted row resolve the latest dist-tag, as -r --latest already did (install: honor --recursive/--filter in non-interactive bun update; re-resolve every named-update target #36360).
  • A catalog entry selects latest only when a catalog: dependency uses it, and only in the parsed map (steer_used_catalogs_to_latest). An entry that only an override reads stays a pin, as in pnpm 12.8.1.
  • Verified: test/cli/install/bun-update-lockfile-sync.test.ts (34 new cells, 20 fail on main). Other suites: see Notes.

Background

  • An override replaces the version a dependent asks for. catalog: takes the root catalog's version. $name takes the range the root declares for name. A selector key applies only to a dependent whose range overlaps it.
  • Considered: patch each of the three readers and the restore. That keeps the temporary value and needs a side channel for the declared text.

Downsides

Notes

Where the temporary value went before. A bare or named bun update --latest rewrote each direct dependency of the invoking package.json to latest before the install, and a bare one rewrote every root catalog entry. The install parsed that text. Four readers took the temporary value for the declared one:

Reader Site Result on main
catalog-valued override PackageManagerEnqueue.rs override hop, dedupe::effective_version the overridden row resolves latest, the entry is restored, bun.lock contradicts its own catalog
$name override OverrideMap.rs parse_override_value the override's value is latest for this run
selector override on a direct dependency OverrideMap.rs scoped_rule_for a dist-tag never matches a selector, so the rule is skipped and package.json gets ^2.0.0
the printer and sync_lockfile package_json_write_back.rs "latest" reaches package.json (-r, --filter) or bun.lock's catalog (member cwd) when no entry moves. An optional dependency that does not resolve keeps "latest"

What changed.

  • edit_update_entries and edit only record the declared literals. record_catalog_entries (was edit_catalogs_before_update) only records the catalog entries. Nothing prints a temporary value into the package.json cache entry.
  • version_pick decides once per row: the command is bun update --latest, no override or catalog replaced the row's version, and a workspace that the command targets declares the row (with names: the row names a request). Its consumers are the manifest lookup, the exact-version cache shortcut and the peer pass. A patched package is not held for such a row. The baseline for "never move below what bun.lock has" is locked_version_in_lockfile for every form. locked_version_of_invoking_workspace_row is deleted.
  • update_target_declares is the one owner test. The -r / --filter target package ids are looked up once (PackageManager::update_target_ids). Lockfile::is_root_dependency and Lockfile::is_dependency_of_workspace_in are deleted. The second one scanned every package for each resolved row.
  • steer_used_catalogs_to_latest runs after each root parse that a resolve uses (the differ and the new-lockfile path). It reads the dependency lists of the root and of every member from the package.json cache, which the root parse has just filled.
  • sync_lockfile copies the root's catalogs again when they were steered and the root was not edited.

Behaviour changes.

  • -r --latest and --filter --latest: direct peerDependencies and patched packages now move to latest. The plain form always moved them, and docs/pm/cli/update.mdx says --latest moves patched packages.
  • Plain and named --latest: selector and $name overrides, the workspace link and a known npm: alias redirect now see the declared range. Before, the temporary dist-tag bypassed them.
  • A catalog entry that only an override reads: its rows no longer move. A range entry still lets the row move inside the range, as under a plain bun update.
  • An optional dependency that the registry cannot resolve keeps its declared literal. Before it became "latest" in package.json.
  • The Duplicate dependency warning quotes the declared literal, not "latest".

Reference behaviour. pnpm 12.8.1, pnpm update --latest, same shapes: a catalog entry that only an override reads stays, and the row stays. With a direct catalog: dependency the entry moves and the override follows. "kept": "$pin" keeps the pin. When pin itself moves, kept stays on the old version and pnpm install --frozen-lockfile then exits 1.

Still open. A row that must follow a $name referent or a catalog entry that moves in the same command keeps the old version. bun.lock then records the new override value, and --frozen-lockfile passes. That is the state of #43975 for bun update <name>. #43981 re-resolves such rows after the write-back. resolve_catalog_literals still trusts any row declared catalog:, also one that a literal override resolved.

Costs, from the diff.

  • bun install: one to_update test per enqueued registry row. The three-term latest_for_target expression per manifest lookup is gone. One updating_catalogs.is_empty() test per install.
  • bun update --latest with a catalog: one print and one JSON parse of the root package.json fewer (the temporary value needed them). One pass over the dependency lists of every workspace package.json (cache hits, no file read). One string append and one Dependency::parse per used entry.
  • bun update -r --latest: one scan of the package list per command. Before: up to two scans per resolved row.
  • size_of::<PackageManager>(): +24 bytes, one Vec<PackageID>.
  • Release binary size: not measured. The build host has no bloaty, valgrind, perf or strace, and a release build did not finish there.

Suites run with the debug build. bun-update, bun-update-transitive, bun-update-lockfile-sync, catalogs, overrides, nested-overrides, bun-add, bun-add-catalog, bun-add-filter, bun-audit, bun-patch, bun-dedupe, bun-prune, bun-workspaces, bun-install-registry, lockfile-only, minimum-release-age, bun-update-security-* under test/cli/install/, and test/cli/update_interactive_install.test.ts.

@robobun

robobun commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Status: draft, tests pass locally, self-review in progress

How this was reproduced, on main and on 1.4.2, with a loopback registry (parent@1.0.0 depends on kept@^1.0.0, the registry's latest is kept@2.0.0):

  1. "overrides": {"kept": "catalog:"} and catalog kept: 1.0.0. bun install, then bun update --latest. Output: ^ kept 1.0.0 -> 2.0.0. bun.lock's row is kept@2.0.0. package.json and bun.lock's catalog still say 1.0.0. bun install --frozen-lockfile exits 0.
  2. "overrides": {"kept": "$pin"} with "pin": "1.0.0": the same result.
  3. "kept": "^1.0.0" with "overrides": {"kept@>=1.0.0": "1.0.0"}: package.json gets "kept": "^2.0.0".
  4. A monorepo with catalog kept: 2.0.0 (already the newest) and a member that declares "kept": "catalog:". bun update --latest -r prints Checked 3 packages (no changes) and writes "kept": "latest" into the root package.json. From the member directory, bun update --latest writes latest into bun.lock's catalog.

With this branch the pin holds in 1 to 3, and both files stay byte-identical in 4.

test/cli/install/bun-update-lockfile-sync.test.ts: 20 of the 34 new cells fail with the released build and all pass with this branch.

@robobun

robobun commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:27 PM PT - Oct 2nd, 2026

✅ @autofix-ci[bot], your commit 8e82052d6e41c2a9d180035fd9cd871fc99cf3a7 passed in Build #123031! 🎉


🧪   To try this PR locally:

bunx bun-pr 44485

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

bun-44485 --bun

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