From 75e94cc26714a9f23722dc127381c8e869ba42d8 Mon Sep 17 00:00:00 2001 From: Ahn Date: Mon, 15 Mar 2021 15:02:30 +0100 Subject: [PATCH] feat(jest-config): add support for `preset` written in ESM (#11200) --- CHANGELOG.md | 1 + docs/Configuration.md | 2 +- e2e/__tests__/presets.test.ts | 14 +- e2e/presets/cjs/__tests__/index.js | 11 ++ .../jest-preset-cjs/jest-preset.cjs | 12 ++ .../node_modules/jest-preset-cjs/mapper.js | 8 + e2e/presets/cjs/package.json | 5 + e2e/presets/js-type-module/__tests__/index.js | 11 ++ .../jest-preset-js-type-module/jest-preset.js | 12 ++ .../jest-preset-js-type-module/mapper.js | 8 + .../jest-preset-js-type-module/package.json | 3 + e2e/presets/js-type-module/package.json | 5 + e2e/presets/mjs/__tests__/index.js | 11 ++ .../jest-preset-mjs/jest-preset.mjs | 12 ++ .../node_modules/jest-preset-mjs/mapper.js | 8 + e2e/presets/mjs/package.json | 5 + .../src/__tests__/normalize.test.ts | 149 ++++++++++++------ packages/jest-config/src/normalize.ts | 44 ++++-- 18 files changed, 262 insertions(+), 59 deletions(-) create mode 100644 e2e/presets/cjs/__tests__/index.js create mode 100644 e2e/presets/cjs/node_modules/jest-preset-cjs/jest-preset.cjs create mode 100644 e2e/presets/cjs/node_modules/jest-preset-cjs/mapper.js create mode 100644 e2e/presets/cjs/package.json create mode 100644 e2e/presets/js-type-module/__tests__/index.js create mode 100644 e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/jest-preset.js create mode 100644 e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/mapper.js create mode 100644 e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/package.json create mode 100644 e2e/presets/js-type-module/package.json create mode 100644 e2e/presets/mjs/__tests__/index.js create mode 100644 e2e/presets/mjs/node_modules/jest-preset-mjs/jest-preset.mjs create mode 100644 e2e/presets/mjs/node_modules/jest-preset-mjs/mapper.js create mode 100644 e2e/presets/mjs/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f8897094d5fb..746172c7f036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[jest-circus, jest-jasmine2]` [**BREAKING**] Fail the test instead of just warning when describe returns a value ([#10947](https://github.com/facebook/jest/pull/10947)) - `[jest-config]` [**BREAKING**] Default to Node testing environment instead of browser (JSDOM) ([#9874](https://github.com/facebook/jest/pull/9874)) - `[jest-config]` [**BREAKING**] Use `jest-circus` as default test runner ([#10686](https://github.com/facebook/jest/pull/10686)) +- `[jest-config]` Add support for `preset` written in ESM ([#11200](https://github.com/facebook/jest/pull/11200)) - `[jest-config, jest-runtime]` Support ESM for files other than `.js` and `.mjs` ([#10823](https://github.com/facebook/jest/pull/10823)) - `[jest-config, jest-runtime]` [**BREAKING**] Use "modern" implementation as default for fake timers ([#10874](https://github.com/facebook/jest/pull/10874) & [#11197](https://github.com/facebook/jest/pull/11197)) - `[jest-core]` make `TestWatcher` extend `emittery` ([#10324](https://github.com/facebook/jest/pull/10324)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 7b9a0dc08757..c1dcd45f6df6 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -616,7 +616,7 @@ Specifies notification mode. Requires `notify: true`. Default: `undefined` -A preset that is used as a base for Jest's configuration. A preset should point to an npm module that has a `jest-preset.json` or `jest-preset.js` file at the root. +A preset that is used as a base for Jest's configuration. A preset should point to an npm module that has a `jest-preset.json`, `jest-preset.js`, `jest-preset.cjs` or `jest-preset.mjs` file at the root. For example, this preset `foo-bar/jest-preset.js` will be configured as follows: diff --git a/e2e/__tests__/presets.test.ts b/e2e/__tests__/presets.test.ts index 17029ef349d5..0e67588d5cc0 100644 --- a/e2e/__tests__/presets.test.ts +++ b/e2e/__tests__/presets.test.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import {onNodeVersions} from '@jest/test-utils'; import runJest from '../runJest'; test('supports json preset', () => { @@ -12,7 +13,16 @@ test('supports json preset', () => { expect(result.exitCode).toBe(0); }); -test('supports js preset', () => { - const result = runJest('presets/js'); +test.each(['js', 'cjs'])('supports %s preset', presetDir => { + const result = runJest(`presets/${presetDir}`); + expect(result.exitCode).toBe(0); }); + +onNodeVersions('^12.17.0 || >=13.2.0', () => { + test.each(['mjs', 'js-type-module'])('supports %s preset', presetDir => { + const result = runJest(`presets/${presetDir}`); + + expect(result.exitCode).toBe(0); + }); +}); diff --git a/e2e/presets/cjs/__tests__/index.js b/e2e/presets/cjs/__tests__/index.js new file mode 100644 index 000000000000..4a76f3aa7e6e --- /dev/null +++ b/e2e/presets/cjs/__tests__/index.js @@ -0,0 +1,11 @@ +/** + * 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'; + +test('load file mapped by cjs preset', () => { + expect(require('./test.foo')).toEqual(42); +}); diff --git a/e2e/presets/cjs/node_modules/jest-preset-cjs/jest-preset.cjs b/e2e/presets/cjs/node_modules/jest-preset-cjs/jest-preset.cjs new file mode 100644 index 000000000000..15cc175989e8 --- /dev/null +++ b/e2e/presets/cjs/node_modules/jest-preset-cjs/jest-preset.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. + */ + +module.exports = { + moduleNameMapper: { + '^.+\\.foo$': 'jest-preset-cjs/mapper.js', + }, +}; diff --git a/e2e/presets/cjs/node_modules/jest-preset-cjs/mapper.js b/e2e/presets/cjs/node_modules/jest-preset-cjs/mapper.js new file mode 100644 index 000000000000..ddedd1efe52c --- /dev/null +++ b/e2e/presets/cjs/node_modules/jest-preset-cjs/mapper.js @@ -0,0 +1,8 @@ +/** + * 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 = 42; diff --git a/e2e/presets/cjs/package.json b/e2e/presets/cjs/package.json new file mode 100644 index 000000000000..45d602f1eda7 --- /dev/null +++ b/e2e/presets/cjs/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "preset": "jest-preset-cjs" + } +} diff --git a/e2e/presets/js-type-module/__tests__/index.js b/e2e/presets/js-type-module/__tests__/index.js new file mode 100644 index 000000000000..854b03bdff9a --- /dev/null +++ b/e2e/presets/js-type-module/__tests__/index.js @@ -0,0 +1,11 @@ +/** + * 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'; + +test('load file mapped by js preset', () => { + expect(require('./test.foo')).toEqual(42); +}); diff --git a/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/jest-preset.js b/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/jest-preset.js new file mode 100644 index 000000000000..9a3aa71dcfa5 --- /dev/null +++ b/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/jest-preset.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. + */ + +export default { + moduleNameMapper: { + '^.+\\.foo$': 'jest-preset-js-type-module/mapper.js', + }, +}; diff --git a/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/mapper.js b/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/mapper.js new file mode 100644 index 000000000000..ddedd1efe52c --- /dev/null +++ b/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/mapper.js @@ -0,0 +1,8 @@ +/** + * 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 = 42; diff --git a/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/package.json b/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/package.json new file mode 100644 index 000000000000..3dbc1ca591c0 --- /dev/null +++ b/e2e/presets/js-type-module/node_modules/jest-preset-js-type-module/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/e2e/presets/js-type-module/package.json b/e2e/presets/js-type-module/package.json new file mode 100644 index 000000000000..b51c57fda90d --- /dev/null +++ b/e2e/presets/js-type-module/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "preset": "jest-preset-js-type-module" + } +} diff --git a/e2e/presets/mjs/__tests__/index.js b/e2e/presets/mjs/__tests__/index.js new file mode 100644 index 000000000000..97e656f04bc9 --- /dev/null +++ b/e2e/presets/mjs/__tests__/index.js @@ -0,0 +1,11 @@ +/** + * 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'; + +test('load file mapped by mjs preset', () => { + expect(require('./test.foo')).toEqual(42); +}); diff --git a/e2e/presets/mjs/node_modules/jest-preset-mjs/jest-preset.mjs b/e2e/presets/mjs/node_modules/jest-preset-mjs/jest-preset.mjs new file mode 100644 index 000000000000..9b359fe2ef81 --- /dev/null +++ b/e2e/presets/mjs/node_modules/jest-preset-mjs/jest-preset.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. + */ + +export default { + moduleNameMapper: { + '^.+\\.foo$': 'jest-preset-mjs/mapper.js', + }, +}; diff --git a/e2e/presets/mjs/node_modules/jest-preset-mjs/mapper.js b/e2e/presets/mjs/node_modules/jest-preset-mjs/mapper.js new file mode 100644 index 000000000000..ddedd1efe52c --- /dev/null +++ b/e2e/presets/mjs/node_modules/jest-preset-mjs/mapper.js @@ -0,0 +1,8 @@ +/** + * 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 = 42; diff --git a/e2e/presets/mjs/package.json b/e2e/presets/mjs/package.json new file mode 100644 index 000000000000..aa46444dcab7 --- /dev/null +++ b/e2e/presets/mjs/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "preset": "jest-preset-mjs" + } +} diff --git a/packages/jest-config/src/__tests__/normalize.test.ts b/packages/jest-config/src/__tests__/normalize.test.ts index 648f897197fe..9d4b7b1d23de 100644 --- a/packages/jest-config/src/__tests__/normalize.test.ts +++ b/packages/jest-config/src/__tests__/normalize.test.ts @@ -10,7 +10,7 @@ import {createHash} from 'crypto'; import path from 'path'; import {wrap} from 'jest-snapshot-serializer-raw'; import stripAnsi from 'strip-ansi'; -import {Config} from '@jest/types'; +import type {Config} from '@jest/types'; import {escapeStrForRegex} from 'jest-regex-util'; import Defaults from '../Defaults'; import {DEFAULT_JS_PATTERN} from '../constants'; @@ -29,13 +29,13 @@ jest }; }); -let root; -let expectedPathFooBar; -let expectedPathFooQux; -let expectedPathAbs; -let expectedPathAbsAnother; +let root: string; +let expectedPathFooBar: string; +let expectedPathFooQux: string; +let expectedPathAbs: string; +let expectedPathAbsAnother: string; -let virtualModuleRegexes; +let virtualModuleRegexes: Array; beforeEach(() => (virtualModuleRegexes = [/jest-circus/, /babel-jest/])); const findNodeModule = jest.fn(name => { if (virtualModuleRegexes.some(regex => regex.test(name))) { @@ -47,7 +47,7 @@ const findNodeModule = jest.fn(name => { // Windows uses backslashes for path separators, which need to be escaped in // regular expressions. This little helper function helps us generate the // expected strings for checking path patterns. -function joinForPattern(...args) { +function joinForPattern(...args: Array) { return args.join(escapeStrForRegex(path.sep)); } @@ -64,7 +64,7 @@ beforeEach(() => { }); afterEach(() => { - console.warn.mockRestore(); + ((console.warn as unknown) as jest.SpyInstance).mockRestore(); }); it('picks a name based on the rootDir', async () => { @@ -148,7 +148,9 @@ describe('rootDir', () => { describe('automock', () => { it('falsy automock is not overwritten', async () => { - console.warn.mockImplementation(() => {}); + ((console.warn as unknown) as jest.SpyInstance).mockImplementation( + () => {}, + ); const {options} = await normalize( { automock: false, @@ -174,7 +176,7 @@ describe('collectCoverageOnlyFrom', () => { {} as Config.Argv, ); - const expected = {}; + const expected = Object.create(null); expected[expectedPathFooBar] = true; expected[expectedPathFooQux] = true; @@ -193,7 +195,7 @@ describe('collectCoverageOnlyFrom', () => { {} as Config.Argv, ); - const expected = {}; + const expected = Object.create(null); expected[expectedPathAbs] = true; expected[expectedPathAbsAnother] = true; @@ -211,7 +213,7 @@ describe('collectCoverageOnlyFrom', () => { {} as Config.Argv, ); - const expected = {}; + const expected = Object.create(null); expected[expectedPathFooBar] = true; expect(options.collectCoverageOnlyFrom).toEqual(expected); @@ -268,7 +270,7 @@ describe('findRelatedTests', () => { }); }); -function testPathArray(key) { +function testPathArray(key: string) { it('normalizes all paths relative to rootDir', async () => { const {options} = await normalize( { @@ -430,7 +432,9 @@ describe('setupTestFrameworkScriptFile', () => { let Resolver; beforeEach(() => { - console.warn.mockImplementation(() => {}); + ((console.warn as unknown) as jest.SpyInstance).mockImplementation( + () => {}, + ); Resolver = require('jest-resolve').default; Resolver.findNodeModule = jest.fn(name => name.startsWith('/') ? name : '/root/path/foo' + path.sep + name, @@ -446,7 +450,9 @@ describe('setupTestFrameworkScriptFile', () => { {} as Config.Argv, ); - expect(console.warn.mock.calls[0][0]).toMatchSnapshot(); + expect( + ((console.warn as unknown) as jest.SpyInstance).mock.calls[0][0], + ).toMatchSnapshot(); }); it('logs an error when `setupTestFrameworkScriptFile` and `setupFilesAfterEnv` are used', async () => { @@ -819,7 +825,9 @@ describe('babel-jest', () => { describe('Upgrade help', () => { beforeEach(() => { - console.warn.mockImplementation(() => {}); + ((console.warn as unknown) as jest.SpyInstance).mockImplementation( + () => {}, + ); const Resolver = require('jest-resolve').default; Resolver.findNodeModule = jest.fn(name => { @@ -846,11 +854,13 @@ describe('Upgrade help', () => { joinForPattern('qux', 'quux'), ]); - expect(options.scriptPreprocessor).toBe(undefined); - expect(options.preprocessorIgnorePatterns).toBe(undefined); + expect(options).not.toHaveProperty('scriptPreprocessor'); + expect(options).not.toHaveProperty('preprocessorIgnorePatterns'); expect(hasDeprecationWarnings).toBeTruthy(); - expect(console.warn.mock.calls[0][0]).toMatchSnapshot(); + expect( + ((console.warn as unknown) as jest.SpyInstance).mock.calls[0][0], + ).toMatchSnapshot(); }); }); @@ -976,6 +986,14 @@ describe('preset', () => { return '/node_modules/react-native-js-preset/jest-preset.js'; } + if (name === 'cjs-preset/jest-preset') { + return '/node_modules/cjs-preset/jest-preset.cjs'; + } + + if (name === 'mjs-preset/jest-preset') { + return '/node_modules/mjs-preset/jest-preset.mjs'; + } + if (name.includes('doesnt-exist')) { return null; } @@ -1002,29 +1020,20 @@ describe('preset', () => { }), {virtual: true}, ); - jest.mock( - '/node_modules/with-json-ext/jest-preset.json', - () => ({ - moduleNameMapper: { - json: true, - }, - }), - {virtual: true}, - ); - jest.mock( - '/node_modules/with-js-ext/jest-preset.js', + jest.doMock( + '/node_modules/cjs-preset/jest-preset.cjs', () => ({ moduleNameMapper: { - js: true, + cjs: true, }, }), {virtual: true}, ); - jest.mock( - '/node_modules/exist-but-no-jest-preset/index.js', + jest.doMock( + '/node_modules/mjs-preset/jest-preset.mjs', () => ({ moduleNameMapper: { - js: true, + mjs: true, }, }), {virtual: true}, @@ -1033,6 +1042,9 @@ describe('preset', () => { afterEach(() => { jest.dontMock('/node_modules/react-native/jest-preset.json'); + jest.dontMock('/node_modules/react-native-js-preset/jest-preset.js'); + jest.dontMock('/node_modules/cjs-preset/jest-preset.cjs'); + jest.dontMock('/node_modules/mjs-preset/jest-preset.mjs'); }); test('throws when preset not found', async () => { @@ -1136,7 +1148,34 @@ describe('preset', () => { ).resolves.not.toThrow(); }); - test('searches for .json and .js preset files', async () => { + test.each(['react-native-js-preset', 'cjs-preset'])( + 'works with cjs preset', + async presetName => { + await expect( + normalize( + { + preset: presetName, + rootDir: '/root/path/foo', + }, + {} as Config.Argv, + ), + ).resolves.not.toThrow(); + }, + ); + + test('works with esm preset', async () => { + await expect( + normalize( + { + preset: 'mjs-preset', + rootDir: '/root/path/foo', + }, + {} as Config.Argv, + ), + ).resolves.not.toThrow(); + }); + + test('searches for .json, .js, .cjs, .mjs preset files', async () => { const Resolver = require('jest-resolve').default; await normalize( @@ -1148,7 +1187,7 @@ describe('preset', () => { ); const options = Resolver.findNodeModule.mock.calls[0][1]; - expect(options.extensions).toEqual(['.json', '.js']); + expect(options.extensions).toEqual(['.json', '.js', '.cjs', '.mjs']); }); test('merges with options', async () => { @@ -1188,7 +1227,7 @@ describe('preset', () => { // Object initializer not used for properties as a workaround for // sort-keys eslint rule while specifying properties in // non-alphabetical order for a better test - const moduleNameMapper = {}; + const moduleNameMapper = {} as Record; moduleNameMapper.e = 'ee'; moduleNameMapper.b = 'bb'; moduleNameMapper.c = 'cc'; @@ -1366,7 +1405,7 @@ describe('runner', () => { }); it('defaults to `jest-runner`', async () => { - const {options} = await normalize({rootDir: '/root'}, {}); + const {options} = await normalize({rootDir: '/root'}, {} as Config.Argv); expect(options.runner).toBe('jest-runner'); }); @@ -1427,7 +1466,7 @@ describe('watchPlugins', () => { }); it('defaults to undefined', async () => { - const {options} = await normalize({rootDir: '/root'}, {}); + const {options} = await normalize({rootDir: '/root'}, {} as Config.Argv); expect(options.watchPlugins).toEqual(undefined); }); @@ -1501,7 +1540,7 @@ describe('testPathPattern', () => { }); it('defaults to empty', async () => { - const {options} = await normalize(initialOptions, {}); + const {options} = await normalize(initialOptions, {} as Config.Argv); expect(options.testPathPattern).toBe(''); }); @@ -1524,7 +1563,9 @@ describe('testPathPattern', () => { const {options} = await normalize(initialOptions, argv); expect(options.testPathPattern).toBe(''); - expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + expect( + ((console.log as unknown) as jest.SpyInstance).mock.calls[0][0], + ).toMatchSnapshot(); }); it('joins multiple ' + opt.name + ' if set', async () => { @@ -1664,7 +1705,9 @@ describe('cwd', () => { }); it('is not lost if the config has its own cwd property', async () => { - console.warn.mockImplementation(() => {}); + ((console.warn as unknown) as jest.SpyInstance).mockImplementation( + () => {}, + ); const {options} = await normalize( { cwd: '/tmp/config-sets-cwd-itself', @@ -1725,14 +1768,16 @@ describe('displayName', () => { }, {} as Config.Argv, ); - expect(displayName.name).toBe('project'); - expect(displayName.color).toMatchSnapshot(); + expect(displayName!.name).toBe('project'); + expect(displayName!.color).toMatchSnapshot(); }); }); describe('testTimeout', () => { it('should return timeout value if defined', async () => { - console.warn.mockImplementation(() => {}); + ((console.warn as unknown) as jest.SpyInstance).mockImplementation( + () => {}, + ); const {options} = await normalize( {rootDir: '/root/', testTimeout: 1000}, {} as Config.Argv, @@ -1750,7 +1795,17 @@ describe('testTimeout', () => { }); describe('extensionsToTreatAsEsm', () => { - async function matchErrorSnapshot(callback) { + async function matchErrorSnapshot(callback: { + (): Promise<{ + hasDeprecationWarnings: boolean; + options: Config.ProjectConfig & Config.GlobalConfig; + }>; + (): Promise<{ + hasDeprecationWarnings: boolean; + options: Config.ProjectConfig & Config.GlobalConfig; + }>; + (): any; + }) { expect.assertions(1); try { diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 66a93282d74f..1c84debbda15 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -7,6 +7,7 @@ import {createHash} from 'crypto'; import * as path from 'path'; +import {pathToFileURL} from 'url'; import chalk = require('chalk'); import merge = require('deepmerge'); import {sync as glob} from 'glob'; @@ -39,7 +40,7 @@ import { } from './utils'; import validatePattern from './validatePattern'; const ERROR = `${BULLET}Validation Error`; -const PRESET_EXTENSIONS = ['.json', '.js']; +const PRESET_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs']; const PRESET_NAME = 'jest-preset'; type AllOptions = Config.ProjectConfig & Config.GlobalConfig; @@ -116,10 +117,10 @@ const mergeGlobalsWithPreset = ( } }; -const setupPreset = ( +const setupPreset = async ( options: Config.InitialOptionsWithRootDir, optionsPreset: string, -): Config.InitialOptionsWithRootDir => { +): Promise => { let preset: Config.InitialOptions; const presetPath = replaceRootDirInPath(options.rootDir, optionsPreset); const presetModule = Resolver.findNodeModule( @@ -176,11 +177,36 @@ const setupPreset = ( ); } - throw createConfigError( - ` An unknown error occurred in ${chalk.bold(presetPath)}:\n\n ${ - error.message - }\n ${error.stack}`, - ); + if (presetModule && error.code === 'ERR_REQUIRE_ESM') { + try { + const presetModuleUrl = pathToFileURL(presetModule); + + // node `import()` supports URL, but TypeScript doesn't know that + const importedPreset = await import(presetModuleUrl.href); + + if (!importedPreset.default) { + throw createConfigError( + `Jest: Failed to load mjs config file ${presetModule} - did you use a default export?`, + ); + } + + preset = importedPreset.default; + } catch (innerError) { + if (innerError.message === 'Not supported') { + throw createConfigError( + `Jest: Your version of Node does not support dynamic import - please enable it or use a .cjs file extension for file ${presetModule}`, + ); + } + + throw innerError; + } + } else { + throw createConfigError( + ` An unknown error occurred in ${chalk.bold(presetPath)}:\n\n ${ + error.message + }\n ${error.stack}`, + ); + } } if (options.setupFiles) { @@ -576,7 +602,7 @@ export default async function normalize( ); if (options.preset) { - options = setupPreset(options, options.preset); + options = await setupPreset(options, options.preset); } if (!options.setupFilesAfterEnv) {