Skip to content

Commit 0eb980f

Browse files
authored
Fix implicit config on old ts versions (#2064)
* Fix bug where ts-node would try to use a @tsconfig/bases that is incompatible with the user's very old version of TS * add ts 4.7 to test matrix * adjust implicit tsconfig tests to understand and expect fallback to empty defaults when *none* of the tsconfig/bases are compatible * fix useDefineForClassFields to be immune to implicit config conditionally skip cases that use target incompatible with ts version Add useDefineForClassFields test cases for es5 emit * use more robust compatibility check: ask ts API to parse the options, check for errors * Fix failing test where I guess swc decided to start quoting import strings differently? * make more tests immune to blank implicit config * jk about that quoting change :/ * fix test bug
1 parent 0022f38 commit 0eb980f

File tree

4 files changed

+160
-51
lines changed

4 files changed

+160
-51
lines changed

.github/workflows/continuous-integration.yml

+10-5
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
matrix:
6464
os: [ubuntu, windows]
6565
# Don't forget to add all new flavors to this list!
66-
flavor: [1, 2, 3, 4, 5, 6]
66+
flavor: [1, 2, 3, 4, 5, 6, 7]
6767
include:
6868
# Node 16
6969
- flavor: 1
@@ -76,25 +76,30 @@ jobs:
7676
nodeFlag: 16
7777
typescript: 4.4
7878
typescriptFlag: 4_4
79-
# Node 18
8079
- flavor: 3
80+
node: 16
81+
nodeFlag: 16
82+
typescript: 4.7
83+
typescriptFlag: 4_7
84+
# Node 18
85+
- flavor: 4
8186
node: 18
8287
nodeFlag: 18
8388
typescript: latest
8489
typescriptFlag: latest
85-
- flavor: 4
90+
- flavor: 5
8691
node: 18
8792
nodeFlag: 18
8893
typescript: next
8994
typescriptFlag: next
9095
# Node 20
91-
- flavor: 5
96+
- flavor: 6
9297
node: 20
9398
nodeFlag: 20
9499
typescript: latest
95100
typescriptFlag: latest
96101
# Node nightly
97-
- flavor: 6
102+
- flavor: 7
98103
node: 21-nightly
99104
nodeFlag: 21_nightly
100105
typescript: latest

src/test/transpilers.spec.ts

+90-22
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
TEST_DIR,
1212
tsSupportsImportAssertions,
1313
tsSupportsReact17JsxFactories,
14+
tsSupportsEs2022,
1415
} from './helpers';
1516
import { createSwcOptions } from '../transpilers/swc';
1617
import * as expect from 'expect';
@@ -101,15 +102,15 @@ test.suite('swc', (test) => {
101102

102103
test(
103104
compileMacro,
104-
{ module: 'esnext', jsx: 'react' },
105+
{ module: 'esnext', target: 'es2020', jsx: 'react' },
105106
input,
106107
`const div = /*#__PURE__*/ React.createElement("div", null);`
107108
);
108109
test.suite('react 17 jsx factories', (test) => {
109110
test.if(tsSupportsReact17JsxFactories);
110111
test(
111112
compileMacro,
112-
{ module: 'esnext', jsx: 'react-jsx' },
113+
{ module: 'esnext', target: 'es2020', jsx: 'react-jsx' },
113114
input,
114115
outdent`
115116
import { jsx as _jsx } from "react/jsx-runtime";
@@ -118,7 +119,7 @@ test.suite('swc', (test) => {
118119
);
119120
test(
120121
compileMacro,
121-
{ module: 'esnext', jsx: 'react-jsxdev' },
122+
{ module: 'esnext', target: 'es2020', jsx: 'react-jsxdev' },
122123
input,
123124
outdent`
124125
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
@@ -169,6 +170,18 @@ test.suite('swc', (test) => {
169170
}
170171
};
171172
`;
173+
const outputCtorAssignmentInEs5Class = outdent`
174+
function _class_call_check(instance, Constructor) {
175+
if (!(instance instanceof Constructor)) {
176+
throw new TypeError("Cannot call a class as a function");
177+
}
178+
}
179+
var Foo = function Foo() {
180+
"use strict";
181+
_class_call_check(this, Foo);
182+
this.bar = 1;
183+
};
184+
`;
172185
const outputDefine = outdent`
173186
function _define_property(obj, key, value) {
174187
if (key in obj) {
@@ -189,19 +202,44 @@ test.suite('swc', (test) => {
189202
}
190203
};
191204
`;
205+
const outputDefineInEs5Class = outdent`
206+
function _class_call_check(instance, Constructor) {
207+
if (!(instance instanceof Constructor)) {
208+
throw new TypeError("Cannot call a class as a function");
209+
}
210+
}
211+
function _define_property(obj, key, value) {
212+
if (key in obj) {
213+
Object.defineProperty(obj, key, {
214+
value: value,
215+
enumerable: true,
216+
configurable: true,
217+
writable: true
218+
});
219+
} else {
220+
obj[key] = value;
221+
}
222+
return obj;
223+
}
224+
var Foo = function Foo() {
225+
"use strict";
226+
_class_call_check(this, Foo);
227+
_define_property(this, "bar", 1);
228+
};
229+
`;
192230
test(
193-
'useDefineForClassFields unset, should default to true and emit native property assignment b/c `next` target',
231+
'useDefineForClassFields unset, `next` target, should default to true and emit native property assignment',
194232
compileMacro,
195233
{ module: 'esnext', target: 'ESNext' },
196234
input,
197235
outputNative
198236
);
199-
test(
200-
'useDefineForClassFields unset, should default to true and emit native property assignment b/c new target',
201-
compileMacro,
202-
{ module: 'esnext', target: 'ES2022' },
203-
input,
204-
outputNative
237+
test.suite(
238+
'useDefineForClassFields unset, new target, should default to true and emit native property assignment',
239+
(test) => {
240+
test.if(tsSupportsEs2022);
241+
test(compileMacro, { module: 'esnext', target: 'ES2022' }, input, outputNative);
242+
}
205243
);
206244
test(
207245
'useDefineForClassFields unset, should default to false b/c old target',
@@ -210,48 +248,78 @@ test.suite('swc', (test) => {
210248
input,
211249
outputCtorAssignment
212250
);
251+
test.suite('useDefineForClassFields=true, new target, should emit native property assignment', (test) => {
252+
test.if(tsSupportsEs2022);
253+
test(
254+
compileMacro,
255+
{
256+
module: 'esnext',
257+
target: 'ES2022',
258+
useDefineForClassFields: true,
259+
},
260+
input,
261+
outputNative
262+
);
263+
});
213264
test(
214-
'useDefineForClassFields=true, should emit native property assignment b/c new target',
265+
'useDefineForClassFields=true, old target, should emit define',
215266
compileMacro,
216267
{
217268
module: 'esnext',
269+
target: 'ES2021',
218270
useDefineForClassFields: true,
219-
target: 'ES2022',
220271
},
221272
input,
222-
outputNative
273+
outputDefine
274+
);
275+
test.suite(
276+
'useDefineForClassFields=false, new target, should still emit legacy property assignment in ctor',
277+
(test) => {
278+
test.if(tsSupportsEs2022);
279+
test(
280+
compileMacro,
281+
{
282+
module: 'esnext',
283+
target: 'ES2022',
284+
useDefineForClassFields: false,
285+
},
286+
input,
287+
outputCtorAssignment
288+
);
289+
}
223290
);
224291
test(
225-
'useDefineForClassFields=true, should emit define b/c old target',
292+
'useDefineForClassFields=false, old target, should emit legacy property assignment in ctor',
226293
compileMacro,
227294
{
228295
module: 'esnext',
229-
useDefineForClassFields: true,
230296
target: 'ES2021',
297+
useDefineForClassFields: false,
231298
},
232299
input,
233-
outputDefine
300+
outputCtorAssignment
234301
);
235302
test(
236-
'useDefineForClassFields=false, new target, should still emit legacy property assignment in ctor',
303+
'useDefineForClassFields=false, ancient target, should emit legacy property assignment in legacy function-based class',
237304
compileMacro,
238305
{
239306
module: 'esnext',
307+
target: 'es5',
240308
useDefineForClassFields: false,
241-
target: 'ES2022',
242309
},
243310
input,
244-
outputCtorAssignment
311+
outputCtorAssignmentInEs5Class
245312
);
246313
test(
247-
'useDefineForClassFields=false, old target, should emit legacy property assignment in ctor',
314+
'useDefineForClassFields=true, ancient target, should emit define in legacy function-based class',
248315
compileMacro,
249316
{
250317
module: 'esnext',
251-
useDefineForClassFields: false,
318+
target: 'es5',
319+
useDefineForClassFields: true,
252320
},
253321
input,
254-
outputCtorAssignment
322+
outputDefineInEs5Class
255323
);
256324
});
257325

src/test/tsconfig-bases.spec.ts

+24-19
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { createExec } from './helpers/exec';
33
import { ctxTmpDirOutsideCheckout } from './helpers/ctx-tmp-dir';
44
import { ctxTsNode } from './helpers/ctx-ts-node';
55
import { BIN_PATH, TEST_DIR } from './helpers/paths';
6-
import { tsSupportsEs2021, tsSupportsEs2022, tsSupportsLibEs2023 } from './helpers/version-checks';
6+
import {
7+
tsSupportsEs2021,
8+
tsSupportsEs2022,
9+
tsSupportsLibEs2023,
10+
tsSupportsStableNodeNextNode16,
11+
} from './helpers/version-checks';
712
import { context, expect } from './testlib';
813
import semver = require('semver');
914
import { testsDirRequire } from './helpers';
@@ -17,14 +22,21 @@ const test = context(ctxTsNode);
1722
test.suite('should use implicit @tsconfig/bases config when one is not loaded from disk', ({ contextEach }) => {
1823
const test = contextEach(ctxTmpDirOutsideCheckout);
1924

20-
let lib = 'es2020';
21-
let target = 'es2020';
22-
if (semver.gte(process.versions.node, '16.0.0') && tsSupportsEs2021) {
23-
lib = target = 'es2021';
24-
}
25-
if (semver.gte(process.versions.node, '18.0.0') && tsSupportsEs2022 && tsSupportsLibEs2023) {
26-
target = 'es2022';
27-
lib = 'es2023';
25+
// Expectations change depending on node and TS version, since ts-node picks an implicit config that is compatible
26+
// with both.
27+
let lib: Array<string> | undefined = undefined;
28+
let target: string = 'es5';
29+
if (tsSupportsStableNodeNextNode16) {
30+
lib = ['es2020'];
31+
target = 'es2020';
32+
if (semver.gte(process.versions.node, '16.0.0') && tsSupportsEs2021) {
33+
lib = ['es2021'];
34+
target = 'es2021';
35+
}
36+
if (semver.gte(process.versions.node, '18.0.0') && tsSupportsEs2022 && tsSupportsLibEs2023) {
37+
lib = ['es2023'];
38+
target = 'es2022';
39+
}
2840
}
2941

3042
test('implicitly uses @tsconfig/node14, @tsconfig/node16, @tsconfig/node18, or @tsconfig/node20 compilerOptions when both TS and node versions support it', async (t) => {
@@ -36,16 +48,9 @@ test.suite('should use implicit @tsconfig/bases config when one is not loaded fr
3648
t.like(JSON.parse(r1.stdout), {
3749
compilerOptions: {
3850
target,
39-
lib: [lib],
51+
lib,
4052
},
4153
});
42-
43-
const r2 = await exec(`${BIN_PATH} -pe 10n`, {
44-
cwd: t.context.tmpDir,
45-
});
46-
47-
expect(r2.err).toBe(null);
48-
expect(r2.stdout).toBe('10n\n');
4954
});
5055

5156
test('implicitly loads @types/node even when not installed within local directory', async (t) => {
@@ -73,8 +78,8 @@ test.suite('should use implicit @tsconfig/bases config when one is not loaded fr
7378
});
7479

7580
test.suite('should bundle @tsconfig/bases to be used in your own tsconfigs', (test) => {
76-
// Older TS versions will complain about newer `target` and `lib` options
77-
test.if(tsSupportsEs2022 && tsSupportsLibEs2023);
81+
// Older TS versions will complain about newer `target`, `lib`, `module`, `moduleResolution` options
82+
test.if(tsSupportsEs2022 && tsSupportsLibEs2023 && tsSupportsStableNodeNextNode16);
7883

7984
const macro = test.macro((nodeVersion: string) => async (t) => {
8085
const config = testsDirRequire(`@tsconfig/${nodeVersion}/tsconfig.json`);

src/tsconfigs.ts

+36-5
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,50 @@ export function getDefaultTsconfigJsonForNodeVersion(ts: TSCommon): any {
2020
const config = require('@tsconfig/node16/tsconfig.json');
2121
if (configCompatible(config)) return config;
2222
}
23-
return require('@tsconfig/node14/tsconfig.json');
23+
{
24+
const config = require('@tsconfig/node14/tsconfig.json');
25+
if (configCompatible(config)) return config;
26+
}
27+
// Old TypeScript compilers may be incompatible with *all* @tsconfig/node* configs,
28+
// so fallback to nothing
29+
return {};
2430

2531
// Verify that tsconfig target and lib options are compatible with TypeScript compiler
2632
function configCompatible(config: {
2733
compilerOptions: {
2834
lib: string[];
2935
target: string;
36+
module: string;
37+
moduleResolution: string;
3038
};
3139
}) {
32-
return (
33-
typeof (ts.ScriptTarget as any)[config.compilerOptions.target.toUpperCase()] === 'number' &&
34-
tsInternal.libs &&
35-
config.compilerOptions.lib.every((lib) => tsInternal.libs!.includes(lib))
40+
const results = ts.parseJsonConfigFileContent(
41+
{
42+
compilerOptions: config.compilerOptions,
43+
files: ['foo.ts'],
44+
},
45+
parseConfigHost,
46+
''
3647
);
48+
return results.errors.length === 0;
3749
}
3850
}
51+
52+
const parseConfigHost = {
53+
useCaseSensitiveFileNames: false,
54+
readDirectory(
55+
rootDir: string,
56+
extensions: readonly string[],
57+
excludes: readonly string[] | undefined,
58+
includes: readonly string[],
59+
depth?: number
60+
) {
61+
return [];
62+
},
63+
fileExists(path: string) {
64+
return false;
65+
},
66+
readFile(path: string) {
67+
return '';
68+
},
69+
};

0 commit comments

Comments
 (0)