@@ -195,6 +195,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
195195 var isolate = v8 .Isolate .init (params );
196196 errdefer isolate .deinit ();
197197
198+ // This is the callback that runs whenever a module is dynamically imported.
199+ isolate .setHostImportModuleDynamicallyCallback (JsContext .dynamicModuleCallback );
200+
198201 isolate .enter ();
199202 errdefer isolate .exit ();
200203
@@ -759,17 +762,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
759762 }
760763
761764 // compile and eval a JS module
762- // It doesn't wait for callbacks execution
763- pub fn module (self : * JsContext , src : []const u8 , url : []const u8 , cacheable : bool ) ! void {
764- if (! cacheable ) {
765- return self .moduleNoCache (src , url );
766- }
767-
765+ // It returns null if the module is already compiled and in the cache.
766+ // It returns a v8.Promise if the module must be evaluated.
767+ pub fn module (self : * JsContext , src : []const u8 , url : []const u8 , cacheable : bool ) ! ? v8.Promise {
768768 const arena = self .context_arena ;
769769
770- const gop = try self .module_cache .getOrPut (arena , url );
771- if (gop .found_existing ) {
772- return ;
770+ if (cacheable and self .module_cache .contains (url )) {
771+ return null ;
773772 }
774773 errdefer _ = self .module_cache .remove (url );
775774
@@ -779,30 +778,26 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
779778 try self .module_identifier .putNoClobber (arena , m .getIdentityHash (), owned_url );
780779 errdefer _ = self .module_identifier .remove (m .getIdentityHash ());
781780
782- gop .key_ptr .* = owned_url ;
783- gop .value_ptr .* = PersistentModule .init (self .isolate , m );
781+ if (cacheable ) {
782+ try self .module_cache .putNoClobber (
783+ arena ,
784+ owned_url ,
785+ PersistentModule .init (self .isolate , m ),
786+ );
787+ }
784788
785789 // resolveModuleCallback loads module's dependencies.
786790 const v8_context = self .v8_context ;
787791 if (try m .instantiate (v8_context , resolveModuleCallback ) == false ) {
788792 return error .ModuleInstantiationError ;
789793 }
790794
791- _ = try m .evaluate (v8_context );
792- }
793-
794- fn moduleNoCache (self : * JsContext , src : []const u8 , url : []const u8 ) ! void {
795- const m = try compileModule (self .isolate , src , url );
796-
797- const arena = self .context_arena ;
798- const owned_url = try arena .dupe (u8 , url );
799- try self .module_identifier .putNoClobber (arena , m .getIdentityHash (), owned_url );
800-
801- const v8_context = self .v8_context ;
802- if (try m .instantiate (v8_context , resolveModuleCallback ) == false ) {
803- return error .ModuleInstantiationError ;
804- }
805- _ = try m .evaluate (v8_context );
795+ const evaluated = try m .evaluate (v8_context );
796+ // https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
797+ // Must be a promise that gets returned here.
798+ std .debug .assert (evaluated .isPromise ());
799+ const promise = v8.Promise { .handle = evaluated .handle };
800+ return promise ;
806801 }
807802
808803 // Wrap a v8.Exception
@@ -1514,6 +1509,160 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
15141509 type_index = prototype_index ;
15151510 }
15161511 }
1512+
1513+ pub fn dynamicModuleCallback (
1514+ v8_ctx : ? * const v8.c.Context ,
1515+ host_defined_options : ? * const v8.c.Data ,
1516+ resource_name : ? * const v8.c.Value ,
1517+ v8_specifier : ? * const v8.c.String ,
1518+ import_attrs : ? * const v8.c.FixedArray ,
1519+ ) callconv (.c ) ? * v8.c.Promise {
1520+ _ = host_defined_options ;
1521+ _ = import_attrs ;
1522+ const ctx : v8.Context = .{ .handle = v8_ctx .? };
1523+ const context : * JsContext = @ptrFromInt (ctx .getEmbedderData (1 ).castTo (v8 .BigInt ).getUint64 ());
1524+ const iso = context .isolate ;
1525+ const resolver = v8 .PromiseResolver .init (context .v8_context );
1526+
1527+ const specifier : v8.String = .{ .handle = v8_specifier .? };
1528+ const specifier_str = jsStringToZig (context .context_arena , specifier , iso ) catch {
1529+ const error_msg = v8 .String .initUtf8 (iso , "Failed to parse module specifier" );
1530+ _ = resolver .reject (ctx , error_msg .toValue ());
1531+ return @constCast (resolver .getPromise ().handle );
1532+ };
1533+ const resource : v8.String = .{ .handle = resource_name .? };
1534+ const resource_str = jsStringToZig (context .context_arena , resource , iso ) catch {
1535+ const error_msg = v8 .String .initUtf8 (iso , "Failed to parse module resource" );
1536+ _ = resolver .reject (ctx , error_msg .toValue ());
1537+ return @constCast (resolver .getPromise ().handle );
1538+ };
1539+
1540+ const normalized_specifier = @import ("../url.zig" ).stitch (
1541+ context .context_arena ,
1542+ specifier_str ,
1543+ resource_str ,
1544+ .{ .alloc = .if_needed },
1545+ ) catch unreachable ;
1546+
1547+ log .debug (.js , "dynamic import" , .{
1548+ .specifier = specifier_str ,
1549+ .resource = resource_str ,
1550+ .normalized_specifier = normalized_specifier ,
1551+ });
1552+
1553+ _dynamicModuleCallback (context , normalized_specifier , & resolver ) catch | err | {
1554+ log .err (.js , "dynamic module callback" , .{
1555+ .err = err ,
1556+ });
1557+ // Must be rejected at this point
1558+ // otherwise, we will just wait on a pending promise.
1559+ std .debug .assert (resolver .getPromise ().getState () == .kRejected );
1560+ };
1561+ return @constCast (resolver .getPromise ().handle );
1562+ }
1563+
1564+ fn _dynamicModuleCallback (
1565+ self : * JsContext ,
1566+ specifier : []const u8 ,
1567+ resolver : * const v8.PromiseResolver ,
1568+ ) ! void {
1569+ const iso = self .isolate ;
1570+ const ctx = self .v8_context ;
1571+
1572+ const module_loader = self .module_loader ;
1573+ const source = module_loader .func (module_loader .ptr , specifier ) catch {
1574+ const error_msg = v8 .String .initUtf8 (iso , "Failed to load module" );
1575+ _ = resolver .reject (ctx , error_msg .toValue ());
1576+ return ;
1577+ } orelse {
1578+ const error_msg = v8 .String .initUtf8 (iso , "Module source not available" );
1579+ _ = resolver .reject (ctx , error_msg .toValue ());
1580+ return ;
1581+ };
1582+
1583+ var try_catch : TryCatch = undefined ;
1584+ try_catch .init (self );
1585+ defer try_catch .deinit ();
1586+
1587+ const maybe_promise = self .module (source , specifier , true ) catch {
1588+ log .err (.js , "module compilation failed" , .{
1589+ .specifier = specifier ,
1590+ .exception = try_catch .exception (self .call_arena ) catch "unknown error" ,
1591+ .stack = try_catch .stack (self .call_arena ) catch null ,
1592+ .line = try_catch .sourceLineNumber () orelse 0 ,
1593+ });
1594+ const error_msg = if (try_catch .hasCaught ()) blk : {
1595+ const exception_str = try_catch .exception (self .call_arena ) catch "Evaluation error" ;
1596+ break :blk v8 .String .initUtf8 (iso , exception_str orelse "Evaluation error" );
1597+ } else v8 .String .initUtf8 (iso , "Module evaluation failed" );
1598+ _ = resolver .reject (ctx , error_msg .toValue ());
1599+ return ;
1600+ };
1601+ const new_module = self .module_cache .get (specifier ).? .castToModule ();
1602+
1603+ if (maybe_promise ) | promise | {
1604+ // This means we must wait for the evaluation.
1605+ const EvaluationData = struct {
1606+ specifier : []const u8 ,
1607+ module : v8 .Persistent (v8 .Module ),
1608+ resolver : v8 .Persistent (v8 .PromiseResolver ),
1609+ };
1610+
1611+ const ev_data = try self .context_arena .create (EvaluationData );
1612+ ev_data .* = .{
1613+ .specifier = specifier ,
1614+ .module = v8 .Persistent (v8 .Module ).init (iso , new_module ),
1615+ .resolver = v8 .Persistent (v8 .PromiseResolver ).init (iso , resolver .* ),
1616+ };
1617+ const external = v8 .External .init (iso , @ptrCast (ev_data ));
1618+
1619+ const then_callback = v8 .Function .initWithData (ctx , struct {
1620+ pub fn callback (info : ? * const v8.c.FunctionCallbackInfo ) callconv (.c ) void {
1621+ const cb_info = v8.FunctionCallbackInfo { .handle = info .? };
1622+ const cb_isolate = cb_info .getIsolate ();
1623+ const cb_context = cb_isolate .getCurrentContext ();
1624+ const data : * EvaluationData = @ptrCast (@alignCast (cb_info .getExternalValue ()));
1625+ const cb_module = data .module .castToModule ();
1626+ const cb_resolver = data .resolver .castToPromiseResolver ();
1627+
1628+ const namespace = cb_module .getModuleNamespace ();
1629+ log .info (.js , "dynamic import complete" , .{ .specifier = data .specifier });
1630+ _ = cb_resolver .resolve (cb_context , namespace );
1631+ }
1632+ }.callback , external );
1633+
1634+ const catch_callback = v8 .Function .initWithData (ctx , struct {
1635+ pub fn callback (info : ? * const v8.c.FunctionCallbackInfo ) callconv (.c ) void {
1636+ const cb_info = v8.FunctionCallbackInfo { .handle = info .? };
1637+ const cb_context = cb_info .getIsolate ().getCurrentContext ();
1638+ const data : * EvaluationData = @ptrCast (@alignCast (cb_info .getExternalValue ()));
1639+ const cb_resolver = data .resolver .castToPromiseResolver ();
1640+
1641+ log .err (.js , "dynamic import failed" , .{ .specifier = data .specifier });
1642+ _ = cb_resolver .reject (cb_context , cb_info .getData ());
1643+ }
1644+ }.callback , external );
1645+
1646+ _ = promise .thenAndCatch (ctx , then_callback , catch_callback ) catch {
1647+ log .err (.js , "module evaluation is promise" , .{
1648+ .specifier = specifier ,
1649+ .line = try_catch .sourceLineNumber () orelse 0 ,
1650+ });
1651+ const error_msg = v8 .String .initUtf8 (iso , "Evaluation is a promise" );
1652+ _ = resolver .reject (ctx , error_msg .toValue ());
1653+ return ;
1654+ };
1655+ } else {
1656+ // This means it is already present in the cache.
1657+ const namespace = new_module .getModuleNamespace ();
1658+ log .info (.js , "dynamic import complete" , .{
1659+ .module = new_module ,
1660+ .namespace = namespace ,
1661+ });
1662+ _ = resolver .resolve (ctx , namespace );
1663+ return ;
1664+ }
1665+ }
15171666 };
15181667
15191668 pub const Function = struct {
0 commit comments