Skip to content

install: sort a dependency group in bun add only when it gets a new package name - #46253

Open
robobun wants to merge 1 commit into
mainfrom
robobun/31842937/add-sort-only-new-name
Open

robobun wants to merge 1 commit into
mainfrom
robobun/31842937/add-sort-only-new-name

Conversation

@robobun

@robobun robobun commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Related to #11536

Problem

  • bun add sorts the group it writes to also when it adds no package: bun add zeta@./zeta rewrites zeta, beta, alpha as alpha, beta, zeta.
  • edit (src/install/PackageManager/PackageJSONEditor.rs:973) rebuilds the group for each resolved request and always sorts it.

Fix

  • The rebuild sorts only when a request gives the group a package name it did not have. Otherwise no entry moves.
  • A group that gets a new name is sorted as before (decision 3 of install: save an edited package.json over its own text #45812).
  • Self-reviewed: 28 concerns raised, 27 addressed. Not done: tracking issues for three bugs of main (Notes).
  • Verified: test/cli/install/bun-add.test.ts (34 new cases, 22 fail on main). Release builds of main and this branch differ only in those cases over test/cli/install/.

Background

Downsides

  • bun add <a package the group has> no longer sorts the group.
  • A key that is still a literal at exit 0 (bun add --optional ./missing, on main too) is now the last entry. Main sorts the group.
  • .text grows by 768 bytes. Instructions in edit: bun add <new name> 26,736 to 25,774, bun update <name> 5,656 to 5,652.
Notes

What counts as a new package name

  • A new entry.
  • An entry that -d, --optional, --peer or --save-prod moves in from another group.
  • A folder, tarball or git URL that resolves to a name the group did not have.

When every request names an entry that the group has, the group keeps its order. The tests cover bun add <name>@<spec>, bun install <name>@<spec>, the group flags, -g, --filter, --catalog, bun link <name> --save, and a folder of a package that the group has.

bun add still prints the whole file. A file that is not in the format Bun prints (comments, CRLF, objects on one line) is printed again when a value changes, as on main. Writes that keep the text are #45812.

The ways into the rebuild of a group

request sorted on main sorted now test case (bun-add.test.ts)
a name that is new to the group yes yes bun add gamma@./gamma: a new package
a name that a group flag moves in yes yes bun add -d delta@./delta
a name over an entry that is not a string yes no bun add zeta@./zeta: an entry that is not a string
a name that the group has, after the install yes no bun add zeta@./zeta, bun add zeta@./zeta2
a folder or URL, before the install yes no bun add ./zeta2
a folder or URL of a new package, after the install yes yes bun add ./gamma
a folder or URL of a package the group has, after the install yes no bun add --peer ./zeta2: another folder of an optional peer

In the last row the entry with the name takes the place of the entry it replaces. Main removed the declared entry and sorted.

npm, pnpm and yarn sort all four groups on each save. This PR does not follow them for a group that gets no new name, as bun remove (#44968) and bun update <name> (#38333) already do not. There is no flag. A group in sorted order gets the bytes of main, with the one exception under Downsides.

Not in this PR

Bugs that main has and this PR does not change

No tracking issue is filed from this PR for these.

  • bun add --optional ./missing (the folder does not exist) exits 0 and saves "./missing": "./missing" in package.json and bun.lock. 1.4.2 and 1.4.3 exit 1 with error: Invalid dependency name "./missing" and write nothing. On this branch that key is the last entry of its group. Main sorts the group, so the key is first.
  • bun add ./zeta zeta@./zeta2 over a declared zeta: ./zeta exits 0 and saves "./zeta": "./zeta" beside zeta. The key is first on main and last on this branch.
  • bun add --peer ./zeta2 over an optional peer zeta writes "zeta": "./zeta2" two times into peerDependencies of bun.lock, on 1.4.3, on main and on this branch. The four cases for that command assert package.json only.

Measurements

Release builds of main 91e7e3b140 and of this branch, same toolchain. The project has local folder packages, so no command needs a registry. The group is written as zeta, beta, alpha.

Entries that change in package.json per command (diff lines), main to this branch:

command main branch
bun add zeta@./zeta 4 0
bun add zeta@./zeta2 4 2
bun add -d zeta@./zeta (group in devDependencies) 4 0
bun add ./zeta2 4 2
bun add ./zeta2 (zeta only in devDependencies) 6 2
bun add --peer ./zeta2 (over an optional peer) 4 2
bun add gamma@./gamma 5 5
bun add ./gamma 5 5
bun add zeta@./zeta gamma@./gamma 5 5
bun add ./zeta 0 0

Counts per command (gdb breakpoints on the profile builds), main to this branch:

command sort passes in edit rebuilds of the group writes of package.json
bun add zeta@./zeta 1 to 0 1 to 1 1 to 0
bun add zeta@./zeta2 1 to 0 1 to 1 1 to 1
bun add ./zeta2 1 to 0 1 to 1 1 to 1
bun add gamma@./gamma 2 to 1 2 to 2 1 to 1
bun add ./gamma 2 to 1 2 to 2 1 to 1
bun add --optional ./missing 1 to 0 1 to 1 1 to 1
bun add ./zeta 0 to 0 0 to 0 0 to 0
bun update zeta 0 to 0 0 to 0 0 to 0
  • bun add zeta@./zeta on the group that is not sorted: the inode and the timestamps of package.json do not change on this branch, and cmp finds no difference. The tree is still rebuilt. The write is skipped because the printed bytes are the bytes of the file.
  • Allocator calls (mimalloc entry points, main thread) inside the edit that runs after the install, three runs each: bun add zeta@./zeta 10 to 8, bun add gamma@./gamma 10 to 8.
  • User instructions inside edit, both calls summed, ten folder dependencies that are not sorted, minimum of 5 runs (largest spread 130): bun update zeta 5,656 to 5,652, bun add zeta@./zeta 19,109 to 16,174, bun add gamma@./gamma 26,736 to 25,774, bun add ./gamma 29,773 to 28,847.
  • Size: .text 81,665,765 to 81,666,533 bytes (+768). edit 14,090 to 14,713 bytes. .rodata does not change.
  • A group in sorted order, 17 commands: 1 of 17 package.json differs between the two builds (bun add --optional ./missing, the first bug above) and 0 of 17 bun.lock. With the group not sorted, bun.lock is the same in all 17.
  • All 100 files of test/cli/install/, release builds: main 12,127 pass and 23 fail, this branch 12,149 pass and 1 fail. The 22 that differ are the new cases. The 1 that fails on both is semver.test.ts, which cannot find the package semver on this machine.

Tests

  • bun-add.test.ts, "the order of a dependency group after bun add": 34 cases, each compares the text of package.json. 22 fail on main. The 12 others pin what must not change.
  • 32 cases use local folders. 2 of them run in an installed project and end with bun install --frozen-lockfile. The other 2 cases use the registry of the test file for the command of the bun update removes comments in package.json #11536 thread, bun install --dev <alias>@npm:<package>@<version>.
  • Each clause of the change fails a case when it is removed. Without the gain of a folder that resolves to a new name: 3. Without the gain of a named new entry: 8. With main's removal of the declared entry in place of the swap: 3, or 2 when it counts as a gain. With either arm of the new index dropped: 1 and 2.
  • On the debug build: bun-add.test.ts 253 pass. bun-link, bun-remove, bun-update, bun-add-filter and bun-add-catalog pass with a test timeout of 180 s. With the default 5 s some of their tests time out on this machine's debug build.

… package name

`bun add` sorted the target dependency group of package.json each time
it added or replaced an entry. So `bun add zeta@./zeta`, for an entry
that the group already has, moved every entry of a group that was not
in sorted order.

The rebuild of the group now sorts only when a request gives the group
a package name that it did not have. A request that names an entry of
the group changes that entry in its place. A group that gets a new name
is sorted as before.
@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 19 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: cb8af51f-fd53-42b3-88d0-ae47bfa2b76e
📥 Commits

Reviewing files that changed from the base of the PR and between 9b4284f and 99f99be.

📒 Files selected for processing: 2
  • src/install/PackageManager/PackageJSONEditor.rs
  • test/cli/install/bun-add.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@robobun

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main 91e7e3b140 with a release build. The project has local folder packages, and dependencies is written as zeta, beta, alpha. After bun install, the command bun add zeta@./zeta exits 0 and writes package.json again with the group as alpha, beta, zeta. bun add ./zeta leaves the file unwritten.

test/cli/install/bun-add.test.ts ("the order of a dependency group after bun add") has 34 cases. 22 fail on main and all 34 pass on this branch.

CI: Buildkite build #129695 passed. Four tests outside test/cli/install/ passed on a retry. The claude-find-issues check failed. That job searches for related issues and does not build or test the change.

PR: #46253

@robobun

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:56 PM PT - Oct 20th, 2026

✅ @robobun, your commit 99f99be92dd0dde362dae95a7c968ce5e8a98c77 passed in Build #129695! 🎉


🧪   To try this PR locally:

bunx bun-pr 46253

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

bun-46253 --bun

@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-visible bun add output (when a group is re-sorted) and the PR itself lists two intentional downsides, a human look at that product decision would still be worthwhile.

What was reviewed:

  • The swap(j, k) + remove(k) + k = if j > k { j - 1 } else { j } rewrite in edit(): traced both j < k and j > k, and the duplicate-key case where the reverse loop matches more than once — every unvisited index stays unvisited-once and k keeps pointing at the re-keyed entry.
  • gained_name across aliased (new empty slot), non-aliased resolved (!replaced_declared), unresolved literal, and bun update paths — each consumer matches the table in the description; the unresolved ./missing key landing last instead of sorted is a pre-existing exit-0 bug, not introduced here.
  • The new test matrix: hermetic folder packages, pipes drained concurrently, using tempDir; the one serial it.each uses the file-level registry/package_dir while the concurrent siblings use their own temp dirs, so they do not share state.
Extended reasoning...

The change touches one Rust function, edit() in src/install/PackageManager/PackageJSONEditor.rs (+20/-8), gating the existing alphabetize call behind a new gained_name flag and reordering the stale-duplicate removal so the re-keyed entry keeps the declared entry's position, plus 315 lines of tests in test/cli/install/bun-add.test.ts. It touches no security-sensitive surface (no auth, crypto, path handling from untrusted input, or network). The index arithmetic and the flag's data flow check out and the test matrix covers the stated variants, so no findings; the reason for deferring rather than approving is that this is a deliberate behavior change to what bun add writes, with downsides the author acknowledges (re-adding a known name no longer sorts; an unresolved literal key now lands last), which is a maintainer judgment rather than a correctness question. No CODEOWNERS entry covers the changed files and there are no third-party review events on the PR.

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.

1 participant