Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: return transformed code as a string, do not wrap in vm.Script #9253

Merged
merged 13 commits into from Dec 1, 2019
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
1 change: 0 additions & 1 deletion packages/jest-environment/package.json
Expand Up @@ -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"
},
Expand Down
6 changes: 2 additions & 4 deletions packages/jest-environment/src/index.ts
Expand Up @@ -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,
Expand All @@ -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'],
Expand All @@ -43,9 +43,7 @@ export declare class JestEnvironment {
fakeTimers: LegacyFakeTimers<unknown> | null;
fakeTimersLolex: LolexFakeTimers | null;
moduleMocker: jestMock.ModuleMocker | null;
runScript(
script: Script,
): {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null;
runScript<T = unknown>(script: Script): T | null;
setup(): Promise<void>;
teardown(): Promise<void>;
handleTestEvent?(event: Circus.Event, state: Circus.State): void;
Expand Down
1 change: 0 additions & 1 deletion packages/jest-environment/tsconfig.json
Expand Up @@ -6,7 +6,6 @@
},
"references": [
{"path": "../jest-fake-timers"},
{"path": "../jest-transform"},
{"path": "../jest-types"},
{"path": "../jest-util"}
]
Expand Down
27 changes: 27 additions & 0 deletions packages/jest-runtime/src/__tests__/runtime_require_module.test.js
Expand Up @@ -347,4 +347,31 @@ describe('Runtime requireModule', () => {
);
expect(exports.isJSONModuleEncodedInUTF8WithBOM).toBe(true);
}));

it('generates the correct args for the module wrapper', async () => {
let runtime = await createRuntime(__filename);

expect(runtime.constructInjectedModuleArguments()).toEqual([
'module',
'exports',
'require',
'__dirname',
'__filename',
'global',
'jest',
]);

runtime = await createRuntime(__filename, {extraGlobals: ['Math']});

expect(runtime.constructInjectedModuleArguments()).toEqual([
'module',
'exports',
'require',
'__dirname',
'__filename',
'global',
'jest',
'Math',
]);
});
});
41 changes: 38 additions & 3 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -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');
Expand Down Expand Up @@ -78,6 +80,10 @@ const getModuleNameMapper = (config: Config.ProjectConfig) => {

const unmockRegExpCache = new WeakMap();

const EVAL_RESULT_VARIABLE = 'Object.<anonymous>';

type RunScriptEvalResult = {[EVAL_RESULT_VARIABLE]: ModuleWrapper};

/* eslint-disable-next-line no-redeclare */
class Runtime {
static ScriptTransformer: typeof ScriptTransformer;
Expand Down Expand Up @@ -489,7 +495,6 @@ class Runtime {
collectCoverage: this._coverageOptions.collectCoverage,
collectCoverageFrom: this._coverageOptions.collectCoverageFrom,
collectCoverageOnlyFrom: this._coverageOptions.collectCoverageOnlyFrom,
extraGlobals: this._config.extraGlobals,
};
}

Expand Down Expand Up @@ -726,7 +731,14 @@ class Runtime {
}
}

const runScript = this._environment.runScript(transformedFile.script);
const script = new Script(this.wrap(transformedFile.code), {
displayErrors: true,
filename: this._resolver.isCoreModule(filename)
? `jest-nodejs-core-${filename}`
: filename,
});

const runScript = this._environment.runScript<RunScriptEvalResult>(script);

if (runScript === null) {
this._logFormattedReferenceError(
Expand All @@ -737,7 +749,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
Expand Down Expand Up @@ -1086,6 +1098,29 @@ class Runtime {
formatStackTrace(stack, this._config, {noStackTrace: false}),
);
}

private wrap(content: string) {
return (
'({"' +
EVAL_RESULT_VARIABLE +
`":function(${this.constructInjectedModuleArguments().join(',')}){` +
content +
'\n}});'
);
}

private constructInjectedModuleArguments() {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
return [
'module',
'exports',
'require',
'__dirname',
'__filename',
'global',
'jest',
...this._config.extraGlobals,
];
}
}

Runtime.ScriptTransformer = ScriptTransformer;
Expand Down
38 changes: 3 additions & 35 deletions packages/jest-transform/src/ScriptTransformer.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -60,7 +59,6 @@ async function waitForPromiseWithCleanup(
}

export default class ScriptTransformer {
static EVAL_RESULT_VARIABLE: 'Object.<anonymous>';
private _cache: ProjectCache;
private _config: Config.ProjectConfig;
private _transformCache: Map<Config.Path, Transformer>;
Expand Down Expand Up @@ -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;

Expand All @@ -369,19 +366,14 @@ 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) {
Expand Down Expand Up @@ -698,27 +690,3 @@ const calcTransformRegExp = (config: Config.ProjectConfig) => {

return transformRegexp;
};

const wrap = (content: string, ...extras: Array<string>) => {
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.<anonymous>';
Expand Up @@ -75,7 +75,7 @@ Object {
`;

exports[`ScriptTransformer transforms a file properly 1`] = `
({"Object.<anonymous>":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";
Expand Down Expand Up @@ -122,11 +122,10 @@ function cov_25u22311x4() {

cov_25u22311x4().s[0]++;
module.exports = "banana";
}});
`;

exports[`ScriptTransformer transforms a file properly 2`] = `
({"Object.<anonymous>":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";
Expand Down Expand Up @@ -217,48 +216,33 @@ module.exports = () => {
cov_23yvu8etmu().s[1]++;
return "kiwi";
};
}});
`;

exports[`ScriptTransformer transforms a file properly 3`] = `
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest,Math){module.exports = "banana";
}});
`;

exports[`ScriptTransformer uses multiple preprocessors 1`] = `
({"Object.<anonymous>":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.<anonymous>":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.<anonymous>":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.<anonymous>":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.<anonymous>":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.`;