diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad94faece02..e70d4005fcd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[jest-config]` [**BREAKING**] Add `mts` and `cts` to default `moduleFileExtensions` config ([#14369](https://github.com/facebook/jest/pull/14369)) - `[jest-config]` [**BREAKING**] Update `testMatch` and `testRegex` default option for supporting `mjs`, `cjs`, `mts`, and `cts` ([#14584](https://github.com/jestjs/jest/pull/14584)) - `[jest-config]` Loads config file from provided path in `package.json` ([#14044](https://github.com/facebook/jest/pull/14044)) +- `[jest-config]` Allow loading `jest.config.cts` files ([#14070](https://github.com/facebook/jest/pull/14070)) - `[@jest/core]` Group together open handles with the same stack trace ([#13417](https://github.com/jestjs/jest/pull/13417), & [#14789](https://github.com/jestjs/jest/pull/14789)) - `[@jest/core]` Add `perfStats` to surface test setup overhead ([#14622](https://github.com/jestjs/jest/pull/14622)) - `[@jest/core]` [**BREAKING**] Changed `--filter` to accept an object with shape `{ filtered: Array }` to match [documentation](https://jestjs.io/docs/cli#--filterfile) ([#13319](https://github.com/jestjs/jest/pull/13319)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 907af0b9ec7a..e14fb57dfd74 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -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|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|cjs|cts|json`. You can use [`--config`](CLI.md#--configpath) flag to pass an explicit path to the file. :::note diff --git a/e2e/__tests__/tsIntegration.test.ts b/e2e/__tests__/tsIntegration.test.ts index 32d9f01ccd66..fcb4f2df10af 100644 --- a/e2e/__tests__/tsIntegration.test.ts +++ b/e2e/__tests__/tsIntegration.test.ts @@ -54,6 +54,45 @@ describe('when `Config` type is imported from "@jest/types"', () => { expect(globalConfig.verbose).toBe(true); }); + test('with object config exported from CTS file', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from '@jest/types'; + const config: Config.InitialOptions = {displayName: 'ts-object-config', verbose: true}; + export default config; + `, + 'package.json': '{}', + }); + + const {configs, globalConfig} = getConfig(path.join(DIR)); + + expect(configs).toHaveLength(1); + expect(configs[0].displayName?.name).toBe('ts-object-config'); + expect(globalConfig.verbose).toBe(true); + }); + + test('with function config exported from CTS file', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from '@jest/types'; + async function getVerbose() {return true;} + export default async (): Promise => { + const verbose: Config.InitialOptions['verbose'] = await getVerbose(); + return {displayName: 'ts-async-function-config', verbose}; + }; + `, + 'package.json': '{}', + }); + + const {configs, globalConfig} = getConfig(path.join(DIR)); + + expect(configs).toHaveLength(1); + expect(configs[0].displayName?.name).toBe('ts-async-function-config'); + expect(globalConfig.verbose).toBe(true); + }); + test('throws if type errors are encountered', () => { writeFiles(DIR, { '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", @@ -92,6 +131,44 @@ describe('when `Config` type is imported from "@jest/types"', () => { expect(exitCode).toBe(1); }); + test('throws if type errors are encountered with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from '@jest/types'; + const config: Config.InitialOptions = {testTimeout: '10000'}; + export default config; + `, + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(2,40): error TS2322: Type 'string' is not assignable to type 'number'.", + ); + expect(exitCode).toBe(1); + }); + + test('throws if syntax errors are encountered with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from '@jest/types'; + const config: Config.InitialOptions = {verbose: true}; + export default get config; + `, + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(3,16): error TS2304: Cannot find name 'get'.", + ); + expect(exitCode).toBe(1); + }); + test('works with object config exported from TS file when package.json#type=module', () => { writeFiles(DIR, { '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", @@ -114,6 +191,45 @@ describe('when `Config` type is imported from "@jest/types"', () => { writeFiles(DIR, { '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", 'jest.config.ts': ` + import type {Config} from '@jest/types'; + async function getVerbose() {return true;} + export default async (): Promise => { + const verbose: Config.InitialOptions['verbose'] = await getVerbose(); + return {displayName: 'ts-esm-async-function-config', verbose}; + }; + `, + 'package.json': '{"type": "module"}', + }); + + const {configs, globalConfig} = getConfig(path.join(DIR)); + + expect(configs).toHaveLength(1); + expect(configs[0].displayName?.name).toBe('ts-esm-async-function-config'); + expect(globalConfig.verbose).toBe(true); + }); + + test('works with object config exported from CTS file when package.json#type=module', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", + 'jest.config.cts': ` + import type {Config} from '@jest/types'; + const config: Config.InitialOptions = {displayName: 'ts-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-esm-object-config'); + expect(globalConfig.verbose).toBe(true); + }); + + test('works with function config exported from CTS file when package.json#type=module', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", + 'jest.config.cts': ` import type {Config} from '@jest/types'; async function getVerbose() {return true;} export default async (): Promise => { @@ -168,6 +284,44 @@ describe('when `Config` type is imported from "@jest/types"', () => { ); expect(exitCode).toBe(1); }); + + test('throws if type errors are encountered when package.json#type=module with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", + 'jest.config.cts': ` + import type {Config} from '@jest/types'; + const config: Config.InitialOptions = {testTimeout: '10000'}; + export default config; + `, + 'package.json': '{"type": "module"}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(2,40): error TS2322: Type 'string' is not assignable to type 'number'.", + ); + expect(exitCode).toBe(1); + }); + + test('throws if syntax errors are encountered when package.json#type=module with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from '@jest/types'; + const config: Config.InitialOptions = {verbose: true}; + export default get config; + `, + 'package.json': '{"type": "module"}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(3,16): error TS2304: Cannot find name 'get'.", + ); + expect(exitCode).toBe(1); + }); }); describe('when `Config` type is imported from "jest"', () => { @@ -210,6 +364,45 @@ describe('when `Config` type is imported from "jest"', () => { expect(globalConfig.verbose).toBe(true); }); + test('with object config exported from CTS file', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + const config: Config = {displayName: 'ts-object-config', verbose: true}; + export default config; + `, + 'package.json': '{}', + }); + + const {configs, globalConfig} = getConfig(path.join(DIR)); + + expect(configs).toHaveLength(1); + expect(configs[0].displayName?.name).toBe('ts-object-config'); + expect(globalConfig.verbose).toBe(true); + }); + + test('with function config exported from CTS file', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + async function getVerbose() {return true;} + export default async (): Promise => { + const verbose: Config['verbose'] = await getVerbose(); + return {displayName: 'ts-async-function-config', verbose}; + }; + `, + 'package.json': '{}', + }); + + const {configs, globalConfig} = getConfig(path.join(DIR)); + + expect(configs).toHaveLength(1); + expect(configs[0].displayName?.name).toBe('ts-async-function-config'); + expect(globalConfig.verbose).toBe(true); + }); + test('throws if type errors are encountered', () => { writeFiles(DIR, { '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", @@ -248,6 +441,44 @@ describe('when `Config` type is imported from "jest"', () => { expect(exitCode).toBe(1); }); + test('throws if type errors are encountered with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + const config: Config = {testTimeout: '10000'}; + export default config; + `, + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(2,25): error TS2322: Type 'string' is not assignable to type 'number'.", + ); + expect(exitCode).toBe(1); + }); + + test('throws if syntax errors are encountered with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + const config: Config = {verbose: true}; + export default get config; + `, + 'package.json': '{}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(3,16): error TS2304: Cannot find name 'get'.", + ); + expect(exitCode).toBe(1); + }); + test('works with object config exported from TS file when package.json#type=module', () => { writeFiles(DIR, { '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", @@ -287,6 +518,45 @@ describe('when `Config` type is imported from "jest"', () => { expect(globalConfig.verbose).toBe(true); }); + test('works with object config exported from CTS file when package.json#type=module', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + const config: Config = {displayName: 'ts-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-esm-object-config'); + expect(globalConfig.verbose).toBe(true); + }); + + test('works with function config exported from CTS file when package.json#type=module', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + async function getVerbose() {return true;} + export default async (): Promise => { + const verbose: Config['verbose'] = await getVerbose(); + return {displayName: 'ts-esm-async-function-config', verbose}; + }; + `, + 'package.json': '{"type": "module"}', + }); + + const {configs, globalConfig} = getConfig(path.join(DIR)); + + expect(configs).toHaveLength(1); + expect(configs[0].displayName?.name).toBe('ts-esm-async-function-config'); + expect(globalConfig.verbose).toBe(true); + }); + test('throws if type errors are encountered when package.json#type=module', () => { writeFiles(DIR, { '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", @@ -324,4 +594,42 @@ describe('when `Config` type is imported from "jest"', () => { ); expect(exitCode).toBe(1); }); + + test('throws if type errors are encountered when package.json#type=module with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(12).toBe(12));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + const config: Config = {testTimeout: '10000'}; + export default config; + `, + 'package.json': '{"type": "module"}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(2,25): error TS2322: Type 'string' is not assignable to type 'number'.", + ); + expect(exitCode).toBe(1); + }); + + test('throws if syntax errors are encountered when package.json#type=module with CTS config', () => { + writeFiles(DIR, { + '__tests__/dummy.test.js': "test('dummy', () => expect(123).toBe(123));", + 'jest.config.cts': ` + import type {Config} from 'jest'; + const config: Config = {verbose: true}; + export default get config; + `, + 'package.json': '{"type": "module"}', + }); + + const {stderr, exitCode} = runJest(DIR); + + expect(stderr).toMatch( + "jest.config.cts(3,16): error TS2304: Cannot find name 'get'.", + ); + expect(exitCode).toBe(1); + }); }); diff --git a/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-cts/jest.config.cts b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-cts/jest.config.cts new file mode 100644 index 000000000000..4f69b4e3bda0 --- /dev/null +++ b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-cts/jest.config.cts @@ -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-cts/package.json b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-cts/package.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/create-jest/src/__tests__/__fixtures__/has-jest-config-file-cts/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 f209d032b847..20e00e7dab4b 100644 --- a/packages/create-jest/src/__tests__/__snapshots__/init.test.ts.snap +++ b/packages/create-jest/src/__tests__/__snapshots__/init.test.ts.snap @@ -18,6 +18,15 @@ Object { } `; +exports[`init has-jest-config-file-cts 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-js ask the user whether to override config or not user answered with "Yes" 1`] = ` Object { "initial": true, diff --git a/packages/jest-cli/src/__tests__/args.test.ts b/packages/jest-cli/src/__tests__/args.test.ts index b36c72d08f38..9cc7994205e1 100644 --- a/packages/jest-cli/src/__tests__/args.test.ts +++ b/packages/jest-cli/src/__tests__/args.test.ts @@ -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, .json', + 'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .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, .json'; + 'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .cts, .json'; expect(() => check(argv({config: 'jest.configjs'}))).toThrow(message); expect(() => check(argv({config: 'jest.config.exe'}))).toThrow(message); diff --git a/packages/jest-config/src/constants.ts b/packages/jest-config/src/constants.ts index 476874874ec7..63172c139250 100644 --- a/packages/jest-config/src/constants.ts +++ b/packages/jest-config/src/constants.ts @@ -15,11 +15,13 @@ export const JEST_CONFIG_EXT_CJS = '.cjs'; export const JEST_CONFIG_EXT_MJS = '.mjs'; export const JEST_CONFIG_EXT_JS = '.js'; export const JEST_CONFIG_EXT_TS = '.ts'; +export const JEST_CONFIG_EXT_CTS = '.cts'; export const JEST_CONFIG_EXT_JSON = '.json'; export const JEST_CONFIG_EXT_ORDER = Object.freeze([ JEST_CONFIG_EXT_JS, JEST_CONFIG_EXT_TS, JEST_CONFIG_EXT_MJS, 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 0b21f18c8038..84ec03e6ccc9 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -13,6 +13,7 @@ import type {Service} from 'ts-node'; import type {Config} from '@jest/types'; import {interopRequireDefault, requireOrImportModule} from 'jest-util'; import { + JEST_CONFIG_EXT_CTS, JEST_CONFIG_EXT_JSON, JEST_CONFIG_EXT_TS, PACKAGE_JSON, @@ -26,7 +27,9 @@ import { export default async function readConfigFileAndSetRootDir( configPath: string, ): Promise { - const isTS = configPath.endsWith(JEST_CONFIG_EXT_TS); + const isTS = + configPath.endsWith(JEST_CONFIG_EXT_TS) || + configPath.endsWith(JEST_CONFIG_EXT_CTS); const isJSON = configPath.endsWith(JEST_CONFIG_EXT_JSON); let configObject;