@@ -70,6 +70,11 @@ async_module_pool: std.heap.MemoryPool(AsyncModule),
7070// and can be requested as needed.
7171sync_modules : std .StringHashMapUnmanaged (* SyncModule ),
7272
73+ // Mapping between module specifier and resolution.
74+ // see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
75+ // importmap contains resolved urls.
76+ importmap : std .StringHashMapUnmanaged ([:0 ]const u8 ),
77+
7378const OrderList = std .DoublyLinkedList ;
7479
7580pub fn init (browser : * Browser , page : * Page ) ScriptManager {
@@ -80,6 +85,7 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
8085 .asyncs = .{},
8186 .scripts = .{},
8287 .deferreds = .{},
88+ .importmap = .empty ,
8389 .sync_modules = .empty ,
8490 .is_evaluating = false ,
8591 .allocator = allocator ,
@@ -106,6 +112,8 @@ pub fn deinit(self: *ScriptManager) void {
106112 self .async_module_pool .deinit ();
107113
108114 self .sync_modules .deinit (self .allocator );
115+ // we don't deinit self.importmap b/c we use the page's arena for its
116+ // allocations.
109117}
110118
111119pub fn reset (self : * ScriptManager ) void {
@@ -115,6 +123,9 @@ pub fn reset(self: *ScriptManager) void {
115123 self .sync_module_pool .destroy (value_ptr .* );
116124 }
117125 self .sync_modules .clearRetainingCapacity ();
126+ // Our allocator is the page arena, it's been reset. We cannot use
127+ // clearAndRetainCapacity, since that space is no longer ours
128+ self .importmap = .empty ;
118129
119130 self .clearList (& self .asyncs );
120131 self .clearList (& self .scripts );
@@ -164,6 +175,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
164175 if (std .ascii .eqlIgnoreCase (script_type , "module" )) {
165176 break :blk .module ;
166177 }
178+ if (std .ascii .eqlIgnoreCase (script_type , "importmap" )) {
179+ break :blk .importmap ;
180+ }
167181
168182 // "type" could be anything, but only the above are ones we need to process.
169183 // 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
248262 });
249263}
250264
265+ // Resolve a module specifier to an valid URL.
266+ pub fn resolveSpecifier (self : * ScriptManager , arena : Allocator , specifier : []const u8 , base : []const u8 ) ! [:0 ]const u8 {
267+ // If the specifier is mapped in the importmap, return the pre-resolved value.
268+ if (self .importmap .get (specifier )) | s | {
269+ return s ;
270+ }
271+
272+ return URL .stitch (
273+ arena ,
274+ specifier ,
275+ base ,
276+ .{ .alloc = .if_needed , .null_terminated = true },
277+ );
278+ }
279+
251280pub fn getModule (self : * ScriptManager , url : [:0 ]const u8 , referrer : []const u8 ) ! void {
252281 const gop = try self .sync_modules .getOrPut (self .allocator , url );
253282 if (gop .found_existing ) {
@@ -452,6 +481,38 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
452481 script .errorCallback (err );
453482}
454483
484+ fn parseImportmap (self : * ScriptManager , script : * const Script ) ! void {
485+ const content = script .source .content ();
486+
487+ const Imports = struct {
488+ imports : std .json .ArrayHashMap ([]const u8 ),
489+ };
490+
491+ const imports = try std .json .parseFromSliceLeaky (
492+ Imports ,
493+ self .page .arena ,
494+ content ,
495+ .{ .allocate = .alloc_always },
496+ );
497+
498+ var iter = imports .imports .map .iterator ();
499+ while (iter .next ()) | entry | {
500+ // > Relative URLs are resolved to absolute URL addresses using the
501+ // > base URL of the document containing the import map.
502+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
503+ const resolved_url = try URL .stitch (
504+ self .page .arena ,
505+ entry .value_ptr .* ,
506+ self .page .url .raw ,
507+ .{ .alloc = .if_needed , .null_terminated = true },
508+ );
509+
510+ try self .importmap .put (self .page .arena , entry .key_ptr .* , resolved_url );
511+ }
512+
513+ return ;
514+ }
515+
455516// A script which is pending execution.
456517// It could be pending because:
457518// (a) we're still downloading its content or
@@ -581,6 +642,7 @@ const Script = struct {
581642 const Kind = enum {
582643 module ,
583644 javascript ,
645+ importmap ,
584646 };
585647
586648 const Callback = union (enum ) {
@@ -621,6 +683,23 @@ const Script = struct {
621683 .cacheable = cacheable ,
622684 });
623685
686+ // Handle importmap special case here: the content is a JSON containing
687+ // imports.
688+ if (self .kind == .importmap ) {
689+ page .script_manager .parseImportmap (self ) catch | err | {
690+ log .err (.browser , "parse importmap script" , .{
691+ .err = err ,
692+ .src = url ,
693+ .kind = self .kind ,
694+ .cacheable = cacheable ,
695+ });
696+ self .executeCallback ("onerror" , page );
697+ return ;
698+ };
699+ self .executeCallback ("onload" , page );
700+ return ;
701+ }
702+
624703 const js_context = page .js ;
625704 var try_catch : js.TryCatch = undefined ;
626705 try_catch .init (js_context );
@@ -634,6 +713,7 @@ const Script = struct {
634713 // We don't care about waiting for the evaluation here.
635714 js_context .module (false , content , url , cacheable ) catch break :blk false ;
636715 },
716+ .importmap = > unreachable , // handled before the try/catch.
637717 }
638718 break :blk true ;
639719 };
0 commit comments