Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/browser/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1018,12 +1018,16 @@ const Script = struct {
.cacheable = cacheable,
});

const result = switch (self.kind) {
.javascript => page.main_context.eval(body, src),
.module => page.main_context.module(body, src, cacheable),
const failed = blk: {
switch (self.kind) {
.javascript => _ = page.main_context.eval(body, src) catch break :blk true,
// We don't care about waiting for the evaluation here.
.module => _ = page.main_context.module(body, src, cacheable) catch break :blk true,
}
break :blk false;
};

result catch {
if (failed) {
if (page.delayed_navigation) {
return error.Terminated;
}
Expand All @@ -1038,7 +1042,8 @@ const Script = struct {

try self.executeCallback("onerror", page);
return error.JsErr;
};
}

try self.executeCallback("onload", page);
}

Expand Down
201 changes: 175 additions & 26 deletions src/runtime/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
var isolate = v8.Isolate.init(params);
errdefer isolate.deinit();

// This is the callback that runs whenever a module is dynamically imported.
isolate.setHostImportModuleDynamicallyCallback(JsContext.dynamicModuleCallback);

isolate.enter();
errdefer isolate.exit();

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

// compile and eval a JS module
// It doesn't wait for callbacks execution
pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !void {
if (!cacheable) {
return self.moduleNoCache(src, url);
}

// It returns null if the module is already compiled and in the cache.
// It returns a v8.Promise if the module must be evaluated.
pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !?v8.Promise {
const arena = self.context_arena;

const gop = try self.module_cache.getOrPut(arena, url);
if (gop.found_existing) {
return;
if (cacheable and self.module_cache.contains(url)) {
return null;
}
errdefer _ = self.module_cache.remove(url);

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

gop.key_ptr.* = owned_url;
gop.value_ptr.* = PersistentModule.init(self.isolate, m);
if (cacheable) {
try self.module_cache.putNoClobber(
arena,
owned_url,
PersistentModule.init(self.isolate, m),
);
}

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

_ = try m.evaluate(v8_context);
}

fn moduleNoCache(self: *JsContext, src: []const u8, url: []const u8) !void {
const m = try compileModule(self.isolate, src, url);

const arena = self.context_arena;
const owned_url = try arena.dupe(u8, url);
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);

const v8_context = self.v8_context;
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
return error.ModuleInstantiationError;
}
_ = try m.evaluate(v8_context);
const evaluated = try m.evaluate(v8_context);
// https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
// Must be a promise that gets returned here.
std.debug.assert(evaluated.isPromise());
const promise = v8.Promise{ .handle = evaluated.handle };
return promise;
}

// Wrap a v8.Exception
Expand Down Expand Up @@ -1507,6 +1502,160 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
type_index = prototype_index;
}
}

pub fn dynamicModuleCallback(
v8_ctx: ?*const v8.c.Context,
host_defined_options: ?*const v8.c.Data,
resource_name: ?*const v8.c.Value,
v8_specifier: ?*const v8.c.String,
import_attrs: ?*const v8.c.FixedArray,
) callconv(.c) ?*v8.c.Promise {
_ = host_defined_options;
_ = import_attrs;
const ctx: v8.Context = .{ .handle = v8_ctx.? };
const context: *JsContext = @ptrFromInt(ctx.getEmbedderData(1).castTo(v8.BigInt).getUint64());
const iso = context.isolate;
const resolver = v8.PromiseResolver.init(context.v8_context);

const specifier: v8.String = .{ .handle = v8_specifier.? };
const specifier_str = jsStringToZig(context.context_arena, specifier, iso) catch {
const error_msg = v8.String.initUtf8(iso, "Failed to parse module specifier");
_ = resolver.reject(ctx, error_msg.toValue());
return @constCast(resolver.getPromise().handle);
};
const resource: v8.String = .{ .handle = resource_name.? };
const resource_str = jsStringToZig(context.context_arena, resource, iso) catch {
const error_msg = v8.String.initUtf8(iso, "Failed to parse module resource");
_ = resolver.reject(ctx, error_msg.toValue());
return @constCast(resolver.getPromise().handle);
};

const normalized_specifier = @import("../url.zig").stitch(
context.context_arena,
specifier_str,
resource_str,
.{ .alloc = .if_needed },
) catch unreachable;

log.debug(.js, "dynamic import", .{
.specifier = specifier_str,
.resource = resource_str,
.normalized_specifier = normalized_specifier,
});

_dynamicModuleCallback(context, normalized_specifier, &resolver) catch |err| {
log.err(.js, "dynamic module callback", .{
.err = err,
});
// Must be rejected at this point
// otherwise, we will just wait on a pending promise.
std.debug.assert(resolver.getPromise().getState() == .kRejected);
};
return @constCast(resolver.getPromise().handle);
}

