From a13ed0bec31a90a53b5c2d9ec45488ebb90b95bb Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 25 Jun 2025 11:38:44 -0700 Subject: [PATCH 1/5] add dynamic import callback to isolate --- src/runtime/js.zig | 199 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 2dde71b2..0b289a0a 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -195,6 +195,205 @@ 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(struct { + pub fn callback( + 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(ctx); + + const specifier: v8.String = .{ .handle = v8_specifier.? }; + const specifier_str = jsStringToZig(context.call_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.call_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 referrer_full_url = blk: { + // Search through module_identifier values to find matching resource + var it = context.module_identifier.valueIterator(); + while (it.next()) |full_url| { + // Extract just the filename from the full URL + const last_slash = std.mem.lastIndexOfScalar(u8, full_url.*, '/') orelse 0; + const filename = full_url.*[last_slash + 1 ..]; + + // Compare with our resource string (removing ./ prefix if present) + const resource_clean = if (std.mem.startsWith(u8, resource_str, "./")) + resource_str[2..] + else + resource_str; + + if (std.mem.eql(u8, filename, resource_clean)) { + break :blk full_url.*; + } + } + + // Fallback - maybe it's already a full URL in some cases? + break :blk resource_str; + }; + + const normalized_specifier = @import("../url.zig").stitch( + context.context_arena, + specifier_str, + referrer_full_url, + .{ .alloc = .if_needed }, + ) catch unreachable; + + // TODO: we need to resolve the full URL here and normalize it. + // That way we can pass the correct one in the module_loader. + + log.info(.js, "dynamic import", .{ + .specifier = specifier_str, + .resource = resource_str, + .normalized = normalized_specifier, + }); + + const module_loader = context.module_loader; + const source = module_loader.func(module_loader.ptr, normalized_specifier) catch { + const error_msg = v8.String.initUtf8(iso, "Failed to load module"); + _ = resolver.reject(ctx, error_msg.toValue()); + return @constCast(resolver.getPromise().handle); + } orelse { + const error_msg = v8.String.initUtf8(iso, "Module source not available"); + _ = resolver.reject(ctx, error_msg.toValue()); + return @constCast(resolver.getPromise().handle); + }; + + var try_catch: TryCatch = undefined; + try_catch.init(context); + defer try_catch.deinit(); + + const module = compileModule(iso, source, specifier_str) catch { + log.err(.js, "module compilation failed", .{ + .specifier = specifier_str, + .exception = try_catch.exception(context.call_arena) catch "unknown error", + .stack = try_catch.stack(context.call_arena) catch null, + }); + const error_msg = if (try_catch.hasCaught()) blk: { + const exception_str = try_catch.exception(context.call_arena) catch "Compilation error"; + break :blk v8.String.initUtf8(iso, exception_str orelse "Compilation error"); + } else v8.String.initUtf8(iso, "Module compilation failed"); + + _ = resolver.reject(ctx, error_msg.toValue()); + return @constCast(resolver.getPromise().handle); + }; + + context.module_identifier.putNoClobber(context.context_arena, module.getIdentityHash(), normalized_specifier) catch unreachable; + context.module_cache.putNoClobber(context.context_arena, normalized_specifier, v8.Persistent(v8.Module).init(iso, module)) catch unreachable; + + const instantiated = module.instantiate(ctx, JsContext.resolveModuleCallback) catch { + log.err(.js, "module instantiation failed", .{ + .specifier = specifier_str, + .exception = try_catch.exception(context.call_arena) catch "unknown error", + .stack = try_catch.stack(context.call_arena) catch null, + }); + const error_msg = if (try_catch.hasCaught()) blk: { + const exception_str = try_catch.exception(context.call_arena) catch "Instantiation error"; + break :blk v8.String.initUtf8(iso, exception_str orelse "Instantiation error"); + } else v8.String.initUtf8(iso, "Module instantiation failed"); + + _ = resolver.reject(ctx, error_msg.toValue()); + return @constCast(resolver.getPromise().handle); + }; + + if (!instantiated) { + const error_msg = v8.String.initUtf8(iso, "Module did not instantiate"); + _ = resolver.reject(ctx, error_msg.toValue()); + return @constCast(resolver.getPromise().handle); + } + + const evaluated = module.evaluate(ctx) catch { + log.err(.js, "module evaluation failed", .{ + .specifier = specifier_str, + .exception = try_catch.exception(context.call_arena) catch "unknown error", + .stack = try_catch.stack(context.call_arena) catch null, + .line = try_catch.sourceLineNumber() orelse 0, + }); + const error_msg = if (try_catch.hasCaught()) blk: { + const exception_str = try_catch.exception(context.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 @constCast(resolver.getPromise().handle); + }; + + if (evaluated.isPromise()) { + const promise = v8.Promise{ .handle = evaluated.handle }; + + const EvaluationData = struct { + module: v8.Persistent(v8.Module), + resolver: v8.Persistent(v8.PromiseResolver), + }; + + const ev_data = context.context_arena.create(EvaluationData) catch unreachable; + ev_data.* = .{ + .module = v8.Persistent(v8.Module).init(iso, 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.warn(.js, "module then promise", .{ + .namespace = namespace, + }); + _ = 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 { + log.warn(.js, "module catch promise", .{}); + 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(); + _ = 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_str, + .line = try_catch.sourceLineNumber() orelse 0, + }); + const error_msg = v8.String.initUtf8(iso, "Evaluation is a promise"); + _ = resolver.reject(ctx, error_msg.toValue()); + return @constCast(resolver.getPromise().handle); + }; + } else { + const namespace = module.getModuleNamespace(); + _ = resolver.resolve(ctx, namespace); + } + + return @constCast(resolver.getPromise().handle); + } + }.callback); + isolate.enter(); errdefer isolate.exit(); From 2a8733787504e6df57a5c0758587dd7bac4bfcd3 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 9 Jul 2025 09:38:16 -0700 Subject: [PATCH 2/5] dynamicImportCallback in JsContext --- src/runtime/js.zig | 406 +++++++++++++++++++++++---------------------- 1 file changed, 209 insertions(+), 197 deletions(-) diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 0b289a0a..9881caa5 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -196,203 +196,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { errdefer isolate.deinit(); // This is the callback that runs whenever a module is dynamically imported. - isolate.setHostImportModuleDynamicallyCallback(struct { - pub fn callback( - 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(ctx); - - const specifier: v8.String = .{ .handle = v8_specifier.? }; - const specifier_str = jsStringToZig(context.call_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.call_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 referrer_full_url = blk: { - // Search through module_identifier values to find matching resource - var it = context.module_identifier.valueIterator(); - while (it.next()) |full_url| { - // Extract just the filename from the full URL - const last_slash = std.mem.lastIndexOfScalar(u8, full_url.*, '/') orelse 0; - const filename = full_url.*[last_slash + 1 ..]; - - // Compare with our resource string (removing ./ prefix if present) - const resource_clean = if (std.mem.startsWith(u8, resource_str, "./")) - resource_str[2..] - else - resource_str; - - if (std.mem.eql(u8, filename, resource_clean)) { - break :blk full_url.*; - } - } - - // Fallback - maybe it's already a full URL in some cases? - break :blk resource_str; - }; - - const normalized_specifier = @import("../url.zig").stitch( - context.context_arena, - specifier_str, - referrer_full_url, - .{ .alloc = .if_needed }, - ) catch unreachable; - - // TODO: we need to resolve the full URL here and normalize it. - // That way we can pass the correct one in the module_loader. - - log.info(.js, "dynamic import", .{ - .specifier = specifier_str, - .resource = resource_str, - .normalized = normalized_specifier, - }); - - const module_loader = context.module_loader; - const source = module_loader.func(module_loader.ptr, normalized_specifier) catch { - const error_msg = v8.String.initUtf8(iso, "Failed to load module"); - _ = resolver.reject(ctx, error_msg.toValue()); - return @constCast(resolver.getPromise().handle); - } orelse { - const error_msg = v8.String.initUtf8(iso, "Module source not available"); - _ = resolver.reject(ctx, error_msg.toValue()); - return @constCast(resolver.getPromise().handle); - }; - - var try_catch: TryCatch = undefined; - try_catch.init(context); - defer try_catch.deinit(); - - const module = compileModule(iso, source, specifier_str) catch { - log.err(.js, "module compilation failed", .{ - .specifier = specifier_str, - .exception = try_catch.exception(context.call_arena) catch "unknown error", - .stack = try_catch.stack(context.call_arena) catch null, - }); - const error_msg = if (try_catch.hasCaught()) blk: { - const exception_str = try_catch.exception(context.call_arena) catch "Compilation error"; - break :blk v8.String.initUtf8(iso, exception_str orelse "Compilation error"); - } else v8.String.initUtf8(iso, "Module compilation failed"); - - _ = resolver.reject(ctx, error_msg.toValue()); - return @constCast(resolver.getPromise().handle); - }; - - context.module_identifier.putNoClobber(context.context_arena, module.getIdentityHash(), normalized_specifier) catch unreachable; - context.module_cache.putNoClobber(context.context_arena, normalized_specifier, v8.Persistent(v8.Module).init(iso, module)) catch unreachable; - - const instantiated = module.instantiate(ctx, JsContext.resolveModuleCallback) catch { - log.err(.js, "module instantiation failed", .{ - .specifier = specifier_str, - .exception = try_catch.exception(context.call_arena) catch "unknown error", - .stack = try_catch.stack(context.call_arena) catch null, - }); - const error_msg = if (try_catch.hasCaught()) blk: { - const exception_str = try_catch.exception(context.call_arena) catch "Instantiation error"; - break :blk v8.String.initUtf8(iso, exception_str orelse "Instantiation error"); - } else v8.String.initUtf8(iso, "Module instantiation failed"); - - _ = resolver.reject(ctx, error_msg.toValue()); - return @constCast(resolver.getPromise().handle); - }; - - if (!instantiated) { - const error_msg = v8.String.initUtf8(iso, "Module did not instantiate"); - _ = resolver.reject(ctx, error_msg.toValue()); - return @constCast(resolver.getPromise().handle); - } - - const evaluated = module.evaluate(ctx) catch { - log.err(.js, "module evaluation failed", .{ - .specifier = specifier_str, - .exception = try_catch.exception(context.call_arena) catch "unknown error", - .stack = try_catch.stack(context.call_arena) catch null, - .line = try_catch.sourceLineNumber() orelse 0, - }); - const error_msg = if (try_catch.hasCaught()) blk: { - const exception_str = try_catch.exception(context.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 @constCast(resolver.getPromise().handle); - }; - - if (evaluated.isPromise()) { - const promise = v8.Promise{ .handle = evaluated.handle }; - - const EvaluationData = struct { - module: v8.Persistent(v8.Module), - resolver: v8.Persistent(v8.PromiseResolver), - }; - - const ev_data = context.context_arena.create(EvaluationData) catch unreachable; - ev_data.* = .{ - .module = v8.Persistent(v8.Module).init(iso, 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.warn(.js, "module then promise", .{ - .namespace = namespace, - }); - _ = 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 { - log.warn(.js, "module catch promise", .{}); - 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(); - _ = 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_str, - .line = try_catch.sourceLineNumber() orelse 0, - }); - const error_msg = v8.String.initUtf8(iso, "Evaluation is a promise"); - _ = resolver.reject(ctx, error_msg.toValue()); - return @constCast(resolver.getPromise().handle); - }; - } else { - const namespace = module.getModuleNamespace(); - _ = resolver.resolve(ctx, namespace); - } - - return @constCast(resolver.getPromise().handle); - } - }.callback); + isolate.setHostImportModuleDynamicallyCallback(JsContext.dynamicModuleCallback); isolate.enter(); errdefer isolate.exit(); @@ -1706,6 +1510,214 @@ 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.call_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.call_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 referrer_full_url = blk: { + var it = context.module_identifier.valueIterator(); + while (it.next()) |full_url| { + const last_slash = std.mem.lastIndexOfScalar(u8, full_url.*, '/') orelse 0; + const filename = full_url.*[last_slash + 1 ..]; + + const resource_clean = if (std.mem.startsWith(u8, resource_str, "./")) + resource_str[2..] + else + resource_str; + + if (std.mem.eql(u8, filename, resource_clean)) { + break :blk full_url.*; + } + } + + break :blk resource_str; + }; + + const normalized_specifier = @import("../url.zig").stitch( + context.context_arena, + specifier_str, + referrer_full_url, + .{ .alloc = .if_needed }, + ) catch unreachable; + + _dynamicModuleCallback(context, resource_str, normalized_specifier, &resolver); + return @constCast(resolver.getPromise().handle); + } + + fn _dynamicModuleCallback( + context: *JsContext, + resource: []const u8, + specifier: []const u8, + resolver: *const v8.PromiseResolver, + ) void { + const iso = context.isolate; + const ctx = context.v8_context; + + // Check module cache first. + if (context.module_cache.get(specifier)) |cached_module| { + const namespace = cached_module.castToModule().getModuleNamespace(); + _ = resolver.resolve(ctx, namespace); + return; + } + + log.info(.js, "dynamic import", .{ + .specifier = specifier, + .resource = resource, + .normalized = specifier, + }); + + const module_loader = context.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(context); + defer try_catch.deinit(); + + const new_module = compileModule(iso, source, specifier) catch { + log.err(.js, "module compilation failed", .{ + .specifier = specifier, + .exception = try_catch.exception(context.call_arena) catch "unknown error", + .stack = try_catch.stack(context.call_arena) catch null, + }); + const error_msg = if (try_catch.hasCaught()) blk: { + const exception_str = try_catch.exception(context.call_arena) catch "Compilation error"; + break :blk v8.String.initUtf8(iso, exception_str orelse "Compilation error"); + } else v8.String.initUtf8(iso, "Module compilation failed"); + + _ = resolver.reject(ctx, error_msg.toValue()); + return; + }; + + // Insert into Module Cache. + context.module_identifier.putNoClobber(context.context_arena, new_module.getIdentityHash(), specifier) catch unreachable; + context.module_cache.putNoClobber(context.context_arena, specifier, v8.Persistent(v8.Module).init(iso, new_module)) catch unreachable; + + const instantiated = new_module.instantiate(ctx, JsContext.resolveModuleCallback) catch { + log.err(.js, "module instantiation failed", .{ + .specifier = specifier, + .exception = try_catch.exception(context.call_arena) catch "unknown error", + .stack = try_catch.stack(context.call_arena) catch null, + }); + const error_msg = if (try_catch.hasCaught()) blk: { + const exception_str = try_catch.exception(context.call_arena) catch "Instantiation error"; + break :blk v8.String.initUtf8(iso, exception_str orelse "Instantiation error"); + } else v8.String.initUtf8(iso, "Module instantiation failed"); + + _ = resolver.reject(ctx, error_msg.toValue()); + return; + }; + + if (!instantiated) { + const error_msg = v8.String.initUtf8(iso, "Module did not instantiate"); + _ = resolver.reject(ctx, error_msg.toValue()); + return; + } + + const evaluated = new_module.evaluate(ctx) catch { + log.err(.js, "module evaluation failed", .{ + .specifier = specifier, + .exception = try_catch.exception(context.call_arena) catch "unknown error", + .stack = try_catch.stack(context.call_arena) catch null, + .line = try_catch.sourceLineNumber() orelse 0, + }); + const error_msg = if (try_catch.hasCaught()) blk: { + const exception_str = try_catch.exception(context.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; + }; + + if (evaluated.isPromise()) { + const promise = v8.Promise{ .handle = evaluated.handle }; + + const EvaluationData = struct { + module: v8.Persistent(v8.Module), + resolver: v8.Persistent(v8.PromiseResolver), + }; + + const ev_data = context.context_arena.create(EvaluationData) catch unreachable; + ev_data.* = .{ + .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.warn(.js, "module then promise", .{ + .namespace = namespace, + }); + _ = 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 { + log.warn(.js, "module catch promise", .{}); + 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(); + _ = 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 { + const namespace = new_module.getModuleNamespace(); + _ = resolver.resolve(ctx, namespace); + } + } }; pub const Function = struct { From dffd8b5fec73d562374f35213c615a1549e808be Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 10 Jul 2025 11:39:46 -0700 Subject: [PATCH 3/5] use module() for dynamic imports --- src/browser/page.zig | 15 +++-- src/runtime/js.zig | 139 +++++++++++++++---------------------------- 2 files changed, 59 insertions(+), 95 deletions(-) diff --git a/src/browser/page.zig b/src/browser/page.zig index fb64baaa..1b07e809 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 9881caa5..bb676820 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -762,17 +762,14 @@ 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) { + const value = self.module_cache.get(url); + if (value != null) return null; } errdefer _ = self.module_cache.remove(url); @@ -782,8 +779,9 @@ 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.put(arena, owned_url, PersistentModule.init(self.isolate, m)); + } // resolveModuleCallback loads module's dependencies. const v8_context = self.v8_context; @@ -791,21 +789,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 @@ -1564,32 +1553,32 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .{ .alloc = .if_needed }, ) catch unreachable; - _dynamicModuleCallback(context, resource_str, normalized_specifier, &resolver); + log.debug(.js, "dynamic import", .{ + .specifier = specifier_str, + .resource = resource_str, + .referrer_full = referrer_full_url, + .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( context: *JsContext, - resource: []const u8, specifier: []const u8, resolver: *const v8.PromiseResolver, - ) void { + ) !void { const iso = context.isolate; const ctx = context.v8_context; - // Check module cache first. - if (context.module_cache.get(specifier)) |cached_module| { - const namespace = cached_module.castToModule().getModuleNamespace(); - _ = resolver.resolve(ctx, namespace); - return; - } - - log.info(.js, "dynamic import", .{ - .specifier = specifier, - .resource = resource, - .normalized = specifier, - }); - const module_loader = context.module_loader; const source = module_loader.func(module_loader.ptr, specifier) catch { const error_msg = v8.String.initUtf8(iso, "Failed to load module"); @@ -1605,71 +1594,30 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { try_catch.init(context); defer try_catch.deinit(); - const new_module = compileModule(iso, source, specifier) catch { + const maybe_promise = context.module(source, specifier, true) catch { log.err(.js, "module compilation failed", .{ .specifier = specifier, .exception = try_catch.exception(context.call_arena) catch "unknown error", .stack = try_catch.stack(context.call_arena) catch null, - }); - const error_msg = if (try_catch.hasCaught()) blk: { - const exception_str = try_catch.exception(context.call_arena) catch "Compilation error"; - break :blk v8.String.initUtf8(iso, exception_str orelse "Compilation error"); - } else v8.String.initUtf8(iso, "Module compilation failed"); - - _ = resolver.reject(ctx, error_msg.toValue()); - return; - }; - - // Insert into Module Cache. - context.module_identifier.putNoClobber(context.context_arena, new_module.getIdentityHash(), specifier) catch unreachable; - context.module_cache.putNoClobber(context.context_arena, specifier, v8.Persistent(v8.Module).init(iso, new_module)) catch unreachable; - - const instantiated = new_module.instantiate(ctx, JsContext.resolveModuleCallback) catch { - log.err(.js, "module instantiation failed", .{ - .specifier = specifier, - .exception = try_catch.exception(context.call_arena) catch "unknown error", - .stack = try_catch.stack(context.call_arena) catch null, - }); - const error_msg = if (try_catch.hasCaught()) blk: { - const exception_str = try_catch.exception(context.call_arena) catch "Instantiation error"; - break :blk v8.String.initUtf8(iso, exception_str orelse "Instantiation error"); - } else v8.String.initUtf8(iso, "Module instantiation failed"); - - _ = resolver.reject(ctx, error_msg.toValue()); - return; - }; - - if (!instantiated) { - const error_msg = v8.String.initUtf8(iso, "Module did not instantiate"); - _ = resolver.reject(ctx, error_msg.toValue()); - return; - } - - const evaluated = new_module.evaluate(ctx) catch { - log.err(.js, "module evaluation failed", .{ - .specifier = specifier, - .exception = try_catch.exception(context.call_arena) catch "unknown error", - .stack = try_catch.stack(context.call_arena) catch null, .line = try_catch.sourceLineNumber() orelse 0, }); const error_msg = if (try_catch.hasCaught()) blk: { const exception_str = try_catch.exception(context.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 = context.module_cache.get(specifier).?.castToModule(); - if (evaluated.isPromise()) { - const promise = v8.Promise{ .handle = evaluated.handle }; - + if (maybe_promise) |promise| { + // This means we must wait for the evaluation. const EvaluationData = struct { module: v8.Persistent(v8.Module), resolver: v8.Persistent(v8.PromiseResolver), }; - const ev_data = context.context_arena.create(EvaluationData) catch unreachable; + const ev_data = try context.context_arena.create(EvaluationData); ev_data.* = .{ .module = v8.Persistent(v8.Module).init(iso, new_module), .resolver = v8.Persistent(v8.PromiseResolver).init(iso, resolver.*), @@ -1686,7 +1634,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const cb_resolver = data.resolver.castToPromiseResolver(); const namespace = cb_module.getModuleNamespace(); - log.warn(.js, "module then promise", .{ + log.info(.js, "dynamic import complete", .{ + .module = cb_module, .namespace = namespace, }); _ = cb_resolver.resolve(cb_context, namespace); @@ -1695,11 +1644,15 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const catch_callback = v8.Function.initWithData(ctx, struct { pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void { - log.warn(.js, "module catch promise", .{}); const cb_info = v8.FunctionCallbackInfo{ .handle = info.? }; const cb_context = cb_info.getIsolate().getCurrentContext(); const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue())); + const cb_module = data.module.castToModule(); const cb_resolver = data.resolver.castToPromiseResolver(); + + log.err(.js, "dynamic import failed", .{ + .module = cb_module, + }); _ = cb_resolver.reject(cb_context, cb_info.getData()); } }.callback, external); @@ -1714,8 +1667,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { 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; } } }; From 06e514cc2efd8501f07636c399f745f19f91c87f Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Fri, 11 Jul 2025 06:12:02 -0700 Subject: [PATCH 4/5] use resource_str for stitching url --- src/runtime/js.zig | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/runtime/js.zig b/src/runtime/js.zig index bb676820..bb4efd5b 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1515,48 +1515,28 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const resolver = v8.PromiseResolver.init(context.v8_context); const specifier: v8.String = .{ .handle = v8_specifier.? }; - const specifier_str = jsStringToZig(context.call_arena, specifier, iso) catch { + 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.call_arena, resource, iso) catch { + 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 referrer_full_url = blk: { - var it = context.module_identifier.valueIterator(); - while (it.next()) |full_url| { - const last_slash = std.mem.lastIndexOfScalar(u8, full_url.*, '/') orelse 0; - const filename = full_url.*[last_slash + 1 ..]; - - const resource_clean = if (std.mem.startsWith(u8, resource_str, "./")) - resource_str[2..] - else - resource_str; - - if (std.mem.eql(u8, filename, resource_clean)) { - break :blk full_url.*; - } - } - - break :blk resource_str; - }; - const normalized_specifier = @import("../url.zig").stitch( context.context_arena, specifier_str, - referrer_full_url, + resource_str, .{ .alloc = .if_needed }, ) catch unreachable; log.debug(.js, "dynamic import", .{ .specifier = specifier_str, .resource = resource_str, - .referrer_full = referrer_full_url, .normalized_specifier = normalized_specifier, }); From 287df42994f768c9c52cac861e55b5258a977377 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Fri, 11 Jul 2025 06:22:04 -0700 Subject: [PATCH 5/5] log module specifier on dynamic import stages --- src/runtime/js.zig | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/runtime/js.zig b/src/runtime/js.zig index bb4efd5b..4effa4b1 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -767,9 +767,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !?v8.Promise { const arena = self.context_arena; - if (cacheable) { - const value = self.module_cache.get(url); - if (value != null) return null; + if (cacheable and self.module_cache.contains(url)) { + return null; } errdefer _ = self.module_cache.remove(url); @@ -780,7 +779,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { errdefer _ = self.module_identifier.remove(m.getIdentityHash()); if (cacheable) { - try self.module_cache.put(arena, owned_url, PersistentModule.init(self.isolate, m)); + try self.module_cache.putNoClobber( + arena, + owned_url, + PersistentModule.init(self.isolate, m), + ); } // resolveModuleCallback loads module's dependencies. @@ -1552,14 +1555,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { } fn _dynamicModuleCallback( - context: *JsContext, + self: *JsContext, specifier: []const u8, resolver: *const v8.PromiseResolver, ) !void { - const iso = context.isolate; - const ctx = context.v8_context; + const iso = self.isolate; + const ctx = self.v8_context; - const module_loader = context.module_loader; + 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()); @@ -1571,34 +1574,36 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; var try_catch: TryCatch = undefined; - try_catch.init(context); + try_catch.init(self); defer try_catch.deinit(); - const maybe_promise = context.module(source, specifier, true) catch { + const maybe_promise = self.module(source, specifier, true) catch { log.err(.js, "module compilation failed", .{ .specifier = specifier, - .exception = try_catch.exception(context.call_arena) catch "unknown error", - .stack = try_catch.stack(context.call_arena) catch null, + .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(context.call_arena) catch "Evaluation error"; + 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 = context.module_cache.get(specifier).?.castToModule(); + 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 context.context_arena.create(EvaluationData); + 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.*), }; @@ -1614,10 +1619,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const cb_resolver = data.resolver.castToPromiseResolver(); const namespace = cb_module.getModuleNamespace(); - log.info(.js, "dynamic import complete", .{ - .module = cb_module, - .namespace = namespace, - }); + log.info(.js, "dynamic import complete", .{ .specifier = data.specifier }); _ = cb_resolver.resolve(cb_context, namespace); } }.callback, external); @@ -1627,12 +1629,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const cb_info = v8.FunctionCallbackInfo{ .handle = info.? }; const cb_context = cb_info.getIsolate().getCurrentContext(); const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue())); - const cb_module = data.module.castToModule(); const cb_resolver = data.resolver.castToPromiseResolver(); - log.err(.js, "dynamic import failed", .{ - .module = cb_module, - }); + log.err(.js, "dynamic import failed", .{ .specifier = data.specifier }); _ = cb_resolver.reject(cb_context, cb_info.getData()); } }.callback, external);