Merge pull request #579 from lightpanda-io/console
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

add console web api
This commit is contained in:
Karl Seguin
2025-05-01 09:50:37 +08:00
committed by GitHub
4 changed files with 268 additions and 8 deletions

View File

@@ -17,12 +17,224 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.console); const builtin = @import("builtin");
const JsObject = @import("../env.zig").Env.JsObject;
const SessionState = @import("../env.zig").SessionState;
const log = if (builtin.is_test) &test_capture else std.log.scoped(.console);
pub const Console = struct { pub const Console = struct {
// TODO: configurable writer // TODO: configurable writer
timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{},
pub fn _log(_: *const Console, str: []const u8) void { pub fn _log(_: *const Console, values: []JsObject, state: *SessionState) !void {
log.debug("{s}\n", .{str}); if (values.len == 0) {
return;
}
log.info("{s}", .{try serializeValues(values, state)});
}
pub fn _info(console: *const Console, values: []JsObject, state: *SessionState) !void {
return console._log(values, state);
}
pub fn _debug(_: *const Console, values: []JsObject, state: *SessionState) !void {
if (values.len == 0) {
return;
}
log.debug("{s}", .{try serializeValues(values, state)});
}
pub fn _warn(_: *const Console, values: []JsObject, state: *SessionState) !void {
if (values.len == 0) {
return;
}
log.warn("{s}", .{try serializeValues(values, state)});
}
pub fn _error(_: *const Console, values: []JsObject, state: *SessionState) !void {
if (values.len == 0) {
return;
}
log.err("{s}", .{try serializeValues(values, state)});
}
pub fn _clear(_: *const Console) void {}
pub fn _count(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
const label = label_ orelse "default";
const gop = try self.counts.getOrPut(state.arena, label);
var current: u32 = 0;
if (gop.found_existing) {
current = gop.value_ptr.*;
} else {
gop.key_ptr.* = try state.arena.dupe(u8, label);
}
const count = current + 1;
gop.value_ptr.* = count;
log.info("{s}: {d}", .{ label, count });
}
pub fn _countReset(self: *Console, label_: ?[]const u8) !void {
const label = label_ orelse "default";
const kv = self.counts.fetchRemove(label) orelse {
log.warn("Counter \"{s}\" doesn't exist.", .{label});
return;
};
log.info("{s}: {d}", .{ label, kv.value });
}
pub fn _time(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
const label = label_ orelse "default";
const gop = try self.timers.getOrPut(state.arena, label);
if (gop.found_existing) {
log.warn("Timer \"{s}\" already exists.", .{label});
return;
}
gop.key_ptr.* = try state.arena.dupe(u8, label);
gop.value_ptr.* = timestamp();
}
pub fn _timeLog(self: *Console, label_: ?[]const u8) void {
const elapsed = timestamp();
const label = label_ orelse "default";
const start = self.timers.get(label) orelse {
log.warn("Timer \"{s}\" doesn't exist.", .{label});
return;
};
log.info("\"{s}\": {d}ms", .{ label, elapsed - start });
}
pub fn _timeStop(self: *Console, label_: ?[]const u8) void {
const elapsed = timestamp();
const label = label_ orelse "default";
const kv = self.timers.fetchRemove(label) orelse {
log.warn("Timer \"{s}\" doesn't exist.", .{label});
return;
};
log.info("\"{s}\": {d}ms - timer ended", .{ label, elapsed - kv.value });
}
pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, state: *SessionState) !void {
if (assertion.isTruthy()) {
return;
}
var serialized_values: []const u8 = "";
if (values.len > 0) {
serialized_values = try serializeValues(values, state);
}
log.err("Assertion failed: {s}", .{serialized_values});
}
fn serializeValues(values: []JsObject, state: *SessionState) ![]const u8 {
const arena = state.call_arena;
var arr: std.ArrayListUnmanaged(u8) = .{};
try arr.appendSlice(arena, try values[0].toString());
for (values[1..]) |value| {
try arr.append(arena, ' ');
try arr.appendSlice(arena, try value.toString());
}
return arr.items;
}
};
fn timestamp() u32 {
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
return @intCast(ts.sec);
}
var test_capture = TestCapture{};
const testing = @import("../../testing.zig");
test "Browser.Console" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
defer testing.reset();
{
try runner.testCases(&.{
.{ "console.log('a')", "undefined" },
.{ "console.warn('hello world', 23, true, new Object())", "undefined" },
}, .{});
const captured = test_capture.captured.items;
try testing.expectEqual("a", captured[0]);
try testing.expectEqual("hello world 23 true [object Object]", captured[1]);
}
{
test_capture.reset();
try runner.testCases(&.{
.{ "console.countReset()", "undefined" },
.{ "console.count()", "undefined" },
.{ "console.count('teg')", "undefined" },
.{ "console.count('teg')", "undefined" },
.{ "console.count('teg')", "undefined" },
.{ "console.count()", "undefined" },
.{ "console.countReset('teg')", "undefined" },
.{ "console.countReset()", "undefined" },
.{ "console.count()", "undefined" },
}, .{});
const captured = test_capture.captured.items;
try testing.expectEqual("Counter \"default\" doesn't exist.", captured[0]);
try testing.expectEqual("default: 1", captured[1]);
try testing.expectEqual("teg: 1", captured[2]);
try testing.expectEqual("teg: 2", captured[3]);
try testing.expectEqual("teg: 3", captured[4]);
try testing.expectEqual("default: 2", captured[5]);
try testing.expectEqual("teg: 3", captured[6]);
try testing.expectEqual("default: 2", captured[7]);
try testing.expectEqual("default: 1", captured[8]);
}
{
test_capture.reset();
try runner.testCases(&.{
.{ "console.assert(true)", "undefined" },
.{ "console.assert('a', 2, 3, 4)", "undefined" },
.{ "console.assert('')", "undefined" },
.{ "console.assert('', 'x', true)", "undefined" },
.{ "console.assert(false, 'x')", "undefined" },
}, .{});
const captured = test_capture.captured.items;
try testing.expectEqual("Assertion failed: ", captured[0]);
try testing.expectEqual("Assertion failed: x true", captured[1]);
try testing.expectEqual("Assertion failed: x", captured[2]);
}
}
const TestCapture = struct {
captured: std.ArrayListUnmanaged([]const u8) = .{},
fn reset(self: *TestCapture) void {
self.captured = .{};
}
fn debug(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
const str = std.fmt.allocPrint(testing.arena_allocator, fmt, args) catch unreachable;
self.captured.append(testing.arena_allocator, str) catch unreachable;
}
fn info(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
self.debug(fmt, args);
}
fn warn(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
self.debug(fmt, args);
}
fn err(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
self.debug(fmt, args);
} }
}; };

