Merge pull request #3621 from lightpanda-io/legacy-constructors

webapi: Legacy constuctor API
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-09-25 06:13:23 +08:00
commit 2bd46136dc
4 files changed
+105 -15

No files matched your search

+45 -15
View File
@@ -326,22 +326,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);
}
}
@@ -721,7 +717,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)) {
@@ -749,6 +747,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>