4.7 KiB
jest-runtime — agent notes
What lives here
jest-runtime is the module loader that runs user test code: resolution, mocking, ESM/CJS interop, the jest object, @jest/globals. The public class is Runtime (src/index.ts — DI wiring + public API + test-state machine + static factories). Everything else lives under src/internals/:
State holders & caches
TestState—'loading' | 'inTest' | 'betweenTests' | 'tornDown'machine +isTornDown()/isBetweenTests()/bailIfTornDown(msg)/throwIfTornDown(msg)/throwIfBetweenTests(msg)helpersMockState— mock decisions, factories, virtual marks; also hostsgenerateMock(automock) at the bottom of the fileModuleRegistries— CJS/ESM/mock registries with an isolation overlay forjest.isolateModulesResolution—jest-resolveadapter with cjs/esm/manual-mock cachesTransformCache/FileCache— code-transform and file-content cachesCjsExportsCache—cjs-module-lexer-based static export analysis for CJS-as-ESMV8CoverageCollector— V8 coverage instrumentation
Loaders & executors
CjsLoader— CJS require + manual-mock dispatchEsmLoader— ESM sync graph walker + legacy async path (one file; legacy block bracketed by a TODO header marking what deletes when min-Node ≥ v24.9)ModuleExecutor—compileFunction+ module body invocationRequireBuilder/CoreModuleProvider— per-modulerequirefactory; core-module shimmingJestGlobals—@jest/globalsplumbing (jestobject + test-runner globals)TestMainModule— shared cell forrequire.main
Stateless helpers
syntheticBuilders.ts,nodeCapabilities.ts,../helpers.ts
Hard rules
Runtime is a public extension point
Runtime is documented as subclassable; downstream consumers override these methods:
requireModule,requireModuleOrMock,requireMock,requireActual,requireInternalModule,unstable_importModule
Preserve their public signatures. Any internal callback that "loads a module" must dispatch through these, never through a sibling internal directly. E.g. wire requireBuilder with requireDispatch: (from, name) => this.requireModuleOrMock(from, name) — not (from, name) => this.cjsLoader.requireModule(...). Subclass overrides flow through the public method, not around it.
Capability gates stay glued to the body they guard
internals/nodeCapabilities.ts exports three gates:
runtimeSupportsVmModules—typeof SyntheticModule === 'function'(Node 18+ with--experimental-vm-modules)supportsSyncEvaluate— Node 24.9+ (gates onhasAsyncGraph); required for the sync ESM graph walkerResolution.canResolveSync()—falsewhen the user's configured resolver only exports an async hook
When moving a method that depends on a gate, carry the gate verbatim with the body it guards. Don't leave a comment in place of the runtime check; PR #16085 lost the supportsSyncEvaluate gate twice doing exactly that.
VM module status invariants
Sync code paths must validate vm.Module#status before reuse:
- A
SourceTextModulemay live in a registry while still'unlinked'— the legacy async path stashes it beforelink()runs. Gate cache reuse onmodule.status === 'evaluated'. - Rethrow
module.errorwhenstatus === 'errored'. - For
'unlinked' | 'linking' | 'linked' | 'evaluating', bail (sync-preferred) or surface a typed error (sync-required). requireEsmModule(sync-required) must neverreturn nullfor non-concurrency reasons — its caller turnsnullinto the genericERR_REQUIRE_ESM"concurrent import()" message. Resolver errors, missing factory, missingmoduleRequestsAPI, errored cache → throw a typed error orinvariant.
Mutex hygiene
TransformCache.mutex entries that aren't cleared after settle become permanent — every later sync-graph read of hasMutex returns true forever. Clear in finally after transformResolve() / transformReject(). Concurrent awaiters that grabbed the Promise reference before clear still see the rejection (the reference is independent of the Map entry).
Tests
- Pick the appropriate test gate:
testWithVmEsmfor legacy-path tests (Node 18+ with--experimental-vm-modules);testWithLinkedSyntheticModulefor sync-graph tests that needlinkRequests(Node 22.21+/24.8+);testWithSyncEsmfor sync-graph tests that needhasAsyncGraph(Node 24.9+). All come from@jest/test-utils(gate details in the root copilot-instructions.md). internals/__tests__/test files mirror production files 1:1; extend with newdescribeblocks rather than splitting. Their APIs are NOT a stability contract — change them in lockstep with the implementation.