diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e9a0acec48..dd9c3f7dd2c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `[jest-haste-map]` [**BREAKING**] Remove name from hash in `HasteMap.getCacheFilePath` ([#7218](https://github.com/facebook/jest/pull/7218)) - `[babel-preset-jest]` [**BREAKING**] Export a function instead of an object for Babel 7 compatibility ([#7203](https://github.com/facebook/jest/pull/7203)) - `[jest-haste-map]` [**BREAKING**] Expose relative paths when getting the file iterator ([#7321](https://github.com/facebook/jest/pull/7321)) +- `[jest-runtime]` Add `extraGlobals` to config to load extra global variables into the execution vm ([#7454](https://github.com/facebook/jest/pull/7454)) - `[jest-util]` Export `specialChars` containing Unicode characters and ANSI escapes for console output ([#7532](https://github.com/facebook/jest/pull/7532)) - `[jest-config]` Handle typescript (`ts` and `tsx`) by default ([#7533](https://github.com/facebook/jest/pull/7533)) - `[jest-validate]` Add support for comments in `package.json` using a `"//"` key ([#7295](https://github.com/facebook/jest/pull/7295)) diff --git a/TestUtils.js b/TestUtils.js index 9203fad967e4..8023e5189826 100644 --- a/TestUtils.js +++ b/TestUtils.js @@ -26,6 +26,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { enabledTestsMap: null, errorOnDeprecated: false, expand: false, + extraGlobals: [], filter: null, findRelatedTests: false, forceExit: false, @@ -77,6 +78,7 @@ const DEFAULT_PROJECT_CONFIG: ProjectConfig = { detectOpenHandles: false, displayName: undefined, errorOnDeprecated: false, + extraGlobals: [], filter: null, forceCoverageMatch: [], globals: {}, diff --git a/docs/Configuration.md b/docs/Configuration.md index 302e9ad385af..554f4a752154 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -273,6 +273,23 @@ Default: `false` Make calling deprecated APIs throw helpful error messages. Useful for easing the upgrade process. +### `extraGlobals` [array] + +Default: `undefined` + +Test files run inside a [vm](https://nodejs.org/api/vm.html), which slows calls to global context properties (e.g. `Math`). With this option you can specify extra properties to be defined inside the vm for faster lookups. + +For example, if your tests call `Math` often, you can pass it by setting `extraGlobals`. + +```json +{ + ... + "jest": { + "extraGlobals": ["Math"] + } +} +``` + ### `forceCoverageMatch` [array] Default: `['']` diff --git a/packages/jest-config/src/ValidConfig.js b/packages/jest-config/src/ValidConfig.js index c91c60b938a9..315f4436919a 100644 --- a/packages/jest-config/src/ValidConfig.js +++ b/packages/jest-config/src/ValidConfig.js @@ -44,6 +44,7 @@ export default ({ displayName: 'project-name', errorOnDeprecated: false, expand: false, + extraGlobals: [], filter: '/filter.js', forceCoverageMatch: ['**/*.t.js'], forceExit: false, diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index b1832b94af6a..118677010793 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -114,6 +114,7 @@ const groupOptions = ( enabledTestsMap: options.enabledTestsMap, errorOnDeprecated: options.errorOnDeprecated, expand: options.expand, + extraGlobals: options.extraGlobals, filter: options.filter, findRelatedTests: options.findRelatedTests, forceExit: options.forceExit, @@ -165,6 +166,7 @@ const groupOptions = ( detectOpenHandles: options.detectOpenHandles, displayName: options.displayName, errorOnDeprecated: options.errorOnDeprecated, + extraGlobals: options.extraGlobals, filter: options.filter, forceCoverageMatch: options.forceCoverageMatch, globals: options.globals, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 16e31b27bf30..9eca803ad751 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -660,6 +660,7 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'displayName': case 'errorOnDeprecated': case 'expand': + case 'extraGlobals': case 'globals': case 'findRelatedTests': case 'forceCoverageMatch': diff --git a/packages/jest-runtime/src/__tests__/__snapshots__/script_transformer.test.js.snap b/packages/jest-runtime/src/__tests__/__snapshots__/script_transformer.test.js.snap index 352ee03642f2..de782a1c3c6a 100644 --- a/packages/jest-runtime/src/__tests__/__snapshots__/script_transformer.test.js.snap +++ b/packages/jest-runtime/src/__tests__/__snapshots__/script_transformer.test.js.snap @@ -158,6 +158,11 @@ module.exports = () => { }});" `; +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 = { diff --git a/packages/jest-runtime/src/__tests__/script_transformer.test.js b/packages/jest-runtime/src/__tests__/script_transformer.test.js index f9985c667359..7233f340297e 100644 --- a/packages/jest-runtime/src/__tests__/script_transformer.test.js +++ b/packages/jest-runtime/src/__tests__/script_transformer.test.js @@ -232,6 +232,12 @@ describe('ScriptTransformer', () => { // If we disable coverage, we get a different result. scriptTransformer.transform('/fruits/kiwi.js', {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(vm.Script.mock.calls[3][0]).toMatchSnapshot(); }); it('does not transform Node core modules', () => { diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index d29cf5fc590e..bb8be2d1824b 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -645,13 +645,14 @@ class Runtime { Object.defineProperty(localModule, 'require', { value: this._createRequireImplementation(localModule, options), }); - + const extraGlobals = this._config.extraGlobals || []; const transformedFile = this._scriptTransformer.transform( filename, { collectCoverage: this._coverageOptions.collectCoverage, collectCoverageFrom: this._coverageOptions.collectCoverageFrom, collectCoverageOnlyFrom: this._coverageOptions.collectCoverageOnlyFrom, + extraGlobals, isInternalModule, }, this._cacheFS[filename], @@ -686,8 +687,7 @@ class Runtime { } const wrapper = runScript[ScriptTransformer.EVAL_RESULT_VARIABLE]; - wrapper.call( - localModule.exports, // module context + const moduleArguments = new Set([ localModule, // module object localModule.exports, // module exports localModule.require, // require implementation @@ -699,7 +699,17 @@ class Runtime { // $FlowFixMe (localModule.require: LocalModuleRequire), ), // jest object - ); + ...extraGlobals.map(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.`, + ); + }), + ]); + wrapper.call(localModule.exports, ...Array.from(moduleArguments)); this._isCurrentlyExecutingManualMock = origCurrExecutingManualMock; this._currentlyExecutingModulePath = lastExecutingModulePath; diff --git a/packages/jest-runtime/src/script_transformer.js b/packages/jest-runtime/src/script_transformer.js index 439972da93b0..8b446b979c63 100644 --- a/packages/jest-runtime/src/script_transformer.js +++ b/packages/jest-runtime/src/script_transformer.js @@ -36,6 +36,7 @@ export type Options = {| collectCoverage: boolean, collectCoverageFrom: Array, collectCoverageOnlyFrom: ?{[key: string]: boolean, __proto__: null}, + extraGlobals?: Array, isCoreModule?: boolean, isInternalModule?: boolean, |}; @@ -313,6 +314,8 @@ export default class ScriptTransformer { (this._shouldTransform(filename) || instrument); try { + const extraGlobals = (options && options.extraGlobals) || []; + if (willTransform) { const transformedSource = this.transformSource( filename, @@ -320,11 +323,11 @@ export default class ScriptTransformer { instrument, ); - wrappedCode = wrap(transformedSource.code); + wrappedCode = wrap(transformedSource.code, ...extraGlobals); sourceMapPath = transformedSource.sourceMapPath; mapCoverage = transformedSource.mapCoverage; } else { - wrappedCode = wrap(content); + wrappedCode = wrap(content, ...extraGlobals); } return { @@ -525,11 +528,25 @@ const calcIgnorePatternRegexp = (config: ProjectConfig): ?RegExp => { return new RegExp(config.transformIgnorePatterns.join('|')); }; -const wrap = content => - '({"' + - ScriptTransformer.EVAL_RESULT_VARIABLE + - '":function(module,exports,require,__dirname,__filename,global,jest){' + - content + - '\n}});'; +const wrap = (content, ...extras) => { + const globals = new Set([ + 'module', + 'exports', + 'require', + '__dirname', + '__filename', + 'global', + 'jest', + ...extras, + ]); + + return ( + '({"' + + ScriptTransformer.EVAL_RESULT_VARIABLE + + `":function(${Array.from(globals).join(',')}){` + + content + + '\n}});' + ); +}; ScriptTransformer.EVAL_RESULT_VARIABLE = 'Object.'; diff --git a/types/Config.js b/types/Config.js index 9eff1a77796e..d301dcf69b38 100644 --- a/types/Config.js +++ b/types/Config.js @@ -111,6 +111,7 @@ export type InitialOptions = { detectOpenHandles?: boolean, displayName?: string, expand?: boolean, + extraGlobals?: Array, filter?: Path, findRelatedTests?: boolean, forceCoverageMatch?: Array, @@ -204,6 +205,7 @@ export type GlobalConfig = {| detectOpenHandles: boolean, enabledTestsMap: ?{[key: string]: {[key: string]: boolean}}, expand: boolean, + extraGlobals: Array, filter: ?Path, findRelatedTests: boolean, forceExit: boolean, @@ -257,6 +259,7 @@ export type ProjectConfig = {| detectOpenHandles: boolean, displayName: ?string, errorOnDeprecated: boolean, + extraGlobals: Array, filter: ?Path, forceCoverageMatch: Array, globals: ConfigGlobals,