From 0d411214f9fc3558855992292788dc7a0d7fd199 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 14 Nov 2020 15:53:57 +0100 Subject: [PATCH] feat: support files other than js to be ESM --- CHANGELOG.md | 1 + docs/Configuration.md | 17 ++++++ docs/ECMAScriptModules.md | 5 +- .../__snapshots__/showConfig.test.ts.snap | 1 + e2e/__tests__/nativeEsmTypescript.test.ts | 26 ++++++++ .../__tests__/double.test.ts | 16 +++++ e2e/native-esm-typescript/babel.config.js | 11 ++++ e2e/native-esm-typescript/double.ts | 10 ++++ e2e/native-esm-typescript/package.json | 8 +++ .../legacy-code-todo-rewrite/jestAdapter.ts | 13 ++-- packages/jest-config/package.json | 2 + packages/jest-config/src/Defaults.ts | 1 + packages/jest-config/src/ValidConfig.ts | 1 + .../__snapshots__/normalize.test.js.snap | 37 ++++++++++++ .../src/__tests__/normalize.test.js | 35 +++++++++++ packages/jest-config/src/index.ts | 1 + packages/jest-config/src/normalize.ts | 60 +++++++++++++++++++ .../logDebugMessages.test.ts.snap | 1 + packages/jest-jasmine2/src/index.ts | 13 ++-- packages/jest-repl/src/cli/runtime-cli.ts | 12 ++-- packages/jest-resolve/src/shouldLoadAsEsm.ts | 24 ++++---- packages/jest-runner/src/runTest.ts | 6 +- .../src/__mocks__/createRuntime.js | 5 +- packages/jest-runtime/src/index.ts | 5 +- .../script_transformer.test.ts.snap | 7 ++- packages/jest-types/src/Config.ts | 3 + packages/test-utils/src/config.ts | 1 + yarn.lock | 2 + 28 files changed, 291 insertions(+), 33 deletions(-) create mode 100644 e2e/__tests__/nativeEsmTypescript.test.ts create mode 100644 e2e/native-esm-typescript/__tests__/double.test.ts create mode 100644 e2e/native-esm-typescript/babel.config.js create mode 100644 e2e/native-esm-typescript/double.ts create mode 100644 e2e/native-esm-typescript/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d8e2fee6a2..3fcbaaae2a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[jest-config]` [**BREAKING**] Default to Node testing environment instead of browser (JSDOM) ([#9874](https://github.com/facebook/jest/pull/9874)) +- `[jest-config, jest-runtime]` Support ESM for files other than `.js` and `.mjs` ([#10823](https://github.com/facebook/jest/pull/10823)) - `[jest-runner]` [**BREAKING**] set exit code to 1 if test logs after teardown ([#10728](https://github.com/facebook/jest/pull/10728)) - `[jest-snapshot]`: [**BREAKING**] Make prettier optional for inline snapshots - fall back to string replacement ([#7792](https://github.com/facebook/jest/pull/7792)) - `[jest-repl, jest-runner]` [**BREAKING**] Run transforms over environment ([#8751](https://github.com/facebook/jest/pull/8751)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 1e4d164b125c..fe05cda6cadd 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -353,6 +353,23 @@ Default: `false` Make calling deprecated APIs throw helpful error messages. Useful for easing the upgrade process. +### `extensionsToTreatAsEsm` [array\] + +Default: `[]` + +Jest will run `.mjs` and `.js` files with nearest `package.json`'s `type` field set to `module` as ECMAScript Modules. If you have any other files that should run with native ESM, you need to specify their file extension here. + +> Note: Jest's ESM support is still experimental, see [its docs for more details](ECMAScriptModules.md). + +```json +{ + ... + "jest": { + "extensionsToTreatAsEsm": [".ts"] + } +} +``` + ### `extraGlobals` [array\] Default: `undefined` diff --git a/docs/ECMAScriptModules.md b/docs/ECMAScriptModules.md index 7b5e4448419d..61855ea5fa9c 100644 --- a/docs/ECMAScriptModules.md +++ b/docs/ECMAScriptModules.md @@ -12,8 +12,9 @@ Jest ships with _experimental_ support for ECMAScript Modules (ESM). With the warnings out of the way, this is how you activate ESM support in your tests. 1. Ensure you either disable [code transforms](./configuration#transform-objectstring-pathtotransformer--pathtotransformer-object) by passing `transform: {}` or otherwise configure your transformer to emit ESM rather than the default CommonJS (CJS). -1. Execute `node` with `--experimental-vm-modules`, e.g. `node --experimental-vm-modules node_modules/.bin/jest` or `NODE_OPTIONS=--experimental-vm-modules npx jest` etc.. On Windows, you can use [`cross-env`](https://github.com/kentcdodds/cross-env) to be able to set environment variables -1. Beyond that, we attempt to follow `node`'s logic for activating "ESM mode" (such as looking at `type` in `package.json` or `mjs` files), see [their docs](https://nodejs.org/api/esm.html#esm_enabling) for details +1. Execute `node` with `--experimental-vm-modules`, e.g. `node --experimental-vm-modules node_modules/.bin/jest` or `NODE_OPTIONS=--experimental-vm-modules npx jest` etc.. On Windows, you can use [`cross-env`](https://github.com/kentcdodds/cross-env) to be able to set environment variables. +1. Beyond that, we attempt to follow `node`'s logic for activating "ESM mode" (such as looking at `type` in `package.json` or `mjs` files), see [their docs](https://nodejs.org/api/esm.html#esm_enabling) for details. +1. If you want to treat other file extensions (such as `ts`) as ESM, please use the [`extensionsToTreatAsEsm` option](Configuration.md#extensionstotreatasesm-arraystring). ## Differences between ESM and CommonJS diff --git a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap index 9e24197f7ab0..2722355d5c34 100644 --- a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap +++ b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap @@ -15,6 +15,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` "detectLeaks": false, "detectOpenHandles": false, "errorOnDeprecated": false, + "extensionsToTreatAsEsm": [], "extraGlobals": [], "forceCoverageMatch": [], "globals": {}, diff --git a/e2e/__tests__/nativeEsmTypescript.test.ts b/e2e/__tests__/nativeEsmTypescript.test.ts new file mode 100644 index 000000000000..afec843cce91 --- /dev/null +++ b/e2e/__tests__/nativeEsmTypescript.test.ts @@ -0,0 +1,26 @@ +/** + * 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 {json as runJest} from '../runJest'; + +const DIR = resolve(__dirname, '../native-esm-typescript'); + +// The versions where vm.Module exists and commonjs with "exports" is not broken +onNodeVersions('^12.16.0 || >=13.7.0', () => { + test('runs TS test with native ESM', () => { + const {exitCode, json} = runJest(DIR, [], { + nodeOptions: '--experimental-vm-modules', + }); + + expect(exitCode).toBe(0); + + expect(json.numTotalTests).toBe(2); + expect(json.numPassedTests).toBe(2); + }); +}); diff --git a/e2e/native-esm-typescript/__tests__/double.test.ts b/e2e/native-esm-typescript/__tests__/double.test.ts new file mode 100644 index 000000000000..ad06fbe075f8 --- /dev/null +++ b/e2e/native-esm-typescript/__tests__/double.test.ts @@ -0,0 +1,16 @@ +/** + * 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 {double} from '../double'; + +test('test double', () => { + expect(double(2)).toBe(4); +}); + +test('test import.meta', () => { + expect(typeof import.meta.url).toBe('string'); +}); diff --git a/e2e/native-esm-typescript/babel.config.js b/e2e/native-esm-typescript/babel.config.js new file mode 100644 index 000000000000..2903dbb9f6be --- /dev/null +++ b/e2e/native-esm-typescript/babel.config.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. + */ + +module.exports = { + // importantly this does _not_ include `preset-env` + presets: ['@babel/preset-typescript'], +}; diff --git a/e2e/native-esm-typescript/double.ts b/e2e/native-esm-typescript/double.ts new file mode 100644 index 000000000000..8e81ae153718 --- /dev/null +++ b/e2e/native-esm-typescript/double.ts @@ -0,0 +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 double(num: number): number { + return num * 2; +} diff --git a/e2e/native-esm-typescript/package.json b/e2e/native-esm-typescript/package.json new file mode 100644 index 000000000000..3e30fb6f9177 --- /dev/null +++ b/e2e/native-esm-typescript/package.json @@ -0,0 +1,8 @@ +{ + "name": "native-esm-typescript", + "version": "1.0.0", + "jest": { + "extensionsToTreatAsEsm": [".ts"], + "testEnvironment": "node" + } +} diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index e16cc8a0a271..51e6621e1347 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -72,8 +72,10 @@ const jestAdapter = async ( }); for (const path of config.setupFilesAfterEnv) { - // TODO: remove ? in Jest 26 - const esm = runtime.unstable_shouldLoadAsEsm?.(path); + const esm = runtime.unstable_shouldLoadAsEsm( + path, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(path); @@ -81,9 +83,10 @@ const jestAdapter = async ( runtime.requireModule(path); } } - - // TODO: remove ? in Jest 26 - const esm = runtime.unstable_shouldLoadAsEsm?.(testPath); + const esm = runtime.unstable_shouldLoadAsEsm( + testPath, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(testPath); diff --git a/packages/jest-config/package.json b/packages/jest-config/package.json index 91d7ca643b5a..a0a39f602585 100644 --- a/packages/jest-config/package.json +++ b/packages/jest-config/package.json @@ -46,6 +46,8 @@ "@types/glob": "^7.1.1", "@types/graceful-fs": "^4.1.3", "@types/micromatch": "^4.0.0", + "jest-snapshot-serializer-raw": "^1.1.0", + "strip-ansi": "^6.0.0", "ts-node": "^9.0.0", "typescript": "^4.0.3" }, diff --git a/packages/jest-config/src/Defaults.ts b/packages/jest-config/src/Defaults.ts index 0c8ef6bbf4db..4c631942859c 100644 --- a/packages/jest-config/src/Defaults.ts +++ b/packages/jest-config/src/Defaults.ts @@ -26,6 +26,7 @@ const defaultOptions: Config.DefaultOptions = { coverageReporters: ['json', 'text', 'lcov', 'clover'], errorOnDeprecated: false, expand: false, + extensionsToTreatAsEsm: [], forceCoverageMatch: [], globals: {}, haste: { diff --git a/packages/jest-config/src/ValidConfig.ts b/packages/jest-config/src/ValidConfig.ts index b05a85163070..c2792a7912c2 100644 --- a/packages/jest-config/src/ValidConfig.ts +++ b/packages/jest-config/src/ValidConfig.ts @@ -44,6 +44,7 @@ const initialOptions: Config.InitialOptions = { } as const), errorOnDeprecated: false, expand: false, + extensionsToTreatAsEsm: [], extraGlobals: [], filter: '/filter.js', forceCoverageMatch: ['**/*.t.js'], diff --git a/packages/jest-config/src/__tests__/__snapshots__/normalize.test.js.snap b/packages/jest-config/src/__tests__/__snapshots__/normalize.test.js.snap index 29f8228c7c3d..e34137553cec 100644 --- a/packages/jest-config/src/__tests__/__snapshots__/normalize.test.js.snap +++ b/packages/jest-config/src/__tests__/__snapshots__/normalize.test.js.snap @@ -91,6 +91,43 @@ exports[`displayName should throw an error when displayName is using invalid val " `; +exports[`extensionsToTreatAsEsm should enforce leading dots 1`] = ` +● Validation Error: + + Option: extensionsToTreatAsEsm: ['ts'] includes a string that does not start with a period (.). + Please change your configuration to extensionsToTreatAsEsm: ['.ts']. + + Configuration Documentation: + https://jestjs.io/docs/configuration.html +`; + +exports[`extensionsToTreatAsEsm throws on .cjs 1`] = ` +● Validation Error: + + Option: extensionsToTreatAsEsm: ['.cjs'] includes '.cjs' which is always treated as CommonJS. + + Configuration Documentation: + https://jestjs.io/docs/configuration.html +`; + +exports[`extensionsToTreatAsEsm throws on .js 1`] = ` +● Validation Error: + + Option: extensionsToTreatAsEsm: ['.js'] includes '.js' which is always inferred based on type in its nearest package.json. + + Configuration Documentation: + https://jestjs.io/docs/configuration.html +`; + +exports[`extensionsToTreatAsEsm throws on .mjs 1`] = ` +● Validation Error: + + Option: extensionsToTreatAsEsm: ['.mjs'] includes '.mjs' which is always treated as an ECMAScript Module. + + Configuration Documentation: + https://jestjs.io/docs/configuration.html +`; + exports[`preset throws when module was found but no "jest-preset.js" or "jest-preset.json" files 1`] = ` "Validation Error: diff --git a/packages/jest-config/src/__tests__/normalize.test.js b/packages/jest-config/src/__tests__/normalize.test.js index f43174c43757..38808a77168b 100644 --- a/packages/jest-config/src/__tests__/normalize.test.js +++ b/packages/jest-config/src/__tests__/normalize.test.js @@ -8,6 +8,8 @@ import crypto from 'crypto'; import path from 'path'; +import {wrap} from 'jest-snapshot-serializer-raw'; +import stripAnsi from 'strip-ansi'; import {escapeStrForRegex} from 'jest-regex-util'; import Defaults from '../Defaults'; import {DEFAULT_JS_PATTERN} from '../constants'; @@ -1713,3 +1715,36 @@ describe('testTimeout', () => { ).toThrowErrorMatchingSnapshot(); }); }); + +describe('extensionsToTreatAsEsm', () => { + function matchErrorSnapshot(callback) { + expect.assertions(1); + + try { + callback(); + } catch (error) { + expect(wrap(stripAnsi(error.message).trim())).toMatchSnapshot(); + } + } + + it('should pass valid config through', () => { + const {options} = normalize( + {extensionsToTreatAsEsm: ['.ts'], rootDir: '/root/'}, + {}, + ); + + expect(options.extensionsToTreatAsEsm).toEqual(['.ts']); + }); + + it('should enforce leading dots', () => { + matchErrorSnapshot(() => + normalize({extensionsToTreatAsEsm: ['ts'], rootDir: '/root/'}, {}), + ); + }); + + it.each(['.js', '.mjs', '.cjs'])('throws on %s', ext => { + matchErrorSnapshot(() => + normalize({extensionsToTreatAsEsm: [ext], rootDir: '/root/'}, {}), + ); + }); +}); diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index eac57f2fd94a..1a7ba7074f47 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -178,6 +178,7 @@ const groupOptions = ( detectOpenHandles: options.detectOpenHandles, displayName: options.displayName, errorOnDeprecated: options.errorOnDeprecated, + extensionsToTreatAsEsm: options.extensionsToTreatAsEsm, extraGlobals: options.extraGlobals, filter: options.filter, forceCoverageMatch: options.forceCoverageMatch, diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 97ea8dc7bfe2..3c6ba5a0cd2a 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -482,6 +482,63 @@ const showTestPathPatternError = (testPathPattern: string) => { ); }; +function validateExtensionsToTreatAsEsm( + extensionsToTreatAsEsm: Config.InitialOptions['extensionsToTreatAsEsm'], +) { + if (!extensionsToTreatAsEsm || extensionsToTreatAsEsm.length === 0) { + return; + } + + function printConfig(opts: Array) { + const string = opts.map(ext => `'${ext}'`).join(', '); + + return chalk.bold(`extensionsToTreatAsEsm: [${string}]`); + } + + const extensionWithoutDot = extensionsToTreatAsEsm.some( + ext => !ext.startsWith('.'), + ); + + if (extensionWithoutDot) { + throw createConfigError( + ` Option: ${printConfig( + extensionsToTreatAsEsm, + )} includes a string that does not start with a period (${chalk.bold( + '.', + )}). + Please change your configuration to ${printConfig( + extensionsToTreatAsEsm.map(ext => (ext.startsWith('.') ? ext : `.${ext}`)), + )}.`, + ); + } + + if (extensionsToTreatAsEsm.includes('.js')) { + throw createConfigError( + ` Option: ${printConfig(extensionsToTreatAsEsm)} includes ${chalk.bold( + "'.js'", + )} which is always inferred based on ${chalk.bold( + 'type', + )} in its nearest ${chalk.bold('package.json')}.`, + ); + } + + if (extensionsToTreatAsEsm.includes('.cjs')) { + throw createConfigError( + ` Option: ${printConfig(extensionsToTreatAsEsm)} includes ${chalk.bold( + "'.cjs'", + )} which is always treated as CommonJS.`, + ); + } + + if (extensionsToTreatAsEsm.includes('.mjs')) { + throw createConfigError( + ` Option: ${printConfig(extensionsToTreatAsEsm)} includes ${chalk.bold( + "'.mjs'", + )} which is always treated as an ECMAScript Module.`, + ); + } +} + export default function normalize( initialOptions: Config.InitialOptions, argv: Config.Argv, @@ -577,6 +634,8 @@ export default function normalize( }); } + validateExtensionsToTreatAsEsm(options.extensionsToTreatAsEsm); + const optionKeys = Object.keys(options) as Array; optionKeys.reduce((newOptions, key: keyof Config.InitialOptions) => { @@ -881,6 +940,7 @@ export default function normalize( case 'detectOpenHandles': case 'errorOnDeprecated': case 'expand': + case 'extensionsToTreatAsEsm': case 'extraGlobals': case 'globals': case 'findRelatedTests': diff --git a/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap b/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap index 3e384ab92cda..7b74de72bd44 100644 --- a/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap +++ b/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap @@ -12,6 +12,7 @@ exports[`prints the config object 1`] = ` "detectLeaks": false, "detectOpenHandles": false, "errorOnDeprecated": false, + "extensionsToTreatAsEsm": [], "extraGlobals": [], "forceCoverageMatch": [], "globals": {}, diff --git a/packages/jest-jasmine2/src/index.ts b/packages/jest-jasmine2/src/index.ts index 29d508cc6f6d..3dd1a4a6e269 100644 --- a/packages/jest-jasmine2/src/index.ts +++ b/packages/jest-jasmine2/src/index.ts @@ -149,8 +149,10 @@ async function jasmine2( }); for (const path of config.setupFilesAfterEnv) { - // TODO: remove ? in Jest 26 - const esm = runtime.unstable_shouldLoadAsEsm?.(path); + const esm = runtime.unstable_shouldLoadAsEsm( + path, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(path); @@ -163,9 +165,10 @@ async function jasmine2( const testNameRegex = new RegExp(globalConfig.testNamePattern, 'i'); env.specFilter = (spec: Spec) => testNameRegex.test(spec.getFullName()); } - - // TODO: remove ? in Jest 26 - const esm = runtime.unstable_shouldLoadAsEsm?.(testPath); + const esm = runtime.unstable_shouldLoadAsEsm( + testPath, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(testPath); diff --git a/packages/jest-repl/src/cli/runtime-cli.ts b/packages/jest-repl/src/cli/runtime-cli.ts index 61fe494222e7..dc8557da20cf 100644 --- a/packages/jest-repl/src/cli/runtime-cli.ts +++ b/packages/jest-repl/src/cli/runtime-cli.ts @@ -97,8 +97,10 @@ export async function run( ); for (const path of config.setupFiles) { - // TODO: remove ? in Jest 26 - const esm = runtime.unstable_shouldLoadAsEsm?.(path); + const esm = runtime.unstable_shouldLoadAsEsm( + path, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(path); @@ -106,8 +108,10 @@ export async function run( runtime.requireModule(path); } } - // TODO: remove ? in Jest 26 - const esm = runtime.unstable_shouldLoadAsEsm?.(filePath); + const esm = runtime.unstable_shouldLoadAsEsm( + filePath, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(filePath); diff --git a/packages/jest-resolve/src/shouldLoadAsEsm.ts b/packages/jest-resolve/src/shouldLoadAsEsm.ts index 9af487405f7c..5f9759cca251 100644 --- a/packages/jest-resolve/src/shouldLoadAsEsm.ts +++ b/packages/jest-resolve/src/shouldLoadAsEsm.ts @@ -24,11 +24,18 @@ export function clearCachedLookups(): void { cachedChecks.clear(); } -export default function cachedShouldLoadAsEsm(path: Config.Path): boolean { +export default function cachedShouldLoadAsEsm( + path: Config.Path, + extensionsToTreatAsEsm: Array, +): boolean { + if (!runtimeSupportsVmModules) { + return false; + } + let cachedLookup = cachedFileLookups.get(path); if (cachedLookup === undefined) { - cachedLookup = shouldLoadAsEsm(path); + cachedLookup = shouldLoadAsEsm(path, extensionsToTreatAsEsm); cachedFileLookups.set(path, cachedLookup); } @@ -36,11 +43,10 @@ export default function cachedShouldLoadAsEsm(path: Config.Path): boolean { } // this is a bad version of what https://github.com/nodejs/modules/issues/393 would provide -function shouldLoadAsEsm(path: Config.Path): boolean { - if (!runtimeSupportsVmModules) { - return false; - } - +function shouldLoadAsEsm( + path: Config.Path, + extensionsToTreatAsEsm: Array, +): boolean { const extension = extname(path); if (extension === '.mjs') { @@ -51,10 +57,8 @@ function shouldLoadAsEsm(path: Config.Path): boolean { return false; } - // this isn't correct - we might wanna load any file as a module (using synthetic module) - // do we need an option to Jest so people can opt in to ESM for non-js? if (extension !== '.js') { - return false; + return extensionsToTreatAsEsm.includes(extension); } const cwd = dirname(path); diff --git a/packages/jest-runner/src/runTest.ts b/packages/jest-runner/src/runTest.ts index f5ff5fbedef7..6cd8778cf2cf 100644 --- a/packages/jest-runner/src/runTest.ts +++ b/packages/jest-runner/src/runTest.ts @@ -171,8 +171,10 @@ async function runTestInternal( const start = Date.now(); for (const path of config.setupFiles) { - // TODO: remove ? in Jest 26 - const esm = runtime.unstable_shouldLoadAsEsm?.(path); + const esm = runtime.unstable_shouldLoadAsEsm( + path, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(path); diff --git a/packages/jest-runtime/src/__mocks__/createRuntime.js b/packages/jest-runtime/src/__mocks__/createRuntime.js index 478359de5c2c..3e371b859671 100644 --- a/packages/jest-runtime/src/__mocks__/createRuntime.js +++ b/packages/jest-runtime/src/__mocks__/createRuntime.js @@ -116,7 +116,10 @@ module.exports = async function createRuntime(filename, config) { ); for (const path of config.setupFiles) { - const esm = runtime.unstable_shouldLoadAsEsm(path); + const esm = runtime.unstable_shouldLoadAsEsm( + path, + config.extensionsToTreatAsEsm, + ); if (esm) { await runtime.unstable_importModule(path); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index aa2ac41977cf..22181968d98d 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -459,7 +459,10 @@ export default class Runtime { if ( this._resolver.isCoreModule(resolved) || - this.unstable_shouldLoadAsEsm(resolved) + this.unstable_shouldLoadAsEsm( + resolved, + this._config.extensionsToTreatAsEsm, + ) ) { return this.loadEsmModule(resolved, query); } diff --git a/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.ts.snap b/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.ts.snap index d935cd744c3e..c9deddaf0a64 100644 --- a/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.ts.snap +++ b/packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.ts.snap @@ -21,6 +21,7 @@ exports[`ScriptTransformer passes expected transform options to getCacheKey 1`] "detectOpenHandles": false, "displayName": undefined, "errorOnDeprecated": false, + "extensionsToTreatAsEsm": Array [], "extraGlobals": Array [], "filter": undefined, "forceCoverageMatch": Array [], @@ -77,7 +78,7 @@ exports[`ScriptTransformer passes expected transform options to getCacheKey 1`] "unmockedModulePathPatterns": undefined, "watchPathIgnorePatterns": Array [], }, - "configString": "{\\"automock\\":false,\\"cache\\":true,\\"cacheDirectory\\":\\"/cache/\\",\\"clearMocks\\":false,\\"coveragePathIgnorePatterns\\":[],\\"cwd\\":\\"/test_root_dir/\\",\\"detectLeaks\\":false,\\"detectOpenHandles\\":false,\\"errorOnDeprecated\\":false,\\"extraGlobals\\":[],\\"forceCoverageMatch\\":[],\\"globals\\":{},\\"haste\\":{},\\"injectGlobals\\":true,\\"moduleDirectories\\":[],\\"moduleFileExtensions\\":[\\"js\\"],\\"moduleLoader\\":\\"/test_module_loader_path\\",\\"moduleNameMapper\\":[],\\"modulePathIgnorePatterns\\":[],\\"modulePaths\\":[],\\"name\\":\\"test\\",\\"prettierPath\\":\\"prettier\\",\\"resetMocks\\":false,\\"resetModules\\":false,\\"restoreMocks\\":false,\\"rootDir\\":\\"/\\",\\"roots\\":[],\\"runner\\":\\"jest-runner\\",\\"setupFiles\\":[],\\"setupFilesAfterEnv\\":[],\\"skipFilter\\":false,\\"skipNodeResolution\\":false,\\"slowTestThreshold\\":5,\\"snapshotSerializers\\":[],\\"testEnvironment\\":\\"node\\",\\"testEnvironmentOptions\\":{},\\"testLocationInResults\\":false,\\"testMatch\\":[],\\"testPathIgnorePatterns\\":[],\\"testRegex\\":[\\"\\\\\\\\.test\\\\\\\\.js$\\"],\\"testRunner\\":\\"jest-jasmine2\\",\\"testURL\\":\\"http://localhost\\",\\"timers\\":\\"real\\",\\"transform\\":[[\\"\\\\\\\\.js$\\",\\"test_preprocessor\\",{}]],\\"transformIgnorePatterns\\":[\\"/node_modules/\\"],\\"watchPathIgnorePatterns\\":[]}", + "configString": "{\\"automock\\":false,\\"cache\\":true,\\"cacheDirectory\\":\\"/cache/\\",\\"clearMocks\\":false,\\"coveragePathIgnorePatterns\\":[],\\"cwd\\":\\"/test_root_dir/\\",\\"detectLeaks\\":false,\\"detectOpenHandles\\":false,\\"errorOnDeprecated\\":false,\\"extensionsToTreatAsEsm\\":[],\\"extraGlobals\\":[],\\"forceCoverageMatch\\":[],\\"globals\\":{},\\"haste\\":{},\\"injectGlobals\\":true,\\"moduleDirectories\\":[],\\"moduleFileExtensions\\":[\\"js\\"],\\"moduleLoader\\":\\"/test_module_loader_path\\",\\"moduleNameMapper\\":[],\\"modulePathIgnorePatterns\\":[],\\"modulePaths\\":[],\\"name\\":\\"test\\",\\"prettierPath\\":\\"prettier\\",\\"resetMocks\\":false,\\"resetModules\\":false,\\"restoreMocks\\":false,\\"rootDir\\":\\"/\\",\\"roots\\":[],\\"runner\\":\\"jest-runner\\",\\"setupFiles\\":[],\\"setupFilesAfterEnv\\":[],\\"skipFilter\\":false,\\"skipNodeResolution\\":false,\\"slowTestThreshold\\":5,\\"snapshotSerializers\\":[],\\"testEnvironment\\":\\"node\\",\\"testEnvironmentOptions\\":{},\\"testLocationInResults\\":false,\\"testMatch\\":[],\\"testPathIgnorePatterns\\":[],\\"testRegex\\":[\\"\\\\\\\\.test\\\\\\\\.js$\\"],\\"testRunner\\":\\"jest-jasmine2\\",\\"testURL\\":\\"http://localhost\\",\\"timers\\":\\"real\\",\\"transform\\":[[\\"\\\\\\\\.js$\\",\\"test_preprocessor\\",{}]],\\"transformIgnorePatterns\\":[\\"/node_modules/\\"],\\"watchPathIgnorePatterns\\":[]}", "coverageProvider": "babel", "instrument": true, "supportsDynamicImport": false, @@ -252,7 +253,7 @@ exports[`ScriptTransformer uses multiple preprocessors 1`] = ` const TRANSFORMED = { filename: '/fruits/banana.js', script: 'module.exports = "banana";', - config: '{"collectCoverage":false,"collectCoverageFrom":[],"coverageProvider":"babel","supportsDynamicImport":false,"supportsExportNamespaceFrom":false,"supportsStaticESM":false,"supportsTopLevelAwait":false,"instrument":false,"config":{"automock":false,"cache":true,"cacheDirectory":"/cache/","clearMocks":false,"coveragePathIgnorePatterns":[],"cwd":"/test_root_dir/","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"forceCoverageMatch":[],"globals":{},"haste":{},"injectGlobals":true,"moduleDirectories":[],"moduleFileExtensions":["js"],"moduleLoader":"/test_module_loader_path","moduleNameMapper":[],"modulePathIgnorePatterns":[],"modulePaths":[],"name":"test","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/","roots":[],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"skipNodeResolution":false,"slowTestThreshold":5,"snapshotSerializers":[],"testEnvironment":"node","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":[],"testPathIgnorePatterns":[],"testRegex":["\\\\.test\\\\.js$"],"testRunner":"jest-jasmine2","testURL":"http://localhost","timers":"real","transform":[["\\\\.js$","test_preprocessor",{}],["\\\\.css$","css-preprocessor",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"configString":"{\\"automock\\":false,\\"cache\\":true,\\"cacheDirectory\\":\\"/cache/\\",\\"clearMocks\\":false,\\"coveragePathIgnorePatterns\\":[],\\"cwd\\":\\"/test_root_dir/\\",\\"detectLeaks\\":false,\\"detectOpenHandles\\":false,\\"errorOnDeprecated\\":false,\\"extraGlobals\\":[],\\"forceCoverageMatch\\":[],\\"globals\\":{},\\"haste\\":{},\\"injectGlobals\\":true,\\"moduleDirectories\\":[],\\"moduleFileExtensions\\":[\\"js\\"],\\"moduleLoader\\":\\"/test_module_loader_path\\",\\"moduleNameMapper\\":[],\\"modulePathIgnorePatterns\\":[],\\"modulePaths\\":[],\\"name\\":\\"test\\",\\"prettierPath\\":\\"prettier\\",\\"resetMocks\\":false,\\"resetModules\\":false,\\"restoreMocks\\":false,\\"rootDir\\":\\"/\\",\\"roots\\":[],\\"runner\\":\\"jest-runner\\",\\"setupFiles\\":[],\\"setupFilesAfterEnv\\":[],\\"skipFilter\\":false,\\"skipNodeResolution\\":false,\\"slowTestThreshold\\":5,\\"snapshotSerializers\\":[],\\"testEnvironment\\":\\"node\\",\\"testEnvironmentOptions\\":{},\\"testLocationInResults\\":false,\\"testMatch\\":[],\\"testPathIgnorePatterns\\":[],\\"testRegex\\":[\\"\\\\\\\\.test\\\\\\\\.js$\\"],\\"testRunner\\":\\"jest-jasmine2\\",\\"testURL\\":\\"http://localhost\\",\\"timers\\":\\"real\\",\\"transform\\":[[\\"\\\\\\\\.js$\\",\\"test_preprocessor\\",{}],[\\"\\\\\\\\.css$\\",\\"css-preprocessor\\",{}]],\\"transformIgnorePatterns\\":[\\"/node_modules/\\"],\\"watchPathIgnorePatterns\\":[]}"}', + config: '{"collectCoverage":false,"collectCoverageFrom":[],"coverageProvider":"babel","supportsDynamicImport":false,"supportsExportNamespaceFrom":false,"supportsStaticESM":false,"supportsTopLevelAwait":false,"instrument":false,"config":{"automock":false,"cache":true,"cacheDirectory":"/cache/","clearMocks":false,"coveragePathIgnorePatterns":[],"cwd":"/test_root_dir/","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extensionsToTreatAsEsm":[],"extraGlobals":[],"forceCoverageMatch":[],"globals":{},"haste":{},"injectGlobals":true,"moduleDirectories":[],"moduleFileExtensions":["js"],"moduleLoader":"/test_module_loader_path","moduleNameMapper":[],"modulePathIgnorePatterns":[],"modulePaths":[],"name":"test","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/","roots":[],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"skipNodeResolution":false,"slowTestThreshold":5,"snapshotSerializers":[],"testEnvironment":"node","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":[],"testPathIgnorePatterns":[],"testRegex":["\\\\.test\\\\.js$"],"testRunner":"jest-jasmine2","testURL":"http://localhost","timers":"real","transform":[["\\\\.js$","test_preprocessor",{}],["\\\\.css$","css-preprocessor",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"configString":"{\\"automock\\":false,\\"cache\\":true,\\"cacheDirectory\\":\\"/cache/\\",\\"clearMocks\\":false,\\"coveragePathIgnorePatterns\\":[],\\"cwd\\":\\"/test_root_dir/\\",\\"detectLeaks\\":false,\\"detectOpenHandles\\":false,\\"errorOnDeprecated\\":false,\\"extensionsToTreatAsEsm\\":[],\\"extraGlobals\\":[],\\"forceCoverageMatch\\":[],\\"globals\\":{},\\"haste\\":{},\\"injectGlobals\\":true,\\"moduleDirectories\\":[],\\"moduleFileExtensions\\":[\\"js\\"],\\"moduleLoader\\":\\"/test_module_loader_path\\",\\"moduleNameMapper\\":[],\\"modulePathIgnorePatterns\\":[],\\"modulePaths\\":[],\\"name\\":\\"test\\",\\"prettierPath\\":\\"prettier\\",\\"resetMocks\\":false,\\"resetModules\\":false,\\"restoreMocks\\":false,\\"rootDir\\":\\"/\\",\\"roots\\":[],\\"runner\\":\\"jest-runner\\",\\"setupFiles\\":[],\\"setupFilesAfterEnv\\":[],\\"skipFilter\\":false,\\"skipNodeResolution\\":false,\\"slowTestThreshold\\":5,\\"snapshotSerializers\\":[],\\"testEnvironment\\":\\"node\\",\\"testEnvironmentOptions\\":{},\\"testLocationInResults\\":false,\\"testMatch\\":[],\\"testPathIgnorePatterns\\":[],\\"testRegex\\":[\\"\\\\\\\\.test\\\\\\\\.js$\\"],\\"testRunner\\":\\"jest-jasmine2\\",\\"testURL\\":\\"http://localhost\\",\\"timers\\":\\"real\\",\\"transform\\":[[\\"\\\\\\\\.js$\\",\\"test_preprocessor\\",{}],[\\"\\\\\\\\.css$\\",\\"css-preprocessor\\",{}]],\\"transformIgnorePatterns\\":[\\"/node_modules/\\"],\\"watchPathIgnorePatterns\\":[]}"}', }; `; @@ -269,7 +270,7 @@ exports[`ScriptTransformer uses the supplied preprocessor 1`] = ` const TRANSFORMED = { filename: '/fruits/banana.js', script: 'module.exports = "banana";', - config: '{"collectCoverage":false,"collectCoverageFrom":[],"coverageProvider":"babel","supportsDynamicImport":false,"supportsExportNamespaceFrom":false,"supportsStaticESM":false,"supportsTopLevelAwait":false,"instrument":false,"config":{"automock":false,"cache":true,"cacheDirectory":"/cache/","clearMocks":false,"coveragePathIgnorePatterns":[],"cwd":"/test_root_dir/","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"forceCoverageMatch":[],"globals":{},"haste":{},"injectGlobals":true,"moduleDirectories":[],"moduleFileExtensions":["js"],"moduleLoader":"/test_module_loader_path","moduleNameMapper":[],"modulePathIgnorePatterns":[],"modulePaths":[],"name":"test","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/","roots":[],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"skipNodeResolution":false,"slowTestThreshold":5,"snapshotSerializers":[],"testEnvironment":"node","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":[],"testPathIgnorePatterns":[],"testRegex":["\\\\.test\\\\.js$"],"testRunner":"jest-jasmine2","testURL":"http://localhost","timers":"real","transform":[["\\\\.js$","test_preprocessor",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"configString":"{\\"automock\\":false,\\"cache\\":true,\\"cacheDirectory\\":\\"/cache/\\",\\"clearMocks\\":false,\\"coveragePathIgnorePatterns\\":[],\\"cwd\\":\\"/test_root_dir/\\",\\"detectLeaks\\":false,\\"detectOpenHandles\\":false,\\"errorOnDeprecated\\":false,\\"extraGlobals\\":[],\\"forceCoverageMatch\\":[],\\"globals\\":{},\\"haste\\":{},\\"injectGlobals\\":true,\\"moduleDirectories\\":[],\\"moduleFileExtensions\\":[\\"js\\"],\\"moduleLoader\\":\\"/test_module_loader_path\\",\\"moduleNameMapper\\":[],\\"modulePathIgnorePatterns\\":[],\\"modulePaths\\":[],\\"name\\":\\"test\\",\\"prettierPath\\":\\"prettier\\",\\"resetMocks\\":false,\\"resetModules\\":false,\\"restoreMocks\\":false,\\"rootDir\\":\\"/\\",\\"roots\\":[],\\"runner\\":\\"jest-runner\\",\\"setupFiles\\":[],\\"setupFilesAfterEnv\\":[],\\"skipFilter\\":false,\\"skipNodeResolution\\":false,\\"slowTestThreshold\\":5,\\"snapshotSerializers\\":[],\\"testEnvironment\\":\\"node\\",\\"testEnvironmentOptions\\":{},\\"testLocationInResults\\":false,\\"testMatch\\":[],\\"testPathIgnorePatterns\\":[],\\"testRegex\\":[\\"\\\\\\\\.test\\\\\\\\.js$\\"],\\"testRunner\\":\\"jest-jasmine2\\",\\"testURL\\":\\"http://localhost\\",\\"timers\\":\\"real\\",\\"transform\\":[[\\"\\\\\\\\.js$\\",\\"test_preprocessor\\",{}]],\\"transformIgnorePatterns\\":[\\"/node_modules/\\"],\\"watchPathIgnorePatterns\\":[]}"}', + config: '{"collectCoverage":false,"collectCoverageFrom":[],"coverageProvider":"babel","supportsDynamicImport":false,"supportsExportNamespaceFrom":false,"supportsStaticESM":false,"supportsTopLevelAwait":false,"instrument":false,"config":{"automock":false,"cache":true,"cacheDirectory":"/cache/","clearMocks":false,"coveragePathIgnorePatterns":[],"cwd":"/test_root_dir/","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extensionsToTreatAsEsm":[],"extraGlobals":[],"forceCoverageMatch":[],"globals":{},"haste":{},"injectGlobals":true,"moduleDirectories":[],"moduleFileExtensions":["js"],"moduleLoader":"/test_module_loader_path","moduleNameMapper":[],"modulePathIgnorePatterns":[],"modulePaths":[],"name":"test","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/","roots":[],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"skipNodeResolution":false,"slowTestThreshold":5,"snapshotSerializers":[],"testEnvironment":"node","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":[],"testPathIgnorePatterns":[],"testRegex":["\\\\.test\\\\.js$"],"testRunner":"jest-jasmine2","testURL":"http://localhost","timers":"real","transform":[["\\\\.js$","test_preprocessor",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"configString":"{\\"automock\\":false,\\"cache\\":true,\\"cacheDirectory\\":\\"/cache/\\",\\"clearMocks\\":false,\\"coveragePathIgnorePatterns\\":[],\\"cwd\\":\\"/test_root_dir/\\",\\"detectLeaks\\":false,\\"detectOpenHandles\\":false,\\"errorOnDeprecated\\":false,\\"extensionsToTreatAsEsm\\":[],\\"extraGlobals\\":[],\\"forceCoverageMatch\\":[],\\"globals\\":{},\\"haste\\":{},\\"injectGlobals\\":true,\\"moduleDirectories\\":[],\\"moduleFileExtensions\\":[\\"js\\"],\\"moduleLoader\\":\\"/test_module_loader_path\\",\\"moduleNameMapper\\":[],\\"modulePathIgnorePatterns\\":[],\\"modulePaths\\":[],\\"name\\":\\"test\\",\\"prettierPath\\":\\"prettier\\",\\"resetMocks\\":false,\\"resetModules\\":false,\\"restoreMocks\\":false,\\"rootDir\\":\\"/\\",\\"roots\\":[],\\"runner\\":\\"jest-runner\\",\\"setupFiles\\":[],\\"setupFilesAfterEnv\\":[],\\"skipFilter\\":false,\\"skipNodeResolution\\":false,\\"slowTestThreshold\\":5,\\"snapshotSerializers\\":[],\\"testEnvironment\\":\\"node\\",\\"testEnvironmentOptions\\":{},\\"testLocationInResults\\":false,\\"testMatch\\":[],\\"testPathIgnorePatterns\\":[],\\"testRegex\\":[\\"\\\\\\\\.test\\\\\\\\.js$\\"],\\"testRunner\\":\\"jest-jasmine2\\",\\"testURL\\":\\"http://localhost\\",\\"timers\\":\\"real\\",\\"transform\\":[[\\"\\\\\\\\.js$\\",\\"test_preprocessor\\",{}]],\\"transformIgnorePatterns\\":[\\"/node_modules/\\"],\\"watchPathIgnorePatterns\\":[]}"}', }; `; diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index bc8c925777bd..e0bc7f5fadab 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -64,6 +64,7 @@ export type DefaultOptions = { coverageProvider: CoverageProvider; errorOnDeprecated: boolean; expand: boolean; + extensionsToTreatAsEsm: Array; forceCoverageMatch: Array; globals: ConfigGlobals; haste: HasteConfig; @@ -142,6 +143,7 @@ export type InitialOptions = Partial<{ detectOpenHandles: boolean; displayName: string | DisplayName; expand: boolean; + extensionsToTreatAsEsm: Array; extraGlobals: Array; filter: Path; findRelatedTests: boolean; @@ -327,6 +329,7 @@ export type ProjectConfig = { detectOpenHandles: boolean; displayName?: DisplayName; errorOnDeprecated: boolean; + extensionsToTreatAsEsm: Array; extraGlobals: Array; filter?: Path; forceCoverageMatch: Array; diff --git a/packages/test-utils/src/config.ts b/packages/test-utils/src/config.ts index f81132fa2215..b72300b00d19 100644 --- a/packages/test-utils/src/config.ts +++ b/packages/test-utils/src/config.ts @@ -75,6 +75,7 @@ const DEFAULT_PROJECT_CONFIG: Config.ProjectConfig = { detectOpenHandles: false, displayName: undefined, errorOnDeprecated: false, + extensionsToTreatAsEsm: [], extraGlobals: [], filter: undefined, forceCoverageMatch: [], diff --git a/yarn.lock b/yarn.lock index f06703de94a8..da4031cc292b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11763,10 +11763,12 @@ fsevents@~2.1.2: jest-jasmine2: ^26.6.3 jest-regex-util: ^26.0.0 jest-resolve: ^26.6.2 + jest-snapshot-serializer-raw: ^1.1.0 jest-util: ^26.6.2 jest-validate: ^26.6.2 micromatch: ^4.0.2 pretty-format: ^26.6.2 + strip-ansi: ^6.0.0 ts-node: ^9.0.0 typescript: ^4.0.3 peerDependencies: