Detect Node-types that don't have an asNode()

Don't set null_as_undefined = false..it's the default.
This commit is contained in:
Karl Seguin
2026-07-15 15:30:49 +08:00
parent 945c6d40cb
commit 58f5f857d9
7 changed files with 82 additions and 72 deletions

View File

@@ -510,31 +510,24 @@ fn errorLocal(comptime T: type, local: *const Local, info: anytype) Local {
return local.*;
}
const frame = switch (local.ctx.global) {
.frame => |f| f,
.worker => return local.*,
};
const Node = @import("../webapi/Node.zig");
const Document = @import("../webapi/Document.zig");
const is_node_type = comptime blk: {
if (@typeInfo(T) != .@"struct") break :blk false;
if (T == Node) break :blk true;
break :blk @hasDecl(T, "JsApi") and @hasDecl(T, "asNode") and @TypeOf(T.asNode) == fn (*T) *Node;
if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "JsApi")) break :blk false;
break :blk @import("bridge.zig").inheritsOrIs(T.JsApi, Node.JsApi);
};
if (comptime !is_node_type) {
return local.*;
}
const node: *Node = blk: {
const instance = TaggedOpaque.fromJS(*T, info.getThis()) catch return local.*;
if (comptime T == Node) {
break :blk instance;
} else {
break :blk instance.asNode();
}
};
const frame = switch (local.ctx.global) {
.frame => |f| f,
.worker => return local.*,
};
const instance = TaggedOpaque.fromJS(*T, info.getThis()) catch return local.*;
const node = protoNode(T, instance);
const doc: *Document = node.ownerDocument(frame) orelse switch (node._type) {
.document => |d| d,
@@ -546,7 +539,7 @@ fn errorLocal(comptime T: type, local: *const Local, info: anytype) Local {
}
const ctx = doc_frame.js;
const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&ctx.handle, ctx.isolate.handle));
const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&ctx.handle, ctx.isolate.handle) orelse return local.*);
return .{
.ctx = ctx,
.handle = local_v8_context,
@@ -555,6 +548,17 @@ fn errorLocal(comptime T: type, local: *const Local, info: anytype) Local {
};
}
// Upcast a Node-descendant instance to *Node by walking the _proto chain.
// Not every node type defines an asNode() helper (e.g. Comment, Text), but
// inheritsOrIs guarantees Node is in the chain
fn protoNode(comptime T: type, instance: *T) *@import("../webapi/Node.zig") {
if (T == @import("../webapi/Node.zig")) {
return instance;
}
const Proto = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child;
return protoNode(Proto, instance._proto);
}
fn handleError(comptime T: type, comptime F: type, local: *const Local, err: anyerror, info: anytype) void {
const isolate = local.isolate;
@@ -568,26 +572,32 @@ fn handleError(comptime T: type, comptime F: type, local: *const Local, err: any
}
}
// early exit
switch (err) {
error.TryCatchRethrow => return,
// A JS exception is already pending in the isolate (e.g. a value's
// toString threw during argument conversion); throwing anything here
// would replace the original exception the script expects to see.
error.JsException => return,
else => {},
}
const err_local = errorLocal(T, local, info);
const js_err: *const v8.Value = blk: {
// Error constructors use the isolate's current context: enter the
// receiver's realm so the exception gets its prototypes.
const entered = err_local.handle != local.handle;
const entered = err_local.ctx != local.ctx;
if (entered) v8.v8__Context__Enter(err_local.handle);
defer if (entered) v8.v8__Context__Exit(err_local.handle);
break :blk switch (err) {
error.TryCatchRethrow => return,
// A JS exception is already pending in the isolate (e.g. a value's
// toString threw during argument conversion); throwing anything here
// would replace the original exception the script expects to see.
error.JsException => return,
error.InvalidArgument => isolate.createTypeError("invalid argument"),
error.TypeError => isolate.createTypeError(""),
error.RangeError => isolate.createRangeError(""),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Constructor"),
error.TryCatchRethrow, error.JsException => unreachable, // early exited a few lines up
else => domExceptionToJs(&err_local, err) orelse isolate.createError(@errorName(err)),
};
};

View File

