From 260eaaa362f9173d3961d930a7f931c3dd874bc0 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 9 Sep 2021 16:07:34 +0200 Subject: [PATCH 01/11] feat: pass `conditions` when resolving modules --- e2e/__tests__/resolveConditions.test.ts | 35 ++++++++++++++ .../__tests__/resolveCjs.test.cjs | 12 +++++ .../__tests__/resolveEsm.test.mjs | 12 +++++ e2e/resolve-conditions/fake-dep/module.cjs | 3 ++ e2e/resolve-conditions/fake-dep/module.mjs | 3 ++ e2e/resolve-conditions/fake-dep/package.json | 10 ++++ e2e/resolve-conditions/package.json | 18 +++++++ e2e/resolve-conditions/resolver.js | 40 ++++++++++++++++ e2e/resolve-conditions/yarn.lock | 21 ++++++++ .../src/__tests__/resolve.test.ts | 2 + packages/jest-resolve/src/defaultResolver.ts | 12 ++++- packages/jest-resolve/src/resolver.ts | 18 +++++-- packages/jest-runtime/src/index.ts | 48 ++++++++++++++----- 13 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 e2e/__tests__/resolveConditions.test.ts create mode 100644 e2e/resolve-conditions/__tests__/resolveCjs.test.cjs create mode 100644 e2e/resolve-conditions/__tests__/resolveEsm.test.mjs create mode 100644 e2e/resolve-conditions/fake-dep/module.cjs create mode 100644 e2e/resolve-conditions/fake-dep/module.mjs create mode 100644 e2e/resolve-conditions/fake-dep/package.json create mode 100644 e2e/resolve-conditions/package.json create mode 100644 e2e/resolve-conditions/resolver.js create mode 100644 e2e/resolve-conditions/yarn.lock diff --git a/e2e/__tests__/resolveConditions.test.ts b/e2e/__tests__/resolveConditions.test.ts new file mode 100644 index 000000000000..ef6cc3980818 --- /dev/null +++ b/e2e/__tests__/resolveConditions.test.ts @@ -0,0 +1,35 @@ +/** + * 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. + */ + +import {resolve} from 'path'; +import {onNodeVersions} from '@jest/test-utils'; +import {runYarnInstall} from '../Utils'; +import runJest from '../runJest'; + +const dir = resolve(__dirname, '..', 'resolve-conditions'); + +beforeAll(() => { + runYarnInstall(dir); +}); + +// The versions where vm.Module exists and commonjs with "exports" is not broken +onNodeVersions('^12.16.0 || >=13.7.0', () => { + test('resolves package exports correctly with custom resolver', () => { + // run multiple times to ensure there are noe caching errors + for (let i = 0; i < 5; i++) { + const {exitCode} = runJest(dir, [], { + nodeOptions: '--experimental-vm-modules', + }); + try { + expect(exitCode).toBe(0); + } catch (error) { + console.log(`Test failed on iteration ${i + 1}`); + throw error; + } + } + }); +}); diff --git a/e2e/resolve-conditions/__tests__/resolveCjs.test.cjs b/e2e/resolve-conditions/__tests__/resolveCjs.test.cjs new file mode 100644 index 000000000000..5a9ba6b23b88 --- /dev/null +++ b/e2e/resolve-conditions/__tests__/resolveCjs.test.cjs @@ -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. + */ + +const {fn} = require('../fake-dep'); + +test('returns correct message', () => { + expect(fn()).toEqual('hello from CJS'); +}); diff --git a/e2e/resolve-conditions/__tests__/resolveEsm.test.mjs b/e2e/resolve-conditions/__tests__/resolveEsm.test.mjs new file mode 100644 index 000000000000..d46c700f2cf2 --- /dev/null +++ b/e2e/resolve-conditions/__tests__/resolveEsm.test.mjs @@ -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. + */ + +import {fn} from '../fake-dep'; + +test('returns correct message', () => { + expect(fn()).toEqual('hello from ESM'); +}); diff --git a/e2e/resolve-conditions/fake-dep/module.cjs b/e2e/resolve-conditions/fake-dep/module.cjs new file mode 100644 index 000000000000..27dffe252340 --- /dev/null +++ b/e2e/resolve-conditions/fake-dep/module.cjs @@ -0,0 +1,3 @@ +module.exports = { + fn: () => 'hello from CJS', +}; diff --git a/e2e/resolve-conditions/fake-dep/module.mjs b/e2e/resolve-conditions/fake-dep/module.mjs new file mode 100644 index 000000000000..bfa964d2127e --- /dev/null +++ b/e2e/resolve-conditions/fake-dep/module.mjs @@ -0,0 +1,3 @@ +export function fn() { + return 'hello from ESM' +} diff --git a/e2e/resolve-conditions/fake-dep/package.json b/e2e/resolve-conditions/fake-dep/package.json new file mode 100644 index 000000000000..fdccd90403cf --- /dev/null +++ b/e2e/resolve-conditions/fake-dep/package.json @@ -0,0 +1,10 @@ +{ + "name": "fake-dep", + "version": "1.0.0", + "exports": { + ".": { + "import": "./module.mjs", + "require": "./module.cjs" + } + } +} diff --git a/e2e/resolve-conditions/package.json b/e2e/resolve-conditions/package.json new file mode 100644 index 000000000000..987f46111b8a --- /dev/null +++ b/e2e/resolve-conditions/package.json @@ -0,0 +1,18 @@ +{ + "jest": { + "moduleFileExtensions": [ + "js", + "cjs", + "mjs", + "json" + ], + "resolver": "/resolver.js", + "testMatch": [ + "/**/*.test.*" + ], + "transform": {} + }, + "dependencies": { + "resolve.exports": "^1.0.2" + } +} diff --git a/e2e/resolve-conditions/resolver.js b/e2e/resolve-conditions/resolver.js new file mode 100644 index 000000000000..f59ccf15a1ad --- /dev/null +++ b/e2e/resolve-conditions/resolver.js @@ -0,0 +1,40 @@ +/** + * 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 {resolve: resolveExports} = require('resolve.exports'); + +module.exports = (path, options) => { + const doWeCare = path.includes('fake-dep'); + + if (doWeCare) { + // console.log(path, options, new Error().stack); + } + + return options.defaultResolver(path, { + ...options, + pathFilter: options.conditions + ? createPathFilter(options.conditions) + : undefined, + }); +}; + +function createPathFilter(conditions) { + return function pathFilter(pkg, _path, relativePath) { + // this `index` thing can backfire, but `resolve` adds it: https://github.com/browserify/resolve/blob/f1b51848ecb7f56f77bfb823511d032489a13eab/lib/sync.js#L192 + const path = relativePath === 'index' ? '.' : relativePath; + + return ( + resolveExports(pkg, path, { + conditions, + // `resolve.exports adds `import` unless `require` is `false`, so let's add this ugly thing + require: !conditions.includes('import'), + }) || relativePath + ); + }; +} diff --git a/e2e/resolve-conditions/yarn.lock b/e2e/resolve-conditions/yarn.lock new file mode 100644 index 000000000000..b98bb300b2b9 --- /dev/null +++ b/e2e/resolve-conditions/yarn.lock @@ -0,0 +1,21 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 4 + cacheKey: 7 + +"resolve.exports@npm:^1.0.2": + version: 1.0.2 + resolution: "resolve.exports@npm:1.0.2" + checksum: 012a46e3ae41c53762abf5b50ea1b4adf2de617bbea1dbc7bf6e609c1ceaedee7782acbc92d443951d5dd0c3a8fb1090ce73285a9ccc24b530e33b5e09ae196f + languageName: node + linkType: hard + +"root-workspace-0b6124@workspace:.": + version: 0.0.0-use.local + resolution: "root-workspace-0b6124@workspace:." + dependencies: + resolve.exports: ^1.0.2 + languageName: unknown + linkType: soft diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index 56d1640b3e7c..cfbe99bc841d 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -105,6 +105,7 @@ describe('findNodeModule', () => { const newPath = Resolver.findNodeModule('test', { basedir: '/', browser: true, + conditions: ['conditions, woooo'], extensions: ['js'], moduleDirectory: ['node_modules'], paths: ['/something'], @@ -116,6 +117,7 @@ describe('findNodeModule', () => { expect(userResolver.mock.calls[0][1]).toStrictEqual({ basedir: '/', browser: true, + conditions: ['conditions, woooo'], defaultResolver, extensions: ['js'], moduleDirectory: ['node_modules'], diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index 7b3dfe17a3bd..942b6db0b1cb 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -19,7 +19,16 @@ type ResolverOptions = { moduleDirectory?: Array; paths?: Array; rootDir?: Config.Path; - packageFilter?: (pkg: any, pkgfile: string) => any; + packageFilter?: ( + pkg: Record, + pkgfile: string, + ) => Record; + pathFilter?: ( + pkg: Record, + path: string, + relativePath: string, + ) => string; + conditions?: Array; }; // https://github.com/facebook/jest/pull/10617 @@ -48,6 +57,7 @@ export default function defaultResolver( isFile, moduleDirectory: options.moduleDirectory, packageFilter: options.packageFilter, + pathFilter: options.pathFilter, paths: options.paths, preserveSymlinks: false, readPackageSync, diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index b532c73ba410..c5cf1d537da9 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -23,6 +23,7 @@ import type {ResolverConfig} from './types'; type FindNodeModuleConfig = { basedir: Config.Path; browser?: boolean; + conditions?: Array; extensions?: Array; moduleDirectory?: Array; paths?: Array; @@ -32,6 +33,7 @@ type FindNodeModuleConfig = { }; export type ResolveModuleConfig = { + conditions?: Array; skipNodeResolution?: boolean; paths?: Array; }; @@ -113,6 +115,7 @@ export default class Resolver { return resolver(path, { basedir: options.basedir, browser: options.browser, + conditions: options.conditions, defaultResolver, extensions: options.extensions, moduleDirectory: options.moduleDirectory, @@ -183,6 +186,7 @@ export default class Resolver { return Resolver.findNodeModule(name, { basedir: dirname, + conditions: options?.conditions, extensions, moduleDirectory, paths, @@ -321,6 +325,7 @@ export default class Resolver { virtualMocks: Map, from: Config.Path, moduleName = '', + options?: ResolveModuleConfig, ): string { const key = from + path.delimiter + moduleName; const cachedModuleID = this._moduleIDCache.get(key); @@ -329,7 +334,12 @@ export default class Resolver { } const moduleType = this._getModuleType(moduleName); - const absolutePath = this._getAbsolutePath(virtualMocks, from, moduleName); + const absolutePath = this._getAbsolutePath( + virtualMocks, + from, + moduleName, + options, + ); const mockPath = this._getMockPath(from, moduleName); const sep = path.delimiter; @@ -351,13 +361,14 @@ export default class Resolver { virtualMocks: Map, from: Config.Path, moduleName: string, + options?: ResolveModuleConfig, ): Config.Path | null { if (this.isCoreModule(moduleName)) { return moduleName; } return this._isModuleResolved(from, moduleName) ? this.getModule(moduleName) - : this._getVirtualMockPath(virtualMocks, from, moduleName); + : this._getVirtualMockPath(virtualMocks, from, moduleName, options); } private _getMockPath( @@ -373,12 +384,13 @@ export default class Resolver { virtualMocks: Map, from: Config.Path, moduleName: string, + options?: ResolveModuleConfig, ): Config.Path { const virtualMockPath = this.getModulePath(from, moduleName); return virtualMocks.get(virtualMockPath) ? virtualMockPath : moduleName - ? this.resolveModule(from, moduleName) + ? this.resolveModule(from, moduleName, options) : from; } diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 340501e0b44b..93b28e05cc06 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -48,7 +48,7 @@ import HasteMap from 'jest-haste-map'; import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; import type {MockFunctionMetadata, ModuleMocker} from 'jest-mock'; import {escapePathForRegex} from 'jest-regex-util'; -import Resolver from 'jest-resolve'; +import Resolver, {ResolveModuleConfig} from 'jest-resolve'; import Snapshot = require('jest-snapshot'); import {createDirectory, deepCyclicCopy} from 'jest-util'; import { @@ -173,6 +173,9 @@ const supportsNodeColonModulePrefixInImport = (() => { return stdout === 'true'; })(); +const esmConditions = ['import', 'require', 'default']; +const cjsConditions = ['require', 'default']; + export default class Runtime { private readonly _cacheFS: Map; private readonly _config: Config.ProjectConfig; @@ -536,12 +539,15 @@ export default class Runtime { referencingIdentifier, path, this._explicitShouldMockModule, + {conditions: esmConditions}, ) ) { return this.importMock(referencingIdentifier, path, context); } - const resolved = this._resolveModule(referencingIdentifier, path); + const resolved = this._resolveModule(referencingIdentifier, path, { + conditions: esmConditions, + }); if ( this._resolver.isCoreModule(resolved) || @@ -589,7 +595,9 @@ export default class Runtime { const [path, query] = (moduleName ?? '').split('?'); - const modulePath = this._resolveModule(from, path); + const modulePath = this._resolveModule(from, path, { + conditions: esmConditions, + }); const module = await this.loadEsmModule(modulePath, query); @@ -685,7 +693,9 @@ export default class Runtime { const namedExports = new Set(exports); reexports.forEach(reexport => { - const resolved = this._resolveModule(modulePath, reexport); + const resolved = this._resolveModule(modulePath, reexport, { + conditions: esmConditions, + }); const exports = this.getExportsOfCjs(resolved); @@ -734,7 +744,9 @@ export default class Runtime { } if (!modulePath) { - modulePath = this._resolveModule(from, moduleName); + modulePath = this._resolveModule(from, moduleName, { + conditions: cjsConditions, + }); } if (this.unstable_shouldLoadAsEsm(modulePath)) { @@ -845,7 +857,7 @@ export default class Runtime { let modulePath = this._resolver.getMockModule(from, moduleName) || - this._resolveModule(from, moduleName); + this._resolveModule(from, moduleName, {conditions: cjsConditions}); let isManualMock = manualMockOrStub && @@ -949,7 +961,11 @@ export default class Runtime { } try { - if (this._shouldMock(from, moduleName, this._explicitShouldMock)) { + if ( + this._shouldMock(from, moduleName, this._explicitShouldMock, { + conditions: cjsConditions, + }) + ) { return this.requireMock(from, moduleName); } else { return this.requireModule(from, moduleName); @@ -1160,8 +1176,12 @@ export default class Runtime { this._moduleImplementation = undefined; } - private _resolveModule(from: Config.Path, to?: string) { - return to ? this._resolver.resolveModule(from, to) : from; + private _resolveModule( + from: Config.Path, + to: string | undefined, + options: ResolveModuleConfig, + ) { + return to ? this._resolver.resolveModule(from, to, options) : from; } private _requireResolve( @@ -1184,7 +1204,7 @@ export default class Runtime { absolutePath, moduleName, // required to also resolve files without leading './' directly in the path - {paths: [absolutePath]}, + {conditions: cjsConditions, paths: [absolutePath]}, ); if (module) { return module; @@ -1198,7 +1218,7 @@ export default class Runtime { ); } try { - return this._resolveModule(from, moduleName); + return this._resolveModule(from, moduleName, {conditions: cjsConditions}); } catch (err) { const module = this._resolver.getMockModule(from, moduleName); @@ -1553,7 +1573,7 @@ export default class Runtime { private _generateMock(from: Config.Path, moduleName: string) { const modulePath = this._resolver.resolveStubModuleName(from, moduleName) || - this._resolveModule(from, moduleName); + this._resolveModule(from, moduleName, {conditions: cjsConditions}); if (!this._mockMetaDataCache.has(modulePath)) { // This allows us to handle circular dependencies while generating an // automock @@ -1597,11 +1617,13 @@ export default class Runtime { from: Config.Path, moduleName: string, explicitShouldMock: Map, + options: ResolveModuleConfig, ): boolean { const moduleID = this._resolver.getModuleID( this._virtualMocks, from, moduleName, + options, ); const key = from + path.delimiter + moduleID; @@ -1625,7 +1647,7 @@ export default class Runtime { let modulePath; try { - modulePath = this._resolveModule(from, moduleName); + modulePath = this._resolveModule(from, moduleName, options); } catch (e) { const manualMock = this._resolver.getMockModule(from, moduleName); if (manualMock) { From 839edc00ff06a48cd7c6beb1a184e8fcbaac26d5 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 9 Sep 2021 16:29:14 +0200 Subject: [PATCH 02/11] ci --- e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap | 4 ++-- .../__snapshots__/resolveNoFileExtensions.test.ts.snap | 2 +- e2e/resolve-conditions/fake-dep/module.cjs | 7 +++++++ e2e/resolve-conditions/fake-dep/module.mjs | 7 +++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 4431860f7880..8a2a22340028 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -41,7 +41,7 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:566:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:574:17) at Object.require (index.js:10:1) `; @@ -70,6 +70,6 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:566:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:574:17) at Object.require (index.js:10:1) `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index 10321533f870..afe1c4a8efb7 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,6 +37,6 @@ FAIL __tests__/test.js | ^ 9 | - at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:318:11) + at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:321:11) at Object.require (index.js:8:18) `; diff --git a/e2e/resolve-conditions/fake-dep/module.cjs b/e2e/resolve-conditions/fake-dep/module.cjs index 27dffe252340..ef70308ccc34 100644 --- a/e2e/resolve-conditions/fake-dep/module.cjs +++ b/e2e/resolve-conditions/fake-dep/module.cjs @@ -1,3 +1,10 @@ +/** + * 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. + */ + module.exports = { fn: () => 'hello from CJS', }; diff --git a/e2e/resolve-conditions/fake-dep/module.mjs b/e2e/resolve-conditions/fake-dep/module.mjs index bfa964d2127e..aa44a4b03e1e 100644 --- a/e2e/resolve-conditions/fake-dep/module.mjs +++ b/e2e/resolve-conditions/fake-dep/module.mjs @@ -1,3 +1,10 @@ +/** + * 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. + */ + export function fn() { return 'hello from ESM' } From 0f29eb58b0238918e6e232aa8b53ecb223da8ee5 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 10:18:29 +0200 Subject: [PATCH 03/11] do not pass require condition for esm --- packages/jest-runtime/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 93b28e05cc06..c76d654f8b83 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -173,7 +173,8 @@ const supportsNodeColonModulePrefixInImport = (() => { return stdout === 'true'; })(); -const esmConditions = ['import', 'require', 'default']; +// consider "node" condition as well - maybe switching on `global.window` in the test env? +const esmConditions = ['import', 'default']; const cjsConditions = ['require', 'default']; export default class Runtime { From 127071a1c02e3fcf2ebd0d44cfeb4146196c792c Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 10:27:03 +0200 Subject: [PATCH 04/11] add resolve options to cache key --- e2e/resolve-conditions/resolver.js | 6 ------ packages/jest-resolve/src/resolver.ts | 9 ++++++--- packages/jest-runtime/src/index.ts | 26 +++++++++++++++++++++----- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/e2e/resolve-conditions/resolver.js b/e2e/resolve-conditions/resolver.js index f59ccf15a1ad..5f27fa6a1300 100644 --- a/e2e/resolve-conditions/resolver.js +++ b/e2e/resolve-conditions/resolver.js @@ -10,12 +10,6 @@ const {resolve: resolveExports} = require('resolve.exports'); module.exports = (path, options) => { - const doWeCare = path.includes('fake-dep'); - - if (doWeCare) { - // console.log(path, options, new Error().stack); - } - return options.defaultResolver(path, { ...options, pathFilter: options.conditions diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index c5cf1d537da9..315537ae4fcd 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -140,7 +140,8 @@ export default class Resolver { ): Config.Path | null { const paths = options?.paths || this._options.modulePaths; const moduleDirectory = this._options.moduleDirectories; - const key = dirname + path.delimiter + moduleName; + const stringifiedOptions = options ? JSON.stringify(options) : ''; + const key = dirname + path.delimiter + moduleName + stringifiedOptions; const defaultPlatform = this._options.defaultPlatform; const extensions = this._options.extensions.slice(); let module; @@ -327,7 +328,8 @@ export default class Resolver { moduleName = '', options?: ResolveModuleConfig, ): string { - const key = from + path.delimiter + moduleName; + const stringifiedOptions = options ? JSON.stringify(options) : ''; + const key = from + path.delimiter + moduleName + stringifiedOptions; const cachedModuleID = this._moduleIDCache.get(key); if (cachedModuleID) { return cachedModuleID; @@ -347,7 +349,8 @@ export default class Resolver { moduleType + sep + (absolutePath ? absolutePath + sep : '') + - (mockPath ? mockPath + sep : ''); + (mockPath ? mockPath + sep : '') + + (stringifiedOptions ? stringifiedOptions + sep : ''); this._moduleIDCache.set(key, id); return id; diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index c76d654f8b83..20c7f8e8963d 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -298,6 +298,9 @@ export default class Runtime { const moduleID = this._resolver.getModuleID( this._virtualMocks, filePath, + undefined, + // is this correct? + {conditions: cjsConditions}, ); this._transitiveShouldMock.set(moduleID, false); } @@ -648,6 +651,7 @@ export default class Runtime { this._virtualModuleMocks, from, moduleName, + {conditions: esmConditions}, ); if (this._moduleMockRegistry.has(moduleID)) { @@ -714,10 +718,12 @@ export default class Runtime { options?: InternalModuleOptions, isRequireActual = false, ): T { + const isInternal = options?.isInternalModule ?? false; const moduleID = this._resolver.getModuleID( this._virtualMocks, from, moduleName, + isInternal ? undefined : {conditions: cjsConditions}, ); let modulePath: string | undefined; @@ -745,9 +751,11 @@ export default class Runtime { } if (!modulePath) { - modulePath = this._resolveModule(from, moduleName, { - conditions: cjsConditions, - }); + modulePath = this._resolveModule( + from, + moduleName, + isInternal ? undefined : {conditions: cjsConditions}, + ); } if (this.unstable_shouldLoadAsEsm(modulePath)) { @@ -764,7 +772,7 @@ export default class Runtime { let moduleRegistry; - if (options?.isInternalModule) { + if (isInternal) { moduleRegistry = this._internalModuleRegistry; } else if (this._isolatedModuleRegistry) { moduleRegistry = this._isolatedModuleRegistry; @@ -839,6 +847,7 @@ export default class Runtime { this._virtualMocks, from, moduleName, + {conditions: cjsConditions}, ); const mockRegistry = this._isolatedMockRegistry || this._mockRegistry; @@ -1109,6 +1118,7 @@ export default class Runtime { this._virtualMocks, from, moduleName, + {conditions: cjsConditions}, ); this._explicitShouldMock.set(moduleID, true); this._mockFactories.set(moduleID, mockFactory); @@ -1129,6 +1139,7 @@ export default class Runtime { this._virtualModuleMocks, from, moduleName, + {conditions: cjsConditions}, ); this._explicitShouldMockModule.set(moduleID, true); this._moduleMockFactories.set(moduleID, mockFactory); @@ -1180,7 +1191,7 @@ export default class Runtime { private _resolveModule( from: Config.Path, to: string | undefined, - options: ResolveModuleConfig, + options?: ResolveModuleConfig, ) { return to ? this._resolver.resolveModule(from, to, options) : from; } @@ -1667,6 +1678,8 @@ export default class Runtime { const currentModuleID = this._resolver.getModuleID( this._virtualMocks, from, + undefined, + options, ); if ( this._transitiveShouldMock.get(currentModuleID) === false || @@ -1755,6 +1768,7 @@ export default class Runtime { this._virtualMocks, from, moduleName, + {conditions: cjsConditions}, ); this._explicitShouldMock.set(moduleID, false); return jestObject; @@ -1764,6 +1778,7 @@ export default class Runtime { this._virtualMocks, from, moduleName, + {conditions: cjsConditions}, ); this._explicitShouldMock.set(moduleID, false); this._transitiveShouldMock.set(moduleID, false); @@ -1778,6 +1793,7 @@ export default class Runtime { this._virtualMocks, from, moduleName, + {conditions: cjsConditions}, ); this._explicitShouldMock.set(moduleID, true); return jestObject; From 9afb6efbda61a46a571cc5f37991dbe8d90e8b15 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 10:38:39 +0200 Subject: [PATCH 05/11] snaps again --- e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap | 4 ++-- .../__snapshots__/resolveNoFileExtensions.test.ts.snap | 2 +- e2e/__tests__/resolveConditions.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 8a2a22340028..c73d54ca2d2e 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -41,7 +41,7 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:574:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17) at Object.require (index.js:10:1) `; @@ -70,6 +70,6 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:574:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17) at Object.require (index.js:10:1) `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index afe1c4a8efb7..0abfba5f3a54 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,6 +37,6 @@ FAIL __tests__/test.js | ^ 9 | - at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:321:11) + at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:322:11) at Object.require (index.js:8:18) `; diff --git a/e2e/__tests__/resolveConditions.test.ts b/e2e/__tests__/resolveConditions.test.ts index ef6cc3980818..01b43d6ea0e4 100644 --- a/e2e/__tests__/resolveConditions.test.ts +++ b/e2e/__tests__/resolveConditions.test.ts @@ -19,7 +19,7 @@ beforeAll(() => { // The versions where vm.Module exists and commonjs with "exports" is not broken onNodeVersions('^12.16.0 || >=13.7.0', () => { test('resolves package exports correctly with custom resolver', () => { - // run multiple times to ensure there are noe caching errors + // run multiple times to ensure there are no caching errors for (let i = 0; i < 5; i++) { const {exitCode} = runJest(dir, [], { nodeOptions: '--experimental-vm-modules', From 30b8beb5e32b5c507e43233ebf728f562edca84a Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 10:46:26 +0200 Subject: [PATCH 06/11] resolve comment --- packages/jest-runtime/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 20c7f8e8963d..d12a7097fdfa 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -173,7 +173,7 @@ const supportsNodeColonModulePrefixInImport = (() => { return stdout === 'true'; })(); -// consider "node" condition as well - maybe switching on `global.window` in the test env? +// consider `node` & `browser` condition as well - maybe switching on `global.window` in the test env? const esmConditions = ['import', 'default']; const cjsConditions = ['require', 'default']; @@ -299,8 +299,12 @@ export default class Runtime { this._virtualMocks, filePath, undefined, - // is this correct? - {conditions: cjsConditions}, + // shouldn't really matter, but in theory this will make sure the caching is correct + { + conditions: this.unstable_shouldLoadAsEsm(filePath) + ? esmConditions + : cjsConditions, + }, ); this._transitiveShouldMock.set(moduleID, false); } From 980ff26cb001d9a75b80365fd47290dd68afccc5 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 12:03:27 +0200 Subject: [PATCH 07/11] oops --- packages/jest-runtime/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index d12a7097fdfa..d52f55e31669 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1143,7 +1143,7 @@ export default class Runtime { this._virtualModuleMocks, from, moduleName, - {conditions: cjsConditions}, + {conditions: esmConditions}, ); this._explicitShouldMockModule.set(moduleID, true); this._moduleMockFactories.set(moduleID, mockFactory); From 03abc32ee252a4eaa4048ce60ed21a6470a97ac9 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 12:25:12 +0200 Subject: [PATCH 08/11] lint --- e2e/resolve-conditions/fake-dep/module.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/resolve-conditions/fake-dep/module.mjs b/e2e/resolve-conditions/fake-dep/module.mjs index aa44a4b03e1e..45d72be15345 100644 --- a/e2e/resolve-conditions/fake-dep/module.mjs +++ b/e2e/resolve-conditions/fake-dep/module.mjs @@ -6,5 +6,5 @@ */ export function fn() { - return 'hello from ESM' + return 'hello from ESM'; } From b7e2b206822c9868440f5bf8b9848db6cf53ee06 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 12:29:42 +0200 Subject: [PATCH 09/11] docs --- docs/Configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index de70e3cdbebd..aa8edd6e5b1d 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -788,11 +788,13 @@ This option allows the use of a custom resolver. This resolver must be a node mo ```json { "basedir": string, + "conditions": [string], "defaultResolver": "function(request, options)", "extensions": [string], "moduleDirectory": [string], "paths": [string], "packageFilter": "function(pkg, pkgdir)", + "pathFilter": "function(pkg, path, relativePath)", "rootDir": [string] } ``` @@ -849,6 +851,8 @@ module.exports = (request, options) => { }; ``` +While Jest does not support [package `exports`](https://nodejs.org/api/packages.html#packages_package_entry_points) (beyond `main`), Jest will provide `conditions` as an option when calling custom resolvers, which can be used to implement support for `exports` in userland. Jest will pass `['import', 'default']` when running a test in ESM mode, and `['require', 'default']` when running with CJS. + ### `restoreMocks` \[boolean] Default: `false` From f3be5cd9188b2fa012d49d9ac20ee432d9c6b19f Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 12:42:55 +0200 Subject: [PATCH 10/11] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d115bde1cf7..fa11af892433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### Features +- `[jest-resolver, jest-runtime]` Pass `conditions` to custom resolvers to enable them to implement support for package.json `exports` field ([#11859](https://github.com/facebook/jest/pull/11859)) + + ### Fixes - `[@jest/reporters]` Use async transform if available to transform files with no coverage ([#11852](https://github.com/facebook/jest/pull/11852)) From 8a7f59a035b3ae019a608321c706b1587c78a137 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Sep 2021 12:44:09 +0200 Subject: [PATCH 11/11] prettier --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa11af892433..c1613581893c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ - `[jest-resolver, jest-runtime]` Pass `conditions` to custom resolvers to enable them to implement support for package.json `exports` field ([#11859](https://github.com/facebook/jest/pull/11859)) - ### Fixes - `[@jest/reporters]` Use async transform if available to transform files with no coverage ([#11852](https://github.com/facebook/jest/pull/11852))