Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support array of paths for moduleNameMapper aliases #9465

Merged
merged 7 commits into from Feb 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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',
SimenB marked this conversation as resolved.
Show resolved Hide resolved
},
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it into an array in normalize?

Thinking about it, that might be a breaking change... Hmm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an error happens we need to properly format error message based on whether it was set as an array or string in configuration. If we normalize it, we will probably lose that information and not be able to properly format error message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be too bad to show [path] even though the user configured path I guess, but probably not worth it also given the breaking change risk. I'd be happy as is - however one thing I do want to verify when I have some time is whether the extra work of Array.isArray, wrapping [mappedModuleName] etc. has a perf impact. I doubt it's significant, but jest-resolve is quite a hot path so better safe than sorry

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could normalize to an array in the constructor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimenB Not sure I understood that. Is there anything for me to do apart from updating docs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeysal Aren't resolved module paths cached?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this._possibleModuleNames = Array.isArray(mappedModuleName) ? mappedModuleName : [mappedModuleName];

in the constructor of this class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimenB The problem is we need both the original config entry for the error message and the array one to iterate over here. As long as we don't want to compromise on the correctness of the error message I think this is as good as it gets 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeysal Aren't resolved module paths cached?

Even with caching often running a single test will need to resolve a large dependency tree, just want to be on the safe side as far as perf goes 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the error message can be passed the original config, but in the happy case it won't have to check if array or not

? 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