Skip to content

Commit f96fb50

Browse files
committed
add dynamic import callback to isolate
1 parent 0d23515 commit f96fb50

File tree

2 files changed

+248
-5
lines changed

2 files changed

+248
-5
lines changed

build.zig.zon

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
1313
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
1414
},
15-
.v8 = .{
16-
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/23718cdb99aaa45105d0a7e0ca22587bf77d3b5f.tar.gz",
17-
.hash = "v8-0.0.0-xddH68m2AwDf42Qp2Udz4wTMVH8p71si7yLlUoZkPeEz",
18-
},
19-
//.v8 = .{ .path = "../zig-v8-fork" },
15+
// .v8 = .{
16+
// .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/23718cdb99aaa45105d0a7e0ca22587bf77d3b5f.tar.gz",
17+
// .hash = "v8-0.0.0-xddH68m2AwDf42Qp2Udz4wTMVH8p71si7yLlUoZkPeEz",
18+
// },
19+
.v8 = .{ .path = "../zig-v8-fork" },
2020
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
2121
},
2222
}

src/runtime/js.zig

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,205 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
194194
var isolate = v8.Isolate.init(params);
195195
errdefer isolate.deinit();
196196

197+
// This is the callback that runs whenever a module is dynamically imported.
198+
isolate.setHostImportModuleDynamicallyCallback(struct {
199+
pub fn callback(
200+
v8_ctx: ?*const v8.c.Context,
201+
host_defined_options: ?*const v8.c.Data,
202+
resource_name: ?*const v8.c.Value,
203+
v8_specifier: ?*const v8.c.String,
204+
import_attrs: ?*const v8.c.FixedArray,
205+
) callconv(.c) ?*v8.c.Promise {
206+
_ = host_defined_options;
207+
_ = import_attrs;
208+
const ctx: v8.Context = .{ .handle = v8_ctx.? };
209+
const context: *JsContext = @ptrFromInt(ctx.getEmbedderData(1).castTo(v8.BigInt).getUint64());
210+
const iso = context.isolate;
211+
const resolver = v8.PromiseResolver.init(ctx);
212+
213+
const specifier: v8.String = .{ .handle = v8_specifier.? };
214+
const specifier_str = jsStringToZig(context.call_arena, specifier, iso) catch {
215+
const error_msg = v8.String.initUtf8(iso, "Failed to parse module specifier");
216+
_ = resolver.reject(ctx, error_msg.toValue());
217+
return @constCast(resolver.getPromise().handle);
218+
};
219+
const resource: v8.String = .{ .handle = resource_name.? };
220+
const resource_str = jsStringToZig(context.call_arena, resource, iso) catch {
221+
const error_msg = v8.String.initUtf8(iso, "Failed to parse module resource");
222+
_ = resolver.reject(ctx, error_msg.toValue());
223+
return @constCast(resolver.getPromise().handle);
224+
};
225+
226+
const referrer_full_url = blk: {
227+
// Search through module_identifier values to find matching resource
228+
var it = context.module_identifier.valueIterator();
229+
while (it.next()) |full_url| {
230+
// Extract just the filename from the full URL
231+
const last_slash = std.mem.lastIndexOfScalar(u8, full_url.*, '/') orelse 0;
232+
const filename = full_url.*[last_slash + 1 ..];
233+
234+
// Compare with our resource string (removing ./ prefix if present)
235+
const resource_clean = if (std.mem.startsWith(u8, resource_str, "./"))
236+
resource_str[2..]
237+
else
238+
resource_str;
239+
240+
if (std.mem.eql(u8, filename, resource_clean)) {
241+
break :blk full_url.*;
242+
}
243+
}
244+
245+
// Fallback - maybe it's already a full URL in some cases?
246+
break :blk resource_str;
247+
};
248+
249+
const normalized_specifier = @import("../url.zig").stitch(
250+
context.context_arena,
251+
specifier_str,
252+
referrer_full_url,
253+
.{ .alloc = .if_needed },
254+
) catch unreachable;
255+
256+
// TODO: we need to resolve the full URL here and normalize it.
257+
// That way we can pass the correct one in the module_loader.
258+
259+
log.info(.js, "dynamic import", .{
260+
.specifier = specifier_str,
261+
.resource = resource_str,
262+
.normalized = normalized_specifier,
263+
});
264+
265+
const module_loader = context.module_loader;
266+
const source = module_loader.func(module_loader.ptr, normalized_specifier) catch {
267+
const error_msg = v8.String.initUtf8(iso, "Failed to load module");
268+
_ = resolver.reject(ctx, error_msg.toValue());
269+
return @constCast(resolver.getPromise().handle);
270+
} orelse {
271+
const error_msg = v8.String.initUtf8(iso, "Module source not available");
272+
_ = resolver.reject(ctx, error_msg.toValue());
273+
return @constCast(resolver.getPromise().handle);
274+
};
275+
276+
var try_catch: TryCatch = undefined;
277+
try_catch.init(context);
278+
defer try_catch.deinit();
279+
280+
const module = compileModule(iso, source, specifier_str) catch {
281+
log.err(.js, "module compilation failed", .{
282+
.specifier = specifier_str,
283+
.exception = try_catch.exception(context.call_arena) catch "unknown error",
284+
.stack = try_catch.stack(context.call_arena) catch null,
285+
});
286+
const error_msg = if (try_catch.hasCaught()) blk: {
287+
const exception_str = try_catch.exception(context.call_arena) catch "Compilation error";
288+
break :blk v8.String.initUtf8(iso, exception_str orelse "Compilation error");
289+
} else v8.String.initUtf8(iso, "Module compilation failed");
290+
291+
_ = resolver.reject(ctx, error_msg.toValue());
292+
return @constCast(resolver.getPromise().handle);
293+
};
294+
295+
context.module_identifier.putNoClobber(context.context_arena, module.getIdentityHash(), normalized_specifier) catch unreachable;
296+
context.module_cache.putNoClobber(context.context_arena, normalized_specifier, v8.Persistent(v8.Module).init(iso, module)) catch unreachable;
297+
298+
const instantiated = module.instantiate(ctx, JsContext.resolveModuleCallback) catch {
299+
log.err(.js, "module instantiation failed", .{
300+
.specifier = specifier_str,
301+
.exception = try_catch.exception(context.call_arena) catch "unknown error",
302+
.stack = try_catch.stack(context.call_arena) catch null,
303+
});
304+
const error_msg = if (try_catch.hasCaught()) blk: {
305+
const exception_str = try_catch.exception(context.call_arena) catch "Instantiation error";
306+
break :blk v8.String.initUtf8(iso, exception_str orelse "Instantiation error");
307+
} else v8.String.initUtf8(iso, "Module instantiation failed");
308+
309+
_ = resolver.reject(ctx, error_msg.toValue());
310+
return @constCast(resolver.getPromise().handle);
311+
};
312+
313+
if (!instantiated) {
314+
const error_msg = v8.String.initUtf8(iso, "Module did not instantiate");
315+
_ = resolver.reject(ctx, error_msg.toValue());
316+
return @constCast(resolver.getPromise().handle);
317+
}
318+
319+
const evaluated = module.evaluate(ctx) catch {
320+
log.err(.js, "module evaluation failed", .{
321+
.specifier = specifier_str,
322+
.exception = try_catch.exception(context.call_arena) catch "unknown error",
323+
.stack = try_catch.stack(context.call_arena) catch null,
324+
.line = try_catch.sourceLineNumber() orelse 0,
325+
});
326+
const error_msg = if (try_catch.hasCaught()) blk: {
327+
const exception_str = try_catch.exception(context.call_arena) catch "Evaluation error";
328+
break :blk v8.String.initUtf8(iso, exception_str orelse "Evaluation error");
329+
} else v8.String.initUtf8(iso, "Module evaluation failed");
330+
331+
_ = resolver.reject(ctx, error_msg.toValue());
332+
return @constCast(resolver.getPromise().handle);
333+
};
334+
335+
if (evaluated.isPromise()) {
336+
const promise = v8.Promise{ .handle = evaluated.handle };
337+
338+
const EvaluationData = struct {
339+
module: v8.Persistent(v8.Module),
340+
resolver: v8.Persistent(v8.PromiseResolver),
341+
};
342+
343+
const ev_data = context.context_arena.create(EvaluationData) catch unreachable;
344+
ev_data.* = .{
345+
.module = v8.Persistent(v8.Module).init(iso, module),
346+
.resolver = v8.Persistent(v8.PromiseResolver).init(iso, resolver),
347+
};
348+
const external = v8.External.init(iso, @ptrCast(ev_data));
349+
350+
const then_callback = v8.Function.initWithData(ctx, struct {
351+
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
352+
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
353+
const cb_isolate = cb_info.getIsolate();
354+
const cb_context = cb_isolate.getCurrentContext();
355+
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
356+
const cb_module = data.module.castToModule();
357+
const cb_resolver = data.resolver.castToPromiseResolver();
358+
359+
const namespace = cb_module.getModuleNamespace();
360+
log.warn(.js, "module then promise", .{
361+
.namespace = namespace,
362+
});
363+
_ = cb_resolver.resolve(cb_context, namespace);
364+
}
365+
}.callback, external);
366+
367+
const catch_callback = v8.Function.initWithData(ctx, struct {
368+
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
369+
log.warn(.js, "module catch promise", .{});
370+
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
371+
const cb_context = cb_info.getIsolate().getCurrentContext();
372+
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
373+
const cb_resolver = data.resolver.castToPromiseResolver();
374+
_ = cb_resolver.reject(cb_context, cb_info.getData());
375+
}
376+
}.callback, external);
377+
378+
_ = promise.thenAndCatch(ctx, then_callback, catch_callback) catch {
379+
log.err(.js, "module evaluation is promise", .{
380+
.specifier = specifier_str,
381+
.line = try_catch.sourceLineNumber() orelse 0,
382+
});
383+
const error_msg = v8.String.initUtf8(iso, "Evaluation is a promise");
384+
_ = resolver.reject(ctx, error_msg.toValue());
385+
return @constCast(resolver.getPromise().handle);
386+
};
387+
} else {
388+
const namespace = module.getModuleNamespace();
389+
_ = resolver.resolve(ctx, namespace);
390+
}
391+
392+
return @constCast(resolver.getPromise().handle);
393+
}
394+
}.callback);
395+
197396
isolate.enter();
198397
errdefer isolate.exit();
199398

