mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 20:39:05 +00:00
Add --stream flag for workspace script execution
Implements a --stream flag for `bun run` that streams logs immediately without buffering or eliding when running scripts across workspace packages. This is similar to pnpm's --stream flag and addresses user feedback about log display in workspace environments. Changes: - Add --stream flag to CLI parameters for both run and auto commands - Implement streaming output in filter_run.zig that bypasses buffering - Add validation to prevent using --stream with --elide-lines - Add comprehensive test coverage for the new flag - Update documentation in filter.md and run.md The flag provides immediate, unbuffered output which is useful for: - Real-time monitoring of long-running processes - CI/CD environments where immediate feedback is important - Debugging scripts that may hang or have timing issues Fixes the issue where workspace script output was being buffered and elided by default, making it difficult to see logs in real-time. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,33 @@ bun --filter '*' dev
|
||||
Both commands will be run in parallel, and you will see a nice terminal UI showing their respective outputs:
|
||||

|
||||
|
||||
### Output options
|
||||
|
||||
By default, when running scripts with `--filter` in a terminal, Bun displays a formatted UI that elides long output to keep the display manageable. You can control this behavior with these flags:
|
||||
|
||||
#### `--elide-lines <number>`
|
||||
|
||||
Controls how many lines of output are shown for each package (default: 10). Set to 0 to show all output:
|
||||
|
||||
```bash
|
||||
# Show only 5 lines of output per package
|
||||
bun --filter '*' --elide-lines 5 test
|
||||
|
||||
# Show all output (no eliding)
|
||||
bun --filter '*' --elide-lines 0 test
|
||||
```
|
||||
|
||||
#### `--stream`
|
||||
|
||||
Stream logs immediately without buffering or eliding. This is useful when you want to see real-time output from all packages, similar to pnpm's `--stream` flag:
|
||||
|
||||
```bash
|
||||
# Stream all output immediately
|
||||
bun --filter '*' --stream dev
|
||||
```
|
||||
|
||||
Note: `--stream` and `--elide-lines` cannot be used together. When `--stream` is enabled, output is written directly to stdout without package name prefixes or formatting.
|
||||
|
||||
### Running scripts in workspaces
|
||||
|
||||
Filters respect your [workspace configuration](https://bun.com/docs/install/workspaces): If you have a `package.json` file that specifies which packages are part of the workspace,
|
||||
|
||||
@@ -164,6 +164,10 @@ bun run --filter 'ba*' <script>
|
||||
|
||||
will execute `<script>` in both `bar` and `baz`, but not in `foo`.
|
||||
|
||||
When using `--filter`, you can control the output display with:
|
||||
- `--elide-lines <number>`: Limit the number of output lines shown per package (default: 10, use 0 for no limit)
|
||||
- `--stream`: Stream output immediately without buffering or formatting (similar to pnpm's `--stream`)
|
||||
|
||||
Find more details in the docs page for [filter](https://bun.com/docs/cli/filter#running-scripts-with-filter).
|
||||
|
||||
## `bun run -` to pipe code from stdin
|
||||
|
||||
@@ -419,6 +419,7 @@ pub const Command = struct {
|
||||
env_behavior: api.DotEnvBehavior = .disable,
|
||||
env_prefix: []const u8 = "",
|
||||
elide_lines: ?usize = null,
|
||||
stream_logs: bool = false,
|
||||
// Compile options
|
||||
compile: bool = false,
|
||||
compile_target: Cli.CompileTarget = .{},
|
||||
|
||||
@@ -123,6 +123,7 @@ pub const auto_only_params = [_]ParamType{
|
||||
// clap.parseParam("--all") catch unreachable,
|
||||
clap.parseParam("--silent Don't print the script command") catch unreachable,
|
||||
clap.parseParam("--elide-lines <NUMBER> Number of lines of script output shown when using --filter (default: 10). Set to 0 to show all lines.") catch unreachable,
|
||||
clap.parseParam("--stream Stream logs immediately without buffering or eliding. Similar to pnpm's --stream flag") catch unreachable,
|
||||
clap.parseParam("-v, --version Print version and exit") catch unreachable,
|
||||
clap.parseParam("--revision Print version with revision and exit") catch unreachable,
|
||||
} ++ auto_or_run_params;
|
||||
@@ -131,6 +132,7 @@ pub const auto_params = auto_only_params ++ runtime_params_ ++ transpiler_params
|
||||
pub const run_only_params = [_]ParamType{
|
||||
clap.parseParam("--silent Don't print the script command") catch unreachable,
|
||||
clap.parseParam("--elide-lines <NUMBER> Number of lines of script output shown when using --filter (default: 10). Set to 0 to show all lines.") catch unreachable,
|
||||
clap.parseParam("--stream Stream logs immediately without buffering or eliding. Similar to pnpm's --stream flag") catch unreachable,
|
||||
} ++ auto_or_run_params;
|
||||
pub const run_params = run_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_;
|
||||
|
||||
@@ -399,6 +401,8 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ctx.bundler_options.stream_logs = args.flag("--stream");
|
||||
}
|
||||
|
||||
if (cmd == .TestCommand) {
|
||||
@@ -1140,6 +1144,8 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ctx.bundler_options.stream_logs = args.flag("--stream");
|
||||
|
||||
if (opts.define) |define| {
|
||||
if (define.keys.len > 0)
|
||||
|
||||
@@ -146,6 +146,7 @@ const State = struct {
|
||||
draw_buf: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator),
|
||||
last_lines_written: usize = 0,
|
||||
pretty_output: bool,
|
||||
stream_logs: bool,
|
||||
shell_bin: [:0]const u8,
|
||||
aborted: bool = false,
|
||||
env: *bun.DotEnv.Loader,
|
||||
@@ -160,7 +161,11 @@ const State = struct {
|
||||
};
|
||||
|
||||
fn readChunk(this: *This, handle: *ProcessHandle, chunk: []const u8) !void {
|
||||
if (this.pretty_output) {
|
||||
if (this.stream_logs) {
|
||||
// In stream mode, output immediately without buffering
|
||||
const stdout = std.io.getStdOut();
|
||||
stdout.writeAll(chunk) catch {};
|
||||
} else if (this.pretty_output) {
|
||||
bun.handleOom(handle.buffer.appendSlice(chunk));
|
||||
this.redraw(false) catch {};
|
||||
} else {
|
||||
@@ -202,7 +207,10 @@ const State = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.pretty_output) {
|
||||
if (this.stream_logs) {
|
||||
// In stream mode, don't do any special exit handling for output
|
||||
// The output has already been streamed
|
||||
} else if (this.pretty_output) {
|
||||
this.redraw(false) catch {};
|
||||
} else {
|
||||
this.draw_buf.clearRetainingCapacity();
|
||||
@@ -542,10 +550,22 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn {
|
||||
.handles = try ctx.allocator.alloc(ProcessHandle, scripts.items.len),
|
||||
.event_loop = event_loop,
|
||||
.pretty_output = if (Environment.isWindows) windowsIsTerminal() and Output.enable_ansi_colors_stdout else Output.enable_ansi_colors_stdout,
|
||||
.stream_logs = ctx.bundler_options.stream_logs,
|
||||
.shell_bin = shell_bin,
|
||||
.env = this_transpiler.env,
|
||||
};
|
||||
|
||||
// Check if both --stream and --elide-lines are used
|
||||
if (state.stream_logs and ctx.bundler_options.elide_lines != null) {
|
||||
Output.prettyErrorln("<r><red>error<r>: --stream and --elide-lines cannot be used together", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
// When --stream is set, disable pretty output
|
||||
if (state.stream_logs) {
|
||||
state.pretty_output = false;
|
||||
}
|
||||
|
||||
// Check if elide-lines is used in a non-terminal environment
|
||||
if (ctx.bundler_options.elide_lines != null and !state.pretty_output) {
|
||||
Output.prettyErrorln("<r><red>error<r>: --elide-lines is only supported in terminal environments", .{});
|
||||
|
||||
@@ -506,4 +506,221 @@ describe("bun", () => {
|
||||
win32ExpectedError: /--elide-lines is only supported in terminal environments/,
|
||||
});
|
||||
});
|
||||
|
||||
describe("--stream flag", () => {
|
||||
test("streams output immediately without buffering", () => {
|
||||
const dir = tempDirWithFiles("testworkspace-stream", {
|
||||
packages: {
|
||||
pkg1: {
|
||||
"index.js": `console.log('pkg1-line1'); console.log('pkg1-line2'); console.log('pkg1-line3');`,
|
||||
"package.json": JSON.stringify({
|
||||
name: "pkg1",
|
||||
scripts: {
|
||||
test: `${bunExe()} run index.js`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
pkg2: {
|
||||
"index.js": `console.log('pkg2-line1'); console.log('pkg2-line2'); console.log('pkg2-line3');`,
|
||||
"package.json": JSON.stringify({
|
||||
name: "pkg2",
|
||||
scripts: {
|
||||
test: `${bunExe()} run index.js`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
"package.json": JSON.stringify({
|
||||
name: "ws",
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
});
|
||||
|
||||
const { exitCode, stdout, stderr } = spawnSync({
|
||||
cwd: dir,
|
||||
cmd: [bunExe(), "run", "--filter", "*", "--stream", "test"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
// In stream mode, output should not have package name prefixes
|
||||
expect(output).toMatch(/pkg1-line1/);
|
||||
expect(output).toMatch(/pkg1-line2/);
|
||||
expect(output).toMatch(/pkg1-line3/);
|
||||
expect(output).toMatch(/pkg2-line1/);
|
||||
expect(output).toMatch(/pkg2-line2/);
|
||||
expect(output).toMatch(/pkg2-line3/);
|
||||
// Should not have the package name prefixes that appear in non-stream mode
|
||||
expect(output).not.toMatch(/pkg1 test:/);
|
||||
expect(output).not.toMatch(/pkg2 test:/);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("--stream and --elide-lines cannot be used together", () => {
|
||||
const dir = tempDirWithFiles("testworkspace-stream-conflict", {
|
||||
packages: {
|
||||
pkg1: {
|
||||
"package.json": JSON.stringify({
|
||||
name: "pkg1",
|
||||
scripts: {
|
||||
test: "echo test",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
"package.json": JSON.stringify({
|
||||
name: "ws",
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
});
|
||||
|
||||
const { exitCode, stderr } = spawnSync({
|
||||
cwd: dir,
|
||||
cmd: [bunExe(), "run", "--filter", "*", "--stream", "--elide-lines", "5", "test"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(stderr.toString()).toMatch(/--stream and --elide-lines cannot be used together/);
|
||||
expect(exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
test("--stream works with dependency ordering", () => {
|
||||
const dir = tempDirWithFiles("testworkspace-stream-deps", {
|
||||
dep0: {
|
||||
"index.js": `await Bun.write('out.txt', 'dep0-complete'); console.log('dep0-output');`,
|
||||
"package.json": JSON.stringify({
|
||||
name: "dep0",
|
||||
scripts: {
|
||||
script: `${bunExe()} run index.js`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
dep1: {
|
||||
"index.js": `const content = await Bun.file("../dep0/out.txt").text(); console.log('dep1-output: ' + content);`,
|
||||
"package.json": JSON.stringify({
|
||||
name: "dep1",
|
||||
dependencies: {
|
||||
dep0: "*",
|
||||
},
|
||||
scripts: {
|
||||
script: `${bunExe()} run index.js`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cwd: dir,
|
||||
cmd: [bunExe(), "run", "--filter", "*", "--stream", "script"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
// Verify that dep0 runs before dep1 and outputs are streamed
|
||||
expect(output).toMatch(/dep0-output/);
|
||||
expect(output).toMatch(/dep1-output: dep0-complete/);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("--stream works with pre and post scripts", () => {
|
||||
const dir = tempDirWithFiles("testworkspace-stream-lifecycle", {
|
||||
pkg1: {
|
||||
"package.json": JSON.stringify({
|
||||
name: "pkg1",
|
||||
scripts: {
|
||||
prescript: "echo pre-script",
|
||||
script: "echo main-script",
|
||||
postscript: "echo post-script",
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cwd: dir,
|
||||
cmd: [bunExe(), "run", "--filter", "*", "--stream", "script"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
// All outputs should be streamed in order
|
||||
expect(output).toMatch(/pre-script[\s\S]*main-script[\s\S]*post-script/);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("--stream handles errors correctly", () => {
|
||||
const dir = tempDirWithFiles("testworkspace-stream-error", {
|
||||
pkg1: {
|
||||
"package.json": JSON.stringify({
|
||||
name: "pkg1",
|
||||
scripts: {
|
||||
test: "echo before-error && exit 42",
|
||||
},
|
||||
}),
|
||||
},
|
||||
pkg2: {
|
||||
"package.json": JSON.stringify({
|
||||
name: "pkg2",
|
||||
scripts: {
|
||||
test: "echo pkg2-runs",
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cwd: dir,
|
||||
cmd: [bunExe(), "run", "--filter", "*", "--stream", "test"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
// Both should run even if one fails
|
||||
expect(output).toMatch(/before-error/);
|
||||
expect(output).toMatch(/pkg2-runs/);
|
||||
// Should exit with the error code from the failed script
|
||||
expect(exitCode).toBe(42);
|
||||
});
|
||||
|
||||
test("--stream works with auto command", () => {
|
||||
const dir = tempDirWithFiles("testworkspace-stream-auto", {
|
||||
packages: {
|
||||
pkg1: {
|
||||
"package.json": JSON.stringify({
|
||||
name: "pkg1",
|
||||
scripts: {
|
||||
test: "echo auto-stream-test",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
"package.json": JSON.stringify({
|
||||
name: "ws",
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
});
|
||||
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cwd: dir,
|
||||
cmd: [bunExe(), "--filter", "*", "--stream", "test"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
expect(output).toMatch(/auto-stream-test/);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user