Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
4bd795f848 fix(console): print blank line when console methods called with no args
JSC's ConsoleClient skips calling messageWithTypeAndLevel when console
methods (log, warn, error, info, debug) are called with no arguments.
Node.js prints a blank line in this case for formatting purposes.

This fix wraps the native console methods with builtins that pass an
empty string when called with no arguments, ensuring our
messageWithTypeAndLevel function is invoked and prints a newline.

Fixes #26151

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 20:18:28 +00:00
4 changed files with 168 additions and 0 deletions

View File

@@ -2806,6 +2806,24 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm)
consoleObject->putDirectCustomAccessor(vm, Identifier::fromString(vm, "Console"_s), CustomGetterSetter::create(vm, getConsoleConstructor, nullptr), PropertyAttribute::CustomValue | 0);
consoleObject->putDirectCustomAccessor(vm, Identifier::fromString(vm, "_stdout"_s), CustomGetterSetter::create(vm, getConsoleStdout, nullptr), PropertyAttribute::DontEnum | PropertyAttribute::CustomValue | 0);
consoleObject->putDirectCustomAccessor(vm, Identifier::fromString(vm, "_stderr"_s), CustomGetterSetter::create(vm, getConsoleStderr, nullptr), PropertyAttribute::DontEnum | PropertyAttribute::CustomValue | 0);
// Wrap console methods to handle zero-argument calls (fixes #26151).
// JSC's ConsoleClient skips calling messageWithTypeAndLevel when there are no arguments,
// but Node.js prints an empty line in that case. These wrappers ensure we pass an empty
// string when called with no arguments, so our messageWithTypeAndLevel is invoked.
auto wrapConsoleMethod = [&](const Identifier& publicName, const Identifier& privateName, JSC::FunctionExecutable* (*codeGenerator)(JSC::VM&)) {
JSValue nativeMethod = consoleObject->get(this, publicName);
if (nativeMethod.isCallable()) {
consoleObject->putDirect(vm, privateName, nativeMethod, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
consoleObject->putDirectBuiltinFunction(vm, this, publicName, codeGenerator(vm), PropertyAttribute::Builtin | 0);
}
};
wrapConsoleMethod(clientData->builtinNames().logPublicName(), clientData->builtinNames().logPrivateName(), consoleObjectLogCodeGenerator);
wrapConsoleMethod(clientData->builtinNames().warnPublicName(), clientData->builtinNames().warnPrivateName(), consoleObjectWarnCodeGenerator);
wrapConsoleMethod(clientData->builtinNames().errorPublicName(), clientData->builtinNames().errorPrivateName(), consoleObjectErrorCodeGenerator);
wrapConsoleMethod(clientData->builtinNames().infoPublicName(), clientData->builtinNames().infoPrivateName(), consoleObjectInfoCodeGenerator);
wrapConsoleMethod(clientData->builtinNames().debugPublicName(), clientData->builtinNames().debugPrivateName(), consoleObjectDebugCodeGenerator);
}
// ===================== start conditional builtin globals =====================

View File

@@ -91,6 +91,7 @@ using namespace JSC;
macro(cwd) \
macro(data) \
macro(dataView) \
macro(debug) \
macro(decode) \
macro(delimiter) \
macro(dest) \
@@ -105,6 +106,7 @@ using namespace JSC;
macro(encoding) \
macro(end) \
macro(errno) \
macro(error) \
macro(errorSteps) \
macro(evaluateCommonJSModule) \
macro(evaluated) \
@@ -139,6 +141,7 @@ using namespace JSC;
macro(httpOnly) \
macro(ignoreBOM) \
macro(importer) \
macro(info) \
macro(inFlightCloseRequest) \
macro(inFlightWriteRequest) \
macro(inherits) \
@@ -159,6 +162,7 @@ using namespace JSC;
macro(lineText) \
macro(loadEsmIntoCjs) \
macro(localStreams) \
macro(log) \
macro(main) \
macro(makeAbortError) \
macro(makeDOMException) \
@@ -273,6 +277,7 @@ using namespace JSC;
macro(version) \
macro(versions) \
macro(view) \
macro(warn) \
macro(warning) \
macro(writable) \
macro(write) \

View File

@@ -1,3 +1,48 @@
// Wrappers for console methods to handle zero-argument calls.
// JSC's ConsoleClient skips calling messageWithTypeAndLevel when there are no arguments,
// but Node.js prints an empty line in that case. These wrappers ensure we pass an empty
// string when called with no arguments, so our messageWithTypeAndLevel is invoked.
export function log(this: Console) {
const nativeLog = $getByIdDirectPrivate(this, "log");
if ($argumentCount() === 0) {
return nativeLog.$call(this, "");
}
return nativeLog.$apply(this, arguments);
}
export function warn(this: Console) {
const nativeWarn = $getByIdDirectPrivate(this, "warn");
if ($argumentCount() === 0) {
return nativeWarn.$call(this, "");
}
return nativeWarn.$apply(this, arguments);
}
export function error(this: Console) {
const nativeError = $getByIdDirectPrivate(this, "error");
if ($argumentCount() === 0) {
return nativeError.$call(this, "");
}
return nativeError.$apply(this, arguments);
}
export function info(this: Console) {
const nativeInfo = $getByIdDirectPrivate(this, "info");
if ($argumentCount() === 0) {
return nativeInfo.$call(this, "");
}
return nativeInfo.$apply(this, arguments);
}
export function debug(this: Console) {
const nativeDebug = $getByIdDirectPrivate(this, "debug");
if ($argumentCount() === 0) {
return nativeDebug.$call(this, "");
}
return nativeDebug.$apply(this, arguments);
}
$overriddenName = "[Symbol.asyncIterator]";
export function asyncIterator(this: Console) {
var stream = Bun.stdin.stream();

View File

@@ -0,0 +1,100 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Test for https://github.com/oven-sh/bun/issues/26151
// console.log() with zero arguments should print an empty line, matching Node.js behavior
describe("console methods with zero arguments print empty line", () => {
test("console.log()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `console.log("foo"); console.log(); console.log("bar");`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("foo\n\nbar\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("console.info()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `console.info("foo"); console.info(); console.info("bar");`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("foo\n\nbar\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("console.debug()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `console.debug("foo"); console.debug(); console.debug("bar");`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("foo\n\nbar\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("console.warn()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `console.warn("foo"); console.warn(); console.warn("bar");`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("foo\n\nbar\n");
expect(exitCode).toBe(0);
});
test("console.error()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `console.error("foo"); console.error(); console.error("bar");`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("foo\n\nbar\n");
expect(exitCode).toBe(0);
});
test("console methods with arguments still work normally", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `console.log("hello", "world"); console.log(123); console.log({ foo: "bar" });`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("hello world");
expect(stdout).toContain("123");
expect(stdout).toContain("foo");
expect(stdout).toContain("bar");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
});