mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
3 Commits
claude/upg
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2f5ce1d2a | ||
|
|
46288ee202 | ||
|
|
f920e401c1 |
@@ -155,7 +155,7 @@ pub const Run = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
bun.jsc.initialize(ctx.runtime_options.eval.eval_and_print);
|
||||
bun.jsc.initialize(ctx.runtime_options.eval.print or ctx.runtime_options.json);
|
||||
|
||||
js_ast.Expr.Data.Store.create();
|
||||
js_ast.Stmt.Data.Store.create();
|
||||
@@ -169,7 +169,7 @@ pub const Run = struct {
|
||||
.args = ctx.args,
|
||||
.store_fd = ctx.debug.hot_reload != .none,
|
||||
.smol = ctx.runtime_options.smol,
|
||||
.eval = ctx.runtime_options.eval.eval_and_print,
|
||||
.eval = ctx.runtime_options.eval.print or ctx.runtime_options.json,
|
||||
.debugger = ctx.runtime_options.debugger,
|
||||
.dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order),
|
||||
.is_main_thread = true,
|
||||
@@ -192,7 +192,7 @@ pub const Run = struct {
|
||||
script_source.* = logger.Source.initPathString(entry_path, ctx.runtime_options.eval.script);
|
||||
vm.module_loader.eval_source = script_source;
|
||||
|
||||
if (ctx.runtime_options.eval.eval_and_print) {
|
||||
if (ctx.runtime_options.eval.print or ctx.runtime_options.json) {
|
||||
b.options.dead_code_elimination = false;
|
||||
}
|
||||
}
|
||||
@@ -263,6 +263,9 @@ pub const Run = struct {
|
||||
vm.hot_reload = this.ctx.debug.hot_reload;
|
||||
vm.onUnhandledRejection = &onUnhandledRejectionBeforeClose;
|
||||
|
||||
// Enable capturing entry point result if --json is set (and not using --eval with script)
|
||||
vm.capture_entry_point_result = this.ctx.runtime_options.json and this.ctx.runtime_options.eval.script.len == 0;
|
||||
|
||||
this.addConditionalGlobals();
|
||||
do_redis_preconnect: {
|
||||
// This must happen within the API lock, which is why it's not in the "doPreconnect" function
|
||||
@@ -412,7 +415,10 @@ pub const Run = struct {
|
||||
vm.eventLoop().autoTickActive();
|
||||
}
|
||||
|
||||
if (this.ctx.runtime_options.eval.eval_and_print) {
|
||||
// Handle --print or --json flags (both for eval and regular entry points)
|
||||
const should_print = this.ctx.runtime_options.eval.print or this.ctx.runtime_options.json;
|
||||
|
||||
if (should_print) {
|
||||
const to_print = brk: {
|
||||
const result: jsc.JSValue = vm.entry_point_result.value.get() orelse .js_undefined;
|
||||
if (result.asAnyPromise()) |promise| {
|
||||
@@ -437,7 +443,22 @@ pub const Run = struct {
|
||||
break :brk result;
|
||||
};
|
||||
|
||||
to_print.print(vm.global, .Log, .Log);
|
||||
if (this.ctx.runtime_options.json) {
|
||||
var str = bun.String.empty;
|
||||
defer str.deref();
|
||||
to_print.jsonStringify(vm.global, 0, &str) catch |err| {
|
||||
vm.global.reportActiveExceptionAsUnhandled(err);
|
||||
return;
|
||||
};
|
||||
// Print the JSON string to stdout
|
||||
const utf8 = str.toUTF8(bun.default_allocator);
|
||||
defer utf8.deinit();
|
||||
_ = Output.writer().write(utf8.slice()) catch {};
|
||||
_ = Output.writer().write("\n") catch {};
|
||||
Output.flush();
|
||||
} else {
|
||||
to_print.print(vm.global, .Log, .Log);
|
||||
}
|
||||
}
|
||||
|
||||
vm.onBeforeExit();
|
||||
|
||||
@@ -18,6 +18,8 @@ comptime {
|
||||
@export(&setEntryPointEvalResultESM, .{ .name = "Bun__VM__setEntryPointEvalResultESM" });
|
||||
@export(&setEntryPointEvalResultCJS, .{ .name = "Bun__VM__setEntryPointEvalResultCJS" });
|
||||
@export(&specifierIsEvalEntryPoint, .{ .name = "Bun__VM__specifierIsEvalEntryPoint" });
|
||||
@export(&shouldCaptureEntryPointResult, .{ .name = "Bun__VM__shouldCaptureEntryPointResult" });
|
||||
@export(&isMainModule, .{ .name = "Bun__VM__isMainModule" });
|
||||
@export(&string_allocation_limit, .{ .name = "Bun__stringSyntheticAllocationLimit" });
|
||||
@export(&allowAddons, .{ .name = "Bun__VM__allowAddons" });
|
||||
@export(&allowRejectionHandledWarning, .{ .name = "Bun__VM__allowRejectionHandledWarning" });
|
||||
@@ -136,6 +138,8 @@ entry_point_result: struct {
|
||||
value: jsc.Strong.Optional = .empty,
|
||||
cjs_set_value: bool = false,
|
||||
} = .{},
|
||||
capture_entry_point_result: bool = false,
|
||||
main_module_key: jsc.Strong.Optional = .empty,
|
||||
|
||||
auto_install_dependencies: bool = false,
|
||||
|
||||
@@ -799,6 +803,26 @@ pub fn specifierIsEvalEntryPoint(this: *VirtualMachine, specifier: JSValue) call
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn shouldCaptureEntryPointResult(this: *VirtualMachine) callconv(.C) bool {
|
||||
return this.capture_entry_point_result;
|
||||
}
|
||||
|
||||
pub fn isMainModule(this: *VirtualMachine, specifier: JSValue) callconv(.C) bool {
|
||||
// If we're capturing for --json and no main module has been set yet
|
||||
if (this.capture_entry_point_result and !this.main_module_key.has()) {
|
||||
this.main_module_key.set(this.global, specifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if this matches the stored main module
|
||||
if (this.main_module_key.has()) {
|
||||
const main_key = this.main_module_key.get() orelse return false;
|
||||
return specifier.isSameValue(main_key, this.global) catch return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn setEntryPointEvalResultESM(this: *VirtualMachine, result: JSValue) callconv(.C) void {
|
||||
// allow esm evaluate to set value multiple times
|
||||
if (!this.entry_point_result.cjs_set_value) {
|
||||
|
||||
@@ -108,6 +108,7 @@ static bool canPerformFastEnumeration(Structure* s)
|
||||
|
||||
extern "C" bool Bun__VM__specifierIsEvalEntryPoint(void*, EncodedJSValue);
|
||||
extern "C" void Bun__VM__setEntryPointEvalResultCJS(void*, EncodedJSValue);
|
||||
extern "C" bool Bun__VM__isMainModule(void*, EncodedJSValue);
|
||||
|
||||
static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObject, JSCommonJSModule* moduleObject, JSString* dirname, JSValue filename)
|
||||
{
|
||||
@@ -145,7 +146,9 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj
|
||||
moduleObject->hasEvaluated = true;
|
||||
};
|
||||
|
||||
if (Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(filename))) [[unlikely]] {
|
||||
bool isEvalOrMainModule = Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(filename)) || Bun__VM__isMainModule(globalObject->bunVM(), JSValue::encode(filename));
|
||||
|
||||
if (isEvalOrMainModule) [[unlikely]] {
|
||||
initializeModuleObject();
|
||||
scope.assertNoExceptionExceptTermination();
|
||||
|
||||
@@ -163,7 +166,14 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj
|
||||
RETURN_IF_EXCEPTION(scope, false);
|
||||
ASSERT(result);
|
||||
|
||||
Bun__VM__setEntryPointEvalResultCJS(globalObject->bunVM(), JSValue::encode(result));
|
||||
if (Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(filename))) {
|
||||
Bun__VM__setEntryPointEvalResultCJS(globalObject->bunVM(), JSValue::encode(result));
|
||||
} else if (Bun__VM__isMainModule(globalObject->bunVM(), JSValue::encode(filename))) {
|
||||
// For --json main modules, capture the module.exports
|
||||
auto exports_final = moduleObject->exportsObject();
|
||||
RETURN_IF_EXCEPTION(scope, false);
|
||||
Bun__VM__setEntryPointEvalResultCJS(globalObject->bunVM(), JSValue::encode(exports_final));
|
||||
}
|
||||
|
||||
RELEASE_AND_RETURN(scope, true);
|
||||
}
|
||||
|
||||
@@ -4440,6 +4440,7 @@ JSC::JSValue GlobalObject::moduleLoaderEvaluate(JSGlobalObject* lexicalGlobalObj
|
||||
|
||||
extern "C" bool Bun__VM__specifierIsEvalEntryPoint(void*, EncodedJSValue);
|
||||
extern "C" void Bun__VM__setEntryPointEvalResultESM(void*, EncodedJSValue);
|
||||
extern "C" bool Bun__VM__isMainModule(void*, EncodedJSValue);
|
||||
|
||||
JSC::JSValue EvalGlobalObject::moduleLoaderEvaluate(JSGlobalObject* lexicalGlobalObject,
|
||||
JSModuleLoader* moduleLoader, JSValue key,
|
||||
@@ -4449,7 +4450,7 @@ JSC::JSValue EvalGlobalObject::moduleLoaderEvaluate(JSGlobalObject* lexicalGloba
|
||||
Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
|
||||
|
||||
if (scriptFetcher && scriptFetcher.isObject()) [[unlikely]] {
|
||||
if (Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(key))) {
|
||||
if (Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(key)) || Bun__VM__isMainModule(globalObject->bunVM(), JSValue::encode(key))) {
|
||||
Bun__VM__setEntryPointEvalResultESM(globalObject->bunVM(), JSValue::encode(scriptFetcher));
|
||||
}
|
||||
return scriptFetcher;
|
||||
@@ -4458,7 +4459,7 @@ JSC::JSValue EvalGlobalObject::moduleLoaderEvaluate(JSGlobalObject* lexicalGloba
|
||||
JSC::JSValue result = moduleLoader->evaluateNonVirtual(lexicalGlobalObject, key, moduleRecordValue,
|
||||
scriptFetcher, sentValue, resumeMode);
|
||||
|
||||
if (Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(key))) {
|
||||
if (Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(key)) || Bun__VM__isMainModule(globalObject->bunVM(), JSValue::encode(key))) {
|
||||
Bun__VM__setEntryPointEvalResultESM(globalObject->bunVM(), JSValue::encode(result));
|
||||
}
|
||||
|
||||
|
||||
@@ -371,8 +371,9 @@ pub const Command = struct {
|
||||
sql_preconnect: bool = false,
|
||||
eval: struct {
|
||||
script: []const u8 = "",
|
||||
eval_and_print: bool = false,
|
||||
print: bool = false,
|
||||
} = .{},
|
||||
json: bool = false,
|
||||
preconnect: []const []const u8 = &[_][]const u8{},
|
||||
dns_result_order: []const u8 = "verbatim",
|
||||
/// `--expose-gc` makes `globalThis.gc()` available. Added for Node
|
||||
|
||||
@@ -91,6 +91,7 @@ pub const runtime_params_ = [_]ParamType{
|
||||
clap.parseParam("-i Auto-install dependencies during execution. Equivalent to --install=fallback.") catch unreachable,
|
||||
clap.parseParam("-e, --eval <STR> Evaluate argument as a script") catch unreachable,
|
||||
clap.parseParam("-p, --print <STR> Evaluate argument as a script and print the result") catch unreachable,
|
||||
clap.parseParam("--json Output result as JSON instead of using console formatter") catch unreachable,
|
||||
clap.parseParam("--prefer-offline Skip staleness checks for packages in the Bun runtime and resolve from disk") catch unreachable,
|
||||
clap.parseParam("--prefer-latest Use the latest matching versions of packages in the Bun runtime, always checking npm") catch unreachable,
|
||||
clap.parseParam("--port <STR> Set the default port for Bun.serve") catch unreachable,
|
||||
@@ -645,7 +646,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
if (comptime cmd == .RunAsNodeCommand) {
|
||||
// TODO: prevent `node --port <script>` from working
|
||||
ctx.runtime_options.eval.script = port_str;
|
||||
ctx.runtime_options.eval.eval_and_print = true;
|
||||
ctx.runtime_options.eval.print = true;
|
||||
} else {
|
||||
opts.port = std.fmt.parseInt(u16, port_str, 10) catch {
|
||||
Output.errFmt(
|
||||
@@ -703,10 +704,11 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
|
||||
if (args.option("--print")) |script| {
|
||||
ctx.runtime_options.eval.script = script;
|
||||
ctx.runtime_options.eval.eval_and_print = true;
|
||||
ctx.runtime_options.eval.print = true;
|
||||
} else if (args.option("--eval")) |script| {
|
||||
ctx.runtime_options.eval.script = script;
|
||||
}
|
||||
ctx.runtime_options.json = args.flag("--json");
|
||||
ctx.runtime_options.if_present = args.flag("--if-present");
|
||||
ctx.runtime_options.smol = args.flag("--smol");
|
||||
ctx.runtime_options.preconnect = args.options("--fetch-preconnect");
|
||||
|
||||
35
test/cli/json-flag-simple.test.ts
Normal file
35
test/cli/json-flag-simple.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
describe("--json flag simple tests", () => {
|
||||
test("--print with --json outputs JSON", async () => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "42", "--json"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
await proc.exited;
|
||||
|
||||
expect(output.trim()).toBe("42");
|
||||
proc.unref();
|
||||
});
|
||||
|
||||
test("--eval with --json outputs JSON", async () => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--eval", "({x: 1, y: 2})", "--json"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
await proc.exited;
|
||||
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed).toEqual({ x: 1, y: 2 });
|
||||
proc.unref();
|
||||
});
|
||||
});
|
||||
338
test/cli/json-flag.test.ts
Normal file
338
test/cli/json-flag.test.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
describe("--json flag", () => {
|
||||
describe("with --print", () => {
|
||||
test("primitive values", async () => {
|
||||
// Number
|
||||
{
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "42", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(stdout.trim()).toBe("42");
|
||||
}
|
||||
|
||||
// String
|
||||
{
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", '"hello world"', "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(stdout.trim()).toBe('"hello world"');
|
||||
}
|
||||
|
||||
// Boolean
|
||||
{
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "true", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(stdout.trim()).toBe("true");
|
||||
}
|
||||
|
||||
// null
|
||||
{
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "null", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(stdout.trim()).toBe("null");
|
||||
}
|
||||
|
||||
// undefined (should output nothing)
|
||||
{
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "undefined", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(stdout.trim()).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
test("complex objects", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "({x: 1, y: 'test', z: [1,2,3], nested: {a: true}})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({
|
||||
x: 1,
|
||||
y: "test",
|
||||
z: [1, 2, 3],
|
||||
nested: { a: true },
|
||||
});
|
||||
});
|
||||
|
||||
test("arrays", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "[1, 'two', {three: 3}, [4,5]]", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual([1, "two", { three: 3 }, [4, 5]]);
|
||||
});
|
||||
|
||||
test("dates", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", 'new Date("2024-01-15T12:30:00.000Z")', "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(stdout.trim()).toBe('"2024-01-15T12:30:00.000Z"');
|
||||
});
|
||||
|
||||
test("promises", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "Promise.resolve({resolved: true})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ resolved: true });
|
||||
});
|
||||
|
||||
test("circular references show error message", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "(() => { const obj = {}; obj.circular = obj; return obj; })()", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
// Error is printed to stdout, not stderr
|
||||
expect(stdout).toContain("JSON.stringify cannot serialize cyclic structures");
|
||||
});
|
||||
|
||||
test("functions are undefined in JSON", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "({fn: () => {}, value: 42})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ value: 42 }); // fn should be omitted
|
||||
});
|
||||
|
||||
test("--print without --json uses console formatter", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "({x: 1, y: 2})"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
// Console formatter includes spaces and formatting
|
||||
expect(stdout.trim()).toContain("x:");
|
||||
expect(stdout.trim()).toContain("y:");
|
||||
expect(stdout.trim()).not.toBe('{"x":1,"y":2}');
|
||||
});
|
||||
});
|
||||
|
||||
describe("with --eval", () => {
|
||||
test("--eval without --json doesn't print", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--eval", "42"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(stdout.trim()).toBe("");
|
||||
});
|
||||
|
||||
test("--eval with --json prints result as JSON", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--eval", "({result: 'success'})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ result: "success" });
|
||||
});
|
||||
|
||||
test("expressions with side effects", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--eval", "console.log('side effect'); ({value: 123})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const lines = stdout.trim().split("\n");
|
||||
expect(lines[0]).toBe("side effect"); // console.log output
|
||||
expect(JSON.parse(lines[1])).toEqual({ value: 123 }); // JSON output
|
||||
});
|
||||
});
|
||||
|
||||
describe("--json with regular script files", () => {
|
||||
test("regular script files do not output JSON (only --print and --eval do)", async () => {
|
||||
const dir = tempDirWithFiles("json-flag-script", {
|
||||
"script.js": `
|
||||
console.log("This will show");
|
||||
const data = { value: 123 };
|
||||
data; // This won't be captured - regular scripts don't have return values
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--json", "script.js"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
// Only console.log output appears, no JSON
|
||||
expect(stdout.trim()).toBe("This will show");
|
||||
});
|
||||
|
||||
test("--json flag is primarily for --print and --eval", async () => {
|
||||
// Test to document that --json is meant for use with --print and --eval
|
||||
// Regular script files don't have a meaningful return value to capture
|
||||
const dir = tempDirWithFiles("json-flag-doc", {
|
||||
"data.js": `module.exports = { value: 42 };`,
|
||||
});
|
||||
|
||||
// This shows the intended usage - evaluating the module
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--eval", "require('./data.js')", "--json"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ value: 42 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("BigInt serialization", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "({bigint: 123n})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
// BigInt error is printed to stdout
|
||||
expect(stdout).toContain("JSON.stringify cannot serialize BigInt");
|
||||
});
|
||||
|
||||
test("Symbol serialization", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "({sym: Symbol('test'), value: 1})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
// Symbols are omitted in JSON
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ value: 1 });
|
||||
});
|
||||
|
||||
test("NaN and Infinity", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "({nan: NaN, inf: Infinity, negInf: -Infinity})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({
|
||||
nan: null,
|
||||
inf: null,
|
||||
negInf: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("deeply nested objects", async () => {
|
||||
const deepObj = "({a: {b: {c: {d: {e: {f: {g: {h: 'deep'}}}}}}}})";
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", deepObj, "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed.a.b.c.d.e.f.g.h).toBe("deep");
|
||||
});
|
||||
|
||||
test("large arrays", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--print", "Array.from({length: 1000}, (_, i) => i)", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed.length).toBe(1000);
|
||||
expect(parsed[0]).toBe(0);
|
||||
expect(parsed[999]).toBe(999);
|
||||
});
|
||||
});
|
||||
|
||||
describe("flag combinations", () => {
|
||||
test("--json can appear before --print", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--json", "--print", "({order: 'reversed'})"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ order: "reversed" });
|
||||
});
|
||||
|
||||
test("--json can appear before --eval", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--json", "--eval", "({order: 'eval-reversed'})"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ order: "eval-reversed" });
|
||||
});
|
||||
|
||||
test("multiple --json flags are idempotent", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--json", "--print", "({multiple: true})", "--json"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed).toEqual({ multiple: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user