Skip to content

install: keep git, folder, tarball and workspace entries as written on bun update <name> - #38827

Closed
robobun wants to merge 1 commit into
mainfrom
farm/8b4039da/update-latest-keeps-non-registry-deps
Closed

install: keep git, folder, tarball and workspace entries as written on bun update <name>#38827
robobun wants to merge 1 commit into
mainfrom
farm/8b4039da/update-latest-keeps-non-registry-deps

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun update <name> --latest on a dependency declared from git, e.g. "repo-a": "git+file:///.../repo-a#v1.0.0", exits 0 and leaves package.json with "repo-a": "^1.0.1" and bun.lock with the registry package repo-a@1.0.1: the git dependency is silently replaced by the unrelated registry package of the same name. For a name the registry does not have (a file: folder, a local tarball) the command fails with error: GET <registry>/<name> - 404 instead.
  • bun update <name>@latest and bun update <name>@^2 on such an entry do the same thing (with @latest the entry is left as the literal string "latest"), and bun update "*" --latest, bun update --dev --latest and bun update <name> -r/--filter --latest expand to the same path.
  • Cause: the named path in PackageJSONEditor::edit (src/install/PackageManager/PackageJSONEditor.rs) rewrites the entry before the install without looking at what kind of literal it holds. It writes latest under --latest and the requested spec under <name>@<spec>, the install resolves that from the registry, and the second pass pins what was resolved. The only kind it already refused to touch was catalog: (line 1340), in both passes.
  • The bare bun update --latest editor (edit_update_entries, same file, line 490) and the -r/--filter resolver (src/install/PackageManager/PackageManagerEnqueue.rs:2449) already restrict themselves to npm ranges and dist-tags; the named editor was the one path without that check.

Fix

  • Widen the existing catalog: check to every entry kind the registry does not resolve: !Tag::infer(entry).is_npm(), i.e. anything other than an npm range or a dist-tag (catalog:, workspace:, git, github, folder, tarball, link:). Under bun update such an entry is now left as written by both passes, whatever was requested for it; bun add is unaffected and still replaces it.
  • Such an entry is still a named request, so its row is re-resolved during the install, which is what a plain bun update <name> already did for it and what the bare bun update --latest does for the same entry. --latest and <name>@<range> both mean "move within the registry", which has no meaning for an entry the registry does not resolve; npm and pnpm do not convert such entries into registry ranges either, and this is already the documented behaviour of bun update for catalog: references (tested with an explicit spec in test/cli/install/bun-add-catalog.test.ts), so the same rule now covers every kind.
  • Because the check runs in the second pass too, the write-back's unconditional workspace:* arm is no longer reached for a workspace:^ / workspace:~ entry named in bun update. install: keep workspace and other non-registry entries as written on bun update <name> #38847 fixes the remaining workspace cases (a plain range that links a member, requests bound to the root's implicit member rows); its widening of this same check is subsumed by this change, the rest of it is not.
  • docs/pm/cli/update.mdx states the rule in one line next to the existing catalog: one.
  • Not changed: entries declared as a registry range whose row resolves elsewhere (an override, or a range that links a workspace member) still go through the registry path on every route, bare included.
  • Verified with five rows plus one -r case in test/cli/install/bun-update-lockfile-sync.test.ts: explicit names with --latest, "*" --latest, plain names, no-deps@latest, no-deps@^2.0.0, and no-deps -r --latest where another workspace declares the same name from the registry and must still move. The fixture declares no-deps from a local git repository while the registry has no-deps@2.0.0, plus file: folder and tarball entries and a workspace:^ member; with the editor from main all six fail (the git entry becomes ^2.0.0 / latest, file: entries 404, workspace:^ becomes workspace:*), with this change all six pass.
  • bun-update-lockfile-sync.test.ts, bun-update.test.ts, bun-update-transitive.test.ts, bun-add-catalog.test.ts and catalogs.test.ts pass with the debug build.

