diff --git a/src/browser/page.zig b/src/browser/page.zig index 84b08b59..fb64baaa 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -120,6 +120,7 @@ pub const Page = struct { .main_context = undefined, }; self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader)); + try polyfill.preload(self.arena, self.main_context); // message loop must run only non-test env if (comptime !builtin.is_test) { diff --git a/src/browser/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig index 899eaa89..59822c62 100644 --- a/src/browser/polyfill/polyfill.zig +++ b/src/browser/polyfill/polyfill.zig @@ -71,7 +71,6 @@ pub const Loader = struct { 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 @@ -103,3 +102,19 @@ pub const Loader = struct { return false; } }; + +pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void { + var try_catch: Env.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.zig b/src/browser/polyfill/webcomponents.zig index 8e76dff6..e38f63f4 100644 --- a/src/browser/polyfill/webcomponents.zig +++ b/src/browser/polyfill/webcomponents.zig @@ -6,21 +6,51 @@ // This is the `webcomponents-ce.js` bundle pub const source = @embedFile("webcomponents.js"); +// The main webcomponents.js is lazilly loaded when window.customElements is +// called. But, if you look at the test below, you'll notice that we declare +// our custom element (LightPanda) before we call `customElements.define`. We +// _have_ to declare it before we can register it. +// That causes an issue, because the LightPanda class extends HTMLElement, which +// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is +// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement +// constructor does (and that's correct). +// However, once HTMLElement is monkeypatched, it'll work. One simple solution +// is to run the webcomponents.js polyfill proactively on each page, ensuring +// that HTMLElement is monkeypatched before any other JavaScript is run. But +// that adds _a lot_ of overhead. +// So instead of always running the [large and intrusive] webcomponents.js +// polyfill, we'll always run this little snippet. It wraps the HTMLElement +// constructor. When the Lightpanda class is created, it'll extend our little +// wrapper. But, unlike the Zig default constructor which throws, our code +// calls the "real" constructor. That might seem like the same thing, but by the +// time our wrapper is called, the webcomponents.js polyfill will have been +// loaded and the "real" constructor will be the monkeypatched version. +// TL;DR creates a layer of indirection for the constructor, so that, when it's +// actually instantiated, the webcomponents.js polyfill will have been loaded. +pub const pre = + \\ (() => { + \\ 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.webcomponents" { var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "
" }); defer runner.deinit(); + try @import("polyfill.zig").preload(testing.allocator, runner.page.main_context); + try runner.testCases(&.{ .{ - \\ window.customElements; // temporarily needed, lazy loading doesn't work! - \\ \\ class LightPanda extends HTMLElement { \\ constructor() { \\ super(); \\ } \\ connectedCallback() { - \\ this.append('connected') + \\ this.append('connected'); \\ } \\ } \\ window.customElements.define("lightpanda-test", LightPanda); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index f2c7d9d0..368a3b51 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -284,6 +284,9 @@ pub fn pageCreated(bc: anytype, page: *Page) !void { if (bc.isolated_world) |*isolated_world| { // We need to recreate the isolated world context try isolated_world.createContext(page); + + const polyfill = @import("../../browser/polyfill/polyfill.zig"); + try polyfill.preload(bc.arena, &isolated_world.executor.js_context.?); } } diff --git a/src/main_wpt.zig b/src/main_wpt.zig index 94a51132..2e78ae5e 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -126,6 +126,8 @@ fn run( }); defer runner.deinit(); + try polyfill.preload(arena, runner.page.main_context); + // loop over the scripts. const doc = parser.documentHTMLToDocument(runner.page.window.document); const scripts = try parser.documentGetElementsByTagName(doc, "script");