eval: serialize returned objects as JSON

Objects and arrays returned from eval now serialize to JSON instead of
"[object Object]". Native errors and functions retain their string form.
This commit is contained in:
Adrià Arrufat
2026-05-31 16:19:34 +02:00
parent 27c2fe00c7
commit ab3deec523
3 changed files with 42 additions and 4 deletions

View File

@@ -56,6 +56,10 @@ pub fn isFunction(self: Value) bool {
return v8.v8__Value__IsFunction(self.handle);
}
pub fn isNativeError(self: Value) bool {
return v8.v8__Value__IsNativeError(self.handle);
}
pub fn isNull(self: Value) bool {
return v8.v8__Value__IsNull(self.handle);
}

View File

@@ -1076,22 +1076,32 @@ fn runEval(arena: std.mem.Allocator, page: *lp.Frame, script: [:0]const u8, fall
}
const settled = promise.result();
const rejected = promise.state() == .rejected;
// No-return async IIFE → undefined → silence, so pipes stay clean.
if (promise.state() == .fulfilled and settled.isUndefined()) return .{ .text = "" };
const text = settled.toStringSliceWithAlloc(arena) catch |err| switch (err) {
if (!rejected and settled.isUndefined()) return .{ .text = "" };
const text = (if (rejected) settled.toStringSliceWithAlloc(arena) else evalResultText(arena, settled)) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return .{ .text = try formatJsError(arena, &try_catch, err), .is_error = true },
};
return .{ .text = text, .is_error = (promise.state() == .rejected) };
return .{ .text = text, .is_error = rejected };
}
const text = js_result.toStringSliceWithAlloc(arena) catch |err| switch (err) {
const text = evalResultText(arena, js_result) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return .{ .text = try formatJsError(arena, &try_catch, err), .is_error = true },
};
return .{ .text = text };
}
/// Objects/arrays serialize as JSON so `return obj` prints data, not
/// `[object Object]`; errors and primitives keep their string form.
fn evalResultText(arena: std.mem.Allocator, value: lp.js.Value) ![]u8 {
if (value.isObject() and !value.isFunction() and !value.isNativeError()) {
return value.toJson(arena);
}
return value.toStringSliceWithAlloc(arena);
}
fn formatJsError(arena: std.mem.Allocator, try_catch: *lp.js.TryCatch, err: anyerror) error{OutOfMemory}![]const u8 {
const caught = try_catch.caughtOrError(arena, err);
var aw: std.Io.Writer.Allocating = .init(arena);

View File

@@ -587,6 +587,30 @@ test "MCP - eval: bare expression still returns its value" {
} }, out.written());
}
test "MCP - eval: object return serializes as JSON" {
defer testing.reset();
var out: std.io.Writer.Allocating = .init(testing.arena_allocator);
const server = try testLoadPage("about:blank", &out.writer);
defer server.deinit();
const msg =
\\{
\\ "jsonrpc": "2.0",
\\ "id": 1,
\\ "method": "tools/call",
\\ "params": {
\\ "name": "eval",
\\ "arguments": { "script": "return { n: 42, items: [1, 2] };" }
\\ }
\\}
;
try router.handleMessage(server, testing.arena_allocator, msg);
try testing.expectJson(.{ .id = 1, .result = .{
.content = &.{.{ .type = "text", .text = "{\"n\":42,\"items\":[1,2]}" }},
} }, out.written());
}
test "MCP - eval: localStorage persists across navigations and is origin-scoped" {
defer testing.reset();
var out: std.io.Writer.Allocating = .init(testing.arena_allocator);