Skip to content

Commit

Permalink
Support array of paths for moduleNameMapper aliases (#9465)
Browse files Browse the repository at this point in the history
  • Loading branch information
OrkhanAlikhanov committed Feb 2, 2020
1 parent ffdaa75 commit c9127bf
Show file tree
Hide file tree
Showing 20 changed files with 147 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
13 changes: 9 additions & 4 deletions docs/Configuration.md
Expand Up @@ -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.

Expand All @@ -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._

Expand Down
2 changes: 1 addition & 1 deletion docs/TutorialReactNative.md
Expand Up @@ -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": {
Expand Down
37 changes: 36 additions & 1 deletion e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Expand Up @@ -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
Expand All @@ -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)
`;
8 changes: 8 additions & 0 deletions e2e/__tests__/moduleNameMapper.test.ts
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions e2e/module-name-mapper-correct-config/index.js
Expand Up @@ -8,5 +8,6 @@
'use strict';

require('./style.css');
require('./style.sass');

module.exports = () => 'test';
3 changes: 2 additions & 1 deletion 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"]
}
}
}
Empty file.
14 changes: 14 additions & 0 deletions 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();
});
12 changes: 12 additions & 0 deletions 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';
7 changes: 7 additions & 0 deletions e2e/module-name-mapper-wrong-array-config/package.json
@@ -0,0 +1,7 @@
{
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": ["no-such-module", "no-such-module-2"]
}
}
}
Empty file.
4 changes: 2 additions & 2 deletions packages/jest-cli/src/cli/args.ts
Expand Up @@ -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: {
Expand Down
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-config/src/Descriptions.ts
Expand Up @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion packages/jest-config/src/__tests__/setFromArgv.test.ts
Expand Up @@ -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',
Expand Down
68 changes: 45 additions & 23 deletions packages/jest-resolve/src/index.ts
Expand Up @@ -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,
Expand All @@ -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))}
}`),
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-resolve/src/types.ts
Expand Up @@ -22,5 +22,5 @@ export type ResolverConfig = {

type ModuleNameMapperConfig = {
regex: RegExp;
moduleName: string;
moduleName: string | Array<string>;
};
4 changes: 2 additions & 2 deletions packages/jest-types/src/Config.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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>;
Expand Down
Expand Up @@ -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'],
Expand Down

0 comments on commit c9127bf

Please sign in to comment.