@@ -63,6 +63,12 @@ interface VMModuleWithAsyncGraph extends VMModule {
6363// loader on any unsupported edge).
6464export type SyncEsmMode = 'sync-preferred' | 'sync-required' ;
6565
66+ // Returned by sync-graph methods when a dependency or condition prevents
67+ // synchronous loading. Callers propagate it upward; the top-level
68+ // `tryLoadGraphSync` caller falls back to the legacy async path.
69+ export const LOAD_ASYNC = 'load-async' as const ;
70+ type LoadAsync = typeof LOAD_ASYNC ;
71+
6672type WorklistEntry = {
6773 cacheKey : string ;
6874 modulePath : string ;
@@ -305,17 +311,17 @@ export class EsmLoader {
305311 this . testState = options . testState ;
306312 }
307313
308- // A `null` here means the legacy async path is mid-flight on this same
309- // module (registry holds a Promise from a concurrent `await import()`);
310- // surface as ERR_REQUIRE_ESM with actionable context.
314+ // `'load-async'` means the sync graph could not be completed — a concurrent
315+ // `await import()` is mid-flight, a dependency is async-only, etc. Surface
316+ // as ERR_REQUIRE_ESM with actionable context.
311317 //
312318 // Root-level mocks (`jest.unstable_mockModule(spec)` then `require(spec)`)
313319 // are not consulted - driving a SyntheticModule from `unlinked` to
314320 // `evaluated` needs the async link()/evaluate() pair. Transitive-dep mocks
315321 // still apply via the graph walker.
316322 requireEsmModule < T > ( modulePath : string ) : T {
317323 const module = this . tryLoadGraphSync ( modulePath , '' , 'sync-required' ) ;
318- if ( ! module ) {
324+ if ( module === LOAD_ASYNC ) {
319325 const error : NodeJS . ErrnoException = new Error (
320326 `Cannot require() ES Module ${ modulePath } synchronously: it is currently being loaded by a concurrent \`import()\`. Await that import before calling require(), or import this module instead of requiring it.` ,
321327 ) ;
@@ -332,34 +338,30 @@ export class EsmLoader {
332338 rootPath : string ,
333339 rootQuery : string ,
334340 mode : SyncEsmMode ,
335- ) : ESModule | null {
336- if (
337- this . testState . bailIfTornDown (
338- 'You are trying to `import` a file after the Jest environment has been torn down.' ,
339- )
340- ) {
341- return null ;
342- }
341+ ) : ESModule | LoadAsync {
342+ this . testState . throwIfTornDown (
343+ 'You are trying to `import` a file after the Jest environment has been torn down.' ,
344+ ) ;
343345
344346 const registry = this . registries . getActiveEsmRegistry ( ) ;
345347 const rootKey = rootPath + rootQuery ;
346348
347349 const cached = registry . get ( rootKey ) ;
348350 if ( cached ) {
349- if ( cached instanceof Promise ) return null ;
351+ if ( cached instanceof Promise ) return LOAD_ASYNC ;
350352 // The legacy `loadEsmModule` source-text branch does `registry.set`
351353 // while the `SourceTextModule` is still `'unlinked'` (link runs later
352354 // in `linkAndEvaluateModule`); accessing `.namespace` on a non-evaluated
353355 // module throws `ERR_VM_MODULE_STATUS`. Surface settled entries
354356 // (`'evaluated'` / `'errored'`); bail otherwise.
355357 if ( cached . status === 'evaluated' ) return cached as ESModule ;
356358 if ( cached . status === 'errored' ) throw cached . error ;
357- return null ;
359+ return LOAD_ASYNC ;
358360 }
359361
360362 const context = this . getContext ( ) ;
361363
362- if ( this . transformCache . hasMutex ( rootKey ) ) return null ;
364+ if ( this . transformCache . hasMutex ( rootKey ) ) return LOAD_ASYNC ;
363365
364366 const scratch = new Map < string , ScratchEntry > ( ) ;
365367 const worklist : Array < WorklistEntry > = [
@@ -376,18 +378,18 @@ export class EsmLoader {
376378 // module into the parent's `linkRequests` would fail Node's link
377379 // cascade; plugging a `'linked'` one would skip its body. Bail.
378380 const fromRegistry = registry . get ( cacheKey ) ;
379- if ( fromRegistry instanceof Promise ) return null ;
381+ if ( fromRegistry instanceof Promise ) return LOAD_ASYNC ;
380382 if ( fromRegistry ) {
381383 if ( fromRegistry . status === 'errored' ) throw fromRegistry . error ;
382- if ( fromRegistry . status !== 'evaluated' ) return null ;
384+ if ( fromRegistry . status !== 'evaluated' ) return LOAD_ASYNC ;
383385 scratch . set ( cacheKey , {
384386 cacheKey,
385387 kind : 'synthetic' ,
386388 module : fromRegistry ,
387389 } ) ;
388390 continue ;
389391 }
390- if ( this . transformCache . hasMutex ( cacheKey ) ) return null ;
392+ if ( this . transformCache . hasMutex ( cacheKey ) ) return LOAD_ASYNC ;
391393
392394 if ( this . resolution . isCoreModule ( modulePath ) ) {
393395 scratch . set ( cacheKey , {
@@ -412,7 +414,7 @@ export class EsmLoader {
412414 worklist ,
413415 mode ,
414416 ) ;
415- if ( built === null ) return null ;
417+ if ( built === LOAD_ASYNC ) return LOAD_ASYNC ;
416418 scratch . set ( cacheKey , built ) ;
417419 continue ;
418420 }
@@ -428,7 +430,7 @@ export class EsmLoader {
428430 worklist ,
429431 mode ,
430432 ) ;
431- if ( wasmEntry === null ) return null ;
433+ if ( wasmEntry === LOAD_ASYNC ) return LOAD_ASYNC ;
432434 scratch . set ( cacheKey , wasmEntry ) ;
433435 continue ;
434436 }
@@ -440,7 +442,7 @@ export class EsmLoader {
440442 'a configured transformer is async-only' ,
441443 ) ;
442444 }
443- return null ;
445+ return LOAD_ASYNC ;
444446 }
445447
446448 if ( modulePath . endsWith ( '.json' ) ) {
@@ -493,7 +495,7 @@ export class EsmLoader {
493495 if ( mode === 'sync-required' ) {
494496 throw makeRequireAsyncError ( modulePath , 'top-level await' ) ;
495497 }
496- return null ;
498+ return LOAD_ASYNC ;
497499 }
498500
499501 // If we got here without `moduleRequests`, the capability gate is lying.
@@ -511,7 +513,7 @@ export class EsmLoader {
511513 registry ,
512514 mode ,
513515 ) ;
514- if ( resolved === null ) return null ;
516+ if ( resolved === LOAD_ASYNC ) return LOAD_ASYNC ;
515517 validateImportAttributes ( resolved . modulePath , attributes , modulePath ) ;
516518 deps . push ( resolved . cacheKey ) ;
517519 if ( resolved . enqueue ) worklist . push ( resolved . enqueue ) ;
@@ -568,7 +570,7 @@ export class EsmLoader {
568570 : `a dependency uses top-level await (${ culprit } )` ,
569571 ) ;
570572 }
571- return null ;
573+ return LOAD_ASYNC ;
572574 }
573575 }
574576
@@ -634,13 +636,13 @@ export class EsmLoader {
634636 scratch : Map < string , ScratchEntry > ,
635637 registry : ModuleRegistry | Map < string , JestModule > ,
636638 mode : SyncEsmMode ,
637- ) : ResolvedSyncSpecifier | null {
639+ ) : ResolvedSyncSpecifier | LoadAsync {
638640 if ( specifier === '@jest/globals' ) {
639641 const cacheKey = `@jest/globals/${ referencingIdentifier } ` ;
640642 const ok = this . tryCommitSynthetic ( cacheKey , registry , scratch , ( ) =>
641643 this . jestGlobals . esmGlobalsModule ( referencingIdentifier , context ) ,
642644 ) ;
643- return ok ? { cacheKey, enqueue : null , modulePath : cacheKey } : null ;
645+ return ok ? { cacheKey, enqueue : null , modulePath : cacheKey } : LOAD_ASYNC ;
644646 }
645647
646648 if ( specifier . startsWith ( 'data:' ) ) {
@@ -667,7 +669,7 @@ export class EsmLoader {
667669 scratch ,
668670 mode ,
669671 ) ;
670- if ( mocked === null ) return null ;
672+ if ( mocked === LOAD_ASYNC ) return LOAD_ASYNC ;
671673 return {
672674 cacheKey : mocked . cacheKey ,
673675 enqueue : null ,
@@ -692,7 +694,7 @@ export class EsmLoader {
692694 ) ;
693695 } catch ( error ) {
694696 if ( mode === 'sync-required' ) throw error ;
695- return null ;
697+ return LOAD_ASYNC ;
696698 }
697699
698700 const cacheKey = resolved + query ;
@@ -708,7 +710,7 @@ export class EsmLoader {
708710 context ,
709711 ) ,
710712 ) ;
711- return ok ? { cacheKey, enqueue : null , modulePath : resolved } : null ;
713+ return ok ? { cacheKey, enqueue : null , modulePath : resolved } : LOAD_ASYNC ;
712714 }
713715
714716 return {
@@ -724,9 +726,9 @@ export class EsmLoader {
724726 context : VMContext ,
725727 scratch : Map < string , ScratchEntry > ,
726728 mode : SyncEsmMode ,
727- ) : { cacheKey : string } | null {
729+ ) : { cacheKey : string } | LoadAsync {
728730 const existing = this . registries . getModuleMock ( moduleID ) ;
729- if ( existing instanceof Promise ) return null ;
731+ if ( existing instanceof Promise ) return LOAD_ASYNC ;
730732 if ( existing ) {
731733 if ( existing . status === 'errored' ) throw existing . error ;
732734
@@ -753,7 +755,7 @@ export class EsmLoader {
753755 if ( mode === 'sync-required' ) {
754756 throw makeRequireAsyncError ( moduleName , 'mock factory is async' ) ;
755757 }
756- return null ;
758+ return LOAD_ASYNC ;
757759 }
758760
759761 const synth = syntheticFromExports (
@@ -785,7 +787,7 @@ export class EsmLoader {
785787 registry : ModuleRegistry | Map < string , JestModule > ,
786788 worklist : Array < WorklistEntry > ,
787789 mode : SyncEsmMode ,
788- ) : ScratchEntry | null {
790+ ) : ScratchEntry | LoadAsync {
789791 const wasmModule = new WebAssembly . Module ( bytes ) ;
790792
791793 const moduleSpecToCacheKey = new Map < string , string > ( ) ;
@@ -799,7 +801,7 @@ export class EsmLoader {
799801 registry ,
800802 mode ,
801803 ) ;
802- if ( resolved === null ) return null ;
804+ if ( resolved === LOAD_ASYNC ) return LOAD_ASYNC ;
803805 moduleSpecToCacheKey . set ( depSpec , resolved . cacheKey ) ;
804806 if ( resolved . enqueue ) worklist . push ( resolved . enqueue ) ;
805807 }
@@ -830,7 +832,7 @@ export class EsmLoader {
830832 registry : ModuleRegistry | Map < string , JestModule > ,
831833 worklist : Array < WorklistEntry > ,
832834 mode : SyncEsmMode ,
833- ) : ScratchEntry | null {
835+ ) : ScratchEntry | LoadAsync {
834836 const esmDynamicImport = this . dynamicImport ;
835837 const { mime, code} = parseDataUri ( specifier ) ;
836838
@@ -877,7 +879,7 @@ export class EsmLoader {
877879 if ( mode === 'sync-required' ) {
878880 throw makeRequireAsyncError ( specifier , 'top-level await' ) ;
879881 }
880- return null ;
882+ return LOAD_ASYNC ;
881883 }
882884
883885 invariant (
@@ -894,7 +896,7 @@ export class EsmLoader {
894896 registry ,
895897 mode ,
896898 ) ;
897- if ( resolved === null ) return null ;
899+ if ( resolved === LOAD_ASYNC ) return LOAD_ASYNC ;
898900 validateImportAttributes ( resolved . modulePath , attributes , specifier ) ;
899901 deps . push ( resolved . cacheKey ) ;
900902 if ( resolved . enqueue ) worklist . push ( resolved . enqueue ) ;
@@ -971,7 +973,7 @@ export class EsmLoader {
971973 // resolver and would silently miss user mappings.
972974 if ( supportsSyncEvaluate && this . resolution . canResolveSync ( ) ) {
973975 const synced = this . tryLoadGraphSync ( modulePath , query , 'sync-preferred' ) ;
974- if ( synced ) return synced ;
976+ if ( synced !== LOAD_ASYNC ) return synced ;
975977 }
976978
977979 const cacheKey = modulePath + query ;
0 commit comments