Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/browser/ScriptManager.zig
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ async_module_pool: std.heap.MemoryPool(AsyncModule),
// and can be requested as needed.
sync_modules: std.StringHashMapUnmanaged(*SyncModule),

// Mapping between module specifier and resolution.
// see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
// importmap contains resolved urls.
importmap: std.StringHashMapUnmanaged([:0]const u8),

const OrderList = std.DoublyLinkedList;

pub fn init(browser: *Browser, page: *Page) ScriptManager {
Expand All @@ -80,6 +85,7 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
.asyncs = .{},
.scripts = .{},
.deferreds = .{},
.importmap = .empty,
.sync_modules = .empty,
.is_evaluating = false,
.allocator = allocator,
Expand All @@ -106,6 +112,8 @@ pub fn deinit(self: *ScriptManager) void {
self.async_module_pool.deinit();

self.sync_modules.deinit(self.allocator);
// we don't deinit self.importmap b/c we use the page's arena for its
// allocations.
}

pub fn reset(self: *ScriptManager) void {
Expand All @@ -115,6 +123,9 @@ pub fn reset(self: *ScriptManager) void {
self.sync_module_pool.destroy(value_ptr.*);
}
self.sync_modules.clearRetainingCapacity();
// Our allocator is the page arena, it's been reset. We cannot use
// clearAndRetainCapacity, since that space is no longer ours
self.importmap = .empty;

self.clearList(&self.asyncs);
self.clearList(&self.scripts);
Expand Down Expand Up @@ -164,6 +175,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
break :blk .module;
}
if (std.ascii.eqlIgnoreCase(script_type, "importmap")) {
break :blk .importmap;
}

// "type" could be anything, but only the above are ones we need to process.
// Common other ones are application/json, application/ld+json, text/template
Expand Down Expand Up @@ -248,6 +262,21 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
});
}

// Resolve a module specifier to an valid URL.
pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, specifier: []const u8, base: []const u8) ![:0]const u8 {
// If the specifier is mapped in the importmap, return the pre-resolved value.
if (self.importmap.get(specifier)) |s| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the URLs in the import map to be resolved based on the document's URL and not the referrer of the module. If that's the case, then

1 - When populating importmap, the imports need to be resolved against page.url
2 - In this function, when found, it should be returned directly from the map

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fixed now

return s;
}

return URL.stitch(
arena,
specifier,
base,
.{ .alloc = .if_needed, .null_terminated = true },
);
}

pub fn getModule(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {
const gop = try self.sync_modules.getOrPut(self.allocator, url);
if (gop.found_existing) {
Expand Down Expand Up @@ -452,6 +481,38 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
script.errorCallback(err);
}

fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
const content = script.source.content();

const Imports = struct {
imports: std.json.ArrayHashMap([]const u8),
};

const imports = try std.json.parseFromSliceLeaky(
Imports,
self.page.arena,
content,
.{ .allocate = .alloc_always },
);

var iter = imports.imports.map.iterator();
while (iter.next()) |entry| {
// > Relative URLs are resolved to absolute URL addresses using the
// > base URL of the document containing the import map.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
const resolved_url = try URL.stitch(
self.page.arena,
entry.value_ptr.*,
self.page.url.raw,
.{ .alloc = .if_needed, .null_terminated = true },
);

try self.importmap.put(self.page.arena, entry.key_ptr.*, resolved_url);
}

return;
}

// A script which is pending execution.
// It could be pending because:
// (a) we're still downloading its content or
Expand Down Expand Up @@ -581,6 +642,7 @@ const Script = struct {
const Kind = enum {
module,
javascript,
importmap,
};

const Callback = union(enum) {
Expand Down Expand Up @@ -621,6 +683,23 @@ const Script = struct {
.cacheable = cacheable,
});

// Handle importmap special case here: the content is a JSON containing
// imports.
if (self.kind == .importmap) {
page.script_manager.parseImportmap(self) catch |err| {
log.err(.browser, "parse importmap script", .{
.err = err,
.src = url,
.kind = self.kind,
.cacheable = cacheable,
});
self.executeCallback("onerror", page);
return;
};
self.executeCallback("onload", page);
return;
}

const js_context = page.js;
var try_catch: js.TryCatch = undefined;
try_catch.init(js_context);
Expand All @@ -634,6 +713,7 @@ const Script = struct {
// We don't care about waiting for the evaluation here.
js_context.module(false, content, url, cacheable) catch break :blk false;
},
.importmap => unreachable, // handled before the try/catch.
}
break :blk true;
};
Expand Down
1 change: 1 addition & 0 deletions src/browser/html/elements.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ test "Browser: HTML.HtmlScriptElement" {
try testing.htmlRunner("html/script/inline_defer.html");
try testing.htmlRunner("html/script/import.html");
try testing.htmlRunner("html/script/dynamic_import.html");
try testing.htmlRunner("html/script/importmap.html");
}

test "Browser: HTML.HtmlSlotElement" {
Expand Down
9 changes: 3 additions & 6 deletions src/browser/js/Context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,10 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
for (0..requests.length()) |i| {
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
const normalized_specifier = try @import("../../url.zig").stitch(
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
self.call_arena,
specifier,
owned_url,
.{ .alloc = .if_needed, .null_terminated = true },
);
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (!gop.found_existing) {
Expand Down Expand Up @@ -1127,11 +1126,10 @@ pub fn dynamicModuleCallback(
return @constCast(self.rejectPromise("Out of memory").handle);
};

const normalized_specifier = @import("../../url.zig").stitch(
const normalized_specifier = self.script_manager.?.resolveSpecifier(
self.arena, // might need to survive until the module is loaded
specifier,
resource,
.{ .alloc = .if_needed, .null_terminated = true },
) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
return @constCast(self.rejectPromise("Out of memory").handle);
Expand Down Expand Up @@ -1171,11 +1169,10 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
return error.UnknownModuleReferrer;
};

const normalized_specifier = try @import("../../url.zig").stitch(
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
self.call_arena,
specifier,
referrer_path,
.{ .alloc = .if_needed, .null_terminated = true },
);

const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
Expand Down
24 changes: 24 additions & 0 deletions src/tests/html/script/importmap.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>

<script src="../../testing.js"></script>

<script type=importmap>
{
"imports": {
"core": "./import.js"
}
}
</script>

<script id=use_importmap type=module>
import * as im from 'core';
testing.expectEqual('hello', im.greeting);
</script>

<script id=cached_importmap type=module>
// hopefully cached, who knows, no real way to assert this
// but at least it works.
import * as im from 'core';
testing.expectEqual('hello', im.greeting);
</script>