Background

  • bun update <name> edits package.json in two passes around the install: before the install, edit() may rewrite the named entries to what should be resolved (latest under --latest, the spec given as <name>@<spec>); the install resolves whatever package.json now says; after the install, edit() runs again and pins the resolved version in the declared range style. Patterns, the --dev/--prod selectors and -r/--filter all feed this same function.
  • dependency::Tag::infer classifies a version literal: ^1.0.0 is Npm, pre-2 is DistTag, npm:foo@^1 follows the part after the alias, git+... / github: are Git / Github, file:... is Folder or Tarball, workspace:* is Workspace, catalog: is Catalog. Tag::is_npm() is true for exactly the two kinds a registry manifest answers, and is the predicate the resolver uses to decide whether to fetch one.
Earlier revision of this PR

The first revision only gated the --latest rewrite (the if update_to_latest branch further down in edit()) on is_npm(). Self-review pointed out that the same entries were still converted by bun update <name>@latest / <name>@<range>, and that the second pass still rewrote workspace:^ to workspace:*, because the rule belongs on the per-entry check that already existed for catalog: and runs in both passes. The current revision moves it there and adds the explicit-spec and plain-name rows to the test.

@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: 17 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: c21f3211-f995-439f-a945-f88a6869395d

📥 Commits

Reviewing files that changed from the base of the PR and between 95cb693 and c2f2216.

📒 Files selected for processing (3)
  • docs/pm/cli/update.mdx
  • src/install/PackageManager/PackageJSONEditor.rs
  • test/cli/install/bun-update-lockfile-sync.test.ts

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

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: closed in favour of #38847

Reproduced on main (debug build, 2c2ef7cbf): with "no-deps" declared from a local git repository while the registry has no-deps@2.0.0, bun update no-deps --latest (and no-deps@latest / no-deps@^2.0.0) rewrote package.json to the registry package; file: folder and tarball entries named the same way failed with a 404; bun update "*" --latest and bun update <name> -r --latest behaved the same.

The fix here (widening the catalog: check in PackageJSONEditor::edit to every entry kind the registry does not resolve) is now also part of #38847 as of its 2776a09, together with the remaining workspace-specific cases, so that PR fixes everything this one was opened for and this one is closed to avoid two PRs carrying the same hunk. The extra coverage from this branch (git and tarball fixtures, the "*" route, the mixed -r --latest case, the docs line) is offered to #38847 in #38847 (comment); the branch stays available at c2f2216 should #38847 not land.

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

LGTM — a one-clause guard that stops bun update <name> --latest from converting non-registry entries (git, folder, tarball, workspace) into <name>@latest registry lookups.

What was reviewed:

  • Confirmed Tag::infer classifies git+file:, file:, workspace:, link: as non-npm and npm:<name>@… / dist-tags as npm, so the new .is_npm() gate matches the bare bun update --latest path (edit_update_entries) exactly.
  • Verified is_npm() is tag < 3 (Uninitialized/Npm/DistTag) and infer never returns Uninitialized, so aliased and dist-tag entries still rewrite to latest; the dep-with-tags^3.0.0 assertion covers this.
  • New tests cover the explicit-name form, the "*" pattern form, and -r where a sibling workspace declares the same name from the registry and must still move — the git no-deps entry is the case that silently swapped packages before.
Extended reasoning...

Overview

The PR touches one line of runtime code in src/install/PackageManager/PackageJSONEditor.rs: the pre-install rewrite in edit() that turns a named entry into latest under bun update <name> --latest now first checks dependency::Tag::infer(version_literal).is_npm(). The rest of the diff is three new test cases plus supporting helpers (gitRepo, setupNonRegistryDeps) in test/cli/install/bun-update-lockfile-sync.test.ts.

Correctness

I traced Tag::infer (src/install/dependency.rs:753) for every literal shape used in the tests: git+file: → Git, file:./dir → Folder, file:./x.tgz → Tarball, workspace:* → Workspace, pre-2 → DistTag, ^1.0.0 → Npm, npm:name@~1.0.0 → recurses to Npm. is_npm() is (tag as u8) < 3, i.e. Uninitialized/Npm/DistTag; infer never returns Uninitialized. So the guard admits exactly the entries the registry answers and skips the rest — the same predicate the bare --latest path in edit_update_entries (same file, ~line 490) and the -r resolver in PackageManagerEnqueue.rs already use. The named and bare forms now agree, which is the stated goal.

The gate is applied to version_literal after with_alias_of re-attaches any npm:<name>@ prefix from the existing entry, so aliased npm entries (npm:no-deps@~1.0.0) still infer as Npm and are rewritten to npm:no-deps@latest. The existing aliased --latest test row and the new dep-with-tags → ^3.0.0 assertion pin that behavior. update_to_latest is only true when subcommand == Update, so bun add is unaffected.

