Support array of paths for `moduleNameMapper` aliases (#9465)

This commit is contained in:
Orkhan Alikhanov 2020-02-02 19:20:28 +04:00 committed by GitHub
parent ffdaa751f5
commit c9127bfec3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 146 additions and 37 deletions

View File

@ -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

View File

@ -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\<string, string>]
### `moduleNameMapper` [object\<string, string | array\<string>>]
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$": "<rootDir>/RelativeImageStub.js",
"module_name_(.*)": "<rootDir>/substituted_module_$1.js"
"module_name_(.*)": "<rootDir>/substituted_module_$1.js",
"assets/(.*)": [
"<rootDir>/images/$1",
"<rootDir>/photos/$1",
"<rootDir>/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._

View File

@ -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": {

View File

@ -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)
`;

View File

@ -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,

View File

@ -8,5 +8,6 @@
'use strict';
require('./style.css');
require('./style.sass');
module.exports = () => 'test';

View File

@ -1,7 +1,8 @@
{
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": "./__mocks__/styleMock.js"
"\\.(css|less)$": "./__mocks__/styleMock.js",
"\\.(sass)$": ["./__mocks__/nonExistentMock.js", "./__mocks__/styleMock.js"]
}
}
}

View File

@ -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();
});

View File

@ -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';

View File

@ -0,0 +1,7 @@
{
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": ["no-such-module", "no-such-module-2"]
}
}
}

View File

@ -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: {

View File

@ -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

View File

@ -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',

View File

@ -45,12 +45,14 @@ test('maps regular values to themselves', () => {
test('works with string objects', () => {
const options = {} as Config.InitialOptions;
const argv = {
moduleNameMapper: '{"types/(.*)": "<rootDir>/src/types/$1"}',
moduleNameMapper:
'{"types/(.*)": "<rootDir>/src/types/$1", "types2/(.*)": ["<rootDir>/src/types2/$1", "<rootDir>/src/types3/$1"]}',
transform: '{"*.js": "<rootDir>/transformer"}',
} as Config.Argv;
expect(setFromArgv(options, argv)).toMatchObject({
moduleNameMapper: {
'types/(.*)': '<rootDir>/src/types/$1',
'types2/(.*)': ['<rootDir>/src/types2/$1', '<rootDir>/src/types3/$1'],
},
transform: {
'*.js': '<rootDir>/transformer',

View File

@ -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 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;
}
}
const module =
this.getModule(updatedName) ||
Resolver.findNodeModule(updatedName, {
basedir: dirname,
browser: this._options.browser,
extensions,
moduleDirectory,
paths,
resolver,
rootDir: this._options.rootDir,
});
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<string>,
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))}
}`),

View File

@ -22,5 +22,5 @@ export type ResolverConfig = {
type ModuleNameMapperConfig = {
regex: RegExp;
moduleName: string;
moduleName: string | Array<string>;
};

View File

@ -50,7 +50,7 @@ export type DefaultOptions = {
maxWorkers: number | string;
moduleDirectories: Array<string>;
moduleFileExtensions: Array<string>;
moduleNameMapper: Record<string, string>;
moduleNameMapper: Record<string, string | Array<string>>;
modulePathIgnorePatterns: Array<string>;
noStackTrace: boolean;
notify: boolean;
@ -143,7 +143,7 @@ export type InitialOptions = Partial<{
moduleFileExtensions: Array<string>;
moduleLoader: Path;
moduleNameMapper: {
[key: string]: string;
[key: string]: string | Array<string>;
};
modulePathIgnorePatterns: Array<string>;
modulePaths: Array<string>;

View File

@ -91,6 +91,7 @@ const validConfig = {
moduleLoader: '<rootDir>',
moduleNameMapper: {
'^React$': '<rootDir>/node_modules/react',
'^Vue$': ['<rootDir>/node_modules/vue', '<rootDir>/node_modules/vue3'],
},
modulePathIgnorePatterns: ['<rootDir>/build/'],
modulePaths: ['/shared/vendor/modules'],