diff --git a/CHANGELOG.md b/CHANGELOG.md index 71f43afaa766..1fb4e8c1433c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[jest-runtime]` Override `module.createRequire` to return a Jest-compatible `require` function ([#9469](https://github.com/facebook/jest/pull/9469)) +- `[*]` Support array of paths for `moduleNameMapper` aliases ([#9465](https://github.com/facebook/jest/pull/9465)) ### Fixes diff --git a/docs/Configuration.md b/docs/Configuration.md index 0e4e5ac70d44..6f66472da9ef 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -458,11 +458,11 @@ An array of file extensions your modules use. If you require modules without spe We recommend placing the extensions most commonly used in your project on the left, so if you are using TypeScript, you may want to consider moving "ts" and/or "tsx" to the beginning of the array. -### `moduleNameMapper` [object\] +### `moduleNameMapper` [object\>] Default: `null` -A map from regular expressions to module names that allow to stub out resources, like images or styles with a single module. +A map from regular expressions to module names or to arrays of module names that allow to stub out resources, like images or styles with a single module. Modules that are mapped to an alias are unmocked by default, regardless of whether automocking is enabled or not. @@ -477,12 +477,17 @@ Example: "moduleNameMapper": { "^image![a-zA-Z0-9$_-]+$": "GlobalImageStub", "^[./a-zA-Z0-9$_-]+\\.png$": "/RelativeImageStub.js", - "module_name_(.*)": "/substituted_module_$1.js" + "module_name_(.*)": "/substituted_module_$1.js", + "assets/(.*)": [ + "/images/$1", + "/photos/$1", + "/recipes/$1" + ] } } ``` -The order in which the mappings are defined matters. Patterns are checked one by one until one fits. The most specific rule should be listed first. +The order in which the mappings are defined matters. Patterns are checked one by one until one fits. The most specific rule should be listed first. This is true for arrays of modules names as. _Note: If you provide module name without boundaries `^$` it may cause hard to spot errors. E.g. `relay` will replace all modules which contain `relay` as a substring in its name: `relay`, `react-relay` and `graphql-relay` will all be pointed to your stub._ diff --git a/docs/TutorialReactNative.md b/docs/TutorialReactNative.md index 3ca4287a6102..620f8021dae3 100644 --- a/docs/TutorialReactNative.md +++ b/docs/TutorialReactNative.md @@ -150,7 +150,7 @@ If you'd like to provide additional configuration for every test file, the [`set ### moduleNameMapper -The [`moduleNameMapper`](configuration.html#modulenamemapper-objectstring-string) can be used to map a module path to a different module. By default the preset maps all images to an image stub module but if a module cannot be found this configuration option can help: +The [`moduleNameMapper`](configuration.html#modulenamemapper-objectstring-string--arraystring) can be used to map a module path to a different module. By default the preset maps all images to an image stub module but if a module cannot be found this configuration option can help: ```json "moduleNameMapper": { diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 73bf63d59f7a..b7f122ffdc54 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -5,6 +5,41 @@ PASS __tests__/index.js ✓ moduleNameMapping correct configuration `; +exports[`moduleNameMapper wrong array configuration 1`] = ` +FAIL __tests__/index.js + ● Test suite failed to run + + Configuration error: + + Could not locate module ./style.css mapped as: + [ + "no-such-module", + "no-such-module-2" + ]. + + Please check your configuration for these entries: + { + "moduleNameMapper": { + "/\\.(css|less)$/": "[ + "no-such-module", + "no-such-module-2" + ]" + }, + "resolver": undefined + } + + 8 | 'use strict'; + 9 | + > 10 | require('./style.css'); + | ^ + 11 | + 12 | module.exports = () => 'test'; + 13 | + + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:507:17) + at Object.require (index.js:10:1) +`; + exports[`moduleNameMapper wrong configuration 1`] = ` FAIL __tests__/index.js ● Test suite failed to run @@ -30,6 +65,6 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:487:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:507:17) at Object.require (index.js:10:1) `; diff --git a/e2e/__tests__/moduleNameMapper.test.ts b/e2e/__tests__/moduleNameMapper.test.ts index 75b5f355ed25..c3faccac226e 100644 --- a/e2e/__tests__/moduleNameMapper.test.ts +++ b/e2e/__tests__/moduleNameMapper.test.ts @@ -17,6 +17,14 @@ test('moduleNameMapper wrong configuration', () => { expect(wrap(rest)).toMatchSnapshot(); }); +test('moduleNameMapper wrong array configuration', () => { + const {stderr, exitCode} = runJest('module-name-mapper-wrong-array-config'); + const {rest} = extractSummary(stderr); + + expect(exitCode).toBe(1); + expect(wrap(rest)).toMatchSnapshot(); +}); + test('moduleNameMapper correct configuration', () => { const {stderr, exitCode} = runJest('module-name-mapper-correct-config', [], { stripAnsi: true, diff --git a/e2e/module-name-mapper-correct-config/index.js b/e2e/module-name-mapper-correct-config/index.js index 905bfb958beb..2bfaa469de03 100644 --- a/e2e/module-name-mapper-correct-config/index.js +++ b/e2e/module-name-mapper-correct-config/index.js @@ -8,5 +8,6 @@ 'use strict'; require('./style.css'); +require('./style.sass'); module.exports = () => 'test'; diff --git a/e2e/module-name-mapper-correct-config/package.json b/e2e/module-name-mapper-correct-config/package.json index bae54df851fe..9a4fb2ec95f8 100644 --- a/e2e/module-name-mapper-correct-config/package.json +++ b/e2e/module-name-mapper-correct-config/package.json @@ -1,7 +1,8 @@ { "jest": { "moduleNameMapper": { - "\\.(css|less)$": "./__mocks__/styleMock.js" + "\\.(css|less)$": "./__mocks__/styleMock.js", + "\\.(sass)$": ["./__mocks__/nonExistentMock.js", "./__mocks__/styleMock.js"] } } } diff --git a/e2e/module-name-mapper-correct-config/style.sass b/e2e/module-name-mapper-correct-config/style.sass new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/e2e/module-name-mapper-wrong-array-config/__tests__/index.js b/e2e/module-name-mapper-wrong-array-config/__tests__/index.js new file mode 100644 index 000000000000..9c6b6debae86 --- /dev/null +++ b/e2e/module-name-mapper-wrong-array-config/__tests__/index.js @@ -0,0 +1,14 @@ +/** + * 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'; + +const importedFn = require('../'); + +test('moduleNameMapping wrong configuration', () => { + expect(importedFn).toBeDefined(); +}); diff --git a/e2e/module-name-mapper-wrong-array-config/index.js b/e2e/module-name-mapper-wrong-array-config/index.js new file mode 100644 index 000000000000..905bfb958beb --- /dev/null +++ b/e2e/module-name-mapper-wrong-array-config/index.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. + */ + +'use strict'; + +require('./style.css'); + +module.exports = () => 'test'; diff --git a/e2e/module-name-mapper-wrong-array-config/package.json b/e2e/module-name-mapper-wrong-array-config/package.json new file mode 100644 index 000000000000..6028d52abe51 --- /dev/null +++ b/e2e/module-name-mapper-wrong-array-config/package.json @@ -0,0 +1,7 @@ +{ + "jest": { + "moduleNameMapper": { + "\\.(css|less)$": ["no-such-module", "no-such-module-2"] + } + } +} diff --git a/e2e/module-name-mapper-wrong-array-config/style.css b/e2e/module-name-mapper-wrong-array-config/style.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index a0d3f191b722..d83f68f358f6 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -377,8 +377,8 @@ export const options = { moduleNameMapper: { description: 'A JSON string with a map from regular expressions to ' + - 'module names that allow to stub out resources, like images or ' + - 'styles with a single module', + 'module names or to arrays of module names that allow to stub ' + + 'out resources, like images or styles with a single module', type: 'string', }, modulePathIgnorePatterns: { diff --git a/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap b/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap index bb0ef6886d5f..a9eb4ea43840 100644 --- a/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap +++ b/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap @@ -126,7 +126,7 @@ module.exports = { // \\"node\\" // ], - // A map from regular expressions to module names that allow to stub out resources with a single module + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // moduleNameMapper: {}, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader diff --git a/packages/jest-config/src/Descriptions.ts b/packages/jest-config/src/Descriptions.ts index 3ff6d9b38b11..16eb727b3925 100644 --- a/packages/jest-config/src/Descriptions.ts +++ b/packages/jest-config/src/Descriptions.ts @@ -43,7 +43,7 @@ const descriptions: {[key in keyof Config.InitialOptions]: string} = { "An array of directory names to be searched recursively up from the requiring module's location", moduleFileExtensions: 'An array of file extensions your modules use', moduleNameMapper: - 'A map from regular expressions to module names that allow to stub out resources with a single module', + 'A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module', modulePathIgnorePatterns: "An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader", notify: 'Activates notifications for test results', diff --git a/packages/jest-config/src/__tests__/setFromArgv.test.ts b/packages/jest-config/src/__tests__/setFromArgv.test.ts index 8216b0e2c35c..8757fb685190 100644 --- a/packages/jest-config/src/__tests__/setFromArgv.test.ts +++ b/packages/jest-config/src/__tests__/setFromArgv.test.ts @@ -45,12 +45,14 @@ test('maps regular values to themselves', () => { test('works with string objects', () => { const options = {} as Config.InitialOptions; const argv = { - moduleNameMapper: '{"types/(.*)": "/src/types/$1"}', + moduleNameMapper: + '{"types/(.*)": "/src/types/$1", "types2/(.*)": ["/src/types2/$1", "/src/types3/$1"]}', transform: '{"*.js": "/transformer"}', } as Config.Argv; expect(setFromArgv(options, argv)).toMatchObject({ moduleNameMapper: { 'types/(.*)': '/src/types/$1', + 'types2/(.*)': ['/src/types2/$1', '/src/types3/$1'], }, transform: { '*.js': '/transformer', diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index f21e520e6f52..f2d135919c3f 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -377,28 +377,42 @@ class Resolver { // Note: once a moduleNameMapper matches the name, it must result // in a module, or else an error is thrown. const matches = moduleName.match(regex); - const updatedName = matches - ? mappedModuleName.replace( - /\$([0-9]+)/g, - (_, index) => matches[parseInt(index, 10)], - ) - : mappedModuleName; - - const module = - this.getModule(updatedName) || - Resolver.findNodeModule(updatedName, { - basedir: dirname, - browser: this._options.browser, - extensions, - moduleDirectory, - paths, - resolver, - rootDir: this._options.rootDir, - }); + const mapModuleName = matches + ? (moduleName: string) => + moduleName.replace( + /\$([0-9]+)/g, + (_, index) => matches[parseInt(index, 10)], + ) + : (moduleName: string) => moduleName; + + const possibleModuleNames = Array.isArray(mappedModuleName) + ? mappedModuleName + : [mappedModuleName]; + let module: string | null = null; + for (const possibleModuleName of possibleModuleNames) { + const updatedName = mapModuleName(possibleModuleName); + + module = + this.getModule(updatedName) || + Resolver.findNodeModule(updatedName, { + basedir: dirname, + browser: this._options.browser, + extensions, + moduleDirectory, + paths, + resolver, + rootDir: this._options.rootDir, + }); + + if (module) { + break; + } + } + if (!module) { throw createNoMappedModuleFoundError( moduleName, - updatedName, + mapModuleName, mappedModuleName, regex, resolver, @@ -414,21 +428,29 @@ class Resolver { const createNoMappedModuleFoundError = ( moduleName: string, - updatedName: string, - mappedModuleName: string, + mapModuleName: (moduleName: string) => string, + mappedModuleName: string | Array, regex: RegExp, resolver?: Function | string | null, ) => { + const mappedAs = Array.isArray(mappedModuleName) + ? JSON.stringify(mappedModuleName.map(mapModuleName), null, 2) + : mappedModuleName; + const original = Array.isArray(mappedModuleName) + ? JSON.stringify(mappedModuleName, null, 6) // using 6 because of misalignment when nested below + .slice(0, -1) + ' ]' /// align last bracket correctly as well + : mappedModuleName; + const error = new Error( chalk.red(`${chalk.bold('Configuration error')}: Could not locate module ${chalk.bold(moduleName)} mapped as: -${chalk.bold(updatedName)}. +${chalk.bold(mappedAs)}. Please check your configuration for these entries: { "moduleNameMapper": { - "${regex.toString()}": "${chalk.bold(mappedModuleName)}" + "${regex.toString()}": "${chalk.bold(original)}" }, "resolver": ${chalk.bold(String(resolver))} }`), diff --git a/packages/jest-resolve/src/types.ts b/packages/jest-resolve/src/types.ts index 03b372b0cfd5..b58fc73546f6 100644 --- a/packages/jest-resolve/src/types.ts +++ b/packages/jest-resolve/src/types.ts @@ -22,5 +22,5 @@ export type ResolverConfig = { type ModuleNameMapperConfig = { regex: RegExp; - moduleName: string; + moduleName: string | Array; }; diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index a69546d8b271..b16cc1c71d46 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -50,7 +50,7 @@ export type DefaultOptions = { maxWorkers: number | string; moduleDirectories: Array; moduleFileExtensions: Array; - moduleNameMapper: Record; + moduleNameMapper: Record>; modulePathIgnorePatterns: Array; noStackTrace: boolean; notify: boolean; @@ -143,7 +143,7 @@ export type InitialOptions = Partial<{ moduleFileExtensions: Array; moduleLoader: Path; moduleNameMapper: { - [key: string]: string; + [key: string]: string | Array; }; modulePathIgnorePatterns: Array; modulePaths: Array; diff --git a/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts b/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts index 3e841c5bbf36..ec4acfd6b59b 100644 --- a/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts +++ b/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts @@ -91,6 +91,7 @@ const validConfig = { moduleLoader: '', moduleNameMapper: { '^React$': '/node_modules/react', + '^Vue$': ['/node_modules/vue', '/node_modules/vue3'], }, modulePathIgnorePatterns: ['/build/'], modulePaths: ['/shared/vendor/modules'],