From bd3da38fc8fd0e98bafbebca2af1de8c623b76bd Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 19 Nov 2025 22:29:25 +0800 Subject: [PATCH] add native custom elements --- src/Scheduler.zig | 107 ----------- src/browser/Factory.zig | 6 + src/browser/Page.zig | 69 ++++++- src/browser/js/Caller.zig | 15 +- src/browser/js/Context.zig | 8 +- src/browser/js/Function.zig | 7 + src/browser/js/Object.zig | 5 + src/browser/js/bridge.zig | 1 + src/browser/js/js.zig | 17 ++ src/browser/polyfill/polyfill.zig | 36 +--- src/browser/polyfill/webcomponents.js | 61 ------ src/browser/polyfill/webcomponents.zig | 43 ----- .../custom_elements/attribute_changed.html | 158 ++++++++++++++++ .../tests/custom_elements/built_in.html | 137 ++++++++++++++ .../tests/custom_elements/connected.html | 93 ++++++++++ .../tests/custom_elements/constructor.html | 55 ++++++ .../tests/custom_elements/disconnected.html | 151 +++++++++++++++ .../tests/custom_elements/registry.html | 83 +++++++++ .../tests/custom_elements/upgrade.html | 95 ++++++++++ .../tests/legacy/polyfill/webcomponents.html | 23 --- src/browser/tests/legacy/window/window.html | 22 +-- .../webapi/CustomElementDefinition.zig | 43 +++++ src/browser/webapi/CustomElementRegistry.zig | 174 ++++++++++++++++++ src/browser/webapi/Document.zig | 16 +- src/browser/webapi/Window.zig | 36 +++- src/browser/webapi/element/Html.zig | 9 + src/browser/webapi/element/html/Custom.zig | 111 +++++++++++ src/cdp/cdp.zig | 6 +- src/cdp/domains/page.zig | 4 +- 29 files changed, 1285 insertions(+), 306 deletions(-) delete mode 100644 src/Scheduler.zig delete mode 100644 src/browser/polyfill/webcomponents.js delete mode 100644 src/browser/polyfill/webcomponents.zig create mode 100644 src/browser/tests/custom_elements/attribute_changed.html create mode 100644 src/browser/tests/custom_elements/built_in.html create mode 100644 src/browser/tests/custom_elements/connected.html create mode 100644 src/browser/tests/custom_elements/constructor.html create mode 100644 src/browser/tests/custom_elements/disconnected.html create mode 100644 src/browser/tests/custom_elements/registry.html create mode 100644 src/browser/tests/custom_elements/upgrade.html delete mode 100644 src/browser/tests/legacy/polyfill/webcomponents.html create mode 100644 src/browser/webapi/CustomElementDefinition.zig create mode 100644 src/browser/webapi/CustomElementRegistry.zig diff --git a/src/Scheduler.zig b/src/Scheduler.zig deleted file mode 100644 index 98efb64f..00000000 --- a/src/Scheduler.zig +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); -const log = @import("log.zig"); - -const timestamp = @import("datetime.zig").milliTimestamp; - -const Queue = std.PriorityQueue(Task, void, struct { - fn compare(_: void, a: Task, b: Task) std.math.Order { - return std.math.order(a.run_at, b.run_at); - } -}.compare); - -const Scheduler = @This(); - -low_priority: Queue, -high_priority: Queue, - -pub fn init(allocator: std.mem.Allocator) Scheduler { - return .{ - .low_priority = Queue.init(allocator, {}), - .high_priority = Queue.init(allocator, {}), - }; -} - -pub fn reset(self: *Scheduler) void { - self.low_priority.cap = 0; - self.low_priority.items.len = 0; - - self.high_priority.cap = 0; - self.high_priority.items.len = 0; -} - -const AddOpts = struct { - name: []const u8 = "", - low_priority: bool = false, -}; -pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void { - log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority }); - var queue = if (opts.low_priority) &self.low_priority else &self.high_priority; - return queue.add(.{ - .ctx = ctx, - .callback = cb, - .name = opts.name, - .run_at = timestamp(.monotonic) + run_in_ms, - }); -} - -pub fn run(self: *Scheduler) !?u64 { - _ = try self.runQueue(&self.low_priority); - return self.runQueue(&self.high_priority); -} - -fn runQueue(self: *Scheduler, queue: *Queue) !?u64 { - if (queue.count() == 0) { - return null; - } - - const now = timestamp(.monotonic); - - std.debug.print("running: {s}\n", .{task.name}); - while (queue.peek()) |*task_| { - if (task_.run_at > now) { - return @intCast(task_.run_at - now); - } - var task = queue.remove(); - log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name }); - - const repeat_in_ms = task.callback(task.ctx) catch |err| { - log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err }); - continue; - }; - - if (repeat_in_ms) |ms| { - // Task cannot be repeated immediately, and they should know that - std.debug.assert(ms != 0); - task.run_at = now + ms; - try self.low_priority.add(task); - } - } - return null; -} - -const Task = struct { - run_at: u64, - ctx: *anyopaque, - name: []const u8, - callback: Callback, -}; - -const Callback = *const fn (ctx: *anyopaque) anyerror!?u32; diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index f6bbc5d9..d4f7428b 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -61,6 +61,8 @@ _size_128_8: MemoryPoolAligned([128]u8, .@"8"), _size_144_8: MemoryPoolAligned([144]u8, .@"8"), _size_152_8: MemoryPoolAligned([152]u8, .@"8"), _size_160_8: MemoryPoolAligned([160]u8, .@"8"), +_size_184_8: MemoryPoolAligned([184]u8, .@"8"), +_size_192_8: MemoryPoolAligned([192]u8, .@"8"), _size_648_8: MemoryPoolAligned([648]u8, .@"8"), pub fn init(page: *Page) Factory { @@ -82,6 +84,8 @@ pub fn init(page: *Page) Factory { ._size_144_8 = MemoryPoolAligned([144]u8, .@"8").init(page.arena), ._size_152_8 = MemoryPoolAligned([152]u8, .@"8").init(page.arena), ._size_160_8 = MemoryPoolAligned([160]u8, .@"8").init(page.arena), + ._size_184_8 = MemoryPoolAligned([184]u8, .@"8").init(page.arena), + ._size_192_8 = MemoryPoolAligned([192]u8, .@"8").init(page.arena), ._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena), }; } @@ -235,6 +239,8 @@ pub fn createT(self: *Factory, comptime T: type) !*T { if (comptime SO == 128) return @ptrCast(try self._size_128_8.create()); if (comptime SO == 152) return @ptrCast(try self._size_152_8.create()); if (comptime SO == 160) return @ptrCast(try self._size_160_8.create()); + if (comptime SO == 184) return @ptrCast(try self._size_184_8.create()); + if (comptime SO == 192) return @ptrCast(try self._size_192_8.create()); if (comptime SO == 648) return @ptrCast(try self._size_648_8.create()); @compileError(std.fmt.comptimePrint("No pool configured for @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(T), @typeName(T) })); } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 9a4a9f96..33e1e565 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -54,6 +54,7 @@ const Performance = @import("webapi/Performance.zig"); const HtmlScript = @import("webapi/Element.zig").Html.Script; const MutationObserver = @import("webapi/MutationObserver.zig"); const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); +const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig"); const storage = @import("webapi/storage/storage.zig"); const timestamp = @import("../datetime.zig").timestamp; @@ -101,6 +102,16 @@ _mutation_delivery_scheduled: bool = false, _intersection_observers: std.ArrayList(*IntersectionObserver) = .{}, _intersection_delivery_scheduled: bool = false, +// Lookup for customized built-in elements. Maps element pointer to definition. +_customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{}, + +// This is set when an element is being upgraded (constructor is called). +// The constructor can access this to get the element being upgraded. +_upgrading_element: ?*Node = null, + +// List of custom elements that were created before their definition was registered +_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{}, + _polyfill_loader: polyfill.Loader = .{}, // for heap allocations and managing WebAPI objects @@ -207,8 +218,9 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._mutation_delivery_scheduled = false; self._intersection_observers = .{}; self._intersection_delivery_scheduled = false; + self._customized_builtin_definitions = .{}; + self._undefined_custom_elements = .{}; - try polyfill.preload(self.arena, self.js); try self.registerBackgroundTasks(); } @@ -1121,9 +1133,39 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); } - // If we had a custom element registry, now is when we would look it up - // and, if found, return an Element.Html.Custom const tag_name = try String.init(self.arena, name, .{}); + + // Check if this is a custom element (must have hyphen for HTML namespace) + const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null; + if (has_hyphen and namespace == .html) { + const definition = self.window._custom_elements._definitions.get(name); + const node = try self.createHtmlElementT(Element.Html.Custom, namespace, attribute_iterator, .{ + ._proto = undefined, + ._tag_name = tag_name, + ._definition = definition, + }); + + const def = definition orelse { + const element = node.as(Element); + const custom = element.is(Element.Html.Custom).?; + try self._undefined_custom_elements.append(self.arena, custom); + return node; + }; + + // Save and restore upgrading element to allow nested createElement calls + const prev_upgrading = self._upgrading_element; + self._upgrading_element = node; + defer self._upgrading_element = prev_upgrading; + + var result: JS.Function.Result = undefined; + _ = def.constructor.newInstance(&result) catch |err| { + log.warn(.js, "custom element constructor", .{ .name = name, .err = err }); + return node; + }; + + return node; + } + return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name }); } @@ -1133,6 +1175,9 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac element._namespace = namespace; try self.populateElementAttributes(element, attribute_iterator); + // Check for customized built-in element via "is" attribute + try Element.Html.Custom.checkAndAttachBuiltIn(element, self); + const node = element.asNode(); if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) { @call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| { @@ -1269,13 +1314,15 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts // The child was connected and now it no longer is. We need to "disconnect" // it and all of its descendants. For now "disconnect" just means updating - // document._elements_by_id + // document._elements_by_id and invoking disconnectedCallback for custom elements var elements_by_id = &self.document._elements_by_id; var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{}); while (tw.next()) |el| { if (el.getAttributeSafe("id")) |id| { _ = elements_by_id.remove(id); } + + Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self); } } @@ -1415,6 +1462,8 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod gop.value_ptr.* = el; } } + + Element.Html.Custom.invokeConnectedCallbackOnElement(el, self); } } @@ -1423,6 +1472,8 @@ pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value: log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err }); }; + Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self); + for (self._mutation_observers.items) |observer| { observer.notifyAttributeChange(element, name, old_value, self) catch |err| { log.err(.page, "attributeChange.notifyObserver", .{ .err = err }); @@ -1435,6 +1486,8 @@ pub fn attributeRemove(self: *Page, element: *Element, name: []const u8, old_val log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err }); }; + Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self); + for (self._mutation_observers.items) |observer| { observer.notifyAttributeChange(element, name, old_value, self) catch |err| { log.err(.page, "attributeRemove.notifyObserver", .{ .err = err }); @@ -1446,6 +1499,14 @@ pub fn hasMutationObservers(self: *const Page) bool { return self._mutation_observers.items.len > 0; } +pub fn getCustomizedBuiltInDefinition(self: *Page, element: *Element) ?*CustomElementDefinition { + return self._customized_builtin_definitions.get(element); +} + +pub fn setCustomizedBuiltInDefinition(self: *Page, element: *Element, definition: *CustomElementDefinition) !void { + try self._customized_builtin_definitions.put(self.arena, element, definition); +} + pub fn characterDataChange( self: *Page, target: *Node, diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 0b1b5e4a..edd01665 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -106,13 +106,22 @@ pub fn _constructor(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo) @compileError(@typeName(F) ++ " has a constructor without a return type"); }; - const this = info.getThis(); + const new_this = info.getThis(); + var this = new_this; if (@typeInfo(ReturnType) == .error_union) { const non_error_res = res catch |err| return err; - _ = try self.context.mapZigInstanceToJs(this, non_error_res); + this = (try self.context.mapZigInstanceToJs(this, non_error_res)).castToObject(); } else { - _ = try self.context.mapZigInstanceToJs(this, res); + this = (try self.context.mapZigInstanceToJs(this, res)).castToObject(); } + + // If we got back a different object (existing wrapper), copy the prototype + // from new object. (this happens when we're upgrading an CustomElement) + if (this.handle != new_this.handle) { + const new_prototype = new_this.getPrototype(); + _ = this.setPrototype(self.context.v8_context, new_prototype.castTo(v8.Object)); + } + info.getReturnValue().set(this); } diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index d2d0640e..1eee85c8 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -399,6 +399,13 @@ pub fn createValue(self: *const Context, value: v8.Value) js.Value { }; } +pub fn createObject(self: *Context, js_value: v8.Value) js.Object { + return .{ + .js_obj = js_value.castTo(v8.Object), + .context = self, + }; +} + pub fn createFunction(self: *Context, js_value: v8.Value) !js.Function { // caller should have made sure this was a function std.debug.assert(js_value.isFunction()); @@ -1930,7 +1937,6 @@ pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void { self.isolate.enqueueMicrotaskFunc(cb.func.castToFunction()); } - // == Misc == // An interface for types that want to have their jsDeinit function to be // called when the call context ends diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index abb46a97..73c02911 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -159,3 +159,10 @@ pub fn src(self: *const Function) ![]const u8 { const value = self.func.castToFunction().toValue(); return self.context.valueToString(value, .{}); } + +pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value { + const func_obj = self.func.castToFunction().toObject(); + const key = v8.String.initUtf8(self.context.isolate, name); + const value = func_obj.getValue(self.context.v8_context, key) catch return null; + return self.context.createValue(value); +} diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 823a5ade..53bcafe7 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -118,6 +118,11 @@ pub fn getFunction(self: Object, name: []const u8) !?js.Function { return try context.createFunction(js_value); } +pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T { + const func = try self.getFunction(method_name) orelse return error.MethodNotFound; + return func.callWithThis(T, self, args); +} + pub fn isNull(self: Object) bool { return self.js_obj.toValue().isNull(); } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 1eab16c9..bd75bee8 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -546,4 +546,5 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Performance.zig"), @import("../webapi/MutationObserver.zig"), @import("../webapi/IntersectionObserver.zig"), + @import("../webapi/CustomElementRegistry.zig"), }); diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 77173ce2..2ddc84b8 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -162,6 +162,23 @@ pub const Value = struct { const value = try v8.Json.parse(ctx.v8_context, json_string); return Value{ .context = ctx, .value = value }; } + + pub fn isArray(self: Value) bool { + return self.value.isArray(); + } + + pub fn arrayLength(self: Value) u32 { + std.debug.assert(self.value.isArray()); + return self.value.castTo(v8.Array).length(); + } + + pub fn arrayGet(self: Value, index: u32) !Value { + std.debug.assert(self.value.isArray()); + const array_obj = self.value.castTo(v8.Array).castTo(v8.Object); + const idx_key = v8.Integer.initU32(self.context.isolate, index); + const elem_val = try array_obj.getValue(self.context.v8_context, idx_key.toValue()); + return self.context.createValue(elem_val); + } }; pub const ValueIterator = struct { diff --git a/src/browser/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig index b05bee8b..cfb502a7 100644 --- a/src/browser/polyfill/polyfill.zig +++ b/src/browser/polyfill/polyfill.zig @@ -26,9 +26,7 @@ const Allocator = std.mem.Allocator; pub const Loader = struct { state: enum { empty, loading } = .empty, - done: struct { - webcomponents: bool = false, - } = .{}, + done: struct {} = .{}, fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *js.Context) void { var try_catch: js.TryCatch = undefined; @@ -55,17 +53,6 @@ pub const Loader = struct { return false; } - if (!self.done.webcomponents and isWebcomponents(name)) { - const source = @import("webcomponents.zig").source; - self.load("webcomponents", source, js_context); - // We return false here: We want v8 to continue the calling chain - // to finally find the polyfill we just inserted. If we want to - // return false and stops the call chain, we have to use - // `info.GetReturnValue.Set()` function, or `undefined` will be - // returned immediately. - return false; - } - if (comptime builtin.mode == .Debug) { log.debug(.unknown_prop, "unkown global property", .{ .info = "but the property can exist in pure JS", @@ -76,25 +63,4 @@ pub const Loader = struct { return false; } - - fn isWebcomponents(name: []const u8) bool { - if (std.mem.eql(u8, name, "customElements")) return true; - return false; - } }; - -pub fn preload(allocator: Allocator, js_context: *js.Context) !void { - var try_catch: js.TryCatch = undefined; - try_catch.init(js_context); - defer try_catch.deinit(); - - const name = "webcomponents-pre"; - const source = @import("webcomponents.zig").pre; - _ = js_context.exec(source, name) catch |err| { - if (try try_catch.err(allocator)) |msg| { - defer allocator.free(msg); - log.fatal(.app, "polyfill error", .{ .name = name, .err = msg }); - } - return err; - }; -} diff --git a/src/browser/polyfill/webcomponents.js b/src/browser/polyfill/webcomponents.js deleted file mode 100644 index 66e586ee..00000000 --- a/src/browser/polyfill/webcomponents.js +++ /dev/null @@ -1,61 +0,0 @@ -/** -@license @nocompile -Copyright (c) 2018 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ -(function(){/* - - Copyright (c) 2016 The Polymer Project Authors. All rights reserved. - This code may only be used under the BSD style license found at - http://polymer.github.io/LICENSE.txt The complete set of authors may be found - at http://polymer.github.io/AUTHORS.txt The complete set of contributors may - be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by - Google as part of the polymer project is also subject to an additional IP - rights grant found at http://polymer.github.io/PATENTS.txt -*/ -'use strict';var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype, -"textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.toggleAttribute,E=window.Element.prototype.getAttributeNS,F=window.Element.prototype.setAttributeNS,G=window.Element.prototype.removeAttributeNS,H=window.Element.prototype.insertAdjacentElement,fa=window.Element.prototype.insertAdjacentHTML, -ha=window.Element.prototype.prepend,ia=window.Element.prototype.append,ja=window.Element.prototype.before,ka=window.Element.prototype.after,la=window.Element.prototype.replaceWith,ma=window.Element.prototype.remove,na=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),oa=window.HTMLElement.prototype.insertAdjacentElement,pa=window.HTMLElement.prototype.insertAdjacentHTML;var qa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return qa.add(a)});function ra(a){var b=qa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var sa=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement); -function J(a){var b=a.isConnected;if(void 0!==b)return b;if(sa(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b} -function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null} -function M(a,b,d){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var c=f;b(c);var e=c.localName;if("link"===e&&"import"===c.getAttribute("rel")){f=c.import;void 0===d&&(d=new Set);if(f instanceof Node&&!d.has(f))for(d.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,d);f=L(a,c);continue}else if("template"===e){f=L(a,c);continue}if(c=c.__CE_shadowRoot)for(c=c.firstChild;c;c=c.nextSibling)M(c,b,d)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.m=[];this.g=[];this.j=!1;this.shadyDomFastWalk=b;this.I=!a}function P(a,b,d,f){var c=window.ShadyDOM;if(a.shadyDomFastWalk&&c&&c.inUse){if(b.nodeType===Node.ELEMENT_NODE&&d(b),b.querySelectorAll)for(a=c.nativeMethods.querySelectorAll.call(b,"*"),b=0;b { - \\ const HE = window.HTMLElement; - \\ const b = function() { return HE.prototype.constructor.call(this); } - \\ b.prototype = HE.prototype; - \\ window.HTMLElement = b; - \\ })(); -; - -const testing = @import("../../testing.zig"); -test "Browser: Polyfill.WebComponents" { - // @ZIGDOM - // try testing.htmlRunner("polyfill/webcomponents.html", .{}); -} diff --git a/src/browser/tests/custom_elements/attribute_changed.html b/src/browser/tests/custom_elements/attribute_changed.html new file mode 100644 index 00000000..24a8a48d --- /dev/null +++ b/src/browser/tests/custom_elements/attribute_changed.html @@ -0,0 +1,158 @@ + + + + diff --git a/src/browser/tests/custom_elements/built_in.html b/src/browser/tests/custom_elements/built_in.html new file mode 100644 index 00000000..0b74acef --- /dev/null +++ b/src/browser/tests/custom_elements/built_in.html @@ -0,0 +1,137 @@ + + + + + + + + diff --git a/src/browser/tests/custom_elements/connected.html b/src/browser/tests/custom_elements/connected.html new file mode 100644 index 00000000..c126fab6 --- /dev/null +++ b/src/browser/tests/custom_elements/connected.html @@ -0,0 +1,93 @@ + + + + diff --git a/src/browser/tests/custom_elements/constructor.html b/src/browser/tests/custom_elements/constructor.html new file mode 100644 index 00000000..eba9c1f4 --- /dev/null +++ b/src/browser/tests/custom_elements/constructor.html @@ -0,0 +1,55 @@ + + + diff --git a/src/browser/tests/custom_elements/disconnected.html b/src/browser/tests/custom_elements/disconnected.html new file mode 100644 index 00000000..6ff07f3c --- /dev/null +++ b/src/browser/tests/custom_elements/disconnected.html @@ -0,0 +1,151 @@ + + + + diff --git a/src/browser/tests/custom_elements/registry.html b/src/browser/tests/custom_elements/registry.html new file mode 100644 index 00000000..8ead6ae2 --- /dev/null +++ b/src/browser/tests/custom_elements/registry.html @@ -0,0 +1,83 @@ + + + diff --git a/src/browser/tests/custom_elements/upgrade.html b/src/browser/tests/custom_elements/upgrade.html new file mode 100644 index 00000000..1b917023 --- /dev/null +++ b/src/browser/tests/custom_elements/upgrade.html @@ -0,0 +1,95 @@ + + + + + diff --git a/src/browser/tests/legacy/polyfill/webcomponents.html b/src/browser/tests/legacy/polyfill/webcomponents.html deleted file mode 100644 index 5854bc82..00000000 --- a/src/browser/tests/legacy/polyfill/webcomponents.html +++ /dev/null @@ -1,23 +0,0 @@ - - - -
- - diff --git a/src/browser/tests/legacy/window/window.html b/src/browser/tests/legacy/window/window.html index 2e49edd2..a8b5c041 100644 --- a/src/browser/tests/legacy/window/window.html +++ b/src/browser/tests/legacy/window/window.html @@ -53,7 +53,7 @@ testing.eventually(() => testing.expectEqual(1, wst1)); let wst2 = 1; - window.setTimeout((a, b) => {wst2 = a + b}, 1, 2, 3); + window.setTimeout((a, b) => {wst2 = a+ b}, 1, 2, 3); testing.eventually(() => testing.expectEqual(5, wst2)); @@ -69,7 +69,7 @@ testing.expectEqual(true, called); - + - - + - + - + diff --git a/src/browser/webapi/CustomElementDefinition.zig b/src/browser/webapi/CustomElementDefinition.zig new file mode 100644 index 00000000..458a3319 --- /dev/null +++ b/src/browser/webapi/CustomElementDefinition.zig @@ -0,0 +1,43 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); +const Element = @import("Element.zig"); + +const CustomElementDefinition = @This(); + +name: []const u8, +constructor: js.Function, +observed_attributes: std.StringHashMapUnmanaged(void) = .{}, +// For customized built-in elements, this is the element tag they extend (e.g., .button) +// For autonomous custom elements, this is null +extends: ?Element.Tag = null, + +pub fn isAttributeObserved(self: *const CustomElementDefinition, name: []const u8) bool { + return self.observed_attributes.contains(name); +} + +pub fn isAutonomous(self: *const CustomElementDefinition) bool { + return self.extends == null; +} + +pub fn isCustomizedBuiltIn(self: *const CustomElementDefinition) bool { + return self.extends != null; +} diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig new file mode 100644 index 00000000..62213754 --- /dev/null +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -0,0 +1,174 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const log = @import("../../log.zig"); +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); +const Element = @import("Element.zig"); +const CustomElementDefinition = @import("CustomElementDefinition.zig"); + +const CustomElementRegistry = @This(); + +_definitions: std.StringHashMapUnmanaged(*CustomElementDefinition) = .{}, + +const DefineOptions = struct { + extends: ?[]const u8 = null, +}; + +pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Function, options_: ?DefineOptions, page: *Page) !void { + const options = options_ orelse DefineOptions{}; + + try validateName(name); + + // Parse and validate extends option + const extends_tag: ?Element.Tag = if (options.extends) |extends_name| blk: { + const tag = std.meta.stringToEnum(Element.Tag, extends_name) orelse return error.NotSupported; + + // Can't extend custom elements + if (tag == .custom) { + return error.NotSupported; + } + + break :blk tag; + } else null; + + const gop = try self._definitions.getOrPut(page.arena, name); + if (gop.found_existing) { + return error.AlreadyDefined; + } + + const owned_name = try page.dupeString(name); + + const definition = try page._factory.create(CustomElementDefinition{ + .name = owned_name, + .constructor = constructor, + .extends = extends_tag, + }); + + // Read observedAttributes static property from constructor + if (constructor.getPropertyValue("observedAttributes") catch null) |observed_attrs| { + if (observed_attrs.isArray()) { + const len = observed_attrs.arrayLength(); + var i: u32 = 0; + while (i < len) : (i += 1) { + const attr_val = observed_attrs.arrayGet(i) catch continue; + const attr_name = attr_val.toString(page.arena) catch continue; + const owned_attr = page.dupeString(attr_name) catch continue; + definition.observed_attributes.put(page.arena, owned_attr, {}) catch continue; + } + } + } + + gop.key_ptr.* = owned_name; + gop.value_ptr.* = definition; + + // Upgrade any undefined custom elements with this name + var idx: usize = 0; + while (idx < page._undefined_custom_elements.items.len) { + const custom = page._undefined_custom_elements.items[idx]; + + if (!custom._tag_name.eqlSlice(name)) { + idx += 1; + continue; + } + + custom._definition = definition; + + const node = custom.asNode(); + const prev_upgrading = page._upgrading_element; + page._upgrading_element = node; + defer page._upgrading_element = prev_upgrading; + + var result: js.Function.Result = undefined; + _ = definition.constructor.newInstance(&result) catch |err| { + log.warn(.js, "custom element upgrade", .{ .name = name, .err = err }); + _ = page._undefined_custom_elements.swapRemove(idx); + continue; + }; + + if (node.isConnected()) { + custom.invokeConnectedCallback(page); + } + + _ = page._undefined_custom_elements.swapRemove(idx); + } +} + +pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function { + const definition = self._definitions.get(name) orelse return null; + return definition.constructor; +} + +fn validateName(name: []const u8) !void { + if (name.len == 0) { + return error.InvalidCustomElementName; + } + + if (std.mem.indexOf(u8, name, "-") == null) { + return error.InvalidCustomElementName; + } + + if (name[0] < 'a' or name[0] > 'z') { + return error.InvalidCustomElementName; + } + + const reserved_names = [_][]const u8{ + "annotation-xml", + "color-profile", + "font-face", + "font-face-src", + "font-face-uri", + "font-face-format", + "font-face-name", + "missing-glyph", + }; + + for (reserved_names) |reserved| { + if (std.mem.eql(u8, name, reserved)) { + return error.InvalidCustomElementName; + } + } + + for (name) |c| { + const valid = (c >= 'a' and c <= 'z') or + (c >= '0' and c <= '9') or + c == '-'; + if (!valid) { + return error.InvalidCustomElementName; + } + } +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CustomElementRegistry); + + pub const Meta = struct { + pub const name = "CustomElementRegistry"; + pub var class_id: bridge.ClassId = undefined; + pub const prototype_chain = bridge.prototypeChain(); + }; + + pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true }); + pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true }); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: CustomElementRegistry" { + try testing.htmlRunner("custom_elements", .{}); +} diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 4f04f22f..eaa9b6e0 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -77,9 +77,21 @@ pub fn getURL(_: *const Document, page: *const Page) [:0]const u8 { return page.url; } -pub fn createElement(_: *const Document, name: []const u8, page: *Page) !*Element { +const CreateElementOptions = struct { + is: ?[]const u8 = null, +}; + +pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element { const node = try page.createElement(null, name, null); - return node.as(Element); + const element = node.as(Element); + + const options = options_ orelse return element; + if (options.is) |is_value| { + try element.setAttribute("is", is_value, page); + try Element.Html.Custom.checkAndAttachBuiltIn(element, page); + } + + return element; } pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index bafb12d4..329a8098 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -33,6 +33,7 @@ const EventTarget = @import("EventTarget.zig"); const ErrorEvent = @import("event/ErrorEvent.zig"); const MediaQueryList = @import("css/MediaQueryList.zig"); const storage = @import("storage/storage.zig"); +const CustomElementRegistry = @import("CustomElementRegistry.zig"); const Window = @This(); @@ -47,6 +48,7 @@ _on_load: ?js.Function = null, _location: *Location, _timer_id: u30 = 0, _timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{}, +_custom_elements: CustomElementRegistry = .{}, pub fn asEventTarget(self: *Window) *EventTarget { return self._proto; @@ -92,6 +94,10 @@ pub fn getHistory(self: *Window) *History { return &self._history; } +pub fn getCustomElements(self: *Window) *CustomElementRegistry { + return &self._custom_elements; +} + pub fn getOnLoad(self: *const Window) ?js.Function { return self._on_load; } @@ -228,8 +234,13 @@ fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: Schedul const timer_id = self._timer_id +% 1; self._timer_id = timer_id; - for (opts.params) |*js_obj| { - js_obj.* = try js_obj.persist(); + const params = opts.params; + var persisted_params: []js.Object = &.{}; + if (params.len > 0) { + persisted_params = try page.arena.alloc(js.Object, params.len); + for (params, persisted_params) |a, *ca| { + ca.* = try a.persist(); + } } const gop = try self._timers.getOrPut(page.arena, timer_id); @@ -244,7 +255,7 @@ fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: Schedul .page = page, .name = opts.name, .timer_id = timer_id, - .params = opts.params, + .params = persisted_params, .animation_frame = opts.animation_frame, .repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null, }); @@ -334,6 +345,7 @@ pub const JsApi = struct { pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" }); pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" }); pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" }); + pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" }); pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{}); pub const fetch = bridge.function(Window.fetch, .{}); pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{}); @@ -350,15 +362,21 @@ pub const JsApi = struct { pub const atob = bridge.function(Window.atob, .{}); pub const reportError = bridge.function(Window.reportError, .{}); pub const frames = bridge.accessor(Window.getWindow, null, .{ .cache = "frames" }); - pub const length = bridge.accessor(struct{ - fn wrap(_: *const Window) u32 { return 0; } + pub const length = bridge.accessor(struct { + fn wrap(_: *const Window) u32 { + return 0; + } }.wrap, null, .{ .cache = "length" }); - pub const innerWidth = bridge.accessor(struct{ - fn wrap(_: *const Window) u32 { return 1920; } + pub const innerWidth = bridge.accessor(struct { + fn wrap(_: *const Window) u32 { + return 1920; + } }.wrap, null, .{ .cache = "innerWidth" }); - pub const innerHeight = bridge.accessor(struct{ - fn wrap(_: *const Window) u32 { return 1080; } + pub const innerHeight = bridge.accessor(struct { + fn wrap(_: *const Window) u32 { + return 1080; + } }.wrap, null, .{ .cache = "innerHeight" }); }; diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 854375d5..788e11bd 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -19,6 +19,7 @@ const js = @import("../../js/js.zig"); const reflect = @import("../../reflect.zig"); +const Page = @import("../../Page.zig"); const Node = @import("../Node.zig"); const Element = @import("../Element.zig"); @@ -57,6 +58,12 @@ const HtmlElement = @This(); _type: Type, _proto: *Element, +// Special constructor for custom elements +pub fn construct(page: *Page) !*Element { + const node = page._upgrading_element orelse return error.IllegalConstructor; + return node.is(Element) orelse return error.IllegalConstructor; +} + pub const Type = union(enum) { anchor: Anchor, body: Body, @@ -145,6 +152,8 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const constructor = bridge.constructor(HtmlElement.construct, .{}); }; pub const Build = struct { diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig index 68638924..abf8a341 100644 --- a/src/browser/webapi/element/html/Custom.zig +++ b/src/browser/webapi/element/html/Custom.zig @@ -16,17 +16,22 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const String = @import("../../../../string.zig").String; const js = @import("../../../js/js.zig"); +const log = @import("../../../../log.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); +const CustomElementDefinition = @import("../../CustomElementDefinition.zig"); const Custom = @This(); _proto: *HtmlElement, _tag_name: String, +_definition: ?*CustomElementDefinition, pub fn asElement(self: *Custom) *Element { return self._proto._proto; @@ -35,6 +40,112 @@ pub fn asNode(self: *Custom) *Node { return self.asElement().asNode(); } +pub fn invokeConnectedCallback(self: *Custom, page: *Page) void { + self.invokeCallback("connectedCallback", .{}, page); +} + +pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void { + self.invokeCallback("disconnectedCallback", .{}, page); +} + +pub fn invokeAttributeChangedCallback(self: *Custom, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void { + const definition = self._definition orelse return; + if (!definition.isAttributeObserved(name)) return; + self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value }, page); +} + +// Static helpers that work on any Element (autonomous or customized built-in) +pub fn invokeConnectedCallbackOnElement(element: *Element, page: *Page) void { + // Autonomous custom element + if (element.is(Custom)) |custom| { + custom.invokeConnectedCallback(page); + return; + } + + // Customized built-in element + invokeCallbackOnElement(element, "connectedCallback", .{}, page); +} + +pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void { + // Autonomous custom element + if (element.is(Custom)) |custom| { + custom.invokeDisconnectedCallback(page); + return; + } + + // Customized built-in element + invokeCallbackOnElement(element, "disconnectedCallback", .{}, page); +} + +pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void { + // Autonomous custom element + if (element.is(Custom)) |custom| { + custom.invokeAttributeChangedCallback(name, old_value, new_value, page); + return; + } + + // Customized built-in element - check if attribute is observed + const definition = page.getCustomizedBuiltInDefinition(element) orelse return; + if (!definition.isAttributeObserved(name)) return; + invokeCallbackOnElement(element, "attributeChangedCallback", .{ name, old_value, new_value }, page); +} + +fn invokeCallbackOnElement(element: *Element, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void { + // Check if this element has a customized built-in definition + _ = page.getCustomizedBuiltInDefinition(element) orelse return; + + const context = page.js; + + // Get the JS element object + const js_val = context.zigValueToJs(element, .{}) catch return; + const js_element = context.createObject(js_val); + + // Call the callback method if it exists + js_element.callMethod(void, callback_name, args) catch return; +} + +// Check if element has "is" attribute and attach customized built-in definition +pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void { + const is_value = element.getAttributeSafe("is") orelse return; + + const custom_elements = page.window.getCustomElements(); + const definition = custom_elements._definitions.get(is_value) orelse return; + + const extends_tag = definition.extends orelse return; + if (extends_tag != element.getTag()) { + return; + } + + // Attach the definition + try page.setCustomizedBuiltInDefinition(element, definition); + + // Invoke constructor + const prev_upgrading = page._upgrading_element; + const node = element.asNode(); + page._upgrading_element = node; + defer page._upgrading_element = prev_upgrading; + + var result: js.Function.Result = undefined; + _ = definition.constructor.newInstance(&result) catch |err| { + log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err }); + return; + }; +} + + +fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void { + if (self._definition == null) { + return; + } + + const context = page.js; + + const js_val = context.zigValueToJs(self, .{}) catch return; + const js_element = context.createObject(js_val); + + js_element.callMethod(void, callback_name, args) catch return; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Custom); diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index dc897346..135bd6ad 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -644,7 +644,6 @@ pub fn BrowserContext(comptime CDP_T: type) type { // debugger events - pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void { // onRunMessageLoopOnPause is called when a breakpoint is hit. // Until quit pause, we must continue to run a nested message loop @@ -743,12 +742,9 @@ const IsolatedWorld = struct { ); } - pub fn createContextAndLoadPolyfills(self: *IsolatedWorld, arena: Allocator, page: *Page) !void { + pub fn createContextAndLoadPolyfills(self: *IsolatedWorld, page: *Page) !void { // We need to recreate the isolated world context try self.createContext(page); - - const loader = @import("../browser/polyfill/polyfill.zig"); - try loader.preload(arena, &self.executor.context.?); } }; diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 7107d686..06b9a861 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -121,7 +121,7 @@ fn createIsolatedWorld(cmd: anytype) !void { const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess); const page = bc.session.currentPage() orelse return error.PageNotLoaded; - try world.createContextAndLoadPolyfills(bc.arena, page); + try world.createContextAndLoadPolyfills(page); const js_context = &world.executor.context.?; // Create the auxdata json for the contextCreated event @@ -281,7 +281,7 @@ pub fn pageRemove(bc: anytype) !void { pub fn pageCreated(bc: anytype, page: *Page) !void { for (bc.isolated_worlds.items) |*isolated_world| { - try isolated_world.createContextAndLoadPolyfills(bc.arena, page); + try isolated_world.createContextAndLoadPolyfills(page); } }