install: make `bun outdated` and `bun update -i` read dependency ranges from package.json

Closed
RO
1mo ago
+567-440

Problem

  • bun outdated and bun update -i compute the "Update" / "Target" column from the dependency range stored in bun.lock, not from package.json. Change "no-deps": "1.0.0" to "^1.0.0" without a reinstall and both still print 1.0.0. A dependency removed from package.json stays listed (as #34262 reports).
  • The cause: print_outdated_info_table (src/runtime/cli/outdated_command.rs:365) and its copy get_outdated_packages (update_interactive_command.rs:847) walk the lockfile's dependency rows, which keep the names, groups and ranges of the last bun install.

Fix

  • Add DeclaredDependencies to bun_install. It parses the root and each selected workspace package.json the way bun install does and joins each declared npm dependency, by name and group, with the package bun.lock installed for it. Both commands iterate it.
  • Correct because bun install makes the same split: it diffs a fresh package.json parse against the lockfile, which records what is installed, not what is wanted.
  • Self-reviewed: 6 concerns raised, 6 addressed (the first draft fixed bun outdated alone).
  • Verified: new cases in test/cli/install/bun-install-registry.test.ts and test/cli/update_interactive_formatting.test.ts, both failing on stock bun, plus every other outdated and update -i test.

Background

  • Both commands print, per direct dependency, Current (installed), Update/Target (best version in the declared range) and Latest (latest dist-tag).
  • bun.lock stores, per workspace package, a copy of its package.json dependency list at the last install, next to the resolved package of each entry.
  • ScratchManifests (workspace_manifests.rs) parses package.json files into a throw-away Lockfile, as bun add --filter already does. DeclaredDependencies is a public facade over it.
Notes
  • Repro without the tests: echo '{"name":"app","dependencies":{"pkg":"1.0.0"}}' > package.json && bun install, change "1.0.0" to "^1.0.0", run bun outdated. Before: Update 1.0.0, while npm outdated prints the new wanted version for the same tree. A no-op bun install refreshes the lockfile copy and the next bun outdated is right, which shows where the range came from.
  • The bun outdated test ("reads dependency ranges from package.json, not bun.lock") covers four cases in one table: a changed root range (~1.0.0), a changed catalog range used by a member (catalog:), a member dependency that changed range and moved to devDependencies, and a root dependency removed from package.json. It also checks that bun.lockb is byte-identical afterwards. The update -i test ("should offer what package.json declares now, not what bun.lock recorded") covers a widened range, a moved group and a removed dependency, and checks bun.lock is untouched.
  • WorkspaceTarget::of_lockfile_package is the former private target_for of audit_fix/package_json_edits.rs. same_row is the former private helper of package_json_write_back.rs. Both move next to ScratchManifests so each has one copy. DeclaredDependencies uses same_row to match a declared dependency to its lockfile row, and falls back to a same-name row when the dependency moved to another group.
  • A dependency that is in package.json but has no lockfile row yet (added, never installed) is still not listed, as before. npm prints MISSING for it. Left for a follow-up.
  • A workspace whose package.json cannot be read now fails the command with failed to read/parse package.json for workspace '<name>', the message bun add --filter uses. Before, its stale lockfile rows were printed. Plain bun outdated (no --filter/-r) only reads the root and the current workspace, which bun install already requires.
  • bun update -i's "Checked N dependencies" line now counts declared npm dependencies that are installed. It no longer counts workspace: links or git dependencies, which it never offered anyway. bun update (non-interactive) already excluded workspace links.
  • The root parse uses Features::main(), so it walks the workspaces globs like an install. That keeps fidelity: a root dependency that names a workspace member by version becomes a workspace: dependency only through that walk.
  • #38813 touches the same file for pack/publish. This PR keeps ScratchManifests crate-private behind a public facade, the shape #38813 uses, and the same crash(log, err, what) helper, so either order rebases cleanly.
  • Also in the first pass of bun outdated: the installed version is now formatted with the lockfile string buffer in the two fallback branches (it was formatted with the manifest buffer, which mis-sizes the column for a pre-release current version). #38649 changes the same two lines.
  • Other suites run locally: bun-update-transitive.test.ts (all 174), bun-add-filter.test.ts, bun-audit.test.ts -t fix, update_interactive_install.test.ts, update_interactive_snapshots.test.ts, the outdated tests in bun-update.test.ts and minimum-release-age.test.ts.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/update_interactive_formatting.test.ts, test/cli/install/bun-install-registry.test.ts