Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
127acefe3d fix: make --parallel work with --filter to skip dependency ordering
When using `--filter` in a monorepo with workspace dependencies, Bun
enforces topological ordering and waits for dependency scripts to exit
before starting dependent scripts. This blocks long-running scripts
(like `dev`) indefinitely since they never exit.

This fix makes `--parallel --filter` skip dependency ordering so all
matched scripts start concurrently. Two changes:

1. Route `--filter`/`--workspaces` to FilterRun before MultiRun so the
   `--parallel` flag is available in the filter code path.
2. Skip dependency graph construction in FilterRun when `--parallel` is
   set, allowing all scripts to start immediately.

Closes #18011

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 04:57:17 +00:00
3 changed files with 105 additions and 31 deletions

View File

@@ -892,15 +892,15 @@ pub const Command = struct {
const ctx = try Command.init(allocator, log, .RunCommand);
ctx.args.target = .bun;
if (ctx.parallel or ctx.sequential) {
MultiRun.run(ctx) catch |err| {
if (ctx.filters.len > 0 or ctx.workspaces) {
FilterRun.runScriptsWithFilter(ctx) catch |err| {
Output.prettyErrorln("<r><red>error<r>: {s}", .{@errorName(err)});
Global.exit(1);
};
}
if (ctx.filters.len > 0 or ctx.workspaces) {
FilterRun.runScriptsWithFilter(ctx) catch |err| {
if (ctx.parallel or ctx.sequential) {
MultiRun.run(ctx) catch |err| {
Output.prettyErrorln("<r><red>error<r>: {s}", .{@errorName(err)});
Global.exit(1);
};
@@ -938,15 +938,15 @@ pub const Command = struct {
};
ctx.args.target = .bun;
if (ctx.parallel or ctx.sequential) {
MultiRun.run(ctx) catch |err| {
if (ctx.filters.len > 0 or ctx.workspaces) {
FilterRun.runScriptsWithFilter(ctx) catch |err| {
Output.prettyErrorln("<r><red>error<r>: {s}", .{@errorName(err)});
Global.exit(1);
};
}
if (ctx.filters.len > 0 or ctx.workspaces) {
FilterRun.runScriptsWithFilter(ctx) catch |err| {
if (ctx.parallel or ctx.sequential) {
MultiRun.run(ctx) catch |err| {
Output.prettyErrorln("<r><red>error<r>: {s}", .{@errorName(err)});
Global.exit(1);
};

View File

@@ -583,34 +583,38 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn {
}
}
// compute dependencies (TODO: maybe we should do this only in a workspace?)
for (state.handles) |*handle| {
const source_buf = handle.config.deps.source_buf;
var iter = handle.config.deps.map.iterator();
while (iter.next()) |entry| {
const name = entry.key_ptr.slice(source_buf);
// is it a workspace dependency?
if (map.get(name)) |pkgs| {
for (pkgs.items) |dep| {
try dep.dependents.append(handle);
handle.remaining_dependencies += 1;
// When --parallel is set, skip dependency ordering so all scripts start concurrently.
// This is important for long-running scripts (like "dev") where dependencies never exit.
if (!ctx.parallel) {
for (state.handles) |*handle| {
const source_buf = handle.config.deps.source_buf;
var iter = handle.config.deps.map.iterator();
while (iter.next()) |entry| {
const name = entry.key_ptr.slice(source_buf);
// is it a workspace dependency?
if (map.get(name)) |pkgs| {
for (pkgs.items) |dep| {
try dep.dependents.append(handle);
handle.remaining_dependencies += 1;
}
}
}
}
}
// check if there is a dependency cycle
var has_cycle = false;
for (state.handles) |*handle| {
if (hasCycle(handle)) {
has_cycle = true;
break;
}
}
// if there is, we ignore dependency order completely
if (has_cycle) {
// check if there is a dependency cycle
var has_cycle = false;
for (state.handles) |*handle| {
handle.dependents.clearRetainingCapacity();
handle.remaining_dependencies = 0;
if (hasCycle(handle)) {
has_cycle = true;
break;
}
}
// if there is, we ignore dependency order completely
if (has_cycle) {
for (state.handles) |*handle| {
handle.dependents.clearRetainingCapacity();
handle.remaining_dependencies = 0;
}
}
}

View File

@@ -0,0 +1,70 @@
import { spawnSync } from "bun";
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// Issue #18011: --filter indefinitely waits for dependent package when using long-running scripts.
// When package "frontend" depends on "ui-kit" (via workspace dependency), running
// `bun --filter '*' dev` should not block "frontend" from starting until "ui-kit" exits.
// The --parallel flag should skip dependency ordering so all scripts start concurrently.
test("--parallel --filter starts all scripts concurrently regardless of dependencies", () => {
using dir = tempDir("issue-18011", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
}),
packages: {
"ui-kit": {
"dev.js": `console.log("ui-kit-started"); await new Promise(r => setTimeout(r, 1000));`,
"package.json": JSON.stringify({
name: "ui-kit",
scripts: {
dev: `${bunExe()} run dev.js`,
},
}),
},
frontend: {
"dev.js": `console.log("frontend-started"); await new Promise(r => setTimeout(r, 1000));`,
"package.json": JSON.stringify({
name: "frontend",
dependencies: {
"ui-kit": "workspace:^",
},
scripts: {
dev: `${bunExe()} run dev.js`,
},
}),
},
unrelated: {
"dev.js": `console.log("unrelated-started"); await new Promise(r => setTimeout(r, 1000));`,
"package.json": JSON.stringify({
name: "unrelated",
scripts: {
dev: `${bunExe()} run dev.js`,
},
}),
},
},
});
// --parallel --filter should: (1) respect the filter pattern, (2) skip dependency ordering.
// We filter to only ui-kit and frontend, excluding "unrelated".
const { exitCode, stdout } = spawnSync({
cmd: [bunExe(), "run", "--parallel", "--filter", "ui-kit", "--filter", "frontend", "dev"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const out = stdout.toString();
// Both filtered scripts should have started concurrently
expect(out).toContain("ui-kit-started");
expect(out).toContain("frontend-started");
// The unrelated package should NOT have run (filter is respected)
expect(out).not.toContain("unrelated-started");
// Verify we go through FilterRun (uses "name script:" format) not MultiRun (uses "name:script |" format)
expect(out).toMatch(/ui-kit dev:/);
expect(out).toMatch(/frontend dev:/);
expect(exitCode).toBe(0);
});