From ecbdcf943ceb9b44c526cf9e5d0c4e535c3979e2 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 24 Aug 2025 22:22:21 +0700 Subject: [PATCH 01/15] feat(jest-config): supports Jest config file with .mts extension Signed-off-by: hainenber --- .../jest.config.mts.test.ts.snap | 66 +++++++ e2e/__tests__/jest.config.mts.test.ts | 186 ++++++++++++++++++ packages/jest-config/package.json | 2 +- packages/jest-config/src/constants.ts | 2 + .../src/readConfigFileAndSetRootDir.ts | 25 ++- 5 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap create mode 100644 e2e/__tests__/jest.config.mts.test.ts diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap new file mode 100644 index 000000000000..3dcaeebf281d --- /dev/null +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`on node <20.19.0 does not work with jest.config.mts when require(esm) is not supported 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + Current Node version <> does not support loading .mts Jest config. + Please upgrade to the range of ^20.19.0 || >=22.12.0" +`; + +exports[`on node >=24 invalid JS in jest.config.mts (node with native TS support) 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + both with the native node TypeScript support and configured TypeScript loaders. + Errors were: + - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' + - TSError: ⨯ Unable to compile TypeScript: +jest.config.mts(1,16): error TS2304: Cannot find name 'i'. +jest.config.mts(1,17): error TS1005: ';' expected. +jest.config.mts(1,39): error TS1002: Unterminated string literal." +`; + +exports[`on node ^20.19.0 || >=22.12.0 traverses directory tree up until it finds jest.config 1`] = ` +" console.log +<>/jest-config-ts/some/nested/directory + + at Object. (__tests__/a-giraffe.js:3:27) +" +`; + +exports[`on node ^20.19.0 || >=22.12.0 traverses directory tree up until it finds jest.config 2`] = ` +"PASS ../../../__tests__/a-giraffe.js + ✓ giraffe + ✓ abc" +`; + +exports[`on node ^20.19.0 || >=22.12.0 traverses directory tree up until it finds jest.config 3`] = ` +"Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; + +exports[`on node ^20.19.0 || >=22.12.0 works with jest.config.mts 1`] = ` +"PASS __tests__/a-giraffe.js + ✓ giraffe" +`; + +exports[`on node ^20.19.0 || >=22.12.0 works with jest.config.mts 2`] = ` +"Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; + +exports[`on node ^20.19.0 || >=22.12.0 works with tsconfig.json 1`] = ` +"PASS __tests__/a-giraffe.js + ✓ giraffe" +`; + +exports[`on node ^20.19.0 || >=22.12.0 works with tsconfig.json 2`] = ` +"Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts new file mode 100644 index 000000000000..19667d772c21 --- /dev/null +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -0,0 +1,186 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import {onNodeVersions} from '@jest/test-utils'; +import {cleanup, extractSummary, writeFiles} from '../Utils'; +import runJest from '../runJest'; + +const DIR = path.resolve(__dirname, '../jest-config-ts'); + +beforeEach(() => cleanup(DIR)); +afterAll(() => cleanup(DIR)); + +onNodeVersions('^20.19.0 || >=22.12.0', () => { + test('works with jest.config.mts', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': + "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", + 'package.json': '{"type": "commonjs"}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + const {rest, summary} = extractSummary(stderr); + expect(exitCode).toBe(0); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); + }); + test('works with tsconfig.json', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': + "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", + 'package.json': '{}', + 'tsconfig.json': '{ "compilerOptions": { "module": "esnext" } }', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + const {rest, summary} = extractSummary(stderr); + expect(exitCode).toBe(0); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); + }); + + test('traverses directory tree up until it finds jest.config', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': ` + const slash = require('slash'); + test('giraffe', () => expect(1).toBe(1)); + test('abc', () => console.log(slash(process.cwd()))); + `, + 'jest.config.mts': + "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", + 'package.json': '{}', + 'some/nested/directory/file.js': '// nothing special', + }); + + const {stderr, exitCode, stdout} = runJest( + path.join(DIR, 'some', 'nested', 'directory'), + ['-w=1', '--ci=false'], + {nodeOptions: '--no-warnings', skipPkgJsonCheck: true}, + ); + + // Snapshot the console.logged `process.cwd()` and make sure it stays the same + expect( + stdout + .replaceAll(/^\W+(.*)e2e/gm, '<>') + // slightly different log in node versions >= 23 + .replace('at Object.log', 'at Object.'), + ).toMatchSnapshot(); + + const {rest, summary} = extractSummary(stderr); + expect(exitCode).toBe(0); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); + }); +}) + +onNodeVersions('<20.19.0', () => { + test('does not work with jest.config.mts when require(esm) is not supported', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': + "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + expect( + stderr + // Remove the stack trace from the error message + .slice(0, Math.max(0, stderr.indexOf('at readConfigFileAndSetRootDir'))) + .trim() + // Replace the path to the config file with a placeholder + .replace( + /(Error: Jest: Failed to parse the TypeScript config file).*$/m, + '$1 <>', + ) + // Replace Node version with + .replace( + /(Current Node version) (.+?) /m, + '$1 <> ' + ), + ).toMatchSnapshot(); + expect(exitCode).toBe(1); + }); +}) + + +onNodeVersions('<23.6', () => { + test('invalid JS in jest.config.mts', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': "export default i'll break this file yo", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']); + expect(stderr).toMatch('SyntaxError: Invalid or unexpected token'); + expect(exitCode).toBe(1); + }); +}); + +onNodeVersions('^23.6', () => { + test('invalid JS in jest.config.mts (node with native TS support)', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': "export default i'll break this file yo", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + expect( + stderr + // Remove the stack trace from the error message + .slice(0, Math.max(0, stderr.indexOf('at readConfigFileAndSetRootDir'))) + .trim() + // Replace the path to the config file with a placeholder + .replace( + /(Error: Jest: Failed to parse the TypeScript config file).*$/m, + '$1 <>', + ), + ).toMatchSnapshot(); + expect(exitCode).toBe(1); + }); +}); + +onNodeVersions('>=24', () => { + // todo fixme + // eslint-disable-next-line jest/no-identical-title + test('invalid JS in jest.config.mts (node with native TS support)', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': "export default i'll break this file yo", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + expect( + stderr + // Remove the stack trace from the error message + .slice(0, Math.max(0, stderr.indexOf('at readConfigFileAndSetRootDir'))) + .trim() + // Replace the path to the config file with a placeholder + .replace( + /(Error: Jest: Failed to parse the TypeScript config file).*$/m, + '$1 <>', + ), + ).toMatchSnapshot(); + expect(exitCode).toBe(1); + }); +}); diff --git a/packages/jest-config/package.json b/packages/jest-config/package.json index c06dce165be5..1f356855e41e 100644 --- a/packages/jest-config/package.json +++ b/packages/jest-config/package.json @@ -57,6 +57,7 @@ "micromatch": "^4.0.8", "parse-json": "^5.2.0", "pretty-format": "workspace:*", + "semver": "^7.7.2", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -66,7 +67,6 @@ "@types/parse-json": "^4.0.2", "esbuild": "^0.25.5", "esbuild-register": "^3.6.0", - "semver": "^7.7.2", "ts-node": "^10.5.0", "typescript": "^5.0.4" }, diff --git a/packages/jest-config/src/constants.ts b/packages/jest-config/src/constants.ts index 63172c139250..13ceb1128ae2 100644 --- a/packages/jest-config/src/constants.ts +++ b/packages/jest-config/src/constants.ts @@ -13,6 +13,7 @@ export const PACKAGE_JSON = 'package.json'; export const JEST_CONFIG_BASE_NAME = 'jest.config'; export const JEST_CONFIG_EXT_CJS = '.cjs'; export const JEST_CONFIG_EXT_MJS = '.mjs'; +export const JEST_CONFIG_EXT_MTS = '.mts'; export const JEST_CONFIG_EXT_JS = '.js'; export const JEST_CONFIG_EXT_TS = '.ts'; export const JEST_CONFIG_EXT_CTS = '.cts'; @@ -21,6 +22,7 @@ export const JEST_CONFIG_EXT_ORDER = Object.freeze([ JEST_CONFIG_EXT_JS, JEST_CONFIG_EXT_TS, JEST_CONFIG_EXT_MJS, + JEST_CONFIG_EXT_MTS, JEST_CONFIG_EXT_CJS, JEST_CONFIG_EXT_CTS, JEST_CONFIG_EXT_JSON, diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index f71d1af715ba..094df591fadb 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -16,9 +16,11 @@ import {interopRequireDefault, requireOrImportModule} from 'jest-util'; import { JEST_CONFIG_EXT_CTS, JEST_CONFIG_EXT_JSON, + JEST_CONFIG_EXT_MTS, JEST_CONFIG_EXT_TS, PACKAGE_JSON, } from './constants'; +import {satisfies} from 'semver'; interface TsLoader { enabled: (bool: boolean) => void; @@ -32,9 +34,12 @@ type TsLoaderModule = 'ts-node' | 'esbuild-register'; export default async function readConfigFileAndSetRootDir( configPath: string, ): Promise { + const isMTS = configPath.endsWith(JEST_CONFIG_EXT_MTS); const isTS = configPath.endsWith(JEST_CONFIG_EXT_TS) || - configPath.endsWith(JEST_CONFIG_EXT_CTS); + configPath.endsWith(JEST_CONFIG_EXT_CTS) || + configPath.endsWith(JEST_CONFIG_EXT_CTS) || + isMTS; const isJSON = configPath.endsWith(JEST_CONFIG_EXT_JSON); let configObject; @@ -68,7 +73,23 @@ export default async function readConfigFileAndSetRootDir( } } } else { - configObject = await loadTSConfigFile(configPath); + if (isMTS) { + // TODO: remove this once dropping Node 20/22 support. + const mtsSupportVersionRange = '^20.19.0 || >=22.12.0'; + if (!satisfies(process.versions.node, mtsSupportVersionRange)) { + // Likely Node version not yet supports require(esm) + // This string is caught further down and merged into a new error message. + // eslint-disable-next-line no-throw-literal + throw ( + ` Current Node version ${process.versions.node} does not support loading .mts Jest config.\n` + + ` Please upgrade to the range of ${mtsSupportVersionRange}` + ); + } + // Relies on import(.mts) before falling back to require(.mts) + configObject = await requireOrImportModule(configPath); + } else { + configObject = await loadTSConfigFile(configPath); + } } } else if (isJSON) { const fileContent = fs.readFileSync(configPath, 'utf8'); From 73135e86ac992dce9a268e885581949f0c53f827 Mon Sep 17 00:00:00 2001 From: hainenber Date: Tue, 26 Aug 2025 22:22:24 +0700 Subject: [PATCH 02/15] feat(jest-config): supports untyped`.mts` config file Signed-off-by: hainenber --- .../jest.config.mts.test.ts.snap | 70 +++++++- e2e/__tests__/jest.config.mts.test.ts | 164 ++++++++++++------ .../src/readConfigFileAndSetRootDir.ts | 22 ++- 3 files changed, 193 insertions(+), 63 deletions(-) diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index 3dcaeebf281d..a1ba676ee52a 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -3,7 +3,7 @@ exports[`on node <20.19.0 does not work with jest.config.mts when require(esm) is not supported 1`] = ` "Error: Jest: Failed to parse the TypeScript config file <> Current Node version <> does not support loading .mts Jest config. - Please upgrade to the range of ^20.19.0 || >=22.12.0" + Please upgrade to ^20.19.0 || >=22.12.0" `; exports[`on node >=24 invalid JS in jest.config.mts (node with native TS support) 1`] = ` @@ -17,7 +17,48 @@ jest.config.mts(1,17): error TS1005: ';' expected. jest.config.mts(1,39): error TS1002: Unterminated string literal." `; -exports[`on node ^20.19.0 || >=22.12.0 traverses directory tree up until it finds jest.config 1`] = ` +exports[`on node ^20.19.0 || >=22.12.0 <23.6.0 does not work with typed jest.config.ts 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + Current Node version <> does not support loading typed .mts Jest config. + Please upgrade to ^23.6 + Error: SyntaxError: Missing initializer in const declaration" +`; + +exports[`on node ^20.19.0 || >=22.12.0 <23.6.0 work with untyped jest.config.mts 1`] = ` +"PASS __tests__/a-giraffe.js + ✓ giraffe" +`; + +exports[`on node ^20.19.0 || >=22.12.0 <23.6.0 work with untyped jest.config.mts 2`] = ` +"Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; + +exports[`on node ^23.6 invalid JS in jest.config.mts (node with native TS support) 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + both with the native node TypeScript support and configured TypeScript loaders. + Errors were: + - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: x Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' + ,---- + 1 | export default i'll break this file yo + : ^^^^^^^^^^^^^^^^^^^^^^ + \`---- + x Unterminated string constant + ,---- + 1 | export default i'll break this file yo + : ^^^^^^^^^^^^^^^^^^^^^^ + \`---- + + - TSError: ⨯ Unable to compile TypeScript: +jest.config.mts(1,16): error TS2304: Cannot find name 'i'. +jest.config.mts(1,17): error TS1005: ';' expected. +jest.config.mts(1,39): error TS1002: Unterminated string literal." +`; + +exports[`on node ^23.6 traverses directory tree up until it finds jest.config 1`] = ` " console.log <>/jest-config-ts/some/nested/directory @@ -25,13 +66,13 @@ exports[`on node ^20.19.0 || >=22.12.0 traverses directory tree up until it find " `; -exports[`on node ^20.19.0 || >=22.12.0 traverses directory tree up until it finds jest.config 2`] = ` +exports[`on node ^23.6 traverses directory tree up until it finds jest.config 2`] = ` "PASS ../../../__tests__/a-giraffe.js ✓ giraffe ✓ abc" `; -exports[`on node ^20.19.0 || >=22.12.0 traverses directory tree up until it finds jest.config 3`] = ` +exports[`on node ^23.6 traverses directory tree up until it finds jest.config 3`] = ` "Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total @@ -39,12 +80,25 @@ Time: <> Ran all test suites." `; -exports[`on node ^20.19.0 || >=22.12.0 works with jest.config.mts 1`] = ` +exports[`on node ^23.6 work with untyped jest.config.mts 1`] = ` +"PASS __tests__/a-giraffe.js + ✓ giraffe" +`; + +exports[`on node ^23.6 work with untyped jest.config.mts 2`] = ` +"Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; + +exports[`on node ^23.6 works with tsconfig.json 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node ^20.19.0 || >=22.12.0 works with jest.config.mts 2`] = ` +exports[`on node ^23.6 works with tsconfig.json 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total @@ -52,12 +106,12 @@ Time: <> Ran all test suites." `; -exports[`on node ^20.19.0 || >=22.12.0 works with tsconfig.json 1`] = ` +exports[`on node ^23.6 works with typed jest.config.mts 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node ^20.19.0 || >=22.12.0 works with tsconfig.json 2`] = ` +exports[`on node ^23.6 works with typed jest.config.mts 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts index 19667d772c21..52e724bc0c48 100644 --- a/e2e/__tests__/jest.config.mts.test.ts +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -15,12 +15,122 @@ const DIR = path.resolve(__dirname, '../jest-config-ts'); beforeEach(() => cleanup(DIR)); afterAll(() => cleanup(DIR)); -onNodeVersions('^20.19.0 || >=22.12.0', () => { - test('works with jest.config.mts', () => { +onNodeVersions('<20.19.0', () => { + test('does not work with jest.config.mts when require(esm) is not supported', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", 'jest.config.mts': "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + expect( + stderr + // Remove the stack trace from the error message + .slice(0, Math.max(0, stderr.indexOf('at readConfigFileAndSetRootDir'))) + .trim() + // Replace the path to the config file with a placeholder + .replace( + /(Error: Jest: Failed to parse the TypeScript config file).*$/m, + '$1 <>', + ) + // Replace Node version with + .replace(/(Current Node version) (.+?) /m, '$1 <> '), + ).toMatchSnapshot(); + expect(exitCode).toBe(1); + }); +}); + +onNodeVersions('^20.19.0 || >=22.12.0 <23.6.0', () => { + test('work with untyped jest.config.mts', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': + "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + const {rest, summary} = extractSummary(stderr); + expect(exitCode).toBe(0); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); + }); + + test('does not work with typed jest.config.ts', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': ` + import {Config} from 'jest'; + const config: Config = {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js' }; + export default config; + `, + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + expect( + stderr + // Remove the stack trace from the error message + .slice(0, Math.max(0, stderr.indexOf('at readConfigFileAndSetRootDir'))) + .trim() + // Replace the path to the config file with a placeholder + .replace( + /(Error: Jest: Failed to parse the TypeScript config file).*$/m, + '$1 <>', + ) + // Replace Node version with + .replace(/(Current Node version) (.+?) /m, '$1 <> '), + ).toMatchSnapshot(); + expect(exitCode).toBe(1); + }); + + test('invalid JS in jest.config.mts', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': "export default i'll break this file yo", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']); + expect(stderr).toMatch('SyntaxError: Invalid or unexpected token'); + expect(exitCode).toBe(1); + }); +}); + +onNodeVersions('^23.6', () => { + test('work with untyped jest.config.mts', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': + "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + const {rest, summary} = extractSummary(stderr); + expect(exitCode).toBe(0); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); + }); + + test('works with typed jest.config.mts', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': ` + import {Config} from 'jest'; + const config: Config = {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js' }; + export default config; + `, 'package.json': '{"type": "commonjs"}', }); @@ -32,6 +142,7 @@ onNodeVersions('^20.19.0 || >=22.12.0', () => { expect(rest).toMatchSnapshot(); expect(summary).toMatchSnapshot(); }); + test('works with tsconfig.json', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", @@ -82,56 +193,7 @@ onNodeVersions('^20.19.0 || >=22.12.0', () => { expect(rest).toMatchSnapshot(); expect(summary).toMatchSnapshot(); }); -}) - -onNodeVersions('<20.19.0', () => { - test('does not work with jest.config.mts when require(esm) is not supported', () => { - writeFiles(DIR, { - '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", - 'jest.config.mts': - "export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};", - 'package.json': '{}', - }); - const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { - nodeOptions: '--no-warnings', - }); - expect( - stderr - // Remove the stack trace from the error message - .slice(0, Math.max(0, stderr.indexOf('at readConfigFileAndSetRootDir'))) - .trim() - // Replace the path to the config file with a placeholder - .replace( - /(Error: Jest: Failed to parse the TypeScript config file).*$/m, - '$1 <>', - ) - // Replace Node version with - .replace( - /(Current Node version) (.+?) /m, - '$1 <> ' - ), - ).toMatchSnapshot(); - expect(exitCode).toBe(1); - }); -}) - - -onNodeVersions('<23.6', () => { - test('invalid JS in jest.config.mts', () => { - writeFiles(DIR, { - '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", - 'jest.config.mts': "export default i'll break this file yo", - 'package.json': '{}', - }); - - const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']); - expect(stderr).toMatch('SyntaxError: Invalid or unexpected token'); - expect(exitCode).toBe(1); - }); -}); - -onNodeVersions('^23.6', () => { test('invalid JS in jest.config.mts (node with native TS support)', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index 094df591fadb..8c921b489c0f 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -75,18 +75,32 @@ export default async function readConfigFileAndSetRootDir( } else { if (isMTS) { // TODO: remove this once dropping Node 20/22 support. - const mtsSupportVersionRange = '^20.19.0 || >=22.12.0'; - if (!satisfies(process.versions.node, mtsSupportVersionRange)) { + const mtsExtSupportVersionRange = '^20.19.0 || >=22.12.0'; + if (!satisfies(process.versions.node, mtsExtSupportVersionRange)) { // Likely Node version not yet supports require(esm) // This string is caught further down and merged into a new error message. // eslint-disable-next-line no-throw-literal throw ( ` Current Node version ${process.versions.node} does not support loading .mts Jest config.\n` + - ` Please upgrade to the range of ${mtsSupportVersionRange}` + ` Please upgrade to ${mtsExtSupportVersionRange}` ); } // Relies on import(.mts) before falling back to require(.mts) - configObject = await requireOrImportModule(configPath); + try { + configObject = await requireOrImportModule(configPath); + } catch (requireOrImportModuleError) { + if (!(requireOrImportModuleError instanceof SyntaxError)) { + throw requireOrImportModuleError; + } + // Likely Node version does not support type stripping when require(esm). + // This string is caught further down and merged into a new error message. + // eslint-disable-next-line no-throw-literal + throw ( + ` Current Node version ${process.versions.node} does not support loading typed .mts Jest config.\n` + + ' Please upgrade to ^23.6\n' + + ` Error: ${requireOrImportModuleError}\n` + ); + } } else { configObject = await loadTSConfigFile(configPath); } From 463729673098bc475a347caaac846ca3a848ae29 Mon Sep 17 00:00:00 2001 From: hainenber Date: Tue, 26 Aug 2025 22:26:56 +0700 Subject: [PATCH 03/15] chore: add CHANGELOG entry Signed-off-by: hainenber --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0179da15bd8..3246a02aad41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## main +### Features + +- `[jest-config]` Supports Jest config file with `.mts` extension ([#15796](https://github.com/jestjs/jest/pull/15796)) + ## 30.0.5 ### Features From e789de51cab5261e5774e57f0ad7da3aa7facc9d Mon Sep 17 00:00:00 2001 From: Christoph Nakazawa Date: Wed, 27 Aug 2025 12:56:03 +0900 Subject: [PATCH 04/15] Update readConfigFileAndSetRootDir.ts --- packages/jest-config/src/readConfigFileAndSetRootDir.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index 8c921b489c0f..5e5a02670102 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -38,7 +38,6 @@ export default async function readConfigFileAndSetRootDir( const isTS = configPath.endsWith(JEST_CONFIG_EXT_TS) || configPath.endsWith(JEST_CONFIG_EXT_CTS) || - configPath.endsWith(JEST_CONFIG_EXT_CTS) || isMTS; const isJSON = configPath.endsWith(JEST_CONFIG_EXT_JSON); let configObject; From dcad33b654e92704b00f39ff289389cdccee6d4c Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 27 Sep 2025 15:55:43 +0700 Subject: [PATCH 05/15] feat(cfg): enhance error message to inform user of default type stripping in Node >=22.18.0 Signed-off-by: hainenber --- .../jest.config.mts.test.ts.snap | 82 +++++++++---------- e2e/__tests__/jest.config.mts.test.ts | 4 +- .../src/readConfigFileAndSetRootDir.ts | 2 +- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index a1ba676ee52a..9ba2aa96aab7 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -6,38 +6,7 @@ exports[`on node <20.19.0 does not work with jest.config.mts when require(esm) i Please upgrade to ^20.19.0 || >=22.12.0" `; -exports[`on node >=24 invalid JS in jest.config.mts (node with native TS support) 1`] = ` -"Error: Jest: Failed to parse the TypeScript config file <> - both with the native node TypeScript support and configured TypeScript loaders. - Errors were: - - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' - - TSError: ⨯ Unable to compile TypeScript: -jest.config.mts(1,16): error TS2304: Cannot find name 'i'. -jest.config.mts(1,17): error TS1005: ';' expected. -jest.config.mts(1,39): error TS1002: Unterminated string literal." -`; - -exports[`on node ^20.19.0 || >=22.12.0 <23.6.0 does not work with typed jest.config.ts 1`] = ` -"Error: Jest: Failed to parse the TypeScript config file <> - Current Node version <> does not support loading typed .mts Jest config. - Please upgrade to ^23.6 - Error: SyntaxError: Missing initializer in const declaration" -`; - -exports[`on node ^20.19.0 || >=22.12.0 <23.6.0 work with untyped jest.config.mts 1`] = ` -"PASS __tests__/a-giraffe.js - ✓ giraffe" -`; - -exports[`on node ^20.19.0 || >=22.12.0 <23.6.0 work with untyped jest.config.mts 2`] = ` -"Test Suites: 1 passed, 1 total -Tests: 1 passed, 1 total -Snapshots: 0 total -Time: <> -Ran all test suites." -`; - -exports[`on node ^23.6 invalid JS in jest.config.mts (node with native TS support) 1`] = ` +exports[`on node >=22.18.0 || ^23.6 invalid JS in jest.config.mts (node with native TS support) 1`] = ` "Error: Jest: Failed to parse the TypeScript config file <> both with the native node TypeScript support and configured TypeScript loaders. Errors were: @@ -58,7 +27,7 @@ jest.config.mts(1,17): error TS1005: ';' expected. jest.config.mts(1,39): error TS1002: Unterminated string literal." `; -exports[`on node ^23.6 traverses directory tree up until it finds jest.config 1`] = ` +exports[`on node >=22.18.0 || ^23.6 traverses directory tree up until it finds jest.config 1`] = ` " console.log <>/jest-config-ts/some/nested/directory @@ -66,13 +35,13 @@ exports[`on node ^23.6 traverses directory tree up until it finds jest.config 1` " `; -exports[`on node ^23.6 traverses directory tree up until it finds jest.config 2`] = ` +exports[`on node >=22.18.0 || ^23.6 traverses directory tree up until it finds jest.config 2`] = ` "PASS ../../../__tests__/a-giraffe.js ✓ giraffe ✓ abc" `; -exports[`on node ^23.6 traverses directory tree up until it finds jest.config 3`] = ` +exports[`on node >=22.18.0 || ^23.6 traverses directory tree up until it finds jest.config 3`] = ` "Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total @@ -80,12 +49,12 @@ Time: <> Ran all test suites." `; -exports[`on node ^23.6 work with untyped jest.config.mts 1`] = ` +exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node ^23.6 work with untyped jest.config.mts 2`] = ` +exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total @@ -93,12 +62,12 @@ Time: <> Ran all test suites." `; -exports[`on node ^23.6 works with tsconfig.json 1`] = ` +exports[`on node >=22.18.0 || ^23.6 works with tsconfig.json 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node ^23.6 works with tsconfig.json 2`] = ` +exports[`on node >=22.18.0 || ^23.6 works with tsconfig.json 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total @@ -106,12 +75,43 @@ Time: <> Ran all test suites." `; -exports[`on node ^23.6 works with typed jest.config.mts 1`] = ` +exports[`on node >=22.18.0 || ^23.6 works with typed jest.config.mts 1`] = ` +"PASS __tests__/a-giraffe.js + ✓ giraffe" +`; + +exports[`on node >=22.18.0 || ^23.6 works with typed jest.config.mts 2`] = ` +"Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; + +exports[`on node >=24 invalid JS in jest.config.mts (node with native TS support) 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + both with the native node TypeScript support and configured TypeScript loaders. + Errors were: + - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' + - TSError: ⨯ Unable to compile TypeScript: +jest.config.mts(1,16): error TS2304: Cannot find name 'i'. +jest.config.mts(1,17): error TS1005: ';' expected. +jest.config.mts(1,39): error TS1002: Unterminated string literal." +`; + +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 does not work with typed jest.config.ts 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + Current Node version <> does not support loading typed .mts Jest config. + Please upgrade to >=22.18.0 || ^23.6 + Error: SyntaxError: Missing initializer in const declaration" +`; + +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node ^23.6 works with typed jest.config.mts 2`] = ` +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts index 52e724bc0c48..d7a02e738c92 100644 --- a/e2e/__tests__/jest.config.mts.test.ts +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -44,7 +44,7 @@ onNodeVersions('<20.19.0', () => { }); }); -onNodeVersions('^20.19.0 || >=22.12.0 <23.6.0', () => { +onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0', () => { test('work with untyped jest.config.mts', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", @@ -105,7 +105,7 @@ onNodeVersions('^20.19.0 || >=22.12.0 <23.6.0', () => { }); }); -onNodeVersions('^23.6', () => { +onNodeVersions('>=22.18.0 || ^23.6', () => { test('work with untyped jest.config.mts', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index 5e5a02670102..466528e02710 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -96,7 +96,7 @@ export default async function readConfigFileAndSetRootDir( // eslint-disable-next-line no-throw-literal throw ( ` Current Node version ${process.versions.node} does not support loading typed .mts Jest config.\n` + - ' Please upgrade to ^23.6\n' + + ' Please upgrade to >=22.18.0 || ^23.6\n' + ` Error: ${requireOrImportModuleError}\n` ); } From 0b2429591cfd6c983fdc451fdac6bba2d67f3d22 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 11 Oct 2025 12:18:16 +0700 Subject: [PATCH 06/15] chore: address failed tests with updated snapshot + expand case for jest.config.mts for `create-jest` Signed-off-by: hainenber --- .../__snapshots__/jest.config.mts.test.ts.snap | 12 +----------- e2e/__tests__/jest.config.mts.test.ts | 4 ++-- .../has-jest-config-file-mts/jest.config.mts | 8 ++++++++ .../has-jest-config-file-mts/package.json | 1 + .../src/__tests__/__snapshots__/init.test.ts.snap | 9 +++++++++ .../jest-config/src/readConfigFileAndSetRootDir.ts | 2 +- 6 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/jest.config.mts create mode 100644 packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/package.json diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index 9ba2aa96aab7..b68dda560c71 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -10,17 +10,7 @@ exports[`on node >=22.18.0 || ^23.6 invalid JS in jest.config.mts (node with nat "Error: Jest: Failed to parse the TypeScript config file <> both with the native node TypeScript support and configured TypeScript loaders. Errors were: - - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: x Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' - ,---- - 1 | export default i'll break this file yo - : ^^^^^^^^^^^^^^^^^^^^^^ - \`---- - x Unterminated string constant - ,---- - 1 | export default i'll break this file yo - : ^^^^^^^^^^^^^^^^^^^^^^ - \`---- - + - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' - TSError: ⨯ Unable to compile TypeScript: jest.config.mts(1,16): error TS2304: Cannot find name 'i'. jest.config.mts(1,17): error TS1005: ';' expected. diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts index d7a02e738c92..3dbd57e061b4 100644 --- a/e2e/__tests__/jest.config.mts.test.ts +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -45,7 +45,7 @@ onNodeVersions('<20.19.0', () => { }); onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0', () => { - test('work with untyped jest.config.mts', () => { + test('work with untyped jest.config.mts for Node versions without default type stripping', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", 'jest.config.mts': @@ -106,7 +106,7 @@ onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0', () => { }); onNodeVersions('>=22.18.0 || ^23.6', () => { - test('work with untyped jest.config.mts', () => { + test('work with untyped jest.config.mts for Node versions with default type stripping', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", 'jest.config.mts': diff --git a/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/jest.config.mts b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/jest.config.mts new file mode 100644 index 000000000000..4f69b4e3bda0 --- /dev/null +++ b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/jest.config.mts @@ -0,0 +1,8 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export default {}; diff --git a/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/package.json b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/package.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-mts/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/create-jest/src/__tests__/__snapshots__/init.test.ts.snap b/packages/create-jest/src/__tests__/__snapshots__/init.test.ts.snap index b81a676fa4a2..63a3c27027ca 100644 --- a/packages/create-jest/src/__tests__/__snapshots__/init.test.ts.snap +++ b/packages/create-jest/src/__tests__/__snapshots__/init.test.ts.snap @@ -54,6 +54,15 @@ Object { } `; +exports[`init has-jest-config-file-mts ask the user whether to override config or not user answered with "Yes" 1`] = ` +Object { + "initial": true, + "message": "It seems that you already have a jest configuration, do you want to override it?", + "name": "continue", + "type": "confirm", +} +`; + exports[`init has-jest-config-file-ts ask the user whether to override config or not user answered with "Yes" 1`] = ` Object { "initial": true, diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index 161fb5fb097d..0c9c374d52ba 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import {isNativeError} from 'util/types'; import * as fs from 'graceful-fs'; import parseJson from 'parse-json'; +import {satisfies} from 'semver'; import stripJsonComments from 'strip-json-comments'; import type {Config} from '@jest/types'; import {type Pragmas, extract, parse} from 'jest-docblock'; @@ -20,7 +21,6 @@ import { JEST_CONFIG_EXT_TS, PACKAGE_JSON, } from './constants'; -import {satisfies} from 'semver'; interface TsLoader { enabled: (bool: boolean) => void; From f13b60b2bc91427f577cb52189227e120c55eee8 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 11 Oct 2025 12:53:16 +0700 Subject: [PATCH 07/15] chore: address failed test with updated snapshot and refactors Signed-off-by: hainenber --- e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap | 8 ++++---- packages/jest-cli/src/__tests__/args.test.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index b68dda560c71..ef6083c4116a 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -39,12 +39,12 @@ Time: <> Ran all test suites." `; -exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts 1`] = ` +exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts for Node versions with default type stripping 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts 2`] = ` +exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts for Node versions with default type stripping 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total @@ -96,12 +96,12 @@ exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 does not work wit Error: SyntaxError: Missing initializer in const declaration" `; -exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts 1`] = ` +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts for Node versions without default type stripping 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts 2`] = ` +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts for Node versions without default type stripping 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total diff --git a/packages/jest-cli/src/__tests__/args.test.ts b/packages/jest-cli/src/__tests__/args.test.ts index 9cc7994205e1..52302fda8f0a 100644 --- a/packages/jest-cli/src/__tests__/args.test.ts +++ b/packages/jest-cli/src/__tests__/args.test.ts @@ -89,13 +89,16 @@ describe('check', () => { it('raises an exception if config is not a valid JSON string', () => { expect(() => check(argv({config: 'x:1'}))).toThrow( - 'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .cts, .json', + `The --config option requires a JSON string literal, or a file path with one of these extensions: ${constants.JEST_CONFIG_EXT_ORDER.join( + ', ', + )}`, ); }); it('raises an exception if config is not a supported file type', () => { - const message = - 'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .cts, .json'; + const message = `The --config option requires a JSON string literal, or a file path with one of these extensions: ${constants.JEST_CONFIG_EXT_ORDER.join( + ', ', + )}`; expect(() => check(argv({config: 'jest.configjs'}))).toThrow(message); expect(() => check(argv({config: 'jest.config.exe'}))).toThrow(message); From 7517cfb4e3650a94d9d2bd66e77d77a99ef35287 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 11 Oct 2025 13:32:31 +0700 Subject: [PATCH 08/15] chore: fix test content for typed jest.config.mts Signed-off-by: hainenber --- e2e/__tests__/jest.config.mts.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts index 3dbd57e061b4..91c710d109a9 100644 --- a/e2e/__tests__/jest.config.mts.test.ts +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -66,7 +66,7 @@ onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", 'jest.config.mts': ` - import {Config} from 'jest'; + import type {Config} from 'jest'; const config: Config = {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js' }; export default config; `, @@ -127,7 +127,7 @@ onNodeVersions('>=22.18.0 || ^23.6', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", 'jest.config.mts': ` - import {Config} from 'jest'; + import type {Config} from 'jest'; const config: Config = {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js' }; export default config; `, From 0da1b275f55369d0e1698e38e810a106ab3bfb98 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 11 Oct 2025 14:14:35 +0700 Subject: [PATCH 09/15] chore: update snapshot for failed test Signed-off-by: hainenber --- e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index ef6083c4116a..95c1c27f3647 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -93,7 +93,7 @@ exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 does not work wit "Error: Jest: Failed to parse the TypeScript config file <> Current Node version <> does not support loading typed .mts Jest config. Please upgrade to >=22.18.0 || ^23.6 - Error: SyntaxError: Missing initializer in const declaration" + Error: SyntaxError: Unexpected token '{'" `; exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts for Node versions without default type stripping 1`] = ` From ae42957a98fdd736fae9a227139f45f118850b67 Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 15 Oct 2025 22:44:52 +0700 Subject: [PATCH 10/15] feat(jest-config): revamp config loadout for .mts config to be JS runtime agnostic Signed-off-by: hainenber --- .../jest.config.mts.test.ts.snap | 71 ++++++++++--------- e2e/__tests__/jest.config.mts.test.ts | 40 ++++++++--- packages/jest-config/package.json | 2 +- .../src/readConfigFileAndSetRootDir.ts | 58 +++++++-------- 4 files changed, 98 insertions(+), 73 deletions(-) diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index 95c1c27f3647..c63b4c9a4c6d 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -1,23 +1,11 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`on node <20.19.0 does not work with jest.config.mts when require(esm) is not supported 1`] = ` +exports[`on node >=22.18.0 invalid JS in jest.config.mts (node with native TS support) 1`] = ` "Error: Jest: Failed to parse the TypeScript config file <> - Current Node version <> does not support loading .mts Jest config. - Please upgrade to ^20.19.0 || >=22.12.0" + SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)'" `; -exports[`on node >=22.18.0 || ^23.6 invalid JS in jest.config.mts (node with native TS support) 1`] = ` -"Error: Jest: Failed to parse the TypeScript config file <> - both with the native node TypeScript support and configured TypeScript loaders. - Errors were: - - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' - - TSError: ⨯ Unable to compile TypeScript: -jest.config.mts(1,16): error TS2304: Cannot find name 'i'. -jest.config.mts(1,17): error TS1005: ';' expected. -jest.config.mts(1,39): error TS1002: Unterminated string literal." -`; - -exports[`on node >=22.18.0 || ^23.6 traverses directory tree up until it finds jest.config 1`] = ` +exports[`on node >=22.18.0 traverses directory tree up until it finds jest.config 1`] = ` " console.log <>/jest-config-ts/some/nested/directory @@ -25,13 +13,13 @@ exports[`on node >=22.18.0 || ^23.6 traverses directory tree up until it finds j " `; -exports[`on node >=22.18.0 || ^23.6 traverses directory tree up until it finds jest.config 2`] = ` +exports[`on node >=22.18.0 traverses directory tree up until it finds jest.config 2`] = ` "PASS ../../../__tests__/a-giraffe.js ✓ giraffe ✓ abc" `; -exports[`on node >=22.18.0 || ^23.6 traverses directory tree up until it finds jest.config 3`] = ` +exports[`on node >=22.18.0 traverses directory tree up until it finds jest.config 3`] = ` "Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total @@ -39,12 +27,12 @@ Time: <> Ran all test suites." `; -exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts for Node versions with default type stripping 1`] = ` +exports[`on node >=22.18.0 work with untyped jest.config.mts for Node versions with default type stripping 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node >=22.18.0 || ^23.6 work with untyped jest.config.mts for Node versions with default type stripping 2`] = ` +exports[`on node >=22.18.0 work with untyped jest.config.mts for Node versions with default type stripping 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total @@ -52,12 +40,12 @@ Time: <> Ran all test suites." `; -exports[`on node >=22.18.0 || ^23.6 works with tsconfig.json 1`] = ` +exports[`on node >=22.18.0 works with tsconfig.json 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node >=22.18.0 || ^23.6 works with tsconfig.json 2`] = ` +exports[`on node >=22.18.0 works with tsconfig.json 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total @@ -65,12 +53,12 @@ Time: <> Ran all test suites." `; -exports[`on node >=22.18.0 || ^23.6 works with typed jest.config.mts 1`] = ` +exports[`on node >=22.18.0 works with typed jest.config.mts 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node >=22.18.0 || ^23.6 works with typed jest.config.mts 2`] = ` +exports[`on node >=22.18.0 works with typed jest.config.mts 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total @@ -80,28 +68,41 @@ Ran all test suites." exports[`on node >=24 invalid JS in jest.config.mts (node with native TS support) 1`] = ` "Error: Jest: Failed to parse the TypeScript config file <> - both with the native node TypeScript support and configured TypeScript loaders. - Errors were: - - SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)' - - TSError: ⨯ Unable to compile TypeScript: -jest.config.mts(1,16): error TS2304: Cannot find name 'i'. -jest.config.mts(1,17): error TS1005: ';' expected. -jest.config.mts(1,39): error TS1002: Unterminated string literal." + SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)'" `; -exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 does not work with typed jest.config.ts 1`] = ` +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 does not work with jest.config.mts when require(esm) is not supported 1`] = ` "Error: Jest: Failed to parse the TypeScript config file <> - Current Node version <> does not support loading typed .mts Jest config. - Please upgrade to >=22.18.0 || ^23.6 + Current JS runtime version <> does not support loading .mts Jest config. + Please upgrade your JS runtime to support process.features.require_module" +`; + +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 does not work with typed jest.config.ts 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + Current JS runtime version <> does not support loading typed .mts Jest config. + Please upgrade your JS runtime to support process.features.typescript Error: SyntaxError: Unexpected token '{'" `; -exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts for Node versions without default type stripping 1`] = ` +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 work with untyped jest.config.mts for Node versions without default type stripping 1`] = ` +"PASS __tests__/a-giraffe.js + ✓ giraffe" +`; + +exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 work with untyped jest.config.mts for Node versions without default type stripping 2`] = ` +"Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; + +exports[`work with typed jest.config.mts when TS loader is used 1`] = ` "PASS __tests__/a-giraffe.js ✓ giraffe" `; -exports[`on node ^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0 work with untyped jest.config.mts for Node versions without default type stripping 2`] = ` +exports[`work with typed jest.config.mts when TS loader is used 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts index 91c710d109a9..ec5066e3cfb3 100644 --- a/e2e/__tests__/jest.config.mts.test.ts +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -15,7 +15,28 @@ const DIR = path.resolve(__dirname, '../jest-config-ts'); beforeEach(() => cleanup(DIR)); afterAll(() => cleanup(DIR)); -onNodeVersions('<20.19.0', () => { +test('work with typed jest.config.mts when TS loader is used', () => { + writeFiles(DIR, { + '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", + 'jest.config.mts': ` + /** @jest-config-loader ts-node */ + import type {Config} from 'jest'; + const config: Config = {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js' }; + export default config; + `, + 'package.json': '{"type": "commonjs"}', + }); + + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { + nodeOptions: '--no-warnings', + }); + const {rest, summary} = extractSummary(stderr); + expect(exitCode).toBe(0); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); +}); + +onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0', () => { test('does not work with jest.config.mts when require(esm) is not supported', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", @@ -25,7 +46,7 @@ onNodeVersions('<20.19.0', () => { }); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { - nodeOptions: '--no-warnings', + nodeOptions: '--no-warnings --no-experimental-require-module', }); expect( stderr @@ -37,14 +58,12 @@ onNodeVersions('<20.19.0', () => { /(Error: Jest: Failed to parse the TypeScript config file).*$/m, '$1 <>', ) - // Replace Node version with - .replace(/(Current Node version) (.+?) /m, '$1 <> '), + // Replace Node version with placeholder + .replace(/(Current JS runtime version) (.+?) /m, '$1 <> '), ).toMatchSnapshot(); expect(exitCode).toBe(1); }); -}); -onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0', () => { test('work with untyped jest.config.mts for Node versions without default type stripping', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", @@ -87,7 +106,7 @@ onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0', () => { '$1 <>', ) // Replace Node version with - .replace(/(Current Node version) (.+?) /m, '$1 <> '), + .replace(/(Current JS runtime version) (.+?) /m, '$1 <> '), ).toMatchSnapshot(); expect(exitCode).toBe(1); }); @@ -100,12 +119,15 @@ onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0 || 23 - 23.6.0', () => { }); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']); - expect(stderr).toMatch('SyntaxError: Invalid or unexpected token'); + expect(stderr).toMatch('does not support loading typed .mts Jest config'); + expect(stderr).toMatch( + 'Please upgrade your JS runtime to support process.features.typescript', + ); expect(exitCode).toBe(1); }); }); -onNodeVersions('>=22.18.0 || ^23.6', () => { +onNodeVersions('>=22.18.0', () => { test('work with untyped jest.config.mts for Node versions with default type stripping', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", diff --git a/packages/jest-config/package.json b/packages/jest-config/package.json index e5b096d5b0d9..539e7a7675e7 100644 --- a/packages/jest-config/package.json +++ b/packages/jest-config/package.json @@ -57,7 +57,6 @@ "micromatch": "^4.0.8", "parse-json": "^5.2.0", "pretty-format": "workspace:*", - "semver": "^7.7.2", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -68,6 +67,7 @@ "@types/parse-json": "^4.0.2", "esbuild": "^0.25.5", "esbuild-register": "^3.6.0", + "semver": "^7.7.2", "ts-node": "^10.5.0", "typescript": "^5.8.3" }, diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index 0c9c374d52ba..cb2622e4a23b 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -9,7 +9,6 @@ import * as path from 'path'; import {isNativeError} from 'util/types'; import * as fs from 'graceful-fs'; import parseJson from 'parse-json'; -import {satisfies} from 'semver'; import stripJsonComments from 'strip-json-comments'; import type {Config} from '@jest/types'; import {type Pragmas, extract, parse} from 'jest-docblock'; @@ -37,8 +36,7 @@ export default async function readConfigFileAndSetRootDir( const isMTS = configPath.endsWith(JEST_CONFIG_EXT_MTS); const isTS = configPath.endsWith(JEST_CONFIG_EXT_TS) || - configPath.endsWith(JEST_CONFIG_EXT_CTS) || - isMTS; + configPath.endsWith(JEST_CONFIG_EXT_CTS); const isJSON = configPath.endsWith(JEST_CONFIG_EXT_JSON); let configObject; @@ -77,37 +75,41 @@ export default async function readConfigFileAndSetRootDir( } } } else { - if (isMTS) { - // TODO: remove this once dropping Node 20/22 support. - const mtsExtSupportVersionRange = '^20.19.0 || >=22.12.0'; - if (!satisfies(process.versions.node, mtsExtSupportVersionRange)) { - // Likely Node version not yet supports require(esm) - // This string is caught further down and merged into a new error message. - // eslint-disable-next-line no-throw-literal - throw ( - ` Current Node version ${process.versions.node} does not support loading .mts Jest config.\n` + - ` Please upgrade to ${mtsExtSupportVersionRange}` - ); - } - // Relies on import(.mts) before falling back to require(.mts) - try { - configObject = await requireOrImportModule(configPath); - } catch (requireOrImportModuleError) { - if (!(requireOrImportModuleError instanceof SyntaxError)) { - throw requireOrImportModuleError; - } - // Likely Node version does not support type stripping when require(esm). + configObject = await loadTSConfigFile(configPath); + } + } else if (isMTS) { + if (!hasTsLoaderExplicitlyConfigured(configPath)) { + // @ts-expect-error: Type assertion can be removed once @types/node is updated to 23 https://nodejs.org/api/process.html#processfeaturestypescript + if (!process.features.require_module) { + // Likely JS runtime does not yet support require(esm) yet. + // This string is caught further down and merged into a new error message. + // eslint-disable-next-line no-throw-literal + throw ( + ` Current JS runtime version ${process.versions.node} does not support loading .mts Jest config.\n` + + ` Please upgrade your JS runtime to support process.features.require_module` + ); + } + + // Relies on import(.mts) before falling back to require(.mts) + try { + configObject = await requireOrImportModule(configPath); + } catch (requireOrImportModuleError) { + // Likely JS runtime does not support type stripping when require(esm). + // @ts-expect-error: Type assertion can be removed once @types/node is updated to 23 https://nodejs.org/api/process.html#processfeaturestypescript + if (!process.features.typescript) { // This string is caught further down and merged into a new error message. // eslint-disable-next-line no-throw-literal throw ( - ` Current Node version ${process.versions.node} does not support loading typed .mts Jest config.\n` + - ' Please upgrade to >=22.18.0 || ^23.6\n' + + ` Current JS runtime version ${process.versions.node} does not support loading typed .mts Jest config.\n` + + ' Please upgrade your JS runtime to support process.features.typescript \n' + ` Error: ${requireOrImportModuleError}\n` ); } - } else { - configObject = await loadTSConfigFile(configPath); + // Encounter unknown errors, thrown to users for further debugging. + throw requireOrImportModuleError; } + } else { + configObject = await loadTSConfigFile(configPath); } } else if (isJSON) { const fileContent = fs.readFileSync(configPath, 'utf8'); @@ -116,7 +118,7 @@ export default async function readConfigFileAndSetRootDir( configObject = await requireOrImportModule(configPath); } } catch (error) { - if (isTS) { + if (isTS || isMTS) { throw new Error( `Jest: Failed to parse the TypeScript config file ${configPath}\n` + ` ${error}`, From 61f869624eac6b288b2cc9244cbacb0d192ca170 Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 15 Oct 2025 22:45:05 +0700 Subject: [PATCH 11/15] chore: reposition CHANGELOG entry Signed-off-by: hainenber --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f779d362926..38cf5f91a886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[jest-config]` Add `defineConfig` and `mergeConfig` helpers for type-safe Jest config ([#15844](https://github.com/jestjs/jest/pull/15844)) +- `[jest-config]` Supports Jest config file with `.mts` extension ([#15796](https://github.com/jestjs/jest/pull/15796)) ### Fixes @@ -23,7 +24,6 @@ ## Features - `[jest-environment-jsdom-abstract]` Add support for JSDOM v27 ([#15834](https://github.com/jestjs/jest/pull/15834)) -- `[jest-config]` Supports Jest config file with `.mts` extension ([#15796](https://github.com/jestjs/jest/pull/15796)) ### Fixes From 80043527b4242a1c44d8e617a72daf1a2cbf5d7b Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 15 Oct 2025 22:47:51 +0700 Subject: [PATCH 12/15] chore: fix lint issues Signed-off-by: hainenber --- packages/jest-config/src/readConfigFileAndSetRootDir.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index cb2622e4a23b..fc78ccc2c10e 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -78,7 +78,9 @@ export default async function readConfigFileAndSetRootDir( configObject = await loadTSConfigFile(configPath); } } else if (isMTS) { - if (!hasTsLoaderExplicitlyConfigured(configPath)) { + if (hasTsLoaderExplicitlyConfigured(configPath)) { + configObject = await loadTSConfigFile(configPath); + } else { // @ts-expect-error: Type assertion can be removed once @types/node is updated to 23 https://nodejs.org/api/process.html#processfeaturestypescript if (!process.features.require_module) { // Likely JS runtime does not yet support require(esm) yet. @@ -86,7 +88,7 @@ export default async function readConfigFileAndSetRootDir( // eslint-disable-next-line no-throw-literal throw ( ` Current JS runtime version ${process.versions.node} does not support loading .mts Jest config.\n` + - ` Please upgrade your JS runtime to support process.features.require_module` + ' Please upgrade your JS runtime to support process.features.require_module' ); } @@ -108,8 +110,6 @@ export default async function readConfigFileAndSetRootDir( // Encounter unknown errors, thrown to users for further debugging. throw requireOrImportModuleError; } - } else { - configObject = await loadTSConfigFile(configPath); } } else if (isJSON) { const fileContent = fs.readFileSync(configPath, 'utf8'); From 2183aa363ffee32b6530e44d9b8bd446882e4d08 Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 15 Oct 2025 23:30:50 +0700 Subject: [PATCH 13/15] chore: refresh yarn cache Signed-off-by: hainenber --- e2e/__tests__/jest.config.mts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts index ec5066e3cfb3..c74a9d6366d7 100644 --- a/e2e/__tests__/jest.config.mts.test.ts +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -24,7 +24,7 @@ test('work with typed jest.config.mts when TS loader is used', () => { const config: Config = {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js' }; export default config; `, - 'package.json': '{"type": "commonjs"}', + 'package.json': '{}', }); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { From e81a7b50f8c111c0a2ab6c618baa2f75457beaaf Mon Sep 17 00:00:00 2001 From: hainenber Date: Thu, 16 Oct 2025 23:08:26 +0700 Subject: [PATCH 14/15] feat: guide users to use native runtime to load .mts Jest config Signed-off-by: hainenber --- .../jest.config.mts.test.ts.snap | 19 ++++++------------ e2e/__tests__/jest.config.mts.test.ts | 20 +++++++++++++------ .../src/readConfigFileAndSetRootDir.ts | 11 ++++++++-- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index c63b4c9a4c6d..acd6aa5a749d 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`do not work with jest.config.mts when TS loader is used 1`] = ` +"Error: Jest: Failed to parse the TypeScript config file <> + Loading .mts Jest config with external loaders is discouraged. + Please use a JS runtime that supports process.features.require_module and process.features.typescript." +`; + exports[`on node >=22.18.0 invalid JS in jest.config.mts (node with native TS support) 1`] = ` "Error: Jest: Failed to parse the TypeScript config file <> SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)'" @@ -96,16 +102,3 @@ Snapshots: 0 total Time: <> Ran all test suites." `; - -exports[`work with typed jest.config.mts when TS loader is used 1`] = ` -"PASS __tests__/a-giraffe.js - ✓ giraffe" -`; - -exports[`work with typed jest.config.mts when TS loader is used 2`] = ` -"Test Suites: 1 passed, 1 total -Tests: 1 passed, 1 total -Snapshots: 0 total -Time: <> -Ran all test suites." -`; diff --git a/e2e/__tests__/jest.config.mts.test.ts b/e2e/__tests__/jest.config.mts.test.ts index c74a9d6366d7..379a97bfacf7 100644 --- a/e2e/__tests__/jest.config.mts.test.ts +++ b/e2e/__tests__/jest.config.mts.test.ts @@ -15,11 +15,11 @@ const DIR = path.resolve(__dirname, '../jest-config-ts'); beforeEach(() => cleanup(DIR)); afterAll(() => cleanup(DIR)); -test('work with typed jest.config.mts when TS loader is used', () => { +test('do not work with jest.config.mts when TS loader is used', () => { writeFiles(DIR, { '__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));", 'jest.config.mts': ` - /** @jest-config-loader ts-node */ + /** @jest-config-loader esbuild-register */ import type {Config} from 'jest'; const config: Config = {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js' }; export default config; @@ -30,10 +30,18 @@ test('work with typed jest.config.mts when TS loader is used', () => { const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], { nodeOptions: '--no-warnings', }); - const {rest, summary} = extractSummary(stderr); - expect(exitCode).toBe(0); - expect(rest).toMatchSnapshot(); - expect(summary).toMatchSnapshot(); + expect( + stderr + // Remove the stack trace from the error message + .slice(0, Math.max(0, stderr.indexOf('at readConfigFileAndSetRootDir'))) + .trim() + // Replace the path to the config file with a placeholder + .replace( + /(Error: Jest: Failed to parse the TypeScript config file).*$/m, + '$1 <>', + ), + ).toMatchSnapshot(); + expect(exitCode).toBe(1); }); onNodeVersions('^20.19.0 || >=22.12.0 <22.18.0', () => { diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index fc78ccc2c10e..13b6770d6230 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -78,10 +78,17 @@ export default async function readConfigFileAndSetRootDir( configObject = await loadTSConfigFile(configPath); } } else if (isMTS) { + // JS runtime's support for Typescript is mature enough that we should + // guide users towards native usage instead of possibly un-maintained + // external loader. if (hasTsLoaderExplicitlyConfigured(configPath)) { - configObject = await loadTSConfigFile(configPath); + // eslint-disable-next-line no-throw-literal + throw ( + ' Loading .mts Jest config with external loaders is discouraged.\n' + + ' Please use a JS runtime that supports process.features.require_module and process.features.typescript' + ); } else { - // @ts-expect-error: Type assertion can be removed once @types/node is updated to 23 https://nodejs.org/api/process.html#processfeaturestypescript + // @ts-expect-error: Type assertion can be removed once @types/node is updated to 23 https://nodejs.org/api/process.html#processfeaturesrequire_module if (!process.features.require_module) { // Likely JS runtime does not yet support require(esm) yet. // This string is caught further down and merged into a new error message. From 8d4363d0d54eaff6725d2a1d9ab5bfb6b6e815f5 Mon Sep 17 00:00:00 2001 From: hainenber Date: Thu, 16 Oct 2025 23:12:57 +0700 Subject: [PATCH 15/15] chore: update snapshot Signed-off-by: hainenber --- e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap index acd6aa5a749d..f0291e130f68 100644 --- a/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap +++ b/e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap @@ -3,7 +3,7 @@ exports[`do not work with jest.config.mts when TS loader is used 1`] = ` "Error: Jest: Failed to parse the TypeScript config file <> Loading .mts Jest config with external loaders is discouraged. - Please use a JS runtime that supports process.features.require_module and process.features.typescript." + Please use a JS runtime that supports process.features.require_module and process.features.typescript" `; exports[`on node >=22.18.0 invalid JS in jest.config.mts (node with native TS support) 1`] = `