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.
This commit is contained in:
Karl Seguin committed 2026-09-24 11:24:10 +08:00
1 parent b09049cbf3
commit 0f44c3ec51
4 files changed
+105 -15

No files matched your search

+45 -15
View File
@@ -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 {
+20
View File
@@ -179,3 +179,23 @@
img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACw=';
testing.expectEqual(undefined, await img.decode());
</script>
<script id="legacy_factory_function">
// https://webidl.spec.whatwg.org/#legacy-factory-functions
testing.expectEqual(true, Image !== HTMLImageElement)
testing.expectEqual('Image', Image.name)
testing.expectEqual(0, Image.length)
testing.expectEqual(HTMLImageElement.prototype, Image.prototype)
testing.expectEqual(HTMLImageElement, new Image().constructor)
testing.expectEqual('img', new Image().localName)
testing.expectError('TypeError', () => new HTMLImageElement())
testing.expectError('TypeError', () => Image())
const desc = Object.getOwnPropertyDescriptor(Image, 'prototype')
testing.expectEqual(false, desc.writable)
testing.expectEqual(false, desc.enumerable)
testing.expectEqual(false, desc.configurable)
testing.expectEqual(false, Object.keys(window).includes('Image'))
testing.expectEqual(false, Object.keys(window).includes('HTMLImageElement'))
</script>
+20
View File
@@ -308,3 +308,23 @@
testing.expectEqual(true, video.paused);
}
</script>
<script id="legacy_factory_function">
// https://webidl.spec.whatwg.org/#legacy-factory-functions
testing.expectEqual(true, Audio !== HTMLAudioElement)
testing.expectEqual('Audio', Audio.name)
testing.expectEqual(0, Audio.length)
testing.expectEqual(HTMLAudioElement.prototype, Audio.prototype)
testing.expectEqual(HTMLAudioElement, new Audio().constructor)
testing.expectEqual('audio', new Audio().localName)
testing.expectError('TypeError', () => new HTMLAudioElement())
testing.expectError('TypeError', () => Audio())
const desc = Object.getOwnPropertyDescriptor(Audio, 'prototype')
testing.expectEqual(false, desc.writable)
testing.expectEqual(false, desc.enumerable)
testing.expectEqual(false, desc.configurable)
testing.expectEqual(false, Object.keys(window).includes('Audio'))
testing.expectEqual(false, Object.keys(window).includes('HTMLAudioElement'))
</script>
@@ -257,3 +257,23 @@
testing.expectEqual(1, select.selectedIndex)
}
</script>
<script id="legacy_factory_function">
// https://webidl.spec.whatwg.org/#legacy-factory-functions
testing.expectEqual(true, Option !== HTMLOptionElement)
testing.expectEqual('Option', Option.name)
testing.expectEqual(0, Option.length)
testing.expectEqual(HTMLOptionElement.prototype, Option.prototype)
testing.expectEqual(HTMLOptionElement, new Option().constructor)
testing.expectEqual('option', new Option().localName)
testing.expectError('TypeError', () => new HTMLOptionElement())
testing.expectError('TypeError', () => Option())
const desc = Object.getOwnPropertyDescriptor(Option, 'prototype')
testing.expectEqual(false, desc.writable)
testing.expectEqual(false, desc.enumerable)
testing.expectEqual(false, desc.configurable)
testing.expectEqual(false, Object.keys(window).includes('Option'))
testing.expectEqual(false, Object.keys(window).includes('HTMLOptionElement'))
</script>