Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ecbdcf9
feat(jest-config): supports Jest config file with .mts extension
hainenber Aug 24, 2025
73135e8
feat(jest-config): supports untyped`.mts` config file
hainenber Aug 26, 2025
4637296
chore: add CHANGELOG entry
hainenber Aug 26, 2025
e789de5
Update readConfigFileAndSetRootDir.ts
cpojer Aug 27, 2025
dcad33b
feat(cfg): enhance error message to inform user of default type strip…
hainenber Sep 27, 2025
7e8a3f5
Merge branch 'main' into feat/support-jest-config-with-mts-extension
hainenber Sep 27, 2025
18e8712
Merge branch 'main' into feat/support-jest-config-with-mts-extension
hainenber Sep 28, 2025
0b24295
chore: address failed tests with updated snapshot + expand case for j…
hainenber Oct 11, 2025
469862b
Merge branch 'main' into feat/support-jest-config-with-mts-extension
hainenber Oct 11, 2025
f13b60b
chore: address failed test with updated snapshot and refactors
hainenber Oct 11, 2025
7517cfb
chore: fix test content for typed jest.config.mts
hainenber Oct 11, 2025
0da1b27
chore: update snapshot for failed test
hainenber Oct 11, 2025
ae42957
feat(jest-config): revamp config loadout for .mts config to be JS run…
hainenber Oct 15, 2025
61f8696
chore: reposition CHANGELOG entry
hainenber Oct 15, 2025
8004352
chore: fix lint issues
hainenber Oct 15, 2025
2183aa3
chore: refresh yarn cache
hainenber Oct 15, 2025
e81a7b5
feat: guide users to use native runtime to load .mts Jest config
hainenber Oct 16, 2025
8d4363d
chore: update snapshot
hainenber Oct 16, 2025
15c06e3
Merge branch 'main' into feat/support-jest-config-with-mts-extension
hainenber Oct 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
120 changes: 120 additions & 0 deletions e2e/__tests__/__snapshots__/jest.config.mts.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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 <<REPLACED>>
Current Node version <<REPLACED>> does not support loading .mts Jest config.
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 <<REPLACED>>
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 <<REPLACED>>
Current Node version <<REPLACED>> 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: <<REPLACED>>
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 <<REPLACED>>
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
<<REPLACED>>/jest-config-ts/some/nested/directory

at Object.<anonymous> (__tests__/a-giraffe.js:3:27)
"
`;

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 ^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
Time: <<REPLACED>>
Ran all test suites."
`;

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: <<REPLACED>>
Ran all test suites."
`;

exports[`on node ^23.6 works with tsconfig.json 1`] = `
"PASS __tests__/a-giraffe.js
✓ giraffe"
`;

exports[`on node ^23.6 works with tsconfig.json 2`] = `
"Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;

exports[`on node ^23.6 works with typed jest.config.mts 1`] = `
"PASS __tests__/a-giraffe.js
✓ giraffe"
`;

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
Time: <<REPLACED>>
Ran all test suites."
`;
248 changes: 248 additions & 0 deletions e2e/__tests__/jest.config.mts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/**
* 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', () => {
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 <<REPLACED>>',
)
// Replace Node version with
.replace(/(Current Node version) (.+?) /m, '$1 <<REPLACED>> '),
).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 <<REPLACED>>',
)
// Replace Node version with
.replace(/(Current Node version) (.+?) /m, '$1 <<REPLACED>> '),
).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"}',
});

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, '<<REPLACED>>')
// slightly different log in node versions >= 23
.replace('at Object.log', 'at Object.<anonymous>'),
).toMatchSnapshot();

const {rest, summary} = extractSummary(stderr);
expect(exitCode).toBe(0);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});

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 <<REPLACED>>',
),
).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 <<REPLACED>>',
),
).toMatchSnapshot();
expect(exitCode).toBe(1);
});
});
2 changes: 1 addition & 1 deletion packages/jest-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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"
},
Expand Down
Loading