Add a "pre" polyfill

This is always run, but only the full webcomponents polyfill, it's very
small and isn't intrusive. This introduces a layer of indirection so that,
if the full polyfill is loaded, its monkeypatched constructor will be called
This commit is contained in:
Karl Seguin
2025-07-12 19:49:19 +08:00
parent 818f4540fd
commit 1602932d72
5 changed files with 55 additions and 4 deletions

View File

@@ -120,6 +120,7 @@ pub const Page = struct {
.main_context = undefined, .main_context = undefined,
}; };
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader)); 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 // message loop must run only non-test env
if (comptime !builtin.is_test) { if (comptime !builtin.is_test) {

View File

@@ -71,7 +71,6 @@ pub const Loader = struct {
if (!self.done.webcomponents and isWebcomponents(name)) { if (!self.done.webcomponents and isWebcomponents(name)) {
const source = @import("webcomponents.zig").source; const source = @import("webcomponents.zig").source;
self.load("webcomponents", source, js_context); self.load("webcomponents", source, js_context);
// We return false here: We want v8 to continue the calling chain // We return false here: We want v8 to continue the calling chain
// to finally find the polyfill we just inserted. If we want to // to finally find the polyfill we just inserted. If we want to
// return false and stops the call chain, we have to use // return false and stops the call chain, we have to use
@@ -103,3 +102,19 @@ pub const Loader = struct {
return false; 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;
};
}

View File

@@ -6,21 +6,51 @@
// This is the `webcomponents-ce.js` bundle // This is the `webcomponents-ce.js` bundle
pub const source = @embedFile("webcomponents.js"); 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"); const testing = @import("../../testing.zig");
test "Browser.webcomponents" { test "Browser.webcomponents" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=main></div>" }); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=main></div>" });
defer runner.deinit(); defer runner.deinit();
try @import("polyfill.zig").preload(testing.allocator, runner.page.main_context);
try runner.testCases(&.{ try runner.testCases(&.{
.{ .{
\\ window.customElements; // temporarily needed, lazy loading doesn't work!
\\
\\ class LightPanda extends HTMLElement { \\ class LightPanda extends HTMLElement {
\\ constructor() { \\ constructor() {
\\ super(); \\ super();
\\ } \\ }
\\ connectedCallback() { \\ connectedCallback() {
\\ this.append('connected') \\ this.append('connected');
\\ } \\ }
\\ } \\ }
\\ window.customElements.define("lightpanda-test", LightPanda); \\ window.customElements.define("lightpanda-test", LightPanda);

View File

@@ -284,6 +284,9 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
if (bc.isolated_world) |*isolated_world| { if (bc.isolated_world) |*isolated_world| {
// We need to recreate the isolated world context // We need to recreate the isolated world context
try isolated_world.createContext(page); try isolated_world.createContext(page);
const polyfill = @import("../../browser/polyfill/polyfill.zig");
try polyfill.preload(bc.arena, &isolated_world.executor.js_context.?);
} }
} }

View File

@@ -126,6 +126,8 @@ fn run(
}); });
defer runner.deinit(); defer runner.deinit();
try polyfill.preload(arena, runner.page.main_context);
// loop over the scripts. // loop over the scripts.
const doc = parser.documentHTMLToDocument(runner.page.window.document); const doc = parser.documentHTMLToDocument(runner.page.window.document);
const scripts = try parser.documentGetElementsByTagName(doc, "script"); const scripts = try parser.documentGetElementsByTagName(doc, "script");