Skip to content

install: write a migrated lockfile and its package.json edits only when the command saves a lockfile - #38804

Open
robobun wants to merge 2 commits into
mainfrom
farm/af71d6d5/migration-writes-follow-lockfile-save
Open

install: write a migrated lockfile and its package.json edits only when the command saves a lockfile#38804
robobun wants to merge 2 commits into
mainfrom
farm/af71d6d5/migration-writes-follow-lockfile-save

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • update_package_json_after_migration now only edits the cached root package.json entry (print_package_json_into_cache_entry + reparse_root, the same path bun add uses) and appends what it moved to a new PackageManager.migrated_package_json_moves list. The frozen check and the differ already read the cache entry, so frozen installs behave exactly as before; nothing touches disk during the load.
  • package_json_write_back takes that list over exactly where the lockfile is saved, printing the moved ... in package.json line once. The two save sites in install_with_manager (the normal path and --lockfile-only, which now also flushes) queue the entry for the existing flush; bun pm migrate and bun pm trust (write_migrated_root) write the entry directly, because they save the lockfile without consulting --dry-run / --no-save and flush honors those flags (pm trust also re-reads package.json from disk before editing it, so the migration's edits have to be on disk first). bun.lock and package.json are therefore written together or not at all, for every command.
  • saves_migrated_lockfile becomes converts_binary_lockfile_to_text: the only save that ignores Do::SAVE_LOCKFILE is the documented bun.lockb to bun.lock recipe (--save-text-lockfile --frozen-lockfile --lockfile-only, still covered by bun-lock.test.ts and frozen-lockfile-pruned.test.ts). A migrated lockfile is saved through FORCE_SAVE_LOCKFILE like any other change, so --frozen-lockfile, --dry-run, and --no-save keep it in memory. --lockfile-only checks the same flag instead of only frozen_lockfile(), so --lockfile-only --dry-run / --no-save stop writing too.
  • Under --frozen-lockfile the install prints one line, e.g. note: the lockfile is frozen, so the migration from pnpm-lock.yaml was not written to bun.lock and package.json; run 'bun install' and commit the result. --dry-run / --no-save print nothing extra.
  • bun remove writes the cache entry for the file it edited instead of the stale pre-install print, which is what the patch --commit branch next to it already did; the two branches are now one.
  • loaded_lockfile_name names the migrated file, so the frozen failure reads overrides in package.json changed since pnpm-lock.yaml was saved instead of claiming a bun.lock that does not exist (update_scope now shares the helper). Small and on the same path; easy to drop if unwanted.
  • Judgment call: this reverses the two lockfile-only.test.ts cases from install: pnpm parity — dedupe, prune, pm licenses, audit fix, add --filter/--catalog, nested overrides, transitive update, and workspace fixes #38333 that pinned "--frozen-lockfile writes bun.lock when migrating". The bun.lockb recipe test in frozen-lockfile-pruned.test.ts calls that recipe "the one sanctioned frozen write", the flag's docs say it disallows lockfile changes, and pnpm's --frozen-lockfile never writes, so the foreign-lockfile write looked like the outlier. Keeping it would be a one-predicate change on top of the rest of this PR.
  • Verified:
    • test/cli/install/lockfile-only.test.ts: --frozen-lockfile, --dry-run, --no-save (with and without --lockfile-only) against all three foreign formats write nothing; plain install still writes. The migration cases and the --dry-run / --no-save --lockfile-only cases fail on main (bun.lock exists / gets rewritten).
    • test/cli/install/migration/pnpm-lock-v9.test.ts, new package.json edits block: frozen / ci / --production install without writing either file; --dry-run, --no-save, outdated, pm why, pm untrusted leave both files alone; install, install --lockfile-only, pm migrate, pm migrate --dry-run, add, remove, pm trust, install --silent write both and the result passes --frozen-lockfile; the out-of-sync frozen error still fires. The untouched / remove / frozen-note cases fail on main.
    • migrate.test.ts B7 used --frozen-lockfile --lockfile-only as a way to write bun.lock; it now uses --lockfile-only (the frozen round trip is asserted separately in the same test).
    • Also run locally: migrate.test.ts, nested-overrides.test.ts, pnpm-*.test.ts, yarn-lock-migration.test.ts, bun-lock.test.ts, frozen-lockfile-pruned.test.ts, bun-dedupe.test.ts, bun-audit.test.ts -t fix, bun-update*.test.ts, bun-add*.test.ts, bun-remove.test.ts, bun-patch.test.ts, bun-install-patch.test.ts, the pm trust tests in bun-install-lifecycle-scripts.test.ts; cargo clippy -p bun_install -p bun_runtime is clean.
  • Related open PRs editing update_package_json_after_migration: install: import pnpm-workspace.yaml even without a migratable pnpm-lock.yaml #38754, install: keep the pnpm block in package.json when migrating from pnpm #38775, install: migrate pnpm onlyBuiltDependencies and report blocked scripts under the isolated linker #38780. They change what the function edits; this one changes when the file is written. Whichever lands second needs a small rebase: push the extra moves into migrated_package_json_moves, drop the dir argument, and (for install: import pnpm-workspace.yaml even without a migratable pnpm-lock.yaml #38754's new call site) rely on the save-time write instead of writing in place.

Background

  • Lockfile migration: when bun.lock / bun.lockb is missing, Lockfile::load_from_dir falls through to migration::detect_and_load_other_lockfile, which builds the in-memory lockfile from package-lock.json, yarn.lock, or pnpm-lock.yaml. Every command that loads the lockfile with "attempt other lockfiles" set (install, add, remove, update, outdated, pm why/ls/trust/untrusted/migrate, audit, ...) goes through it, so anything the migrator does to disk happens for all of them.
  • pnpm keeps workspaces, catalogs, overrides, and patches in pnpm-workspace.yaml and the pnpm key of package.json; bun reads them from root-level package.json fields. The pnpm migration therefore has to edit package.json, and the rest of the install has to see those edits, or the frozen check reports that overrides / workspaces changed.
  • WorkspacePackageJSONCache is the per-process cache of parsed package.json files keyed by absolute path. The install reads the root manifest from it, which is why editing the cache entry is enough for the load; MapEntry::reparse_root re-derives the cached AST from the printed text so later readers of entry.root (bun update -r, audit fix, the frozen error message) see the edits as well.
  • package_json_write_back is the existing deferred writer: commands record the cache entries they edited, and flush (run after bun.lock is saved, gated on Do::WRITE_PACKAGE_JSON, which --dry-run / --no-save clear) writes the ones whose bytes differ from disk. The migration's edit is now one more recorded entry.
  • Do::SAVE_LOCKFILE is cleared by --frozen-lockfile, --dry-run, --no-save, and BUN_CONFIG_SKIP_SAVE_LOCKFILE in PackageManagerOptions::load; Enable::FORCE_SAVE_LOCKFILE is set for migrated loads in install_with_manager and only consulted when SAVE_LOCKFILE is on.
Before / after on a pnpm repo (root package.json with a pnpm.overrides block, pnpm-workspace.yaml, one workspace member, no registry needed)

main (e7460e3c7):

$ bun install --frozen-lockfile      # same for: bun ci, bun install --dry-run, bun install --no-save
moved pnpm.overrides to overrides, pnpm-workspace.yaml to workspaces in package.json
[27ms] migrated lockfile from pnpm-lock.yaml
Saved lockfile
$ git status --short
 M package.json
?? bun.lock

$ bun outdated                       # same for: bun pm why a
moved pnpm.overrides to overrides, pnpm-workspace.yaml to workspaces in package.json
[27ms] migrated lockfile from pnpm-lock.yaml
$ git status --short
 M package.json

$ bun remove foo
moved pnpm.overrides to overrides, pnpm-workspace.yaml to workspaces in package.json
...
$ bun install --frozen-lockfile
error: Workspace dependency "a" not found

this branch:

$ bun install --frozen-lockfile
[27ms] migrated lockfile from pnpm-lock.yaml
note: the lockfile is frozen, so the migration from pnpm-lock.yaml was not written to bun.lock and package.json; run 'bun install' and commit the result
Done! Checked 2 packages (no changes) [124.00ms]
$ git status --short
(clean)

$ bun install --dry-run / --no-save / bun outdated / bun pm why a     -> clean tree, no note

$ bun install                        # same for: --lockfile-only, bun pm migrate, bun add, bun remove, bun pm trust
[27ms] migrated lockfile from pnpm-lock.yaml
moved pnpm.overrides to overrides, pnpm-workspace.yaml to workspaces in package.json
Saved lockfile
$ bun install --frozen-lockfile
Done! Checked 2 packages (no changes)

no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/migration/migrate.test.ts

@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: 1 minute

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: 16aa8a83-e5dc-414c-8ca8-5376596bee9f

📥 Commits

Reviewing files that changed from the base of the PR and between a5c86ae and 9b53a1c.

📒 Files selected for processing (15)
  • docs/pm/cli/install.mdx
  • docs/pm/lockfile.mdx
  • src/install/PackageManager.rs
  • src/install/PackageManager/install_with_manager.rs
  • src/install/PackageManager/package_json_write_back.rs
  • src/install/PackageManager/updatePackageJSONAndInstall.rs
  • src/install/lockfile.rs
  • src/install/migration.rs
  • src/install/pnpm.rs
  • src/install/update_scope.rs
  • src/runtime/cli/package_manager_command.rs
  • src/runtime/cli/pm_trusted_command.rs
  • test/cli/install/lockfile-only.test.ts
  • test/cli/install/migration/migrate.test.ts
  • test/cli/install/migration/pnpm-lock-v9.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 9b53a1c is building: #97150

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on main (e7460e3c7) with a pnpm repo (root package.json with a pnpm.overrides block, pnpm-workspace.yaml, one workspace member, no registry): bun install --frozen-lockfile, bun ci, bun install --dry-run, bun install --no-save all left bun.lock plus a modified package.json behind; bun outdated and bun pm why rewrote package.json; bun remove left bun.lock and package.json disagreeing. The same dry-run / no-save bun.lock write reproduces with package-lock.json and yarn.lock.

Fix and tests are in this PR (test/cli/install/lockfile-only.test.ts, test/cli/install/migration/pnpm-lock-v9.test.ts "package.json edits"); the new cases fail on main and pass with the change. Rebased on main; the review finding about bun pm migrate --dry-run is addressed in 9b53a1c.

CI for 9b53a1c (build 97150): 177 of 179 jobs passed with no test failures. The remaining two are the darwin 14 aarch64 test lanes, which expired without ever running because the test-darwin / release-tier=previous aarch64 pool (6 agents) has been saturated for hours; the darwin 15/26 aarch64 and darwin 14 x64 lanes passed. Nothing in the diff is platform-specific. Ready for a maintainer's decision on the --frozen-lockfile behavior change described above; happy to re-run the two expired lanes if wanted.

Comment thread src/install/PackageManager/package_json_write_back.rs Outdated
…en the install saves a lockfile

Loading a package-lock.json, yarn.lock, or pnpm-lock.yaml when there is no
bun.lock used to write files as a side effect of the load: bun.lock was
saved even under --frozen-lockfile, --dry-run, and --no-save, and the pnpm
migration wrote the root package.json (workspaces, overrides,
patchedDependencies moved in from pnpm-workspace.yaml and the pnpm key)
from inside the migrator, so read-only commands such as bun outdated and
bun pm why rewrote package.json too.

The pnpm migration now only edits the cached package.json entry and records
what it moved on the PackageManager. install_with_manager, bun pm migrate,
and bun pm trust queue that entry for package_json_write_back at the point
where they save the lockfile, so both files are written together or not at
all. The unconditional save exception is narrowed to the documented
bun.lockb -> bun.lock conversion; a migrated lockfile saves through the
normal FORCE_SAVE_LOCKFILE path, which --frozen-lockfile, --dry-run, and
--no-save disable. --frozen-lockfile prints a note saying the migration
was not written. --lockfile-only honors the same flag. bun remove writes
the cache entry instead of the source it printed before the install, so it
no longer drops the migration's edits. Messages about the loaded lockfile
name the migrated file instead of bun.lock.
…migrate and bun pm trust

Both commands save the lockfile without looking at --dry-run / --no-save, so
routing the package.json half through flush (which those flags disable) left
bun.lock migrated and package.json not. Write the entry directly instead.
@robobun
robobun force-pushed the farm/af71d6d5/migration-writes-follow-lockfile-save branch from 5ef3df5 to 9b53a1c Compare August 15, 2026 02:34

@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.

Thanks for addressing the earlier note — write_migrated_root now writes package.json directly (bypassing flush's WRITE_PACKAGE_JSON gate) and the pm migrate --dry-run test confirms both files land together. I re-reviewed and found no further issues.

Given this reverses the #38333 decision on whether --frozen-lockfile writes bun.lock during migration (flagged as a judgment call in the description) and reshapes the write-path across install/add/remove/pm migrate/pm trust, a maintainer should confirm the behavior change before merge.

What was reviewed:

  • converts_binary_lockfile_to_text correctly narrows the frozen-write exception to bun.lockb→bun.lock only; migrated foreign lockfiles now go through FORCE_SAVE_LOCKFILE and are gated by Do::SAVE_LOCKFILE.
  • The bun remove / patch --commit write-back now reads the cache entry (which the migration edited) instead of the stale pre-install print — verified this is the same path for both branches.
  • write_migrated_root vs record_migrated_root split keeps bun.lock and package.json paired at all four save sites; the pm migrate --dry-run and pm trust cases I raised earlier are covered.
Extended reasoning...

Overview

This PR changes when the package manager writes a migrated lockfile and its accompanying package.json edits to disk. Previously, migrating from pnpm-lock.yaml/package-lock.json/yarn.lock wrote bun.lock (and, for pnpm, rewrote package.json) even under --frozen-lockfile, --dry-run, --no-save, and read-only commands like bun outdated. Now the pnpm migration edits only the in-memory cache entry, and the file writes are deferred to the same points that save bun.lock — so both files are written together or not at all. Touches install_with_manager.rs, package_json_write_back.rs, updatePackageJSONAndInstall.rs, pnpm.rs, migration.rs, lockfile.rs, update_scope.rs, package_manager_command.rs, pm_trusted_command.rs, plus docs and three test files.

Prior feedback addressed

My earlier inline comment noted that bun pm migrate --dry-run / bun pm trust --dry-run would write bun.lock unconditionally while the WRITE_PACKAGE_JSON gate on record_migrated_root skipped package.json, breaking the pair. Commit 9b53a1c split the function: record_migrated_root (install path, flushed later) and write_migrated_root (pm migrate/trust, calls write_target directly). The doc comment on write_migrated_root explains the reasoning, and the new ["pm migrate --dry-run", ...] test case in pnpm-lock-v9.test.ts asserts both files are written together. This resolves the concern.

Security risks

None identified. No untrusted input parsing changes; the pnpm YAML and package.json parsing paths are unchanged. The change is about when already-computed edits hit disk, not what is computed.

Level of scrutiny

High — this is core package-manager write-path logic with user-visible behavioral changes. The PR explicitly reverses two tests from #38333 and changes what --frozen-lockfile does during migration; the author correctly flags this as a judgment call. There are also three overlapping open PRs (#38754/#38775/#38780) touching update_package_json_after_migration. A maintainer should confirm the --frozen-lockfile semantics change and the interaction with those PRs.

Other factors

Test coverage is thorough: the new package.json edits block in pnpm-lock-v9.test.ts covers frozen/ci/production/dry-run/no-save/outdated/pm why/pm untrusted (untouched), install/lockfile-only/pm migrate/pm migrate --dry-run/add/remove/pm trust/silent (both written), and the out-of-sync frozen error. lockfile-only.test.ts covers all three foreign formats × frozen/dry-run/no-save. The one CI failure (test-http-chunk-problem.js) is unrelated to this change.

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.

2 participants