diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c4b839b05a..cb85a4c6818a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - `[jest-runtime]` Fix error when `require()` is called after the Jest environment has been torn down ([#15951](https://github.com/jestjs/jest/pull/15951)) - `[jest-runtime]` Fix missing error when `import()` is called after the Jest environment has been torn down ([#16080](https://github.com/jestjs/jest/pull/16080)) - `[jest-runtime]` Fix virtual `unstable_mockModule` registrations not respected in ESM ([#16081](https://github.com/jestjs/jest/pull/16081)) +- `[jest-runtime]` Apply `moduleNameMapper` when resolving modules with `require.resolve()` and the `paths` option ([#16135](https://github.com/jestjs/jest/pull/16135)) ### Chore & Maintenance diff --git a/e2e/resolve-with-paths/__tests__/resolveWithPaths.test.js b/e2e/resolve-with-paths/__tests__/resolveWithPaths.test.js index a49aca191757..aaabe0cf9d05 100644 --- a/e2e/resolve-with-paths/__tests__/resolveWithPaths.test.js +++ b/e2e/resolve-with-paths/__tests__/resolveWithPaths.test.js @@ -40,3 +40,9 @@ test('throws module not found error if the module cannot be found from given pat expect.objectContaining({code: 'MODULE_NOT_FOUND'}), ); }); + +test('resolves moduleNameMapper with paths option', () => { + expect(require.resolve('@foo/js', {paths: [__dirname]})).toBe( + require.resolve('../js/index.js'), + ); +}); diff --git a/e2e/resolve-with-paths/js/index.js b/e2e/resolve-with-paths/js/index.js new file mode 100644 index 000000000000..7c0677ef1f33 --- /dev/null +++ b/e2e/resolve-with-paths/js/index.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +module.exports = 'mapped module'; diff --git a/e2e/resolve-with-paths/package.json b/e2e/resolve-with-paths/package.json index 148788b25446..63659c7bea5e 100644 --- a/e2e/resolve-with-paths/package.json +++ b/e2e/resolve-with-paths/package.json @@ -1,5 +1,8 @@ { "jest": { - "testEnvironment": "node" + "testEnvironment": "node", + "moduleNameMapper": { + "^@foo/js$": "/js/index.js" + } } } diff --git a/packages/jest-runtime/src/internals/__tests__/cjsRequire.test.ts b/packages/jest-runtime/src/internals/__tests__/cjsRequire.test.ts index 3b0dbc5cb041..189cce7e4be1 100644 --- a/packages/jest-runtime/src/internals/__tests__/cjsRequire.test.ts +++ b/packages/jest-runtime/src/internals/__tests__/cjsRequire.test.ts @@ -42,6 +42,7 @@ function makeResolution( isCoreModule: jest.fn(() => false), resolveCjs: jest.fn(), resolveCjsFromDirIfExists: jest.fn(() => null), + resolveCjsStub: jest.fn(() => null), ...overrides, } as unknown as Resolution; } @@ -170,6 +171,25 @@ describe('RequireBuilder', () => { ); }); + test('resolves moduleNameMapper before walking options.paths', () => { + const resolveCjsStub = jest.fn( + (_from: string, _moduleName: string) => '/mapped.js', + ); + const resolveCjsFromDirIfExists = jest.fn(() => null); + const builder = makeBuilder({ + resolution: makeResolution({ + resolveCjsFromDirIfExists, + resolveCjsStub, + }), + }); + + expect(resolveVia(builder, '@foo/js', {paths: ['./a']})).toBe( + '/mapped.js', + ); + expect(resolveCjsStub).toHaveBeenCalledWith('/a/b/from.js', '@foo/js'); + expect(resolveCjsFromDirIfExists).not.toHaveBeenCalled(); + }); + test('falls back to mock module when resolveCjs throws', () => { const builder = makeBuilder({ resolution: makeResolution({ diff --git a/packages/jest-runtime/src/internals/cjsRequire.ts b/packages/jest-runtime/src/internals/cjsRequire.ts index 22d5d4fb6583..6566073816f4 100644 --- a/packages/jest-runtime/src/internals/cjsRequire.ts +++ b/packages/jest-runtime/src/internals/cjsRequire.ts @@ -130,14 +130,22 @@ export class RequireBuilder { return module; } } else if (options.paths) { + const module = this.resolution.resolveCjsStub(from, moduleName); + + if (module) { + return module; + } + for (const searchPath of options.paths) { const absolutePath = path.resolve(from, '..', searchPath); + // required to also resolve files without leading './' directly in the path const module = this.resolution.resolveCjsFromDirIfExists( absolutePath, moduleName, [absolutePath], ); + if (module) { return module; }