mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Merge pull request #1135 from lightpanda-io/importmap
Importmap support
This commit is contained in:
@@ -70,6 +70,11 @@ async_module_pool: std.heap.MemoryPool(AsyncModule),
|
|||||||
// and can be requested as needed.
|
// and can be requested as needed.
|
||||||
sync_modules: std.StringHashMapUnmanaged(*SyncModule),
|
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;
|
const OrderList = std.DoublyLinkedList;
|
||||||
|
|
||||||
pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
||||||
@@ -80,6 +85,7 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
|||||||
.asyncs = .{},
|
.asyncs = .{},
|
||||||
.scripts = .{},
|
.scripts = .{},
|
||||||
.deferreds = .{},
|
.deferreds = .{},
|
||||||
|
.importmap = .empty,
|
||||||
.sync_modules = .empty,
|
.sync_modules = .empty,
|
||||||
.is_evaluating = false,
|
.is_evaluating = false,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
@@ -106,6 +112,8 @@ pub fn deinit(self: *ScriptManager) void {
|
|||||||
self.async_module_pool.deinit();
|
self.async_module_pool.deinit();
|
||||||
|
|
||||||
self.sync_modules.deinit(self.allocator);
|
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 {
|
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_module_pool.destroy(value_ptr.*);
|
||||||
}
|
}
|
||||||
self.sync_modules.clearRetainingCapacity();
|
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.asyncs);
|
||||||
self.clearList(&self.scripts);
|
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")) {
|
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
|
||||||
break :blk .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.
|
// "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
|
// 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 {
|
pub fn getModule(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {
|
||||||
const gop = try self.sync_modules.getOrPut(self.allocator, url);
|
const gop = try self.sync_modules.getOrPut(self.allocator, url);
|
||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
@@ -452,6 +481,38 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
|||||||
script.errorCallback(err);
|
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.
|
// A script which is pending execution.
|
||||||
// It could be pending because:
|
// It could be pending because:
|
||||||
// (a) we're still downloading its content or
|
// (a) we're still downloading its content or
|
||||||
@@ -581,6 +642,7 @@ const Script = struct {
|
|||||||
const Kind = enum {
|
const Kind = enum {
|
||||||
module,
|
module,
|
||||||
javascript,
|
javascript,
|
||||||
|
importmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Callback = union(enum) {
|
const Callback = union(enum) {
|
||||||
@@ -621,6 +683,23 @@ const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.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;
|
const js_context = page.js;
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(js_context);
|
try_catch.init(js_context);
|
||||||
@@ -634,6 +713,7 @@ const Script = struct {
|
|||||||
// We don't care about waiting for the evaluation here.
|
// We don't care about waiting for the evaluation here.
|
||||||
js_context.module(false, content, url, cacheable) catch break :blk false;
|
js_context.module(false, content, url, cacheable) catch break :blk false;
|
||||||
},
|
},
|
||||||
|
.importmap => unreachable, // handled before the try/catch.
|
||||||
}
|
}
|
||||||
break :blk true;
|
break :blk true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1353,6 +1353,7 @@ test "Browser: HTML.HtmlScriptElement" {
|
|||||||
try testing.htmlRunner("html/script/inline_defer.html");
|
try testing.htmlRunner("html/script/inline_defer.html");
|
||||||
try testing.htmlRunner("html/script/import.html");
|
try testing.htmlRunner("html/script/import.html");
|
||||||
try testing.htmlRunner("html/script/dynamic_import.html");
|
try testing.htmlRunner("html/script/dynamic_import.html");
|
||||||
|
try testing.htmlRunner("html/script/importmap.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser: HTML.HtmlSlotElement" {
|
test "Browser: HTML.HtmlSlotElement" {
|
||||||
|
|||||||
@@ -251,11 +251,10 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
|||||||
for (0..requests.length()) |i| {
|
for (0..requests.length()) |i| {
|
||||||
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
|
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
|
||||||
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
|
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,
|
self.call_arena,
|
||||||
specifier,
|
specifier,
|
||||||
owned_url,
|
owned_url,
|
||||||
.{ .alloc = .if_needed, .null_terminated = true },
|
|
||||||
);
|
);
|
||||||
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
||||||
if (!gop.found_existing) {
|
if (!gop.found_existing) {
|
||||||
@@ -1127,11 +1126,10 @@ pub fn dynamicModuleCallback(
|
|||||||
return @constCast(self.rejectPromise("Out of memory").handle);
|
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
|
self.arena, // might need to survive until the module is loaded
|
||||||
specifier,
|
specifier,
|
||||||
resource,
|
resource,
|
||||||
.{ .alloc = .if_needed, .null_terminated = true },
|
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
||||||
return @constCast(self.rejectPromise("Out of memory").handle);
|
return @constCast(self.rejectPromise("Out of memory").handle);
|
||||||
@@ -1171,11 +1169,10 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
|||||||
return error.UnknownModuleReferrer;
|
return error.UnknownModuleReferrer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalized_specifier = try @import("../../url.zig").stitch(
|
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
||||||
self.call_arena,
|
self.call_arena,
|
||||||
specifier,
|
specifier,
|
||||||
referrer_path,
|
referrer_path,
|
||||||
.{ .alloc = .if_needed, .null_terminated = true },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
||||||
|
|||||||
24
src/tests/html/script/importmap.html
Normal file
24
src/tests/html/script/importmap.html
Normal file
@@ -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>
|
||||||
|
|
||||||
Reference in New Issue
Block a user