@@ -737,7 +737,7 @@ fn inheritsFromHtmlElement(comptime JsApi: type) bool {
if (JsApi.bridge.type == HtmlElement) {
return false;
}
return inheritsOrIs(JsApi, HtmlElement.JsApi);
return bridge.inheritsOrIs(JsApi, HtmlElement.JsApi);
}
const Unforgeable = struct {
@@ -874,7 +874,7 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
if (comptime flatten == false) {
inline for (unforgeables) |u| {
if (comptime inheritsOrIs(JsApi, u.Owner)) {
if (comptime bridge.inheritsOrIs(JsApi, u.Owner)) {
// unforgeables attributes aren't only applied directly on the
// instance, they're also applied directly on every child instance
attachAccessorProperty(u.name, u.accessor, isolate, template, signature, instance);
@@ -949,23 +949,6 @@ const unforgeables: []const Unforgeable = blk: {
break :blk list;
};
// Whether Child is Ancestor or inherits from it, following the _proto chain.
fn inheritsOrIs(comptime Child: type, comptime Ancestor: type) bool {
comptime {
const target = Ancestor.bridge.type;
var T = Child.bridge.type;
while (true) {
if (T == target) {
return true;
}
if (!@hasField(T, "_proto")) {
return false;
}
T = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child;
}
}
}
// Attach JsApi members to a template (public for reuse). This is called on all
// types. But, for globals (window, WGS) it's called twice. The first time, it's
// called like any other interface. The 2nd time, it's called with flatten == true

View File

@@ -1269,3 +1269,20 @@ pub const JsApis = blk: {
}
break :blk base ++ [_]type{@import("../webapi/WebDriver.zig").JsApi};
};
// Whether Child is Ancestor or inherits from it, following the _proto chain.
pub fn inheritsOrIs(comptime Child: type, comptime Ancestor: type) bool {
comptime {
const target = Ancestor.bridge.type;
var T = Child.bridge.type;
while (true) {
if (T == target) {
return true;
}
if (!@hasField(T, "_proto")) {
return false;
}
T = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child;
}
}
}

View File

@@ -108,24 +108,6 @@ pub fn setOnClick(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame
}
}
// Generates a getter/setter pair backed by the frame's attribute-listener
// map, like onclick above, for other document event handler properties.
fn handlerAccessor(comptime handler: @import("global_event_handlers.zig").Handler) type {
return struct {
pub fn get(self: *Document, frame: *Frame) ?js.Function.Global {
return frame._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = handler });
}
pub fn set(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame) !void {
if (Window.getFunctionFromSetter(setter)) |cb| {
try frame._event_target_attr_listeners.put(frame.arena, .{ .target = self.asEventTarget(), .handler = handler }, cb);
} else {
_ = frame._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = handler });
}
}
};
}
pub const Type = union(enum) {
generic,
html: *HTMLDocument,
@@ -1530,6 +1512,26 @@ pub const JsApi = struct {
return doc_frame.charset;
}
pub const referrer = bridge.property("", .{ .template = false });
// Generates a getter/setter pair backed by the frame's attribute-listener
// map, like onclick above, for other document event handler properties.
fn handlerAccessor(comptime handler: @import("global_event_handlers.zig").Handler) type {
return struct {
pub fn get(self: *Document, frame: *Frame) ?js.Function.Global {
const owner = self._frame orelse frame;
return owner._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = handler });
}
pub fn set(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame) !void {
const owner = self._frame orelse frame;
if (Window.getFunctionFromSetter(setter)) |cb| {
try owner._event_target_attr_listeners.put(owner.arena, .{ .target = self.asEventTarget(), .handler = handler }, cb);
} else {
_ = owner._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = handler });
}
}
};
}
};
const testing = @import("../../testing.zig");

View File

@@ -89,8 +89,8 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(DeviceMotionEvent.init, .{});
pub const acceleration = bridge.accessor(DeviceMotionEvent.getAcceleration, null, .{ .null_as_undefined = false });
pub const accelerationIncludingGravity = bridge.accessor(DeviceMotionEvent.getAccelerationIncludingGravity, null, .{ .null_as_undefined = false });
pub const rotationRate = bridge.accessor(DeviceMotionEvent.getRotationRate, null, .{ .null_as_undefined = false });
pub const acceleration = bridge.accessor(DeviceMotionEvent.getAcceleration, null, .{});
pub const accelerationIncludingGravity = bridge.accessor(DeviceMotionEvent.getAccelerationIncludingGravity, null, .{});
pub const rotationRate = bridge.accessor(DeviceMotionEvent.getRotationRate, null, .{});
pub const interval = bridge.accessor(DeviceMotionEvent.getInterval, null, .{});
};

View File

@@ -97,8 +97,8 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(DeviceOrientationEvent.init, .{});
pub const alpha = bridge.accessor(DeviceOrientationEvent.getAlpha, null, .{ .null_as_undefined = false });
pub const beta = bridge.accessor(DeviceOrientationEvent.getBeta, null, .{ .null_as_undefined = false });
pub const gamma = bridge.accessor(DeviceOrientationEvent.getGamma, null, .{ .null_as_undefined = false });
pub const alpha = bridge.accessor(DeviceOrientationEvent.getAlpha, null, .{});
pub const beta = bridge.accessor(DeviceOrientationEvent.getBeta, null, .{});
pub const gamma = bridge.accessor(DeviceOrientationEvent.getGamma, null, .{});
pub const absolute = bridge.accessor(DeviceOrientationEvent.getAbsolute, null, .{});
};

View File

@@ -111,7 +111,6 @@ pub fn initStorageEvent(
old_value: ?[]const u8,
new_value: ?[]const u8,
url: ?[]const u8,
frame: *Frame,
) !void {
const event = self._proto;
if (event._event_phase != .none) {
@@ -127,7 +126,6 @@ pub fn initStorageEvent(
self._old_value = if (old_value) |v| try arena.dupe(u8, v) else null;
self._new_value = if (new_value) |v| try arena.dupe(u8, v) else null;
self._url = if (url) |u| try arena.dupe(u8, u) else "";
_ = frame;
}
pub const JsApi = struct {
@@ -140,10 +138,10 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(StorageEvent.init, .{});
pub const key = bridge.accessor(StorageEvent.getKey, null, .{ .null_as_undefined = false });
pub const oldValue = bridge.accessor(StorageEvent.getOldValue, null, .{ .null_as_undefined = false });
pub const newValue = bridge.accessor(StorageEvent.getNewValue, null, .{ .null_as_undefined = false });
pub const key = bridge.accessor(StorageEvent.getKey, null, .{});
pub const oldValue = bridge.accessor(StorageEvent.getOldValue, null, .{});
pub const newValue = bridge.accessor(StorageEvent.getNewValue, null, .{});
pub const url = bridge.accessor(StorageEvent.getUrl, null, .{});
pub const storageArea = bridge.accessor(StorageEvent.getStorageArea, null, .{ .null_as_undefined = false });
pub const storageArea = bridge.accessor(StorageEvent.getStorageArea, null, .{});
pub const initStorageEvent = bridge.function(StorageEvent.initStorageEvent, .{});
};