diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaa1257f146..3b506e8b99b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ - `[jest-util]` Always load `mjs` files with `import` ([#15447](https://github.com/jestjs/jest/pull/15447)) - `[jest-worker]` Properly handle a circular reference error when worker tries to send an assertion fails where either the expected or actual value is circular ([#15191](https://github.com/jestjs/jest/pull/15191)) - `[jest-worker]` Properly handle a BigInt when worker tries to send an assertion fails where either the expected or actual value is BigInt ([#15191](https://github.com/jestjs/jest/pull/15191)) +- `[jest-resolve,jest-runtime,jest-resolve-dependencies]` Pass the conditions when resolving stub modules ([#15489](https://github.com/jestjs/jest/pull/15489)) ### Performance diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 19bec39f956a..92b214d76d49 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -41,7 +41,7 @@ exports[`moduleNameMapper wrong array configuration 1`] = ` 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1182:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1184:17) at Object.require (index.js:10:1) at Object.require (__tests__/index.js:10:20)" `; @@ -71,7 +71,7 @@ exports[`moduleNameMapper wrong configuration 1`] = ` 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1182:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1184:17) at Object.require (index.js:10:1) at Object.require (__tests__/index.js:10:20)" `; diff --git a/e2e/resolve-conditions/__tests__/module-name-mapper.test.mjs b/e2e/resolve-conditions/__tests__/module-name-mapper.test.mjs new file mode 100644 index 000000000000..8b2344f2b269 --- /dev/null +++ b/e2e/resolve-conditions/__tests__/module-name-mapper.test.mjs @@ -0,0 +1,14 @@ +/** + * 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. + * + * @jest-environment jest-environment-jsdom + */ + +import {fn} from 'fake-dual-dep2'; + +test('returns correct message', () => { + expect(fn()).toBe('from browser'); +}); diff --git a/e2e/resolve-conditions/node_modules/fake-dual-dep2/browser.mjs b/e2e/resolve-conditions/node_modules/fake-dual-dep2/browser.mjs new file mode 100644 index 000000000000..798335f16820 --- /dev/null +++ b/e2e/resolve-conditions/node_modules/fake-dual-dep2/browser.mjs @@ -0,0 +1,10 @@ +/** + * 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. + */ + +export function fn () { + return 'from browser'; +} diff --git a/e2e/resolve-conditions/node_modules/fake-dual-dep2/node.mjs b/e2e/resolve-conditions/node_modules/fake-dual-dep2/node.mjs new file mode 100644 index 000000000000..9c9a7bd6e2e5 --- /dev/null +++ b/e2e/resolve-conditions/node_modules/fake-dual-dep2/node.mjs @@ -0,0 +1,10 @@ +/** + * 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. + */ + +export function fn () { + return 'from node'; +} diff --git a/e2e/resolve-conditions/node_modules/fake-dual-dep2/package.json b/e2e/resolve-conditions/node_modules/fake-dual-dep2/package.json new file mode 100644 index 000000000000..f1cd27cca5f8 --- /dev/null +++ b/e2e/resolve-conditions/node_modules/fake-dual-dep2/package.json @@ -0,0 +1,10 @@ +{ + "name": "fake-dual-dep2", + "version": "1.0.0", + "exports": { + ".": { + "node": "./node.mjs", + "browser": "./browser.mjs" + } + } +} diff --git a/e2e/resolve-conditions/package.json b/e2e/resolve-conditions/package.json index 6bfa1448215b..30e105b58d26 100644 --- a/e2e/resolve-conditions/package.json +++ b/e2e/resolve-conditions/package.json @@ -9,6 +9,9 @@ "testMatch": [ "/**/*.test.*" ], + "moduleNameMapper": { + "^fake-dual-dep2$": "fake-dual-dep2" + }, "transform": {} } } diff --git a/packages/jest-resolve-dependencies/src/index.ts b/packages/jest-resolve-dependencies/src/index.ts index 2057367291a1..ba19aefa8a4d 100644 --- a/packages/jest-resolve-dependencies/src/index.ts +++ b/packages/jest-resolve-dependencies/src/index.ts @@ -36,6 +36,7 @@ export class DependencyResolver { resolve(file: string, options?: ResolveModuleConfig): Array { const dependencies = this._hasteFS.getDependencies(file); + const fallbackOptions: ResolveModuleConfig = {conditions: undefined}; if (!dependencies) { return []; } @@ -51,11 +52,15 @@ export class DependencyResolver { resolvedDependency = this._resolver.resolveModule( file, dependency, - options, + options ?? fallbackOptions, ); } catch { try { - resolvedDependency = this._resolver.getMockModule(file, dependency); + resolvedDependency = this._resolver.getMockModule( + file, + dependency, + options ?? fallbackOptions, + ); } catch { // leave resolvedDependency as undefined if nothing can be found } @@ -73,6 +78,7 @@ export class DependencyResolver { resolvedMockDependency = this._resolver.getMockModule( resolvedDependency, path.basename(dependency), + options ?? fallbackOptions, ); } catch { // leave resolvedMockDependency as undefined if nothing can be found diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index df3096e927e5..321796e71242 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -742,7 +742,9 @@ describe('getMockModuleAsync', () => { } as ResolverConfig); const src = require.resolve('../'); - await resolver.resolveModuleAsync(src, 'dependentModule'); + await resolver.resolveModuleAsync(src, 'dependentModule', { + conditions: ['browser'], + }); expect(mockUserResolverAsync.async).toHaveBeenCalled(); expect(mockUserResolverAsync.async.mock.calls[0][0]).toBe( @@ -752,6 +754,10 @@ describe('getMockModuleAsync', () => { 'basedir', path.dirname(src), ); + expect(mockUserResolverAsync.async.mock.calls[0][1]).toHaveProperty( + 'conditions', + ['browser'], + ); }); }); diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index 32bb4d8cd9f9..e7dcc1e36b06 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -341,11 +341,11 @@ export default class Resolver { resolveModule( from: string, moduleName: string, - options?: ResolveModuleConfig, + options: ResolveModuleConfig, ): string { const dirname = path.dirname(from); const module = - this.resolveStubModuleName(from, moduleName) || + this.resolveStubModuleName(from, moduleName, options) || this.resolveModuleFromDirIfExists(dirname, moduleName, options); if (module) return module; @@ -362,7 +362,7 @@ export default class Resolver { ): Promise { const dirname = path.dirname(from); const module = - (await this.resolveStubModuleNameAsync(from, moduleName)) || + (await this.resolveStubModuleNameAsync(from, moduleName, options)) || (await this.resolveModuleFromDirIfExistsAsync( dirname, moduleName, @@ -482,12 +482,16 @@ export default class Resolver { ); } - getMockModule(from: string, name: string): string | null { + getMockModule( + from: string, + name: string, + options: Pick, + ): string | null { const mock = this._moduleMap.getMockModule(name); if (mock) { return mock; } else { - const moduleName = this.resolveStubModuleName(from, name); + const moduleName = this.resolveStubModuleName(from, name, options); if (moduleName) { return this.getModule(moduleName) || moduleName; } @@ -495,12 +499,20 @@ export default class Resolver { return null; } - async getMockModuleAsync(from: string, name: string): Promise { + async getMockModuleAsync( + from: string, + name: string, + options: Pick, + ): Promise { const mock = this._moduleMap.getMockModule(name); if (mock) { return mock; } else { - const moduleName = await this.resolveStubModuleNameAsync(from, name); + const moduleName = await this.resolveStubModuleNameAsync( + from, + name, + options, + ); if (moduleName) { return this.getModule(moduleName) || moduleName; } @@ -536,7 +548,7 @@ export default class Resolver { virtualMocks: Map, from: string, moduleName = '', - options?: ResolveModuleConfig, + options: ResolveModuleConfig, ): string { const stringifiedOptions = options ? JSON.stringify(options) : ''; const key = from + path.delimiter + moduleName + stringifiedOptions; @@ -552,7 +564,7 @@ export default class Resolver { moduleName, options, ); - const mockPath = this._getMockPath(from, moduleName); + const mockPath = this._getMockPath(from, moduleName, options); const sep = path.delimiter; const id = @@ -570,7 +582,7 @@ export default class Resolver { virtualMocks: Map, from: string, moduleName = '', - options?: ResolveModuleConfig, + options: ResolveModuleConfig, ): Promise { const stringifiedOptions = options ? JSON.stringify(options) : ''; const key = from + path.delimiter + moduleName + stringifiedOptions; @@ -589,7 +601,7 @@ export default class Resolver { moduleName, options, ); - const mockPath = await this._getMockPathAsync(from, moduleName); + const mockPath = await this._getMockPathAsync(from, moduleName, options); const sep = path.delimiter; const id = @@ -611,7 +623,7 @@ export default class Resolver { virtualMocks: Map, from: string, moduleName: string, - options?: ResolveModuleConfig, + options: ResolveModuleConfig, ): string | null { if (this.isCoreModule(moduleName)) { return moduleName; @@ -619,7 +631,7 @@ export default class Resolver { if (moduleName.startsWith('data:')) { return moduleName; } - return this._isModuleResolved(from, moduleName) + return this._isModuleResolved(from, moduleName, options) ? this.getModule(moduleName) : this._getVirtualMockPath(virtualMocks, from, moduleName, options); } @@ -628,7 +640,7 @@ export default class Resolver { virtualMocks: Map, from: string, moduleName: string, - options?: ResolveModuleConfig, + options: ResolveModuleConfig, ): Promise { if (this.isCoreModule(moduleName)) { return moduleName; @@ -639,32 +651,38 @@ export default class Resolver { const isModuleResolved = await this._isModuleResolvedAsync( from, moduleName, + options, ); return isModuleResolved ? this.getModule(moduleName) : this._getVirtualMockPathAsync(virtualMocks, from, moduleName, options); } - private _getMockPath(from: string, moduleName: string): string | null { + private _getMockPath( + from: string, + moduleName: string, + options: Pick, + ): string | null { return this.isCoreModule(moduleName) ? null - : this.getMockModule(from, moduleName); + : this.getMockModule(from, moduleName, options); } private async _getMockPathAsync( from: string, moduleName: string, + options: Pick, ): Promise { return this.isCoreModule(moduleName) ? null - : this.getMockModuleAsync(from, moduleName); + : this.getMockModuleAsync(from, moduleName, options); } private _getVirtualMockPath( virtualMocks: Map, from: string, moduleName: string, - options?: ResolveModuleConfig, + options: ResolveModuleConfig, ): string { const virtualMockPath = this.getModulePath(from, moduleName); return virtualMocks.get(virtualMockPath) @@ -688,23 +706,33 @@ export default class Resolver { : from; } - private _isModuleResolved(from: string, moduleName: string): boolean { + private _isModuleResolved( + from: string, + moduleName: string, + options: Pick, + ): boolean { return !!( - this.getModule(moduleName) || this.getMockModule(from, moduleName) + this.getModule(moduleName) || + this.getMockModule(from, moduleName, options) ); } private async _isModuleResolvedAsync( from: string, moduleName: string, + options: Pick, ): Promise { return !!( this.getModule(moduleName) || - (await this.getMockModuleAsync(from, moduleName)) + (await this.getMockModuleAsync(from, moduleName, options)) ); } - resolveStubModuleName(from: string, moduleName: string): string | null { + resolveStubModuleName( + from: string, + moduleName: string, + options: Pick, + ): string | null { const dirname = path.dirname(from); const {extensions, moduleDirectory, paths} = this._prepareForResolution( @@ -727,11 +755,11 @@ export default class Resolver { let module: string | null = null; for (const possibleModuleName of possibleModuleNames) { const updatedName = mapModuleName(possibleModuleName); - module = this.getModule(updatedName) || Resolver.findNodeModule(updatedName, { basedir: dirname, + conditions: options?.conditions, extensions, moduleDirectory, paths, @@ -763,6 +791,7 @@ export default class Resolver { async resolveStubModuleNameAsync( from: string, moduleName: string, + options?: Pick, ): Promise { const dirname = path.dirname(from); @@ -791,6 +820,7 @@ export default class Resolver { this.getModule(updatedName) || (await Resolver.findNodeModuleAsync(updatedName, { basedir: dirname, + conditions: options?.conditions, extensions, moduleDirectory, paths, diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index e054e894d211..d66826c0a72e 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -925,11 +925,12 @@ export default class Runtime { isRequireActual = false, ): T { const isInternal = options?.isInternalModule ?? false; + const resolveModuleOptions = {conditions: this.cjsConditions}; const moduleID = this._resolver.getModuleID( this._virtualMocks, from, moduleName, - {conditions: this.cjsConditions}, + resolveModuleOptions, ); let modulePath: string | undefined; @@ -937,7 +938,8 @@ export default class Runtime { // to be more explicit. const moduleResource = moduleName && this._resolver.getModule(moduleName); const manualMock = - moduleName && this._resolver.getMockModule(from, moduleName); + moduleName && + this._resolver.getMockModule(from, moduleName, resolveModuleOptions); if ( !options?.isInternalModule && !isRequireActual && @@ -1043,11 +1045,12 @@ export default class Runtime { } requireMock(from: string, moduleName: string): T { + const options = {conditions: this.cjsConditions}; const moduleID = this._resolver.getModuleID( this._virtualMocks, from, moduleName, - {conditions: this.cjsConditions}, + options, ); if (this._isolatedMockRegistry?.has(moduleID)) { @@ -1065,15 +1068,19 @@ export default class Runtime { return module as T; } - const manualMockOrStub = this._resolver.getMockModule(from, moduleName); + const manualMockOrStub = this._resolver.getMockModule( + from, + moduleName, + options, + ); let modulePath = - this._resolver.getMockModule(from, moduleName) || + this._resolver.getMockModule(from, moduleName, options) || this._resolveCjsModule(from, moduleName); let isManualMock = manualMockOrStub && - !this._resolver.resolveStubModuleName(from, moduleName); + !this._resolver.resolveStubModuleName(from, moduleName, options); if (!isManualMock) { // If the actual module file has a __mocks__ dir sitting immediately next // to it, look to see if there is a manual mock for this file. @@ -1507,7 +1514,9 @@ export default class Runtime { try { return this._resolveCjsModule(from, moduleName); } catch (error) { - const module = this._resolver.getMockModule(from, moduleName); + const module = this._resolver.getMockModule(from, moduleName, { + conditions: this.cjsConditions, + }); if (module) { return module; @@ -1899,8 +1908,9 @@ export default class Runtime { private _generateMock(from: string, moduleName: string) { const modulePath = - this._resolver.resolveStubModuleName(from, moduleName) || - this._resolveCjsModule(from, moduleName); + this._resolver.resolveStubModuleName(from, moduleName, { + conditions: this.cjsConditions, + }) || this._resolveCjsModule(from, moduleName); if (!this._mockMetaDataCache.has(modulePath)) { // This allows us to handle circular dependencies while generating an // automock @@ -1982,7 +1992,11 @@ export default class Runtime { try { modulePath = this._resolveCjsModule(from, moduleName); } catch (error) { - const manualMock = this._resolver.getMockModule(from, moduleName); + const manualMock = this._resolver.getMockModule( + from, + moduleName, + options, + ); if (manualMock) { this._shouldMockModuleCache.set(moduleID, true); return true; @@ -2056,6 +2070,7 @@ export default class Runtime { const manualMock = await this._resolver.getMockModuleAsync( from, moduleName, + options, ); if (manualMock) { this._shouldMockModuleCache.set(moduleID, true);