Skip to content

install: keep a workspace when a root alias names the registry package of the same name - #43567

Open
robobun wants to merge 2 commits into
mainfrom
robobun/1e888d94/alias-keeps-workspace-member
Open

robobun wants to merge 2 commits into
mainfrom
robobun/1e888d94/alias-keeps-workspace-member

Conversation

@robobun

@robobun robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Root "published": "npm:no-deps@1.0.0" plus a workspace no-deps@3.0.0: bun install exits 0 and drops the workspace. It leaves bun.lock, and its own dependencies are not installed. bun add published@npm:no-deps@1.0.0 prints Removed: 1.
  • Cause: the Tag::Npm arm of Package::parse_dependency (src/install/lockfile/Package.rs:1910). When the workspace's version is not in the range, the registry dependency replaces the root's dependency on the workspace. The match uses the package name behind the alias, not the folder.
  • A workspace without a version field skips that branch, so main already keeps both.

Fix

  • The replacement now also needs the dependency to install in the workspace's folder (external_alias.hash == name_hash). The replacement settles one folder, node_modules/<name>, and an alias installs in node_modules/<alias>. bun.lock already records this state for a workspace without a version.
  • Not changed: "no-deps": "1.0.0" in the root still replaces the workspace. That case needs a decision: bun install drops a workspace and its dependencies when the root depends on the registry package of the same name #43568.
  • Cost, for a maintainer to confirm: a bun.lock from an older bun misses the workspace. --frozen-lockfile fails on it once (lockfile had changes), and a plain bun install repairs it.
  • Verified: test/cli/install/bun-workspaces.test.ts, 7 new rows, 5 fail on bun 1.4.3-canary.1. Notes: other suites, self-review.

Background

  • The root has one implicit dependency per workspaces entry. A workspace that nothing depends on leaves the lockfile.
  • bun links an npm range to a workspace of the same name when its version is in the range (linked_workspace_path).
  • "published": "npm:no-deps@1.0.0" is an alias: the package no-deps installs in node_modules/published.
Notes

Result with this change (hoisted linker; the isolated linker links a-dep in packages/no-deps/node_modules):

"workspaces": { "": { ... }, "packages/no-deps": { "name": "no-deps", "version": "3.0.0", "dependencies": { "a-dep": "1.0.1" } } }
"packages": {
  "a-dep": ["a-dep@1.0.1", ...],
  "no-deps": ["no-deps@workspace:packages/no-deps"],
  "published": ["no-deps@1.0.0", ...],
}

A second bun install saves nothing, and bun install --frozen-lockfile passes.

Lockfiles written before this change. A bun.lock that an older bun wrote for this shape does not have the workspace. With this change bun install --frozen-lockfile fails once on it with lockfile had changes, but lockfile is frozen. A plain bun install adds the workspace and its dependencies and saves. The file was missing a workspace, so this is the same result as for a workspace folder added without a new lockfile (stock bun prints the same error there). The test bun.lock without the workspace that a root alias used to drop is out of date pins both steps. A load-time migration is not possible: a frozen install cannot add the workspace's dependencies.

Self-review (by hand). Checked: no other code on main repeats the replacement rule (the bun.lock loader adds one root dependency per workspaces entry and has no such rule). bun add, bun update, bun update --latest, bun remove, bun pm ls, bun why and bun outdated on the fixed shape, each followed by --frozen-lockfile. A registry package with a peer range that the workspace satisfies binds to the workspace again (on the loopback fixture stock bun warns incorrect peer dependency "host@1.0.0" there, because the workspace is gone). linkWorkspacePackages = false: the alias resolves from the registry and the workspace stays. Two aliases of the same package. An alias named like a second workspace: that folder conflict is the same as on main, and the first workspace is no longer dropped. One cost found: the frozen-lockfile result above.

Origin. The replacement rule is from #11177. Its only test for a dependency with a workspace's name is bun add bar@0.0.7 inside another workspace, which does not reach this branch.

