From 9b9bd8ef0b6f5168c77fde7f72f87ffa0473c9d0 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 16 Jan 2025 11:55:49 +0100 Subject: [PATCH 1/7] feat(runtime): use `compileFunction` over `new Script` --- ...consoleLogOutputWhenRunInBand.test.ts.snap | 2 +- .../coverageProviderV8.test.ts.snap | 4 +- .../__snapshots__/globals.test.ts.snap | 4 +- .../__snapshots__/runtime_wrap.js.snap | 11 --- .../src/__tests__/runtime_wrap.js | 35 ------- packages/jest-runtime/src/index.ts | 96 +++++++------------ packages/jest-test-result/src/types.ts | 4 +- 7 files changed, 41 insertions(+), 115 deletions(-) delete mode 100644 packages/jest-runtime/src/__tests__/__snapshots__/runtime_wrap.js.snap delete mode 100644 packages/jest-runtime/src/__tests__/runtime_wrap.js diff --git a/e2e/__tests__/__snapshots__/consoleLogOutputWhenRunInBand.test.ts.snap b/e2e/__tests__/__snapshots__/consoleLogOutputWhenRunInBand.test.ts.snap index 7afe4b918b81..5482a613a9f3 100644 --- a/e2e/__tests__/__snapshots__/consoleLogOutputWhenRunInBand.test.ts.snap +++ b/e2e/__tests__/__snapshots__/consoleLogOutputWhenRunInBand.test.ts.snap @@ -20,6 +20,6 @@ exports[`prints console.logs when run with forceExit 3`] = ` " console.log Hey - at Object. (__tests__/a-banana.js:1:41) + at Object.log (__tests__/a-banana.js:1:30) " `; diff --git a/e2e/__tests__/__snapshots__/coverageProviderV8.test.ts.snap b/e2e/__tests__/__snapshots__/coverageProviderV8.test.ts.snap index 102d755384bb..eaa2662c08cb 100644 --- a/e2e/__tests__/__snapshots__/coverageProviderV8.test.ts.snap +++ b/e2e/__tests__/__snapshots__/coverageProviderV8.test.ts.snap @@ -9,8 +9,8 @@ exports[`prints correct coverage report, if a CJS module is put under test witho --------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s --------------|---------|----------|---------|---------|------------------- -All files | 59.37 | 60 | 50 | 59.37 | - module.js | 79.16 | 75 | 66.66 | 79.16 | 14-16,19-20 +All files | 59.37 | 50 | 33.33 | 59.37 | + module.js | 79.16 | 66.66 | 50 | 79.16 | 14-16,19-20 uncovered.js | 0 | 0 | 0 | 0 | 1-8 --------------|---------|----------|---------|---------|-------------------" `; diff --git a/e2e/__tests__/__snapshots__/globals.test.ts.snap b/e2e/__tests__/__snapshots__/globals.test.ts.snap index ce1c4359a507..15e0eee294e7 100644 --- a/e2e/__tests__/__snapshots__/globals.test.ts.snap +++ b/e2e/__tests__/__snapshots__/globals.test.ts.snap @@ -24,9 +24,9 @@ exports[`cannot have describe with no implementation 1`] = ` Missing second argument. It must be a callback function. > 1 | describe('describe, no implementation'); - | ^ + | ^ - at Object. (__tests__/onlyConstructs.test.js:1:40)" + at Object.describe (__tests__/onlyConstructs.test.js:1:1)" `; exports[`cannot have describe with no implementation 2`] = ` diff --git a/packages/jest-runtime/src/__tests__/__snapshots__/runtime_wrap.js.snap b/packages/jest-runtime/src/__tests__/__snapshots__/runtime_wrap.js.snap deleted file mode 100644 index 5d3099f81f8a..000000000000 --- a/packages/jest-runtime/src/__tests__/__snapshots__/runtime_wrap.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Runtime wrapCodeInModuleWrapper generates the correct args for the module wrapper 1`] = ` -"({"Object.":function(module,exports,require,__dirname,__filename,jest){module.exports = "Hello!" -}});" -`; - -exports[`Runtime wrapCodeInModuleWrapper injects "extra globals" 1`] = ` -"({"Object.":function(module,exports,require,__dirname,__filename,jest,Math){module.exports = "Hello!" -}});" -`; diff --git a/packages/jest-runtime/src/__tests__/runtime_wrap.js b/packages/jest-runtime/src/__tests__/runtime_wrap.js deleted file mode 100644 index be61ce37313f..000000000000 --- a/packages/jest-runtime/src/__tests__/runtime_wrap.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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. - * - */ - -let createRuntime; - -describe('Runtime', () => { - beforeEach(() => { - createRuntime = require('createRuntime'); - }); - - describe('wrapCodeInModuleWrapper', () => { - it('generates the correct args for the module wrapper', async () => { - const runtime = await createRuntime(__filename); - - expect( - runtime.wrapCodeInModuleWrapper('module.exports = "Hello!"'), - ).toMatchSnapshot(); - }); - - it('injects "extra globals"', async () => { - const runtime = await createRuntime(__filename, { - sandboxInjectedGlobals: ['Math'], - }); - - expect( - runtime.wrapCodeInModuleWrapper('module.exports = "Hello!"'), - ).toMatchSnapshot(); - }); - }); -}); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index e054e894d211..f3f50841f408 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -9,7 +9,6 @@ import nativeModule = require('module'); import * as path from 'path'; import {URL, fileURLToPath, pathToFileURL} from 'url'; import { - Script, // @ts-expect-error: experimental, not added to the types SourceTextModule, // @ts-expect-error: experimental, not added to the types @@ -17,6 +16,7 @@ import { type Context as VMContext, // @ts-expect-error: experimental, not added to the types type Module as VMModule, + compileFunction, } from 'vm'; import {parse as parseCjs} from 'cjs-module-lexer'; import {CoverageInstrumenter, type V8Coverage} from 'collect-v8-coverage'; @@ -145,10 +145,6 @@ const isWasm = (modulePath: string): boolean => modulePath.endsWith('.wasm'); const unmockRegExpCache = new WeakMap(); -const EVAL_RESULT_VARIABLE = 'Object.'; - -type RunScriptEvalResult = {[EVAL_RESULT_VARIABLE]: ModuleWrapper}; - const runtimeSupportsVmModules = typeof SyntheticModule === 'function'; const supportsNodeColonModulePrefixInRequire = (() => { @@ -1593,21 +1589,10 @@ export default class Runtime { const transformedCode = this.transformFile(filename, options); - let compiledFunction: ModuleWrapper | null = null; - - const script = this.createScriptFromCode(transformedCode, filename); - - let runScript: RunScriptEvalResult | null = null; - - const vmContext = this._environment.getVmContext(); - - if (vmContext) { - runScript = script.runInContext(vmContext, {filename}); - } - - if (runScript !== null) { - compiledFunction = runScript[EVAL_RESULT_VARIABLE]; - } + const compiledFunction = this.createScriptFromCode( + transformedCode, + filename, + ); if (compiledFunction === null) { this._logFormattedReferenceError( @@ -1680,10 +1665,7 @@ export default class Runtime { source, ); - this._fileTransforms.set(filename, { - ...transformedFile, - wrapperLength: this.constructModuleWrapperStart().length, - }); + this._fileTransforms.set(filename, transformedFile); if (transformedFile.sourceMapPath) { this._sourceMapRegistry.set(filename, transformedFile.sourceMapPath); @@ -1708,10 +1690,7 @@ export default class Runtime { ); if (this._fileTransforms.get(filename)?.code !== transformedFile.code) { - this._fileTransforms.set(filename, { - ...transformedFile, - wrapperLength: 0, - }); + this._fileTransforms.set(filename, transformedFile); } if (transformedFile.sourceMapPath) { @@ -1721,34 +1700,39 @@ export default class Runtime { } private createScriptFromCode(scriptSource: string, filename: string) { + const vmContext = this._environment.getVmContext(); + + if (vmContext == null) { + return null; + } + try { const scriptFilename = this._resolver.isCoreModule(filename) ? `jest-nodejs-core-${filename}` : filename; - return new Script(this.wrapCodeInModuleWrapper(scriptSource), { - columnOffset: this._fileTransforms.get(filename)?.wrapperLength, - displayErrors: true, - filename: scriptFilename, - // @ts-expect-error: Experimental ESM API - importModuleDynamically: async (specifier: string) => { - 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', - ); - - const context = this._environment.getVmContext?.(); - - invariant(context, 'Test environment has been torn down'); - - const module = await this.resolveModule( - specifier, - scriptFilename, - context, - ); - - return this.linkAndEvaluateModule(module); + return compileFunction( + scriptSource, + this.constructInjectedModuleParameters(), + { + filename: scriptFilename, + // @ts-expect-error: Experimental ESM API + importModuleDynamically: async (specifier: string) => { + 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', + ); + + const module = await this.resolveModule( + specifier, + scriptFilename, + vmContext, + ); + + return this.linkAndEvaluateModule(module); + }, + parsingContext: vmContext, }, - }); + ) as ModuleWrapper; } catch (error: any) { throw handlePotentialSyntaxError(error); } @@ -2466,16 +2450,6 @@ export default class Runtime { ); } - private wrapCodeInModuleWrapper(content: string) { - return `${this.constructModuleWrapperStart() + content}\n}});`; - } - - private constructModuleWrapperStart() { - const args = this.constructInjectedModuleParameters(); - - return `({"${EVAL_RESULT_VARIABLE}":function(${args.join(',')}){`; - } - private constructInjectedModuleParameters(): Array { return [ 'module', diff --git a/packages/jest-test-result/src/types.ts b/packages/jest-test-result/src/types.ts index 6fb6c645e2c9..bb9ce49e6cb7 100644 --- a/packages/jest-test-result/src/types.ts +++ b/packages/jest-test-result/src/types.ts @@ -12,9 +12,7 @@ import type {Circus, Config, TestResult, TransformTypes} from '@jest/types'; import type {IHasteFS, IModuleMap} from 'jest-haste-map'; import type Resolver from 'jest-resolve'; -export interface RuntimeTransformResult extends TransformTypes.TransformResult { - wrapperLength: number; -} +export type RuntimeTransformResult = TransformTypes.TransformResult; export type V8CoverageResult = Array<{ codeTransformResult: RuntimeTransformResult | undefined; From 43226cd6a90e7a5f0b3bb0f8555906a2d1d718d4 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 16 Jan 2025 11:58:40 +0100 Subject: [PATCH 2/7] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa86dbf20cb6..3283c6e8a344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - `[jest-runtime]` [**BREAKING**] Make it mandatory to pass `globalConfig` to the `Runtime` constructor ([#15044](https://github.com/jestjs/jest/pull/15044)) - `[jest-runtime]` Add `unstable_unmockModule` ([#15080](https://github.com/jestjs/jest/pull/15080)) - `[jest-runtime]` Add `onGenerateMock` transformer callback for auto generated callbacks ([#15433](https://github.com/jestjs/jest/pull/15433)) +- `[jest-runtime]` [**BREAKING**] User `vm.compileFunction` over `vm.Script` ([#15461](https://github.com/jestjs/jest/pull/15461)) - `[@jest/schemas]` Upgrade `@sinclair/typebox` to v0.34 ([#15450](https://github.com/jestjs/jest/pull/15450)) - `[@jest/types]` `test.each()`: Accept a readonly (`as const`) table properly ([#14565](https://github.com/jestjs/jest/pull/14565)) - `[@jest/types]` Improve argument type inference passed to `test` and `describe` callback functions from `each` tables ([#14920](https://github.com/jestjs/jest/pull/14920)) From 108f9f2d53ee99ff1d533eb0b81a8d6ae4da48f8 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 16 Jan 2025 12:48:35 +0100 Subject: [PATCH 3/7] Update CoverageReporter.ts --- packages/jest-reporters/src/CoverageReporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-reporters/src/CoverageReporter.ts b/packages/jest-reporters/src/CoverageReporter.ts index 39b8636e39fa..5dacdbc4cebe 100644 --- a/packages/jest-reporters/src/CoverageReporter.ts +++ b/packages/jest-reporters/src/CoverageReporter.ts @@ -467,7 +467,7 @@ export default class CoverageReporter extends BaseReporter { const converter = v8toIstanbul( res.url, - fileTransform?.wrapperLength ?? 0, + 0, fileTransform && sourcemapContent ? { originalSource: fileTransform.originalCode, From 8b90423899d54a8781badd760e70efa1de652d8e Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 16 Jan 2025 12:58:14 +0100 Subject: [PATCH 4/7] skip alias --- packages/jest-runtime/src/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index f3f50841f408..c827a8572b5c 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -33,15 +33,12 @@ import type { import type {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers'; import type {expect, jest} from '@jest/globals'; import type {SourceMapRegistry} from '@jest/source-map'; -import type { - RuntimeTransformResult, - TestContext, - V8CoverageResult, -} from '@jest/test-result'; +import type {TestContext, V8CoverageResult} from '@jest/test-result'; import { type CallerTransformOptions, type ScriptTransformer, type ShouldInstrumentOptions, + type TransformResult, type TransformationOptions, handlePotentialSyntaxError, shouldInstrument, @@ -199,11 +196,11 @@ export default class Runtime { >; private readonly _sourceMapRegistry: SourceMapRegistry; private readonly _scriptTransformer: ScriptTransformer; - private readonly _fileTransforms: Map; + private readonly _fileTransforms: Map; private readonly _fileTransformsMutex: Map>; private _v8CoverageInstrumenter: CoverageInstrumenter | undefined; private _v8CoverageResult: V8Coverage | undefined; - private _v8CoverageSources: Map | undefined; + private _v8CoverageSources: Map | undefined; private readonly _transitiveShouldMock: Map; private _unmockList: RegExp | undefined; private readonly _virtualMocks: Map; From ad5a6d281a87b9aee7e9c60889314de2b674745d Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 16 Jan 2025 13:19:34 +0100 Subject: [PATCH 5/7] skip leak test on node 16 --- e2e/__tests__/jestEnvironmentJsdom.test.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/e2e/__tests__/jestEnvironmentJsdom.test.ts b/e2e/__tests__/jestEnvironmentJsdom.test.ts index 7fa6711ad2f2..47594cc90f4b 100644 --- a/e2e/__tests__/jestEnvironmentJsdom.test.ts +++ b/e2e/__tests__/jestEnvironmentJsdom.test.ts @@ -7,6 +7,7 @@ import {tmpdir} from 'os'; import * as path from 'path'; +import {onNodeVersions} from '@jest/test-utils'; import {cleanup, writeFiles} from '../Utils'; import runJest from '../runJest'; @@ -15,14 +16,16 @@ const DIR = path.resolve(tmpdir(), 'jest_environment_jsdom_test'); beforeEach(() => cleanup(DIR)); afterAll(() => cleanup(DIR)); -test('check is not leaking memory', () => { - writeFiles(DIR, { - '__tests__/a.test.js': "test('a', () => console.log('a'));", - '__tests__/b.test.js': "test('b', () => console.log('b'));", - 'package.json': JSON.stringify({jest: {testEnvironment: 'jsdom'}}), - }); +onNodeVersions('> 16', () => { + test('check is not leaking memory', () => { + writeFiles(DIR, { + '__tests__/a.test.js': "test('a', () => console.log('a'));", + '__tests__/b.test.js': "test('b', () => console.log('b'));", + 'package.json': JSON.stringify({jest: {testEnvironment: 'jsdom'}}), + }); - const {stderr} = runJest(DIR, ['--detect-leaks', '--runInBand']); - expect(stderr).toMatch(/PASS\s__tests__\/a.test.js/); - expect(stderr).toMatch(/PASS\s__tests__\/b.test.js/); + const {stderr} = runJest(DIR, ['--detect-leaks', '--runInBand']); + expect(stderr).toMatch(/PASS\s__tests__\/a.test.js/); + expect(stderr).toMatch(/PASS\s__tests__\/b.test.js/); + }); }); From 1b6d54f48d6ce178dc392b1ef848a166199435df Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 16 Jan 2025 15:26:47 +0100 Subject: [PATCH 6/7] re-add test --- .../src/__tests__/runtime_wrap.js | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/jest-runtime/src/__tests__/runtime_wrap.js diff --git a/packages/jest-runtime/src/__tests__/runtime_wrap.js b/packages/jest-runtime/src/__tests__/runtime_wrap.js new file mode 100644 index 000000000000..9da3ed46e150 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/runtime_wrap.js @@ -0,0 +1,60 @@ +/** + * 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. + * + */ + +let createRuntime; + +describe('Runtime', () => { + beforeEach(() => { + createRuntime = require('createRuntime'); + }); + + describe('constructInjectedModuleParameters', () => { + it('generates the correct args', async () => { + const runtime = await createRuntime(__filename); + + expect(runtime.constructInjectedModuleParameters()).toEqual([ + 'module', + 'exports', + 'require', + '__dirname', + '__filename', + 'jest', + ]); + }); + + it('injects "extra globals"', async () => { + const runtime = await createRuntime(__filename, { + sandboxInjectedGlobals: ['Math'], + }); + + expect(runtime.constructInjectedModuleParameters()).toEqual([ + 'module', + 'exports', + 'require', + '__dirname', + '__filename', + 'jest', + 'Math', + ]); + }); + + it('avoid injecting `jest` if `injectGlobals = false`', async () => { + const runtime = await createRuntime(__filename, { + injectGlobals: false, + }); + + expect(runtime.constructInjectedModuleParameters()).toEqual([ + 'module', + 'exports', + 'require', + '__dirname', + '__filename', + ]); + }); + }); +}); From d72158a01605f3826bac4c6efd1d54a9e89906d5 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 17 Jan 2025 07:24:34 +0100 Subject: [PATCH 7/7] Update CHANGELOG.md Co-authored-by: Gareth Jones --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3283c6e8a344..d8c0ab02d71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ - `[jest-runtime]` [**BREAKING**] Make it mandatory to pass `globalConfig` to the `Runtime` constructor ([#15044](https://github.com/jestjs/jest/pull/15044)) - `[jest-runtime]` Add `unstable_unmockModule` ([#15080](https://github.com/jestjs/jest/pull/15080)) - `[jest-runtime]` Add `onGenerateMock` transformer callback for auto generated callbacks ([#15433](https://github.com/jestjs/jest/pull/15433)) -- `[jest-runtime]` [**BREAKING**] User `vm.compileFunction` over `vm.Script` ([#15461](https://github.com/jestjs/jest/pull/15461)) +- `[jest-runtime]` [**BREAKING**] Use `vm.compileFunction` over `vm.Script` ([#15461](https://github.com/jestjs/jest/pull/15461)) - `[@jest/schemas]` Upgrade `@sinclair/typebox` to v0.34 ([#15450](https://github.com/jestjs/jest/pull/15450)) - `[@jest/types]` `test.each()`: Accept a readonly (`as const`) table properly ([#14565](https://github.com/jestjs/jest/pull/14565)) - `[@jest/types]` Improve argument type inference passed to `test` and `describe` callback functions from `each` tables ([#14920](https://github.com/jestjs/jest/pull/14920))