Skip to content

Commit d336118

Browse files
authored
feat(config): if process.features.typescript is set, load jest.config.ts without external loader (#15480)
if `process.features.typescript` is set, load `jest.config.ts` without external loader see https://nodejs.org/api/process.html#processfeaturestypescript This value is set if node is run with `--experimental-transform-types` or `--experimental-strip-types` so no external package is required for transpilation in those cases
1 parent 7ea9a40 commit d336118

File tree

6 files changed

+130
-64
lines changed

6 files changed

+130
-64
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
- `[jest-config]` Allow loading `jest.config.cts` files ([#14070](https://github.com/facebook/jest/pull/14070))
1616
- `[jest-config]` Show `rootDir` in error message when a `preset` fails to load ([#15194](https://github.com/jestjs/jest/pull/15194))
1717
- `[jest-config]` Support loading TS config files using `esbuild-register` via docblock loader ([#15190](https://github.com/jestjs/jest/pull/15190))
18-
- `[jest-config]` allow passing TS config loader options via docblock comment ([#15234](https://github.com/jestjs/jest/pull/15234))
18+
- `[jest-config]` Allow passing TS config loader options via docblock comment ([#15234](https://github.com/jestjs/jest/pull/15234))
19+
- `[jest-config]` If Node is running with type stripping enabled, do not require a TS loader ([#15480](https://github.com/jestjs/jest/pull/15480))
1920
- `[@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))
2021
- `[@jest/core]` Add `perfStats` to surface test setup overhead ([#14622](https://github.com/jestjs/jest/pull/14622))
2122
- `[@jest/core]` [**BREAKING**] Changed `--filter` to accept an object with shape `{ filtered: Array<string> }` to match [documentation](https://jestjs.io/docs/cli#--filterfile) ([#13319](https://github.com/jestjs/jest/pull/13319))

e2e/__tests__/__snapshots__/jest.config.ts.test.ts.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`on node >=23.6 invalid JS in jest.config.ts (node with native TS support) 1`] = `
4+
"Error: Jest: Failed to parse the TypeScript config file <<REPLACED>>
5+
SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: x Expected ';', got 'string literal (ll break this file yo, 'll break this file yo)'
6+
,----
7+
1 | export default i'll break this file yo
8+
: ^^^^^^^^^^^^^^^^^^^^^^
9+
\`----
10+
x Unterminated string constant
11+
,----
12+
1 | export default i'll break this file yo
13+
: ^^^^^^^^^^^^^^^^^^^^^^
14+
\`----"
15+
`;
16+
317
exports[`traverses directory tree up until it finds jest.config 1`] = `
418
" console.log
519
<<REPLACED>>/jest-config-ts/some/nested/directory

e2e/__tests__/jest.config.ts.test.ts

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import * as path from 'path';
99
import * as fs from 'graceful-fs';
10+
import {onNodeVersions} from '@jest/test-utils';
1011
import {cleanup, extractSummary, writeFiles} from '../Utils';
1112
import runJest from '../runJest';
1213

@@ -23,7 +24,9 @@ test('works with jest.config.ts', () => {
2324
'package.json': '{}',
2425
});
2526

26-
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
27+
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], {
28+
nodeOptions: '--no-warnings',
29+
});
2730
const {rest, summary} = extractSummary(stderr);
2831
expect(exitCode).toBe(0);
2932
expect(rest).toMatchSnapshot();
@@ -39,7 +42,9 @@ test('works with tsconfig.json', () => {
3942
'tsconfig.json': '{ "compilerOptions": { "module": "esnext" } }',
4043
});
4144

42-
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
45+
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], {
46+
nodeOptions: '--no-warnings',
47+
});
4348
const {rest, summary} = extractSummary(stderr);
4449
expect(exitCode).toBe(0);
4550
expect(rest).toMatchSnapshot();
@@ -62,62 +67,95 @@ test('traverses directory tree up until it finds jest.config', () => {
6267
const {stderr, exitCode, stdout} = runJest(
6368
path.join(DIR, 'some', 'nested', 'directory'),
6469
['-w=1', '--ci=false'],
65-
{skipPkgJsonCheck: true},
70+
{nodeOptions: '--no-warnings', skipPkgJsonCheck: true},
6671
);
6772

6873
// Snapshot the console.logged `process.cwd()` and make sure it stays the same
69-
expect(stdout.replaceAll(/^\W+(.*)e2e/gm, '<<REPLACED>>')).toMatchSnapshot();
74+
expect(
75+
stdout
76+
.replaceAll(/^\W+(.*)e2e/gm, '<<REPLACED>>')
77+
// slightly different log in node versions >= 23
78+
.replace('at Object.log', 'at Object.<anonymous>'),
79+
).toMatchSnapshot();
7080

7181
const {rest, summary} = extractSummary(stderr);
7282
expect(exitCode).toBe(0);
7383
expect(rest).toMatchSnapshot();
7484
expect(summary).toMatchSnapshot();
7585
});
7686

77-
const jestPath = require.resolve('jest');
78-
const jestTypesPath = jestPath.replace(/\.js$/, '.d.ts');
79-
const jestTypesExists = fs.existsSync(jestTypesPath);
87+
onNodeVersions('<23.6', () => {
88+
const jestPath = require.resolve('jest');
89+
const jestTypesPath = jestPath.replace(/\.js$/, '.d.ts');
90+
const jestTypesExists = fs.existsSync(jestTypesPath);
91+
92+
(jestTypesExists ? test : test.skip).each([true, false])(
93+
'check the config disabled (skip type check: %p)',
94+
skipTypeCheck => {
95+
writeFiles(DIR, {
96+
'__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));",
97+
'jest.config.ts': `
98+
/**@jest-config-loader-options {"transpileOnly":${!!skipTypeCheck}}*/
99+
import {Config} from 'jest';
100+
const config: Config = { testTimeout: "10000" };
101+
export default config;
102+
`,
103+
'package.json': '{}',
104+
});
105+
106+
const typeErrorString =
107+
"TS2322: Type 'string' is not assignable to type 'number'.";
108+
const runtimeErrorString = 'Option "testTimeout" must be of type:';
109+
110+
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
111+
112+
if (skipTypeCheck) {
113+
expect(stderr).not.toMatch(typeErrorString);
114+
expect(stderr).toMatch(runtimeErrorString);
115+
} else {
116+
expect(stderr).toMatch(typeErrorString);
117+
expect(stderr).not.toMatch(runtimeErrorString);
118+
}
119+
120+
expect(exitCode).toBe(1);
121+
},
122+
);
80123

81-
(jestTypesExists ? test : test.skip).each([true, false])(
82-
'check the config disabled (skip type check: %p)',
83-
skipTypeCheck => {
124+
test('invalid JS in jest.config.ts', () => {
84125
writeFiles(DIR, {
85126
'__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));",
86-
'jest.config.ts': `
87-
/**@jest-config-loader-options {"transpileOnly":${!!skipTypeCheck}}*/
88-
import {Config} from 'jest';
89-
const config: Config = { testTimeout: "10000" };
90-
export default config;
91-
`,
127+
'jest.config.ts': "export default i'll break this file yo",
92128
'package.json': '{}',
93129
});
94130

95-
const typeErrorString =
96-
"TS2322: Type 'string' is not assignable to type 'number'.";
97-
const runtimeErrorString = 'Option "testTimeout" must be of type:';
98-
99131
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
132+
expect(stderr).toMatch('TSError: ⨯ Unable to compile TypeScript:');
133+
expect(exitCode).toBe(1);
134+
});
135+
});
100136

101-
if (skipTypeCheck) {
102-
expect(stderr).not.toMatch(typeErrorString);
103-
expect(stderr).toMatch(runtimeErrorString);
104-
} else {
105-
expect(stderr).toMatch(typeErrorString);
106-
expect(stderr).not.toMatch(runtimeErrorString);
107-
}
137+
onNodeVersions('>=23.6', () => {
138+
test('invalid JS in jest.config.ts (node with native TS support)', () => {
139+
writeFiles(DIR, {
140+
'__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));",
141+
'jest.config.ts': "export default i'll break this file yo",
142+
'package.json': '{}',
143+
});
108144

145+
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false'], {
146+
nodeOptions: '--no-warnings',
147+
});
148+
expect(
149+
stderr
150+
// Remove the stack trace from the error message
151+
.slice(0, Math.max(0, stderr.indexOf('Caused by')))
152+
.trim()
153+
// Replace the path to the config file with a placeholder
154+
.replace(
155+
/(Error: Jest: Failed to parse the TypeScript config file).*$/m,
156+
'$1 <<REPLACED>>',
157+
),
158+
).toMatchSnapshot();
109159
expect(exitCode).toBe(1);
110-
},
111-
);
112-
113-
test('invalid JS in jest.config.ts', () => {
114-
writeFiles(DIR, {
115-
'__tests__/a-giraffe.js': "test('giraffe', () => expect(1).toBe(1));",
116-
'jest.config.ts': "export default i'll break this file yo",
117-
'package.json': '{}',
118160
});
119-
120-
const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
121-
expect(stderr).toMatch('TSError: ⨯ Unable to compile TypeScript:');
122-
expect(exitCode).toBe(1);
123161
});

e2e/__tests__/readInitialOptions.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
import path = require('path');
88
import execa = require('execa');
9+
import {onNodeVersions} from '@jest/test-utils';
910
import type {ReadJestConfigOptions, readInitialOptions} from 'jest-config';
1011

1112
function resolveFixture(...pathSegments: Array<string>) {
@@ -88,14 +89,16 @@ describe('readInitialOptions', () => {
8889
expect(configPath).toEqual(expectedConfigFile);
8990
});
9091

91-
test('should give an error when using unsupported loader', async () => {
92-
const cwd = resolveFixture('ts-loader-config');
93-
const error: Error = await proxyReadInitialOptions(undefined, {cwd}).catch(
94-
error => error,
95-
);
96-
expect(error.message).toContain(
97-
"Jest: 'ts-loader' is not a valid TypeScript configuration loader.",
98-
);
92+
onNodeVersions('<22.6', () => {
93+
test('should give an error when using unsupported loader', async () => {
94+
const cwd = resolveFixture('ts-loader-config');
95+
const error: Error = await proxyReadInitialOptions(undefined, {
96+
cwd,
97+
}).catch(error => error);
98+
expect(error.message).toContain(
99+
"Jest: 'ts-loader' is not a valid TypeScript configuration loader.",
100+
);
101+
});
99102
});
100103

101104
test('should give an error when there are multiple config files', async () => {

e2e/__tests__/typescriptConfigFile.test.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77

88
import {tmpdir} from 'os';
99
import * as path from 'path';
10+
import * as semver from 'semver';
11+
import {onNodeVersions} from '@jest/test-utils';
1012
import {cleanup, writeFiles} from '../Utils';
1113
import runJest, {getConfig} from '../runJest';
1214

1315
const DIR = path.resolve(tmpdir(), 'typescript-config-file');
16+
const useNativeTypeScript = semver.satisfies(process.versions.node, '>=23.6.0');
17+
const importFileExtension = useNativeTypeScript ? '.ts' : '';
1418

1519
beforeEach(() => cleanup(DIR));
1620
afterEach(() => cleanup(DIR));
@@ -20,7 +24,7 @@ test('works with single typescript config that imports something', () => {
2024
'__tests__/mytest.alpha.js': "test('alpha', () => expect(1).toBe(1));",
2125
'__tests__/mytest.common.js': "test('common', () => expect(1).toBe(1));",
2226
'alpha.config.ts': `
23-
import commonRegex from './common';
27+
import commonRegex from './common${importFileExtension}';
2428
export default {
2529
testRegex: [ commonRegex, '__tests__/mytest.alpha.js' ]
2630
};`,
@@ -77,12 +81,12 @@ test('works with multiple typescript configs that import something', () => {
7781
'__tests__/mytest.beta.js': "test('beta', () => expect(1).toBe(1));",
7882
'__tests__/mytest.common.js': "test('common', () => expect(1).toBe(1));",
7983
'alpha.config.ts': `
80-
import commonRegex from './common';
84+
import commonRegex from './common${importFileExtension}';
8185
export default {
8286
testRegex: [ commonRegex, '__tests__/mytest.alpha.js' ]
8387
};`,
8488
'beta.config.ts': `
85-
import commonRegex from './common';
89+
import commonRegex from './common${importFileExtension}';
8690
export default {
8791
testRegex: [ commonRegex, '__tests__/mytest.beta.js' ]
8892
};`,
@@ -108,18 +112,20 @@ test('works with multiple typescript configs that import something', () => {
108112
expect(stdout).toBe('');
109113
});
110114

111-
test("works with single typescript config that does not import anything with project's moduleResolution set to Node16", () => {
112-
const {configs} = getConfig(
113-
'typescript-config/modern-module-resolution',
114-
[],
115-
{
116-
skipPkgJsonCheck: true,
117-
},
118-
);
115+
onNodeVersions('<23.6', () => {
116+
test("works with single typescript config that does not import anything with project's moduleResolution set to Node16", () => {
117+
const {configs} = getConfig(
118+
'typescript-config/modern-module-resolution',
119+
[],
120+
{
121+
skipPkgJsonCheck: true,
122+
},
123+
);
119124

120-
expect(configs).toHaveLength(1);
121-
expect(configs[0].displayName).toEqual({
122-
color: 'white',
123-
name: 'Config from modern ts file',
125+
expect(configs).toHaveLength(1);
126+
expect(configs[0].displayName).toEqual({
127+
color: 'white',
128+
name: 'Config from modern ts file',
129+
});
124130
});
125131
});

packages/jest-config/src/readConfigFileAndSetRootDir.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ export default async function readConfigFileAndSetRootDir(
3636
configPath.endsWith(JEST_CONFIG_EXT_TS) ||
3737
configPath.endsWith(JEST_CONFIG_EXT_CTS);
3838
const isJSON = configPath.endsWith(JEST_CONFIG_EXT_JSON);
39+
// type assertion can be removed once @types/node is updated
40+
// https://nodejs.org/api/process.html#processfeaturestypescript
41+
const supportsTS = (process.features as {typescript?: boolean | string})
42+
.typescript;
3943
let configObject;
4044

4145
try {
42-
if (isTS) {
46+
if (isTS && !supportsTS) {
4347
configObject = await loadTSConfigFile(configPath);
4448
} else if (isJSON) {
4549
const fileContent = fs.readFileSync(configPath, 'utf8');

0 commit comments

Comments
 (0)