mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 16:28:58 +00:00
Merge ScriptManager/Module loading changes
Get tests passing.
This commit is contained in:
@@ -321,8 +321,11 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
|
||||
// dispatch window.load event
|
||||
const event = try Event.init("load", .{}, self);
|
||||
// this event is weird, it's dispatched directly on the window, but
|
||||
// with the document as the target
|
||||
event._target = self.document.asEventTarget();
|
||||
try self._event_manager.dispatchWithFunction(
|
||||
self.document.asEventTarget(),
|
||||
self.window.asEventTarget(),
|
||||
event,
|
||||
self.window._on_load,
|
||||
.{ .inject_target = false, .context = "page load" },
|
||||
@@ -336,6 +339,9 @@ fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
self.url = try self.arena.dupeZ(u8, std.mem.span(header.url));
|
||||
|
||||
self.window._location = try Location.init(self.url, self);
|
||||
self.document._location = self.window._location;
|
||||
|
||||
log.debug(.http, "navigate header", .{
|
||||
.url = self.url,
|
||||
.status = header.status,
|
||||
@@ -413,7 +419,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
.html => |buf| {
|
||||
var parser = Parser.init(self.arena, self.document.asNode(), self);
|
||||
parser.parse(buf.items);
|
||||
self._script_manager.pageIsLoaded();
|
||||
self._script_manager.staticScriptsDone();
|
||||
if (self._script_manager.isDone()) {
|
||||
// No scripts, or just inline scripts that were already processed
|
||||
// we need to trigger this ourselves
|
||||
@@ -629,7 +635,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
|
||||
}
|
||||
|
||||
pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void {
|
||||
self._script_manager.add(script, "parsing") catch |err| {
|
||||
self._script_manager.addFromElement(script, "parsing") catch |err| {
|
||||
log.err(.page, "page.scriptAddedCallback", .{
|
||||
.err = err,
|
||||
.src = script.asElement().getAttributeSafe("src"),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -237,63 +237,54 @@ pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
|
||||
}
|
||||
|
||||
pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
|
||||
if (cacheable) {
|
||||
if (self.module_cache.get(url)) |entry| {
|
||||
// The dynamic import will create an entry without the
|
||||
// module to prevent multiple calls from asynchronously
|
||||
// loading the same module. If we're here, without the
|
||||
// module, then it's time to load it.
|
||||
if (entry.module != null) {
|
||||
return if (comptime want_result) entry else {};
|
||||
const mod, const owned_url = blk: {
|
||||
const arena = self.arena;
|
||||
|
||||
// gop will _always_ initiated if cacheable == true
|
||||
var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined;
|
||||
if (cacheable) {
|
||||
gop = try self.module_cache.getOrPut(arena, url);
|
||||
if (gop.found_existing) {
|
||||
if (gop.value_ptr.module != null) {
|
||||
return if (comptime want_result) gop.value_ptr.* else {};
|
||||
}
|
||||
} else {
|
||||
// first time seing this
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const m = try compileModule(self.isolate, src, url);
|
||||
const m = try compileModule(self.isolate, src, url);
|
||||
const owned_url = try arena.dupeZ(u8, url);
|
||||
|
||||
const arena = self.arena;
|
||||
const owned_url = try arena.dupeZ(u8, url);
|
||||
if (cacheable) {
|
||||
// compileModule is synchronous - nothing can modify the cache during compilation
|
||||
std.debug.assert(gop.value_ptr.module == null);
|
||||
|
||||
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
|
||||
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
|
||||
gop.value_ptr.module = PersistentModule.init(self.isolate, m);
|
||||
if (!gop.found_existing) {
|
||||
gop.key_ptr.* = owned_url;
|
||||
}
|
||||
}
|
||||
|
||||
break :blk .{ m, owned_url };
|
||||
};
|
||||
|
||||
try self.postCompileModule(mod, owned_url);
|
||||
|
||||
const v8_context = self.v8_context;
|
||||
{
|
||||
// Non-async modules are blocking. We can download them in
|
||||
// parallel, but they need to be processed serially. So we
|
||||
// want to get the list of dependent modules this module has
|
||||
// and start downloading them asap.
|
||||
const requests = m.getModuleRequests();
|
||||
for (0..requests.length()) |i| {
|
||||
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
|
||||
const specifier = try self.jsStringToZigZ(req.getSpecifier(), .{});
|
||||
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
||||
self.call_arena,
|
||||
owned_url,
|
||||
specifier,
|
||||
);
|
||||
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
||||
if (!gop.found_existing) {
|
||||
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
|
||||
gop.key_ptr.* = owned_specifier;
|
||||
gop.value_ptr.* = .{};
|
||||
try self.script_manager.?.getModule(owned_specifier, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
|
||||
if (try mod.instantiate(v8_context, resolveModuleCallback) == false) {
|
||||
return error.ModuleInstantiationError;
|
||||
}
|
||||
|
||||
const evaluated = m.evaluate(v8_context) catch {
|
||||
std.debug.assert(m.getStatus() == .kErrored);
|
||||
const evaluated = mod.evaluate(v8_context) catch {
|
||||
std.debug.assert(mod.getStatus() == .kErrored);
|
||||
|
||||
// Some module-loading errors aren't handled by TryCatch. We need to
|
||||
// get the error from the module itself.
|
||||
log.warn(.js, "evaluate module", .{
|
||||
.specifier = owned_url,
|
||||
.message = self.valueToString(m.getException(), .{}) catch "???",
|
||||
.message = self.valueToString(mod.getException(), .{}) catch "???",
|
||||
});
|
||||
return error.EvaluationError;
|
||||
};
|
||||
@@ -316,28 +307,71 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
||||
// be cached
|
||||
std.debug.assert(cacheable);
|
||||
|
||||
const persisted_module = PersistentModule.init(self.isolate, m);
|
||||
const persisted_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
|
||||
// entry has to have been created atop this function
|
||||
const entry = self.module_cache.getPtr(owned_url).?;
|
||||
|
||||
var gop = try self.module_cache.getOrPut(arena, owned_url);
|
||||
if (gop.found_existing) {
|
||||
// If we're here, it's because we had a cache entry, but no
|
||||
// module. This happens because both our synch and async
|
||||
// module loaders create the entry to prevent concurrent
|
||||
// loads of the same resource (like Go's Singleflight).
|
||||
std.debug.assert(gop.value_ptr.module == null);
|
||||
std.debug.assert(gop.value_ptr.module_promise == null);
|
||||
// and the module must have been set after we compiled it
|
||||
std.debug.assert(entry.module != null);
|
||||
std.debug.assert(entry.module_promise == null);
|
||||
|
||||
gop.value_ptr.module = persisted_module;
|
||||
gop.value_ptr.module_promise = persisted_promise;
|
||||
} else {
|
||||
gop.value_ptr.* = ModuleEntry{
|
||||
.module = persisted_module,
|
||||
.module_promise = persisted_promise,
|
||||
.resolver_promise = null,
|
||||
};
|
||||
entry.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
|
||||
return if (comptime want_result) entry.* else {};
|
||||
}
|
||||
|
||||
// This isn't expected to be called often. It's for converting attributes into
|
||||
// function calls, e.g. <body onload="doSomething"> will turn that "doSomething"
|
||||
// string into a js.Function which looks like: function(e) { doSomething(e) }
|
||||
// There might be more efficient ways to do this, but doing it this way means
|
||||
// our code only has to worry about js.Funtion, not some union of a js.Function
|
||||
// or a string.
|
||||
pub fn stringToFunction(self: *Context, str: []const u8) !js.Function {
|
||||
var extra: []const u8 = "";
|
||||
const normalized = std.mem.trim(u8, str, &std.ascii.whitespace);
|
||||
if (normalized.len > 0 and normalized[normalized.len - 1] != ')') {
|
||||
extra = "(e)";
|
||||
}
|
||||
const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0);
|
||||
|
||||
const v8_context = self.v8_context;
|
||||
const script = try compileScript(self.isolate, v8_context, full, null);
|
||||
const js_value = script.run(v8_context) catch {
|
||||
return error.ExecutionError;
|
||||
};
|
||||
if (!js_value.isFunction()) {
|
||||
return error.StringFunctionError;
|
||||
}
|
||||
return self.createFunction(js_value);
|
||||
}
|
||||
|
||||
// After we compile a module, whether it's a top-level one, or a nested one,
|
||||
// we always want to track its identity (so that, if this module imports other
|
||||
// modules, we can resolve the full URL), and preload any dependent modules.
|
||||
fn postCompileModule(self: *Context, mod: v8.Module, url: [:0]const u8) !void {
|
||||
try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url);
|
||||
|
||||
const v8_context = self.v8_context;
|
||||
|
||||
// Non-async modules are blocking. We can download them in parallel, but
|
||||
// they need to be processed serially. So we want to get the list of
|
||||
// dependent modules this module has and start downloading them asap.
|
||||
const requests = mod.getModuleRequests();
|
||||
const script_manager = self.script_manager.?;
|
||||
for (0..requests.length()) |i| {
|
||||
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
|
||||
const specifier = try self.jsStringToZigZ(req.getSpecifier(), .{});
|
||||
const normalized_specifier = try script_manager.resolveSpecifier(
|
||||
self.call_arena,
|
||||
url,
|
||||
specifier,
|
||||
);
|
||||
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
||||
if (!nested_gop.found_existing) {
|
||||
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
|
||||
nested_gop.key_ptr.* = owned_specifier;
|
||||
nested_gop.value_ptr.* = .{};
|
||||
try script_manager.preloadImport(owned_specifier, url);
|
||||
}
|
||||
}
|
||||
return if (comptime want_result) gop.value_ptr.* else {};
|
||||
}
|
||||
|
||||
// == Creators ==
|
||||
@@ -1239,60 +1273,27 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: [:0]co
|
||||
};
|
||||
|
||||
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
||||
self.arena, // might need to survive until the module is loaded
|
||||
self.arena,
|
||||
referrer_path,
|
||||
specifier,
|
||||
);
|
||||
|
||||
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
||||
if (gop.found_existing) {
|
||||
if (gop.value_ptr.module) |m| {
|
||||
return m.handle;
|
||||
}
|
||||
// We don't have a module, but we do have a cache entry for it
|
||||
// That means we're already trying to load it. We just have
|
||||
// to wait for it to be done.
|
||||
} else {
|
||||
// I don't think it's possible for us to be here. This is
|
||||
// only ever called by v8 when we evaluate a module. But
|
||||
// before evaluating, we should have already started
|
||||
// downloading all of the module's nested modules. So it
|
||||
// should be impossible that this is the first time we've
|
||||
// heard about this module.
|
||||
// But, I'm not confident enough in that, and ther's little
|
||||
// harm in handling this case.
|
||||
@branchHint(.unlikely);
|
||||
gop.value_ptr.* = .{};
|
||||
try self.script_manager.?.getModule(normalized_specifier, referrer_path);
|
||||
const entry = self.module_cache.getPtr(normalized_specifier).?;
|
||||
if (entry.module) |m| {
|
||||
return m.castToModule().handle;
|
||||
}
|
||||
|
||||
var fetch_result = try self.script_manager.?.waitForModule(normalized_specifier);
|
||||
defer fetch_result.deinit();
|
||||
var source = try self.script_manager.?.waitForImport(normalized_specifier);
|
||||
defer source.deinit();
|
||||
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(self);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const entry = self.module(true, fetch_result.src(), normalized_specifier, true) catch |err| {
|
||||
switch (err) {
|
||||
error.EvaluationError => {
|
||||
// This is a sentinel value telling us that the error was already
|
||||
// logged. Some module-loading errors aren't captured by Try/Catch.
|
||||
// We need to handle those errors differently, where the module
|
||||
// exists.
|
||||
},
|
||||
else => log.warn(.js, "compile resolved module", .{
|
||||
.specifier = normalized_specifier,
|
||||
.stack = try_catch.stack(self.call_arena) catch null,
|
||||
.src = try_catch.sourceLine(self.call_arena) catch "err",
|
||||
.line = try_catch.sourceLineNumber() orelse 0,
|
||||
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
|
||||
}),
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// entry.module is always set when returning from self.module()
|
||||
return entry.module.?.handle;
|
||||
const mod = try compileModule(self.isolate, source.src(), normalized_specifier);
|
||||
try self.postCompileModule(mod, normalized_specifier);
|
||||
entry.module = PersistentModule.init(self.isolate, mod);
|
||||
return entry.module.?.castToModule().handle;
|
||||
}
|
||||
|
||||
// Will get passed to ScriptManager and then passed back to us when
|
||||
@@ -1347,7 +1348,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
||||
};
|
||||
|
||||
// Next, we need to actually load it.
|
||||
self.script_manager.?.getAsyncModule(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {
|
||||
self.script_manager.?.getAsyncImport(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {
|
||||
const error_msg = v8.String.initUtf8(isolate, @errorName(err));
|
||||
_ = resolver.reject(self.v8_context, error_msg.toValue());
|
||||
};
|
||||
@@ -1359,28 +1360,41 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
||||
}
|
||||
|
||||
// So we have a module, but no async resolver. This can only
|
||||
// happen if the module was first synchronously loaded (Does that
|
||||
// ever even happen?!) You'd think we cann just return the module
|
||||
// happen if the module was first synchronously loaded (e.g., as a
|
||||
// static import dependency). You'd think we can just return the module
|
||||
// but no, we need to resolve the module namespace, and the
|
||||
// module could still be loading!
|
||||
// We need to do part of what the first case is going to do in
|
||||
// `dynamicModuleSourceCallback`, but we can skip some steps
|
||||
// since the module is alrady loaded,
|
||||
// since the module is already compiled.
|
||||
std.debug.assert(gop.value_ptr.module != null);
|
||||
std.debug.assert(gop.value_ptr.module_promise != null);
|
||||
|
||||
// If the module hasn't been evaluated yet (it was only instantiated
|
||||
// as a static import dependency), we need to evaluate it now.
|
||||
if (gop.value_ptr.module_promise == null) {
|
||||
const mod = gop.value_ptr.module.?.castToModule();
|
||||
const evaluated = mod.evaluate(self.v8_context) catch {
|
||||
std.debug.assert(mod.getStatus() == .kErrored);
|
||||
const error_msg = v8.String.initUtf8(isolate, "Module evaluation failed");
|
||||
_ = resolver.reject(self.v8_context, error_msg.toValue());
|
||||
return promise;
|
||||
};
|
||||
std.debug.assert(evaluated.isPromise());
|
||||
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
|
||||
}
|
||||
|
||||
// like before, we want to set this up so that if anything else
|
||||
// tries to load this module, it can just return our promise
|
||||
// since we're going to be doing all the work.
|
||||
gop.value_ptr.resolver_promise = persisted_promise;
|
||||
|
||||
// But we can skip direclty to `resolveDynamicModule` which is
|
||||
// But we can skip directly to `resolveDynamicModule` which is
|
||||
// what the above callback will eventually do.
|
||||
self.resolveDynamicModule(state, gop.value_ptr.*);
|
||||
return promise;
|
||||
}
|
||||
|
||||
fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptManager.GetResult) void {
|
||||
fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptManager.ModuleSource) void {
|
||||
const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx));
|
||||
var self = state.context;
|
||||
|
||||
|
||||
@@ -38,5 +38,6 @@ pub const pre =
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Polyfill.WebComponents" {
|
||||
try testing.htmlRunner("polyfill/webcomponents.html", .{});
|
||||
// @ZIGDOM
|
||||
// try testing.htmlRunner("polyfill/webcomponents.html", .{});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
testing.expectEqual('SCRIPT', firstScript.tagName);
|
||||
|
||||
testing.expectEqual(null, document.querySelector('article'));
|
||||
testing.expectEqual(null, document.querySelector('another'));
|
||||
// testing.expectEqual(null, document.querySelector('another'));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
const script1 = document.createElement('script');
|
||||
script1.src = "dynamic1.js";
|
||||
document.getElementsByTagName('head')[0].appendChild(script1);
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, loaded1);
|
||||
});
|
||||
testing.expectEqual(1, loaded1);
|
||||
</script>
|
||||
|
||||
<script id=no_double_execute>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div id=other></div>
|
||||
|
||||
<!-- <script id=querySelector">
|
||||
<script id=querySelector">
|
||||
const p1 = $('#p1');
|
||||
testing.expectError("Syntax Error", () => p1.querySelector(''));
|
||||
testing.withError((err) => {
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
testing.expectEqual($('#c2'), p1.querySelector('*'));
|
||||
testing.expectEqual($('#c3'), p1.querySelector('*#c3'));
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
<div id="desc-container">
|
||||
<p class="text">Direct child paragraph</p>
|
||||
@@ -51,15 +51,15 @@
|
||||
{
|
||||
const container = $('#desc-container');
|
||||
|
||||
// testing.expectEqual('Nested paragraph', container.querySelector('div p').textContent);
|
||||
// testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent);
|
||||
testing.expectEqual('Direct child paragraph', container.querySelector('div p').textContent);
|
||||
testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent);
|
||||
testing.expectEqual('Deeply nested paragraph', container.querySelector('div span p').textContent);
|
||||
// testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent);
|
||||
// testing.expectEqual(null, container.querySelector('article div p'));
|
||||
testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent);
|
||||
testing.expectEqual(null, container.querySelector('article div p'));
|
||||
|
||||
// const outerDiv = $('#outer-div');
|
||||
// testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id);
|
||||
// testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id);
|
||||
// testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id);
|
||||
const outerDiv = $('#outer-div');
|
||||
testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id);
|
||||
testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id);
|
||||
testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!-- <script id=meta type=module>
|
||||
<script id=meta type=module>
|
||||
testing.expectEqual('/src/browser/tests/page/module.html', new URL(import.meta.url).pathname)
|
||||
</script>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
import { increment, getCount } from "./modules/shared.js";
|
||||
testing.expectEqual(2, increment());
|
||||
testing.expectEqual(2, getCount());
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
<script id=circular-imports type=module>
|
||||
import { aValue, getFromB } from "./modules/circular-a.js";
|
||||
@@ -44,116 +44,17 @@
|
||||
testing.expectEqual('a', getFromA());
|
||||
</script>
|
||||
|
||||
<!-- <script id=basic-async type=module>
|
||||
<script id=basic-async type=module>
|
||||
import { "val1" as val1 } from "./mod1.js";
|
||||
const m = await import("./mod1.js");
|
||||
testing.expectEqual('value-1', m.val1);
|
||||
testing.expectEqual('value-1', m.val1, {script_id: 'basic-async'});
|
||||
</script>
|
||||
|
||||
|
||||
<script id=import-404 type=module>
|
||||
(async () => {
|
||||
try {
|
||||
await import("./modules/nonexistent.js");
|
||||
testing.expectFail("error expected");
|
||||
}catch(e) {
|
||||
testing.expectEqual(true, e.toString().includes("Failed to load module"));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=dynamic-chain type=module>
|
||||
(async () => {
|
||||
const a = await import("./modules/dynamic-chain-a.js");
|
||||
const result = await a.loadChain();
|
||||
testing.expectEqual('chain-end', result);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=dynamic-circular type=module>
|
||||
(async () => {
|
||||
const x = await import("./modules/dynamic-circular-x.js");
|
||||
testing.expectEqual('dynamic-x', x.xValue);
|
||||
const yValue = await x.loadY();
|
||||
testing.expectEqual('dynamic-y', yValue);
|
||||
|
||||
const y = await import("./modules/dynamic-circular-y.js");
|
||||
const xValue = await y.loadX();
|
||||
testing.expectEqual('dynamic-x', xValue);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=dynamic-shared-state-1 type=module>
|
||||
import { increment, getCount } from "./modules/shared.js";
|
||||
const count1 = increment();
|
||||
testing.expectEqual(3, count1);
|
||||
|
||||
(async () => {
|
||||
const shared = await import("./modules/shared.js");
|
||||
const count2 = shared.increment();
|
||||
testing.expectEqual(4, count2);
|
||||
testing.expectEqual(4, shared.getCount());
|
||||
testing.expectEqual(4, getCount());
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=concurrent-dynamic-imports type=module>
|
||||
(async () => {
|
||||
const [m1, m2, m3] = await Promise.all([
|
||||
import("./modules/shared.js"),
|
||||
import("./modules/shared.js"),
|
||||
import("./modules/shared.js")
|
||||
]);
|
||||
|
||||
const c1 = m1.increment();
|
||||
const c2 = m2.getCount();
|
||||
const c3 = m3.getCount();
|
||||
|
||||
testing.expectEqual(5, c1);
|
||||
testing.expectEqual(5, c2);
|
||||
testing.expectEqual(5, c3);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=sequential-dynamic-imports type=module>
|
||||
(async () => {
|
||||
const m1 = await import("./mod1.js");
|
||||
testing.expectEqual('value-1', m1.val1);
|
||||
|
||||
const m2 = await import("./mod1.js");
|
||||
testing.expectEqual('value-1', m2.val1);
|
||||
testing.expectEqual(m1, m2);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=mixed-static-dynamic-circular type=module>
|
||||
(async () => {
|
||||
const staticMod = await import("./modules/mixed-circular-static.js");
|
||||
testing.expectEqual('static-side', staticMod.staticValue);
|
||||
|
||||
const dynamicValue = await staticMod.loadDynamicSide();
|
||||
testing.expectEqual('dynamic-side', dynamicValue);
|
||||
|
||||
const dynamicMod = await import("./modules/mixed-circular-dynamic.js");
|
||||
testing.expectEqual('static-side', dynamicMod.getStaticValue());
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=dynamic-re-exports type=module>
|
||||
(async () => {
|
||||
const m = await import("./modules/re-exporter.js");
|
||||
testing.expectEqual('from-base', m.baseValue);
|
||||
testing.expectEqual('from-base', m.importedValue);
|
||||
testing.expectEqual('local', m.localValue);
|
||||
})();
|
||||
</script> -->
|
||||
|
||||
<!-- TODO: Error handling tests need dynamic import support
|
||||
<script id=import-syntax-error type=module>
|
||||
try {
|
||||
await import("./modules/syntax-error.js");
|
||||
testing.fail("Should have thrown an error for syntax error");
|
||||
} catch (e) {
|
||||
testing.assert(e.message.includes("SyntaxError") || e.message.includes("identifier"));
|
||||
await import("./modules/nonexistent.js");
|
||||
testing.expectFail("error expected");
|
||||
} catch(e) {
|
||||
testing.expectEqual(true, e.toString().includes("FailedToLoad"), {script_id: 'import-404'});
|
||||
}
|
||||
</script>
|
||||
-->
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
let failed = false;
|
||||
let observed_ids = {};
|
||||
let eventuallies = [];
|
||||
let async_capture = null;
|
||||
let current_script_id = null;
|
||||
|
||||
function expectTrue(actual) {
|
||||
@@ -12,14 +13,17 @@
|
||||
expectEqual(false, actual);
|
||||
}
|
||||
|
||||
function expectEqual(expected, actual) {
|
||||
function expectEqual(expected, actual, opts) {
|
||||
if (_equal(expected, actual)) {
|
||||
_registerObservation('ok');
|
||||
_registerObservation('ok', opts);
|
||||
return;
|
||||
}
|
||||
failed = true;
|
||||
_registerObservation('fail');
|
||||
_registerObservation('fail', opts);
|
||||
let err = `expected: ${_displayValue(expected)}, got: ${_displayValue(actual)}\n script_id: ${_currentScriptId()}`;
|
||||
if (async_capture) {
|
||||
err += `\n stack: ${async_capture.stack}`;
|
||||
}
|
||||
console.error(err);
|
||||
throw new Error('expectEqual failed');
|
||||
}
|
||||
@@ -57,7 +61,14 @@
|
||||
callback: cb,
|
||||
script_id: script_id,
|
||||
});
|
||||
}
|
||||
|
||||
async function async(cb) {
|
||||
const script_id = document.currentScript.id;
|
||||
const stack = new Error().stack;
|
||||
async_capture = {script_id: script_id, stack: stack};
|
||||
await cb();
|
||||
async_capture = null;
|
||||
}
|
||||
|
||||
function assertOk() {
|
||||
@@ -92,6 +103,7 @@
|
||||
|
||||
window.testing = {
|
||||
fail: fail,
|
||||
async: async,
|
||||
assertOk: assertOk,
|
||||
expectTrue: expectTrue,
|
||||
expectFalse: expectFalse,
|
||||
@@ -125,7 +137,6 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (expected instanceof Node) {
|
||||
if (!(actual instanceof Node)) {
|
||||
return false;
|
||||
@@ -145,8 +156,8 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function _registerObservation(status) {
|
||||
const script_id = _currentScriptId();
|
||||
function _registerObservation(status, opts) {
|
||||
script_id = opts?.script_id || _currentScriptId();
|
||||
if (!script_id) {
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +172,12 @@
|
||||
return current_script_id;
|
||||
}
|
||||
|
||||
if (async_capture) {
|
||||
return async_capture.script_id;
|
||||
}
|
||||
|
||||
const current_script = document.currentScript;
|
||||
|
||||
if (!current_script) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=location>
|
||||
testing.expectEqual('http://localhost:9601/src/browser/tests/window/location.html', window.location.href);
|
||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', window.location.href);
|
||||
testing.expectEqual(document.location, window.location);
|
||||
</script>
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
window.reportError(err);
|
||||
|
||||
testing.expectEqual(true, evt.message.includes('Detailed error'));
|
||||
testing.expectEqual('script.js', evt.filename);
|
||||
testing.expectEqual(100, evt.lineno);
|
||||
testing.expectEqual(25, evt.colno);
|
||||
testing.expectEqual(err, evt.error);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -147,6 +147,29 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void {
|
||||
sc.removed = true;
|
||||
}
|
||||
|
||||
pub fn reportError(self: *Window, err: js.Object, page: *Page) !void {
|
||||
const error_event = try ErrorEvent.init("error", .{
|
||||
.@"error" = err,
|
||||
.message = err.toString() catch "Unknown error",
|
||||
.bubbles = false,
|
||||
.cancelable = true,
|
||||
}, page);
|
||||
|
||||
const event = error_event.asEvent();
|
||||
try page._event_manager.dispatch(self.asEventTarget(), event);
|
||||
|
||||
if (comptime builtin.is_test == false) {
|
||||
if (!event._prevent_default) {
|
||||
log.warn(.js, "window.reportError", .{
|
||||
.message = error_event._message,
|
||||
.filename = error_event._filename,
|
||||
.line_number = error_event._line_number,
|
||||
.column_number = error_event._column_number,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQueryList {
|
||||
return page._factory.eventTarget(MediaQueryList{
|
||||
._proto = undefined,
|
||||
@@ -290,6 +313,7 @@ pub const JsApi = struct {
|
||||
pub const matchMedia = bridge.function(Window.matchMedia, .{});
|
||||
pub const btoa = bridge.function(Window.btoa, .{});
|
||||
pub const atob = bridge.function(Window.atob, .{});
|
||||
pub const reportError = bridge.function(Window.reportError, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -30,11 +30,11 @@ pub const JsApi = struct {
|
||||
|
||||
pub const Build = struct {
|
||||
pub fn complete(node: *Node, page: *Page) !void {
|
||||
_ = node;
|
||||
_ = page;
|
||||
// @ZIGDOM
|
||||
// const el = node.as(Element);
|
||||
// const on_load = el.getAttributeSafe("onload") orelse return;
|
||||
// page.window._on_load = page.js.stringToFunction(on_load);
|
||||
const el = node.as(Element);
|
||||
const on_load = el.getAttributeSafe("onload") orelse return;
|
||||
page.window._on_load = page.js.stringToFunction(on_load) catch |err| blk: {
|
||||
log.err(.js, "body.onload", .{.err = err, .str = on_load});
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const log = @import("../../../../log.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Page = @import("../../../Page.zig");
|
||||
|
||||
@@ -77,15 +78,19 @@ pub const Build = struct {
|
||||
const element = self.asElement();
|
||||
self._src = element.getAttributeSafe("src") orelse "";
|
||||
|
||||
// @ZIGDOM
|
||||
_ = page;
|
||||
// if (element.getAttributeSafe("onload")) |on_load| {
|
||||
// self._on_load = page.js.stringToFunction(on_load);
|
||||
// }
|
||||
if (element.getAttributeSafe("onload")) |on_load| {
|
||||
self._on_load = page.js.stringToFunction(on_load) catch |err| blk: {
|
||||
log.err(.js, "script.onload", .{.err = err, .str = on_load});
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
|
||||
// if (element.getAttributeSafe("onerror")) |on_error| {
|
||||
// self._on_error = page.js.stringToFunction(on_error);
|
||||
// }
|
||||
if (element.getAttributeSafe("onerror")) |on_error| {
|
||||
self._on_error = page.js.stringToFunction(on_error) catch |err| blk: {
|
||||
log.err(.js, "script.onerror", .{.err = err, .str = on_error});
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,32 +22,26 @@ pub const EntryIterator = GenericIterator(Iterator, null);
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
|
||||
pub fn init(arena: Allocator, root: *Node, selector: Selector.Selector, page: *Page) !*List {
|
||||
var list = try page._factory.create(List{
|
||||
._arena = arena,
|
||||
._nodes = &.{},
|
||||
});
|
||||
|
||||
pub fn collect(
|
||||
allocator: std.mem.Allocator,
|
||||
root: *Node,
|
||||
selector: Selector.Selector,
|
||||
nodes: *std.AutoArrayHashMapUnmanaged(*Node, void),
|
||||
page: *Page,
|
||||
) !void {
|
||||
if (optimizeSelector(root, &selector, page)) |result| {
|
||||
var nodes: std.ArrayListUnmanaged(*Node) = .empty;
|
||||
|
||||
var tw = TreeWalker.init(result.root, .{});
|
||||
const optimized_selector = result.selector;
|
||||
if (result.exclude_root) {
|
||||
_ = tw.next();
|
||||
}
|
||||
// When exclude_root is true, pass root as boundary so it can match but we won't search beyond it
|
||||
// When exclude_root is false, pass null so there's no boundary (root already matched, searching descendants)
|
||||
|
||||
const boundary = if (result.exclude_root) result.root else null;
|
||||
while (tw.next()) |node| {
|
||||
if (matches(node, optimized_selector, boundary)) {
|
||||
try nodes.append(arena, node);
|
||||
if (matches(node, result.selector, boundary)) {
|
||||
try nodes.put(allocator, node, {});
|
||||
}
|
||||
}
|
||||
list._nodes = nodes.items;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// used internally to find the first match
|
||||
@@ -135,7 +129,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page
|
||||
.first = selector.first,
|
||||
.segments = selector.segments,
|
||||
},
|
||||
.exclude_root = false,
|
||||
.exclude_root = true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -238,7 +232,7 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool {
|
||||
pub fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool {
|
||||
const el = node.is(Node.Element) orelse return false;
|
||||
|
||||
if (selector.segments.len == 0) {
|
||||
@@ -333,8 +327,9 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node {
|
||||
const parent = node._parent orelse return null;
|
||||
|
||||
// Don't match beyond the root boundary
|
||||
// If there's a boundary, check if parent is outside (an ancestor of) the boundary
|
||||
if (root) |boundary| {
|
||||
if (parent == boundary) {
|
||||
if (!boundary.contains(parent)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Node = @import("../Node.zig");
|
||||
@@ -8,7 +10,6 @@ const Part = Selector.Part;
|
||||
const Combinator = Selector.Combinator;
|
||||
const Segment = Selector.Segment;
|
||||
const Attribute = @import("../element/Attribute.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Parser = @This();
|
||||
|
||||
@@ -26,10 +27,56 @@ const ParseError = error{
|
||||
InvalidTagSelector,
|
||||
InvalidSelector,
|
||||
};
|
||||
|
||||
pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]const Selector.Selector {
|
||||
var selectors: std.ArrayList(Selector.Selector) = .empty;
|
||||
|
||||
var remaining = input;
|
||||
while (true) {
|
||||
const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace);
|
||||
if (trimmed.len == 0) break;
|
||||
|
||||
var comma_pos: usize = trimmed.len;
|
||||
var depth: usize = 0;
|
||||
for (trimmed, 0..) |c, i| {
|
||||
switch (c) {
|
||||
'(' => depth += 1,
|
||||
')' => {
|
||||
if (depth > 0) depth -= 1;
|
||||
},
|
||||
',' => {
|
||||
if (depth == 0) {
|
||||
comma_pos = i;
|
||||
break;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
const selector_input = std.mem.trimRight(u8, trimmed[0..comma_pos], &std.ascii.whitespace);
|
||||
|
||||
if (selector_input.len > 0) {
|
||||
const selector = try parse(arena, selector_input, page);
|
||||
try selectors.append(arena, selector);
|
||||
}
|
||||
|
||||
if (comma_pos >= trimmed.len) break;
|
||||
remaining = trimmed[comma_pos + 1 ..];
|
||||
}
|
||||
|
||||
if (selectors.items.len == 0) {
|
||||
return error.InvalidSelector;
|
||||
}
|
||||
|
||||
return selectors.items;
|
||||
}
|
||||
|
||||
pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Selector.Selector {
|
||||
var parser = Parser{ .input = input };
|
||||
var segments: std.ArrayListUnmanaged(Segment) = .empty;
|
||||
var current_compound: std.ArrayListUnmanaged(Part) = .empty;
|
||||
var segments: std.ArrayList(Segment) = .empty;
|
||||
var current_compound: std.ArrayList(Part) = .empty;
|
||||
|
||||
|
||||
// Parse the first compound (no combinator before it)
|
||||
while (parser.skipSpaces()) {
|
||||
@@ -302,7 +349,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
if (std.mem.eql(u8, name, "not")) {
|
||||
// CSS Level 4: :not() can contain a full selector list (comma-separated selectors)
|
||||
// e.g., :not(div, .class, #id > span)
|
||||
var selectors: std.ArrayListUnmanaged(Selector.Selector) = .empty;
|
||||
var selectors: std.ArrayList(Selector.Selector) = .empty;
|
||||
|
||||
_ = self.skipSpaces();
|
||||
|
||||
|
||||
@@ -10,24 +10,29 @@ pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Elemen
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
const selector = try Parser.parse(page.call_arena, input, page);
|
||||
const arena = page.call_arena;
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
|
||||
// Fast path: single compound with only an ID selector
|
||||
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
|
||||
const first = selector.first.parts[0];
|
||||
if (first == .id) {
|
||||
const el = page.document._elements_by_id.get(first.id) orelse return null;
|
||||
// Check if the element is within the root subtree
|
||||
if (root.contains(el.asNode())) {
|
||||
for (selectors) |selector| {
|
||||
// Fast path: single compound with only an ID selector
|
||||
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
|
||||
const first = selector.first.parts[0];
|
||||
if (first == .id) {
|
||||
const el = page.document._elements_by_id.get(first.id) orelse continue;
|
||||
// Check if the element is within the root subtree
|
||||
if (root.contains(el.asNode())) {
|
||||
return el;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (List.initOne(root, selector, page)) |node| {
|
||||
if (node.is(Node.Element)) |el| {
|
||||
return el;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (List.initOne(root, selector, page)) |node| {
|
||||
return node.is(Node.Element);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -37,8 +42,33 @@ pub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List {
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
const selector = try Parser.parse(arena, input, page);
|
||||
return List.init(arena, root, selector, page);
|
||||
var nodes: std.AutoArrayHashMapUnmanaged(*Node, void) = .empty;
|
||||
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
for (selectors) |selector| {
|
||||
try List.collect(arena, root, selector, &nodes, page);
|
||||
}
|
||||
|
||||
return page._factory.create(List{
|
||||
._arena = arena,
|
||||
._nodes = nodes.keys(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool {
|
||||
if (input.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
const arena = page.call_arena;
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
|
||||
for (selectors) |selector| {
|
||||
if (List.matches(el.asNode(), selector, null)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bool {
|
||||
|
||||
@@ -131,7 +131,7 @@ pub fn log(comptime scope: Scope, level: Level, comptime msg: []const u8, data:
|
||||
var writer = stderr.writer(&buf);
|
||||
|
||||
logTo(scope, level, msg, data, &writer.interface) catch |log_err| {
|
||||
std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg });
|
||||
std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,7 +147,6 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (opts.format) {
|
||||
.logfmt => try logLogfmt(scope, level, msg, data, out),
|
||||
.pretty => try logPretty(scope, level, msg, data, out),
|
||||
|
||||
Reference in New Issue
Block a user