Security risks

None. This narrows a rewrite; it cannot cause a package that was not previously fetched to be fetched. If anything it removes a supply-chain footgun (silently swapping a git dependency for the registry package of the same name).

Level of scrutiny

Low-to-medium: a single boolean added to an existing conditional in the package manager, mirroring an established sibling check in the same file. No new unsafe, no allocation/lifetime changes, no cross-platform branching. The tests are hermetic (local Verdaccio, git+file:// repo built in the temp dir with GIT_CONFIG_NOSYSTEM=1), assert exact package.json / bun.lock contents, and include the positive case (registry-declared workspace still moves under -r --latest).

Other factors

No prior human review comments; the only timeline entry is a CodeRabbit rate-limit notice. No CODEOWNERS covers src/install/. The bug-hunting pass found nothing. Test helpers follow the file's existing conventions (setup, run, expectInSync, describe.concurrent).

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:05 PM PT - Aug 14th, 2026

@robobun, your commit f7d9008 is building: #97140

1 similar comment
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:05 PM PT - Aug 14th, 2026

@robobun, your commit f7d9008 is building: #97140

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:05 AM PT - Aug 15th, 2026

@robobun, your commit c2f2216 has 1 failures in Build #97769 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 38827

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

bun-38827 --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.

LGTM — one-line guard bringing the named --latest path in line with the bare path.

What was reviewed

  • Confirmed Tag::is_npm() covers exactly Npm + DistTag (tags 0-2; infer never returns Uninitialized), so aliased npm:pkg@… literals still rewrite while git/folder/tarball/workspace stay put.
  • Checked the guard placement: it runs after with_alias_of, so an aliased npm entry is still classified as npm and moved.
  • Tests cover explicit names, the "*" expansion, and the -r mixed case (git stays, registry sibling moves), with strict-equal assertions on package.json, lockfile, installed version, and a frozen-lockfile round-trip.
Extended reasoning...

Overview

One-line change to PackageJSONEditor::edit (src/install/PackageManager/PackageJSONEditor.rs:1372): the before-install rewrite of a named entry to latest under bun update <name> --latest is now gated on dependency::Tag::infer(version_literal).is_npm(). Three new test cases in test/cli/install/bun-update-lockfile-sync.test.ts plus gitRepo/setupNonRegistryDeps helpers.

Security risks

None. This narrows when a rewrite happens; no new I/O, allocation, or untrusted-input parsing. The git repo in tests is local (git+file://) with GIT_CONFIG_NOSYSTEM=1, explicit author/committer env, and --no-gpg-sign for hermeticity.

Level of scrutiny

Low-to-medium. The fix is a strict narrowing that only makes --latest more conservative — non-registry literals are now left as written instead of being swapped for a registry lookup of the same name. The predicate and the pattern are copied from the sibling edit_update_entries path (which already gates on tag != Npm && (tag != DistTag || !update_to_latest)), and the PR description enumerates the other --latest paths (bare update, -r/--filter resolver in PackageManagerEnqueue.rs) that already had the check. I verified is_npm() is (self as u8) < 3 over Uninitialized=0/Npm=1/DistTag=2, and that the guard sits after with_alias_of so npm:name@range aliases are still classified as npm and rewritten.

Other factors

Test construction is solid: no-deps is declared from a local git repo while the Verdaccio registry has no-deps@2.0.0, so on main the assertion expect(await resolutions(dir, "no-deps")).toStrictEqual([gitResolution]) fails loudly (the git entry becomes ^2.0.0), and the folder/tarball entries 404 — the test proves the mechanism, not just absence of crash. The -r case additionally asserts the registry-declaring workspace still moves to ~2.0.0, so the guard doesn't over-suppress. dep-with-tags: pre-2 is the positive control that dist-tags still rewrite to ^3.0.0. All assertions are toStrictEqual on the full dependencies object plus expectInSync (frozen-lockfile). Tests follow harness conventions (await using, bunEnv spread, concurrent describe, no public network). No CODEOWNERS on this path.

…n bun update <name>

A named update rewrote whatever the package.json entry held when the
command carried --latest or an explicit <name>@<spec>: a git, file:,
tarball or workspace: entry was replaced with `latest` or the requested
range before the install, resolved from the registry, and written back
as a range of the unrelated registry package of that name (or the
command failed with a 404 when the registry has no such name). The
write-back after the install also turned a `workspace:^` entry into
`workspace:*`. The editor already skips `catalog:` entries in both
passes; widen that check to every entry kind the registry does not
resolve, so such entries are only re-resolved, like the bare update
already treats them.
@robobun
robobun force-pushed the farm/8b4039da/update-latest-keeps-non-registry-deps branch from 45c929c to c2f2216 Compare August 15, 2026 09:00
@robobun robobun changed the title install: keep git, folder, tarball and workspace entries as written on bun update <name> --latest install: keep git, folder, tarball and workspace entries as written on bun update <name> Aug 15, 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.

LGTM — widens the existing per-entry catalog: skip in PackageJSONEditor::edit to !Tag::infer(entry).is_npm(), so bun update <name> no longer converts git/folder/tarball/workspace: entries into registry ranges.

What was reviewed

  • Confirmed Tag::is_npm() = {Npm, DistTag} on inferred tags (empty string → DistTag), so freshly-created empty slots and npm:alias@… entries still fall through, and the dep-with-tags control row proves dist-tags are still rewritten by --latest.
  • Checked the earlier keep_catalog_reference guard — it's already gated on subcommand != Update, so widening it is unnecessary.
  • Verified this brings the named path in line with edit_update_entries (bare bun update --latest), which already gates on tag != Npm && (tag != DistTag || !--latest).
  • Test fixture covers --latest, "*" --latest, plain names, @latest, @^2, and -r with a sibling workspace that must still move; assertions check package.json, bun.lock, resolutions, and installed contents.
Extended reasoning...

Overview

The PR changes one predicate in src/install/PackageManager/PackageJSONEditor.rs (the final write-back loop of edit()): the existing bun update skip for catalog: entries becomes a skip for every entry whose inferred tag is not Npm/DistTag. This runs in both the before-install and after-install passes, so a git/github/folder/tarball/symlink/workspace: entry is now left byte-identical however it is named — with --latest, with an explicit @spec, via a pattern, or via -r/--filter. One doc line and ~100 lines of tests accompany the two-line code change.

Security risks

None introduced. The old behaviour was arguably the riskier one: bun update <name> --latest on a git entry silently swapped it for whatever registry package shares the name. The fix narrows what the registry is consulted for.

Level of scrutiny

Moderate: this is a user-visible behaviour change in bun update, but it is a targeted correctness fix that aligns the named-update path with what the bare bun update --latest editor and the -r/--filter resolver already do (both already restrict themselves to npm ranges/dist-tags). The design question — silently keep the entry vs. error — is answered by precedent: catalog: already behaves this way, npm/pnpm do not convert such entries, and the entry's row is still re-resolved during the install. The PR description explicitly reasons through each of these.

Other factors

  • Tag::infer never returns Uninitialized (empty → DistTag), so is_npm() on an inferred tag is exactly Npm || DistTag; freshly-allocated b"" slots still pass the check.
  • The now-unreachable resolution::Tag::Workspace => b"workspace:*" write-back arm is intentional: the PR notes #38847 handles the remaining workspace cases, and under bun add that arm is still live.
  • Tests are hermetic (Verdaccio + local git init under gitEnv with GIT_CONFIG_NOSYSTEM=1), assert exact toStrictEqual shapes on both files plus lockfile resolutions and installed node_modules, and include a positive control (dep-with-tags) and a mixed -r case where one workspace's registry entry must still move.
  • No CODEOWNERS cover the touched paths; no prior human review comments to address.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up on overlap: #38847 (named update of workspace member entries) now skips every non-registry entry at the top of the write-back loop in edit() (Subcommand::Update && !Tag::infer(e_string).is_npm() -> continue, the check that previously only covered catalog:), in both passes. If it lands first, the is_npm() guard this PR adds on the update_to_latest line is no longer reached for such entries on the named path, and this PR's tests pass either way; if this one lands first, #38847 rebases trivially (different lines). The explicit-spec forms (bun update <folder-dep>@^2.0.0, @latest) are covered over there, so no need to extend this one.

@robobun robobun closed this Aug 15, 2026
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