Skip to content

Commit b084dde

Browse files
authored
Merge pull request #872 from lightpanda-io/dynamic-import
2 parents 5229a7c + 287df42 commit b084dde

File tree

2 files changed

+185
-31
lines changed

2 files changed

+185
-31
lines changed

src/browser/page.zig

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,12 +1018,16 @@ const Script = struct {
10181018
.cacheable = cacheable,
10191019
});
10201020

1021-
const result = switch (self.kind) {
1022-
.javascript => page.main_context.eval(body, src),
1023-
.module => page.main_context.module(body, src, cacheable),
1021+
const failed = blk: {
1022+
switch (self.kind) {
1023+
.javascript => _ = page.main_context.eval(body, src) catch break :blk true,
1024+
// We don't care about waiting for the evaluation here.
1025+
.module => _ = page.main_context.module(body, src, cacheable) catch break :blk true,
1026+
}
1027+
break :blk false;
10241028
};
10251029

1026-
result catch {
1030+
if (failed) {
10271031
if (page.delayed_navigation) {
10281032
return error.Terminated;
10291033
}
@@ -1038,7 +1042,8 @@ const Script = struct {
10381042

10391043
try self.executeCallback("onerror", page);
10401044
return error.JsErr;
1041-
};
1045+
}
1046+
10421047
try self.executeCallback("onload", page);
10431048
}
10441049

src/runtime/js.zig

Lines changed: 175 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
195195
var isolate = v8.Isolate.init(params);
196196
errdefer isolate.deinit();
197197

198+
// This is the callback that runs whenever a module is dynamically imported.
199+
isolate.setHostImportModuleDynamicallyCallback(JsContext.dynamicModuleCallback);
200+
198201
isolate.enter();
199202
errdefer isolate.exit();
200203

@@ -759,17 +762,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
759762
}
760763

761764
// compile and eval a JS module
762-
// It doesn't wait for callbacks execution
763-
pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !void {
764-
if (!cacheable) {
765-
return self.moduleNoCache(src, url);
766-
}
767-
765+
// It returns null if the module is already compiled and in the cache.
766+
// It returns a v8.Promise if the module must be evaluated.
767+
pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !?v8.Promise {
768768
const arena = self.context_arena;
769769

770-
const gop = try self.module_cache.getOrPut(arena, url);
771-
if (gop.found_existing) {
772-
return;
770+
if (cacheable and self.module_cache.contains(url)) {
771+
return null;
773772
}
774773
errdefer _ = self.module_cache.remove(url);
775774

@@ -779,30 +778,26 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
779778
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
780779
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
781780

782-
gop.key_ptr.* = owned_url;
783-
gop.value_ptr.* = PersistentModule.init(self.isolate, m);
781+
if (cacheable) {
782+
try self.module_cache.putNoClobber(
783+
arena,
784+
owned_url,
785+
PersistentModule.init(self.isolate, m),
786+
);
787+
}
784788

785789
// resolveModuleCallback loads module's dependencies.
786790
const v8_context = self.v8_context;
787791
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
788792
return error.ModuleInstantiationError;
789793
}
790794

791-
_ = try m.evaluate(v8_context);
792-
}
793-
794-
fn moduleNoCache(self: *JsContext, src: []const u8, url: []const u8) !void {
795-
const m = try compileModule(self.isolate, src, url);
796-
797-
const arena = self.context_arena;
798-
const owned_url = try arena.dupe(u8, url);
799-
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
800-
801-
const v8_context = self.v8_context;
802-
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
803-
return error.ModuleInstantiationError;
804-
}
805-
_ = try m.evaluate(v8_context);
795+
const evaluated = try m.evaluate(v8_context);
796+
// https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
797+
// Must be a promise that gets returned here.
798+
std.debug.assert(evaluated.isPromise());
799+
const promise = v8.Promise{ .handle = evaluated.handle };
800+
return promise;
806801
}
807802

