From 4bf79e4bc939223e83d44b4e745d08d7f902f8a4 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 9 Oct 2025 16:08:46 +0200 Subject: [PATCH] add importmap support --- src/browser/ScriptManager.zig | 60 +++++++++++++++++++++++++++- src/tests/html/script/importmap.html | 4 +- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 3ee86be6..9d9b505e 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -70,6 +70,10 @@ 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: std.StringHashMapUnmanaged([]const u8), + const OrderList = std.DoublyLinkedList; pub fn init(browser: *Browser, page: *Page) ScriptManager { @@ -80,6 +84,7 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager { .asyncs = .{}, .scripts = .{}, .deferreds = .{}, + .importmap = .empty, .sync_modules = .empty, .is_evaluating = false, .allocator = allocator, @@ -106,6 +111,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 +122,7 @@ pub fn reset(self: *ScriptManager) void { self.sync_module_pool.destroy(value_ptr.*); } self.sync_modules.clearRetainingCapacity(); + self.importmap.clearRetainingCapacity(); self.clearList(&self.asyncs); self.clearList(&self.scripts); @@ -164,6 +172,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 @@ -249,7 +260,13 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c } // Resolve a module specifier to an valid URL. -pub fn resolveSpecifier(_: *ScriptManager, arena: Allocator, specifier: []const u8, base: []const u8) ![:0]const u8 { +pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, _specifier: []const u8, base: []const u8) ![:0]const u8 { + var specifier = _specifier; + // If the specifier is mapped in the importmap, use it to resolve the path. + if (self.importmap.get(specifier)) |s| { + specifier = s; + } + return URL.stitch( arena, specifier, @@ -462,6 +479,28 @@ 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| { + try self.importmap.put(self.page.arena, entry.key_ptr.*, entry.value_ptr.*); + } + + return; +} + // A script which is pending execution. // It could be pending because: // (a) we're still downloading its content or @@ -591,6 +630,7 @@ const Script = struct { const Kind = enum { module, javascript, + importmap, }; const Callback = union(enum) { @@ -631,6 +671,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); @@ -644,6 +701,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/tests/html/script/importmap.html b/src/tests/html/script/importmap.html index d3f457a9..c294674f 100644 --- a/src/tests/html/script/importmap.html +++ b/src/tests/html/script/importmap.html @@ -2,10 +2,10 @@ -