fn _dynamicModuleCallback(
self: *JsContext,
specifier: []const u8,
resolver: *const v8.PromiseResolver,
) !void {
const iso = self.isolate;
const ctx = self.v8_context;

const module_loader = self.module_loader;
const source = module_loader.func(module_loader.ptr, specifier) catch {
const error_msg = v8.String.initUtf8(iso, "Failed to load module");
_ = resolver.reject(ctx, error_msg.toValue());
return;
} orelse {
const error_msg = v8.String.initUtf8(iso, "Module source not available");
_ = resolver.reject(ctx, error_msg.toValue());
return;
};

var try_catch: TryCatch = undefined;
try_catch.init(self);
defer try_catch.deinit();

const maybe_promise = self.module(source, specifier, true) catch {
log.err(.js, "module compilation failed", .{
.specifier = specifier,
.exception = try_catch.exception(self.call_arena) catch "unknown error",
.stack = try_catch.stack(self.call_arena) catch null,
.line = try_catch.sourceLineNumber() orelse 0,
});
const error_msg = if (try_catch.hasCaught()) blk: {
const exception_str = try_catch.exception(self.call_arena) catch "Evaluation error";
break :blk v8.String.initUtf8(iso, exception_str orelse "Evaluation error");
} else v8.String.initUtf8(iso, "Module evaluation failed");
_ = resolver.reject(ctx, error_msg.toValue());
return;
};
const new_module = self.module_cache.get(specifier).?.castToModule();

if (maybe_promise) |promise| {
// This means we must wait for the evaluation.
const EvaluationData = struct {
specifier: []const u8,
module: v8.Persistent(v8.Module),
resolver: v8.Persistent(v8.PromiseResolver),
};

const ev_data = try self.context_arena.create(EvaluationData);
ev_data.* = .{
.specifier = specifier,
.module = v8.Persistent(v8.Module).init(iso, new_module),
.resolver = v8.Persistent(v8.PromiseResolver).init(iso, resolver.*),
};
const external = v8.External.init(iso, @ptrCast(ev_data));

const then_callback = v8.Function.initWithData(ctx, struct {
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
const cb_isolate = cb_info.getIsolate();
const cb_context = cb_isolate.getCurrentContext();
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
const cb_module = data.module.castToModule();
const cb_resolver = data.resolver.castToPromiseResolver();

const namespace = cb_module.getModuleNamespace();
log.info(.js, "dynamic import complete", .{ .specifier = data.specifier });
_ = cb_resolver.resolve(cb_context, namespace);
}
}.callback, external);

const catch_callback = v8.Function.initWithData(ctx, struct {
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
const cb_context = cb_info.getIsolate().getCurrentContext();
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
const cb_resolver = data.resolver.castToPromiseResolver();

log.err(.js, "dynamic import failed", .{ .specifier = data.specifier });
_ = cb_resolver.reject(cb_context, cb_info.getData());
}
}.callback, external);

_ = promise.thenAndCatch(ctx, then_callback, catch_callback) catch {
log.err(.js, "module evaluation is promise", .{
.specifier = specifier,
.line = try_catch.sourceLineNumber() orelse 0,
});
const error_msg = v8.String.initUtf8(iso, "Evaluation is a promise");
_ = resolver.reject(ctx, error_msg.toValue());
return;
};
} else {
// This means it is already present in the cache.
const namespace = new_module.getModuleNamespace();
log.info(.js, "dynamic import complete", .{
.module = new_module,
.namespace = namespace,
});
_ = resolver.resolve(ctx, namespace);
return;
}
}
};

pub const Function = struct {
Expand Down