diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 5931a39568..a33632b160 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -1450,6 +1450,65 @@ Process::~Process() } extern "C" bool Bun__NODE_NO_WARNINGS(); +extern "C" bool Bun__NODE_TRACE_WARNINGS(); + +// Track whether we've shown the hint message about --trace-warnings +static bool hasShownTraceWarningsHint = false; + +static void printWarningToStderr(JSC::JSGlobalObject* globalObject, JSValue errorInstance, bool traceWarnings) +{ + auto& vm = JSC::getVM(globalObject); + + // Get the warning name (e.g., "Warning", "DeprecationWarning") + auto warningName = errorInstance.get(globalObject, vm.propertyNames->name); + String nameStr = "Warning"_s; + if (warningName.isString()) { + nameStr = warningName.getString(globalObject); + } + + // Get the warning message + auto warningMessage = errorInstance.get(globalObject, vm.propertyNames->message); + String messageStr = ""_s; + if (warningMessage.isString()) { + messageStr = warningMessage.getString(globalObject); + } + + // Get PID + pid_t pid = getpid(); + + // Format: (bun:PID) WarningName: message + auto firstLine = makeString("(bun:"_s, String::number(pid), ") "_s, nameStr); + if (!messageStr.isEmpty()) { + firstLine = makeString(firstLine, ": "_s, messageStr); + } + + fprintf(stderr, "%s\n", firstLine.utf8().data()); + + if (traceWarnings) { + // Show stack trace + auto stackValue = errorInstance.get(globalObject, vm.propertyNames->stack); + if (stackValue.isString()) { + String stackStr = stackValue.getString(globalObject); + // The stack typically includes the error name and message as first line, + // so we need to extract just the stack frames + auto lines = stackStr.split('\n'); + bool firstLine = true; + for (const auto& line : lines) { + // Skip the first line if it looks like an error message line + if (firstLine && (line.startsWith(nameStr) || line.contains(messageStr))) { + firstLine = false; + continue; + } + firstLine = false; + fprintf(stderr, "%s\n", line.utf8().data()); + } + } + } else if (!hasShownTraceWarningsHint) { + // Show the hint about --trace-warnings only once per process + fprintf(stderr, "(Use `bun --trace-warnings ...` to show where the warning was created)\n"); + hasShownTraceWarningsHint = true; + } +} JSC_DEFINE_HOST_FUNCTION(jsFunction_emitWarning, (JSC::JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { @@ -1466,8 +1525,8 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_emitWarning, (JSC::JSGlobalObject * lexicalG process->wrapped().emit(ident, args); return JSValue::encode(jsUndefined()); } else if (!Bun__NODE_NO_WARNINGS()) { - auto jsArgs = JSValue::encode(value); - Bun__ConsoleObject__messageWithTypeAndLevel(reinterpret_cast(globalObject->consoleClient().get())->m_client, static_cast(MessageType::Log), static_cast(MessageLevel::Warning), globalObject, &jsArgs, 1); + // Use Node.js-compatible warning formatting + printWarningToStderr(globalObject, value, Bun__NODE_TRACE_WARNINGS()); RETURN_IF_EXCEPTION(scope, {}); } return JSValue::encode(jsUndefined()); diff --git a/src/bun.js/node/node_process.zig b/src/bun.js/node/node_process.zig index 91362e45f0..0151fd3618 100644 --- a/src/bun.js/node/node_process.zig +++ b/src/bun.js/node/node_process.zig @@ -342,6 +342,11 @@ pub export fn Bun__NODE_NO_WARNINGS() bool { return bun.feature_flag.NODE_NO_WARNINGS.get(); } +extern var Bun__Node__ProcessTraceWarnings: bool; +pub export fn Bun__NODE_TRACE_WARNINGS() bool { + return Bun__Node__ProcessTraceWarnings; +} + pub export fn Bun__suppressCrashOnProcessKillSelfIfDesired() void { if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_ON_PROCESS_KILL_SELF.get()) { bun.crash_handler.suppressReporting(); diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 2c55ffb5d1..416754f3d0 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -102,6 +102,7 @@ pub const runtime_params_ = [_]ParamType{ clap.parseParam("--expose-gc Expose gc() on the global object. Has no effect on Bun.gc().") catch unreachable, clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable, clap.parseParam("--throw-deprecation Determine whether or not deprecation warnings result in errors.") catch unreachable, + clap.parseParam("--trace-warnings Show stack traces on process warnings") catch unreachable, clap.parseParam("--title Set the process title") catch unreachable, clap.parseParam("--zero-fill-buffers Boolean to force Buffer.allocUnsafe(size) to be zero-filled.") catch unreachable, clap.parseParam("--use-system-ca Use the system's trusted certificate authorities") catch unreachable, @@ -784,6 +785,9 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C if (args.flag("--throw-deprecation")) { Bun__Node__ProcessThrowDeprecation = true; } + if (args.flag("--trace-warnings")) { + Bun__Node__ProcessTraceWarnings = true; + } if (args.option("--title")) |title| { CLI.Bun__Node__ProcessTitle = title; } @@ -1319,6 +1323,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C export var Bun__Node__ZeroFillBuffers = false; export var Bun__Node__ProcessNoDeprecation = false; export var Bun__Node__ProcessThrowDeprecation = false; +export var Bun__Node__ProcessTraceWarnings = false; pub const BunCAStore = enum(u8) { bundled, openssl, system }; pub export var Bun__Node__CAStore: BunCAStore = .bundled; diff --git a/test/js/node/process/process-trace-warnings.test.ts b/test/js/node/process/process-trace-warnings.test.ts new file mode 100644 index 0000000000..2078293982 --- /dev/null +++ b/test/js/node/process/process-trace-warnings.test.ts @@ -0,0 +1,165 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("process.emitWarning without --trace-warnings shows minimal format", async () => { + using dir = tempDir("trace-warnings-test", { + "test.js": `process.emitWarning('test warning');`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + // Should show minimal format with PID + expect(stderr).toMatch(/\(bun:\d+\) Warning: test warning/); + // Should show the hint about --trace-warnings + expect(stderr).toContain("(Use `bun --trace-warnings ...` to show where the warning was created)"); + // Should NOT show stack trace + expect(stderr).not.toContain("at "); + + expect(exitCode).toBe(0); +}); + +test("process.emitWarning with --trace-warnings shows stack trace", async () => { + using dir = tempDir("trace-warnings-test", { + "test.js": `process.emitWarning('test warning');`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--trace-warnings", "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + // Should show minimal format with PID + expect(stderr).toMatch(/\(bun:\d+\) Warning: test warning/); + // Should show stack trace + expect(stderr).toContain("at "); + // Should NOT show the hint when --trace-warnings is used + expect(stderr).not.toContain("(Use `bun --trace-warnings ...` to show where the warning was created)"); + + expect(exitCode).toBe(0); +}); + +test("process.emitWarning with custom type", async () => { + using dir = tempDir("trace-warnings-test", { + "test.js": `process.emitWarning('custom message', 'CustomWarning');`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + // Should show custom warning type + expect(stderr).toMatch(/\(bun:\d+\) CustomWarning: custom message/); + + expect(exitCode).toBe(0); +}); + +test("multiple warnings show hint only once", async () => { + using dir = tempDir("trace-warnings-test", { + "test.js": ` + process.emitWarning('warning 1'); + process.emitWarning('warning 2'); + process.emitWarning('warning 3'); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + // Should show all three warnings + expect(stderr).toContain("Warning: warning 1"); + expect(stderr).toContain("Warning: warning 2"); + expect(stderr).toContain("Warning: warning 3"); + + // Should show the hint only once + const hintCount = (stderr.match(/Use `bun --trace-warnings/g) || []).length; + expect(hintCount).toBe(1); + + expect(exitCode).toBe(0); +}); + +test("DeprecationWarning is shown by default", async () => { + using dir = tempDir("trace-warnings-test", { + "test.js": `process.emitWarning('deprecated API', 'DeprecationWarning');`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(stderr).toMatch(/\(bun:\d+\) DeprecationWarning: deprecated API/); + + expect(exitCode).toBe(0); +}); + +test("NODE_NO_WARNINGS=1 suppresses warnings", async () => { + using dir = tempDir("trace-warnings-test", { + "test.js": `process.emitWarning('test warning');`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: { ...bunEnv, NODE_NO_WARNINGS: "1" }, + cwd: String(dir), + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + // Should NOT show any warning + expect(stderr).not.toContain("Warning:"); + expect(stderr).not.toContain("test warning"); + + expect(exitCode).toBe(0); +}); + +test("process.emitWarning with code and detail", async () => { + using dir = tempDir("trace-warnings-test", { + "test.js": ` + process.emitWarning('test warning', { + type: 'CustomWarning', + code: 'CODE001', + detail: 'Additional details here' + }); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(stderr).toMatch(/\(bun:\d+\) CustomWarning: test warning/); + + expect(exitCode).toBe(0); +});