diff --git a/CHANGELOG.md b/CHANGELOG.md index d8bf74829b8e..1ad046caa216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - `[jest-snapshot]` Improve colors when snapshots are updatable ([#9132](https://github.com/facebook/jest/pull/9132)) - `[jest-snapshot]` Ignore indentation for most serialized objects ([#9203](https://github.com/facebook/jest/pull/9203)) - `[jest-transform]` Create `createTranspilingRequire` function for easy transpiling modules ([#9194](https://github.com/facebook/jest/pull/9194)) +- `[jest-transform]` [**BREAKING**] Return transformed code as a string, do not wrap in `vm.Script` ([#9253](https://github.com/facebook/jest/pull/9253)) - `[@jest/test-result]` Create method to create empty `TestResult` ([#8867](https://github.com/facebook/jest/pull/8867)) - `[jest-worker]` [**BREAKING**] Return a promise from `end()`, resolving with the information whether workers exited gracefully ([#8206](https://github.com/facebook/jest/pull/8206)) - `[jest-reporters]` Transform file paths into hyperlinks ([#8980](https://github.com/facebook/jest/pull/8980)) diff --git a/packages/jest-environment/package.json b/packages/jest-environment/package.json index bc0712d971a6..290eacd140fe 100644 --- a/packages/jest-environment/package.json +++ b/packages/jest-environment/package.json @@ -11,7 +11,6 @@ "types": "build/index.d.ts", "dependencies": { "@jest/fake-timers": "^24.9.0", - "@jest/transform": "^24.9.0", "@jest/types": "^24.9.0", "jest-mock": "^24.9.0" }, diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 2dd7fa911480..d28dba5dda70 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -8,7 +8,6 @@ import {Script} from 'vm'; import {Circus, Config, Global} from '@jest/types'; import jestMock = require('jest-mock'); -import {ScriptTransformer} from '@jest/transform'; import { JestFakeTimers as LegacyFakeTimers, LolexFakeTimers, @@ -27,6 +26,7 @@ export type EnvironmentContext = Partial<{ // Different Order than https://nodejs.org/api/modules.html#modules_the_module_wrapper , however needs to be in the form [jest-transform]ScriptTransformer accepts export type ModuleWrapper = ( + this: Module['exports'], module: Module, exports: Module['exports'], require: Module['require'], @@ -43,9 +43,7 @@ export declare class JestEnvironment { fakeTimers: LegacyFakeTimers | null; fakeTimersLolex: LolexFakeTimers | null; moduleMocker: jestMock.ModuleMocker | null; - runScript( - script: Script, - ): {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null; + runScript(script: Script): T | null; setup(): Promise; teardown(): Promise; handleTestEvent?(event: Circus.Event, state: Circus.State): void; diff --git a/packages/jest-environment/tsconfig.json b/packages/jest-environment/tsconfig.json index cfce4b39485b..658545960324 100644 --- a/packages/jest-environment/tsconfig.json +++ b/packages/jest-environment/tsconfig.json @@ -6,7 +6,6 @@ }, "references": [ {"path": "../jest-fake-timers"}, - {"path": "../jest-transform"}, {"path": "../jest-types"}, {"path": "../jest-util"} ] diff --git a/packages/jest-runtime/src/__tests__/__snapshots__/runtime_wrap.js.snap b/packages/jest-runtime/src/__tests__/__snapshots__/runtime_wrap.js.snap new file mode 100644 index 000000000000..fd65bf149b33 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/__snapshots__/runtime_wrap.js.snap @@ -0,0 +1,11 @@ +// 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,global,jest){module.exports = "Hello!" +}}); +`; + +exports[`Runtime wrapCodeInModuleWrapper injects "extra globals" 1`] = ` +({"Object.":function(module,exports,require,__dirname,__filename,global,jest,Math){module.exports = "Hello!" +}}); +`; diff --git a/packages/jest-runtime/src/__tests__/instrumentation.test.ts b/packages/jest-runtime/src/__tests__/instrumentation.test.ts index 48fedd2e51d5..0de407f14862 100644 --- a/packages/jest-runtime/src/__tests__/instrumentation.test.ts +++ b/packages/jest-runtime/src/__tests__/instrumentation.test.ts @@ -6,7 +6,6 @@ * */ -import * as vm from 'vm'; import * as path from 'path'; import * as os from 'os'; import {ScriptTransformer} from '@jest/transform'; @@ -32,10 +31,9 @@ it('instruments files', () => { ...makeGlobalConfig({collectCoverage: true}), changedFiles: undefined, }, - ).script; - expect(instrumented instanceof vm.Script).toBe(true); + ); // We can't really snapshot the resulting coverage, because it depends on // absolute path of the file, which will be different on different // machines - expect(vm.Script.mock.calls[0][0]).toMatch(`gcv = "__coverage__"`); + expect(instrumented.code).toMatch(`gcv = "__coverage__"`); }); 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..af60683b5f3d --- /dev/null +++ b/packages/jest-runtime/src/__tests__/runtime_wrap.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {wrap} from 'jest-snapshot-serializer-raw'; +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( + wrap(runtime.wrapCodeInModuleWrapper('module.exports = "Hello!"')), + ).toMatchSnapshot(); + }); + + it('injects "extra globals"', async () => { + const runtime = await createRuntime(__filename, {extraGlobals: ['Math']}); + + expect( + wrap(runtime.wrapCodeInModuleWrapper('module.exports = "Hello!"')), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index e6de0ba3d73e..05e46de5d6b5 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -6,12 +6,14 @@ */ import * as path from 'path'; +import {Script} from 'vm'; import {Config} from '@jest/types'; import { Jest, JestEnvironment, LocalModuleRequire, Module, + ModuleWrapper, } from '@jest/environment'; import {SourceMapRegistry} from '@jest/source-map'; import jestMock = require('jest-mock'); @@ -25,6 +27,7 @@ import { ScriptTransformer, ShouldInstrumentOptions, TransformationOptions, + handlePotentialSyntaxError, shouldInstrument, } from '@jest/transform'; import * as fs from 'graceful-fs'; @@ -78,6 +81,10 @@ const getModuleNameMapper = (config: Config.ProjectConfig) => { const unmockRegExpCache = new WeakMap(); +const EVAL_RESULT_VARIABLE = 'Object.'; + +type RunScriptEvalResult = {[EVAL_RESULT_VARIABLE]: ModuleWrapper}; + /* eslint-disable-next-line no-redeclare */ class Runtime { static ScriptTransformer: typeof ScriptTransformer; @@ -489,7 +496,6 @@ class Runtime { collectCoverage: this._coverageOptions.collectCoverage, collectCoverageFrom: this._coverageOptions.collectCoverageFrom, collectCoverageOnlyFrom: this._coverageOptions.collectCoverageOnlyFrom, - extraGlobals: this._config.extraGlobals, }; } @@ -726,7 +732,9 @@ class Runtime { } } - const runScript = this._environment.runScript(transformedFile.script); + const script = this.createScriptFromCode(transformedFile.code, filename); + + const runScript = this._environment.runScript(script); if (runScript === null) { this._logFormattedReferenceError( @@ -737,7 +745,7 @@ class Runtime { } //Wrapper - runScript[ScriptTransformer.EVAL_RESULT_VARIABLE].call( + runScript[EVAL_RESULT_VARIABLE].call( localModule.exports, localModule as NodeModule, // module object localModule.exports, // module exports @@ -764,6 +772,19 @@ class Runtime { this._currentlyExecutingModulePath = lastExecutingModulePath; } + private createScriptFromCode(scriptSource: string, filename: string) { + try { + return new Script(this.wrapCodeInModuleWrapper(scriptSource), { + displayErrors: true, + filename: this._resolver.isCoreModule(filename) + ? `jest-nodejs-core-${filename}` + : filename, + }); + } catch (e) { + throw handlePotentialSyntaxError(e); + } + } + private _requireCoreModule(moduleName: string) { if (moduleName === 'process') { return this._environment.global.process; @@ -1086,6 +1107,27 @@ class Runtime { formatStackTrace(stack, this._config, {noStackTrace: false}), ); } + + private wrapCodeInModuleWrapper(content: string) { + const args = [ + 'module', + 'exports', + 'require', + '__dirname', + '__filename', + 'global', + 'jest', + ...this._config.extraGlobals, + ]; + + return ( + '({"' + + EVAL_RESULT_VARIABLE + + `":function(${args.join(',')}){` + + content + + '\n}});' + ); + } } Runtime.ScriptTransformer = ScriptTransformer; diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index c50d97494438..9ab22cac463e 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -7,7 +7,6 @@ import {createHash} from 'crypto'; import * as path from 'path'; -import {Script} from 'vm'; import {Config} from '@jest/types'; import {createDirectory, interopRequireDefault, isPromise} from 'jest-util'; import * as fs from 'graceful-fs'; @@ -28,7 +27,7 @@ import { Transformer, } from './types'; import shouldInstrument from './shouldInstrument'; -import enhanceUnexpectedTokenMessage from './enhanceUnexpectedTokenMessage'; +import handlePotentialSyntaxError from './enhanceUnexpectedTokenMessage'; type ProjectCache = { configString: string; @@ -60,7 +59,6 @@ async function waitForPromiseWithCleanup( } export default class ScriptTransformer { - static EVAL_RESULT_VARIABLE: 'Object.'; private _cache: ProjectCache; private _config: Config.ProjectConfig; private _transformCache: Map; @@ -347,12 +345,11 @@ export default class ScriptTransformer { ): TransformResult { const isInternalModule = !!(options && options.isInternalModule); const isCoreModule = !!(options && options.isCoreModule); - const extraGlobals = (options && options.extraGlobals) || []; const content = stripShebang( fileSource || fs.readFileSync(filename, 'utf8'), ); - let wrappedCode: string; + let code = content; let sourceMapPath: string | null = null; let mapCoverage = false; @@ -369,36 +366,18 @@ export default class ScriptTransformer { instrument, ); - wrappedCode = wrap(transformedSource.code, ...extraGlobals); + code = transformedSource.code; sourceMapPath = transformedSource.sourceMapPath; mapCoverage = transformedSource.mapCoverage; - } else { - wrappedCode = wrap(content, ...extraGlobals); } return { + code, mapCoverage, - script: new Script(wrappedCode, { - displayErrors: true, - filename: isCoreModule ? 'jest-nodejs-core-' + filename : filename, - }), sourceMapPath, }; } catch (e) { - if (e.codeFrame) { - e.stack = e.message + '\n' + e.codeFrame; - } - - if ( - e instanceof SyntaxError && - (e.message.includes('Unexpected token') || - e.message.includes('Cannot use import')) && - !e.message.includes(' expected') - ) { - throw enhanceUnexpectedTokenMessage(e); - } - - throw e; + throw handlePotentialSyntaxError(e); } } @@ -698,27 +677,3 @@ const calcTransformRegExp = (config: Config.ProjectConfig) => { return transformRegexp; }; - -const wrap = (content: string, ...extras: Array) => { - const globals = new Set([ - 'module', - 'exports', - 'require', - '__dirname', - '__filename', - 'global', - 'jest', - ...extras, - ]); - - return ( - '({"' + - ScriptTransformer.EVAL_RESULT_VARIABLE + - `":function(${Array.from(globals).join(',')}){` + - content + - '\n}});' - ); -}; - -// TODO: Can this be added to the static property? -ScriptTransformer.EVAL_RESULT_VARIABLE = 'Object.'; diff --git a/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.js.snap b/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.js.snap index b30f6502fa79..38b4e2ae1b32 100644 --- a/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.js.snap +++ b/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.js.snap @@ -75,7 +75,7 @@ Object { `; exports[`ScriptTransformer transforms a file properly 1`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */ +/* istanbul ignore next */ function cov_25u22311x4() { var path = "/fruits/banana.js"; var hash = "4be0f6184160be573fc43f7c2a5877c28b7ce249"; @@ -122,11 +122,10 @@ function cov_25u22311x4() { cov_25u22311x4().s[0]++; module.exports = "banana"; -}}); `; exports[`ScriptTransformer transforms a file properly 2`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */ +/* istanbul ignore next */ function cov_23yvu8etmu() { var path = "/fruits/kiwi.js"; var hash = "7705dd5fcfbc884dcea7062944cfb8cc5d141d1a"; @@ -217,48 +216,33 @@ module.exports = () => { cov_23yvu8etmu().s[1]++; return "kiwi"; }; -}}); -`; - -exports[`ScriptTransformer transforms a file properly 3`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest,Math){module.exports = "banana"; -}}); `; exports[`ScriptTransformer uses multiple preprocessors 1`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest){const TRANSFORMED = { +const TRANSFORMED = { filename: '/fruits/banana.js', script: 'module.exports = "banana";', config: '{"automock":false,"browser":false,"cache":true,"cacheDirectory":"/cache/","clearMocks":false,"coveragePathIgnorePatterns":[],"cwd":"/test_root_dir/","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"filter":null,"forceCoverageMatch":[],"globalSetup":null,"globalTeardown":null,"globals":{},"haste":{"providesModuleNodeModules":[]},"moduleDirectories":[],"moduleFileExtensions":["js"],"moduleLoader":"/test_module_loader_path","moduleNameMapper":[],"modulePathIgnorePatterns":[],"modulePaths":[],"name":"test","prettierPath":"prettier","resetMocks":false,"resetModules":false,"resolver":null,"restoreMocks":false,"rootDir":"/","roots":[],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"skipNodeResolution":false,"snapshotResolver":null,"snapshotSerializers":[],"testEnvironment":"node","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":[],"testPathIgnorePatterns":[],"testRegex":["\\\\.test\\\\.js$"],"testRunner":"jest-jasmine2","testURL":"http://localhost","timers":"real","transform":[["^.+\\\\.js$","test_preprocessor"],["^.+\\\\.css$","css-preprocessor"]],"transformIgnorePatterns":["/node_modules/"],"unmockedModulePathPatterns":null,"watchPathIgnorePatterns":[]}', }; -}}); `; exports[`ScriptTransformer uses multiple preprocessors 2`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest){module.exports = { +module.exports = { filename: /styles/App.css, rawFirstLine: root {, }; -}}); `; -exports[`ScriptTransformer uses multiple preprocessors 3`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest){module.exports = "react"; -}}); -`; +exports[`ScriptTransformer uses multiple preprocessors 3`] = `module.exports = "react";`; exports[`ScriptTransformer uses the supplied preprocessor 1`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest){const TRANSFORMED = { +const TRANSFORMED = { filename: '/fruits/banana.js', script: 'module.exports = "banana";', config: '{"automock":false,"browser":false,"cache":true,"cacheDirectory":"/cache/","clearMocks":false,"coveragePathIgnorePatterns":[],"cwd":"/test_root_dir/","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"filter":null,"forceCoverageMatch":[],"globalSetup":null,"globalTeardown":null,"globals":{},"haste":{"providesModuleNodeModules":[]},"moduleDirectories":[],"moduleFileExtensions":["js"],"moduleLoader":"/test_module_loader_path","moduleNameMapper":[],"modulePathIgnorePatterns":[],"modulePaths":[],"name":"test","prettierPath":"prettier","resetMocks":false,"resetModules":false,"resolver":null,"restoreMocks":false,"rootDir":"/","roots":[],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"skipNodeResolution":false,"snapshotResolver":null,"snapshotSerializers":[],"testEnvironment":"node","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":[],"testPathIgnorePatterns":[],"testRegex":["\\\\.test\\\\.js$"],"testRunner":"jest-jasmine2","testURL":"http://localhost","timers":"real","transform":[["^.+\\\\.js$","test_preprocessor"]],"transformIgnorePatterns":["/node_modules/"],"unmockedModulePathPatterns":null,"watchPathIgnorePatterns":[]}', }; -}}); `; -exports[`ScriptTransformer uses the supplied preprocessor 2`] = ` -({"Object.":function(module,exports,require,__dirname,__filename,global,jest){module.exports = "react"; -}}); -`; +exports[`ScriptTransformer uses the supplied preprocessor 2`] = `module.exports = "react";`; exports[`ScriptTransformer warns of unparseable inlined source maps from the preprocessor 1`] = `jest-transform: The source map produced for the file /fruits/banana.js by preprocessor-with-sourcemaps was invalid. Proceeding without source mapping for that file.`; diff --git a/packages/jest-transform/src/__tests__/script_transformer.test.js b/packages/jest-transform/src/__tests__/script_transformer.test.js index 4ebc8d40c256..40086c968fba 100644 --- a/packages/jest-transform/src/__tests__/script_transformer.test.js +++ b/packages/jest-transform/src/__tests__/script_transformer.test.js @@ -37,7 +37,6 @@ jest ...jest.requireActual('jest-util'), createDirectory: jest.fn(), })) - .mock('vm') .mock('path', () => jest.requireActual('path').posix); jest.mock( @@ -142,7 +141,6 @@ let config; let fs; let mockFs; let object; -let vm; let writeFileAtomic; jest.mock('write-file-atomic', () => ({ @@ -157,8 +155,6 @@ describe('ScriptTransformer', () => { object = data => Object.assign(Object.create(null), data); - vm = require('vm'); - mockFs = object({ '/fruits/banana.js': ['module.exports = "banana";'].join('\n'), '/fruits/banana:colon.js': ['module.exports = "bananaColon";'].join('\n'), @@ -213,52 +209,46 @@ describe('ScriptTransformer', () => { it('transforms a file properly', () => { const scriptTransformer = new ScriptTransformer(config); - const response = scriptTransformer.transform( + const transformedBananaWithCoverage = scriptTransformer.transform( '/fruits/banana.js', makeGlobalConfig({collectCoverage: true}), - ).script; + ); - expect(response instanceof vm.Script).toBe(true); - expect(wrap(vm.Script.mock.calls[0][0])).toMatchSnapshot(); + expect(wrap(transformedBananaWithCoverage.code)).toMatchSnapshot(); // no-cache case expect(fs.readFileSync).toHaveBeenCalledTimes(1); expect(fs.readFileSync).toBeCalledWith('/fruits/banana.js', 'utf8'); // in-memory cache - const response2 = scriptTransformer.transform( + const transformedBananaWithCoverageAgain = scriptTransformer.transform( '/fruits/banana.js', makeGlobalConfig({collectCoverage: true}), - ).script; - expect(response2).toBe(response); - - scriptTransformer.transform( - '/fruits/kiwi.js', - makeGlobalConfig({collectCoverage: true}), ); - const snapshot = vm.Script.mock.calls[1][0]; - expect(wrap(snapshot)).toMatchSnapshot(); + expect(transformedBananaWithCoverageAgain).toBe( + transformedBananaWithCoverage, + ); - scriptTransformer.transform( + const transformedKiwiWithCoverage = scriptTransformer.transform( '/fruits/kiwi.js', makeGlobalConfig({collectCoverage: true}), ); + expect(wrap(transformedKiwiWithCoverage.code)).toMatchSnapshot(); - expect(vm.Script.mock.calls[0][0]).not.toEqual(snapshot); - expect(vm.Script.mock.calls[0][0]).not.toMatch(/instrumented kiwi/); + expect(transformedBananaWithCoverage.code).not.toEqual( + transformedKiwiWithCoverage.code, + ); + expect(transformedBananaWithCoverage.code).not.toMatch(/instrumented kiwi/); // If we disable coverage, we get a different result. - scriptTransformer.transform( + const transformedKiwiWithoutCoverage = scriptTransformer.transform( '/fruits/kiwi.js', makeGlobalConfig({collectCoverage: false}), ); - expect(vm.Script.mock.calls[1][0]).toEqual(snapshot); - scriptTransformer.transform('/fruits/banana.js', { - // to make sure jest isn't declared twice - extraGlobals: ['Math', 'jest'], - }).script; - expect(wrap(vm.Script.mock.calls[3][0])).toMatchSnapshot(); + expect(transformedKiwiWithoutCoverage.code).not.toEqual( + transformedKiwiWithCoverage.code, + ); }); it('does not transform Node core modules', () => { @@ -272,10 +262,9 @@ describe('ScriptTransformer', () => { 'fs', {isCoreModule: true}, fsSourceCode, - ).script; + ); - expect(response instanceof vm.Script).toBe(true); - expect(vm.Script.mock.calls[0][0]).toContain(fsSourceCode); + expect(response.code).toEqual(fsSourceCode); // Native files should never be transformed. expect(shouldInstrument).toHaveBeenCalledTimes(0); @@ -358,15 +347,15 @@ describe('ScriptTransformer', () => { it('uses the supplied preprocessor', () => { config = {...config, transform: [['^.+\\.js$', 'test_preprocessor']]}; const scriptTransformer = new ScriptTransformer(config); - scriptTransformer.transform('/fruits/banana.js', {}); + const res1 = scriptTransformer.transform('/fruits/banana.js', {}); expect(require('test_preprocessor').getCacheKey).toBeCalled(); - expect(wrap(vm.Script.mock.calls[0][0])).toMatchSnapshot(); + expect(wrap(res1.code)).toMatchSnapshot(); - scriptTransformer.transform('/node_modules/react.js', {}); + const res2 = scriptTransformer.transform('/node_modules/react.js', {}); // ignores preprocessor - expect(wrap(vm.Script.mock.calls[1][0])).toMatchSnapshot(); + expect(wrap(res2.code)).toMatchSnapshot(); }); it('uses multiple preprocessors', () => { @@ -379,17 +368,17 @@ describe('ScriptTransformer', () => { }; const scriptTransformer = new ScriptTransformer(config); - scriptTransformer.transform('/fruits/banana.js', {}); - scriptTransformer.transform('/styles/App.css', {}); + const res1 = scriptTransformer.transform('/fruits/banana.js', {}); + const res2 = scriptTransformer.transform('/styles/App.css', {}); expect(require('test_preprocessor').getCacheKey).toBeCalled(); expect(require('css-preprocessor').getCacheKey).toBeCalled(); - expect(wrap(vm.Script.mock.calls[0][0])).toMatchSnapshot(); - expect(wrap(vm.Script.mock.calls[1][0])).toMatchSnapshot(); + expect(wrap(res1.code)).toMatchSnapshot(); + expect(wrap(res2.code)).toMatchSnapshot(); - scriptTransformer.transform('/node_modules/react.js', {}); + const res3 = scriptTransformer.transform('/node_modules/react.js', {}); // ignores preprocessor - expect(wrap(vm.Script.mock.calls[2][0])).toMatchSnapshot(); + expect(wrap(res3.code)).toMatchSnapshot(); }); it('writes source map if preprocessor supplies it', () => { diff --git a/packages/jest-transform/src/enhanceUnexpectedTokenMessage.ts b/packages/jest-transform/src/enhanceUnexpectedTokenMessage.ts index 0e146d8944b0..69a0ac2a2660 100644 --- a/packages/jest-transform/src/enhanceUnexpectedTokenMessage.ts +++ b/packages/jest-transform/src/enhanceUnexpectedTokenMessage.ts @@ -9,7 +9,26 @@ import chalk = require('chalk'); const DOT = ' \u2022 '; -export default function enhanceUnexpectedTokenMessage(e: Error) { +export default function handlePotentialSyntaxError( + e: Error & {codeFrame?: string}, +) { + if (e.codeFrame) { + e.stack = e.message + '\n' + e.codeFrame; + } + + if ( + e instanceof SyntaxError && + (e.message.includes('Unexpected token') || + e.message.includes('Cannot use import')) && + !e.message.includes(' expected') + ) { + throw enhanceUnexpectedTokenMessage(e); + } + + return e; +} + +export function enhanceUnexpectedTokenMessage(e: Error) { e.stack = `${chalk.bold.red('Jest encountered an unexpected token')} diff --git a/packages/jest-transform/src/index.ts b/packages/jest-transform/src/index.ts index 5ef88c45ce60..392b79c9a410 100644 --- a/packages/jest-transform/src/index.ts +++ b/packages/jest-transform/src/index.ts @@ -15,3 +15,4 @@ export { ShouldInstrumentOptions, Options as TransformationOptions, } from './types'; +export {default as handlePotentialSyntaxError} from './enhanceUnexpectedTokenMessage'; diff --git a/packages/jest-transform/src/types.ts b/packages/jest-transform/src/types.ts index e83fa34fcd1b..064ebf380cb9 100644 --- a/packages/jest-transform/src/types.ts +++ b/packages/jest-transform/src/types.ts @@ -5,22 +5,19 @@ * LICENSE file in the root directory of this source tree. */ -import {Script} from 'vm'; import {RawSourceMap} from 'source-map'; import {Config} from '@jest/types'; export type ShouldInstrumentOptions = Pick< Config.GlobalConfig, 'collectCoverage' | 'collectCoverageFrom' | 'collectCoverageOnlyFrom' -> & { - changedFiles?: Set; -}; +> & {changedFiles?: Set}; export type Options = ShouldInstrumentOptions & - Partial> & { - isCoreModule?: boolean; - isInternalModule?: boolean; - }; + Partial<{ + isCoreModule: boolean; + isInternalModule: boolean; + }>; // https://stackoverflow.com/a/48216010/1850276 type Omit = Pick>; @@ -37,7 +34,7 @@ export type TransformedSource = { }; export type TransformResult = { - script: Script; + code: string; mapCoverage: boolean; sourceMapPath: string | null; };