Skip to content

install: don't create package.json for bun add --dry-run in an empty directory - #36694

Open
robobun wants to merge 3 commits into
mainfrom
claude/farm/612a150f/add-dry-run-no-package-json
Open

robobun wants to merge 3 commits into
mainfrom
claude/farm/612a150f/add-dry-run-no-package-json

Conversation

@robobun

@robobun robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #13244.

Repro

cd "$(mktemp -d)" && ls -A   # empty
bun add zod --dry-run
ls -A                        # package.json   <- created despite --dry-run

Same for bun install zod --dry-run, bun link --dry-run and bun unlink --dry-run. Expected: --dry-run performs no writes.

Cause

PackageManager::init walks up from cwd looking for package.json. When none is found and the command is bun install <pkg>, it writes {"dependencies": {}} to disk so the add can proceed. bun add, bun link and bun unlink each catch the MissingPackageJSON error and call attempt_to_create_package_json() before retrying init. All of this happens before options.load() reads cli.dry_run.

Fix

bun add / bun install <pkg>: when cli.dry_run is set and no package.json exists, init() holds Fd::invalid() for the root file instead of creating it, fills ROOT_PACKAGE_JSON_PATH from the computed path instead of get_fd_path(fd), and seeds workspace_package_json_cache with an in-memory {"dependencies": {}} entry so every later lookup hits cache. The bun add create-then-retry path is folded into init(), so the retry wrapper in update_package_json_and_install_and_cli is removed.

bun link / bun unlink: under --dry-run the MissingPackageJSON error now propagates ("could not find a package.json") instead of writing a file and then failing on the missing name.

Verification

$ USE_SYSTEM_BUN=1 bun test cli/install/bun-add.test.ts -t 'should not create package.json'
(fail) ... bun add ...      -> ["package.json"]
(fail) ... bun install ...  -> ["package.json"]
(fail) ... bun link ...     -> "package.json missing name"
(fail) ... bun unlink ...   -> "package.json missing name"

$ bun bd test cli/install/bun-add.test.ts -t 'should not create package.json'
4 pass, 0 fail

Full bun-add.test.ts suite: 58 pass, 0 fail.


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/bun-add.test.ts

…y directory

`bun add <pkg> --dry-run` and `bun install <pkg> --dry-run` in a
directory without package.json were writing `{"dependencies": {}}` to
disk before the dry_run flag was ever consulted.

`PackageManager::init` walks up looking for package.json and, for
`bun install <pkg>`, creates one when none is found so the add can
proceed. `update_package_json_and_install_and_cli` does the same for
`bun add` on the MissingPackageJSON retry path. Both happened
unconditionally.

Skip the disk write when `cli.dry_run` is set: hold an invalid fd for
the root file, fill `ROOT_PACKAGE_JSON_PATH` from the computed path
instead of `get_fd_path`, and seed `workspace_package_json_cache` with
an in-memory `{"dependencies": {}}` so later lookups never touch disk.
Fold the `bun add` creation path into `init()` so dry-run doesn't
need the create-then-retry dance.

Fixes #13244.
@coderabbitai

coderabbitai Bot commented Aug 1, 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: 7 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: bd73b8a9-8e7a-4456-9c4e-e37ad72e4be2

📥 Commits

Reviewing files that changed from the base of the PR and between f91d5c9 and da3021f.

📒 Files selected for processing (8)
  • src/install/PackageManager.rs
  • src/install/PackageManager/CommandLineArguments.rs
  • src/install/PackageManager/PackageManagerDirectories.rs
  • src/install/PackageManager/WorkspacePackageJSONCache.rs
  • src/install/PackageManager/updatePackageJSONAndInstall.rs
  • src/runtime/cli/link_command.rs
  • src/runtime/cli/unlink_command.rs
  • test/cli/install/bun-add.test.ts

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

@github-actions github-actions Bot added the claude label Aug 1, 2026

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

Beyond the inline nits, I checked two things and ruled them out: (1) downstream reads of manager.root_package_json_file with the new Fd::invalid() sentinel — the only field reader is pm_trusted_command.rs (different subcommand), and init_runtime_manager already stores Fd::invalid() there, so this is an established pattern; (2) cache-key agreement between seed_with_contents(original_package_json_path) and the later get_with_path(root_package_json_path) in updatePackageJSONAndInstall.rs — both derive from the same cwd via b"/package.json" and both normalize to POSIX separators on Windows, so the seeded entry is hit.

Extended reasoning...

This PR threads a synthetic invalid-fd + seeded-cache path through PackageManager::init() for the dry-run/no-package.json case, and folds the bun add create-then-retry into init(). The two inline findings are nits (dead retry arm, unfixed link/unlink siblings). I additionally traced the invalid Fd through the struct field to confirm no other consumer reads it on the add/install path, and checked that the seeded cache key matches both get_with_path call sites in the install-with-manager flow (including Windows separator normalization). Given the change touches core init control flow and introduces a new sentinel-fd pattern, deferring to human review rather than approving.

Comment thread src/install/PackageManager.rs
Comment thread src/install/PackageManager/updatePackageJSONAndInstall.rs Outdated
…unlink

