Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 01676a3

Browse files
authored
chore(runtime): avoid magical null value (#16160)
* chore(runtime): avoid magical `null` value * changelog * review comments * another fix * constant * sorted * ah, not here
1 parent 34c5952 commit 01676a3

5 files changed

Lines changed: 67 additions & 61 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/examples/*/node_modules/
77
/examples/react-native/.watchman-cookie-*
88

9+
/e2e/setup-files-after-env-config/package.json
910
/e2e/*/node_modules
1011
/e2e/*/.pnp
1112
/e2e/*/.pnp.js
@@ -15,6 +16,8 @@
1516
/e2e/transform/*/node_modules
1617
/e2e/custom-jsdom-version/*/node_modules
1718

19+
!packages/jest-runtime/src/__tests__/test_root/node_modules
20+
1821
/node_modules
1922

2023
/packages/*/build/

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## main
22

3+
### Chore & Maintenance
4+
5+
- `[jest-runtime]` Avoid magical `null` value in ESM loader ([#16160](https://github.com/jestjs/jest/pull/16160))
6+
37
## 30.4.2
48

59
### Fixes

e2e/setup-files-after-env-config/package.json

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/jest-runtime/src/internals/EsmLoader.ts

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ interface VMModuleWithAsyncGraph extends VMModule {
6363
// loader on any unsupported edge).
6464
export 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+
6672
type 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

Comments
 (0)