808803
// Wrap a v8.Exception
@@ -1514,6 +1509,160 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
15141509
type_index = prototype_index;
15151510
}
15161511
}
1512+
1513+
pub fn dynamicModuleCallback(
1514+
v8_ctx: ?*const v8.c.Context,
1515+
host_defined_options: ?*const v8.c.Data,
1516+
resource_name: ?*const v8.c.Value,
1517+
v8_specifier: ?*const v8.c.String,
1518+
import_attrs: ?*const v8.c.FixedArray,
1519+
) callconv(.c) ?*v8.c.Promise {
1520+
_ = host_defined_options;
1521+
_ = import_attrs;
1522+
const ctx: v8.Context = .{ .handle = v8_ctx.? };
1523+
const context: *JsContext = @ptrFromInt(ctx.getEmbedderData(1).castTo(v8.BigInt).getUint64());
1524+
const iso = context.isolate;
1525+
const resolver = v8.PromiseResolver.init(context.v8_context);
1526+
1527+
const specifier: v8.String = .{ .handle = v8_specifier.? };
1528+
const specifier_str = jsStringToZig(context.context_arena, specifier, iso) catch {
1529+
const error_msg = v8.String.initUtf8(iso, "Failed to parse module specifier");
1530+
_ = resolver.reject(ctx, error_msg.toValue());
1531+
return @constCast(resolver.getPromise().handle);
1532+
};
1533+
const resource: v8.String = .{ .handle = resource_name.? };
1534+
const resource_str = jsStringToZig(context.context_arena, resource, iso) catch {
1535+
const error_msg = v8.String.initUtf8(iso, "Failed to parse module resource");
1536+
_ = resolver.reject(ctx, error_msg.toValue());
1537+
return @constCast(resolver.getPromise().handle);
1538+
};
1539+
1540+
const normalized_specifier = @import("../url.zig").stitch(
1541+
context.context_arena,
1542+
specifier_str,
1543+
resource_str,
1544+
.{ .alloc = .if_needed },
1545+
) catch unreachable;
1546+
1547+
log.debug(.js, "dynamic import", .{
1548+
.specifier = specifier_str,
1549+
.resource = resource_str,
1550+
.normalized_specifier = normalized_specifier,
1551+
});
1552+
1553+
_dynamicModuleCallback(context, normalized_specifier, &resolver) catch |err| {
1554+
log.err(.js, "dynamic module callback", .{
1555+
.err = err,
1556+
});
1557+
// Must be rejected at this point
1558+
// otherwise, we will just wait on a pending promise.
1559+
std.debug.assert(resolver.getPromise().getState() == .kRejected);
1560+
};
1561+
return @constCast(resolver.getPromise().handle);
1562+
}
1563+
1564+
fn _dynamicModuleCallback(
1565+
self: *JsContext,
1566+
specifier: []const u8,
1567+
resolver: *const v8.PromiseResolver,
1568+
) !void {
1569+
const iso = self.isolate;
1570+
const ctx = self.v8_context;
1571+
1572+
const module_loader = self.module_loader;
1573+
const source = module_loader.func(module_loader.ptr, specifier) catch {
1574+
const error_msg = v8.String.initUtf8(iso, "Failed to load module");
1575+
_ = resolver.reject(ctx, error_msg.toValue());
1576+
return;
1577+
} orelse {
1578+
const error_msg = v8.String.initUtf8(iso, "Module source not available");
1579+
_ = resolver.reject(ctx, error_msg.toValue());
1580+
return;
1581+
};
1582+
1583+
var try_catch: TryCatch = undefined;
1584+
try_catch.init(self);
1585+
defer try_catch.deinit();
1586+
1587+
const maybe_promise = self.module(source, specifier, true) catch {
1588+
log.err(.js, "module compilation failed", .{
1589+
.specifier = specifier,
1590+
.exception = try_catch.exception(self.call_arena) catch "unknown error",
1591+
.stack = try_catch.stack(self.call_arena) catch null,
1592+
.line = try_catch.sourceLineNumber() orelse 0,
1593+
});
1594+
const error_msg = if (try_catch.hasCaught()) blk: {
1595+
const exception_str = try_catch.exception(self.call_arena) catch "Evaluation error";
1596+
break :blk v8.String.initUtf8(iso, exception_str orelse "Evaluation error");
1597+
} else v8.String.initUtf8(iso, "Module evaluation failed");
1598+
_ = resolver.reject(ctx, error_msg.toValue());
1599+
return;
1600+
};
1601+
const new_module = self.module_cache.get(specifier).?.castToModule();
1602+
1603+
if (maybe_promise) |promise| {
1604+
// This means we must wait for the evaluation.
1605+
const EvaluationData = struct {
1606+
specifier: []const u8,
1607+
module: v8.Persistent(v8.Module),
1608+
resolver: v8.Persistent(v8.PromiseResolver),
1609+
};
1610+
1611+
const ev_data = try self.context_arena.create(EvaluationData);
1612+
ev_data.* = .{
1613+
.specifier = specifier,
1614+
.module = v8.Persistent(v8.Module).init(iso, new_module),
1615+
.resolver = v8.Persistent(v8.PromiseResolver).init(iso, resolver.*),
1616+
};
1617+
const external = v8.External.init(iso, @ptrCast(ev_data));
1618+
1619+
const then_callback = v8.Function.initWithData(ctx, struct {
1620+
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
1621+
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
1622+
const cb_isolate = cb_info.getIsolate();
1623+
const cb_context = cb_isolate.getCurrentContext();
1624+
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
1625+
const cb_module = data.module.castToModule();
1626+
const cb_resolver = data.resolver.castToPromiseResolver();
1627+
1628+
const namespace = cb_module.getModuleNamespace();
1629+
log.info(.js, "dynamic import complete", .{ .specifier = data.specifier });
1630+
_ = cb_resolver.resolve(cb_context, namespace);
1631+
}
1632+
}.callback, external);
1633+
1634+
const catch_callback = v8.Function.initWithData(ctx, struct {
1635+
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
1636+
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
1637+
const cb_context = cb_info.getIsolate().getCurrentContext();
1638+
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
1639+
const cb_resolver = data.resolver.castToPromiseResolver();
1640+
1641+
log.err(.js, "dynamic import failed", .{ .specifier = data.specifier });
1642+
_ = cb_resolver.reject(cb_context, cb_info.getData());
1643+
}
1644+
}.callback, external);
1645+
1646+
_ = promise.thenAndCatch(ctx, then_callback, catch_callback) catch {
1647+
log.err(.js, "module evaluation is promise", .{
1648+
.specifier = specifier,
1649+
.line = try_catch.sourceLineNumber() orelse 0,
1650+
});
1651+
const error_msg = v8.String.initUtf8(iso, "Evaluation is a promise");
1652+
_ = resolver.reject(ctx, error_msg.toValue());
1653+
return;
1654+
};
1655+
} else {
1656+
// This means it is already present in the cache.
1657+
const namespace = new_module.getModuleNamespace();
1658+
log.info(.js, "dynamic import complete", .{
1659+
.module = new_module,
1660+
.namespace = namespace,
1661+
});
1662+
_ = resolver.resolve(ctx, namespace);
1663+
return;
1664+
}
1665+
}
15171666
};
15181667

15191668
pub const Function = struct {

0 commit comments

Comments
 (0)