Open PRs with tests that assume the drop. #37248 has the test aliased npm dependency colliding with a member's name. It asserts that bun.lock has no no-deps@workspace: entry for "my-alias": "npm:no-deps@1.0.0" plus a workspace without a version. main keeps the workspace in that shape today. #43468 has the history "a root alias takes the registry package of that name". Both need a new expectation for the alias rows if this lands first.

Other package managers on the alias fixture (loopback registry, root "published": "npm:host@1.0.0", workspace host@1.5.0 with a dependency leaf). npm 11.16, yarn 1.22 and yarn 4.18: node_modules/published is the registry copy, node_modules/host links the workspace, leaf is installed. pnpm 12.4: node_modules/published is the registry copy, packages/host stays an importer, leaf is in packages/host/node_modules.

Suites run with the debug build: bun-workspaces (89), bun-add (71), bun-update (159), bun-lock (40), bun-remove (14), overrides (7), catalogs (89), isolated-install (85), bun-install-registry (281). All pass. bun-install has 13 failures. All are bitbucket, gitlab or external URL tests, and the same 13 fail with the released build.


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

…e of the same name

A root dependency on the registry package that has a workspace's name
replaces the root's workspace dependency when the workspace's version is
not in its range, because both would install in node_modules/<name>.
An aliased dependency installs in node_modules/<alias>, so there is no
folder to share. It still replaced the workspace dependency, and the
workspace and its own dependencies left the install.

The replacement now needs the dependency to take the workspace's folder
name.
@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:56 PM PT - Sep 19th, 2026

✅ @robobun, your commit b05429866723411767608c33db3a7c5e07c0f140 passed in Build #118546! 🎉


🧪   To try this PR locally:

bunx bun-pr 43567

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

bun-43567 --bun

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: fc0a178b-7288-4e18-8203-40714a2aaf78

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7c982 and 24f1daf.

📒 Files selected for processing (2)
  • src/install/lockfile/Package.rs
  • test/cli/install/bun-workspaces.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

The dependency parser now preserves a workspace when a conflicting registry dependency uses an alias. Tests cover hoisted and isolated linkers, versioned and versionless workspaces, lockfile behavior, repeated installs, and frozen-lockfile installs.

Changes

Workspace alias handling

Layer / File(s) Summary
Alias resolution and validation
src/install/lockfile/Package.rs, test/cli/install/bun-workspaces.test.ts
parse_dependency replaces a workspace only for matching package-name hashes. Tests verify that aliased registry packages remain installed under the alias while the workspace and its dependencies remain intact.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: preserving a workspace when a root alias targets the registry package with the same name.
Description check ✅ Passed The description explains the problem, cause, fix, compatibility impact, verification steps, and test coverage. It does not use the exact template headings, but it provides the required information in …

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

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the sibling linked_workspace_path branch at src/install/lockfile/Package.rs:1887: an alias whose range the workspace does satisfy (e.g. published: "npm:no-deps@^3.0.0") still links node_modules/published to the workspace because that lookup keys on the real name, not the alias — that is pre-existing behavior on main and not changed by this diff, so it is not a regression here.

Extended reasoning...

The diff narrows one else if in the Tag::Npm arm so the workspace-override only fires when the dependency's alias equals the package's real name. The satisfying-range branch directly above it (linked_workspace_path) still matches on name_hash only, so an alias whose range happens to include the workspace version resolves to the workspace path rather than the registry package; I traced this and confirmed it is untouched by the PR and behaves identically on the base commit, so it is an adjacent pre-existing inconsistency rather than something introduced here. The new test matrix covers both linkers, version present/absent, and the bun add path, and asserts whole lockfile maps with toEqual, so it can fail for the right reason; the confirmed inline finding about older lockfiles failing --frozen-lockfile once after upgrade is already posted and is the item a human should weigh.

