/
githubmirror
/
jest
Обзор
Документация
Войти
/
githubmirror
/
jest
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/jest-runtime/src/internals/ModuleExecutor.ts
262 строки
8 KB
Simen Bekkhus
fix(jest-runtime): fall back to native ESM when a .js file contains ESM syntax (#16152)
11 май 2026, 16:14
Не верифицирован
11 май 2026, 16:14
c24dcd4
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import { type Context as VMContext, type Module as VMModule, compileFunction, } from 'node:vm'; import type { Jest, JestEnvironment, Module, ModuleWrapper, } from '@jest/environment'; import {handlePotentialSyntaxError} from '@jest/transform'; import type {Config, Global} from '@jest/types'; import Resolver from 'jest-resolve'; import {invariant, isError, isNonNullable} from 'jest-util'; import type {JestGlobals} from './JestGlobals'; import type {Resolution} from './Resolution'; import type {TestMainModule} from './TestMainModule'; import type {TransformCache, TransformOptions} from './TransformCache'; import type {RequireBuilder} from './cjsRequire'; import {hasEsmSyntax} from './esmLexer'; import type { ImportAttributes, InitialModule, ModuleRegistry, } from './moduleTypes'; import {runtimeSupportsVmModules} from './nodeCapabilities'; export type ExecResult = 'loaded' | 'env-disposed'; // Thrown by the CJS-as-ESM fallback paths when a file has ESM syntax but no // ESM marker (.mjs / "type":"module"). Callers catch it by instanceof and // retry with loadEsmModule / requireEsm. The original error is in `.cause`. export class CjsParseError extends SyntaxError { override name = 'CjsParseError'; constructor(cause: Error) { super(cause.message, {cause}); } } export interface ModuleExecutorOptions { resolution: Resolution; transformCache: TransformCache; environment: JestEnvironment; config: Config.ProjectConfig; testPath: string; requireBuilder: RequireBuilder; testMainModule: TestMainModule; jestGlobals: JestGlobals; dynamicImport: ( specifier: string, identifier: string, context: VMContext, importAttributes?: ImportAttributes, ) => Promise<VMModule>; } export class ModuleExecutor { private readonly resolution: Resolution; private readonly transformCache: TransformCache; private readonly environment: JestEnvironment; private readonly config: Config.ProjectConfig; private readonly testPath: string; private readonly requireBuilder: RequireBuilder; private readonly testMainModule: TestMainModule; private readonly jestGlobals: JestGlobals; private readonly dynamicImport: ModuleExecutorOptions['dynamicImport']; private currentlyExecutingManualMock: string | null = null; constructor(options: ModuleExecutorOptions) { this.resolution = options.resolution; this.transformCache = options.transformCache; this.environment = options.environment; this.config = options.config; this.testPath = options.testPath; this.requireBuilder = options.requireBuilder; this.testMainModule = options.testMainModule; this.jestGlobals = options.jestGlobals; this.dynamicImport = options.dynamicImport; } getCurrentlyExecutingManualMock(): string | null { return this.currentlyExecutingManualMock; } exec( localModule: InitialModule, options: TransformOptions | undefined, moduleRegistry: ModuleRegistry, from: string | null, moduleName: string | undefined, ): ExecResult { if (!this.environment.global) { return 'env-disposed'; } const module = localModule as Module; const filename = module.filename; const origCurrExecutingManualMock = this.currentlyExecutingManualMock; this.currentlyExecutingManualMock = filename; try { module.children = []; Object.defineProperty(module, 'parent', { enumerable: true, get() { const key = from || ''; return moduleRegistry.get(key) || null; }, }); const modulePaths = this.resolution.getModulePaths(module.path); const globalPaths = this.resolution.getGlobalPaths(moduleName); module.paths = [...modulePaths, ...globalPaths]; Object.defineProperty(module, 'require', { value: this.requireBuilder.for(localModule, options), }); const transformedCode = this.transformCache.transform(filename, options); const compiledFunction = this.compile(transformedCode, filename); if (compiledFunction === null) { return 'env-disposed'; } const jestObject = this.jestGlobals.jestObjectFor(filename); const lastArgs: [Jest | undefined, ...Array<Global.Global>] = [ this.config.injectGlobals ? jestObject : undefined, ...this.config.sandboxInjectedGlobals.map<Global.Global>( globalVariable => { if (this.environment.global[globalVariable]) { return this.environment.global[globalVariable]; } throw new Error( `You have requested '${globalVariable}' as a global variable, but it was not present. Please check your config or your global environment.`, ); }, ), ]; if (!this.testMainModule.current && filename === this.testPath) { this.testMainModule.current = module; } Object.defineProperty(module, 'main', { enumerable: true, value: this.testMainModule.current, }); try { compiledFunction.call( module.exports, module, // module object module.exports, // module exports module.require, // require implementation module.path, // __dirname module.filename, // __filename lastArgs[0], ...lastArgs.slice(1).filter(isNonNullable), ); } catch (error: any) { this.handleExecutionError(error, module); } } finally { this.currentlyExecutingManualMock = origCurrExecutingManualMock; } return 'loaded'; } private compile( scriptSource: string, filename: string, ): ModuleWrapper | null { const vmContext = this.environment.getVmContext(); if (vmContext == null) { return null; } try { const scriptFilename = this.resolution.isCoreModule(filename) ? `jest-nodejs-core-${filename}` : filename; return compileFunction( scriptSource, this.constructInjectedModuleParameters(), { filename: scriptFilename, importModuleDynamically: async ( specifier, _function, importAttributes, ) => { invariant( runtimeSupportsVmModules, 'You need to run with a version of node that supports ES Modules in the VM API. See https://jestjs.io/docs/ecmascript-modules', ); return this.dynamicImport( specifier, scriptFilename, vmContext, importAttributes as ImportAttributes | undefined, ); }, parsingContext: vmContext, }, ) as ModuleWrapper; } catch (error: any) { if ( runtimeSupportsVmModules && isError(error) && error.name === 'SyntaxError' && hasEsmSyntax(scriptSource) ) { throw new CjsParseError(error); } throw handlePotentialSyntaxError(error); } } constructInjectedModuleParameters(): Array<string> { return [ 'module', 'exports', 'require', '__dirname', '__filename', this.config.injectGlobals ? 'jest' : undefined, ...this.config.sandboxInjectedGlobals, ].filter(isNonNullable); } private handleExecutionError(error: Error, module: Module): never { const moduleNotFoundError = Resolver.tryCastModuleNotFoundError(error); if (moduleNotFoundError) { if (!moduleNotFoundError.requireStack) { moduleNotFoundError.requireStack = [module.filename || module.id]; for (let cursor = module.parent; cursor; cursor = cursor.parent) { moduleNotFoundError.requireStack.push(cursor.filename || cursor.id); } moduleNotFoundError.buildMessage(this.config.rootDir); } throw moduleNotFoundError; } throw error; } }