View File

@@ -35,4 +35,9 @@ pub const SessionState = struct {
http_client: *HttpClient, http_client: *HttpClient,
cookie_jar: *storage.CookieJar, cookie_jar: *storage.CookieJar,
document: ?*parser.DocumentHTML, document: ?*parser.DocumentHTML,
// dangerous, but set by the JS framework
// shorter-lived than the arena above, which
// exists for the entire rendering of the page
call_arena: std.mem.Allocator = undefined,
}; };

View File

@@ -25,6 +25,7 @@ const SessionState = @import("../env.zig").SessionState;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History; const History = @import("history.zig").History;
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const Console = @import("../console/console.zig").Console;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const storage = @import("../storage/storage.zig"); const storage = @import("../storage/storage.zig");
@@ -48,6 +49,7 @@ pub const Window = struct {
timeoutid: u32 = 0, timeoutid: u32 = 0,
timeoutids: [512]u64 = undefined, timeoutids: [512]u64 = undefined,
console: Console = .{},
navigator: Navigator = .{}, navigator: Navigator = .{},
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
@@ -85,6 +87,10 @@ pub const Window = struct {
return &self.location; return &self.location;
} }
pub fn get_console(self: *Window) *Console {
return &self.console;
}
pub fn get_self(self: *Window) *Window { pub fn get_self(self: *Window) *Window {
return self; return self;
} }

View File

@@ -416,6 +416,16 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
} }
errdefer if (enter) handle_scope.?.deinit(); errdefer if (enter) handle_scope.?.deinit();
{
// If we want to overwrite the built-in console, we have to
// delete the built-in one.
const js_obj = context.getGlobal();
const console_key = v8.String.initUtf8(isolate, "console");
if (js_obj.deleteValue(context, console_key) == false) {
return error.ConsoleDeleteError;
}
}
self.scope = Scope{ self.scope = Scope{
.state = state, .state = state,
.isolate = isolate, .isolate = isolate,
@@ -439,6 +449,16 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
context.setEmbedderData(1, data); context.setEmbedderData(1, data);
} }
{
// Not the prettiest but we want to make the `call_arena`
// optionally available to the WebAPIs. If `state` has a
// call_arena field, fill-it in now.
const state_type_info = @typeInfo(@TypeOf(state));
if (state_type_info == .pointer and @hasField(state_type_info.pointer.child, "call_arena")) {
scope.state.call_arena = scope.call_arena;
}
}
// Custom exception // Custom exception
// NOTE: there is no way in v8 to subclass the Error built-in type // NOTE: there is no way in v8 to subclass the Error built-in type
// TODO: this is an horrible hack // TODO: this is an horrible hack
@@ -880,6 +900,21 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
return error.FailedToSet; return error.FailedToSet;
} }
} }
pub fn isTruthy(self: JsObject) bool {
const js_value = self.js_obj.toValue();
return js_value.toBool(self.scope.isolate);
}
pub fn toString(self: JsObject) ![]const u8 {
const scope = self.scope;
const js_value = self.js_obj.toValue();
return valueToString(scope.call_arena, js_value, scope.isolate, scope.context);
}
pub fn format(self: JsObject, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
return writer.writeAll(try self.toString());
}
}; };
// This only exists so that we know whether a function wants the opaque // This only exists so that we know whether a function wants the opaque
@@ -1995,13 +2030,15 @@ fn Caller(comptime E: type) type {
is_variadic = true; is_variadic = true;
if (js_parameter_count == 0) { if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{}; @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else { } else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1); const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| { for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i))); const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try self.jsValueToZig(named_function, slice_type, js_value); a.* = try self.jsValueToZig(named_function, slice_type, js_value);
} }
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr; @field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} }
} }
} }
@@ -2173,10 +2210,6 @@ fn Caller(comptime E: type) type {
}; };
} }
if (!js_value.isObject()) {
return error.InvalidArgument;
}
const js_obj = js_value.castTo(v8.Object); const js_obj = js_value.castTo(v8.Object);
if (comptime isJsObject(T)) { if (comptime isJsObject(T)) {
@@ -2188,6 +2221,10 @@ fn Caller(comptime E: type) type {
}; };
} }
if (!js_value.isObject()) {
return error.InvalidArgument;
}
const context = self.context; const context = self.context;
const isolate = self.isolate; const isolate = self.isolate;