@@ -57,11 +57,6 @@ import type {Context} from './types';
5757
5858export type { Context } from './types' ;
5959
60- interface EsmModuleCache {
61- beforeEvaluation : Promise < VMModule > ;
62- fullyEvaluated : Promise < VMModule > ;
63- }
64-
6560const esmIsAvailable = typeof SourceTextModule === 'function' ;
6661
6762interface JestGlobals extends Global . TestFrameworkGlobals {
@@ -176,8 +171,9 @@ export default class Runtime {
176171 private readonly _moduleMocker : ModuleMocker ;
177172 private _isolatedModuleRegistry : ModuleRegistry | null ;
178173 private _moduleRegistry : ModuleRegistry ;
179- private readonly _esmoduleRegistry : Map < Config . Path , EsmModuleCache > ;
174+ private readonly _esmoduleRegistry : Map < Config . Path , VMModule > ;
180175 private readonly _cjsNamedExports : Map < Config . Path , Set < string > > ;
176+ private readonly _esmModuleLinkingMap : WeakMap < VMModule , Promise < unknown > > ;
181177 private readonly _testPath : Config . Path ;
182178 private readonly _resolver : Resolver ;
183179 private _shouldAutoMock : boolean ;
@@ -227,6 +223,7 @@ export default class Runtime {
227223 this . _moduleRegistry = new Map ( ) ;
228224 this . _esmoduleRegistry = new Map ( ) ;
229225 this . _cjsNamedExports = new Map ( ) ;
226+ this . _esmModuleLinkingMap = new WeakMap ( ) ;
230227 this . _testPath = testPath ;
231228 this . _resolver = resolver ;
232229 this . _scriptTransformer = new ScriptTransformer ( config , this . _cacheFS ) ;
@@ -368,10 +365,10 @@ export default class Runtime {
368365 ) ;
369366 }
370367
368+ // not async _now_, but transform will be
371369 private async loadEsmModule (
372370 modulePath : Config . Path ,
373371 query = '' ,
374- isStaticImport = false ,
375372 ) : Promise < VMModule > {
376373 const cacheKey = modulePath + query ;
377374
@@ -383,14 +380,11 @@ export default class Runtime {
383380
384381 const context = this . _environment . getVmContext ( ) ;
385382
386- invariant ( context ) ;
383+ invariant ( context , 'Test environment has been torn down' ) ;
387384
388385 if ( this . _resolver . isCoreModule ( modulePath ) ) {
389386 const core = this . _importCoreModule ( modulePath , context ) ;
390- this . _esmoduleRegistry . set ( cacheKey , {
391- beforeEvaluation : core ,
392- fullyEvaluated : core ,
393- } ) ;
387+ this . _esmoduleRegistry . set ( cacheKey , core ) ;
394388 return core ;
395389 }
396390
@@ -405,89 +399,49 @@ export default class Runtime {
405399 const module = new SourceTextModule ( transformedCode , {
406400 context,
407401 identifier : modulePath ,
408- importModuleDynamically : (
402+ importModuleDynamically : async (
409403 specifier : string ,
410404 referencingModule : VMModule ,
411- ) =>
412- this . linkModules (
405+ ) => {
406+ const module = await this . resolveModule (
413407 specifier ,
414408 referencingModule . identifier ,
415409 referencingModule . context ,
416- false ,
417- ) ,
410+ ) ;
411+
412+ return this . linkAndEvaluateModule ( module ) ;
413+ } ,
418414 initializeImportMeta ( meta : ImportMeta ) {
419415 meta . url = pathToFileURL ( modulePath ) . href ;
420416 } ,
421417 } ) ;
422418
423- let resolve : ( value : VMModule ) => void ;
424- let reject : ( value : any ) => void ;
425- const promise = new Promise < VMModule > ( ( _resolve , _reject ) => {
426- resolve = _resolve ;
427- reject = _reject ;
428- } ) ;
429-
430- // add to registry before link so that circular import won't end up stack overflow
431- this . _esmoduleRegistry . set (
432- cacheKey ,
433- // we wanna put the linking promise in the cache so modules loaded in
434- // parallel can all await it. We then await it synchronously below, so
435- // we shouldn't get any unhandled rejections
436- {
437- beforeEvaluation : Promise . resolve ( module ) ,
438- fullyEvaluated : promise ,
439- } ,
440- ) ;
441-
442- module
443- . link ( ( specifier : string , referencingModule : VMModule ) =>
444- this . linkModules (
445- specifier ,
446- referencingModule . identifier ,
447- referencingModule . context ,
448- true ,
449- ) ,
450- )
451- . then ( ( ) => module . evaluate ( ) )
452- . then (
453- ( ) => resolve ( module ) ,
454- ( e : any ) => reject ( e ) ,
455- ) ;
419+ this . _esmoduleRegistry . set ( cacheKey , module ) ;
456420 }
457421
458- const entry = this . _esmoduleRegistry . get ( cacheKey ) ;
422+ const module = this . _esmoduleRegistry . get ( cacheKey ) ;
459423
460- // return the already resolved, pre-evaluation promise
461- // is loaded through static import to prevent promise deadlock
462- // because module is evaluated after all static import is resolved
463- const module = isStaticImport
464- ? entry ?. beforeEvaluation
465- : entry ?. fullyEvaluated ;
466-
467- invariant ( module ) ;
424+ invariant (
425+ module ,
426+ 'Module cache does not contain module. This is a bug in Jest, please open up an issue' ,
427+ ) ;
468428
469429 return module ;
470430 }
471431
472- private linkModules (
432+ private resolveModule (
473433 specifier : string ,
474434 referencingIdentifier : string ,
475435 context : VMContext ,
476- isStaticImport : boolean ,
477436 ) {
478437 if ( specifier === '@jest/globals' ) {
479438 const fromCache = this . _esmoduleRegistry . get ( '@jest/globals' ) ;
480439
481440 if ( fromCache ) {
482- return isStaticImport
483- ? fromCache . beforeEvaluation
484- : fromCache . fullyEvaluated ;
441+ return fromCache ;
485442 }
486443 const globals = this . getGlobalsForEsm ( referencingIdentifier , context ) ;
487- this . _esmoduleRegistry . set ( '@jest/globals' , {
488- beforeEvaluation : globals ,
489- fullyEvaluated : globals ,
490- } ) ;
444+ this . _esmoduleRegistry . set ( '@jest/globals' , globals ) ;
491445
492446 return globals ;
493447 }
@@ -504,12 +458,37 @@ export default class Runtime {
504458 this . _resolver . isCoreModule ( resolved ) ||
505459 this . unstable_shouldLoadAsEsm ( resolved )
506460 ) {
507- return this . loadEsmModule ( resolved , query , isStaticImport ) ;
461+ return this . loadEsmModule ( resolved , query ) ;
508462 }
509463
510464 return this . loadCjsAsEsm ( referencingIdentifier , resolved , context ) ;
511465 }
512466
467+ private async linkAndEvaluateModule ( module : VMModule ) {
468+ if ( module . status === 'unlinked' ) {
469+ // since we might attempt to link the same module in parallel, stick the promise in a weak map so every call to
470+ // this method can await it
471+ this . _esmModuleLinkingMap . set (
472+ module ,
473+ module . link ( ( specifier : string , referencingModule : VMModule ) =>
474+ this . resolveModule (
475+ specifier ,
476+ referencingModule . identifier ,
477+ referencingModule . context ,
478+ ) ,
479+ ) ,
480+ ) ;
481+ }
482+
483+ await this . _esmModuleLinkingMap . get ( module ) ;
484+
485+ if ( module . status === 'linked' ) {
486+ await module . evaluate ( ) ;
487+ }
488+
489+ return module ;
490+ }
491+
513492 async unstable_importModule (
514493 from : Config . Path ,
515494 moduleName ?: string ,
@@ -523,7 +502,9 @@ export default class Runtime {
523502
524503 const modulePath = this . _resolveModule ( from , path ) ;
525504
526- return this . loadEsmModule ( modulePath , query ) ;
505+ const module = await this . loadEsmModule ( modulePath , query ) ;
506+
507+ return this . linkAndEvaluateModule ( module ) ;
527508 }
528509
529510 private loadCjsAsEsm (
@@ -1227,12 +1208,18 @@ export default class Runtime {
12271208 displayErrors : true ,
12281209 filename : scriptFilename ,
12291210 // @ts -expect-error: Experimental ESM API
1230- importModuleDynamically : ( specifier : string ) => {
1211+ importModuleDynamically : async ( specifier : string ) => {
12311212 const context = this . _environment . getVmContext ?.( ) ;
12321213
1233- invariant ( context ) ;
1214+ invariant ( context , 'Test environment has been torn down' ) ;
1215+
1216+ const module = await this . resolveModule (
1217+ specifier ,
1218+ scriptFilename ,
1219+ context ,
1220+ ) ;
12341221
1235- return this . linkModules ( specifier , scriptFilename , context , false ) ;
1222+ return this . linkAndEvaluateModule ( module ) ;
12361223 } ,
12371224 } ) ;
12381225 } catch ( e ) {
0 commit comments