Skip to content

Commit 1602932

Browse files
committed
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
1 parent 818f454 commit 1602932

File tree

5 files changed

+55
-4
lines changed

5 files changed

+55
-4
lines changed

src/browser/page.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ pub const Page = struct {
120120
.main_context = undefined,
121121
};
122122
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
123+
try polyfill.preload(self.arena, self.main_context);
123124

124125
// message loop must run only non-test env
125126
if (comptime !builtin.is_test) {

src/browser/polyfill/polyfill.zig

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ pub const Loader = struct {
7171
if (!self.done.webcomponents and isWebcomponents(name)) {
7272
const source = @import("webcomponents.zig").source;
7373
self.load("webcomponents", source, js_context);
74-
7574
// We return false here: We want v8 to continue the calling chain
7675
// to finally find the polyfill we just inserted. If we want to
7776
// return false and stops the call chain, we have to use
@@ -103,3 +102,19 @@ pub const Loader = struct {
103102
return false;
104103
}
105104
};
105+
106+
pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void {
107+
var try_catch: Env.TryCatch = undefined;
108+
try_catch.init(js_context);
109+
defer try_catch.deinit();
110+
111+
const name = "webcomponents-pre";
112+
const source = @import("webcomponents.zig").pre;
113+
_ = js_context.exec(source, name) catch |err| {
114+
if (try try_catch.err(allocator)) |msg| {
115+
defer allocator.free(msg);
116+
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
117+
}
118+
return err;
119+
};
120+
}

src/browser/polyfill/webcomponents.zig

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,51 @@
66
// This is the `webcomponents-ce.js` bundle
77
pub const source = @embedFile("webcomponents.js");
88

9+
// The main webcomponents.js is lazilly loaded when window.customElements is
10+
// called. But, if you look at the test below, you'll notice that we declare
11+
// our custom element (LightPanda) before we call `customElements.define`. We
12+
// _have_ to declare it before we can register it.
13+
// That causes an issue, because the LightPanda class extends HTMLElement, which
14+
// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is
15+
// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement
16+
// constructor does (and that's correct).
17+
// However, once HTMLElement is monkeypatched, it'll work. One simple solution
18+
// is to run the webcomponents.js polyfill proactively on each page, ensuring
19+
// that HTMLElement is monkeypatched before any other JavaScript is run. But
20+
// that adds _a lot_ of overhead.
21+
// So instead of always running the [large and intrusive] webcomponents.js
22+
// polyfill, we'll always run this little snippet. It wraps the HTMLElement
23+
// constructor. When the Lightpanda class is created, it'll extend our little
24+
// wrapper. But, unlike the Zig default constructor which throws, our code
25+
// calls the "real" constructor. That might seem like the same thing, but by the
26+
// time our wrapper is called, the webcomponents.js polyfill will have been
27+
// loaded and the "real" constructor will be the monkeypatched version.
28+
// TL;DR creates a layer of indirection for the constructor, so that, when it's
29+
// actually instantiated, the webcomponents.js polyfill will have been loaded.
30+
pub const pre =
31+
\\ (() => {
32+
\\ const HE = window.HTMLElement;
33+
\\ const b = function() { return HE.prototype.constructor.call(this); }
34+
\\ b.prototype = HE.prototype;
35+
\\ window.HTMLElement = b;
36+
\\ })();
37+
;
38+
939
const testing = @import("../../testing.zig");
1040
test "Browser.webcomponents" {
1141
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=main></div>" });
1242
defer runner.deinit();
1343

44+
try @import("polyfill.zig").preload(testing.allocator, runner.page.main_context);
45+
1446
try runner.testCases(&.{
1547
.{
16-
\\ window.customElements; // temporarily needed, lazy loading doesn't work!
17-
\\
1848
\\ class LightPanda extends HTMLElement {
1949
\\ constructor() {
2050
\\ super();
2151
\\ }
2252
\\ connectedCallback() {
23-
\\ this.append('connected')
53+
\\ this.append('connected');
2454
\\ }
2555
\\ }
2656
\\ window.customElements.define("lightpanda-test", LightPanda);

src/cdp/domains/page.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
284284
if (bc.isolated_world) |*isolated_world| {
285285
// We need to recreate the isolated world context
286286
try isolated_world.createContext(page);
287+
288+
const polyfill = @import("../../browser/polyfill/polyfill.zig");
289+
try polyfill.preload(bc.arena, &isolated_world.executor.js_context.?);
287290
}
288291
}
289292

src/main_wpt.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ fn run(
126126
});
127127
defer runner.deinit();
128128

129+
try polyfill.preload(arena, runner.page.main_context);
130+
129131
// loop over the scripts.
130132
const doc = parser.documentHTMLToDocument(runner.page.window.document);
131133
const scripts = try parser.documentGetElementsByTagName(doc, "script");

0 commit comments

Comments
 (0)