The create-then-retry in update_package_json_and_install_and_cli became
dead once init() handles Subcommand::Add directly; drop it along with the
now-unused import and the no-longer-needed cli.clone().

Apply the same dry-run guard to bun link and bun unlink: both were
catching MissingPackageJSON and writing package.json unconditionally.
Under --dry-run they now propagate the error ("could not find a
package.json") instead of touching disk.
Comment thread src/install/PackageManager.rs Outdated
Comment thread src/install/PackageManager.rs Outdated
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:02 AM PT - Aug 1st, 2026

@robobun, your commit da3021f is building: #87163

Comment thread src/install/PackageManager.rs Outdated
Comment thread src/install/PackageManager/WorkspacePackageJSONCache.rs Outdated
Comment thread src/install/PackageManager/updatePackageJSONAndInstall.rs Outdated
Comment thread src/install/PackageManager.rs
Comment thread src/install/PackageManager/WorkspacePackageJSONCache.rs

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

Both earlier nits are addressed — the link/unlink sibling sites now guard on !dry_run with tests, and the dead retry path in update_package_json_and_install_and_cli is gone. I didn't find any new issues in the follow-up commits.

Deferring only because this threads a synthetic Fd::invalid() root file through PackageManager::init() and adds a cache-seeding path that all install/add flows depend on — worth a maintainer glance at the approach rather than an auto-approve.

Checked: root_package_json_file is otherwise only read by bun pm trusted (unaffected here), and the runtime-init path already stores Fd::invalid() in that slot, so the sentinel is a known-safe state. seed_with_contents mirrors get_with_path's Windows path-normalization, so both cache lookups in updatePackageJSONAndInstall should hit the seeded entry.

Extended reasoning...

Overview

Fixes #13244: bun add|install <pkg>|link|unlink --dry-run in an empty directory was writing package.json to disk before options.dry_run was consulted. The fix moves the create-or-synthesize decision into PackageManager::init(): under --dry-run it holds Fd::invalid() for the root file, fills ROOT_PACKAGE_JSON_PATH from the computed path instead of get_fd_path(fd), and seeds workspace_package_json_cache with an in-memory {"dependencies": {}} entry so downstream lookups never touch disk. bun link/bun unlink now propagate MissingPackageJSON under --dry-run instead of writing a file. Eight files touched; four new tests covering all four commands.

Follow-up since last review

Commit 25703472 addressed both nits I raised: (1) the link_command.rs/unlink_command.rs sibling sites now gate attempt_to_create_package_json() on !dry_run, with matching tests; (2) the dead _-arm retry in update_package_json_and_install_and_cli was removed along with the cli.clone() and unused import. Commit da3021fb trimmed the paragraph-length comments that comment-cop flagged. All prior inline threads are resolved.

Security risks

None. This narrows disk writes under an explicit no-write flag. No new untrusted-input parsing, no auth/crypto/permission surfaces.

Level of scrutiny

Medium-high. PackageManager::init() is the entry point for every install/add/remove/update/link/pm invocation, and this change introduces a sentinel fd that flows through the rest of init and into the manager struct. I traced downstream reads: manager.root_package_json_file is only otherwise consumed by pm_trusted_command.rs (a bun pm path not reachable from add/install), and the runtime-init constructor at PackageManager.rs:2334 already uses Fd::invalid() for this field, so the invalid-fd state is precedented. The new seed_with_contents mirrors get_with_path's key normalization (Windows backslash→slash) and _path_storage ownership pattern exactly, so both get_with_path calls in updatePackageJSONAndInstall (via original_package_json_path and via top_level_dir() + /package.json) should hit the seeded entry after posix-normalization.

Other factors

Test coverage is good: four new tests exercise add/install (positive — resolves against the dummy registry, exits 0, dir stays empty modulo .cache) and link/unlink (negative — surfaces "could not find a package.json" via the crash-handler's MissingPackageJSON message, exits nonzero, dir stays empty). PR description shows USE_SYSTEM_BUN=1 fails / bun bd passes for all four, and the full 58-test suite passes. CI build #87163 is running on the latest commit.

Why not auto-approve

Nothing is wrong that I can see. But the synthetic-file approach in init() is a design choice a maintainer should ratify — an alternative would have been to reorder options.load() before the create step, or to gate creation on cli.dry_run without the cache-seed path. The chosen approach is reasonable (it lets the full add pipeline run and print "installed BaR@0.0.2" as the test asserts), but it's the kind of structural decision that belongs to a human reviewer rather than a bot.

@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

CI on da3021f: the new dry-run tests pass on every lane. The only [new] red is bun-upgrade.test.ts on windows-aarch64 ("Canary builds are not available for this platform yet"), which is about upstream canary artifact availability and not touched by this diff. Everything else is tagged flaky by the CI helper (memory-threshold leak tests, install-proxy SIGTERM timeout, hot-reload EBADF, Windows metadata encoding) and passed on retry or alone. Diff is green; ready for review.

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.

--dry-run should not create a package.json file

2 participants