mirror of
https://github.com/hanzoai/jest.git
synced 2026-09-24 16:59:57 +00:00
Support jest.config.mts (#16005)
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
- `[babel-jest]` Support collecting coverage from `.mts`, `.cts` (and other) files ([#15994](https://github.com/jestjs/jest/pull/15994))
|
||||
- `[jest-circus, jest-cli, jest-config, jest-core, jest-jasmine2, jest-types]` Add `--collect-tests` flag to discover and list tests without executing them ([#16006](https://github.com/jestjs/jest/pull/16006))
|
||||
- `[jest-config]` Add support for `jest.config.mts` as a valid configuration file ([#16005](https://github.com/jestjs/jest/pull/16005))
|
||||
- `[jest-mock]` Add `clearMocksOnScope(scope)` on `ModuleMocker` for clearing every mock function exposed on a scope object ([#16088](https://github.com/jestjs/jest/pull/16088))
|
||||
- `[jest-resolve]` Add `canResolveSync()` on `Resolver` so callers can detect when a user-configured resolver only exports an `async` hook ([#16064](https://github.com/jestjs/jest/pull/16064))
|
||||
- `[jest-runtime]` Use synchronous `evaluate()` for ES modules without top-level `await` on Node versions that support it (v24.9+), and prefer the synchronous transform path when a sync transformer is configured ([#16062](https://github.com/jestjs/jest/pull/16062))
|
||||
|
||||
@@ -5,7 +5,7 @@ title: Configuring Jest
|
||||
|
||||
The Jest philosophy is to work great by default, but sometimes you just need more configuration power.
|
||||
|
||||
It is recommended to define the configuration in a dedicated JavaScript, TypeScript or JSON file. The file will be discovered automatically, if it is named `jest.config.js|ts|mjs|cjs|cts|json`. You can use [`--config`](CLI.md#--configpath) flag to pass an explicit path to the file.
|
||||
It is recommended to define the configuration in a dedicated JavaScript, TypeScript or JSON file. The file will be discovered automatically, if it is named `jest.config.js|ts|mjs|mts|cjs|cts|json`. You can use [`--config`](CLI.md#--configpath) flag to pass an explicit path to the file.
|
||||
|
||||
:::note
|
||||
|
||||
|
||||
@@ -69,6 +69,27 @@ describe('readInitialOptions', () => {
|
||||
},
|
||||
);
|
||||
|
||||
onNodeVersions('^22.18 || >=23.6', () => {
|
||||
test('should read mts-config/jest.config.mts file', async () => {
|
||||
const configFile = resolveFixture('mts-config', 'jest.config.mts');
|
||||
const rootDir = resolveFixture('mts-config');
|
||||
const {config, configPath} = await proxyReadInitialOptions(undefined, {
|
||||
cwd: rootDir,
|
||||
});
|
||||
expect(config).toEqual({jestConfig: 'jest.config.mts', rootDir});
|
||||
expect(configPath).toEqual(configFile);
|
||||
});
|
||||
});
|
||||
|
||||
onNodeVersions('<22.18', () => {
|
||||
test('should fail to read mts-config/jest.config.mts with a clear error', async () => {
|
||||
const rootDir = resolveFixture('mts-config');
|
||||
await expect(
|
||||
proxyReadInitialOptions(undefined, {cwd: rootDir}),
|
||||
).rejects.toThrow(/jest\.config\.mts requires native TypeScript support/);
|
||||
});
|
||||
});
|
||||
|
||||
test('should be able to skip config reading, instead read from cwd', async () => {
|
||||
const expectedConfigFile = resolveFixture(
|
||||
'json-config',
|
||||
|
||||
@@ -17,6 +17,7 @@ afterAll(() => cleanup(DIR));
|
||||
// Node.js's builtin TypeScript support doesn't do type checking, so skip tests
|
||||
// that validate type-checking behavior.
|
||||
const testIfTsLoader = useNativeTypeScript ? test.skip : test;
|
||||
const testIfNativeTypeScript = useNativeTypeScript ? test : test.skip;
|
||||
|
||||
describe('when `Config` type is imported from "@jest/types"', () => {
|
||||
test('with object config exported from TS file', () => {
|
||||
@@ -97,6 +98,49 @@ describe('when `Config` type is imported from "@jest/types"', () => {
|
||||
expect(globalConfig.verbose).toBe(true);
|
||||
});
|
||||
|
||||
testIfNativeTypeScript(
|
||||
'with object config exported from MTS file when package.json#type=commonjs',
|
||||
() => {
|
||||
writeFiles(DIR, {
|
||||
'__tests__/dummy.test.js':
|
||||
"test('dummy', () => expect(123).toBe(123));",
|
||||
'jest.config.mts': `
|
||||
import type {Config} from '@jest/types';
|
||||
const config: Config.InitialOptions = {displayName: 'ts-mts-object-config', verbose: true};
|
||||
export default config;
|
||||
`,
|
||||
'package.json': '{"type": "commonjs"}',
|
||||
});
|
||||
|
||||
const {configs, globalConfig} = getConfig(path.join(DIR));
|
||||
|
||||
expect(configs).toHaveLength(1);
|
||||
expect(configs[0].displayName?.name).toBe('ts-mts-object-config');
|
||||
expect(globalConfig.verbose).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
testIfNativeTypeScript(
|
||||
'with object config exported from MTS file when package.json#type=module',
|
||||
() => {
|
||||
writeFiles(DIR, {
|
||||
'__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));",
|
||||
'jest.config.mts': `
|
||||
import type {Config} from '@jest/types';
|
||||
const config: Config.InitialOptions = {displayName: 'ts-mts-esm-object-config', verbose: true};
|
||||
export default config;
|
||||
`,
|
||||
'package.json': '{"type": "module"}',
|
||||
});
|
||||
|
||||
const {configs, globalConfig} = getConfig(path.join(DIR));
|
||||
|
||||
expect(configs).toHaveLength(1);
|
||||
expect(configs[0].displayName?.name).toBe('ts-mts-esm-object-config');
|
||||
expect(globalConfig.verbose).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
testIfTsLoader('throws if type errors are encountered', () => {
|
||||
writeFiles(DIR, {
|
||||
'__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 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 {
|
||||
jestConfig: 'jest.config.mts',
|
||||
};
|
||||
+8
@@ -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 {};
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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,
|
||||
|
||||
@@ -89,13 +89,13 @@ 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: .js, .ts, .mjs, .mts, .cjs, .cts, .json',
|
||||
);
|
||||
});
|
||||
|
||||
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';
|
||||
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .mts, .cjs, .cts, .json';
|
||||
|
||||
expect(() => check(argv({config: 'jest.configjs'}))).toThrow(message);
|
||||
expect(() => check(argv({config: 'jest.config.exe'}))).toThrow(message);
|
||||
|
||||
@@ -14,6 +14,50 @@ import {onNodeVersions} from '@jest/test-utils';
|
||||
jest.mock('graceful-fs').mock('jest-util');
|
||||
|
||||
describe('readConfigFileAndSetRootDir', () => {
|
||||
describe('TypeScript ESM file', () => {
|
||||
test('reads .mts config and sets `rootDir`', async () => {
|
||||
jest.mocked(requireOrImportModule).mockResolvedValueOnce({notify: true});
|
||||
|
||||
const rootDir = path.resolve('some', 'path', 'to');
|
||||
const config = await readConfigFileAndSetRootDir(
|
||||
path.join(rootDir, 'jest.config.mts'),
|
||||
);
|
||||
|
||||
expect(config).toEqual({notify: true, rootDir});
|
||||
});
|
||||
|
||||
test('throws a clear error when native import fails, without falling back to ts-node', async () => {
|
||||
jest
|
||||
.mocked(requireOrImportModule)
|
||||
.mockRejectedValueOnce(new Error('Unknown file extension ".mts"'));
|
||||
|
||||
const configPath = path.join(
|
||||
path.resolve('some', 'path', 'to'),
|
||||
'jest.config.mts',
|
||||
);
|
||||
await expect(readConfigFileAndSetRootDir(configPath)).rejects.toThrow(
|
||||
/jest\.config\.mts requires native TypeScript support/,
|
||||
);
|
||||
// loadTSConfigFile reads the file for docblock parsing - it must not be called
|
||||
expect(fs.readFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('throws a clear error when native import fails with a SyntaxError', async () => {
|
||||
jest
|
||||
.mocked(requireOrImportModule)
|
||||
.mockRejectedValueOnce(new SyntaxError('Unexpected token'));
|
||||
|
||||
const configPath = path.join(
|
||||
path.resolve('some', 'path', 'to'),
|
||||
'jest.config.mts',
|
||||
);
|
||||
await expect(readConfigFileAndSetRootDir(configPath)).rejects.toThrow(
|
||||
/jest\.config\.mts requires native TypeScript support/,
|
||||
);
|
||||
expect(fs.readFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('JavaScript file', () => {
|
||||
test('reads config and sets `rootDir`', async () => {
|
||||
jest.mocked(requireOrImportModule).mockResolvedValueOnce({notify: true});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {interopRequireDefault, isError, 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';
|
||||
@@ -25,7 +26,8 @@ interface TsLoader {
|
||||
type TsLoaderModule = 'ts-node' | 'esbuild-register';
|
||||
// Read the configuration and set its `rootDir`
|
||||
// 1. If it's a `package.json` file, we look into its "jest" property
|
||||
// 2. If it's a `jest.config.ts` file, we use `ts-node` to transpile & require it
|
||||
// 2. If it's a `jest.config.ts`/`jest.config.cts`/`jest.config.mts` file,
|
||||
// we use native TypeScript support when possible, otherwise a TS loader.
|
||||
// 3. For any other file, we just require it. If we receive an 'ERR_REQUIRE_ESM'
|
||||
// from node, perform a dynamic import instead.
|
||||
export default async function readConfigFileAndSetRootDir(
|
||||
@@ -33,18 +35,28 @@ export default async function readConfigFileAndSetRootDir(
|
||||
): Promise<Config.InitialOptions> {
|
||||
const isTS =
|
||||
configPath.endsWith(JEST_CONFIG_EXT_TS) ||
|
||||
configPath.endsWith(JEST_CONFIG_EXT_MTS) ||
|
||||
configPath.endsWith(JEST_CONFIG_EXT_CTS);
|
||||
const isMTS = configPath.endsWith(JEST_CONFIG_EXT_MTS);
|
||||
const isJSON = configPath.endsWith(JEST_CONFIG_EXT_JSON);
|
||||
let configObject;
|
||||
|
||||
try {
|
||||
if (isTS) {
|
||||
// .mts is always ESM, so attempt import-based loading first.
|
||||
// @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) {
|
||||
if (isMTS || process.features.typescript) {
|
||||
try {
|
||||
// Try native node TypeScript support first.
|
||||
configObject = await requireOrImportModule<any>(configPath);
|
||||
} catch (requireOrImportModuleError) {
|
||||
if (isMTS) {
|
||||
// .mts is always ESM and cannot be loaded via require()/ts-node.
|
||||
throw new Error(
|
||||
'jest.config.mts requires native TypeScript support. Ensure you are using Node.js 22.18+ or 23.6+.',
|
||||
{cause: requireOrImportModuleError},
|
||||
);
|
||||
}
|
||||
if (!(requireOrImportModuleError instanceof SyntaxError)) {
|
||||
if (!hasTsLoaderExplicitlyConfigured(configPath)) {
|
||||
throw requireOrImportModuleError;
|
||||
@@ -226,6 +238,7 @@ async function registerTsLoader(loader: TsLoaderModule): Promise<TsLoader> {
|
||||
) {
|
||||
throw new Error(
|
||||
`Jest: '${loader}' is required for the TypeScript configuration files. Make sure it is installed\nError: ${error.message}`,
|
||||
{cause: error},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export default async function requireOrImportModule<T>(
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (filePath.endsWith('.mjs')) {
|
||||
if (filePath.endsWith('.mjs') || filePath.endsWith('.mts')) {
|
||||
return importModule(filePath, applyInteropRequireDefault);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user