From 0f44c3ec51627fcdb42ac01a01845d3b95e9394f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 24 Sep 2026 11:24:10 +0800 Subject: [PATCH] webapi: Legacy constuctor API We previously supported the legacy constructors via new Image() and new Audio() and https://github.com/lightpanda-io/browser/pull/3620 adds support for new Option(). In most cases, that was good enough, but it wasn't quite up to spec, e.g. new HTMLImageElement() still worked and HtMLImageElement showed up in window. This PR cleans it up by giving each one a distinct constructor for their legacy function and NOT creating the default one bridge.constructor would otherwise create. --- src/browser/js/Snapshot.zig | 60 ++++++++++++++++------ src/browser/tests/element/html/image.html | 20 ++++++++ src/browser/tests/element/html/media.html | 20 ++++++++ src/browser/tests/element/html/option.html | 20 ++++++++ 4 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index c7a33ae95..d7c537473 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -325,22 +325,18 @@ fn createSnapshotContext( const template_index = comptime bridge.JsApiLookup.getId(JsApi); const func = v8.v8__FunctionTemplate__GetFunction(templates[template_index], context); if (@hasDecl(JsApi.Meta, "name")) { + const name = JsApi.Meta.name; + const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); + var maybe_result: v8.MaybeBool = undefined; + // Web IDL: interface objects on the global are non-enumerable. + v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, v8.DontEnum, &maybe_result); + if (@hasDecl(JsApi.Meta, "constructor_alias")) { const alias = JsApi.Meta.constructor_alias; - const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len)); - var maybe_result: v8.MaybeBool = undefined; - v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result); - - const name = JsApi.Meta.name; - const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); - var maybe_result2: v8.MaybeBool = undefined; - v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2); - } else { - const name = JsApi.Meta.name; - const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); - var maybe_result: v8.MaybeBool = undefined; - // Web IDL: interface objects on the global are non-enumerable. - v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, v8.DontEnum, &maybe_result); + const alias_func = generateLegacyFactoryFunction(JsApi, isolate, templates[template_index], func.?, context.?); + const v8_alias_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len)); + var maybe_alias_result: v8.MaybeBool = undefined; + v8.v8__Object__DefineOwnProperty(global_obj, context, v8_alias_name, @ptrCast(alias_func), v8.DontEnum, &maybe_alias_result); } } @@ -720,7 +716,9 @@ fn protoIndexLookup(comptime JsApi: type) ?u16 { // Generate a constructor template for a JsApi type (public for reuse) fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate { const callback, const arity = comptime blk: { - if (@hasDecl(JsApi, "constructor")) { + // The constructor belongs to the legacy factory function (`Image`), + // see generateLegacyFactoryFunction. + if (@hasDecl(JsApi, "constructor") and !@hasDecl(JsApi.Meta, "constructor_alias")) { break :blk .{ JsApi.constructor.func, JsApi.constructor.arity }; } if (inheritsFromHtmlElement(JsApi)) { @@ -748,6 +746,38 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.Fun return template; } +// https://webidl.spec.whatwg.org/#legacy-factory-functions +// `Image`, `Audio`, `Option`: a function distinct from the interface object +// (so `new HTMLImageElement()` stays illegal and `Image.name` is "Image"), +// whose `prototype` is the interface's prototype object. +fn generateLegacyFactoryFunction(comptime JsApi: type, isolate: *v8.Isolate, interface_template: *const v8.FunctionTemplate, interface_func: *const v8.Function, context: *const v8.Context) *const v8.Function { + const alias = JsApi.Meta.constructor_alias; + const template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ + .length = JsApi.constructor.arity, + .callback = JsApi.constructor.func, + .behavior = v8.kConstructorBehavior_Allow, + }).?; + // Inherit so that the objects we construct pass the interface's + // accessor and method signature checks. + v8.v8__FunctionTemplate__Inherit(template, interface_template); + { + const internal_field_count = comptime countInternalFields(JsApi); + if (internal_field_count > 0) { + const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template); + v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count); + } + } + const class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len)); + v8.v8__FunctionTemplate__SetClassName(template, class_name); + + const func = v8.v8__FunctionTemplate__GetFunction(template, context).?; + const prototype_key = v8.v8__String__NewFromUtf8(isolate, "prototype", v8.kNormal, 9); + const interface_prototype = v8.v8__Object__Get(@ptrCast(interface_func), context, prototype_key).?; + var maybe_result: v8.MaybeBool = undefined; + v8.v8__Object__DefineOwnProperty(@ptrCast(func), context, prototype_key, interface_prototype, v8.ReadOnly + v8.DontEnum + v8.DontDelete, &maybe_result); + return func; +} + // hard-coded special case for HtmlElement which can be extended but not // instantiated. fn inheritsFromHtmlElement(comptime JsApi: type) bool { diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html index fcb9455e4..a778285cb 100644 --- a/src/browser/tests/element/html/image.html +++ b/src/browser/tests/element/html/image.html @@ -179,3 +179,23 @@ img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACw='; testing.expectEqual(undefined, await img.decode()); + + diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html index a0e23f437..3d248e075 100644 --- a/src/browser/tests/element/html/media.html +++ b/src/browser/tests/element/html/media.html @@ -308,3 +308,23 @@ testing.expectEqual(true, video.paused); } + + diff --git a/src/browser/tests/element/html/option.html b/src/browser/tests/element/html/option.html index e35cc306e..93bafa0b9 100644 --- a/src/browser/tests/element/html/option.html +++ b/src/browser/tests/element/html/option.html @@ -257,3 +257,23 @@ testing.expectEqual(1, select.selectedIndex) } + +