diff --git a/src/browser/page.zig b/src/browser/page.zig index 5e7afc48..f96d5791 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -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; } @@ -1038,7 +1042,8 @@ const Script = struct { try self.executeCallback("onerror", page); return error.JsErr; - }; + } + try self.executeCallback("onload", page); } diff --git a/src/runtime/js.zig b/src/runtime/js.zig index fcd58b17..789ca93e 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -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(); @@ -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); @@ -779,8 +778,13 @@ 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; @@ -788,21 +792,12 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { 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 @@ -1514,6 +1509,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 {