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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-02-09 21:13:18 +00:00
parent b7475d8768
commit 0adcd694e0
2 changed files with 60 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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);
});