From 0adcd694e07ec643f8ea3b77ad92be1cc91c95c1 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Mon, 9 Feb 2026 21:13:18 +0000 Subject: [PATCH] fix(install): exclude optional peers resolved against dev deps in --prod When running `bun install --production`, optional peer dependencies of production packages (e.g. typescript as an optional peer of @prisma/client) were incorrectly installed if they also appeared in the root devDependencies. During the `.resolvable` tree-building phase, optional peers get resolved against any matching dependency in the tree, including dev deps. In the subsequent `.filter` phase, the root dev dependency is correctly filtered out, but the optional peer retains its resolved package ID and passes through the filter since remote_package_features.peer_dependencies is true. The fix adds a check in the filter phase: when an optional peer dependency from a non-root package matches a root dev dependency and dev_dependencies are disabled (--prod mode), it is filtered out. Closes #26837 Co-Authored-By: Claude --- src/install/lockfile/Tree.zig | 15 ++++++++++ test/regression/issue/26837.test.ts | 45 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 test/regression/issue/26837.test.ts diff --git a/src/install/lockfile/Tree.zig b/src/install/lockfile/Tree.zig index b4a25ae9cd..ea5df3ab77 100644 --- a/src/install/lockfile/Tree.zig +++ b/src/install/lockfile/Tree.zig @@ -394,6 +394,21 @@ pub fn isFilteredDependencyOrWorkspace( // Filtering only applies to the root package dependencies. Also // --filter has a different meaning if a new package is being installed. if (manager.subcommand != .install or parent_pkg_id != 0) { + // Optional peer dependencies can get resolved against root dev + // dependencies during the `.resolvable` phase. In `--prod` mode + // (dev_dependencies disabled), these optional peers should not + // pull in packages that would otherwise only exist as dev deps. + if (parent_pkg_id != 0 and dep.behavior.isOptionalPeer() and + !manager.options.local_package_features.dev_dependencies) + { + const root_dep_list = pkgs.items(.dependencies)[0]; + const root_deps = lockfile.buffers.dependencies.items[root_dep_list.begin()..root_dep_list.end()]; + for (root_deps) |root_dep| { + if (root_dep.name_hash == dep.name_hash and root_dep.behavior.isDev()) { + return true; + } + } + } return false; } diff --git a/test/regression/issue/26837.test.ts b/test/regression/issue/26837.test.ts new file mode 100644 index 0000000000..6179c48432 --- /dev/null +++ b/test/regression/issue/26837.test.ts @@ -0,0 +1,45 @@ +// https://github.com/oven-sh/bun/issues/26837 +// Test that `bun install --production` does not install optional peer +// dependencies that are also listed as devDependencies. When a production +// dependency (e.g. @prisma/client) has an optional peer (e.g. typescript), +// and that same package appears in the root devDependencies, `--production` +// should NOT install it. + +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("--production should not install optional peers that match devDependencies", async () => { + using dir = tempDir("issue-26837", { + "package.json": JSON.stringify({ + name: "test-issue-26837", + dependencies: { + "@prisma/client": "^6.3.1", + }, + devDependencies: { + typescript: "^5.7.3", + }, + }), + }); + + // Run bun install --production + await using proc = Bun.spawn({ + cmd: [bunExe(), "install", "--production"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // typescript should NOT be installed since it's a devDependency + // and we're installing with --production + const output = stdout + stderr; + expect(output).not.toContain("error:"); + + // Check that typescript is not in node_modules + const typescriptExists = await Bun.file(`${dir}/node_modules/typescript/package.json`).exists(); + expect(typescriptExists).toBe(false); + + expect(exitCode).toBe(0); +});