From c0e8ddeb4d5eef5e5af7322c32e596aef5875714 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 07:13:23 +0000 Subject: [PATCH 1/8] install: hoist workspace members in path order, not package-name order When two workspaces depend on conflicting versions of the same package, the hoisted linker picks whichever workspace it processes first for the root node_modules slot. DepSorter ordered workspace entries by package name, so a workspace named `@libs/lib` would beat `my-app` even though its directory (`libs/lib`) sorts after `apps/app`. npm orders workspaces by relative path, so the app wins root there and transitive peer dependents dedupe onto it. Sort workspace-behavior dependencies by their workspace path before falling back to name, which matches npm and stops @nestjs/core and similar peer-shared packages from being nested under every consumer. Fixes #9838 --- src/install/lockfile.rs | 18 +++ test/cli/install/bun-install-registry.test.ts | 149 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/install/lockfile.rs b/src/install/lockfile.rs index ac7f20716b6d..1788eb14060e 100644 --- a/src/install/lockfile.rs +++ b/src/install/lockfile.rs @@ -228,6 +228,24 @@ impl<'a> DepSorter<'a> { Ordering::Less => true, Ordering::Greater => false, Ordering::Equal => { + // npm's hoister visits workspaces in path order, so the + // path-alphabetically-first workspace's direct deps win the + // root node_modules slot. Sorting by package name here breaks + // peer-sharing packages like @nestjs/core when a scoped + // workspace name (`@libs/...`) sorts ahead of the app that + // actually provides the peer (#9838). + if l_dep.behavior.is_workspace() + && l_dep.version.tag == dependency::Tag::Workspace + && r_dep.version.tag == dependency::Tag::Workspace + { + let l_path = l_dep.version.workspace().slice(string_buf); + let r_path = r_dep.version.workspace().slice(string_buf); + match strings::order(l_path, r_path) { + Ordering::Less => return true, + Ordering::Greater => return false, + Ordering::Equal => {} + } + } strings::order(l_dep.name.slice(string_buf), r_dep.name.slice(string_buf)) == Ordering::Less } diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 225eaf863dcc..f83a8b7799a1 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -3923,6 +3923,155 @@ describe("hoisting", async () => { assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); + // https://github.com/oven-sh/bun/issues/9838 + test("conflicting workspace deps are hoisted in workspace-path order, not package-name order", async () => { + // `@libs/lib` sorts before `my-app` by name, but `apps/app` sorts before + // `libs/lib` by path. npm hoists by path, so the app's `no-deps@2.0.0` + // should win the root slot and `strict-peer-dep` (peer `no-deps@^2.0.0`) + // should dedupe onto it instead of nesting its own copy. + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + private: true, + workspaces: ["libs/*", "apps/*"], + }), + ), + write( + join(packageDir, "apps", "app", "package.json"), + JSON.stringify({ + name: "my-app", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + "strict-peer-dep": "1.0.0", + }, + }), + ), + write( + join(packageDir, "libs", "lib", "package.json"), + JSON.stringify({ + name: "@libs/lib", + version: "1.0.0", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + ]); + + async function checkLayout() { + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "2.0.0", + }); + expect(await file(join(packageDir, "libs", "lib", "node_modules", "no-deps", "package.json")).json()).toMatchObject( + { + name: "no-deps", + version: "1.0.0", + }, + ); + expect(await exists(join(packageDir, "apps", "app", "node_modules"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "strict-peer-dep", "node_modules"))).toBeFalse(); + } + + for (const cwd of [packageDir, join(packageDir, "apps", "app")]) { + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "apps", "app", "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "libs", "lib", "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + await rm(join(packageDir, "bun.lock"), { force: true }); + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd, + stdout: "ignore", + stderr: "pipe", + env, + }); + + let err = await stderr.text(); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + await checkLayout(); + + // re-install with the saved lockfile + empty node_modules + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "libs", "lib", "node_modules"), { recursive: true, force: true }); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd, + stdout: "ignore", + stderr: "pipe", + env, + })); + + err = await stderr.text(); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + await checkLayout(); + } + }); + + test("conflicting workspace deps are hoisted in workspace-path order (reversed)", async () => { + // Reversed: path-first workspace now wants `no-deps@1.0.0`, so that is + // what should hoist to root regardless of which name sorts first. + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + private: true, + workspaces: ["zed/*", "aah/*"], + }), + ), + write( + join(packageDir, "aah", "a", "package.json"), + JSON.stringify({ + name: "zzz-lib", + version: "1.0.0", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "zed", "z", "package.json"), + JSON.stringify({ + name: "aaa-app", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + ]); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.0.0", + }); + expect(await file(join(packageDir, "zed", "z", "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "2.0.0", + }); + expect(await exists(join(packageDir, "aah", "a", "node_modules"))).toBeFalse(); + }); + test("hoisting/using incorrect peer dep on initial install", async () => { await writeFile( packageJson, From 5e4432ff9c78050a19899b98836c03d64d8b012a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 07:16:26 +0000 Subject: [PATCH 2/8] [autofix.ci] apply automated fixes --- test/cli/install/bun-install-registry.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index f83a8b7799a1..91d56d898e4c 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -3966,12 +3966,12 @@ describe("hoisting", async () => { name: "no-deps", version: "2.0.0", }); - expect(await file(join(packageDir, "libs", "lib", "node_modules", "no-deps", "package.json")).json()).toMatchObject( - { - name: "no-deps", - version: "1.0.0", - }, - ); + expect( + await file(join(packageDir, "libs", "lib", "node_modules", "no-deps", "package.json")).json(), + ).toMatchObject({ + name: "no-deps", + version: "1.0.0", + }); expect(await exists(join(packageDir, "apps", "app", "node_modules"))).toBeFalse(); expect(await exists(join(packageDir, "node_modules", "strict-peer-dep", "node_modules"))).toBeFalse(); } From 7c92ae4c87548a7f36ef5949ba3784bcd017a8ac Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:46:35 +0000 Subject: [PATCH 3/8] review: trim comment to the invariant, drain spawn output concurrently --- src/install/lockfile.rs | 8 +--- test/cli/install/bun-install-registry.test.ts | 48 +++++++------------ 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/src/install/lockfile.rs b/src/install/lockfile.rs index 1788eb14060e..b3a29fede847 100644 --- a/src/install/lockfile.rs +++ b/src/install/lockfile.rs @@ -228,12 +228,8 @@ impl<'a> DepSorter<'a> { Ordering::Less => true, Ordering::Greater => false, Ordering::Equal => { - // npm's hoister visits workspaces in path order, so the - // path-alphabetically-first workspace's direct deps win the - // root node_modules slot. Sorting by package name here breaks - // peer-sharing packages like @nestjs/core when a scoped - // workspace name (`@libs/...`) sorts ahead of the app that - // actually provides the peer (#9838). + // Match npm: order workspaces by relative path so the path-first + // workspace's deps win the root node_modules slot. if l_dep.behavior.is_workspace() && l_dep.version.tag == dependency::Tag::Workspace && r_dep.version.tag == dependency::Tag::Workspace diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 91d56d898e4c..1f47fd8b1033 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -3925,10 +3925,6 @@ describe("hoisting", async () => { // https://github.com/oven-sh/bun/issues/9838 test("conflicting workspace deps are hoisted in workspace-path order, not package-name order", async () => { - // `@libs/lib` sorts before `my-app` by name, but `apps/app` sorts before - // `libs/lib` by path. npm hoists by path, so the app's `no-deps@2.0.0` - // should win the root slot and `strict-peer-dep` (peer `no-deps@^2.0.0`) - // should dedupe onto it instead of nesting its own copy. await Promise.all([ write( packageJson, @@ -3976,48 +3972,38 @@ describe("hoisting", async () => { expect(await exists(join(packageDir, "node_modules", "strict-peer-dep", "node_modules"))).toBeFalse(); } - for (const cwd of [packageDir, join(packageDir, "apps", "app")]) { - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "apps", "app", "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "libs", "lib", "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); - await rm(join(packageDir, "bun.lock"), { force: true }); - - let { stderr, exited } = spawn({ + async function install(cwd: string) { + const { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd, stdout: "ignore", stderr: "pipe", env, }); - - let err = await stderr.text(); + const [err, exitCode] = await Promise.all([stderr.text(), exited]); expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - await checkLayout(); + expect(exitCode).toBe(0); + } - // re-install with the saved lockfile + empty node_modules + for (const cwd of [packageDir, join(packageDir, "apps", "app")]) { await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "apps", "app", "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "libs", "lib", "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + await rm(join(packageDir, "bun.lock"), { force: true }); - ({ stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd, - stdout: "ignore", - stderr: "pipe", - env, - })); + await install(cwd); + await checkLayout(); - err = await stderr.text(); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "libs", "lib", "node_modules"), { recursive: true, force: true }); + + await install(cwd); await checkLayout(); } }); test("conflicting workspace deps are hoisted in workspace-path order (reversed)", async () => { - // Reversed: path-first workspace now wants `no-deps@1.0.0`, so that is - // what should hoist to root regardless of which name sorts first. await Promise.all([ write( packageJson, @@ -4057,9 +4043,9 @@ describe("hoisting", async () => { env, }); - const err = await stderr.text(); + const [err, exitCode] = await Promise.all([stderr.text(), exited]); expect(err).not.toContain("error:"); - expect(await exited).toBe(0); + expect(exitCode).toBe(0); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ name: "no-deps", From d8d2b622ff287eeb3d8b5a2d9d38fb30ba42b78f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:14:19 +0000 Subject: [PATCH 4/8] ci: retrigger From 9a464033c826b2f0a3e6f01d50bbf118388072a9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:06:59 +0000 Subject: [PATCH 5/8] DepSorter: key workspace path order off lockfile.workspace_paths dep.version.workspace() does not survive clone_with_different_buffers when the literal is empty (bun.lock load path), so the sort key disagreed between the pre-clean and post-clean lockfiles and --frozen-lockfile failed. lockfile.workspace_paths is populated on every construction path (fresh parse, bun.lock load, migration) and is copied into the cloned lockfile before resolve() runs, so use that as the key instead. Add --frozen-lockfile coverage to the regression test. --- src/install/lockfile.rs | 20 +++++++++---------- test/cli/install/bun-install-registry.test.ts | 7 +++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/install/lockfile.rs b/src/install/lockfile.rs index b3a29fede847..14ff26530d42 100644 --- a/src/install/lockfile.rs +++ b/src/install/lockfile.rs @@ -230,16 +230,16 @@ impl<'a> DepSorter<'a> { Ordering::Equal => { // Match npm: order workspaces by relative path so the path-first // workspace's deps win the root node_modules slot. - if l_dep.behavior.is_workspace() - && l_dep.version.tag == dependency::Tag::Workspace - && r_dep.version.tag == dependency::Tag::Workspace - { - let l_path = l_dep.version.workspace().slice(string_buf); - let r_path = r_dep.version.workspace().slice(string_buf); - match strings::order(l_path, r_path) { - Ordering::Less => return true, - Ordering::Greater => return false, - Ordering::Equal => {} + if l_dep.behavior.is_workspace() { + if let (Some(l_path), Some(r_path)) = ( + self.lockfile.workspace_paths.get(&l_dep.name_hash), + self.lockfile.workspace_paths.get(&r_dep.name_hash), + ) { + match strings::order(l_path.slice(string_buf), r_path.slice(string_buf)) { + Ordering::Less => return true, + Ordering::Greater => return false, + Ordering::Equal => {} + } } } strings::order(l_dep.name.slice(string_buf), r_dep.name.slice(string_buf)) diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 1f47fd8b1033..b4ff25bb2913 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -3972,9 +3972,9 @@ describe("hoisting", async () => { expect(await exists(join(packageDir, "node_modules", "strict-peer-dep", "node_modules"))).toBeFalse(); } - async function install(cwd: string) { + async function install(cwd: string, ...args: string[]) { const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], + cmd: [bunExe(), "install", ...args], cwd, stdout: "ignore", stderr: "pipe", @@ -3982,6 +3982,7 @@ describe("hoisting", async () => { }); const [err, exitCode] = await Promise.all([stderr.text(), exited]); expect(err).not.toContain("error:"); + expect(err).not.toContain("lockfile had changes"); expect(exitCode).toBe(0); } @@ -3995,6 +3996,8 @@ describe("hoisting", async () => { await install(cwd); await checkLayout(); + await install(cwd, "--frozen-lockfile"); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "libs", "lib", "node_modules"), { recursive: true, force: true }); From 0ffba3c10c14f0242fd6e46863120386983a9782 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 1 Aug 2026 07:22:44 +0000 Subject: [PATCH 6/8] bun.lock: leave optional peers unresolved on load so --frozen-lockfile is stable On load from bun.lock, optional peer edges were bound via the pkg_map path walk, so when process_subtree later visited them the target was already resolved and got enqueued immediately. A fresh resolve leaves optional peers at invalid_package_id until process_subtree binds them via ResolveLater/ResolveReplace, which enqueues them later (via the real dependent that brings the package in). With DepSorter now ordering workspaces by path, this asymmetry surfaced as a different BFS order on the two sides of the frozen-lockfile compare on the #9838 repro (ts-jest's optional @jest/transform peer reached root before @cspotcode/source-map-support on the load side only, flipping which @jridgewell/trace-mapping won the root slot). Skip optional peers in both resolution-binding loops in parse_into_binary_lockfile; process_subtree already handles them identically on both sides. Extend the path-order test with optional-peer-deps to exercise the skip on the --frozen-lockfile path. --- src/install/lockfile/bun.lock.rs | 10 ++++++++++ test/cli/install/bun-install-registry.test.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index ada9255061ad..06db696f2283 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -2865,6 +2865,13 @@ pub(crate) fn parse_into_binary_lockfile( let dep = &mut dependencies[dep_id as usize]; let dep_name = dep.name.slice(string_buf); + if dep.behavior.is_optional_peer() { + // fresh resolve leaves optional peers unresolved until + // process_subtree; binding them here via the path walk + // shifts BFS enqueue order and breaks --frozen-lockfile. + continue; + } + let workspace_node_modules = { let buf_slice = &mut path_buf[..]; let needed = workspace_name.len() + 1 + dep_name.len(); @@ -2955,6 +2962,9 @@ pub(crate) fn parse_into_binary_lockfile( let dep_id: DependencyID = _dep_id; let dep = &mut dependencies[dep_id as usize]; + if dep.behavior.is_optional_peer() { + continue 'deps; + } let peer_res_id = if is_deferred_peer(dep) { resolve_peer_dep_version_based( dep, diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index b4ff25bb2913..6f275c7fdabb 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -3942,6 +3942,7 @@ describe("hoisting", async () => { dependencies: { "no-deps": "2.0.0", "strict-peer-dep": "1.0.0", + "optional-peer-deps": "1.0.0", }, }), ), @@ -3970,6 +3971,7 @@ describe("hoisting", async () => { }); expect(await exists(join(packageDir, "apps", "app", "node_modules"))).toBeFalse(); expect(await exists(join(packageDir, "node_modules", "strict-peer-dep", "node_modules"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "optional-peer-deps", "node_modules"))).toBeFalse(); } async function install(cwd: string, ...args: string[]) { From 9446384d4033d8fc564a751153fb825c41cdf2ef Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:54:42 +0000 Subject: [PATCH 7/8] bun.lock: skip optional peers in the root-deps loop too, update snapshots The root-deps loop is the third sibling of the workspace and per-package loops; enqueue_dependency_with_main_and_success_fn returns early for optional peers before reading is_root, so root optional peers are left unresolved on fresh resolve the same as the other two paths. Drop the redundant inline comment (now covered by the is_deferred_peer doc) and reword that doc for the optional-peer half. The next-pages lockfile snapshots record which DependencyID owns each tree slot; jiti's slot is now owned by tailwindcss's real dependency edge instead of eslint's optional peer edge, so the id moves 434 -> 937 (same package_id). --- src/install/lockfile/bun.lock.rs | 13 ++++++------- .../__snapshots__/dev-server-ssr-100.test.ts.snap | 2 +- .../test/__snapshots__/dev-server.test.ts.snap | 2 +- .../test/__snapshots__/next-build.test.ts.snap | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index 06db696f2283..c8ff4413ed52 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -2801,6 +2801,9 @@ pub(crate) fn parse_into_binary_lockfile( let dep_id: DependencyID = _dep_id; let dep = &mut dependencies[dep_id as usize]; + if dep.behavior.is_optional_peer() { + continue; + } let peer_res_id = if is_deferred_peer(dep) { resolve_peer_dep_version_based( dep, @@ -2866,9 +2869,6 @@ pub(crate) fn parse_into_binary_lockfile( let dep_name = dep.name.slice(string_buf); if dep.behavior.is_optional_peer() { - // fresh resolve leaves optional peers unresolved until - // process_subtree; binding them here via the path walk - // shifts BFS enqueue order and breaks --frozen-lockfile. continue; } @@ -3029,10 +3029,9 @@ pub(crate) fn parse_into_binary_lockfile( /// True for peer edges the fresh resolver defers to its second phase /// (`install_peer`) and binds by version there. Two exemptions, matching /// `enqueue_dependency_with_main_and_success_fn`: optional peers return -/// before the deferred phase and are bound to the hoisted-tree sibling by -/// `process_subtree` instead, and `*` peers express no version preference -/// and bind to whatever sibling pin existed first. Both of those are -/// exactly what the printed tree's path walk reproduces, so they keep it. +/// before the deferred phase (callers skip them entirely and leave the slot +/// for `process_subtree`), and `*` peers express no version preference so +/// the printed tree's path walk binds them. fn is_deferred_peer(dep: &Dependency) -> bool { dep.behavior.is_peer() && !dep.behavior.is_optional_peer() diff --git a/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap b/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap index 3cd4b4ce78dc..6e02f75c4986 100644 --- a/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap @@ -26725,7 +26725,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 315, }, "jiti": { - "id": 434, + "id": 937, "package_id": 316, }, "js-tokens": { diff --git a/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap b/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap index 355ec90bb530..6f77a41c6fa5 100644 --- a/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap @@ -26725,7 +26725,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 315, }, "jiti": { - "id": 434, + "id": 937, "package_id": 316, }, "js-tokens": { diff --git a/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap b/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap index a5c6c56c8b3a..77966fad2d8b 100644 --- a/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap @@ -26725,7 +26725,7 @@ exports[`next build works: bun 1`] = ` "package_id": 315, }, "jiti": { - "id": 434, + "id": 937, "package_id": 316, }, "js-tokens": { @@ -54704,7 +54704,7 @@ exports[`next build works: node 1`] = ` "package_id": 315, }, "jiti": { - "id": 434, + "id": 937, "package_id": 316, }, "js-tokens": { From d11647e0fb4bd94fcbe87cc58734e83ca7993b1c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:26:04 +0000 Subject: [PATCH 8/8] is_deferred_peer: drop now-unreachable optional-peer clause All three callers skip is_optional_peer() before calling, so the clause was dead. Keep the contract as a debug_assert! and tighten the doc. --- src/install/lockfile/bun.lock.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index c8ff4413ed52..a65001adfa79 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -3027,14 +3027,12 @@ pub(crate) fn parse_into_binary_lockfile( } /// True for peer edges the fresh resolver defers to its second phase -/// (`install_peer`) and binds by version there. Two exemptions, matching -/// `enqueue_dependency_with_main_and_success_fn`: optional peers return -/// before the deferred phase (callers skip them entirely and leave the slot -/// for `process_subtree`), and `*` peers express no version preference so -/// the printed tree's path walk binds them. +/// (`install_peer`) and binds by version there. Callers skip optional peers +/// entirely; `*` peers express no version preference so the printed tree's +/// path walk binds them. fn is_deferred_peer(dep: &Dependency) -> bool { + debug_assert!(!dep.behavior.is_optional_peer()); dep.behavior.is_peer() - && !dep.behavior.is_optional_peer() && !(dep.version.tag == DependencyVersionTag::Npm && dep.version.npm().version.is_star()) }