From 47da9cffac0be57a7c9a199aefba06e0986ea309 Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Wed, 26 Jun 2019 07:20:22 -0400 Subject: [PATCH] Transformer configs (#7288) --- CHANGELOG.md | 1 + docs/Configuration.md | 4 +- .../__snapshots__/transform.test.ts.snap | 9 ++ e2e/__tests__/transform.test.ts | 26 +++++ e2e/transform/transformer-config/.babelrc | 1 + .../transformer-config/NotCovered.js | 12 +++ .../__tests__/transformer-config.test.js | 15 +++ e2e/transform/transformer-config/package.json | 25 +++++ .../this-directory-is-covered/Covered.js | 19 ++++ .../ExcludedFromCoverage.js | 15 +++ e2e/transform/transformer-config/yarn.lock | 34 +++++++ .../src/__tests__/normalize.test.js | 18 ++++ packages/jest-config/src/normalize.ts | 95 +++++++++++-------- .../jest-transform/src/ScriptTransformer.ts | 15 ++- .../src/__tests__/script_transformer.test.js | 25 +++++ packages/jest-types/src/Config.ts | 7 +- 16 files changed, 273 insertions(+), 48 deletions(-) create mode 100644 e2e/transform/transformer-config/.babelrc create mode 100644 e2e/transform/transformer-config/NotCovered.js create mode 100644 e2e/transform/transformer-config/__tests__/transformer-config.test.js create mode 100644 e2e/transform/transformer-config/package.json create mode 100644 e2e/transform/transformer-config/this-directory-is-covered/Covered.js create mode 100644 e2e/transform/transformer-config/this-directory-is-covered/ExcludedFromCoverage.js create mode 100644 e2e/transform/transformer-config/yarn.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index e2707c15aecc..901b86ea539c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `[*]` Manage the global timeout with `--testTimeout` command line argument. ([#8456](https://github.com/facebook/jest/pull/8456)) - `[pretty-format]` Render custom displayName of memoized components - `[jest-validate]` Allow `maxWorkers` as part of the `jest.config.js` ([#8565](https://github.com/facebook/jest/pull/8565)) +- `[jest-runtime]` Allow passing configuration objects to transformers ([#7288](https://github.com/facebook/jest/pull/7288)) ### Fixes diff --git a/docs/Configuration.md b/docs/Configuration.md index 97d00a4c2261..bc9f585c63f4 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1072,7 +1072,7 @@ Default: `real` Setting this value to `fake` allows the use of fake timers for functions such as `setTimeout`. Fake timers are useful when a piece of code sets a long timeout that we don't want to wait for in a test. -### `transform` [object] +### `transform` [object] Default: `undefined` @@ -1080,6 +1080,8 @@ A map from regular expressions to paths to transformers. A transformer is a modu Examples of such compilers include [Babel](https://babeljs.io/), [TypeScript](http://www.typescriptlang.org/) and [async-to-gen](http://github.com/leebyron/async-to-gen#jest). +You can pass configuration to a transformer like `{filePattern: ['path-to-transformer', {options}]}` For example, to configure babel-jest for non-default behavior, `{"\\.js$": ['babel-jest', {rootMode: "upward"}]}` + _Note: a transformer is only run once per file unless the file has changed. During development of a transformer it can be useful to run Jest with `--no-cache` to frequently [delete Jest's cache](Troubleshooting.md#caching-issues)._ _Note: if you are using the `babel-jest` transformer and want to use an additional code preprocessor, keep in mind that when "transform" is overwritten in any way the `babel-jest` is not loaded automatically anymore. If you want to use it to compile JavaScript code it has to be explicitly defined. See [babel-jest plugin](https://github.com/facebook/jest/tree/master/packages/babel-jest#setup)_ diff --git a/e2e/__tests__/__snapshots__/transform.test.ts.snap b/e2e/__tests__/__snapshots__/transform.test.ts.snap index a8a139c79177..6d95e205d6e7 100644 --- a/e2e/__tests__/__snapshots__/transform.test.ts.snap +++ b/e2e/__tests__/__snapshots__/transform.test.ts.snap @@ -35,3 +35,12 @@ All files | 83.33 | 100 | 50 | 80 | | ------------|----------|----------|----------|----------|-------------------| `; + +exports[`transformer-config instruments only specific files and collects coverage 1`] = ` +"------------|----------|----------|----------|----------|-------------------| +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | +------------|----------|----------|----------|----------|-------------------| +All files | 83.33 | 100 | 50 | 80 | | + Covered.js | 83.33 | 100 | 50 | 80 | 13 | +------------|----------|----------|----------|----------|-------------------|" +`; diff --git a/e2e/__tests__/transform.test.ts b/e2e/__tests__/transform.test.ts index 9bb9fea5efd0..04534a059c55 100644 --- a/e2e/__tests__/transform.test.ts +++ b/e2e/__tests__/transform.test.ts @@ -159,3 +159,29 @@ describe('ecmascript-modules-support', () => { expect(json.numTotalTests).toBeGreaterThanOrEqual(1); }); }); + +describe('transformer-config', () => { + const dir = path.resolve(__dirname, '..', 'transform/transformer-config'); + + beforeEach(() => { + run('yarn', dir); + }); + + it('runs transpiled code', () => { + // --no-cache because babel can cache stuff and result in false green + const {json} = runWithJson(dir, ['--no-cache']); + expect(json.success).toBe(true); + expect(json.numTotalTests).toBeGreaterThanOrEqual(1); + }); + + it('instruments only specific files and collects coverage', () => { + const {stdout} = runJest(dir, ['--coverage', '--no-cache'], { + stripAnsi: true, + }); + expect(stdout).toMatch('Covered.js'); + expect(stdout).not.toMatch('NotCovered.js'); + expect(stdout).not.toMatch('ExcludedFromCoverage.js'); + // coverage result should not change + expect(stdout).toMatchSnapshot(); + }); +}); diff --git a/e2e/transform/transformer-config/.babelrc b/e2e/transform/transformer-config/.babelrc new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/e2e/transform/transformer-config/.babelrc @@ -0,0 +1 @@ +{} diff --git a/e2e/transform/transformer-config/NotCovered.js b/e2e/transform/transformer-config/NotCovered.js new file mode 100644 index 000000000000..763e42aa4447 --- /dev/null +++ b/e2e/transform/transformer-config/NotCovered.js @@ -0,0 +1,12 @@ +/** + * 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. + */ + +const thisFunctionIsNeverInstrumented = () => null; + +module.exports = { + thisFunctionIsNeverInstrumented, +}; diff --git a/e2e/transform/transformer-config/__tests__/transformer-config.test.js b/e2e/transform/transformer-config/__tests__/transformer-config.test.js new file mode 100644 index 000000000000..92ff5836630e --- /dev/null +++ b/e2e/transform/transformer-config/__tests__/transformer-config.test.js @@ -0,0 +1,15 @@ +/** + * 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. + */ + +'use strict'; + +require('../this-directory-is-covered/ExcludedFromCoverage'); + +it('strips flowtypes using babel-jest and config passed to transformer', () => { + const a: string = 'a'; + expect(a).toBe('a'); +}); diff --git a/e2e/transform/transformer-config/package.json b/e2e/transform/transformer-config/package.json new file mode 100644 index 000000000000..99aaf98fe45c --- /dev/null +++ b/e2e/transform/transformer-config/package.json @@ -0,0 +1,25 @@ +{ + "dependencies": { + "@babel/preset-flow": "7.0.0" + }, + "jest": { + "collectCoverageOnlyFrom": { + "/this-directory-is-covered/Covered.js": true, + "/this-directory-is-covered/ExcludedFromCoverage.js": true + }, + "coveragePathIgnorePatterns": [ + "ExcludedFromCoverage" + ], + "testEnvironment": "node", + "transform": { + "\\.js$": [ + "babel-jest", + { + "presets": [ + "@babel/preset-flow" + ] + } + ] + } + } +} diff --git a/e2e/transform/transformer-config/this-directory-is-covered/Covered.js b/e2e/transform/transformer-config/this-directory-is-covered/Covered.js new file mode 100644 index 000000000000..e120afa84d33 --- /dev/null +++ b/e2e/transform/transformer-config/this-directory-is-covered/Covered.js @@ -0,0 +1,19 @@ +/** + * 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. + */ + +const thisFunctionIsCovered = (): null => null; + +thisFunctionIsCovered(); + +const thisFunctionIsNotCovered = (): void => { + throw new Error('Never Called'); +}; + +module.exports = { + thisFunctionIsCovered, + thisFunctionIsNotCovered, +}; diff --git a/e2e/transform/transformer-config/this-directory-is-covered/ExcludedFromCoverage.js b/e2e/transform/transformer-config/this-directory-is-covered/ExcludedFromCoverage.js new file mode 100644 index 000000000000..12616bcb0a6e --- /dev/null +++ b/e2e/transform/transformer-config/this-directory-is-covered/ExcludedFromCoverage.js @@ -0,0 +1,15 @@ +/** + * 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. + */ + +require('./Covered'); +require('../NotCovered'); + +const thisFunctionIsNeverInstrumented = () => null; + +module.exports = { + thisFunctionIsNeverInstrumented, +}; diff --git a/e2e/transform/transformer-config/yarn.lock b/e2e/transform/transformer-config/yarn.lock new file mode 100644 index 000000000000..d6663766aa10 --- /dev/null +++ b/e2e/transform/transformer-config/yarn.lock @@ -0,0 +1,34 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +babel-plugin-syntax-flow@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + integrity sha1-TDqyCiryaqIM0lmVw5jE63AxDI0= + +babel-plugin-transform-flow-strip-types@6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.8.0.tgz#2351d85e3a52152e1a55d3f08ae635e21ece17a0" + integrity sha1-I1HYXjpSFS4aVdPwiuY14h7OF6A= + dependencies: + babel-plugin-syntax-flow "^6.8.0" + babel-runtime "^6.0.0" + +babel-runtime@^6.0.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" + integrity sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +core-js@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + integrity sha1-TekR5mew6ukSTjQlS1OupvxhjT4= + +regenerator-runtime@^0.10.0: + version "0.10.4" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.4.tgz#74cb6598d3ba2eb18694e968a40e2b3b4df9cf93" + integrity sha1-dMtlmNO6LrGGlOlopA4rO035z5M= diff --git a/packages/jest-config/src/__tests__/normalize.test.js b/packages/jest-config/src/__tests__/normalize.test.js index 3930cd473e09..0fc406830e29 100644 --- a/packages/jest-config/src/__tests__/normalize.test.js +++ b/packages/jest-config/src/__tests__/normalize.test.js @@ -331,6 +331,24 @@ describe('transform', () => { ['abs-path', '/qux/quux'], ]); }); + it("pulls in config if it's passed as an array", () => { + const {options} = normalize( + { + rootDir: '/root/', + transform: { + [DEFAULT_CSS_PATTERN]: '/node_modules/jest-regex-util', + [DEFAULT_JS_PATTERN]: ['babel-jest', {rootMode: 'upward'}], + 'abs-path': '/qux/quux', + }, + }, + {}, + ); + expect(options.transform).toEqual([ + [DEFAULT_CSS_PATTERN, '/root/node_modules/jest-regex-util'], + [DEFAULT_JS_PATTERN, require.resolve('babel-jest'), {rootMode: 'upward'}], + ['abs-path', '/qux/quux'], + ]); + }); }); describe('haste', () => { diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index cf429d39862e..d1f3201a700d 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -46,19 +46,29 @@ type AllOptions = Config.ProjectConfig & Config.GlobalConfig; const createConfigError = (message: string) => new ValidationError(ERROR, message, DOCUMENTATION_NOTE); -const mergeOptionWithPreset = ( +// TS 3.5 forces us to split these into 2 +const mergeModuleNameMapperWithPreset = ( + options: Config.InitialOptions, + preset: Config.InitialOptions, +) => { + if (options['moduleNameMapper'] && preset['moduleNameMapper']) { + options['moduleNameMapper'] = { + ...options['moduleNameMapper'], + ...preset['moduleNameMapper'], + ...options['moduleNameMapper'], + }; + } +}; + +const mergeTransformWithPreset = ( options: Config.InitialOptions, preset: Config.InitialOptions, - optionName: keyof Pick< - Config.InitialOptions, - 'moduleNameMapper' | 'transform' - >, ) => { - if (options[optionName] && preset[optionName]) { - options[optionName] = { - ...options[optionName], - ...preset[optionName], - ...options[optionName], + if (options['transform'] && preset['transform']) { + options['transform'] = { + ...options['transform'], + ...preset['transform'], + ...options['transform'], }; } }; @@ -121,8 +131,8 @@ const setupPreset = ( options.modulePathIgnorePatterns, ); } - mergeOptionWithPreset(options, preset, 'moduleNameMapper'); - mergeOptionWithPreset(options, preset, 'transform'); + mergeModuleNameMapperWithPreset(options, preset); + mergeTransformWithPreset(options, preset); return {...preset, ...options}; }; @@ -140,27 +150,26 @@ const setupBabelJest = (options: Config.InitialOptions) => { return regex.test('a.ts') || regex.test('a.tsx'); }); - if (customJSPattern) { - const customJSTransformer = transform[customJSPattern]; - - if (customJSTransformer === 'babel-jest') { - babelJest = require.resolve('babel-jest'); - transform[customJSPattern] = babelJest; - } else if (customJSTransformer.includes('babel-jest')) { - babelJest = customJSTransformer; - } - } - - if (customTSPattern) { - const customTSTransformer = transform[customTSPattern]; - - if (customTSTransformer === 'babel-jest') { - babelJest = require.resolve('babel-jest'); - transform[customTSPattern] = babelJest; - } else if (customTSTransformer.includes('babel-jest')) { - babelJest = customTSTransformer; + [customJSPattern, customTSPattern].forEach(pattern => { + if (pattern) { + const customTransformer = transform[pattern]; + if (Array.isArray(customTransformer)) { + if (customTransformer[0] === 'babel-jest') { + babelJest = require.resolve('babel-jest'); + customTransformer[0] = babelJest; + } else if (customTransformer[0].includes('babel-jest')) { + babelJest = customTransformer[0]; + } + } else { + if (customTransformer === 'babel-jest') { + babelJest = require.resolve('babel-jest'); + transform[pattern] = babelJest; + } else if (customTransformer.includes('babel-jest')) { + babelJest = customTransformer; + } + } } - } + }); } else { babelJest = require.resolve('babel-jest'); options.transform = { @@ -620,14 +629,20 @@ export default function normalize( const transform = oldOptions[key]; value = transform && - Object.keys(transform).map(regex => [ - regex, - resolve(newOptions.resolver, { - filePath: transform[regex], - key, - rootDir: options.rootDir, - }), - ]); + Object.keys(transform).map(regex => { + const transformElement = transform[regex]; + return [ + regex, + resolve(newOptions.resolver, { + filePath: Array.isArray(transformElement) + ? transformElement[0] + : transformElement, + key, + rootDir: options.rootDir, + }), + ...(Array.isArray(transformElement) ? [transformElement[1]] : []), + ]; + }); break; case 'coveragePathIgnorePatterns': case 'modulePathIgnorePatterns': diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index dff0dd3fae13..aa594db3ed00 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -32,7 +32,7 @@ import enhanceUnexpectedTokenMessage from './enhanceUnexpectedTokenMessage'; type ProjectCache = { configString: string; ignorePatternsRegExp?: RegExp; - transformRegExp?: Array<[RegExp, string]>; + transformRegExp?: Array<[RegExp, string, Record]>; transformedFiles: Map; }; @@ -55,10 +55,12 @@ export default class ScriptTransformer { private _cache: ProjectCache; private _config: Config.ProjectConfig; private _transformCache: Map; + private _transformConfigCache: Map; constructor(config: Config.ProjectConfig) { this._config = config; this._transformCache = new Map(); + this._transformConfigCache = new Map(); let projectCache = projectCaches.get(config); @@ -141,7 +143,10 @@ export default class ScriptTransformer { for (let i = 0; i < transformRegExp.length; i++) { if (transformRegExp[i][0].test(filename)) { - return transformRegExp[i][1]; + const transformPath = transformRegExp[i][1]; + this._transformConfigCache.set(transformPath, transformRegExp[i][2]); + + return transformPath; } } @@ -162,8 +167,9 @@ export default class ScriptTransformer { } transform = require(transformPath) as Transformer; + const transformerConfig = this._transformConfigCache.get(transformPath); if (typeof transform.createTransformer === 'function') { - transform = transform.createTransformer(); + transform = transform.createTransformer(transformerConfig); } if (typeof transform.process !== 'function') { throw new TypeError( @@ -584,11 +590,12 @@ const calcTransformRegExp = (config: Config.ProjectConfig) => { return undefined; } - const transformRegexp: Array<[RegExp, string]> = []; + const transformRegexp: Array<[RegExp, string, Record]> = []; for (let i = 0; i < config.transform.length; i++) { transformRegexp.push([ new RegExp(config.transform[i][0]), config.transform[i][1], + config.transform[i][2], ]); } diff --git a/packages/jest-transform/src/__tests__/script_transformer.test.js b/packages/jest-transform/src/__tests__/script_transformer.test.js index bcacc14fb8ee..7a826e079600 100644 --- a/packages/jest-transform/src/__tests__/script_transformer.test.js +++ b/packages/jest-transform/src/__tests__/script_transformer.test.js @@ -60,6 +60,16 @@ jest.mock( {virtual: true}, ); +jest.mock( + 'configureable-preprocessor', + () => ({ + createTransformer: jest.fn(() => ({ + process: jest.fn(() => 'processedCode'), + })), + }), + {virtual: true}, +); + jest.mock( 'preprocessor-with-sourcemaps', () => ({ @@ -492,6 +502,21 @@ describe('ScriptTransformer', () => { expect(getCacheKey.mock.calls[0][3]).toMatchSnapshot(); }); + it('creates transformer with config', () => { + const transformerConfig = {}; + config = Object.assign(config, { + transform: [ + ['^.+\\.js$', 'configureable-preprocessor', transformerConfig], + ], + }); + + const scriptTransformer = new ScriptTransformer(config); + scriptTransformer.transform('/fruits/banana.js', {}); + expect( + require('configureable-preprocessor').createTransformer, + ).toHaveBeenCalledWith(transformerConfig); + }); + it('reads values from the cache', () => { const transformConfig = { ...config, diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 1bd054592c49..f3974dcae59e 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -22,6 +22,7 @@ export type HasteConfig = { }; export type ReporterConfig = [string, Record]; +export type TransformerConfig = [string, Record]; export type ConfigGlobals = Record; @@ -95,7 +96,7 @@ export type DefaultOptions = { timers: 'real' | 'fake'; transform: | { - [key: string]: string; + [regex: string]: Path | TransformerConfig; } | null | undefined; @@ -212,7 +213,7 @@ export type InitialOptions = { testTimeout?: number; timers?: 'real' | 'fake'; transform?: { - [key: string]: string; + [regex: string]: Path | TransformerConfig; }; transformIgnorePatterns?: Array; watchPathIgnorePatterns?: Array; @@ -413,7 +414,7 @@ export type ProjectConfig = { testRunner: string; testURL: string; timers: 'real' | 'fake'; - transform: Array<[string, Path]>; + transform: Array<[string, Path, Record]>; transformIgnorePatterns: Array; watchPathIgnorePatterns: Array; unmockedModulePathPatterns: Array | null | undefined;