diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig
index 06b6ca1d4..735a2eab8 100644
--- a/src/browser/ScriptManager.zig
+++ b/src/browser/ScriptManager.zig
@@ -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 {
@@ -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,
@@ -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 {
@@ -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);
@@ -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
@@ -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| {
+ 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) {
@@ -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
@@ -581,6 +642,7 @@ const Script = struct {
const Kind = enum {
module,
javascript,
+ importmap,
};
const Callback = union(enum) {
@@ -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);
@@ -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;
};
diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig
index 0fc5502f6..e9e95fdfc 100644
--- a/src/browser/html/elements.zig
+++ b/src/browser/html/elements.zig
@@ -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" {
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index 8d298f0d6..46c6261fd 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -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) {
@@ -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);
@@ -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);
diff --git a/src/tests/html/script/importmap.html b/src/tests/html/script/importmap.html
new file mode 100644
index 000000000..973d50806
--- /dev/null
+++ b/src/tests/html/script/importmap.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+