Comment thread src/install/lockfile/Package.rs
@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on bun 1.4.3-canary.1 (367d939) and with src/ at main on a debug build. bun bd test test/cli/install/bun-workspaces.test.ts fails 5 of the 7 new rows there. The 2 rows for a workspace without a version pass there by design. All 7 pass with this change, and the whole file passes (89 tests).

Manual repro: a root with "workspaces": ["packages/*"] and a workspace packages/host at version 1.5.0 with one dependency. Run bun install, then bun add published@npm:host@1.0.0. Stock bun prints Removed: 1, and packages/host leaves bun.lock.

The case where the root dependency has the workspace's own name is not changed here. It needs a decision: #43568.

…ses the workspace

A bun.lock written before the alias fix does not list the workspace.
A frozen install rejects it with 'lockfile had changes', and a plain
install adds the workspace.

@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 re-reviewed after the second commit and found no bugs in the change itself; since the PR now deliberately pins a one-time --frozen-lockfile failure for lockfiles an older bun wrote for this shape (rather than migrating them), a maintainer should confirm that trade-off before merging.

What was reviewed:

  • The narrowed guard at src/install/lockfile/Package.rs:1910: for a non-aliased dep npm().name is the package.json key, so external_alias.hash == name_hash holds and the existing override is unchanged; a self-alias like "no-deps": "npm:no-deps@ 1.0.0" still overrides, and only a differently named alias falls through.
  • Grepped src/install for other sites that replace or drop a workspace dependency by name_hash — this loop is the only one, so there is no sibling site left unfixed.
  • The 6 new rows cover both linkers, bun install and bun add, the versionless-workspace shape, no-op re-install and frozen install; the new "out of date bun.lock" test asserts the exact frozen-lockfile error and that a plain install repairs the lockfile.
Extended reasoning...

Overview

The PR changes one condition in Package<u64>::parse_dependency (src/install/lockfile/Package.rs:1910): the branch that replaces the root's implicit workspace dependency with a same-named npm dependency now also requires external_alias.hash == name_hash, i.e. the dependency must install into the workspace's own folder. An alias such as "published": "npm:no-deps@ 1.0.0" no longer removes the no-deps workspace and its transitive dependencies from the lockfile. test/cli/install/bun-workspaces.test.ts adds a describe.each over both linkers with three rows (out-of-range workspace version, versionless workspace, alias added via bun add) and, in the second commit, one test that hand-edits bun.lock to the shape an older bun produced and asserts --frozen-lockfile rejects it while a plain install repairs it.

Security risks

None identified. The change only affects which dependency entry the root package keeps for a workspace name collision; it does not touch path handling, integrity verification, network access, or credential flow. The tests use the Verdaccio harness and no public network.

Level of scrutiny

The Rust change is small and I verified the semantics of the new clause rather than trusting the names: dependency::parse_with_tag sets npm.name to the alias only for npm:-prefixed specs, otherwise to the key itself, so the equality check is exactly "alias differs from resolved name". I also confirmed no other site in src/install performs the same workspace replacement, so the bug class is contained to this loop. What keeps this from an approve is the product-level trade-off the author chose in the second commit: a lockfile written by a previous release for this shape now fails once under --frozen-lockfile and is pinned as intended behavior in a test instead of being migrated at load time. That is a user-visible compatibility decision, and the PR description also notes two open PRs whose tests assume the old drop behavior plus an unresolved sibling case ("no-deps": "1.0.0" in the root) tracked separately. A maintainer should weigh those explicitly.

Other factors

The test matrix is reasonably complete: both linkers, bun install and bun add, the versionless-workspace shape that already passed on main (regression guard), a no-op re-install assertion via savesLockfile: false and byte-equal lockfile text, and a frozen install at the end. Assertions use toEqual on whole objects (lockfile package resolutions, installed package.json contents) and toContain for the exact frozen-lockfile error. The multi-agent hunt ran to a dry streak with no findings, and the second commit addressed the compatibility point raised in my earlier inline comment by making it explicit rather than silent.

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