Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
d898328df9 Add --console-log-file and --console-error-file CLI flags
Implements CLI flags to redirect console.log and console.error output to files:
- --console-log-file=<path>: redirects console.log to specified file
- --console-error-file=<path>: redirects console.error to specified file

Implementation details:
- Added OutputDestination union type in ConsoleObject.zig with three states:
  .std (use stdout/stderr), .path (file path to open), .file (opened file)
- File opening is lazy - files are opened on first write attempt
- Files are opened with WRONLY | CREAT | TRUNC flags (0644 permissions)
- Added ensureLogFileWriter() and ensureErrorFileWriter() helper functions
- Updated VirtualMachine init functions to pass console file paths from CLI context
- Added comprehensive tests in test/cli/console-file-redirect.test.ts

This allows conditional redirection of console output for logging and debugging purposes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 04:48:01 +00:00
5 changed files with 306 additions and 4 deletions

View File

@@ -8,6 +8,13 @@ const DEFAULT_CONSOLE_LOG_DEPTH: u16 = 2;
const Counter = std.AutoHashMapUnmanaged(u64, u32);
/// Output destination for console output - can be stdout/stderr or a file path
pub const OutputDestination = union(enum) {
std: void,
path: []const u8,
file: bun.sys.File,
};
const BufferedWriter = std.io.BufferedWriter(4096, Output.WriterType);
error_writer: BufferedWriter,
writer: BufferedWriter,
@@ -15,6 +22,11 @@ default_indent: u16 = 0,
counts: Counter = .{},
/// Lazy console log file destination
lazy_console_log_file: OutputDestination = .{ .std = {} },
/// Lazy console error file destination
lazy_console_error_file: OutputDestination = .{ .std = {} },
pub fn format(_: @This(), comptime _: []const u8, _: anytype, _: anytype) !void {}
pub fn init(error_writer: Output.WriterType, writer: Output.WriterType) ConsoleObject {
@@ -24,6 +36,78 @@ pub fn init(error_writer: Output.WriterType, writer: Output.WriterType) ConsoleO
};
}
pub fn initWithFiles(
error_writer: Output.WriterType,
writer: Output.WriterType,
log_file_path: ?[]const u8,
error_file_path: ?[]const u8,
) ConsoleObject {
var console = ConsoleObject{
.error_writer = BufferedWriter{ .unbuffered_writer = error_writer },
.writer = BufferedWriter{ .unbuffered_writer = writer },
};
if (log_file_path) |path| {
console.lazy_console_log_file = .{ .path = path };
}
if (error_file_path) |path| {
console.lazy_console_error_file = .{ .path = path };
}
return console;
}
/// Ensures the console log file is opened if needed and returns the appropriate writer
fn ensureLogFileWriter(console: *ConsoleObject) !Output.WriterType {
switch (console.lazy_console_log_file) {
.std => return Output.writer(),
.path => |path| {
// Open the file and transition to .file state
// Convert path to null-terminated string
var path_buf: bun.PathBuffer = undefined;
if (path.len >= path_buf.len) return error.PathTooLong;
@memcpy(path_buf[0..path.len], path);
path_buf[path.len] = 0;
const path_z = path_buf[0..path.len :0];
const file = try bun.sys.File.openat(
bun.FD.cwd(),
path_z,
bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC,
0o644,
).unwrap();
console.lazy_console_log_file = .{ .file = file };
return file.handle.quietWriter();
},
.file => |file| return file.handle.quietWriter(),
}
}
/// Ensures the console error file is opened if needed and returns the appropriate writer
fn ensureErrorFileWriter(console: *ConsoleObject) !Output.WriterType {
switch (console.lazy_console_error_file) {
.std => return Output.errorWriter(),
.path => |path| {
// Open the file and transition to .file state
// Convert path to null-terminated string
var path_buf: bun.PathBuffer = undefined;
if (path.len >= path_buf.len) return error.PathTooLong;
@memcpy(path_buf[0..path.len], path);
path_buf[path.len] = 0;
const path_z = path_buf[0..path.len :0];
const file = try bun.sys.File.openat(
bun.FD.cwd(),
path_z,
bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC,
0o644,
).unwrap();
console.lazy_console_error_file = .{ .file = file };
return file.handle.quietWriter();
},
.file => |file| return file.handle.quietWriter(),
}
}
pub const MessageLevel = enum(u32) {
Log = 0,
Warning = 1,
@@ -124,6 +208,15 @@ fn messageWithTypeAndLevel_(
}
}
// Ensure file writers are set up if redirecting console output
if (level == .Warning or level == .Error or message_type == .Assert) {
const error_writer = ensureErrorFileWriter(console) catch Output.errorWriter();
console.error_writer.unbuffered_writer = error_writer;
} else {
const log_writer = ensureLogFileWriter(console) catch Output.writer();
console.writer.unbuffered_writer = log_writer;
}
if (message_type == .Clear) {
Output.resetTerminal();
return;

View File

@@ -981,7 +981,13 @@ pub fn initWithModuleGraph(
const allocator = opts.allocator;
VMHolder.vm = try allocator.create(VirtualMachine);
const console = try allocator.create(ConsoleObject);
console.* = ConsoleObject.init(Output.errorWriter(), Output.writer());
const cli_ctx = CLI.get();
console.* = ConsoleObject.initWithFiles(
Output.errorWriter(),
Output.writer(),
cli_ctx.runtime_options.console_log_file,
cli_ctx.runtime_options.console_error_file,
);
const log = opts.log.?;
const transpiler = try Transpiler.init(
allocator,
@@ -1103,7 +1109,13 @@ pub fn init(opts: Options) !*VirtualMachine {
VMHolder.vm = try allocator.create(VirtualMachine);
const console = try allocator.create(ConsoleObject);
console.* = ConsoleObject.init(Output.errorWriter(), Output.writer());
const cli_ctx = CLI.get();
console.* = ConsoleObject.initWithFiles(
Output.errorWriter(),
Output.writer(),
cli_ctx.runtime_options.console_log_file,
cli_ctx.runtime_options.console_error_file,
);
const transpiler = try Transpiler.init(
allocator,
log,
@@ -1266,7 +1278,13 @@ pub fn initWorker(
VMHolder.vm = try allocator.create(VirtualMachine);
const console = try allocator.create(ConsoleObject);
console.* = ConsoleObject.init(Output.errorWriter(), Output.writer());
const cli_ctx = CLI.get();
console.* = ConsoleObject.initWithFiles(
Output.errorWriter(),
Output.writer(),
cli_ctx.runtime_options.console_log_file,
cli_ctx.runtime_options.console_error_file,
);
const transpiler = try Transpiler.init(
allocator,
log,
@@ -1364,7 +1382,13 @@ pub fn initBake(opts: Options) anyerror!*VirtualMachine {
VMHolder.vm = try allocator.create(VirtualMachine);
const console = try allocator.create(ConsoleObject);
console.* = ConsoleObject.init(Output.errorWriter(), Output.writer());
const cli_ctx = CLI.get();
console.* = ConsoleObject.initWithFiles(
Output.errorWriter(),
Output.writer(),
cli_ctx.runtime_options.console_log_file,
cli_ctx.runtime_options.console_error_file,
);
const transpiler = try Transpiler.init(
allocator,
log,
@@ -3713,6 +3737,7 @@ const Ordinal = bun.Ordinal;
const Output = bun.Output;
const SourceMap = bun.SourceMap;
const String = bun.String;
const CLI = bun.cli.Command;
const Transpiler = bun.Transpiler;
const Watcher = bun.Watcher;
const default_allocator = bun.default_allocator;

View File

@@ -387,6 +387,8 @@ pub const Command = struct {
expose_gc: bool = false,
preserve_symlinks_main: bool = false,
console_depth: ?u16 = null,
console_log_file: ?[]const u8 = null,
console_error_file: ?[]const u8 = null,
};
var global_cli_ctx: Context = undefined;

View File

@@ -112,6 +112,8 @@ pub const runtime_params_ = [_]ParamType{
clap.parseParam("--no-addons Throw an error if process.dlopen is called, and disable export condition \"node-addons\"") catch unreachable,
clap.parseParam("--unhandled-rejections <STR> One of \"strict\", \"throw\", \"warn\", \"none\", or \"warn-with-error-code\"") catch unreachable,
clap.parseParam("--console-depth <NUMBER> Set the default depth for console.log object inspection (default: 2)") catch unreachable,
clap.parseParam("--console-log-file <STR> Redirect console.log output to a file") catch unreachable,
clap.parseParam("--console-error-file <STR> Redirect console.error output to a file") catch unreachable,
clap.parseParam("--user-agent <STR> Set the default User-Agent header for HTTP requests") catch unreachable,
};
@@ -737,6 +739,14 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
ctx.runtime_options.console_depth = if (depth == 0) std.math.maxInt(u16) else depth;
}
if (args.option("--console-log-file")) |log_file| {
ctx.runtime_options.console_log_file = log_file;
}
if (args.option("--console-error-file")) |error_file| {
ctx.runtime_options.console_error_file = error_file;
}
if (args.option("--dns-result-order")) |order| {
ctx.runtime_options.dns_result_order = order;
}

View File

@@ -0,0 +1,172 @@
import { expect, test } from "bun:test";
import { existsSync, readFileSync } from "fs";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
test("--console-log-file redirects console.log to file", async () => {
using dir = tempDir("console-log-file", {
"script.js": `
console.log("hello from console.log");
console.log("second line");
console.log({ foo: "bar" });
`,
});
const logFile = join(String(dir), "console.log");
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-log-file", logFile, "script.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// stdout should be empty since console.log was redirected
expect(stdout).toBe("");
expect(stderr).toBe("");
// Check that the log file was created and contains the output
expect(existsSync(logFile)).toBe(true);
const logContent = readFileSync(logFile, "utf-8");
expect(logContent).toContain("hello from console.log");
expect(logContent).toContain("second line");
expect(logContent).toContain("foo");
expect(logContent).toContain("bar");
expect(exitCode).toBe(0);
});
test("--console-error-file redirects console.error to file", async () => {
using dir = tempDir("console-error-file", {
"script.js": `
console.error("error message");
console.error("another error");
console.warn("warning message");
`,
});
const errorFile = join(String(dir), "console.error");
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-error-file", errorFile, "script.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// stderr should be empty since console.error was redirected
expect(stdout).toBe("");
expect(stderr).toBe("");
// Check that the error file was created and contains the output
expect(existsSync(errorFile)).toBe(true);
const errorContent = readFileSync(errorFile, "utf-8");
expect(errorContent).toContain("error message");
expect(errorContent).toContain("another error");
expect(errorContent).toContain("warning message");
expect(exitCode).toBe(0);
});
test("both --console-log-file and --console-error-file work together", async () => {
using dir = tempDir("console-both-files", {
"script.js": `
console.log("log message");
console.error("error message");
console.log("another log");
console.error("another error");
`,
});
const logFile = join(String(dir), "console.log");
const errorFile = join(String(dir), "console.error");
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-log-file", logFile, "--console-error-file", errorFile, "script.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Both stdout and stderr should be empty
expect(stdout).toBe("");
expect(stderr).toBe("");
// Check log file
expect(existsSync(logFile)).toBe(true);
const logContent = readFileSync(logFile, "utf-8");
expect(logContent).toContain("log message");
expect(logContent).toContain("another log");
expect(logContent).not.toContain("error message");
// Check error file
expect(existsSync(errorFile)).toBe(true);
const errorContent = readFileSync(errorFile, "utf-8");
expect(errorContent).toContain("error message");
expect(errorContent).toContain("another error");
expect(errorContent).not.toContain("log message");
expect(exitCode).toBe(0);
});
test("console file redirection with relative paths", async () => {
using dir = tempDir("console-relative-path", {
"script.js": `
console.log("relative path log");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-log-file", "output.log", "script.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
// Check that the file was created in the working directory
const logFile = join(String(dir), "output.log");
expect(existsSync(logFile)).toBe(true);
const logContent = readFileSync(logFile, "utf-8");
expect(logContent).toContain("relative path log");
expect(exitCode).toBe(0);
});
test("console file redirection overwrites existing file", async () => {
using dir = tempDir("console-overwrite", {
"script.js": `
console.log("new content");
`,
"console.log": "old content that should be overwritten",
});
const logFile = join(String(dir), "console.log");
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-log-file", logFile, "script.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
const logContent = readFileSync(logFile, "utf-8");
expect(logContent).not.toContain("old content");
expect(logContent).toContain("new content");
});