From 5215d1d0fa1756f57b24fde770b8432aff74964c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:59:33 +0000 Subject: [PATCH 1/3] install: do not auto-install a transitive peer dependency on bun A registry package that declares a non-optional peer dependency on bun (for example bun-plugin-tailwind) made bun install pull the npm bun package and its platform binaries into node_modules. The running runtime already satisfies that peer. Treat a remote package's peer dependency named bun as an optional peer, so it is not auto-installed but still binds when bun is installed through a real dependency edge. Fixes #39755 --- src/install/lockfile/Package.rs | 15 ++- test/cli/install/bun-install-registry.test.ts | 103 ++++++++++++++++++ .../registry/packages/bun/bun-1.0.0.tgz | Bin 0 -> 153 bytes .../registry/packages/bun/bun-1.1.0.tgz | Bin 0 -> 153 bytes .../registry/packages/bun/package.json | 29 +++++ .../packages/create-bun-peer-packages.ts | 57 ++++++++++ .../packages/peer-on-bun/package.json | 22 ++++ .../peer-on-bun/peer-on-bun-1.0.0.tgz | Bin 0 -> 195 bytes 8 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 test/cli/install/registry/packages/bun/bun-1.0.0.tgz create mode 100644 test/cli/install/registry/packages/bun/bun-1.1.0.tgz create mode 100644 test/cli/install/registry/packages/bun/package.json create mode 100644 test/cli/install/registry/packages/create-bun-peer-packages.ts create mode 100644 test/cli/install/registry/packages/peer-on-bun/package.json create mode 100644 test/cli/install/registry/packages/peer-on-bun/peer-on-bun-1.0.0.tgz diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index c3f774d9d18c..03c06eaa1903 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -789,9 +789,14 @@ impl Package { let mut behavior = group.behavior; if is_peer { + // A peer dependency on `bun` is satisfied by the + // running runtime. Treat it as optional so it is not + // auto-installed; it still binds when `bun` is + // installed through a real dependency edge. behavior.set( Behavior::OPTIONAL, - (i as u32) < package_version.non_optional_peer_dependencies_start, + (i as u32) < package_version.non_optional_peer_dependencies_start + || key.slice(&manifest.string_buf) == b"bun", ); } if package_version_ptr.all_dependencies_bundled() { @@ -2937,7 +2942,13 @@ impl Package { )? { let mut dep = dep_; if group.behavior.is_peer() - && optional_peer_dependencies.swap_remove(&external_name.hash) + && (optional_peer_dependencies.swap_remove(&external_name.hash) + // A remote package's peer dependency on + // `bun` is satisfied by the running + // runtime; do not auto-install it. + || (!FEATURES.is_main + && !FEATURES.is_workspace + && key == b"bun")) { dep.behavior.insert(Behavior::OPTIONAL); } diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 0e6f5e17b6ed..915d824f66c3 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -871,6 +871,109 @@ describe("text lockfile", () => { }); }); +// #39755: a transitive peer dependency on `bun` is satisfied by the running +// runtime and must not auto-install the npm `bun` package. +describe("peer dependency on bun", () => { + test("transitive peer is not auto-installed", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "peer-on-bun": "1.0.0", + }, + }), + ); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "peer-on-bun"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "bun"))).toBeFalse(); + + const firstLockfile = (await file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // an install from the saved lockfile keeps the peer unbound + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "bun"))).toBeFalse(); + expect((await file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + firstLockfile, + ); + }); + + test("explicit root dependency on bun still installs", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "peer-on-bun": "1.0.0", + bun: "1.0.0", + }, + }), + ); + + const { exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "node_modules", "bun", "package.json")).json()).toMatchObject({ + name: "bun", + version: "1.0.0", + }); + }); + + test("root peerDependencies on bun is still auto-installed", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + peerDependencies: { + bun: ">=1.0.0", + }, + }), + ); + + const { exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "node_modules", "bun", "package.json")).json()).toMatchObject({ + name: "bun", + version: "1.1.0", + }); + }); +}); + test("--lockfile-only", async () => { await Promise.all([ write( diff --git a/test/cli/install/registry/packages/bun/bun-1.0.0.tgz b/test/cli/install/registry/packages/bun/bun-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f1084bce3f3cb8464a6ce0ccbe1d33dc5d4f816d GIT binary patch literal 153 zcmb2|=3oGW|8LK4W)LUXLaTC zJ$47*Du4a(>L-h@mNheGni_g-Se|-zP1(mP;k_Sw!voJ~n{y{UW)LUXLaTC zJ$47*Du4a(>L-h@mNheGni_g-Se|-zP1(mP;k_Sw!voJ~n{y{U=1.0.0") + */ + +import { mkdir, writeFile } from "fs/promises"; +import { join } from "path"; + +const packagesDir = import.meta.dir; + +type Manifest = { + version: string; + dependencies?: Record; + peerDependencies?: Record; +}; + +const packages: Record = { + bun: [{ version: "1.0.0" }, { version: "1.1.0" }], + "peer-on-bun": [{ version: "1.0.0", peerDependencies: { bun: ">=1.0.0" } }], +}; + +for (const [name, manifests] of Object.entries(packages)) { + const dir = join(packagesDir, name); + await mkdir(dir, { recursive: true }); + + const versions: Record = {}; + let latest = ""; + for (const manifest of manifests) { + const pkgJson = { name, ...manifest }; + const files: Record = { "package/package.json": JSON.stringify(pkgJson, null, 2) }; + const tarball = join(dir, `${name}-${manifest.version}.tgz`); + await Bun.Archive.write(tarball, files, { compress: "gzip" }); + + const bytes = await Bun.file(tarball).bytes(); + versions[manifest.version] = { + ...pkgJson, + _id: `${name}@${manifest.version}`, + dist: { + integrity: `sha512-${Buffer.from(new Bun.CryptoHasher("sha512").update(bytes).digest()).toString("base64")}`, + shasum: new Bun.CryptoHasher("sha1").update(bytes).digest("hex"), + tarball: `http://localhost:4873/${name}/-/${name}-${manifest.version}.tgz`, + }, + }; + latest = manifest.version; + } + + await writeFile( + join(dir, "package.json"), + JSON.stringify({ _id: name, name, "dist-tags": { latest }, versions }, null, 2), + ); +} + +console.log("Created bun-peer test packages"); diff --git a/test/cli/install/registry/packages/peer-on-bun/package.json b/test/cli/install/registry/packages/peer-on-bun/package.json new file mode 100644 index 000000000000..826498708a7c --- /dev/null +++ b/test/cli/install/registry/packages/peer-on-bun/package.json @@ -0,0 +1,22 @@ +{ + "_id": "peer-on-bun", + "name": "peer-on-bun", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "1.0.0": { + "name": "peer-on-bun", + "version": "1.0.0", + "peerDependencies": { + "bun": ">=1.0.0" + }, + "_id": "peer-on-bun@1.0.0", + "dist": { + "integrity": "sha512-r+jJvV2iX8gP4+nhiuMh4U6VI0m0q0ZmAYvx4dS2+2WyLWDGAfru4oj/UF80NfMgw4pqN+GKOK+K2JDZG9zQ4w==", + "shasum": "7dc24a1eff8fc4102c96d01f9dc34def606a77e0", + "tarball": "http://localhost:4873/peer-on-bun/-/peer-on-bun-1.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/peer-on-bun/peer-on-bun-1.0.0.tgz b/test/cli/install/registry/packages/peer-on-bun/peer-on-bun-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3c470196615c5c381ea9abf05ec0ceceaf54b11b GIT binary patch literal 195 zcmb2|=3oGW|8LJ5W*v4AXnnZ$yU5`y8;!JcV>l*hiBFuXXrZ{;wd<0^ecOc6yj-5P z|Jiap{B7awf4_ftdA{`Qvuru<9froYpXnStr*~QBsoy2<*_X|>t~{Nz()$|Q#sc=! zf2@5DuKQPN`OZ;hamTcW$G3{FeRnxcXZGEoFPdF1=KFt6)VbBY?3h&a`m427-(NjF reEQ#m&EJZ8l$3TwpS!#L`c_$2zmFmiXTr$s-}x1pr`a%QFfafBjBQ%! literal 0 HcmV?d00001 From 2d4120a591a5051a4782e6f42933cd202d2a5324 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:07:18 +0000 Subject: [PATCH 2/3] test: cover the folder parse path and capture install diagnostics Add a folder dependency fixture whose peer on bun goes through the generic package.json parser. Pipe stderr in the new tests and assert it before the exit code. Assert that the optional peer binds to an explicitly installed bun through bun why. --- test/cli/install/bun-install-registry.test.ts | 86 ++++++++++++------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 915d824f66c3..3a827ec0c1ce 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -874,6 +874,20 @@ describe("text lockfile", () => { // #39755: a transitive peer dependency on `bun` is satisfied by the running // runtime and must not auto-install the npm `bun` package. describe("peer dependency on bun", () => { + async function run(...args: string[]) { + await using proc = spawn({ + cmd: [bunExe(), ...args], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(err).not.toContain("error:"); + expect(exitCode).toBe(0); + return out; + } + test("transitive peer is not auto-installed", async () => { await write( packageJson, @@ -885,14 +899,7 @@ describe("peer dependency on bun", () => { }), ); - let { exited } = spawn({ - cmd: [bunExe(), "install", "--save-text-lockfile"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - expect(await exited).toBe(0); + await run("install", "--save-text-lockfile"); expect(await exists(join(packageDir, "node_modules", "peer-on-bun"))).toBeTrue(); expect(await exists(join(packageDir, "node_modules", "bun"))).toBeFalse(); @@ -905,14 +912,7 @@ describe("peer dependency on bun", () => { await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); // an install from the saved lockfile keeps the peer unbound - ({ exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - })); - expect(await exited).toBe(0); + await run("install"); expect(await exists(join(packageDir, "node_modules", "bun"))).toBeFalse(); expect((await file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( @@ -920,7 +920,36 @@ describe("peer dependency on bun", () => { ); }); - test("explicit root dependency on bun still installs", async () => { + test("transitive peer of a folder dependency is not auto-installed", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "peer-on-bun-folder": "file:./peer-on-bun-folder", + }, + }), + ), + write( + join(packageDir, "peer-on-bun-folder", "package.json"), + JSON.stringify({ + name: "peer-on-bun-folder", + version: "1.0.0", + peerDependencies: { + bun: ">=1.0.0", + }, + }), + ), + ]); + + await run("install"); + + expect(await exists(join(packageDir, "node_modules", "peer-on-bun-folder"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "bun"))).toBeFalse(); + }); + + test("explicit root dependency on bun still installs and binds the peer", async () => { await write( packageJson, JSON.stringify({ @@ -932,19 +961,17 @@ describe("peer dependency on bun", () => { }), ); - const { exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - expect(await exited).toBe(0); + await run("install", "--save-text-lockfile"); expect(await file(join(packageDir, "node_modules", "bun", "package.json")).json()).toMatchObject({ name: "bun", version: "1.0.0", }); + + // the optional peer binds to the explicitly installed bun@1.0.0 + const why = await run("why", "bun"); + expect(why).toContain("bun@1.0.0"); + expect(why).toContain("peer-on-bun@1.0.0"); }); test("root peerDependencies on bun is still auto-installed", async () => { @@ -958,14 +985,7 @@ describe("peer dependency on bun", () => { }), ); - const { exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - expect(await exited).toBe(0); + await run("install"); expect(await file(join(packageDir, "node_modules", "bun", "package.json")).json()).toMatchObject({ name: "bun", From 353856bcf4aba0f9c9e690410784d6298ff8800e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:08:05 +0000 Subject: [PATCH 3/3] Shorten the peer-on-bun comments --- src/install/lockfile/Package.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index 03c06eaa1903..ad7ccfb0613a 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -789,10 +789,7 @@ impl Package { let mut behavior = group.behavior; if is_peer { - // A peer dependency on `bun` is satisfied by the - // running runtime. Treat it as optional so it is not - // auto-installed; it still binds when `bun` is - // installed through a real dependency edge. + // a peer dependency on `bun` is satisfied by the running runtime (#39755) behavior.set( Behavior::OPTIONAL, (i as u32) < package_version.non_optional_peer_dependencies_start @@ -2943,9 +2940,7 @@ impl Package { let mut dep = dep_; if group.behavior.is_peer() && (optional_peer_dependencies.swap_remove(&external_name.hash) - // A remote package's peer dependency on - // `bun` is satisfied by the running - // runtime; do not auto-install it. + // a peer dependency on `bun` is satisfied by the running runtime (#39755) || (!FEATURES.is_main && !FEATURES.is_workspace && key == b"bun"))