@@ -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