diff --git a/packages/jest-cli/src/__tests__/__fixtures__/run/aFile.js b/packages/jest-cli/src/__tests__/__fixtures__/run/aFile.js new file mode 100644 index 000000000000..fcdaa7a88d0a --- /dev/null +++ b/packages/jest-cli/src/__tests__/__fixtures__/run/aFile.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +module.exports = { + toReplace: 1, +}; diff --git a/packages/jest-cli/src/__tests__/__fixtures__/run/aFile_spec.js b/packages/jest-cli/src/__tests__/__fixtures__/run/aFile_spec.js new file mode 100644 index 000000000000..a3fc4f8165a8 --- /dev/null +++ b/packages/jest-cli/src/__tests__/__fixtures__/run/aFile_spec.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +const expect = require('expect'); +const aFile = require('./aFile'); + +describe('aFile test', () => { + it('should have transformed aFile', () => { + expect(JSON.stringify(aFile)).toEqual(JSON.stringify({runReplaced: 1})); + }); +}); diff --git a/packages/jest-cli/src/__tests__/__fixtures__/run/transform-module.js b/packages/jest-cli/src/__tests__/__fixtures__/run/transform-module.js new file mode 100644 index 000000000000..4b6d60e96ba9 --- /dev/null +++ b/packages/jest-cli/src/__tests__/__fixtures__/run/transform-module.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +module.exports = { + process: (src, filename) => src.replace('toReplace', 'runReplaced'), +}; diff --git a/packages/jest-cli/src/__tests__/__fixtures__/runCLI/aFile.js b/packages/jest-cli/src/__tests__/__fixtures__/runCLI/aFile.js new file mode 100644 index 000000000000..fcdaa7a88d0a --- /dev/null +++ b/packages/jest-cli/src/__tests__/__fixtures__/runCLI/aFile.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +module.exports = { + toReplace: 1, +}; diff --git a/packages/jest-cli/src/__tests__/__fixtures__/runCLI/aFile_spec.js b/packages/jest-cli/src/__tests__/__fixtures__/runCLI/aFile_spec.js new file mode 100644 index 000000000000..b992de8e8899 --- /dev/null +++ b/packages/jest-cli/src/__tests__/__fixtures__/runCLI/aFile_spec.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +const expect = require('expect'); +const aFile = require('./aFile'); + +describe('aFile test', () => { + it('should have transformed aFile', () => { + expect(JSON.stringify(aFile)).toEqual(JSON.stringify({runCLIReplaced: 1})); + }); +}); diff --git a/packages/jest-cli/src/__tests__/__fixtures__/runCLI/transform-module.js b/packages/jest-cli/src/__tests__/__fixtures__/runCLI/transform-module.js new file mode 100644 index 000000000000..66370bd9efb1 --- /dev/null +++ b/packages/jest-cli/src/__tests__/__fixtures__/runCLI/transform-module.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +module.exports = { + process: (src, filename) => src.replace('toReplace', 'runCLIReplaced'), +}; diff --git a/packages/jest-cli/src/__tests__/cli/run.test.js b/packages/jest-cli/src/__tests__/cli/run.test.js new file mode 100644 index 000000000000..79f7b8f09221 --- /dev/null +++ b/packages/jest-cli/src/__tests__/cli/run.test.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +import path from 'path'; +import {run} from '../../cli'; + +const runProject = path.join(__dirname, '../__fixtures__/run'); + +jest.mock('exit'); + +const runArgvString = [ + '--config', + JSON.stringify({ + rootDir: runProject, + testMatch: ['/*_spec.js'], + transform: { + '^.+\\.jsx?$': './transform-module', + }, + }), +]; + +const runArgvObject = [ + '--config', + { + rootDir: runProject, + testMatch: ['/*_spec.js'], + transform: { + '^.+\\.jsx?$': './transform-module', + }, + }, +]; + +const processOnFn = process.on; +const processExitFn = process.exit; +const processErrWriteFn = process.stderr.write; +const consoleErrorFn = console.error; + +const noSubTestLogs = true; + +describe('run', () => { + beforeEach(() => { + process.on = jest.fn(); + process.on.mockReset(); + process.exit = jest.fn(); + process.exit.mockReset(); + if (noSubTestLogs) { + process.stderr.write = jest.fn(); + process.stderr.write.mockReset(); + console.error = jest.fn(); + console.error.mockReset(); + } + }); + + afterEach(() => { + process.on = processOnFn; + process.exit = processExitFn; + if (noSubTestLogs) { + process.stderr.write = processErrWriteFn; + console.error = consoleErrorFn; + } + }); + + describe('config as string', () => { + it('passes the test when the config has a transform module path', async () => { + let runResult = null; + let error = null; + try { + runResult = await run(runArgvString, runProject); + } catch (ex) { + error = ex; + } + const numPassedTests = runResult ? runResult.numPassedTests : -1; + expect(error).toBe(null); + expect(numPassedTests).toBe(1); + }); + }); + + describe('config as object', () => { + it('throws running the test when the config is an object', async () => { + let runResult = null; + let error = null; + try { + runResult = await run(runArgvObject, runProject); + } catch (ex) { + error = ex; + } + const numPassedTests = runResult ? runResult.numPassedTests : -1; + expect(error).not.toBe(null); + expect(numPassedTests).toBe(-1); + }); + }); +}); diff --git a/packages/jest-cli/src/__tests__/cli/runCLI.test.js b/packages/jest-cli/src/__tests__/cli/runCLI.test.js new file mode 100644 index 000000000000..d7d7371ed425 --- /dev/null +++ b/packages/jest-cli/src/__tests__/cli/runCLI.test.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + */ + +'use strict'; + +import path from 'path'; +import {runCLI} from '../../cli'; +import transformModule from '../__fixtures__/runCLI/transform-module'; + +const project = path.join(__dirname, '../__fixtures__/runCLI'); +const projects = [project]; + +const argvObject = { + config: { + testMatch: ['/*_spec.js'], + transform: { + '^.+\\.jsx?$': () => transformModule, + }, + }, +}; + +const argvString = { + config: JSON.stringify({ + rootDir: project, + testMatch: ['/*_spec.js'], + transform: { + '^.+\\.jsx?$': './transform-module', + }, + }), +}; + +const processErrWriteFn = process.stderr.write; + +const noSubTestLogs = true; + +describe('runCLI', () => { + beforeEach(() => { + if (noSubTestLogs) { + process.stderr.write = jest.fn(); + process.stderr.write.mockReset(); + } + }); + + afterEach(() => { + if (noSubTestLogs) { + process.stderr.write = processErrWriteFn; + } + }); + + describe('config as object', () => { + it('passes the test when the config has a transform function', async () => { + let runResult = null; + let error = null; + try { + runResult = await runCLI(argvObject, projects); + } catch (ex) { + error = ex; + } + const numPassedTests = runResult ? runResult.results.numPassedTests : -1; + expect(error).toBe(null); + expect(numPassedTests).toBe(1); + }); + }); + + describe('config as string', () => { + it('passes the test when the config is a string', async () => { + let runResult = null; + let error = null; + try { + runResult = await runCLI(argvString, projects); + } catch (ex) { + error = ex; + } + const numPassedTests = runResult ? runResult.results.numPassedTests : -1; + expect(error).toBe(null); + expect(numPassedTests).toBe(1); + }); + }); +}); diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 889a35f77808..fb6a16c91278 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -33,18 +33,22 @@ import {sync as realpath} from 'realpath-native'; import init from '../lib/init'; import logDebugMessages from '../lib/log_debug_messages'; -export async function run(maybeArgv?: Argv, project?: Path) { +export const run = async ( + maybeArgv?: Argv, + project?: Path, +): Promise => { + let results, globalConfig; try { const argv: Argv = buildArgv(maybeArgv, project); if (argv.init) { await init(); - return; + return results; } const projects = getProjectListFromCLIArgs(argv, project); - const {results, globalConfig} = await runCLI(argv, projects); + ({results, globalConfig} = await runCLI(argv, projects)); readResultsAndExit(results, globalConfig); } catch (error) { clearLine(process.stderr); @@ -53,7 +57,8 @@ export async function run(maybeArgv?: Argv, project?: Path) { exit(1); throw error; } -} + return Promise.resolve(results); +}; export const runCLI = async ( argv: Argv, @@ -170,19 +175,26 @@ const readResultsAndExit = ( }; const buildArgv = (maybeArgv: ?Argv, project: ?Path) => { - const argv: Argv = yargs(maybeArgv || process.argv.slice(2)) - .usage(args.usage) - .alias('help', 'h') - .options(args.options) - .epilogue(args.docs) - .check(args.check).argv; - - validateCLIOptions( - argv, - Object.assign({}, args.options, {deprecationEntries}), - ); + try { + const argv: Argv = yargs(maybeArgv || process.argv.slice(2)) + .usage(args.usage) + .alias('help', 'h') + .options(args.options) + .epilogue(args.docs) + .check(args.check).argv; + + validateCLIOptions( + argv, + Object.assign({}, args.options, {deprecationEntries}), + ); - return argv; + return argv; + } catch (err) { + if (maybeArgv) { + throw new Error('Command line arguments should be an array of strings'); + } + throw err; + } }; const getProjectListFromCLIArgs = (argv, project: ?Path) => { diff --git a/packages/jest-config/src/__tests__/normalize.test.js b/packages/jest-config/src/__tests__/normalize.test.js index 28c2aea79422..95334daf3005 100644 --- a/packages/jest-config/src/__tests__/normalize.test.js +++ b/packages/jest-config/src/__tests__/normalize.test.js @@ -285,7 +285,7 @@ describe('transform', () => { Resolver.findNodeModule = jest.fn(name => name); }); - it('normalizes the path', () => { + it('normalizes the path if it is a string', () => { const {options} = normalize( { rootDir: '/root/', @@ -304,6 +304,28 @@ describe('transform', () => { ['abs-path', '/qux/quux'], ]); }); + + it('does not normalize the path if it is a function', () => { + const theFunction = () => {}; + + const {options} = normalize( + { + rootDir: '/root/', + transform: { + [DEFAULT_CSS_PATTERN]: '/node_modules/jest-regex-util', + [DEFAULT_JS_PATTERN]: theFunction, + 'abs-path': '/qux/quux', + }, + }, + {}, + ); + + expect(options.transform).toEqual([ + [DEFAULT_CSS_PATTERN, '/root/node_modules/jest-regex-util'], + [DEFAULT_JS_PATTERN, theFunction], + ['abs-path', '/qux/quux'], + ]); + }); }); describe('haste', () => { @@ -1077,6 +1099,8 @@ describe('preset', () => { }); test('merges with options', () => { + const aFunction = () => {}; + const {options} = normalize( { moduleNameMapper: {a: 'a'}, @@ -1084,7 +1108,7 @@ describe('preset', () => { preset: 'react-native', rootDir: '/root/path/foo', setupFiles: ['a'], - transform: {a: 'a'}, + transform: {a: 'a', c: aFunction}, }, {}, ); @@ -1095,9 +1119,11 @@ describe('preset', () => { '/node_modules/a', '/node_modules/b', ]); - expect(options.transform).toEqual([ + const sorter = (a, b) => a[0].localeCompare(b[0]); + expect(options.transform.sort(sorter)).toEqual([ ['a', '/node_modules/a'], ['b', '/node_modules/b'], + ['c', aFunction], ]); }); diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index b1832b94af6a..e8f0dac8bbff 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -60,16 +60,23 @@ export function readConfig( 'Jest: Cannot use configuration as an object without a file path.', ); } - } else if (isJSONString(argv.config)) { + } else if ( + argv.config && + (typeof argv.config === 'object' || isJSONString(argv.config)) + ) { // A JSON string was passed to `--config` argument and we can parse it // and use as is. let config; - try { - config = JSON.parse(argv.config); - } catch (e) { - throw new Error( - 'There was an error while parsing the `--config` argument as a JSON string.', - ); + if (typeof argv.config === 'object') { + config = argv.config; + } else { + try { + config = JSON.parse(argv.config); + } catch (e) { + throw new Error( + 'There was an error while parsing the `--config` argument as a JSON string.', + ); + } } // NOTE: we might need to resolve this dir to an absolute path in the future diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index b29e3e690091..8efbf6f1d4cc 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -144,7 +144,11 @@ const setupBabelJest = (options: InitialOptions) => { if (customJSTransformer === 'babel-jest') { babelJest = require.resolve('babel-jest'); transform[customJSPattern] = babelJest; - } else if (customJSTransformer.includes('babel-jest')) { + } else if ( + customJSTransformer && + customJSTransformer.includes && + customJSTransformer.includes('babel-jest') + ) { babelJest = customJSTransformer; } } @@ -535,11 +539,13 @@ export default function normalize(options: InitialOptions, argv: Argv) { transform && Object.keys(transform).map(regex => [ regex, - resolve(newOptions.resolver, { - filePath: transform[regex], - key, - rootDir: options.rootDir, - }), + typeof transform[regex] === 'string' + ? resolve(newOptions.resolver, { + filePath: transform[regex], + key, + rootDir: options.rootDir, + }) + : transform[regex], ]); break; case 'coveragePathIgnorePatterns': 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 68522bebc832..ddde78369fd1 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 @@ -180,7 +180,7 @@ exports[`ScriptTransformer uses multiple preprocessors 3`] = ` }});" `; -exports[`ScriptTransformer uses the supplied preprocessor 1`] = ` +exports[`ScriptTransformer uses the supplied preprocessor if it is a file path 1`] = ` "({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){ const TRANSFORMED = { filename: '/fruits/banana.js', @@ -191,7 +191,25 @@ exports[`ScriptTransformer uses the supplied preprocessor 1`] = ` }});" `; -exports[`ScriptTransformer uses the supplied preprocessor 2`] = ` +exports[`ScriptTransformer uses the supplied preprocessor if it is a file path 2`] = ` +"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){module.exports = \\"react\\"; +}});" +`; + +exports[`ScriptTransformer uses the supplied preprocessor if it is a function 1`] = ` +"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){ + const TRANSFORMED = { + filename: '/fruits/banana.js', + script: 'module.exports = \\"banana\\";', + config: '{\\"cache\\":true,\\"cacheDirectory\\":\\"/cache/\\",\\"name\\":\\"test\\",\\"rootDir\\":\\"/\\",\\"transformIgnorePatterns\\":[\\"/node_modules/\\"],\\"transform\\":[[\\"^.+\\\\\\\\.js$\\",null]]}', + }; + +}});" +`; + +exports[`ScriptTransformer uses the supplied preprocessor if it is a function 2`] = `"module.exports = \\"banana\\";"`; + +exports[`ScriptTransformer uses the supplied preprocessor if it is a function 3`] = ` "({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){module.exports = \\"react\\"; }});" `; diff --git a/packages/jest-runtime/src/__tests__/script_transformer.test.js b/packages/jest-runtime/src/__tests__/script_transformer.test.js index 60ae65c665ff..71c2671030c9 100644 --- a/packages/jest-runtime/src/__tests__/script_transformer.test.js +++ b/packages/jest-runtime/src/__tests__/script_transformer.test.js @@ -316,7 +316,7 @@ describe('ScriptTransformer', () => { ).not.toThrow(); }); - it('uses the supplied preprocessor', () => { + it('uses the supplied preprocessor if it is a file path', () => { config = Object.assign(config, { transform: [['^.+\\.js$', 'test_preprocessor']], }); @@ -332,6 +332,36 @@ describe('ScriptTransformer', () => { expect(vm.Script.mock.calls[1][0]).toMatchSnapshot(); }); + it('uses the supplied preprocessor if it is a function', () => { + const escapeStrings = str => str.replace(/'/, `'`); + + const process = jest.fn( + (content, filename, config) => ` + const TRANSFORMED = { + filename: '${escapeStrings(filename)}', + script: '${escapeStrings(content)}', + config: '${escapeStrings(JSON.stringify(config))}', + }; + `, + ); + + const preprocessor = jest.fn(() => ({process})); + config = Object.assign(config, { + transform: [['^.+\\.js$', preprocessor]], + }); + const scriptTransformer = new ScriptTransformer(config); + scriptTransformer.transform('/fruits/banana.js', {}); + + expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + expect(process.mock.calls[0][0]).toMatchSnapshot(); + + scriptTransformer.transform('/node_modules/react.js', {}); + + expect(process.mock.calls.length).toBe(1); + // ignores preprocessor + expect(vm.Script.mock.calls[1][0]).toMatchSnapshot(); + }); + it('uses multiple preprocessors', () => { config = Object.assign(config, { transform: [ diff --git a/packages/jest-runtime/src/script_transformer.js b/packages/jest-runtime/src/script_transformer.js index a25683156afd..c8dfa2edf0d3 100644 --- a/packages/jest-runtime/src/script_transformer.js +++ b/packages/jest-runtime/src/script_transformer.js @@ -131,7 +131,7 @@ export default class ScriptTransformer { return cachePath; } - _getTransformPath(filename: Path) { + _getTransformFunctionOrPath(filename: Path) { for (let i = 0; i < this._config.transform.length; i++) { if (new RegExp(this._config.transform[i][0]).test(filename)) { return this._config.transform[i][1]; @@ -146,15 +146,19 @@ export default class ScriptTransformer { return null; } - const transformPath = this._getTransformPath(filename); - if (transformPath) { - const transformer = this._transformCache.get(transformPath); - if (transformer != null) { - return transformer; - } + const transformFunctionOrPath = this._getTransformFunctionOrPath(filename); + if (transformFunctionOrPath) { + if (typeof transformFunctionOrPath === 'function') { + transform = (transformFunctionOrPath(): Transformer); + } else { + const transformer = this._transformCache.get(transformFunctionOrPath); + if (transformer != null) { + return transformer; + } - // $FlowFixMe - transform = (require(transformPath): Transformer); + // $FlowFixMe + transform = (require(transformFunctionOrPath): Transformer); + } if (typeof transform.createTransformer === 'function') { transform = transform.createTransformer(); } @@ -163,7 +167,9 @@ export default class ScriptTransformer { 'Jest: a transform must export a `process` function.', ); } - this._transformCache.set(transformPath, transform); + if (typeof transformFunctionOrPath !== 'function') { + this._transformCache.set(transformFunctionOrPath, transform); + } } return transform; }