Skip to content

Commit b53c2bf

Browse files
Merge pull request #1135 from lightpanda-io/importmap
Importmap support
2 parents 8060563 + acf06fd commit b53c2bf

File tree

4 files changed

+108
-6
lines changed

4 files changed

+108
-6
lines changed

src/browser/ScriptManager.zig

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ async_module_pool: std.heap.MemoryPool(AsyncModule),
7070
// and can be requested as needed.
7171
sync_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+
7378
const OrderList = std.DoublyLinkedList;
7479

7580
pub 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

111119
pub 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+
251280
pub 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
};

src/browser/html/elements.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,7 @@ test "Browser: HTML.HtmlScriptElement" {
13531353
try testing.htmlRunner("html/script/inline_defer.html");
13541354
try testing.htmlRunner("html/script/import.html");
13551355
try testing.htmlRunner("html/script/dynamic_import.html");
1356+
try testing.htmlRunner("html/script/importmap.html");
13561357
}
13571358

13581359
test "Browser: HTML.HtmlSlotElement" {

src/browser/js/Context.zig

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,10 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
251251
for (0..requests.length()) |i| {
252252
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
253253
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
254-
const normalized_specifier = try @import("../../url.zig").stitch(
254+
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
255255
self.call_arena,
256256
specifier,
257257
owned_url,
258-
.{ .alloc = .if_needed, .null_terminated = true },
259258
);
260259
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
261260
if (!gop.found_existing) {
@@ -1127,11 +1126,10 @@ pub fn dynamicModuleCallback(
11271126
return @constCast(self.rejectPromise("Out of memory").handle);
11281127
};
11291128

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

1174-
const normalized_specifier = try @import("../../url.zig").stitch(
1172+
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
11751173
self.call_arena,
11761174
specifier,
11771175
referrer_path,
1178-
.{ .alloc = .if_needed, .null_terminated = true },
11791176
);
11801177

11811178
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
3+
<script src="../../testing.js"></script>
4+
5+
<script type=importmap>
6+
{
7+
"imports": {
8+
"core": "./import.js"
9+
}
10+
}
11+
</script>
12+
13+
<script id=use_importmap type=module>
14+
import * as im from 'core';
15+
testing.expectEqual('hello', im.greeting);
16+
</script>
17+
18+
<script id=cached_importmap type=module>
19+
// hopefully cached, who knows, no real way to assert this
20+
// but at least it works.
21+
import * as im from 'core';
22+
testing.expectEqual('hello', im.greeting);
23+
</script>
24+

0 commit comments

Comments
 (0)