@@ -1355,6 +1554,50 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
13551554
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_specifier);
13561555
return m.handle;
13571556
}
1557+
1558+
fn _resolveModuleCallback2(
1559+
self: *JsContext,
1560+
base_url: []const u8,
1561+
specifier: []const u8,
1562+
) !?*const v8.C_Module {
1563+
const normalized_specifier = try @import("../url.zig").stitch(
1564+
self.call_arena,
1565+
specifier,
1566+
base_url,
1567+
.{ .alloc = .if_needed },
1568+
);
1569+
1570+
if (self.module_cache.get(normalized_specifier)) |pm| {
1571+
return pm.handle;
1572+
}
1573+
1574+
const module_loader = self.module_loader;
1575+
const source = try module_loader.func(module_loader.ptr, normalized_specifier) orelse return null;
1576+
1577+
var try_catch: TryCatch = undefined;
1578+
try_catch.init(self);
1579+
defer try_catch.deinit();
1580+
1581+
const m = compileModule(self.isolate, source, specifier) catch |err| {
1582+
log.warn(.js, "compile resolved module", .{
1583+
.specifier = specifier,
1584+
.stack = try_catch.stack(self.call_arena) catch null,
1585+
.src = try_catch.sourceLine(self.call_arena) catch "err",
1586+
.line = try_catch.sourceLineNumber() orelse 0,
1587+
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
1588+
});
1589+
return null;
1590+
};
1591+
1592+
// We were hoping to find the module in our cache, and thus used
1593+
// the short-lived call_arena to create the normalized_specifier.
1594+
// But now this'll live for the lifetime of the context.
1595+
const arena = self.context_arena;
1596+
const owned_specifier = try arena.dupe(u8, normalized_specifier);
1597+
try self.module_cache.put(arena, owned_specifier, PersistentModule.init(self.isolate, m));
1598+
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_specifier);
1599+
return m.handle;
1600+
}
13581601
};
13591602

13601603
pub const Function = struct {

0 commit comments

Comments
 (0)