tstyche 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +10 -0
- package/README.md +80 -0
- package/build/bin.js +5 -0
- package/build/index.d.ts +276 -0
- package/build/index.js +12 -0
- package/build/tstyche.d.ts +679 -0
- package/build/tstyche.js +3550 -0
- package/lib/schema.json +47 -0
- package/package.json +128 -0
package/build/tstyche.js
ADDED
|
@@ -0,0 +1,3550 @@
|
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { setInterval } from 'node:timers/promises';
|
|
9
|
+
import https from 'node:https';
|
|
10
|
+
|
|
11
|
+
class EventEmitter {
|
|
12
|
+
static #handlers = new Set();
|
|
13
|
+
static addHandler(handler) {
|
|
14
|
+
EventEmitter.#handlers.add(handler);
|
|
15
|
+
}
|
|
16
|
+
static dispatch(event) {
|
|
17
|
+
for (const handler of EventEmitter.#handlers) {
|
|
18
|
+
handler(event);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
static removeHandler(handler) {
|
|
22
|
+
EventEmitter.#handlers.delete(handler);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class Environment {
|
|
27
|
+
static #noColor = Environment.#resolveNoColor();
|
|
28
|
+
static #storePath = Environment.#normalizePath(Environment.#resolveStorePath());
|
|
29
|
+
static #timeout = Environment.#resolveTimeout();
|
|
30
|
+
static get noColor() {
|
|
31
|
+
return Environment.#noColor;
|
|
32
|
+
}
|
|
33
|
+
static get storePath() {
|
|
34
|
+
return Environment.#storePath;
|
|
35
|
+
}
|
|
36
|
+
static get timeout() {
|
|
37
|
+
return Environment.#timeout;
|
|
38
|
+
}
|
|
39
|
+
static #normalizePath(value) {
|
|
40
|
+
if (path.sep === "/") {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return value.replace(/\\/g, "/");
|
|
44
|
+
}
|
|
45
|
+
static #parseBoolean(value) {
|
|
46
|
+
if (value != null) {
|
|
47
|
+
return ["1", "on", "t", "true", "y", "yes"].includes(value.toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
static #resolveNoColor() {
|
|
52
|
+
if (process.env["TSTYCHE_NO_COLOR"] != null) {
|
|
53
|
+
return Environment.#parseBoolean(process.env["TSTYCHE_NO_COLOR"]);
|
|
54
|
+
}
|
|
55
|
+
if (process.env["NO_COLOR"] != null && process.env["NO_COLOR"] !== "") {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
static #resolveStorePath() {
|
|
61
|
+
if (process.env["TSTYCHE_STORE_PATH"] != null) {
|
|
62
|
+
return path.resolve(process.env["TSTYCHE_STORE_PATH"]);
|
|
63
|
+
}
|
|
64
|
+
if (process.platform === "darwin") {
|
|
65
|
+
return path.resolve(os.homedir(), "Library", "TSTyche");
|
|
66
|
+
}
|
|
67
|
+
if (process.env["LocalAppData"] != null) {
|
|
68
|
+
return path.resolve(process.env["LocalAppData"], "TSTyche");
|
|
69
|
+
}
|
|
70
|
+
if (process.env["XDG_DATA_HOME"] != null) {
|
|
71
|
+
return path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
|
|
72
|
+
}
|
|
73
|
+
return path.resolve(os.homedir(), ".local", "share", "TSTyche");
|
|
74
|
+
}
|
|
75
|
+
static #resolveTimeout() {
|
|
76
|
+
if (process.env["TSTYCHE_TIMEOUT"] != null) {
|
|
77
|
+
return Number(process.env["TSTYCHE_TIMEOUT"]);
|
|
78
|
+
}
|
|
79
|
+
return 30;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var Color;
|
|
84
|
+
(function (Color) {
|
|
85
|
+
Color["Reset"] = "0";
|
|
86
|
+
Color["Red"] = "31";
|
|
87
|
+
Color["Green"] = "32";
|
|
88
|
+
Color["Yellow"] = "33";
|
|
89
|
+
Color["Blue"] = "34";
|
|
90
|
+
Color["Magenta"] = "35";
|
|
91
|
+
Color["Cyan"] = "36";
|
|
92
|
+
Color["Gray"] = "90";
|
|
93
|
+
})(Color || (Color = {}));
|
|
94
|
+
|
|
95
|
+
class Scribbler {
|
|
96
|
+
#noColor;
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.#noColor = options?.noColors ?? false;
|
|
99
|
+
}
|
|
100
|
+
static createElement(type, props, ...children) {
|
|
101
|
+
return {
|
|
102
|
+
$$typeof: Symbol.for("tstyche:scribbler"),
|
|
103
|
+
children: children.length > 1 ? children : children[0],
|
|
104
|
+
props,
|
|
105
|
+
type,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
#escapeSequence(attributes) {
|
|
109
|
+
return ["\x1b[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
|
|
110
|
+
}
|
|
111
|
+
#indentEachLine(lines, level) {
|
|
112
|
+
const indentStep = " ";
|
|
113
|
+
const notEmptyLineRegExp = /^(?!$)/gm;
|
|
114
|
+
return lines.replace(notEmptyLineRegExp, indentStep.repeat(level));
|
|
115
|
+
}
|
|
116
|
+
render(element) {
|
|
117
|
+
if (element != null) {
|
|
118
|
+
if (typeof element.type === "function") {
|
|
119
|
+
const instance = new element.type({
|
|
120
|
+
...element.props,
|
|
121
|
+
children: element.children,
|
|
122
|
+
});
|
|
123
|
+
return this.render(instance.render());
|
|
124
|
+
}
|
|
125
|
+
if (element.type === "ansi" && !this.#noColor) {
|
|
126
|
+
const flags = typeof element.props?.["escapes"] === "string" || Array.isArray(element.props?.["escapes"])
|
|
127
|
+
? element.props?.["escapes"]
|
|
128
|
+
: undefined;
|
|
129
|
+
if (flags != null) {
|
|
130
|
+
return this.#escapeSequence(flags);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (element.type === "newline") {
|
|
134
|
+
return "\r\n";
|
|
135
|
+
}
|
|
136
|
+
if (element.type === "text") {
|
|
137
|
+
const indentLevel = typeof element.props?.["indent"] === "number" ? element.props["indent"] : 0;
|
|
138
|
+
let text = this.#visitElementChildren(element.children);
|
|
139
|
+
if (indentLevel > 0) {
|
|
140
|
+
text = this.#indentEachLine(text, indentLevel);
|
|
141
|
+
}
|
|
142
|
+
return text;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
#visitElementChildren(children) {
|
|
148
|
+
if (typeof children === "string") {
|
|
149
|
+
return children;
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(children)) {
|
|
152
|
+
const text = [];
|
|
153
|
+
for (const child of children) {
|
|
154
|
+
if (typeof child === "string") {
|
|
155
|
+
text.push(child);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(child)) {
|
|
159
|
+
text.push(this.#visitElementChildren(child));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (child != null && typeof child === "object") {
|
|
163
|
+
text.push(this.render(child));
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return text.join("");
|
|
168
|
+
}
|
|
169
|
+
if (children != null && typeof children === "object") {
|
|
170
|
+
return this.render(children);
|
|
171
|
+
}
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class Text {
|
|
177
|
+
props;
|
|
178
|
+
constructor(props) {
|
|
179
|
+
this.props = props;
|
|
180
|
+
}
|
|
181
|
+
render() {
|
|
182
|
+
const ansiEscapes = [];
|
|
183
|
+
if (this.props.color != null) {
|
|
184
|
+
ansiEscapes.push(this.props.color);
|
|
185
|
+
}
|
|
186
|
+
return (Scribbler.createElement("text", { indent: this.props.indent },
|
|
187
|
+
ansiEscapes.length > 0 ? Scribbler.createElement("ansi", { escapes: ansiEscapes }) : undefined,
|
|
188
|
+
this.props.children,
|
|
189
|
+
ansiEscapes.length > 0 ? Scribbler.createElement("ansi", { escapes: "0" }) : undefined));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
class Line {
|
|
194
|
+
props;
|
|
195
|
+
constructor(props) {
|
|
196
|
+
this.props = props;
|
|
197
|
+
}
|
|
198
|
+
render() {
|
|
199
|
+
return (Scribbler.createElement("text", null,
|
|
200
|
+
Scribbler.createElement(Text, { color: this.props.color, indent: this.props.indent }, this.props.children),
|
|
201
|
+
Scribbler.createElement("newline", null)));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function addsPackageStepText(compilerVersion, installationPath) {
|
|
206
|
+
return (Scribbler.createElement(Line, null,
|
|
207
|
+
Scribbler.createElement(Text, { color: "90" }, "adds"),
|
|
208
|
+
" TypeScript ",
|
|
209
|
+
compilerVersion,
|
|
210
|
+
Scribbler.createElement(Text, { color: "90" },
|
|
211
|
+
" to ",
|
|
212
|
+
installationPath)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function describeNameText(name, indent = 0) {
|
|
216
|
+
return Scribbler.createElement(Line, { indent: indent + 1 }, name);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
class RelativePathText {
|
|
220
|
+
props;
|
|
221
|
+
constructor(props) {
|
|
222
|
+
this.props = props;
|
|
223
|
+
}
|
|
224
|
+
render() {
|
|
225
|
+
const relativePath = path.relative("", this.props.to).replace(/\\/g, "/");
|
|
226
|
+
if (relativePath === "") {
|
|
227
|
+
return Scribbler.createElement(Text, null, "./");
|
|
228
|
+
}
|
|
229
|
+
return (Scribbler.createElement(Text, null,
|
|
230
|
+
relativePath.startsWith(".") ? "" : "./",
|
|
231
|
+
relativePath,
|
|
232
|
+
this.props.isDirectory === true ? "/" : ""));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class CodeSpanText {
|
|
237
|
+
props;
|
|
238
|
+
constructor(props) {
|
|
239
|
+
this.props = props;
|
|
240
|
+
}
|
|
241
|
+
render() {
|
|
242
|
+
const lastLineInFile = this.props.file.getLineAndCharacterOfPosition(this.props.file.text.length).line;
|
|
243
|
+
const { character: markedCharacter, line: markedLine } = this.props.file.getLineAndCharacterOfPosition(this.props.start);
|
|
244
|
+
const firstLine = Math.max(markedLine - 2, 0);
|
|
245
|
+
const lastLine = Math.min(firstLine + 5, lastLineInFile);
|
|
246
|
+
const lineNumberMaxWidth = `${lastLine + 1}`.length;
|
|
247
|
+
const codeSpan = [];
|
|
248
|
+
for (let i = firstLine; i <= lastLine; i++) {
|
|
249
|
+
const lineStart = this.props.file.getPositionOfLineAndCharacter(i, 0);
|
|
250
|
+
const lineEnd = i === lastLineInFile ? this.props.file.text.length : this.props.file.getPositionOfLineAndCharacter(i + 1, 0);
|
|
251
|
+
const lineNumberText = String(i + 1);
|
|
252
|
+
const lineText = this.props.file.text.slice(lineStart, lineEnd).trimEnd().replace(/\t/g, " ");
|
|
253
|
+
if (i === markedLine) {
|
|
254
|
+
codeSpan.push(Scribbler.createElement(Line, null,
|
|
255
|
+
Scribbler.createElement(Text, { color: "31" }, ">"),
|
|
256
|
+
" ",
|
|
257
|
+
lineNumberText.padStart(lineNumberMaxWidth),
|
|
258
|
+
" ",
|
|
259
|
+
Scribbler.createElement(Text, { color: "90" }, "|"),
|
|
260
|
+
" ",
|
|
261
|
+
lineText));
|
|
262
|
+
codeSpan.push(Scribbler.createElement(Line, null,
|
|
263
|
+
" ".repeat(lineNumberMaxWidth + 3),
|
|
264
|
+
Scribbler.createElement(Text, { color: "90" }, "|"),
|
|
265
|
+
" ".repeat(markedCharacter + 1),
|
|
266
|
+
Scribbler.createElement(Text, { color: "31" }, "^")));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
codeSpan.push(Scribbler.createElement(Line, null,
|
|
270
|
+
" ".repeat(2),
|
|
271
|
+
Scribbler.createElement(Text, { color: "90" },
|
|
272
|
+
lineNumberText.padStart(lineNumberMaxWidth),
|
|
273
|
+
" | ",
|
|
274
|
+
lineText || "")));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const breadcrumbs = this.props.breadcrumbs?.flatMap((ancestor) => [
|
|
278
|
+
Scribbler.createElement(Text, { color: "90" }, " \u276D "),
|
|
279
|
+
Scribbler.createElement(Text, null, ancestor),
|
|
280
|
+
]);
|
|
281
|
+
const location = (Scribbler.createElement(Line, null,
|
|
282
|
+
" ".repeat(lineNumberMaxWidth + 5),
|
|
283
|
+
Scribbler.createElement(Text, { color: "90" }, "at"),
|
|
284
|
+
" ",
|
|
285
|
+
Scribbler.createElement(Text, { color: "36" },
|
|
286
|
+
Scribbler.createElement(RelativePathText, { to: this.props.file.fileName })),
|
|
287
|
+
Scribbler.createElement(Text, { color: "90" },
|
|
288
|
+
":",
|
|
289
|
+
String(markedLine + 1),
|
|
290
|
+
":",
|
|
291
|
+
String(markedCharacter + 1)),
|
|
292
|
+
breadcrumbs));
|
|
293
|
+
return (Scribbler.createElement(Text, null,
|
|
294
|
+
codeSpan,
|
|
295
|
+
Scribbler.createElement(Line, null),
|
|
296
|
+
location));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
class DiagnosticText {
|
|
301
|
+
props;
|
|
302
|
+
constructor(props) {
|
|
303
|
+
this.props = props;
|
|
304
|
+
}
|
|
305
|
+
render() {
|
|
306
|
+
const code = typeof this.props.diagnostic.code === "string" ? (Scribbler.createElement(Text, { color: "90" },
|
|
307
|
+
" ",
|
|
308
|
+
this.props.diagnostic.code)) : undefined;
|
|
309
|
+
const text = Array.isArray(this.props.diagnostic.text) ? this.props.diagnostic.text : [this.props.diagnostic.text];
|
|
310
|
+
const message = text.map((text, index) => (Scribbler.createElement(Text, null,
|
|
311
|
+
index === 1 ? Scribbler.createElement(Line, null) : undefined,
|
|
312
|
+
Scribbler.createElement(Line, null,
|
|
313
|
+
text,
|
|
314
|
+
code))));
|
|
315
|
+
const related = this.props.diagnostic.related?.map((relatedDiagnostic) => (Scribbler.createElement(DiagnosticText, { diagnostic: relatedDiagnostic })));
|
|
316
|
+
const codeSpan = this.props.diagnostic.origin ? (Scribbler.createElement(Text, null,
|
|
317
|
+
Scribbler.createElement(Line, null),
|
|
318
|
+
Scribbler.createElement(CodeSpanText, { ...this.props.diagnostic.origin }))) : undefined;
|
|
319
|
+
return (Scribbler.createElement(Text, null,
|
|
320
|
+
message,
|
|
321
|
+
codeSpan,
|
|
322
|
+
Scribbler.createElement(Line, null),
|
|
323
|
+
Scribbler.createElement(Text, { indent: 2 }, related)));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function diagnosticText(diagnostic) {
|
|
327
|
+
let prefix;
|
|
328
|
+
switch (diagnostic.category) {
|
|
329
|
+
case "error":
|
|
330
|
+
prefix = Scribbler.createElement(Text, { color: "31" }, "Error: ");
|
|
331
|
+
break;
|
|
332
|
+
case "warning":
|
|
333
|
+
prefix = Scribbler.createElement(Text, { color: "33" }, "Warning: ");
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
return (Scribbler.createElement(Text, null,
|
|
337
|
+
prefix,
|
|
338
|
+
Scribbler.createElement(DiagnosticText, { diagnostic: diagnostic })));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
class FileNameText {
|
|
342
|
+
props;
|
|
343
|
+
constructor(props) {
|
|
344
|
+
this.props = props;
|
|
345
|
+
}
|
|
346
|
+
render() {
|
|
347
|
+
return (Scribbler.createElement(Text, null,
|
|
348
|
+
Scribbler.createElement(Text, { color: "90" },
|
|
349
|
+
Scribbler.createElement(RelativePathText, { isDirectory: true, to: path.dirname(this.props.filePath) })),
|
|
350
|
+
path.basename(this.props.filePath)));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function fileStatusText(status, testFile) {
|
|
354
|
+
let statusColor;
|
|
355
|
+
let statusText;
|
|
356
|
+
switch (status) {
|
|
357
|
+
case "runs":
|
|
358
|
+
statusColor = "33";
|
|
359
|
+
statusText = "runs";
|
|
360
|
+
break;
|
|
361
|
+
case "passed":
|
|
362
|
+
statusColor = "32";
|
|
363
|
+
statusText = "pass";
|
|
364
|
+
break;
|
|
365
|
+
case "failed":
|
|
366
|
+
statusColor = "31";
|
|
367
|
+
statusText = "fail";
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
return (Scribbler.createElement(Line, null,
|
|
371
|
+
Scribbler.createElement(Text, { color: statusColor }, statusText),
|
|
372
|
+
" ",
|
|
373
|
+
Scribbler.createElement(FileNameText, { filePath: fileURLToPath(testFile) })));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function fileViewText(lines, addEmptyFinalLine) {
|
|
377
|
+
return (Scribbler.createElement(Text, null,
|
|
378
|
+
[...lines],
|
|
379
|
+
addEmptyFinalLine ? Scribbler.createElement(Line, null) : undefined));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
class RowText {
|
|
383
|
+
props;
|
|
384
|
+
constructor(props) {
|
|
385
|
+
this.props = props;
|
|
386
|
+
}
|
|
387
|
+
render() {
|
|
388
|
+
return (Scribbler.createElement(Line, null,
|
|
389
|
+
`${this.props.label}:`.padEnd(12),
|
|
390
|
+
this.props.text));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
class CountText {
|
|
394
|
+
props;
|
|
395
|
+
constructor(props) {
|
|
396
|
+
this.props = props;
|
|
397
|
+
}
|
|
398
|
+
render() {
|
|
399
|
+
return (Scribbler.createElement(Text, null,
|
|
400
|
+
this.props.failed > 0 ? (Scribbler.createElement(Text, null,
|
|
401
|
+
Scribbler.createElement(Text, { color: "31" },
|
|
402
|
+
String(this.props.failed),
|
|
403
|
+
" failed"),
|
|
404
|
+
Scribbler.createElement(Text, null, ", "))) : undefined,
|
|
405
|
+
this.props.skipped > 0 ? (Scribbler.createElement(Text, null,
|
|
406
|
+
Scribbler.createElement(Text, { color: "33" },
|
|
407
|
+
String(this.props.skipped),
|
|
408
|
+
" skipped"),
|
|
409
|
+
Scribbler.createElement(Text, null, ", "))) : undefined,
|
|
410
|
+
this.props.todo > 0 ? (Scribbler.createElement(Text, null,
|
|
411
|
+
Scribbler.createElement(Text, { color: "35" },
|
|
412
|
+
String(this.props.todo),
|
|
413
|
+
" todo"),
|
|
414
|
+
Scribbler.createElement(Text, null, ", "))) : undefined,
|
|
415
|
+
this.props.passed > 0 ? (Scribbler.createElement(Text, null,
|
|
416
|
+
Scribbler.createElement(Text, { color: "32" },
|
|
417
|
+
String(this.props.passed),
|
|
418
|
+
" passed"),
|
|
419
|
+
Scribbler.createElement(Text, null, ", "))) : undefined,
|
|
420
|
+
Scribbler.createElement(Text, null,
|
|
421
|
+
String(this.props.total),
|
|
422
|
+
Scribbler.createElement(Text, null, " total"))));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
class DurationText {
|
|
426
|
+
props;
|
|
427
|
+
constructor(props) {
|
|
428
|
+
this.props = props;
|
|
429
|
+
}
|
|
430
|
+
render() {
|
|
431
|
+
const duration = this.props.duration / 1000;
|
|
432
|
+
const minutes = Math.floor(duration / 60);
|
|
433
|
+
const seconds = duration % 60;
|
|
434
|
+
return (Scribbler.createElement(Text, null,
|
|
435
|
+
minutes > 0 ? `${minutes}m ` : undefined,
|
|
436
|
+
`${Math.round(seconds * 10) / 10}s`));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
class MatchText {
|
|
440
|
+
props;
|
|
441
|
+
constructor(props) {
|
|
442
|
+
this.props = props;
|
|
443
|
+
}
|
|
444
|
+
render() {
|
|
445
|
+
if (typeof this.props.text === "string") {
|
|
446
|
+
return Scribbler.createElement(Text, null,
|
|
447
|
+
"'",
|
|
448
|
+
this.props.text,
|
|
449
|
+
"'");
|
|
450
|
+
}
|
|
451
|
+
if (this.props.text.length <= 1) {
|
|
452
|
+
return Scribbler.createElement(Text, null,
|
|
453
|
+
"'",
|
|
454
|
+
...this.props.text,
|
|
455
|
+
"'");
|
|
456
|
+
}
|
|
457
|
+
const lastItem = this.props.text.pop();
|
|
458
|
+
return (Scribbler.createElement(Text, null,
|
|
459
|
+
this.props.text.map((match, index, list) => (Scribbler.createElement(Text, null,
|
|
460
|
+
"'",
|
|
461
|
+
match,
|
|
462
|
+
"'",
|
|
463
|
+
index === list.length - 1 ? Scribbler.createElement(Text, null, " ") : Scribbler.createElement(Text, { color: "90" }, ", ")))),
|
|
464
|
+
Scribbler.createElement(Text, { color: "90" }, "or"),
|
|
465
|
+
" '",
|
|
466
|
+
lastItem,
|
|
467
|
+
"'"));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
class RanFilesText {
|
|
471
|
+
props;
|
|
472
|
+
constructor(props) {
|
|
473
|
+
this.props = props;
|
|
474
|
+
}
|
|
475
|
+
render() {
|
|
476
|
+
const testNameMatch = [];
|
|
477
|
+
if (this.props.onlyMatch != null) {
|
|
478
|
+
testNameMatch.push(Scribbler.createElement(Text, null,
|
|
479
|
+
Scribbler.createElement(Text, { color: "90" }, "matching "),
|
|
480
|
+
Scribbler.createElement(MatchText, { text: this.props.onlyMatch })));
|
|
481
|
+
}
|
|
482
|
+
if (this.props.skipMatch != null) {
|
|
483
|
+
testNameMatch.push(Scribbler.createElement(Text, null,
|
|
484
|
+
this.props.onlyMatch == null ? undefined : Scribbler.createElement(Text, { color: "90" }, " and "),
|
|
485
|
+
Scribbler.createElement(Text, { color: "90" }, "not matching "),
|
|
486
|
+
Scribbler.createElement(MatchText, { text: this.props.skipMatch })));
|
|
487
|
+
}
|
|
488
|
+
let pathMatch;
|
|
489
|
+
if (this.props.pathMatch.length > 0) {
|
|
490
|
+
pathMatch = (Scribbler.createElement(Text, null,
|
|
491
|
+
Scribbler.createElement(Text, { color: "90" }, "test files matching "),
|
|
492
|
+
Scribbler.createElement(MatchText, { text: this.props.pathMatch }),
|
|
493
|
+
Scribbler.createElement(Text, { color: "90" }, ".")));
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
pathMatch = Scribbler.createElement(Text, { color: "90" }, "all test files.");
|
|
497
|
+
}
|
|
498
|
+
return (Scribbler.createElement(Line, null,
|
|
499
|
+
Scribbler.createElement(Text, { color: "90" }, "Ran "),
|
|
500
|
+
testNameMatch.length > 0 ? Scribbler.createElement(Text, { color: "90" }, "tests ") : undefined,
|
|
501
|
+
testNameMatch,
|
|
502
|
+
testNameMatch.length > 0 ? Scribbler.createElement(Text, { color: "90" }, " in ") : undefined,
|
|
503
|
+
pathMatch));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) {
|
|
507
|
+
const targetCountText = (Scribbler.createElement(RowText, { label: "Targets", text: Scribbler.createElement(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) }));
|
|
508
|
+
const fileCountText = (Scribbler.createElement(RowText, { label: "Test files", text: Scribbler.createElement(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) }));
|
|
509
|
+
const testCountText = (Scribbler.createElement(RowText, { label: "Tests", text: Scribbler.createElement(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) }));
|
|
510
|
+
const assertionCountText = (Scribbler.createElement(RowText, { label: "Assertions", text: Scribbler.createElement(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) }));
|
|
511
|
+
return (Scribbler.createElement(Text, null,
|
|
512
|
+
targetCountText,
|
|
513
|
+
fileCountText,
|
|
514
|
+
testCount.total > 0 ? testCountText : undefined,
|
|
515
|
+
expectCount.total > 0 ? assertionCountText : undefined,
|
|
516
|
+
Scribbler.createElement(RowText, { label: "Duration", text: Scribbler.createElement(DurationText, { duration: duration }) }),
|
|
517
|
+
Scribbler.createElement(Line, null),
|
|
518
|
+
Scribbler.createElement(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch })));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
class StatusText {
|
|
522
|
+
props;
|
|
523
|
+
constructor(props) {
|
|
524
|
+
this.props = props;
|
|
525
|
+
}
|
|
526
|
+
render() {
|
|
527
|
+
switch (this.props.status) {
|
|
528
|
+
case "fail":
|
|
529
|
+
return Scribbler.createElement(Text, { color: "31" }, "\u00D7");
|
|
530
|
+
case "pass":
|
|
531
|
+
return Scribbler.createElement(Text, { color: "32" }, "+");
|
|
532
|
+
case "skip":
|
|
533
|
+
return Scribbler.createElement(Text, { color: "33" }, "- skip");
|
|
534
|
+
case "todo":
|
|
535
|
+
return Scribbler.createElement(Text, { color: "35" }, "- todo");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function testNameText(status, name, indent = 0) {
|
|
540
|
+
return (Scribbler.createElement(Line, { indent: indent + 1 },
|
|
541
|
+
Scribbler.createElement(StatusText, { status: status }),
|
|
542
|
+
" ",
|
|
543
|
+
Scribbler.createElement(Text, { color: "90" }, name)));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
class ProjectNameText {
|
|
547
|
+
props;
|
|
548
|
+
constructor(props) {
|
|
549
|
+
this.props = props;
|
|
550
|
+
}
|
|
551
|
+
render() {
|
|
552
|
+
return (Scribbler.createElement(Text, { color: "90" },
|
|
553
|
+
" with ",
|
|
554
|
+
Scribbler.createElement(RelativePathText, { to: this.props.filePath })));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) {
|
|
558
|
+
const projectPathText = typeof tsconfigFilePath === "string" ? Scribbler.createElement(ProjectNameText, { filePath: tsconfigFilePath }) : undefined;
|
|
559
|
+
return (Scribbler.createElement(Text, null,
|
|
560
|
+
options?.prependEmptyLine === true ? Scribbler.createElement(Line, null) : undefined,
|
|
561
|
+
Scribbler.createElement(Line, null,
|
|
562
|
+
Scribbler.createElement(Text, { color: "34" }, "uses"),
|
|
563
|
+
" TypeScript ",
|
|
564
|
+
compilerVersion,
|
|
565
|
+
projectPathText),
|
|
566
|
+
Scribbler.createElement(Line, null)));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
class Logger {
|
|
570
|
+
#noColor;
|
|
571
|
+
#scribbler;
|
|
572
|
+
#stderr;
|
|
573
|
+
#stdout;
|
|
574
|
+
constructor(options) {
|
|
575
|
+
this.#noColor = options?.noColor ?? Environment.noColor;
|
|
576
|
+
this.#stderr = options?.stderr ?? process.stderr;
|
|
577
|
+
this.#stdout = options?.stdout ?? process.stdout;
|
|
578
|
+
this.#scribbler = new Scribbler({ noColors: this.#noColor });
|
|
579
|
+
}
|
|
580
|
+
eraseLastLine() {
|
|
581
|
+
if (!this.isInteractive()) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this.#stdout.write("\x1b[1A\x1b[0K");
|
|
585
|
+
}
|
|
586
|
+
isInteractive() {
|
|
587
|
+
if ("isTTY" in this.#stdout && typeof this.#stdout.isTTY === "boolean") {
|
|
588
|
+
return this.#stdout.isTTY;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
#write(stream, body) {
|
|
593
|
+
const elements = Array.isArray(body) ? body : [body];
|
|
594
|
+
for (const element of elements) {
|
|
595
|
+
if (element.$$typeof !== Symbol.for("tstyche:scribbler")) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
stream.write(this.#scribbler.render(element));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
writeError(body) {
|
|
602
|
+
this.#write(this.#stderr, body);
|
|
603
|
+
}
|
|
604
|
+
writeMessage(body) {
|
|
605
|
+
this.#write(this.#stdout, body);
|
|
606
|
+
}
|
|
607
|
+
writeWarning(body) {
|
|
608
|
+
this.#write(this.#stderr, body);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
class Reporter {
|
|
613
|
+
resolvedConfig;
|
|
614
|
+
logger;
|
|
615
|
+
constructor(resolvedConfig) {
|
|
616
|
+
this.resolvedConfig = resolvedConfig;
|
|
617
|
+
this.logger = new Logger();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
class SummaryReporter extends Reporter {
|
|
622
|
+
handleEvent([eventName, payload]) {
|
|
623
|
+
switch (eventName) {
|
|
624
|
+
case "end":
|
|
625
|
+
this.logger.writeMessage(summaryText({
|
|
626
|
+
duration: payload.result.timing.duration,
|
|
627
|
+
expectCount: payload.result.expectCount,
|
|
628
|
+
fileCount: payload.result.fileCount,
|
|
629
|
+
onlyMatch: payload.result.resolvedConfig.only,
|
|
630
|
+
pathMatch: payload.result.resolvedConfig.pathMatch,
|
|
631
|
+
skipMatch: payload.result.resolvedConfig.skip,
|
|
632
|
+
targetCount: payload.result.targetCount,
|
|
633
|
+
testCount: payload.result.testCount,
|
|
634
|
+
}));
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
class Diagnostic {
|
|
641
|
+
text;
|
|
642
|
+
category;
|
|
643
|
+
origin;
|
|
644
|
+
code;
|
|
645
|
+
related;
|
|
646
|
+
constructor(text, category, origin) {
|
|
647
|
+
this.text = text;
|
|
648
|
+
this.category = category;
|
|
649
|
+
this.origin = origin;
|
|
650
|
+
}
|
|
651
|
+
add(options) {
|
|
652
|
+
if (options.code != null) {
|
|
653
|
+
this.code = options.code;
|
|
654
|
+
}
|
|
655
|
+
if (options.related != null) {
|
|
656
|
+
this.related = options.related;
|
|
657
|
+
}
|
|
658
|
+
return this;
|
|
659
|
+
}
|
|
660
|
+
static error(text, origin) {
|
|
661
|
+
return new Diagnostic(text, "error", origin);
|
|
662
|
+
}
|
|
663
|
+
static fromDiagnostics(diagnostics, compiler) {
|
|
664
|
+
return diagnostics.map((diagnostic) => {
|
|
665
|
+
let category;
|
|
666
|
+
switch (diagnostic.category) {
|
|
667
|
+
case compiler.DiagnosticCategory.Error:
|
|
668
|
+
category = "error";
|
|
669
|
+
break;
|
|
670
|
+
default:
|
|
671
|
+
category = "warning";
|
|
672
|
+
}
|
|
673
|
+
const code = `ts(${diagnostic.code})`;
|
|
674
|
+
const text = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\r\n");
|
|
675
|
+
if (Diagnostic.isTsDiagnosticWithLocation(diagnostic)) {
|
|
676
|
+
const origin = {
|
|
677
|
+
end: diagnostic.start + diagnostic.length,
|
|
678
|
+
file: diagnostic.file,
|
|
679
|
+
start: diagnostic.start,
|
|
680
|
+
};
|
|
681
|
+
return new Diagnostic(text, category, origin).add({ code });
|
|
682
|
+
}
|
|
683
|
+
return new Diagnostic(text, category).add({ code });
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
static fromError(text, error) {
|
|
687
|
+
const messageText = Array.isArray(text) ? text : [text];
|
|
688
|
+
if (error instanceof Error) {
|
|
689
|
+
if (error.cause != null) {
|
|
690
|
+
messageText.push(this.#normalizeMessage(String(error.cause)));
|
|
691
|
+
}
|
|
692
|
+
messageText.push(this.#normalizeMessage(String(error.message)));
|
|
693
|
+
if (error.stack != null) {
|
|
694
|
+
const stackLines = error.stack
|
|
695
|
+
.split("\n")
|
|
696
|
+
.slice(1)
|
|
697
|
+
.map((line) => line.trimStart());
|
|
698
|
+
messageText.push(...stackLines);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return Diagnostic.error(messageText);
|
|
702
|
+
}
|
|
703
|
+
static isTsDiagnosticWithLocation(diagnostic) {
|
|
704
|
+
return diagnostic.file != null && diagnostic.start != null && diagnostic.length != null;
|
|
705
|
+
}
|
|
706
|
+
static #normalizeMessage(text) {
|
|
707
|
+
if (text.endsWith(".")) {
|
|
708
|
+
return text;
|
|
709
|
+
}
|
|
710
|
+
return `${text}.`;
|
|
711
|
+
}
|
|
712
|
+
static warning(text, origin) {
|
|
713
|
+
return new Diagnostic(text, "warning", origin);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
var DiagnosticCategory;
|
|
718
|
+
(function (DiagnosticCategory) {
|
|
719
|
+
DiagnosticCategory["Error"] = "error";
|
|
720
|
+
DiagnosticCategory["Warning"] = "warning";
|
|
721
|
+
})(DiagnosticCategory || (DiagnosticCategory = {}));
|
|
722
|
+
|
|
723
|
+
class FileViewService {
|
|
724
|
+
#indent = 0;
|
|
725
|
+
#lines = [];
|
|
726
|
+
#messages = [];
|
|
727
|
+
get hasErrors() {
|
|
728
|
+
return this.#messages.length > 0;
|
|
729
|
+
}
|
|
730
|
+
addMessage(message) {
|
|
731
|
+
if (Array.isArray(message)) {
|
|
732
|
+
this.#messages.push(...message);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
this.#messages.push(message);
|
|
736
|
+
}
|
|
737
|
+
addTest(status, name) {
|
|
738
|
+
this.#lines.push(testNameText(status, name, this.#indent));
|
|
739
|
+
}
|
|
740
|
+
beginDescribe(name) {
|
|
741
|
+
this.#lines.push(describeNameText(name, this.#indent));
|
|
742
|
+
this.#indent++;
|
|
743
|
+
}
|
|
744
|
+
endDescribe() {
|
|
745
|
+
this.#indent--;
|
|
746
|
+
}
|
|
747
|
+
getMessages() {
|
|
748
|
+
return this.#messages;
|
|
749
|
+
}
|
|
750
|
+
getViewText(options) {
|
|
751
|
+
return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
|
|
752
|
+
}
|
|
753
|
+
reset() {
|
|
754
|
+
this.#indent = 0;
|
|
755
|
+
this.#lines = [];
|
|
756
|
+
this.#messages = [];
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
class ThoroughReporter extends Reporter {
|
|
761
|
+
#currentCompilerVersion;
|
|
762
|
+
#currentProjectConfigFilePath;
|
|
763
|
+
#fileCount = 0;
|
|
764
|
+
#fileView = new FileViewService();
|
|
765
|
+
#hasReportedAdds = false;
|
|
766
|
+
#hasReportedError = false;
|
|
767
|
+
#isFileViewExpanded = false;
|
|
768
|
+
get #isLastFile() {
|
|
769
|
+
return this.#fileCount === 0;
|
|
770
|
+
}
|
|
771
|
+
handleEvent([eventName, payload]) {
|
|
772
|
+
switch (eventName) {
|
|
773
|
+
case "start":
|
|
774
|
+
this.#isFileViewExpanded = payload.result.testFiles.length === 1;
|
|
775
|
+
break;
|
|
776
|
+
case "store:info":
|
|
777
|
+
this.logger.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
|
|
778
|
+
this.#hasReportedAdds = true;
|
|
779
|
+
break;
|
|
780
|
+
case "store:error":
|
|
781
|
+
for (const diagnostic of payload.diagnostics) {
|
|
782
|
+
this.logger.writeError(diagnosticText(diagnostic));
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
case "target:start":
|
|
786
|
+
this.#fileCount = payload.result.testFiles.length;
|
|
787
|
+
break;
|
|
788
|
+
case "target:end":
|
|
789
|
+
this.#currentCompilerVersion = undefined;
|
|
790
|
+
this.#currentProjectConfigFilePath = undefined;
|
|
791
|
+
break;
|
|
792
|
+
case "project:info":
|
|
793
|
+
if (this.#currentCompilerVersion !== payload.compilerVersion ||
|
|
794
|
+
this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) {
|
|
795
|
+
this.logger.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
|
|
796
|
+
prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds && !this.#hasReportedError,
|
|
797
|
+
}));
|
|
798
|
+
this.#hasReportedAdds = false;
|
|
799
|
+
if (payload.projectConfigFilePath == null) {
|
|
800
|
+
const text = [
|
|
801
|
+
"The default compiler options are used for the following tests files.",
|
|
802
|
+
"Make sure that 'tsconfig.json' exists and the test files are included in the program.",
|
|
803
|
+
];
|
|
804
|
+
this.logger.writeWarning(diagnosticText(Diagnostic.warning(text)));
|
|
805
|
+
}
|
|
806
|
+
this.#currentCompilerVersion = payload.compilerVersion;
|
|
807
|
+
this.#currentProjectConfigFilePath = payload.projectConfigFilePath;
|
|
808
|
+
}
|
|
809
|
+
break;
|
|
810
|
+
case "project:error":
|
|
811
|
+
for (const diagnostic of payload.diagnostics) {
|
|
812
|
+
this.logger.writeError(diagnosticText(diagnostic));
|
|
813
|
+
}
|
|
814
|
+
break;
|
|
815
|
+
case "file:start":
|
|
816
|
+
if (this.logger.isInteractive()) {
|
|
817
|
+
this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
|
|
818
|
+
}
|
|
819
|
+
this.#fileCount--;
|
|
820
|
+
this.#hasReportedError = false;
|
|
821
|
+
break;
|
|
822
|
+
case "file:error":
|
|
823
|
+
for (const diagnostic of payload.diagnostics) {
|
|
824
|
+
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
825
|
+
}
|
|
826
|
+
break;
|
|
827
|
+
case "file:end":
|
|
828
|
+
this.logger.eraseLastLine();
|
|
829
|
+
this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
|
|
830
|
+
this.logger.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
|
|
831
|
+
if (this.#fileView.hasErrors) {
|
|
832
|
+
this.logger.writeError(this.#fileView.getMessages());
|
|
833
|
+
this.#hasReportedError = true;
|
|
834
|
+
}
|
|
835
|
+
this.#fileView.reset();
|
|
836
|
+
break;
|
|
837
|
+
case "describe:start":
|
|
838
|
+
if (this.#isFileViewExpanded) {
|
|
839
|
+
this.#fileView.beginDescribe(payload.result.describe.name);
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
case "describe:end":
|
|
843
|
+
if (this.#isFileViewExpanded) {
|
|
844
|
+
this.#fileView.endDescribe();
|
|
845
|
+
}
|
|
846
|
+
break;
|
|
847
|
+
case "test:skip":
|
|
848
|
+
if (this.#isFileViewExpanded) {
|
|
849
|
+
this.#fileView.addTest("skip", payload.result.test.name);
|
|
850
|
+
}
|
|
851
|
+
break;
|
|
852
|
+
case "test:todo":
|
|
853
|
+
if (this.#isFileViewExpanded) {
|
|
854
|
+
this.#fileView.addTest("todo", payload.result.test.name);
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
case "test:error":
|
|
858
|
+
if (this.#isFileViewExpanded) {
|
|
859
|
+
this.#fileView.addTest("fail", payload.result.test.name);
|
|
860
|
+
}
|
|
861
|
+
for (const diagnostic of payload.diagnostics) {
|
|
862
|
+
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
863
|
+
}
|
|
864
|
+
break;
|
|
865
|
+
case "test:fail":
|
|
866
|
+
if (this.#isFileViewExpanded) {
|
|
867
|
+
this.#fileView.addTest("fail", payload.result.test.name);
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
case "test:pass":
|
|
871
|
+
if (this.#isFileViewExpanded) {
|
|
872
|
+
this.#fileView.addTest("pass", payload.result.test.name);
|
|
873
|
+
}
|
|
874
|
+
break;
|
|
875
|
+
case "expect:error":
|
|
876
|
+
case "expect:fail":
|
|
877
|
+
for (const diagnostic of payload.diagnostics) {
|
|
878
|
+
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
879
|
+
}
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
class ResultTiming {
|
|
886
|
+
end = Date.now();
|
|
887
|
+
start = Date.now();
|
|
888
|
+
get duration() {
|
|
889
|
+
return this.end - this.start;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
class DescribeResult {
|
|
894
|
+
describe;
|
|
895
|
+
parent;
|
|
896
|
+
results = [];
|
|
897
|
+
timing = new ResultTiming();
|
|
898
|
+
constructor(describe, parent) {
|
|
899
|
+
this.describe = describe;
|
|
900
|
+
this.parent = parent;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
class ExpectResult {
|
|
905
|
+
assertion;
|
|
906
|
+
parent;
|
|
907
|
+
diagnostics = [];
|
|
908
|
+
status = "runs";
|
|
909
|
+
timing = new ResultTiming();
|
|
910
|
+
constructor(assertion, parent) {
|
|
911
|
+
this.assertion = assertion;
|
|
912
|
+
this.parent = parent;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
class ResultCount {
|
|
917
|
+
failed = 0;
|
|
918
|
+
passed = 0;
|
|
919
|
+
skipped = 0;
|
|
920
|
+
todo = 0;
|
|
921
|
+
get total() {
|
|
922
|
+
return this.failed + this.passed + this.skipped + this.todo;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
class FileResult {
|
|
927
|
+
testFile;
|
|
928
|
+
diagnostics = [];
|
|
929
|
+
expectCount = new ResultCount();
|
|
930
|
+
results = [];
|
|
931
|
+
status = "runs";
|
|
932
|
+
testCount = new ResultCount();
|
|
933
|
+
timing = new ResultTiming();
|
|
934
|
+
constructor(testFile) {
|
|
935
|
+
this.testFile = testFile;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
class ProjectResult {
|
|
940
|
+
compilerVersion;
|
|
941
|
+
projectConfigFilePath;
|
|
942
|
+
diagnostics = [];
|
|
943
|
+
results = [];
|
|
944
|
+
constructor(compilerVersion, projectConfigFilePath) {
|
|
945
|
+
this.compilerVersion = compilerVersion;
|
|
946
|
+
this.projectConfigFilePath = projectConfigFilePath;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
class Result {
|
|
951
|
+
resolvedConfig;
|
|
952
|
+
testFiles;
|
|
953
|
+
expectCount = new ResultCount();
|
|
954
|
+
fileCount = new ResultCount();
|
|
955
|
+
results = [];
|
|
956
|
+
targetCount = new ResultCount();
|
|
957
|
+
testCount = new ResultCount();
|
|
958
|
+
timing = new ResultTiming();
|
|
959
|
+
constructor(resolvedConfig, testFiles) {
|
|
960
|
+
this.resolvedConfig = resolvedConfig;
|
|
961
|
+
this.testFiles = testFiles;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
class ResultManager {
|
|
966
|
+
#describeResult;
|
|
967
|
+
#expectResult;
|
|
968
|
+
#fileResult;
|
|
969
|
+
#projectResult;
|
|
970
|
+
#result;
|
|
971
|
+
#targetResult;
|
|
972
|
+
#testResult;
|
|
973
|
+
handleEvent([eventName, payload]) {
|
|
974
|
+
switch (eventName) {
|
|
975
|
+
case "start":
|
|
976
|
+
this.#result = payload.result;
|
|
977
|
+
this.#result.timing.start = Date.now();
|
|
978
|
+
break;
|
|
979
|
+
case "end":
|
|
980
|
+
this.#result.timing.end = Date.now();
|
|
981
|
+
this.#result = undefined;
|
|
982
|
+
break;
|
|
983
|
+
case "target:start":
|
|
984
|
+
this.#result.results.push(payload.result);
|
|
985
|
+
this.#targetResult = payload.result;
|
|
986
|
+
this.#targetResult.timing.start = Date.now();
|
|
987
|
+
break;
|
|
988
|
+
case "target:end":
|
|
989
|
+
if (this.#targetResult.status === "failed") {
|
|
990
|
+
this.#result.targetCount.failed++;
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
this.#result.targetCount.passed++;
|
|
994
|
+
this.#targetResult.status = "passed";
|
|
995
|
+
}
|
|
996
|
+
this.#targetResult.timing.end = Date.now();
|
|
997
|
+
this.#targetResult = undefined;
|
|
998
|
+
break;
|
|
999
|
+
case "store:error": {
|
|
1000
|
+
if (payload.diagnostics.some(({ category }) => category === "error")) {
|
|
1001
|
+
this.#targetResult.status = "failed";
|
|
1002
|
+
}
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
case "project:info":
|
|
1006
|
+
{
|
|
1007
|
+
let projectResult = this.#targetResult.results.get(payload.projectConfigFilePath);
|
|
1008
|
+
if (!projectResult) {
|
|
1009
|
+
projectResult = new ProjectResult(payload.compilerVersion, payload.projectConfigFilePath);
|
|
1010
|
+
this.#targetResult.results.set(payload.projectConfigFilePath, projectResult);
|
|
1011
|
+
}
|
|
1012
|
+
this.#projectResult = projectResult;
|
|
1013
|
+
}
|
|
1014
|
+
break;
|
|
1015
|
+
case "project:error":
|
|
1016
|
+
this.#targetResult.status = "failed";
|
|
1017
|
+
this.#projectResult.diagnostics.push(...payload.diagnostics);
|
|
1018
|
+
break;
|
|
1019
|
+
case "file:start":
|
|
1020
|
+
this.#projectResult.results.push(payload.result);
|
|
1021
|
+
this.#fileResult = payload.result;
|
|
1022
|
+
this.#fileResult.timing.start = Date.now();
|
|
1023
|
+
break;
|
|
1024
|
+
case "file:error":
|
|
1025
|
+
this.#targetResult.status = "failed";
|
|
1026
|
+
this.#fileResult.status = "failed";
|
|
1027
|
+
this.#fileResult.diagnostics.push(...payload.diagnostics);
|
|
1028
|
+
break;
|
|
1029
|
+
case "file:end":
|
|
1030
|
+
if (this.#fileResult.status === "failed" ||
|
|
1031
|
+
this.#fileResult.expectCount.failed > 0 ||
|
|
1032
|
+
this.#fileResult.testCount.failed > 0) {
|
|
1033
|
+
this.#result.fileCount.failed++;
|
|
1034
|
+
this.#targetResult.status = "failed";
|
|
1035
|
+
this.#fileResult.status = "failed";
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
this.#result.fileCount.passed++;
|
|
1039
|
+
this.#fileResult.status = "passed";
|
|
1040
|
+
}
|
|
1041
|
+
this.#fileResult.timing.end = Date.now();
|
|
1042
|
+
this.#fileResult = undefined;
|
|
1043
|
+
break;
|
|
1044
|
+
case "describe:start":
|
|
1045
|
+
if (this.#describeResult) {
|
|
1046
|
+
this.#describeResult.results.push(payload.result);
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
this.#fileResult.results.push(payload.result);
|
|
1050
|
+
}
|
|
1051
|
+
this.#describeResult = payload.result;
|
|
1052
|
+
this.#describeResult.timing.start = Date.now();
|
|
1053
|
+
break;
|
|
1054
|
+
case "describe:end":
|
|
1055
|
+
this.#describeResult.timing.end = Date.now();
|
|
1056
|
+
this.#describeResult = this.#describeResult.parent;
|
|
1057
|
+
break;
|
|
1058
|
+
case "test:start":
|
|
1059
|
+
if (this.#describeResult) {
|
|
1060
|
+
this.#describeResult.results.push(payload.result);
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
this.#fileResult.results.push(payload.result);
|
|
1064
|
+
}
|
|
1065
|
+
this.#testResult = payload.result;
|
|
1066
|
+
this.#testResult.timing.start = Date.now();
|
|
1067
|
+
break;
|
|
1068
|
+
case "test:error":
|
|
1069
|
+
this.#result.testCount.failed++;
|
|
1070
|
+
this.#fileResult.testCount.failed++;
|
|
1071
|
+
this.#testResult.status = "failed";
|
|
1072
|
+
this.#testResult.diagnostics.push(...payload.diagnostics);
|
|
1073
|
+
this.#testResult.timing.end = Date.now();
|
|
1074
|
+
this.#testResult = undefined;
|
|
1075
|
+
break;
|
|
1076
|
+
case "test:fail":
|
|
1077
|
+
this.#result.testCount.failed++;
|
|
1078
|
+
this.#fileResult.testCount.failed++;
|
|
1079
|
+
this.#testResult.status = "failed";
|
|
1080
|
+
this.#testResult.timing.end = Date.now();
|
|
1081
|
+
this.#testResult = undefined;
|
|
1082
|
+
break;
|
|
1083
|
+
case "test:pass":
|
|
1084
|
+
this.#result.testCount.passed++;
|
|
1085
|
+
this.#fileResult.testCount.passed++;
|
|
1086
|
+
this.#testResult.status = "passed";
|
|
1087
|
+
this.#testResult.timing.end = Date.now();
|
|
1088
|
+
this.#testResult = undefined;
|
|
1089
|
+
break;
|
|
1090
|
+
case "test:skip":
|
|
1091
|
+
this.#result.testCount.skipped++;
|
|
1092
|
+
this.#fileResult.testCount.skipped++;
|
|
1093
|
+
this.#testResult.status = "skipped";
|
|
1094
|
+
this.#testResult.timing.end = Date.now();
|
|
1095
|
+
this.#testResult = undefined;
|
|
1096
|
+
break;
|
|
1097
|
+
case "test:todo":
|
|
1098
|
+
this.#result.testCount.todo++;
|
|
1099
|
+
this.#fileResult.testCount.todo++;
|
|
1100
|
+
this.#testResult.status = "todo";
|
|
1101
|
+
this.#testResult.timing.end = Date.now();
|
|
1102
|
+
this.#testResult = undefined;
|
|
1103
|
+
break;
|
|
1104
|
+
case "expect:start":
|
|
1105
|
+
if (this.#testResult) {
|
|
1106
|
+
this.#testResult.results.push(payload.result);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
this.#fileResult.results.push(payload.result);
|
|
1110
|
+
}
|
|
1111
|
+
this.#expectResult = payload.result;
|
|
1112
|
+
this.#expectResult.timing.start = Date.now();
|
|
1113
|
+
break;
|
|
1114
|
+
case "expect:error":
|
|
1115
|
+
this.#result.expectCount.failed++;
|
|
1116
|
+
this.#fileResult.expectCount.failed++;
|
|
1117
|
+
if (this.#testResult) {
|
|
1118
|
+
this.#testResult.expectCount.failed++;
|
|
1119
|
+
}
|
|
1120
|
+
this.#expectResult.status = "failed";
|
|
1121
|
+
this.#expectResult.diagnostics.push(...payload.diagnostics);
|
|
1122
|
+
this.#expectResult.timing.end = Date.now();
|
|
1123
|
+
this.#expectResult = undefined;
|
|
1124
|
+
break;
|
|
1125
|
+
case "expect:fail":
|
|
1126
|
+
this.#result.expectCount.failed++;
|
|
1127
|
+
this.#fileResult.expectCount.failed++;
|
|
1128
|
+
if (this.#testResult) {
|
|
1129
|
+
this.#testResult.expectCount.failed++;
|
|
1130
|
+
}
|
|
1131
|
+
this.#expectResult.status = "failed";
|
|
1132
|
+
this.#expectResult.timing.end = Date.now();
|
|
1133
|
+
this.#expectResult = undefined;
|
|
1134
|
+
break;
|
|
1135
|
+
case "expect:pass":
|
|
1136
|
+
this.#result.expectCount.passed++;
|
|
1137
|
+
this.#fileResult.expectCount.passed++;
|
|
1138
|
+
if (this.#testResult) {
|
|
1139
|
+
this.#testResult.expectCount.passed++;
|
|
1140
|
+
}
|
|
1141
|
+
this.#expectResult.status = "passed";
|
|
1142
|
+
this.#expectResult.timing.end = Date.now();
|
|
1143
|
+
this.#expectResult = undefined;
|
|
1144
|
+
break;
|
|
1145
|
+
case "expect:skip":
|
|
1146
|
+
this.#result.expectCount.skipped++;
|
|
1147
|
+
this.#fileResult.expectCount.skipped++;
|
|
1148
|
+
if (this.#testResult) {
|
|
1149
|
+
this.#testResult.expectCount.skipped++;
|
|
1150
|
+
}
|
|
1151
|
+
this.#expectResult.status = "skipped";
|
|
1152
|
+
this.#expectResult.timing.end = Date.now();
|
|
1153
|
+
this.#expectResult = undefined;
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
var ResultStatus;
|
|
1160
|
+
(function (ResultStatus) {
|
|
1161
|
+
ResultStatus["Runs"] = "runs";
|
|
1162
|
+
ResultStatus["Passed"] = "passed";
|
|
1163
|
+
ResultStatus["Failed"] = "failed";
|
|
1164
|
+
ResultStatus["Skipped"] = "skipped";
|
|
1165
|
+
ResultStatus["Todo"] = "todo";
|
|
1166
|
+
})(ResultStatus || (ResultStatus = {}));
|
|
1167
|
+
|
|
1168
|
+
class TargetResult {
|
|
1169
|
+
versionTag;
|
|
1170
|
+
testFiles;
|
|
1171
|
+
results = new Map();
|
|
1172
|
+
status = "runs";
|
|
1173
|
+
timing = new ResultTiming();
|
|
1174
|
+
constructor(versionTag, testFiles) {
|
|
1175
|
+
this.versionTag = versionTag;
|
|
1176
|
+
this.testFiles = testFiles;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
class TestResult {
|
|
1181
|
+
test;
|
|
1182
|
+
parent;
|
|
1183
|
+
diagnostics = [];
|
|
1184
|
+
expectCount = new ResultCount();
|
|
1185
|
+
results = [];
|
|
1186
|
+
status = "runs";
|
|
1187
|
+
timing = new ResultTiming();
|
|
1188
|
+
constructor(test, parent) {
|
|
1189
|
+
this.test = test;
|
|
1190
|
+
this.parent = parent;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
class TestMember {
|
|
1195
|
+
brand;
|
|
1196
|
+
node;
|
|
1197
|
+
parent;
|
|
1198
|
+
flags;
|
|
1199
|
+
compiler;
|
|
1200
|
+
diagnostics;
|
|
1201
|
+
members = [];
|
|
1202
|
+
name;
|
|
1203
|
+
typeChecker;
|
|
1204
|
+
constructor(brand, node, parent, flags) {
|
|
1205
|
+
this.brand = brand;
|
|
1206
|
+
this.node = node;
|
|
1207
|
+
this.parent = parent;
|
|
1208
|
+
this.flags = flags;
|
|
1209
|
+
this.compiler = parent.compiler;
|
|
1210
|
+
this.typeChecker = parent.typeChecker;
|
|
1211
|
+
this.diagnostics = this.#mapDiagnostics(node, this.parent);
|
|
1212
|
+
this.name = this.#resolveName(node);
|
|
1213
|
+
}
|
|
1214
|
+
get ancestorNames() {
|
|
1215
|
+
const ancestorNames = [];
|
|
1216
|
+
let ancestor = this.parent;
|
|
1217
|
+
while ("name" in ancestor) {
|
|
1218
|
+
ancestorNames.unshift(ancestor.name);
|
|
1219
|
+
ancestor = ancestor.parent;
|
|
1220
|
+
}
|
|
1221
|
+
return ancestorNames;
|
|
1222
|
+
}
|
|
1223
|
+
#mapDiagnostics(node, parent) {
|
|
1224
|
+
const mapped = [];
|
|
1225
|
+
const unmapped = [];
|
|
1226
|
+
if (node.arguments[1] != null &&
|
|
1227
|
+
parent.compiler.isFunctionLike(node.arguments[1]) &&
|
|
1228
|
+
parent.compiler.isBlock(node.arguments[1].body)) {
|
|
1229
|
+
const blockStart = node.arguments[1].body.getStart();
|
|
1230
|
+
const blockEnd = node.arguments[1].body.getEnd();
|
|
1231
|
+
parent.diagnostics.forEach((diagnostic) => {
|
|
1232
|
+
if (diagnostic.start != null && diagnostic.start >= blockStart && diagnostic.start <= blockEnd) {
|
|
1233
|
+
mapped.push(diagnostic);
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
unmapped.push(diagnostic);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
parent.diagnostics = unmapped;
|
|
1240
|
+
}
|
|
1241
|
+
return mapped;
|
|
1242
|
+
}
|
|
1243
|
+
#resolveName(node) {
|
|
1244
|
+
return node.arguments[0] !== undefined && this.compiler.isStringLiteral(node.arguments[0])
|
|
1245
|
+
? node.arguments[0].text
|
|
1246
|
+
: "";
|
|
1247
|
+
}
|
|
1248
|
+
validate() {
|
|
1249
|
+
const diagnostics = [];
|
|
1250
|
+
const getText = (node) => `'${node.expression.getText()}()' cannot be nested within '${this.node.expression.getText()}()' helper.`;
|
|
1251
|
+
switch (this.brand) {
|
|
1252
|
+
case "expect":
|
|
1253
|
+
break;
|
|
1254
|
+
case "describe":
|
|
1255
|
+
for (const member of this.members) {
|
|
1256
|
+
if (member.brand === "expect") {
|
|
1257
|
+
diagnostics.push(Diagnostic.error(getText(member.node), {
|
|
1258
|
+
end: member.node.getEnd(),
|
|
1259
|
+
file: member.node.getSourceFile(),
|
|
1260
|
+
start: member.node.getStart(),
|
|
1261
|
+
}));
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
break;
|
|
1265
|
+
case "test":
|
|
1266
|
+
for (const member of this.members) {
|
|
1267
|
+
if (member.brand !== "expect") {
|
|
1268
|
+
diagnostics.push(Diagnostic.error(getText(member.node), {
|
|
1269
|
+
end: member.node.getEnd(),
|
|
1270
|
+
file: member.node.getSourceFile(),
|
|
1271
|
+
start: member.node.getStart(),
|
|
1272
|
+
}));
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
return diagnostics;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
class Assertion extends TestMember {
|
|
1282
|
+
matcherNode;
|
|
1283
|
+
modifierNode;
|
|
1284
|
+
notNode;
|
|
1285
|
+
isNot;
|
|
1286
|
+
name = "";
|
|
1287
|
+
constructor(brand, node, parent, flags, matcherNode, modifierNode, notNode) {
|
|
1288
|
+
super(brand, node, parent, flags);
|
|
1289
|
+
this.matcherNode = matcherNode;
|
|
1290
|
+
this.modifierNode = modifierNode;
|
|
1291
|
+
this.notNode = notNode;
|
|
1292
|
+
this.diagnostics = this.#mapDiagnostics();
|
|
1293
|
+
this.isNot = notNode ? true : false;
|
|
1294
|
+
}
|
|
1295
|
+
get ancestorNames() {
|
|
1296
|
+
const ancestorNames = [];
|
|
1297
|
+
if ("ancestorNames" in this.parent) {
|
|
1298
|
+
ancestorNames.push(...this.parent.ancestorNames);
|
|
1299
|
+
}
|
|
1300
|
+
if ("name" in this.parent) {
|
|
1301
|
+
ancestorNames.push(this.parent.name);
|
|
1302
|
+
}
|
|
1303
|
+
return ancestorNames;
|
|
1304
|
+
}
|
|
1305
|
+
get matcherName() {
|
|
1306
|
+
return this.matcherNode.expression.name;
|
|
1307
|
+
}
|
|
1308
|
+
get sourceArguments() {
|
|
1309
|
+
return this.node.arguments;
|
|
1310
|
+
}
|
|
1311
|
+
get sourceType() {
|
|
1312
|
+
if (!this.typeChecker) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (this.node.typeArguments?.[0]) {
|
|
1316
|
+
return {
|
|
1317
|
+
source: 1,
|
|
1318
|
+
type: this.typeChecker.getTypeFromTypeNode(this.node.typeArguments[0]),
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
if (this.node.arguments[0]) {
|
|
1322
|
+
return {
|
|
1323
|
+
source: 0,
|
|
1324
|
+
type: this.typeChecker.getTypeAtLocation(this.node.arguments[0]),
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
get targetArguments() {
|
|
1330
|
+
return this.matcherNode.arguments;
|
|
1331
|
+
}
|
|
1332
|
+
get targetType() {
|
|
1333
|
+
if (!this.typeChecker) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (this.matcherNode.typeArguments?.[0]) {
|
|
1337
|
+
return {
|
|
1338
|
+
source: 1,
|
|
1339
|
+
type: this.typeChecker.getTypeFromTypeNode(this.matcherNode.typeArguments[0]),
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
if (this.matcherNode.arguments[0]) {
|
|
1343
|
+
return {
|
|
1344
|
+
source: 0,
|
|
1345
|
+
type: this.typeChecker.getTypeAtLocation(this.matcherNode.arguments[0]),
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
#mapDiagnostics() {
|
|
1351
|
+
const mapped = [];
|
|
1352
|
+
const unmapped = [];
|
|
1353
|
+
let argStart;
|
|
1354
|
+
let argEnd;
|
|
1355
|
+
if (this.node.typeArguments?.[0]) {
|
|
1356
|
+
argStart = this.node.typeArguments[0].getStart();
|
|
1357
|
+
argEnd = this.node.typeArguments[0].getEnd();
|
|
1358
|
+
}
|
|
1359
|
+
else if (this.node.arguments[0]) {
|
|
1360
|
+
argStart = this.node.arguments[0].getStart();
|
|
1361
|
+
argEnd = this.node.arguments[0].getEnd();
|
|
1362
|
+
}
|
|
1363
|
+
this.parent.diagnostics.forEach((diagnostic) => {
|
|
1364
|
+
if (diagnostic.start != null && diagnostic.start >= argStart && diagnostic.start <= argEnd) {
|
|
1365
|
+
mapped.push(diagnostic);
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
unmapped.push(diagnostic);
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
this.parent.diagnostics = unmapped;
|
|
1372
|
+
return mapped;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
var AssertionSource;
|
|
1377
|
+
(function (AssertionSource) {
|
|
1378
|
+
AssertionSource[AssertionSource["Argument"] = 0] = "Argument";
|
|
1379
|
+
AssertionSource[AssertionSource["TypeArgument"] = 1] = "TypeArgument";
|
|
1380
|
+
})(AssertionSource || (AssertionSource = {}));
|
|
1381
|
+
|
|
1382
|
+
class IdentifierLookup {
|
|
1383
|
+
compiler;
|
|
1384
|
+
#identifiers;
|
|
1385
|
+
#moduleSpecifiers = ['"tstyche"', "'tstyche'"];
|
|
1386
|
+
constructor(compiler, identifiers) {
|
|
1387
|
+
this.compiler = compiler;
|
|
1388
|
+
this.#identifiers = identifiers ?? {
|
|
1389
|
+
namedImports: {
|
|
1390
|
+
context: undefined,
|
|
1391
|
+
describe: undefined,
|
|
1392
|
+
expect: undefined,
|
|
1393
|
+
it: undefined,
|
|
1394
|
+
namespace: undefined,
|
|
1395
|
+
test: undefined,
|
|
1396
|
+
},
|
|
1397
|
+
namespace: undefined,
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
clone() {
|
|
1401
|
+
return {
|
|
1402
|
+
namedImports: { ...this.#identifiers.namedImports },
|
|
1403
|
+
namespace: this.#identifiers.namespace,
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
handleImportDeclaration(node) {
|
|
1407
|
+
if (this.#moduleSpecifiers.includes(node.moduleSpecifier.getText()) &&
|
|
1408
|
+
node.importClause?.isTypeOnly !== true &&
|
|
1409
|
+
node.importClause?.namedBindings != null) {
|
|
1410
|
+
if (this.compiler.isNamedImports(node.importClause.namedBindings)) {
|
|
1411
|
+
for (const element of node.importClause.namedBindings.elements) {
|
|
1412
|
+
if (element.isTypeOnly) {
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
let identifierKey;
|
|
1416
|
+
if (element.propertyName) {
|
|
1417
|
+
identifierKey = element.propertyName.getText();
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
identifierKey = element.name.getText();
|
|
1421
|
+
}
|
|
1422
|
+
if (identifierKey in this.#identifiers.namedImports) {
|
|
1423
|
+
this.#identifiers.namedImports[identifierKey] = element.name.getText();
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (this.compiler.isNamespaceImport(node.importClause.namedBindings)) {
|
|
1428
|
+
this.#identifiers.namespace = node.importClause.namedBindings.name.getText();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
resolveTestMemberMeta(node) {
|
|
1433
|
+
let flags = 0;
|
|
1434
|
+
let expression = node.expression;
|
|
1435
|
+
while (this.compiler.isPropertyAccessExpression(expression)) {
|
|
1436
|
+
if (expression.expression.getText() === this.#identifiers.namespace) {
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
switch (expression.name.getText()) {
|
|
1440
|
+
case "fail":
|
|
1441
|
+
flags |= 1;
|
|
1442
|
+
break;
|
|
1443
|
+
case "only":
|
|
1444
|
+
flags |= 2;
|
|
1445
|
+
break;
|
|
1446
|
+
case "skip":
|
|
1447
|
+
flags |= 4;
|
|
1448
|
+
break;
|
|
1449
|
+
case "todo":
|
|
1450
|
+
flags |= 8;
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
expression = expression.expression;
|
|
1454
|
+
}
|
|
1455
|
+
let identifierName;
|
|
1456
|
+
if (this.compiler.isPropertyAccessExpression(expression) &&
|
|
1457
|
+
expression.expression.getText() === this.#identifiers.namespace) {
|
|
1458
|
+
identifierName = expression.name.getText();
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
identifierName = Object.keys(this.#identifiers.namedImports).find((key) => this.#identifiers.namedImports[key] === expression.getText());
|
|
1462
|
+
}
|
|
1463
|
+
if (identifierName == null) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
switch (identifierName) {
|
|
1467
|
+
case "context":
|
|
1468
|
+
case "describe":
|
|
1469
|
+
return { brand: "describe", flags };
|
|
1470
|
+
case "it":
|
|
1471
|
+
case "test":
|
|
1472
|
+
return { brand: "test", flags };
|
|
1473
|
+
case "expect":
|
|
1474
|
+
return { brand: "expect", flags };
|
|
1475
|
+
}
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
class TestTree {
|
|
1481
|
+
compiler;
|
|
1482
|
+
diagnostics;
|
|
1483
|
+
sourceFile;
|
|
1484
|
+
typeChecker;
|
|
1485
|
+
members = [];
|
|
1486
|
+
constructor(compiler, diagnostics, sourceFile, typeChecker) {
|
|
1487
|
+
this.compiler = compiler;
|
|
1488
|
+
this.diagnostics = diagnostics;
|
|
1489
|
+
this.sourceFile = sourceFile;
|
|
1490
|
+
this.typeChecker = typeChecker;
|
|
1491
|
+
}
|
|
1492
|
+
get hasOnly() {
|
|
1493
|
+
function hasOnly(root) {
|
|
1494
|
+
return root.members.some((branch) => branch.flags & 2 || ("members" in branch && hasOnly(branch)));
|
|
1495
|
+
}
|
|
1496
|
+
return hasOnly(this);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
class CollectService {
|
|
1501
|
+
compiler;
|
|
1502
|
+
matcherIdentifiers = [
|
|
1503
|
+
"toBeAny",
|
|
1504
|
+
"toBeAssignable",
|
|
1505
|
+
"toBeBigInt",
|
|
1506
|
+
"toBeBoolean",
|
|
1507
|
+
"toBeNever",
|
|
1508
|
+
"toBeNull",
|
|
1509
|
+
"toBeNumber",
|
|
1510
|
+
"toBeString",
|
|
1511
|
+
"toBeSymbol",
|
|
1512
|
+
"toBeUndefined",
|
|
1513
|
+
"toBeUniqueSymbol",
|
|
1514
|
+
"toBeUnknown",
|
|
1515
|
+
"toBeVoid",
|
|
1516
|
+
"toEqual",
|
|
1517
|
+
"toMatch",
|
|
1518
|
+
"toRaiseError",
|
|
1519
|
+
];
|
|
1520
|
+
modifierIdentifiers = ["type"];
|
|
1521
|
+
notIdentifier = "not";
|
|
1522
|
+
constructor(compiler) {
|
|
1523
|
+
this.compiler = compiler;
|
|
1524
|
+
}
|
|
1525
|
+
#collectTestMembers(node, identifiers, parent) {
|
|
1526
|
+
if (this.compiler.isBlock(node)) {
|
|
1527
|
+
this.compiler.forEachChild(node, (node) => {
|
|
1528
|
+
this.#collectTestMembers(node, new IdentifierLookup(this.compiler, identifiers.clone()), parent);
|
|
1529
|
+
});
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (this.compiler.isCallExpression(node)) {
|
|
1533
|
+
const meta = identifiers.resolveTestMemberMeta(node);
|
|
1534
|
+
if (meta != null && (meta.brand === "describe" || meta.brand === "test")) {
|
|
1535
|
+
const testMember = new TestMember(meta.brand, node, parent, meta.flags);
|
|
1536
|
+
parent.members.push(testMember);
|
|
1537
|
+
this.compiler.forEachChild(node, (node) => {
|
|
1538
|
+
this.#collectTestMembers(node, identifiers, testMember);
|
|
1539
|
+
});
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (meta != null && meta.brand === "expect") {
|
|
1543
|
+
const modifierNode = this.#getMatchingChainNode(node, this.modifierIdentifiers);
|
|
1544
|
+
if (!modifierNode) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const notNode = this.#getMatchingChainNode(modifierNode, [this.notIdentifier]);
|
|
1548
|
+
const matcherNode = this.#getMatchingChainNode(notNode ?? modifierNode, this.matcherIdentifiers)?.parent;
|
|
1549
|
+
if (matcherNode == null || !this.#isMatcherNode(matcherNode)) {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
parent.members.push(new Assertion(meta.brand, node, parent, meta.flags, matcherNode, modifierNode, notNode));
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (this.compiler.isImportDeclaration(node)) {
|
|
1557
|
+
identifiers.handleImportDeclaration(node);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (this.compiler.isVariableDeclaration(node)) ;
|
|
1561
|
+
if (this.compiler.isBinaryExpression(node)) ;
|
|
1562
|
+
this.compiler.forEachChild(node, (node) => {
|
|
1563
|
+
this.#collectTestMembers(node, identifiers, parent);
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
createTestTree(sourceFile, semanticDiagnostics = [], typeChecker) {
|
|
1567
|
+
const testTree = new TestTree(this.compiler, semanticDiagnostics, sourceFile, typeChecker);
|
|
1568
|
+
this.#collectTestMembers(sourceFile, new IdentifierLookup(this.compiler), testTree);
|
|
1569
|
+
return testTree;
|
|
1570
|
+
}
|
|
1571
|
+
#getMatchingChainNode({ parent }, name) {
|
|
1572
|
+
if (this.compiler.isPropertyAccessExpression(parent) && name.includes(parent.name.getText())) {
|
|
1573
|
+
return parent;
|
|
1574
|
+
}
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
#isMatcherNode(node) {
|
|
1578
|
+
return this.compiler.isCallExpression(node) && this.compiler.isPropertyAccessExpression(node.expression);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
var TestMemberBrand;
|
|
1583
|
+
(function (TestMemberBrand) {
|
|
1584
|
+
TestMemberBrand["Describe"] = "describe";
|
|
1585
|
+
TestMemberBrand["Test"] = "test";
|
|
1586
|
+
TestMemberBrand["Expect"] = "expect";
|
|
1587
|
+
})(TestMemberBrand || (TestMemberBrand = {}));
|
|
1588
|
+
|
|
1589
|
+
var TestMemberFlags;
|
|
1590
|
+
(function (TestMemberFlags) {
|
|
1591
|
+
TestMemberFlags[TestMemberFlags["None"] = 0] = "None";
|
|
1592
|
+
TestMemberFlags[TestMemberFlags["Fail"] = 1] = "Fail";
|
|
1593
|
+
TestMemberFlags[TestMemberFlags["Only"] = 2] = "Only";
|
|
1594
|
+
TestMemberFlags[TestMemberFlags["Skip"] = 4] = "Skip";
|
|
1595
|
+
TestMemberFlags[TestMemberFlags["Todo"] = 8] = "Todo";
|
|
1596
|
+
})(TestMemberFlags || (TestMemberFlags = {}));
|
|
1597
|
+
|
|
1598
|
+
class ProjectService {
|
|
1599
|
+
compiler;
|
|
1600
|
+
#service;
|
|
1601
|
+
constructor(compiler) {
|
|
1602
|
+
this.compiler = compiler;
|
|
1603
|
+
function doNothing() {
|
|
1604
|
+
}
|
|
1605
|
+
function returnFalse() {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
function returnUndefined() {
|
|
1609
|
+
return undefined;
|
|
1610
|
+
}
|
|
1611
|
+
const noopWatcher = { close: doNothing };
|
|
1612
|
+
const noopLogger = {
|
|
1613
|
+
close: doNothing,
|
|
1614
|
+
endGroup: doNothing,
|
|
1615
|
+
getLogFileName: returnUndefined,
|
|
1616
|
+
hasLevel: returnFalse,
|
|
1617
|
+
info: doNothing,
|
|
1618
|
+
loggingEnabled: returnFalse,
|
|
1619
|
+
msg: doNothing,
|
|
1620
|
+
perftrc: doNothing,
|
|
1621
|
+
startGroup: doNothing,
|
|
1622
|
+
};
|
|
1623
|
+
const host = {
|
|
1624
|
+
...compiler.sys,
|
|
1625
|
+
clearImmediate,
|
|
1626
|
+
clearTimeout,
|
|
1627
|
+
setImmediate,
|
|
1628
|
+
setTimeout,
|
|
1629
|
+
watchDirectory: () => noopWatcher,
|
|
1630
|
+
watchFile: () => noopWatcher,
|
|
1631
|
+
};
|
|
1632
|
+
this.#service = new this.compiler.server.ProjectService({
|
|
1633
|
+
cancellationToken: this.compiler.server.nullCancellationToken,
|
|
1634
|
+
host,
|
|
1635
|
+
logger: noopLogger,
|
|
1636
|
+
session: undefined,
|
|
1637
|
+
useInferredProjectPerProjectRoot: true,
|
|
1638
|
+
useSingleInferredProject: false,
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
closeFile(filePath) {
|
|
1642
|
+
this.#service.closeClientFile(filePath);
|
|
1643
|
+
}
|
|
1644
|
+
getDefaultProject(filePath) {
|
|
1645
|
+
return this.#service.getDefaultProjectForFile(this.compiler.server.toNormalizedPath(filePath), true);
|
|
1646
|
+
}
|
|
1647
|
+
getLanguageService(filePath) {
|
|
1648
|
+
const project = this.getDefaultProject(filePath);
|
|
1649
|
+
if (!project) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
return project.getLanguageService(true);
|
|
1653
|
+
}
|
|
1654
|
+
openFile(filePath, sourceText, projectRootPath) {
|
|
1655
|
+
const { configFileErrors, configFileName } = this.#service.openClientFile(filePath, sourceText, undefined, projectRootPath);
|
|
1656
|
+
EventEmitter.dispatch([
|
|
1657
|
+
"project:info",
|
|
1658
|
+
{ compilerVersion: this.compiler.version, projectConfigFilePath: configFileName },
|
|
1659
|
+
]);
|
|
1660
|
+
if (configFileErrors && configFileErrors.length > 0) {
|
|
1661
|
+
EventEmitter.dispatch([
|
|
1662
|
+
"project:error",
|
|
1663
|
+
{ diagnostics: Diagnostic.fromDiagnostics(configFileErrors, this.compiler) },
|
|
1664
|
+
]);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
class Checker {
|
|
1670
|
+
compiler;
|
|
1671
|
+
constructor(compiler) {
|
|
1672
|
+
this.compiler = compiler;
|
|
1673
|
+
}
|
|
1674
|
+
#assertNonNullish(value, message) {
|
|
1675
|
+
if (value == null) {
|
|
1676
|
+
throw Error(message);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
#assertNonNullishSourceType(assertion) {
|
|
1680
|
+
this.#assertNonNullish(assertion.sourceType, "An argument for 'source' was not provided.");
|
|
1681
|
+
}
|
|
1682
|
+
#assertNonNullishTargetType(assertion) {
|
|
1683
|
+
this.#assertNonNullish(assertion.targetType, "An argument for 'target' was not provided.");
|
|
1684
|
+
}
|
|
1685
|
+
#assertNonNullishTypeChecker(assertion) {
|
|
1686
|
+
this.#assertNonNullish(assertion.typeChecker, "The 'typeChecker' was not provided.");
|
|
1687
|
+
}
|
|
1688
|
+
#assertStringsOrNumbers(nodes) {
|
|
1689
|
+
return nodes.every((expression) => this.compiler.isStringLiteral(expression) || this.compiler.isNumericLiteral(expression));
|
|
1690
|
+
}
|
|
1691
|
+
explain(assertion) {
|
|
1692
|
+
this.#assertNonNullishTypeChecker(assertion);
|
|
1693
|
+
const matcher = assertion.matcherName.getText();
|
|
1694
|
+
const origin = {
|
|
1695
|
+
breadcrumbs: assertion.ancestorNames,
|
|
1696
|
+
end: assertion.matcherName.getEnd(),
|
|
1697
|
+
file: assertion.matcherName.getSourceFile(),
|
|
1698
|
+
start: assertion.matcherName.getStart(),
|
|
1699
|
+
};
|
|
1700
|
+
switch (matcher) {
|
|
1701
|
+
case "toBeAssignable": {
|
|
1702
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1703
|
+
this.#assertNonNullishTargetType(assertion);
|
|
1704
|
+
const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
|
|
1705
|
+
const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
|
|
1706
|
+
return [
|
|
1707
|
+
Diagnostic.error(assertion.isNot
|
|
1708
|
+
? `Type '${targetTypeText}' is assignable to type '${sourceTypeText}'.`
|
|
1709
|
+
: `Type '${targetTypeText}' is not assignable to type '${sourceTypeText}'.`, origin),
|
|
1710
|
+
];
|
|
1711
|
+
}
|
|
1712
|
+
case "toBeAny":
|
|
1713
|
+
return this.#isType(assertion, "any");
|
|
1714
|
+
case "toBeBigInt":
|
|
1715
|
+
return this.#isType(assertion, "bigint");
|
|
1716
|
+
case "toBeBoolean":
|
|
1717
|
+
return this.#isType(assertion, "boolean");
|
|
1718
|
+
case "toBeNever":
|
|
1719
|
+
return this.#isType(assertion, "never");
|
|
1720
|
+
case "toBeNull":
|
|
1721
|
+
return this.#isType(assertion, "null");
|
|
1722
|
+
case "toBeNumber":
|
|
1723
|
+
return this.#isType(assertion, "number");
|
|
1724
|
+
case "toBeString":
|
|
1725
|
+
return this.#isType(assertion, "string");
|
|
1726
|
+
case "toBeSymbol":
|
|
1727
|
+
return this.#isType(assertion, "symbol");
|
|
1728
|
+
case "toBeUndefined":
|
|
1729
|
+
return this.#isType(assertion, "undefined");
|
|
1730
|
+
case "toBeUniqueSymbol":
|
|
1731
|
+
return this.#isType(assertion, "unique symbol");
|
|
1732
|
+
case "toBeUnknown":
|
|
1733
|
+
return this.#isType(assertion, "unknown");
|
|
1734
|
+
case "toBeVoid":
|
|
1735
|
+
return this.#isType(assertion, "void");
|
|
1736
|
+
case "toEqual": {
|
|
1737
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1738
|
+
this.#assertNonNullishTargetType(assertion);
|
|
1739
|
+
const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
|
|
1740
|
+
const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
|
|
1741
|
+
return [
|
|
1742
|
+
Diagnostic.error(assertion.isNot
|
|
1743
|
+
? `Type '${targetTypeText}' is identical to type '${sourceTypeText}'.`
|
|
1744
|
+
: `Type '${targetTypeText}' is not identical to type '${sourceTypeText}'.`, origin),
|
|
1745
|
+
];
|
|
1746
|
+
}
|
|
1747
|
+
case "toMatch": {
|
|
1748
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1749
|
+
this.#assertNonNullishTargetType(assertion);
|
|
1750
|
+
const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
|
|
1751
|
+
const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
|
|
1752
|
+
return [
|
|
1753
|
+
Diagnostic.error(assertion.isNot
|
|
1754
|
+
? `Type '${targetTypeText}' is a subtype of type '${sourceTypeText}'.`
|
|
1755
|
+
: `Type '${targetTypeText}' is not a subtype of type '${sourceTypeText}'.`, origin),
|
|
1756
|
+
];
|
|
1757
|
+
}
|
|
1758
|
+
case "toRaiseError": {
|
|
1759
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1760
|
+
if (!this.#assertStringsOrNumbers(assertion.targetArguments)) {
|
|
1761
|
+
throw new Error("An argument for 'target' must be of type 'string | number'.");
|
|
1762
|
+
}
|
|
1763
|
+
const sourceText = assertion.sourceType.source === 0 ? "Expression" : "Type definition";
|
|
1764
|
+
if (assertion.diagnostics.length === 0) {
|
|
1765
|
+
return [Diagnostic.error(`${sourceText} did not raise a type error.`, origin)];
|
|
1766
|
+
}
|
|
1767
|
+
if (assertion.isNot && assertion.targetArguments.length === 0) {
|
|
1768
|
+
const related = [
|
|
1769
|
+
Diagnostic.error(`The raised type error${assertion.diagnostics.length === 1 ? "" : "s"}:`),
|
|
1770
|
+
...Diagnostic.fromDiagnostics(assertion.diagnostics, this.compiler),
|
|
1771
|
+
];
|
|
1772
|
+
return [
|
|
1773
|
+
Diagnostic.error(`${sourceText} raised ${assertion.diagnostics.length === 1 ? "a" : assertion.diagnostics.length} type error${assertion.diagnostics.length === 1 ? "" : "s"}.`, origin).add({ related }),
|
|
1774
|
+
];
|
|
1775
|
+
}
|
|
1776
|
+
if (assertion.diagnostics.length !== assertion.targetArguments.length) {
|
|
1777
|
+
const expectedText = assertion.diagnostics.length > assertion.targetArguments.length
|
|
1778
|
+
? `only ${assertion.targetArguments.length} type error${assertion.targetArguments.length === 1 ? "" : "s"}`
|
|
1779
|
+
: `${assertion.targetArguments.length} type error${assertion.targetArguments.length === 1 ? "" : "s"}`;
|
|
1780
|
+
const foundText = assertion.diagnostics.length > assertion.targetArguments.length
|
|
1781
|
+
? `${assertion.diagnostics.length}`
|
|
1782
|
+
: `only ${assertion.diagnostics.length}`;
|
|
1783
|
+
const related = [
|
|
1784
|
+
Diagnostic.error(`The raised type error${assertion.diagnostics.length === 1 ? "" : "s"}:`),
|
|
1785
|
+
...Diagnostic.fromDiagnostics(assertion.diagnostics, this.compiler),
|
|
1786
|
+
];
|
|
1787
|
+
const diagnostic = Diagnostic.error(`Expected ${expectedText}, but ${foundText} ${assertion.diagnostics.length === 1 ? "was" : "were"} raised.`, origin).add({
|
|
1788
|
+
related,
|
|
1789
|
+
});
|
|
1790
|
+
return [diagnostic];
|
|
1791
|
+
}
|
|
1792
|
+
const diagnostics = [];
|
|
1793
|
+
assertion.targetArguments.forEach((argument, index) => {
|
|
1794
|
+
const diagnostic = assertion.diagnostics[index];
|
|
1795
|
+
if (!diagnostic) {
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
const isMatch = this.#matchExpectedError(diagnostic, argument);
|
|
1799
|
+
if (!assertion.isNot && !isMatch) {
|
|
1800
|
+
const expectedText = this.compiler.isStringLiteral(argument)
|
|
1801
|
+
? `matching substring '${argument.text}'`
|
|
1802
|
+
: `with code ${argument.text}`;
|
|
1803
|
+
const related = [
|
|
1804
|
+
Diagnostic.error("The raised type error:"),
|
|
1805
|
+
...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
|
|
1806
|
+
];
|
|
1807
|
+
diagnostics.push(Diagnostic.error(`${sourceText} did not raise a type error ${expectedText}.`, origin).add({ related }));
|
|
1808
|
+
}
|
|
1809
|
+
if (assertion.isNot && isMatch) {
|
|
1810
|
+
const expectedText = this.compiler.isStringLiteral(argument)
|
|
1811
|
+
? `matching substring '${argument.text}'`
|
|
1812
|
+
: `with code ${argument.text}`;
|
|
1813
|
+
const related = [
|
|
1814
|
+
Diagnostic.error("The raised type error:"),
|
|
1815
|
+
...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
|
|
1816
|
+
];
|
|
1817
|
+
diagnostics.push(Diagnostic.error(`${sourceText} raised a type error ${expectedText}.`, origin).add({ related }));
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
return diagnostics;
|
|
1821
|
+
}
|
|
1822
|
+
default:
|
|
1823
|
+
throw new Error(`The '${matcher}' matcher is not supported.`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
#hasTypeFlag(assertion, targetTypeFlag) {
|
|
1827
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1828
|
+
return Boolean(assertion.sourceType.type.flags & targetTypeFlag);
|
|
1829
|
+
}
|
|
1830
|
+
#isType(assertion, targetText) {
|
|
1831
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1832
|
+
this.#assertNonNullishTypeChecker(assertion);
|
|
1833
|
+
const origin = {
|
|
1834
|
+
breadcrumbs: assertion.ancestorNames,
|
|
1835
|
+
end: assertion.matcherName.getEnd(),
|
|
1836
|
+
file: assertion.matcherName.getSourceFile(),
|
|
1837
|
+
start: assertion.matcherName.getStart(),
|
|
1838
|
+
};
|
|
1839
|
+
const sourceText = assertion.typeChecker.typeToString(assertion.sourceType.type);
|
|
1840
|
+
return [
|
|
1841
|
+
Diagnostic.error(assertion.isNot ? `Type '${sourceText}' is '${targetText}'.` : `Type '${sourceText}' is not '${targetText}'.`, origin),
|
|
1842
|
+
];
|
|
1843
|
+
}
|
|
1844
|
+
match(assertion) {
|
|
1845
|
+
const matcher = assertion.matcherName.getText();
|
|
1846
|
+
switch (matcher) {
|
|
1847
|
+
case "toBeAssignable":
|
|
1848
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1849
|
+
this.#assertNonNullishTargetType(assertion);
|
|
1850
|
+
this.#assertNonNullish(assertion.typeChecker?.isTypeAssignableTo, "The 'isTypeAssignableTo' method is missing in the provided type checker.");
|
|
1851
|
+
return assertion.typeChecker.isTypeAssignableTo(assertion.targetType.type, assertion.sourceType.type);
|
|
1852
|
+
case "toBeAny": {
|
|
1853
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Any);
|
|
1854
|
+
}
|
|
1855
|
+
case "toBeBigInt": {
|
|
1856
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.BigInt);
|
|
1857
|
+
}
|
|
1858
|
+
case "toBeBoolean": {
|
|
1859
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Boolean);
|
|
1860
|
+
}
|
|
1861
|
+
case "toBeNever": {
|
|
1862
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Never);
|
|
1863
|
+
}
|
|
1864
|
+
case "toBeNull": {
|
|
1865
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Null);
|
|
1866
|
+
}
|
|
1867
|
+
case "toBeNumber": {
|
|
1868
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Number);
|
|
1869
|
+
}
|
|
1870
|
+
case "toBeString": {
|
|
1871
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.String);
|
|
1872
|
+
}
|
|
1873
|
+
case "toBeSymbol": {
|
|
1874
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.ESSymbol);
|
|
1875
|
+
}
|
|
1876
|
+
case "toBeUndefined": {
|
|
1877
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Undefined);
|
|
1878
|
+
}
|
|
1879
|
+
case "toBeUniqueSymbol": {
|
|
1880
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.UniqueESSymbol);
|
|
1881
|
+
}
|
|
1882
|
+
case "toBeUnknown": {
|
|
1883
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Unknown);
|
|
1884
|
+
}
|
|
1885
|
+
case "toBeVoid": {
|
|
1886
|
+
return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Void);
|
|
1887
|
+
}
|
|
1888
|
+
case "toEqual": {
|
|
1889
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1890
|
+
this.#assertNonNullishTargetType(assertion);
|
|
1891
|
+
this.#assertNonNullish(assertion.typeChecker?.isTypeIdenticalTo, "The 'isTypeIdenticalTo' method is missing in the provided type checker.");
|
|
1892
|
+
return assertion.typeChecker.isTypeIdenticalTo(assertion.sourceType.type, assertion.targetType.type);
|
|
1893
|
+
}
|
|
1894
|
+
case "toMatch": {
|
|
1895
|
+
this.#assertNonNullishSourceType(assertion);
|
|
1896
|
+
this.#assertNonNullishTargetType(assertion);
|
|
1897
|
+
this.#assertNonNullish(assertion.typeChecker?.isTypeSubtypeOf, "The 'isTypeSubtypeOf' method is missing in the provided type checker.");
|
|
1898
|
+
return assertion.typeChecker.isTypeSubtypeOf(assertion.sourceType.type, assertion.targetType.type);
|
|
1899
|
+
}
|
|
1900
|
+
case "toRaiseError": {
|
|
1901
|
+
if (!this.#assertStringsOrNumbers(assertion.targetArguments)) {
|
|
1902
|
+
throw new Error("An argument for 'target' must be of type 'string | number'.");
|
|
1903
|
+
}
|
|
1904
|
+
if (assertion.targetArguments.length === 0) {
|
|
1905
|
+
return assertion.diagnostics.length > 0;
|
|
1906
|
+
}
|
|
1907
|
+
if (assertion.diagnostics.length !== assertion.targetArguments.length) {
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
return assertion.targetArguments.every((expectedArgument, index) => {
|
|
1911
|
+
if (this.compiler.isStringLiteral(expectedArgument)) {
|
|
1912
|
+
return this.compiler
|
|
1913
|
+
.flattenDiagnosticMessageText(assertion.diagnostics[index]?.messageText, " ", 0)
|
|
1914
|
+
.includes(expectedArgument.text);
|
|
1915
|
+
}
|
|
1916
|
+
if (this.compiler.isNumericLiteral(expectedArgument)) {
|
|
1917
|
+
return Number(expectedArgument.text) === assertion.diagnostics[index]?.code;
|
|
1918
|
+
}
|
|
1919
|
+
return false;
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
default:
|
|
1923
|
+
throw new Error(`The '${matcher}' matcher is not supported.`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
#matchExpectedError(diagnostic, argument) {
|
|
1927
|
+
if (this.compiler.isStringLiteral(argument)) {
|
|
1928
|
+
return this.compiler.flattenDiagnosticMessageText(diagnostic.messageText, " ", 0).includes(argument.text);
|
|
1929
|
+
}
|
|
1930
|
+
if (this.compiler.isNumericLiteral(argument)) {
|
|
1931
|
+
return Number(argument.text) === diagnostic.code;
|
|
1932
|
+
}
|
|
1933
|
+
throw new Error("An argument for 'target' must be of type 'string | number'.");
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
class TestTreeWorker {
|
|
1938
|
+
resolvedConfig;
|
|
1939
|
+
compiler;
|
|
1940
|
+
#checker;
|
|
1941
|
+
#fileResult;
|
|
1942
|
+
#hasOnly;
|
|
1943
|
+
#position;
|
|
1944
|
+
#signal;
|
|
1945
|
+
constructor(resolvedConfig, compiler, options) {
|
|
1946
|
+
this.resolvedConfig = resolvedConfig;
|
|
1947
|
+
this.compiler = compiler;
|
|
1948
|
+
this.#checker = new Checker(compiler);
|
|
1949
|
+
this.#hasOnly = options.hasOnly || resolvedConfig.only != null || options.position != null;
|
|
1950
|
+
this.#position = options.position;
|
|
1951
|
+
this.#signal = options.signal;
|
|
1952
|
+
this.#fileResult = options.fileResult;
|
|
1953
|
+
}
|
|
1954
|
+
#resolveRunMode(mode, member) {
|
|
1955
|
+
if (member.flags & 1) {
|
|
1956
|
+
mode |= 1;
|
|
1957
|
+
}
|
|
1958
|
+
if (member.flags & 2 ||
|
|
1959
|
+
(this.resolvedConfig.only != null &&
|
|
1960
|
+
member.name.toLowerCase().includes(this.resolvedConfig.only.toLowerCase())) ||
|
|
1961
|
+
(this.#position != null && member.node.getStart() === this.#position)) {
|
|
1962
|
+
mode |= 2;
|
|
1963
|
+
}
|
|
1964
|
+
if (member.flags & 4 ||
|
|
1965
|
+
(this.resolvedConfig.skip != null && member.name.toLowerCase().includes(this.resolvedConfig.skip.toLowerCase()))) {
|
|
1966
|
+
mode |= 4;
|
|
1967
|
+
}
|
|
1968
|
+
if (member.flags & 8) {
|
|
1969
|
+
mode |= 8;
|
|
1970
|
+
}
|
|
1971
|
+
return mode;
|
|
1972
|
+
}
|
|
1973
|
+
visit(members, runMode, parentResult) {
|
|
1974
|
+
for (const member of members) {
|
|
1975
|
+
if (this.#signal?.aborted === true) {
|
|
1976
|
+
break;
|
|
1977
|
+
}
|
|
1978
|
+
const validationError = member.validate();
|
|
1979
|
+
if (validationError.length > 0) {
|
|
1980
|
+
EventEmitter.dispatch([
|
|
1981
|
+
"file:error",
|
|
1982
|
+
{
|
|
1983
|
+
diagnostics: validationError,
|
|
1984
|
+
result: this.#fileResult,
|
|
1985
|
+
},
|
|
1986
|
+
]);
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
switch (member.brand) {
|
|
1990
|
+
case "describe":
|
|
1991
|
+
this.#visitDescribe(member, runMode, parentResult);
|
|
1992
|
+
break;
|
|
1993
|
+
case "test":
|
|
1994
|
+
this.#visitTest(member, runMode, parentResult);
|
|
1995
|
+
break;
|
|
1996
|
+
case "expect":
|
|
1997
|
+
this.#visitAssertion(member, runMode, parentResult);
|
|
1998
|
+
break;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
#visitAssertion(assertion, runMode, parentResult) {
|
|
2003
|
+
const expectResult = new ExpectResult(assertion, parentResult);
|
|
2004
|
+
EventEmitter.dispatch(["expect:start", { result: expectResult }]);
|
|
2005
|
+
runMode = this.#resolveRunMode(runMode, assertion);
|
|
2006
|
+
if (runMode & 4 || (this.#hasOnly && !(runMode & 2))) {
|
|
2007
|
+
EventEmitter.dispatch(["expect:skip", { result: expectResult }]);
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (assertion.diagnostics.length > 0 && assertion.matcherName.getText() !== "toRaiseError") {
|
|
2011
|
+
EventEmitter.dispatch([
|
|
2012
|
+
"expect:error",
|
|
2013
|
+
{
|
|
2014
|
+
diagnostics: Diagnostic.fromDiagnostics(assertion.diagnostics, this.compiler),
|
|
2015
|
+
result: expectResult,
|
|
2016
|
+
},
|
|
2017
|
+
]);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const isPass = this.#checker.match(assertion);
|
|
2021
|
+
if (assertion.isNot ? !isPass : isPass) {
|
|
2022
|
+
if (runMode & 1) {
|
|
2023
|
+
const text = ["The assertion was supposed to fail, but it passed.", "Consider removing the '.fail' flag."];
|
|
2024
|
+
const origin = {
|
|
2025
|
+
end: assertion.node.getEnd(),
|
|
2026
|
+
file: assertion.node.getSourceFile(),
|
|
2027
|
+
start: assertion.node.getStart(),
|
|
2028
|
+
};
|
|
2029
|
+
EventEmitter.dispatch([
|
|
2030
|
+
"expect:error",
|
|
2031
|
+
{
|
|
2032
|
+
diagnostics: [Diagnostic.error(text, origin)],
|
|
2033
|
+
result: expectResult,
|
|
2034
|
+
},
|
|
2035
|
+
]);
|
|
2036
|
+
}
|
|
2037
|
+
else {
|
|
2038
|
+
EventEmitter.dispatch(["expect:pass", { result: expectResult }]);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
if (runMode & 1) {
|
|
2043
|
+
EventEmitter.dispatch(["expect:pass", { result: expectResult }]);
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
EventEmitter.dispatch([
|
|
2047
|
+
"expect:fail",
|
|
2048
|
+
{
|
|
2049
|
+
diagnostics: this.#checker.explain(assertion),
|
|
2050
|
+
result: expectResult,
|
|
2051
|
+
},
|
|
2052
|
+
]);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
#visitDescribe(describe, runMode, parentResult) {
|
|
2057
|
+
const describeResult = new DescribeResult(describe, parentResult);
|
|
2058
|
+
EventEmitter.dispatch(["describe:start", { result: describeResult }]);
|
|
2059
|
+
runMode = this.#resolveRunMode(runMode, describe);
|
|
2060
|
+
if (!(runMode & 4 || runMode & 8) && describe.diagnostics.length > 0) {
|
|
2061
|
+
EventEmitter.dispatch([
|
|
2062
|
+
"file:error",
|
|
2063
|
+
{
|
|
2064
|
+
diagnostics: Diagnostic.fromDiagnostics(describe.diagnostics, this.compiler),
|
|
2065
|
+
result: this.#fileResult,
|
|
2066
|
+
},
|
|
2067
|
+
]);
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
this.visit(describe.members, runMode, describeResult);
|
|
2071
|
+
}
|
|
2072
|
+
EventEmitter.dispatch(["describe:end", { result: describeResult }]);
|
|
2073
|
+
}
|
|
2074
|
+
#visitTest(test, runMode, parentResult) {
|
|
2075
|
+
const testResult = new TestResult(test, parentResult);
|
|
2076
|
+
EventEmitter.dispatch(["test:start", { result: testResult }]);
|
|
2077
|
+
runMode = this.#resolveRunMode(runMode, test);
|
|
2078
|
+
if (runMode & 8) {
|
|
2079
|
+
EventEmitter.dispatch(["test:todo", { result: testResult }]);
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (runMode & 4) {
|
|
2083
|
+
EventEmitter.dispatch(["test:skip", { result: testResult }]);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (test.diagnostics.length > 0) {
|
|
2087
|
+
EventEmitter.dispatch([
|
|
2088
|
+
"test:error",
|
|
2089
|
+
{
|
|
2090
|
+
diagnostics: Diagnostic.fromDiagnostics(test.diagnostics, this.compiler),
|
|
2091
|
+
result: testResult,
|
|
2092
|
+
},
|
|
2093
|
+
]);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
this.visit(test.members, runMode, testResult);
|
|
2097
|
+
if (testResult.expectCount.skipped > 0 && testResult.expectCount.skipped === testResult.expectCount.total) {
|
|
2098
|
+
EventEmitter.dispatch(["test:skip", { result: testResult }]);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (testResult.expectCount.failed > 0) {
|
|
2102
|
+
EventEmitter.dispatch(["test:fail", { result: testResult }]);
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
EventEmitter.dispatch(["test:pass", { result: testResult }]);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
class TestFileRunner {
|
|
2111
|
+
resolvedConfig;
|
|
2112
|
+
compiler;
|
|
2113
|
+
#collectService;
|
|
2114
|
+
#projectService;
|
|
2115
|
+
constructor(resolvedConfig, compiler) {
|
|
2116
|
+
this.resolvedConfig = resolvedConfig;
|
|
2117
|
+
this.compiler = compiler;
|
|
2118
|
+
this.#collectService = new CollectService(compiler);
|
|
2119
|
+
this.#projectService = new ProjectService(compiler);
|
|
2120
|
+
}
|
|
2121
|
+
run(testFile, signal) {
|
|
2122
|
+
if (signal?.aborted === true) {
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
const testFilePath = fileURLToPath(testFile);
|
|
2126
|
+
const position = testFile.searchParams.has("position") ? Number(testFile.searchParams.get("position")) : undefined;
|
|
2127
|
+
this.#projectService.openFile(testFilePath, undefined, this.resolvedConfig.rootPath);
|
|
2128
|
+
const fileResult = new FileResult(testFile);
|
|
2129
|
+
EventEmitter.dispatch(["file:start", { result: fileResult }]);
|
|
2130
|
+
this.#runFile(testFilePath, fileResult, position, signal);
|
|
2131
|
+
EventEmitter.dispatch(["file:end", { result: fileResult }]);
|
|
2132
|
+
this.#projectService.closeFile(testFilePath);
|
|
2133
|
+
}
|
|
2134
|
+
#runFile(testFilePath, fileResult, position, signal) {
|
|
2135
|
+
const languageService = this.#projectService.getLanguageService(testFilePath);
|
|
2136
|
+
if (!languageService) {
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const syntacticDiagnostics = languageService.getSyntacticDiagnostics(testFilePath);
|
|
2140
|
+
if (syntacticDiagnostics.length > 0) {
|
|
2141
|
+
EventEmitter.dispatch([
|
|
2142
|
+
"file:error",
|
|
2143
|
+
{
|
|
2144
|
+
diagnostics: Diagnostic.fromDiagnostics(syntacticDiagnostics, this.compiler),
|
|
2145
|
+
result: fileResult,
|
|
2146
|
+
},
|
|
2147
|
+
]);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
const semanticDiagnostics = languageService.getSemanticDiagnostics(testFilePath);
|
|
2151
|
+
const program = languageService.getProgram();
|
|
2152
|
+
if (!program) {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
const sourceFile = program.getSourceFile(testFilePath);
|
|
2156
|
+
if (!sourceFile) {
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const typeChecker = program.getTypeChecker();
|
|
2160
|
+
const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics, typeChecker);
|
|
2161
|
+
if (testTree.diagnostics.length > 0) {
|
|
2162
|
+
EventEmitter.dispatch([
|
|
2163
|
+
"file:error",
|
|
2164
|
+
{
|
|
2165
|
+
diagnostics: Diagnostic.fromDiagnostics(testTree.diagnostics, this.compiler),
|
|
2166
|
+
result: fileResult,
|
|
2167
|
+
},
|
|
2168
|
+
]);
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const testTreeWorker = new TestTreeWorker(this.resolvedConfig, this.compiler, {
|
|
2172
|
+
fileResult,
|
|
2173
|
+
hasOnly: testTree.hasOnly,
|
|
2174
|
+
position,
|
|
2175
|
+
signal,
|
|
2176
|
+
});
|
|
2177
|
+
testTreeWorker.visit(testTree.members, 0, undefined);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
class TaskRunner {
|
|
2182
|
+
resolvedConfig;
|
|
2183
|
+
#resultManager;
|
|
2184
|
+
#storeService;
|
|
2185
|
+
constructor(resolvedConfig, storeService) {
|
|
2186
|
+
this.resolvedConfig = resolvedConfig;
|
|
2187
|
+
this.#resultManager = new ResultManager();
|
|
2188
|
+
this.#storeService = storeService;
|
|
2189
|
+
EventEmitter.addHandler((event) => {
|
|
2190
|
+
this.#resultManager.handleEvent(event);
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
async run(testFiles, target, signal) {
|
|
2194
|
+
const result = new Result(this.resolvedConfig, testFiles);
|
|
2195
|
+
EventEmitter.dispatch(["start", { result }]);
|
|
2196
|
+
for (const versionTag of target) {
|
|
2197
|
+
const targetResult = new TargetResult(versionTag, testFiles);
|
|
2198
|
+
EventEmitter.dispatch(["target:start", { result: targetResult }]);
|
|
2199
|
+
const compiler = await this.#storeService.loadCompilerModule(versionTag, signal);
|
|
2200
|
+
if (compiler) {
|
|
2201
|
+
const testFileRunner = new TestFileRunner(this.resolvedConfig, compiler);
|
|
2202
|
+
for (const testFile of testFiles) {
|
|
2203
|
+
testFileRunner.run(testFile, signal);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
EventEmitter.dispatch(["target:end", { result: targetResult }]);
|
|
2207
|
+
}
|
|
2208
|
+
EventEmitter.dispatch(["end", { result }]);
|
|
2209
|
+
return result;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
class TSTyche {
|
|
2214
|
+
resolvedConfig;
|
|
2215
|
+
#abortController = new AbortController();
|
|
2216
|
+
#storeService;
|
|
2217
|
+
#taskRunner;
|
|
2218
|
+
constructor(resolvedConfig, storeService) {
|
|
2219
|
+
this.resolvedConfig = resolvedConfig;
|
|
2220
|
+
this.#storeService = storeService;
|
|
2221
|
+
this.#taskRunner = new TaskRunner(this.resolvedConfig, this.#storeService);
|
|
2222
|
+
this.#addEventHandlers();
|
|
2223
|
+
}
|
|
2224
|
+
static get version() {
|
|
2225
|
+
const packageConfig = readFileSync(new URL("../package.json", import.meta.url), { encoding: "utf8" });
|
|
2226
|
+
const { version } = JSON.parse(packageConfig);
|
|
2227
|
+
return version;
|
|
2228
|
+
}
|
|
2229
|
+
#addEventHandlers() {
|
|
2230
|
+
EventEmitter.addHandler(([eventName, payload]) => {
|
|
2231
|
+
if (eventName.includes("error") || eventName.includes("fail")) {
|
|
2232
|
+
if ("diagnostics" in payload &&
|
|
2233
|
+
!payload.diagnostics.some(({ category }) => category === "error")) {
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
process.exitCode = 1;
|
|
2237
|
+
if (this.resolvedConfig.failFast) {
|
|
2238
|
+
this.#abortController.abort();
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
const outputHandlers = [new ThoroughReporter(this.resolvedConfig), new SummaryReporter(this.resolvedConfig)];
|
|
2243
|
+
for (const outputHandler of outputHandlers) {
|
|
2244
|
+
EventEmitter.addHandler((event) => {
|
|
2245
|
+
outputHandler.handleEvent(event);
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
#normalizePaths(testFiles) {
|
|
2250
|
+
return testFiles.map((filePath) => {
|
|
2251
|
+
if (typeof filePath !== "string") {
|
|
2252
|
+
return filePath;
|
|
2253
|
+
}
|
|
2254
|
+
if (filePath.startsWith("file:")) {
|
|
2255
|
+
return new URL(filePath);
|
|
2256
|
+
}
|
|
2257
|
+
return pathToFileURL(filePath);
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
async run(testFiles) {
|
|
2261
|
+
await this.#taskRunner.run(this.#normalizePaths(testFiles), this.resolvedConfig.target, this.#abortController.signal);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
class OptionDefinitionsMap {
|
|
2266
|
+
static #definitions = [
|
|
2267
|
+
{
|
|
2268
|
+
brand: "boolean",
|
|
2269
|
+
description: "Do not raise an error, if no test files are selected.",
|
|
2270
|
+
group: 4 | 2,
|
|
2271
|
+
name: "allowNoTestFiles",
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
brand: "string",
|
|
2275
|
+
description: "The path to a TSTyche configuration file.",
|
|
2276
|
+
group: 2,
|
|
2277
|
+
name: "config",
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
brand: "boolean",
|
|
2281
|
+
description: "Stop running tests after the first failed assertion.",
|
|
2282
|
+
group: 4 | 2,
|
|
2283
|
+
name: "failFast",
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
brand: "boolean",
|
|
2287
|
+
description: "Print the list of CLI options with brief descriptions and exit.",
|
|
2288
|
+
group: 2,
|
|
2289
|
+
name: "help",
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
brand: "boolean",
|
|
2293
|
+
description: "Install specified versions of the 'typescript' package and exit.",
|
|
2294
|
+
group: 2,
|
|
2295
|
+
name: "install",
|
|
2296
|
+
},
|
|
2297
|
+
{
|
|
2298
|
+
brand: "boolean",
|
|
2299
|
+
description: "Print the list of the selected test files and exit.",
|
|
2300
|
+
group: 2,
|
|
2301
|
+
name: "listFiles",
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
brand: "string",
|
|
2305
|
+
description: "Only run tests with matching name.",
|
|
2306
|
+
group: 2,
|
|
2307
|
+
name: "only",
|
|
2308
|
+
},
|
|
2309
|
+
{
|
|
2310
|
+
brand: "boolean",
|
|
2311
|
+
description: "Remove all installed versions of the 'typescript' package and exit.",
|
|
2312
|
+
group: 2,
|
|
2313
|
+
name: "prune",
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
brand: "string",
|
|
2317
|
+
description: "The path to a directory containing files of a test project.",
|
|
2318
|
+
group: 4,
|
|
2319
|
+
name: "rootPath",
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
brand: "boolean",
|
|
2323
|
+
description: "Print the resolved configuration and exit.",
|
|
2324
|
+
group: 2,
|
|
2325
|
+
name: "showConfig",
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
brand: "string",
|
|
2329
|
+
description: "Skip tests with matching name.",
|
|
2330
|
+
group: 2,
|
|
2331
|
+
name: "skip",
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
brand: "list",
|
|
2335
|
+
description: "The list of TypeScript versions to be tested on.",
|
|
2336
|
+
group: 2 | 4,
|
|
2337
|
+
items: {
|
|
2338
|
+
brand: "string",
|
|
2339
|
+
name: "target",
|
|
2340
|
+
pattern: "^([45]\\.[0-9](\\.[0-9])?)|beta|latest|next|rc$",
|
|
2341
|
+
},
|
|
2342
|
+
name: "target",
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
brand: "list",
|
|
2346
|
+
description: "The list of glob patterns matching the test files.",
|
|
2347
|
+
group: 4,
|
|
2348
|
+
items: {
|
|
2349
|
+
brand: "string",
|
|
2350
|
+
name: "testFileMatch",
|
|
2351
|
+
},
|
|
2352
|
+
name: "testFileMatch",
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
brand: "boolean",
|
|
2356
|
+
description: "Fetch the 'typescript' package metadata from the registry and exit.",
|
|
2357
|
+
group: 2,
|
|
2358
|
+
name: "update",
|
|
2359
|
+
},
|
|
2360
|
+
{
|
|
2361
|
+
brand: "boolean",
|
|
2362
|
+
description: "Print the version number and exit.",
|
|
2363
|
+
group: 2,
|
|
2364
|
+
name: "version",
|
|
2365
|
+
},
|
|
2366
|
+
];
|
|
2367
|
+
static for(optionGroup) {
|
|
2368
|
+
const definitionMap = new Map();
|
|
2369
|
+
for (const definition of this.#definitions) {
|
|
2370
|
+
if (definition.group & optionGroup) {
|
|
2371
|
+
definitionMap.set(definition.name, definition);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return definitionMap;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
class OptionDiagnosticText {
|
|
2379
|
+
#optionGroup;
|
|
2380
|
+
constructor(optionGroup) {
|
|
2381
|
+
this.#optionGroup = optionGroup;
|
|
2382
|
+
}
|
|
2383
|
+
doubleQuotesExpected() {
|
|
2384
|
+
return "String literal with double quotes expected.";
|
|
2385
|
+
}
|
|
2386
|
+
expectsArgument(optionName) {
|
|
2387
|
+
optionName = this.#optionName(optionName);
|
|
2388
|
+
return `Option '${optionName}' expects an argument.`;
|
|
2389
|
+
}
|
|
2390
|
+
expectsListItemType(optionName, optionBrand) {
|
|
2391
|
+
return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
|
|
2392
|
+
}
|
|
2393
|
+
fileDoesNotExist(filePath) {
|
|
2394
|
+
return `The specified path '${filePath}' does not exist.`;
|
|
2395
|
+
}
|
|
2396
|
+
#optionName(optionName) {
|
|
2397
|
+
switch (this.#optionGroup) {
|
|
2398
|
+
case 2:
|
|
2399
|
+
return `--${optionName}`;
|
|
2400
|
+
case 4:
|
|
2401
|
+
return optionName;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
requiresArgumentType(optionName, optionBrand) {
|
|
2405
|
+
optionName = this.#optionName(optionName);
|
|
2406
|
+
return `Option '${optionName}' requires an argument of type ${optionBrand}.`;
|
|
2407
|
+
}
|
|
2408
|
+
unknownOption(optionName) {
|
|
2409
|
+
return `Unknown option '${optionName}'.`;
|
|
2410
|
+
}
|
|
2411
|
+
unknownProperty(optionName) {
|
|
2412
|
+
return `Unknown property '${optionName}'.`;
|
|
2413
|
+
}
|
|
2414
|
+
versionIsNotSupported(value) {
|
|
2415
|
+
return `TypeScript version '${value}' is not supported.`;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
class OptionUsageText {
|
|
2420
|
+
#optionDiagnosticText;
|
|
2421
|
+
#optionGroup;
|
|
2422
|
+
#storeService;
|
|
2423
|
+
constructor(optionGroup, storeService) {
|
|
2424
|
+
this.#optionGroup = optionGroup;
|
|
2425
|
+
this.#optionDiagnosticText = new OptionDiagnosticText(this.#optionGroup);
|
|
2426
|
+
this.#storeService = storeService;
|
|
2427
|
+
}
|
|
2428
|
+
get(optionName, optionBrand) {
|
|
2429
|
+
const usageText = [];
|
|
2430
|
+
switch (optionName) {
|
|
2431
|
+
case "target": {
|
|
2432
|
+
const supportedTags = this.#storeService.getSupportedTags();
|
|
2433
|
+
const supportedTagsText = `Supported tags: ${["'", supportedTags.join("', '"), "'"].join("")}.`;
|
|
2434
|
+
switch (this.#optionGroup) {
|
|
2435
|
+
case 2:
|
|
2436
|
+
usageText.push("Argument for the '--target' option must be a single tag or a comma separated list of versions.", "Usage examples: '--target 4.9', '--target 5.0.4', '--target 4.7,4.8,latest'.", supportedTagsText);
|
|
2437
|
+
break;
|
|
2438
|
+
case 4:
|
|
2439
|
+
usageText.push("Item of the 'target' list must be a supported version tag.", supportedTagsText);
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
2442
|
+
break;
|
|
2443
|
+
}
|
|
2444
|
+
default:
|
|
2445
|
+
usageText.push(this.#optionDiagnosticText.requiresArgumentType(optionName, optionBrand));
|
|
2446
|
+
}
|
|
2447
|
+
return usageText;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
class OptionValidator {
|
|
2452
|
+
#onDiagnostic;
|
|
2453
|
+
#optionDiagnosticText;
|
|
2454
|
+
#optionGroup;
|
|
2455
|
+
#optionUsageText;
|
|
2456
|
+
#storeService;
|
|
2457
|
+
constructor(optionGroup, storeService, onDiagnostic) {
|
|
2458
|
+
this.#optionGroup = optionGroup;
|
|
2459
|
+
this.#storeService = storeService;
|
|
2460
|
+
this.#onDiagnostic = onDiagnostic;
|
|
2461
|
+
this.#optionDiagnosticText = new OptionDiagnosticText(this.#optionGroup);
|
|
2462
|
+
this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
|
|
2463
|
+
}
|
|
2464
|
+
check(optionName, optionValue, optionBrand, origin) {
|
|
2465
|
+
switch (optionName) {
|
|
2466
|
+
case "config":
|
|
2467
|
+
case "rootPath":
|
|
2468
|
+
if (!existsSync(optionValue)) {
|
|
2469
|
+
const text = [this.#optionDiagnosticText.fileDoesNotExist(optionValue)];
|
|
2470
|
+
this.#onDiagnostic(Diagnostic.error(text, origin));
|
|
2471
|
+
}
|
|
2472
|
+
break;
|
|
2473
|
+
case "target":
|
|
2474
|
+
{
|
|
2475
|
+
if (!this.#storeService.validateTag(optionValue)) {
|
|
2476
|
+
this.#onDiagnostic(Diagnostic.error([
|
|
2477
|
+
this.#optionDiagnosticText.versionIsNotSupported(optionValue),
|
|
2478
|
+
...this.#optionUsageText.get(optionName, optionBrand),
|
|
2479
|
+
], origin));
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
break;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
class CommandLineOptionsWorker {
|
|
2488
|
+
#commandLineOptionDefinitions;
|
|
2489
|
+
#commandLineOptions;
|
|
2490
|
+
#onDiagnostic;
|
|
2491
|
+
#optionDiagnosticText;
|
|
2492
|
+
#optionUsageText;
|
|
2493
|
+
#optionValidator;
|
|
2494
|
+
#pathMatch;
|
|
2495
|
+
#storeService;
|
|
2496
|
+
constructor(commandLineOptions, pathMatch, storeService, onDiagnostic) {
|
|
2497
|
+
this.#commandLineOptions = commandLineOptions;
|
|
2498
|
+
this.#pathMatch = pathMatch;
|
|
2499
|
+
this.#storeService = storeService;
|
|
2500
|
+
this.#onDiagnostic = onDiagnostic;
|
|
2501
|
+
this.#commandLineOptionDefinitions = OptionDefinitionsMap.for(2);
|
|
2502
|
+
this.#optionDiagnosticText = new OptionDiagnosticText(2);
|
|
2503
|
+
this.#optionUsageText = new OptionUsageText(2, this.#storeService);
|
|
2504
|
+
this.#optionValidator = new OptionValidator(2, this.#storeService, this.#onDiagnostic);
|
|
2505
|
+
}
|
|
2506
|
+
#normalizePath(filePath) {
|
|
2507
|
+
if (path.sep === "/") {
|
|
2508
|
+
return filePath;
|
|
2509
|
+
}
|
|
2510
|
+
return filePath.replace(/\\/g, "/");
|
|
2511
|
+
}
|
|
2512
|
+
#onExpectsArgumentDiagnostic(optionDefinition) {
|
|
2513
|
+
const text = [
|
|
2514
|
+
this.#optionDiagnosticText.expectsArgument(optionDefinition.name),
|
|
2515
|
+
...this.#optionUsageText.get(optionDefinition.name, optionDefinition.brand),
|
|
2516
|
+
];
|
|
2517
|
+
this.#onDiagnostic(Diagnostic.error(text));
|
|
2518
|
+
}
|
|
2519
|
+
parse(commandLineArgs) {
|
|
2520
|
+
let index = 0;
|
|
2521
|
+
let arg = commandLineArgs[index];
|
|
2522
|
+
while (arg !== undefined) {
|
|
2523
|
+
index++;
|
|
2524
|
+
if (arg.startsWith("--")) {
|
|
2525
|
+
const optionName = arg.slice(2);
|
|
2526
|
+
const optionDefinition = this.#commandLineOptionDefinitions.get(optionName);
|
|
2527
|
+
if (optionDefinition) {
|
|
2528
|
+
index = this.#parseOptionValue(commandLineArgs, index, optionDefinition);
|
|
2529
|
+
}
|
|
2530
|
+
else {
|
|
2531
|
+
this.#onDiagnostic(Diagnostic.error(this.#optionDiagnosticText.unknownOption(arg)));
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
else if (arg.startsWith("-")) {
|
|
2535
|
+
this.#onDiagnostic(Diagnostic.error(this.#optionDiagnosticText.unknownOption(arg)));
|
|
2536
|
+
}
|
|
2537
|
+
else {
|
|
2538
|
+
this.#pathMatch.push(arg);
|
|
2539
|
+
}
|
|
2540
|
+
arg = commandLineArgs[index];
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
#parseOptionValue(commandLineArgs, index, optionDefinition) {
|
|
2544
|
+
let optionValue = this.#resolveOptionValue(commandLineArgs[index]);
|
|
2545
|
+
switch (optionDefinition.brand) {
|
|
2546
|
+
case "boolean":
|
|
2547
|
+
this.#commandLineOptions[optionDefinition.name] = optionValue !== "false";
|
|
2548
|
+
if (optionValue === "false" || optionValue === "true") {
|
|
2549
|
+
index++;
|
|
2550
|
+
}
|
|
2551
|
+
break;
|
|
2552
|
+
case "list":
|
|
2553
|
+
if (optionValue != null) {
|
|
2554
|
+
const optionValues = optionValue
|
|
2555
|
+
.split(",")
|
|
2556
|
+
.map((value) => value.trim())
|
|
2557
|
+
.filter((value) => value !== "");
|
|
2558
|
+
for (const optionValue of optionValues) {
|
|
2559
|
+
this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
2560
|
+
}
|
|
2561
|
+
this.#commandLineOptions[optionDefinition.name] = optionValues;
|
|
2562
|
+
index++;
|
|
2563
|
+
break;
|
|
2564
|
+
}
|
|
2565
|
+
this.#onExpectsArgumentDiagnostic(optionDefinition);
|
|
2566
|
+
break;
|
|
2567
|
+
case "number":
|
|
2568
|
+
if (optionValue != null) {
|
|
2569
|
+
this.#commandLineOptions[optionDefinition.name] = Number(optionValue);
|
|
2570
|
+
index++;
|
|
2571
|
+
break;
|
|
2572
|
+
}
|
|
2573
|
+
this.#onExpectsArgumentDiagnostic(optionDefinition);
|
|
2574
|
+
break;
|
|
2575
|
+
case "string":
|
|
2576
|
+
if (optionValue != null) {
|
|
2577
|
+
if (optionDefinition.name === "config") {
|
|
2578
|
+
optionValue = this.#normalizePath(path.resolve(optionValue));
|
|
2579
|
+
}
|
|
2580
|
+
this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
2581
|
+
this.#commandLineOptions[optionDefinition.name] = optionValue;
|
|
2582
|
+
index++;
|
|
2583
|
+
break;
|
|
2584
|
+
}
|
|
2585
|
+
this.#onExpectsArgumentDiagnostic(optionDefinition);
|
|
2586
|
+
break;
|
|
2587
|
+
}
|
|
2588
|
+
return index;
|
|
2589
|
+
}
|
|
2590
|
+
#resolveOptionValue(optionValue) {
|
|
2591
|
+
if (optionValue == null || optionValue.startsWith("-")) {
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
return optionValue;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
class ConfigFileOptionsWorker {
|
|
2599
|
+
compiler;
|
|
2600
|
+
#configFileOptionDefinitions;
|
|
2601
|
+
#configFileOptions;
|
|
2602
|
+
#configFilePath;
|
|
2603
|
+
#onDiagnostic;
|
|
2604
|
+
#optionDiagnosticText;
|
|
2605
|
+
#optionValidator;
|
|
2606
|
+
#storeService;
|
|
2607
|
+
constructor(compiler, configFileOptions, configFilePath, storeService, onDiagnostic) {
|
|
2608
|
+
this.compiler = compiler;
|
|
2609
|
+
this.#configFileOptions = configFileOptions;
|
|
2610
|
+
this.#configFilePath = configFilePath;
|
|
2611
|
+
this.#storeService = storeService;
|
|
2612
|
+
this.#onDiagnostic = onDiagnostic;
|
|
2613
|
+
this.#configFileOptionDefinitions = OptionDefinitionsMap.for(4);
|
|
2614
|
+
this.#optionDiagnosticText = new OptionDiagnosticText(4);
|
|
2615
|
+
this.#optionValidator = new OptionValidator(4, this.#storeService, this.#onDiagnostic);
|
|
2616
|
+
}
|
|
2617
|
+
#isDoubleQuotedString(node, sourceFile) {
|
|
2618
|
+
return (node.kind === this.compiler.SyntaxKind.StringLiteral &&
|
|
2619
|
+
sourceFile.text.slice(this.#skipTrivia(node.pos, sourceFile), node.end).startsWith('"'));
|
|
2620
|
+
}
|
|
2621
|
+
#normalizePath(filePath) {
|
|
2622
|
+
if (path.sep === "/") {
|
|
2623
|
+
return filePath;
|
|
2624
|
+
}
|
|
2625
|
+
return filePath.replace(/\\/g, "/");
|
|
2626
|
+
}
|
|
2627
|
+
async parse(sourceText) {
|
|
2628
|
+
if (sourceText === "") {
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
const configSourceFile = this.compiler.parseJsonText(this.#configFilePath, sourceText);
|
|
2632
|
+
if (configSourceFile.parseDiagnostics.length > 0) {
|
|
2633
|
+
for (const diagnostic of Diagnostic.fromDiagnostics(configSourceFile.parseDiagnostics, this.compiler)) {
|
|
2634
|
+
this.#onDiagnostic(diagnostic);
|
|
2635
|
+
}
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
const rootExpression = configSourceFile.statements[0]?.expression;
|
|
2639
|
+
if (rootExpression == null || !this.compiler.isObjectLiteralExpression(rootExpression)) {
|
|
2640
|
+
const origin = { end: 0, file: configSourceFile, start: 0 };
|
|
2641
|
+
this.#onDiagnostic(Diagnostic.error("The root value of a configuration file must be an object literal.", origin));
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
for (const property of rootExpression.properties) {
|
|
2645
|
+
if (this.compiler.isPropertyAssignment(property)) {
|
|
2646
|
+
if (!this.#isDoubleQuotedString(property.name, configSourceFile)) {
|
|
2647
|
+
const origin = {
|
|
2648
|
+
end: property.end,
|
|
2649
|
+
file: configSourceFile,
|
|
2650
|
+
start: this.#skipTrivia(property.pos, configSourceFile),
|
|
2651
|
+
};
|
|
2652
|
+
this.#onDiagnostic(Diagnostic.error(this.#optionDiagnosticText.doubleQuotesExpected(), origin));
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
const optionName = this.#resolvePropertyName(property);
|
|
2656
|
+
if (optionName === "$schema") {
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
const optionDefinition = this.#configFileOptionDefinitions.get(optionName);
|
|
2660
|
+
if (optionDefinition) {
|
|
2661
|
+
this.#configFileOptions[optionDefinition.name] = await this.#parseOptionValue(configSourceFile, property.initializer, optionDefinition);
|
|
2662
|
+
}
|
|
2663
|
+
else {
|
|
2664
|
+
const origin = {
|
|
2665
|
+
end: property.end,
|
|
2666
|
+
file: configSourceFile,
|
|
2667
|
+
start: this.#skipTrivia(property.pos, configSourceFile),
|
|
2668
|
+
};
|
|
2669
|
+
this.#onDiagnostic(Diagnostic.error(this.#optionDiagnosticText.unknownOption(optionName), origin));
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
async #parseOptionValue(sourceFile, valueExpression, optionDefinition, isListItem = false) {
|
|
2676
|
+
switch (valueExpression.kind) {
|
|
2677
|
+
case this.compiler.SyntaxKind.NullKeyword:
|
|
2678
|
+
if (optionDefinition.nullable === true) {
|
|
2679
|
+
return null;
|
|
2680
|
+
}
|
|
2681
|
+
break;
|
|
2682
|
+
case this.compiler.SyntaxKind.TrueKeyword:
|
|
2683
|
+
if (optionDefinition.brand === "boolean") {
|
|
2684
|
+
return true;
|
|
2685
|
+
}
|
|
2686
|
+
break;
|
|
2687
|
+
case this.compiler.SyntaxKind.FalseKeyword:
|
|
2688
|
+
if (optionDefinition.brand === "boolean") {
|
|
2689
|
+
return false;
|
|
2690
|
+
}
|
|
2691
|
+
break;
|
|
2692
|
+
case this.compiler.SyntaxKind.StringLiteral:
|
|
2693
|
+
if (!this.#isDoubleQuotedString(valueExpression, sourceFile)) {
|
|
2694
|
+
const origin = {
|
|
2695
|
+
end: valueExpression.end,
|
|
2696
|
+
file: sourceFile,
|
|
2697
|
+
start: this.#skipTrivia(valueExpression.pos, sourceFile),
|
|
2698
|
+
};
|
|
2699
|
+
this.#onDiagnostic(Diagnostic.error(this.#optionDiagnosticText.doubleQuotesExpected(), origin));
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (optionDefinition.brand === "string") {
|
|
2703
|
+
let value = valueExpression.text;
|
|
2704
|
+
if (optionDefinition.name === "rootPath") {
|
|
2705
|
+
value = this.#normalizePath(path.resolve(path.dirname(this.#configFilePath), value));
|
|
2706
|
+
}
|
|
2707
|
+
const origin = {
|
|
2708
|
+
end: valueExpression.end,
|
|
2709
|
+
file: sourceFile,
|
|
2710
|
+
start: this.#skipTrivia(valueExpression.pos, sourceFile),
|
|
2711
|
+
};
|
|
2712
|
+
this.#optionValidator.check(optionDefinition.name, value, optionDefinition.brand, origin);
|
|
2713
|
+
return value;
|
|
2714
|
+
}
|
|
2715
|
+
break;
|
|
2716
|
+
case this.compiler.SyntaxKind.NumericLiteral:
|
|
2717
|
+
if (optionDefinition.brand === "number") {
|
|
2718
|
+
return Number(valueExpression.text);
|
|
2719
|
+
}
|
|
2720
|
+
break;
|
|
2721
|
+
case this.compiler.SyntaxKind.ArrayLiteralExpression:
|
|
2722
|
+
if (optionDefinition.brand === "list") {
|
|
2723
|
+
const value = [];
|
|
2724
|
+
for (const element of valueExpression.elements) {
|
|
2725
|
+
value.push(await this.#parseOptionValue(sourceFile, element, optionDefinition.items, true));
|
|
2726
|
+
}
|
|
2727
|
+
return value;
|
|
2728
|
+
}
|
|
2729
|
+
break;
|
|
2730
|
+
case this.compiler.SyntaxKind.ObjectLiteralExpression:
|
|
2731
|
+
if (optionDefinition.brand === "object" && "getDefinition" in optionDefinition) {
|
|
2732
|
+
const propertyDefinition = optionDefinition.getDefinition(4);
|
|
2733
|
+
const propertyOptions = {};
|
|
2734
|
+
for (const property of valueExpression.properties) {
|
|
2735
|
+
if (this.compiler.isPropertyAssignment(property)) {
|
|
2736
|
+
if (!this.#isDoubleQuotedString(property.name, sourceFile)) {
|
|
2737
|
+
const origin = {
|
|
2738
|
+
end: property.end,
|
|
2739
|
+
file: sourceFile,
|
|
2740
|
+
start: this.#skipTrivia(property.pos, sourceFile),
|
|
2741
|
+
};
|
|
2742
|
+
this.#onDiagnostic(Diagnostic.error(this.#optionDiagnosticText.doubleQuotesExpected(), origin));
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
const optionName = this.#resolvePropertyName(property);
|
|
2746
|
+
const optionDefinition = propertyDefinition.get(optionName);
|
|
2747
|
+
if (optionDefinition) {
|
|
2748
|
+
propertyOptions[optionDefinition.name] = await this.#parseOptionValue(sourceFile, property.initializer, optionDefinition);
|
|
2749
|
+
}
|
|
2750
|
+
else {
|
|
2751
|
+
const origin = {
|
|
2752
|
+
end: property.end,
|
|
2753
|
+
file: sourceFile,
|
|
2754
|
+
start: this.#skipTrivia(property.pos, sourceFile),
|
|
2755
|
+
};
|
|
2756
|
+
this.#onDiagnostic(Diagnostic.error(this.#optionDiagnosticText.unknownProperty(optionName), origin));
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
return propertyOptions;
|
|
2761
|
+
}
|
|
2762
|
+
break;
|
|
2763
|
+
}
|
|
2764
|
+
const origin = {
|
|
2765
|
+
end: valueExpression.end,
|
|
2766
|
+
file: sourceFile,
|
|
2767
|
+
start: this.#skipTrivia(valueExpression.pos, sourceFile),
|
|
2768
|
+
};
|
|
2769
|
+
const text = isListItem
|
|
2770
|
+
? this.#optionDiagnosticText.expectsListItemType(optionDefinition.name, optionDefinition.brand)
|
|
2771
|
+
: this.#optionDiagnosticText.requiresArgumentType(optionDefinition.name, optionDefinition.brand);
|
|
2772
|
+
this.#onDiagnostic(Diagnostic.error(text, origin));
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
#resolvePropertyName({ name }) {
|
|
2776
|
+
if ("text" in name) {
|
|
2777
|
+
return name.text;
|
|
2778
|
+
}
|
|
2779
|
+
return "";
|
|
2780
|
+
}
|
|
2781
|
+
#skipTrivia(position, sourceFile) {
|
|
2782
|
+
const { text } = sourceFile.getSourceFile();
|
|
2783
|
+
while (position < text.length) {
|
|
2784
|
+
if (/\s/.test(text.charAt(position))) {
|
|
2785
|
+
position++;
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
else if (text.charAt(position) === "/") {
|
|
2789
|
+
if (text.charAt(position + 1) === "/") {
|
|
2790
|
+
position += 2;
|
|
2791
|
+
while (position < text.length) {
|
|
2792
|
+
if (text.charAt(position) === "\n") {
|
|
2793
|
+
break;
|
|
2794
|
+
}
|
|
2795
|
+
position++;
|
|
2796
|
+
}
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
if (text.charAt(position + 1) === "*") {
|
|
2800
|
+
position += 2;
|
|
2801
|
+
while (position < text.length) {
|
|
2802
|
+
if (text.charAt(position) === "*" && text.charAt(position + 1) === "/") {
|
|
2803
|
+
position += 2;
|
|
2804
|
+
break;
|
|
2805
|
+
}
|
|
2806
|
+
position++;
|
|
2807
|
+
}
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
position++;
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
break;
|
|
2814
|
+
}
|
|
2815
|
+
return position;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
class ConfigService {
|
|
2820
|
+
compiler;
|
|
2821
|
+
#commandLineOptions = {};
|
|
2822
|
+
#configFileOptions = {};
|
|
2823
|
+
static #defaultOptions = {
|
|
2824
|
+
allowNoTestFiles: false,
|
|
2825
|
+
failFast: false,
|
|
2826
|
+
rootPath: "./",
|
|
2827
|
+
target: ["latest"],
|
|
2828
|
+
testFileMatch: ["**/*.tst.*", "**/__typetests__/*.test.*", "**/typetests/*.test.*"],
|
|
2829
|
+
};
|
|
2830
|
+
#pathMatch = [];
|
|
2831
|
+
#storeService;
|
|
2832
|
+
constructor(compiler, storeService) {
|
|
2833
|
+
this.compiler = compiler;
|
|
2834
|
+
this.#storeService = storeService;
|
|
2835
|
+
}
|
|
2836
|
+
get commandLineOptions() {
|
|
2837
|
+
return this.#commandLineOptions;
|
|
2838
|
+
}
|
|
2839
|
+
get configFileOptions() {
|
|
2840
|
+
return this.#configFileOptions;
|
|
2841
|
+
}
|
|
2842
|
+
static get defaultOptions() {
|
|
2843
|
+
return ConfigService.#defaultOptions;
|
|
2844
|
+
}
|
|
2845
|
+
#normalizePath(filePath) {
|
|
2846
|
+
if (path.sep === "/") {
|
|
2847
|
+
return filePath;
|
|
2848
|
+
}
|
|
2849
|
+
return filePath.replace(/\\/g, "/");
|
|
2850
|
+
}
|
|
2851
|
+
#onDiagnostic = (diagnostic) => {
|
|
2852
|
+
EventEmitter.dispatch(["config:error", { diagnostics: [diagnostic] }]);
|
|
2853
|
+
};
|
|
2854
|
+
parseCommandLine(commandLineArgs) {
|
|
2855
|
+
this.#commandLineOptions = {};
|
|
2856
|
+
this.#pathMatch = [];
|
|
2857
|
+
const commandLineWorker = new CommandLineOptionsWorker(this.#commandLineOptions, this.#pathMatch, this.#storeService, this.#onDiagnostic);
|
|
2858
|
+
commandLineWorker.parse(commandLineArgs);
|
|
2859
|
+
}
|
|
2860
|
+
async readConfigFile(filePath, sourceText) {
|
|
2861
|
+
const configFilePath = filePath ?? this.#commandLineOptions.config ?? path.resolve("./tstyche.config.json");
|
|
2862
|
+
this.#configFileOptions = {
|
|
2863
|
+
rootPath: this.#normalizePath(path.dirname(configFilePath)),
|
|
2864
|
+
};
|
|
2865
|
+
let configFileText = sourceText ?? "";
|
|
2866
|
+
if (sourceText == null && existsSync(configFilePath)) {
|
|
2867
|
+
configFileText = await fs.readFile(configFilePath, {
|
|
2868
|
+
encoding: "utf8",
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
const configFileWorker = new ConfigFileOptionsWorker(this.compiler, this.#configFileOptions, configFilePath, this.#storeService, this.#onDiagnostic);
|
|
2872
|
+
await configFileWorker.parse(configFileText);
|
|
2873
|
+
}
|
|
2874
|
+
resolveConfig() {
|
|
2875
|
+
const mergedOptions = {
|
|
2876
|
+
...ConfigService.#defaultOptions,
|
|
2877
|
+
...this.#configFileOptions,
|
|
2878
|
+
...this.#commandLineOptions,
|
|
2879
|
+
pathMatch: this.#pathMatch.map((match) => this.#normalizePath(match)),
|
|
2880
|
+
};
|
|
2881
|
+
return mergedOptions;
|
|
2882
|
+
}
|
|
2883
|
+
selectTestFiles() {
|
|
2884
|
+
const { allowNoTestFiles, pathMatch, rootPath, testFileMatch } = this.resolveConfig();
|
|
2885
|
+
let testFilePaths = this.compiler.sys.readDirectory(rootPath, undefined, undefined, testFileMatch);
|
|
2886
|
+
if (pathMatch.length > 0) {
|
|
2887
|
+
testFilePaths = testFilePaths.filter((testFilePath) => pathMatch.some((match) => {
|
|
2888
|
+
const relativeTestFilePath = this.#normalizePath(`./${path.relative("", testFilePath)}`);
|
|
2889
|
+
return relativeTestFilePath.toLowerCase().includes(match.toLowerCase());
|
|
2890
|
+
}));
|
|
2891
|
+
}
|
|
2892
|
+
if (testFilePaths.length === 0 && !allowNoTestFiles) {
|
|
2893
|
+
const text = [
|
|
2894
|
+
"No test files were selected using current configuration.",
|
|
2895
|
+
`Root path: ${rootPath}`,
|
|
2896
|
+
`Test file match: ${testFileMatch.join(", ")}`,
|
|
2897
|
+
];
|
|
2898
|
+
if (pathMatch.length > 0) {
|
|
2899
|
+
text.push(`Path match: ${pathMatch.join(", ")}`);
|
|
2900
|
+
}
|
|
2901
|
+
this.#onDiagnostic(Diagnostic.error(text));
|
|
2902
|
+
}
|
|
2903
|
+
return testFilePaths;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
var OptionBrand;
|
|
2908
|
+
(function (OptionBrand) {
|
|
2909
|
+
OptionBrand["String"] = "string";
|
|
2910
|
+
OptionBrand["Number"] = "number";
|
|
2911
|
+
OptionBrand["Boolean"] = "boolean";
|
|
2912
|
+
OptionBrand["List"] = "list";
|
|
2913
|
+
OptionBrand["Object"] = "object";
|
|
2914
|
+
})(OptionBrand || (OptionBrand = {}));
|
|
2915
|
+
|
|
2916
|
+
var OptionGroup;
|
|
2917
|
+
(function (OptionGroup) {
|
|
2918
|
+
OptionGroup[OptionGroup["CommandLine"] = 2] = "CommandLine";
|
|
2919
|
+
OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
|
|
2920
|
+
})(OptionGroup || (OptionGroup = {}));
|
|
2921
|
+
|
|
2922
|
+
class Lock {
|
|
2923
|
+
#lockFilePath;
|
|
2924
|
+
static #lockSuffix = "__lock__";
|
|
2925
|
+
constructor(targetPath) {
|
|
2926
|
+
this.#lockFilePath = Lock.getLockFilePath(targetPath);
|
|
2927
|
+
writeFileSync(this.#lockFilePath, "");
|
|
2928
|
+
process.on("exit", () => {
|
|
2929
|
+
this.release();
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
static getLockFilePath(targetPath) {
|
|
2933
|
+
return `${targetPath}${Lock.#lockSuffix}`;
|
|
2934
|
+
}
|
|
2935
|
+
static isLocked(targetPath) {
|
|
2936
|
+
return existsSync(Lock.getLockFilePath(targetPath));
|
|
2937
|
+
}
|
|
2938
|
+
release() {
|
|
2939
|
+
rmSync(this.#lockFilePath, { force: true });
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
class CompilerModuleWorker {
|
|
2944
|
+
#cachePath;
|
|
2945
|
+
#readyFileName = "__ready__";
|
|
2946
|
+
#timeout = Environment.timeout * 1000;
|
|
2947
|
+
constructor(cachePath) {
|
|
2948
|
+
this.#cachePath = cachePath;
|
|
2949
|
+
}
|
|
2950
|
+
async ensure(compilerVersion, signal) {
|
|
2951
|
+
const installationPath = path.join(this.#cachePath, compilerVersion);
|
|
2952
|
+
const readyFilePath = path.join(installationPath, this.#readyFileName);
|
|
2953
|
+
const tsserverFilePath = path.join(installationPath, "node_modules", "typescript", "lib", "tsserverlibrary.js");
|
|
2954
|
+
const typescriptFilePath = path.join(installationPath, "node_modules", "typescript", "lib", "typescript.js");
|
|
2955
|
+
if (Lock.isLocked(installationPath)) {
|
|
2956
|
+
for await (const now of setInterval(1000, Date.now(), { signal })) {
|
|
2957
|
+
const startTime = Date.now();
|
|
2958
|
+
if (!Lock.isLocked(installationPath)) {
|
|
2959
|
+
break;
|
|
2960
|
+
}
|
|
2961
|
+
if (startTime - now > this.#timeout) {
|
|
2962
|
+
throw new Error(`Lock wait timeout of ${this.#timeout / 1000}s was exceeded.`);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (existsSync(readyFilePath)) {
|
|
2967
|
+
return tsserverFilePath;
|
|
2968
|
+
}
|
|
2969
|
+
EventEmitter.dispatch(["store:info", { compilerVersion, installationPath }]);
|
|
2970
|
+
await fs.mkdir(installationPath, { recursive: true });
|
|
2971
|
+
const lock = new Lock(installationPath);
|
|
2972
|
+
await fs.writeFile(path.join(installationPath, "package.json"), this.#getPackageJson(compilerVersion));
|
|
2973
|
+
await this.#installPackage(installationPath, signal);
|
|
2974
|
+
await fs.writeFile(tsserverFilePath, await this.#getPatched(compilerVersion, tsserverFilePath));
|
|
2975
|
+
await fs.writeFile(typescriptFilePath, await this.#getPatched(compilerVersion, typescriptFilePath));
|
|
2976
|
+
await fs.writeFile(readyFilePath, "");
|
|
2977
|
+
lock.release();
|
|
2978
|
+
return tsserverFilePath;
|
|
2979
|
+
}
|
|
2980
|
+
#getPackageJson(version) {
|
|
2981
|
+
const packageJson = {
|
|
2982
|
+
name: "@tstyche/typescript",
|
|
2983
|
+
version,
|
|
2984
|
+
description: "Do not change. This package was generated by TSTyche",
|
|
2985
|
+
private: true,
|
|
2986
|
+
license: "MIT",
|
|
2987
|
+
dependencies: {
|
|
2988
|
+
typescript: version,
|
|
2989
|
+
},
|
|
2990
|
+
};
|
|
2991
|
+
return JSON.stringify(packageJson, null, 2);
|
|
2992
|
+
}
|
|
2993
|
+
async #getPatched(version, filePath) {
|
|
2994
|
+
function ts5Patch(match, indent) {
|
|
2995
|
+
return [match, indent, "isTypeIdenticalTo,", indent, "isTypeSubtypeOf,"].join("");
|
|
2996
|
+
}
|
|
2997
|
+
function ts4Patch(match, indent) {
|
|
2998
|
+
return [match, indent, "isTypeIdenticalTo: isTypeIdenticalTo,", indent, "isTypeSubtypeOf: isTypeSubtypeOf,"].join("");
|
|
2999
|
+
}
|
|
3000
|
+
const fileContent = await fs.readFile(filePath, { encoding: "utf8" });
|
|
3001
|
+
if (version.startsWith("5")) {
|
|
3002
|
+
return fileContent.replace(/(\s+)isTypeAssignableTo,/, ts5Patch);
|
|
3003
|
+
}
|
|
3004
|
+
else {
|
|
3005
|
+
return fileContent.replace(/(\s+)isTypeAssignableTo: isTypeAssignableTo,/, ts4Patch);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
async #installPackage(cwd, signal) {
|
|
3009
|
+
const args = ["install", "--ignore-scripts", "--no-bin-links", "--no-package-lock"];
|
|
3010
|
+
return new Promise((resolve, reject) => {
|
|
3011
|
+
const spawnedNpm = spawn("npm", args, {
|
|
3012
|
+
cwd,
|
|
3013
|
+
shell: true,
|
|
3014
|
+
signal,
|
|
3015
|
+
stdio: "ignore",
|
|
3016
|
+
timeout: this.#timeout,
|
|
3017
|
+
});
|
|
3018
|
+
spawnedNpm.on("error", (error) => {
|
|
3019
|
+
reject(error);
|
|
3020
|
+
});
|
|
3021
|
+
spawnedNpm.on("close", (code, signal) => {
|
|
3022
|
+
if (code === 0) {
|
|
3023
|
+
resolve();
|
|
3024
|
+
}
|
|
3025
|
+
if (signal != null) {
|
|
3026
|
+
reject(new Error(`Setup timeout of ${this.#timeout / 1000}s was exceeded.`));
|
|
3027
|
+
}
|
|
3028
|
+
reject(new Error(`Process exited with code ${String(code)}.`));
|
|
3029
|
+
});
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
class ManifestWorker {
|
|
3035
|
+
#cachePath;
|
|
3036
|
+
#manifestFileName = "store-manifest.json";
|
|
3037
|
+
#manifestFilePath;
|
|
3038
|
+
#prune;
|
|
3039
|
+
#registryUrl = new URL("https://registry.npmjs.org");
|
|
3040
|
+
#timeout = Environment.timeout * 1000;
|
|
3041
|
+
constructor(cachePath, prune) {
|
|
3042
|
+
this.#cachePath = cachePath;
|
|
3043
|
+
this.#manifestFilePath = path.join(cachePath, this.#manifestFileName);
|
|
3044
|
+
this.#prune = prune;
|
|
3045
|
+
}
|
|
3046
|
+
async #fetch(signal) {
|
|
3047
|
+
return new Promise((resolve, reject) => {
|
|
3048
|
+
const request = https.get(new URL("typescript", this.#registryUrl), {
|
|
3049
|
+
headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*" },
|
|
3050
|
+
signal,
|
|
3051
|
+
}, (result) => {
|
|
3052
|
+
if (result.statusCode !== 200) {
|
|
3053
|
+
reject(new Error(`Request failed with status code ${String(result.statusCode)}.`));
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
result.setEncoding("utf8");
|
|
3057
|
+
let rawData = "";
|
|
3058
|
+
result.on("data", (chunk) => {
|
|
3059
|
+
rawData += chunk;
|
|
3060
|
+
});
|
|
3061
|
+
result.on("end", () => {
|
|
3062
|
+
try {
|
|
3063
|
+
const packageMetadata = JSON.parse(rawData);
|
|
3064
|
+
resolve(packageMetadata);
|
|
3065
|
+
}
|
|
3066
|
+
catch (error) {
|
|
3067
|
+
reject(error);
|
|
3068
|
+
}
|
|
3069
|
+
});
|
|
3070
|
+
});
|
|
3071
|
+
request.on("error", (error) => {
|
|
3072
|
+
reject(error);
|
|
3073
|
+
});
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
isOutdated(manifest, ageTolerance = 0) {
|
|
3077
|
+
if (Date.now() - manifest.lastUpdated > 2 * 60 * 60 * 1000 + ageTolerance * 1000) {
|
|
3078
|
+
return true;
|
|
3079
|
+
}
|
|
3080
|
+
return false;
|
|
3081
|
+
}
|
|
3082
|
+
async #load(signal, isUpdate = false) {
|
|
3083
|
+
const manifest = {
|
|
3084
|
+
lastUpdated: Date.now(),
|
|
3085
|
+
resolutions: {},
|
|
3086
|
+
versions: [],
|
|
3087
|
+
};
|
|
3088
|
+
let packageMetadata;
|
|
3089
|
+
const abortController = new AbortController();
|
|
3090
|
+
const timeoutSignal = AbortSignal.timeout(this.#timeout);
|
|
3091
|
+
timeoutSignal.addEventListener("abort", () => {
|
|
3092
|
+
abortController.abort(`Setup timeout of ${this.#timeout / 1000}s was exceeded.`);
|
|
3093
|
+
}, { once: true });
|
|
3094
|
+
signal?.addEventListener("abort", () => {
|
|
3095
|
+
abortController.abort(`Fetch got canceled by request.`);
|
|
3096
|
+
}, { once: true });
|
|
3097
|
+
try {
|
|
3098
|
+
packageMetadata = await this.#fetch(abortController.signal);
|
|
3099
|
+
}
|
|
3100
|
+
catch (error) {
|
|
3101
|
+
if (!isUpdate) {
|
|
3102
|
+
const text = [`Failed to fetch metadata of the 'typescript' package from '${this.#registryUrl.href}'.`];
|
|
3103
|
+
if (error instanceof Error && error.name !== "AbortError") {
|
|
3104
|
+
text.push("Might be there is an issue with the registry or the network connection.");
|
|
3105
|
+
}
|
|
3106
|
+
this.#onDiagnostic(Diagnostic.fromError(text, error));
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
if (!packageMetadata) {
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
manifest.versions = Object.keys(packageMetadata.versions)
|
|
3113
|
+
.filter((version) => /^(4|5)\.\d\.\d$/.test(version))
|
|
3114
|
+
.sort();
|
|
3115
|
+
const minorVersions = [...new Set(manifest.versions.map((version) => version.slice(0, -2)))];
|
|
3116
|
+
for (const tag of minorVersions) {
|
|
3117
|
+
const resolvedVersion = manifest.versions.filter((version) => version.startsWith(tag)).pop();
|
|
3118
|
+
if (resolvedVersion != null) {
|
|
3119
|
+
manifest.resolutions[tag] = resolvedVersion;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
for (const distributionTagKey of ["beta", "latest", "next", "rc"]) {
|
|
3123
|
+
const distributionTagValue = packageMetadata["dist-tags"][distributionTagKey];
|
|
3124
|
+
if (distributionTagValue != null) {
|
|
3125
|
+
manifest.resolutions[distributionTagKey] = distributionTagValue;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
return manifest;
|
|
3129
|
+
}
|
|
3130
|
+
#onDiagnostic(diagnostic) {
|
|
3131
|
+
EventEmitter.dispatch(["store:error", { diagnostics: [diagnostic] }]);
|
|
3132
|
+
}
|
|
3133
|
+
async open(signal) {
|
|
3134
|
+
let manifest;
|
|
3135
|
+
if (!existsSync(this.#manifestFilePath)) {
|
|
3136
|
+
manifest = await this.#load(signal);
|
|
3137
|
+
if (manifest == null) {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
await this.persist(manifest);
|
|
3141
|
+
return manifest;
|
|
3142
|
+
}
|
|
3143
|
+
let manifestText;
|
|
3144
|
+
try {
|
|
3145
|
+
manifestText = await fs.readFile(this.#manifestFilePath, { encoding: "utf8" });
|
|
3146
|
+
}
|
|
3147
|
+
catch (error) {
|
|
3148
|
+
this.#onDiagnostic(Diagnostic.fromError("Failed to open store manifest.", error));
|
|
3149
|
+
}
|
|
3150
|
+
if (manifestText == null) {
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
try {
|
|
3154
|
+
manifest = JSON.parse(manifestText);
|
|
3155
|
+
}
|
|
3156
|
+
catch (error) {
|
|
3157
|
+
this.#onDiagnostic(Diagnostic.fromError([
|
|
3158
|
+
`Failed to parse '${this.#manifestFilePath}'.`,
|
|
3159
|
+
"Cached files appeared to be corrupt and got removed. Try running 'tstyche' again.",
|
|
3160
|
+
], error));
|
|
3161
|
+
}
|
|
3162
|
+
if (manifest == null) {
|
|
3163
|
+
await this.#prune();
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
if (this.isOutdated(manifest)) {
|
|
3167
|
+
manifest = await this.update(manifest, signal);
|
|
3168
|
+
}
|
|
3169
|
+
return manifest;
|
|
3170
|
+
}
|
|
3171
|
+
async persist(manifest) {
|
|
3172
|
+
if (!existsSync(this.#cachePath)) {
|
|
3173
|
+
await fs.mkdir(this.#cachePath, { recursive: true });
|
|
3174
|
+
}
|
|
3175
|
+
await fs.writeFile(this.#manifestFilePath, JSON.stringify(manifest));
|
|
3176
|
+
}
|
|
3177
|
+
async update(manifest, signal) {
|
|
3178
|
+
const freshManifest = await this.#load(signal, true);
|
|
3179
|
+
if (freshManifest != null) {
|
|
3180
|
+
manifest = { ...manifest, ...freshManifest };
|
|
3181
|
+
await this.persist(manifest);
|
|
3182
|
+
}
|
|
3183
|
+
return manifest;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
class StoreService {
|
|
3188
|
+
#cachePath;
|
|
3189
|
+
#compilerModuleWorker;
|
|
3190
|
+
#manifest;
|
|
3191
|
+
#manifestWorker;
|
|
3192
|
+
#nodeRequire = createRequire(import.meta.url);
|
|
3193
|
+
constructor() {
|
|
3194
|
+
this.#cachePath = Environment.storePath;
|
|
3195
|
+
this.#compilerModuleWorker = new CompilerModuleWorker(this.#cachePath);
|
|
3196
|
+
this.#manifestWorker = new ManifestWorker(this.#cachePath, async () => this.prune());
|
|
3197
|
+
}
|
|
3198
|
+
getSupportedTags() {
|
|
3199
|
+
if (!this.#manifest) {
|
|
3200
|
+
this.#onDiagnostic(Diagnostic.error("Store manifest is not open. Call 'StoreService.open()' first."));
|
|
3201
|
+
return [];
|
|
3202
|
+
}
|
|
3203
|
+
return [...Object.keys(this.#manifest.resolutions), ...this.#manifest.versions].sort();
|
|
3204
|
+
}
|
|
3205
|
+
async loadCompilerModule(tag, signal) {
|
|
3206
|
+
let modulePath;
|
|
3207
|
+
if (tag === "local") {
|
|
3208
|
+
try {
|
|
3209
|
+
modulePath = this.#nodeRequire.resolve("typescript/lib/tsserverlibrary.js");
|
|
3210
|
+
}
|
|
3211
|
+
catch {
|
|
3212
|
+
tag = "latest";
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if (modulePath == null) {
|
|
3216
|
+
modulePath = await this.prepareCompilerModule(tag, signal);
|
|
3217
|
+
}
|
|
3218
|
+
if (modulePath != null) {
|
|
3219
|
+
return this.#nodeRequire(modulePath);
|
|
3220
|
+
}
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
#onDiagnostic(diagnostic) {
|
|
3224
|
+
EventEmitter.dispatch(["store:error", { diagnostics: [diagnostic] }]);
|
|
3225
|
+
}
|
|
3226
|
+
async open(signal) {
|
|
3227
|
+
if (this.#manifest) {
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
this.#manifest = await this.#manifestWorker.open(signal);
|
|
3231
|
+
}
|
|
3232
|
+
async prepareCompilerModule(tag, signal) {
|
|
3233
|
+
if (!this.#manifest) {
|
|
3234
|
+
this.#onDiagnostic(Diagnostic.error("Store manifest is not open. Call 'StoreService.open()' first."));
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
const version = this.resolveTag(tag);
|
|
3238
|
+
if (version == null) {
|
|
3239
|
+
this.#onDiagnostic(Diagnostic.error(`Cannot add the 'typescript' package for the '${tag}' tag.`));
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
let modulePath;
|
|
3243
|
+
try {
|
|
3244
|
+
modulePath = await this.#compilerModuleWorker.ensure(version, signal);
|
|
3245
|
+
}
|
|
3246
|
+
catch (error) {
|
|
3247
|
+
this.#onDiagnostic(Diagnostic.fromError(`Failed to install 'typescript@${version}'.`, error));
|
|
3248
|
+
}
|
|
3249
|
+
if (modulePath != null) {
|
|
3250
|
+
if (!("lastUsed" in this.#manifest)) {
|
|
3251
|
+
this.#manifest.lastUsed = {};
|
|
3252
|
+
}
|
|
3253
|
+
this.#manifest.lastUsed[version] = Date.now();
|
|
3254
|
+
}
|
|
3255
|
+
return modulePath;
|
|
3256
|
+
}
|
|
3257
|
+
async prune() {
|
|
3258
|
+
await fs.rm(this.#cachePath, { force: true, recursive: true });
|
|
3259
|
+
}
|
|
3260
|
+
resolveTag(tag) {
|
|
3261
|
+
if (!this.#manifest) {
|
|
3262
|
+
this.#onDiagnostic(Diagnostic.error("Store manifest is not open. Call 'StoreService.open()' first."));
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
if (this.#manifest.versions.includes(tag)) {
|
|
3266
|
+
return tag;
|
|
3267
|
+
}
|
|
3268
|
+
const version = this.#manifest.resolutions[tag];
|
|
3269
|
+
if (this.#manifestWorker.isOutdated(this.#manifest, 60) &&
|
|
3270
|
+
Object.keys(this.#manifest.resolutions).slice(-5).includes(tag)) {
|
|
3271
|
+
this.#onDiagnostic(Diagnostic.warning([
|
|
3272
|
+
"Failed to update metadata of the 'typescript' package from the registry.",
|
|
3273
|
+
`The resolution of the '${tag}' tag may be outdated.`,
|
|
3274
|
+
]));
|
|
3275
|
+
}
|
|
3276
|
+
return version;
|
|
3277
|
+
}
|
|
3278
|
+
async update(signal) {
|
|
3279
|
+
if (!this.#manifest) {
|
|
3280
|
+
this.#onDiagnostic(Diagnostic.error("Store manifest is not open. Call 'StoreService.open()' first."));
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
await this.#manifestWorker.update(this.#manifest, signal);
|
|
3284
|
+
}
|
|
3285
|
+
validateTag(tag) {
|
|
3286
|
+
if (!this.#manifest) {
|
|
3287
|
+
this.#onDiagnostic(Diagnostic.error("Store manifest is not open. Call 'StoreService.open()' first."));
|
|
3288
|
+
return false;
|
|
3289
|
+
}
|
|
3290
|
+
if (this.#manifest.versions.includes(tag) || tag in this.#manifest.resolutions) {
|
|
3291
|
+
return true;
|
|
3292
|
+
}
|
|
3293
|
+
if (this.#manifest.resolutions["latest"] != null &&
|
|
3294
|
+
tag.startsWith(this.#manifest.resolutions["latest"].slice(0, 3))) {
|
|
3295
|
+
this.#onDiagnostic(Diagnostic.warning([
|
|
3296
|
+
"Failed to update metadata of the 'typescript' package from the registry.",
|
|
3297
|
+
`The resolution of the '${tag}' tag may be outdated.`,
|
|
3298
|
+
]));
|
|
3299
|
+
}
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
class JsonText {
|
|
3305
|
+
props;
|
|
3306
|
+
constructor(props) {
|
|
3307
|
+
this.props = props;
|
|
3308
|
+
}
|
|
3309
|
+
render() {
|
|
3310
|
+
return Scribbler.createElement(Line, null, JSON.stringify(this.#sortObject(this.props.input), null, 2));
|
|
3311
|
+
}
|
|
3312
|
+
#sortObject(target) {
|
|
3313
|
+
if (Array.isArray(target)) {
|
|
3314
|
+
return target;
|
|
3315
|
+
}
|
|
3316
|
+
return Object.keys(target)
|
|
3317
|
+
.sort()
|
|
3318
|
+
.reduce((result, key) => {
|
|
3319
|
+
result[key] = target[key];
|
|
3320
|
+
return result;
|
|
3321
|
+
}, {});
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
function formattedText(input) {
|
|
3325
|
+
if (typeof input === "string") {
|
|
3326
|
+
return Scribbler.createElement(Line, null, input);
|
|
3327
|
+
}
|
|
3328
|
+
return Scribbler.createElement(JsonText, { input: input });
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
const usageExamples = [
|
|
3332
|
+
["tstyche", "Run all tests."],
|
|
3333
|
+
["tstyche path/to/first.test.ts second", "Only run the test files with matching path."],
|
|
3334
|
+
["tstyche --target 4.7,4.8,latest", "Test on all specified versions of TypeScript."],
|
|
3335
|
+
];
|
|
3336
|
+
class HintText {
|
|
3337
|
+
props;
|
|
3338
|
+
constructor(props) {
|
|
3339
|
+
this.props = props;
|
|
3340
|
+
}
|
|
3341
|
+
render() {
|
|
3342
|
+
return (Scribbler.createElement(Text, { indent: 1, color: "90" }, this.props.children));
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
class HelpHeaderText {
|
|
3346
|
+
props;
|
|
3347
|
+
constructor(props) {
|
|
3348
|
+
this.props = props;
|
|
3349
|
+
}
|
|
3350
|
+
render() {
|
|
3351
|
+
const hint = (Scribbler.createElement(HintText, null,
|
|
3352
|
+
Scribbler.createElement(Text, null, this.props.tstycheVersion)));
|
|
3353
|
+
return (Scribbler.createElement(Line, null,
|
|
3354
|
+
Scribbler.createElement(Text, null, "The TSTyche Type Test Runner"),
|
|
3355
|
+
hint));
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
class CommandText {
|
|
3359
|
+
props;
|
|
3360
|
+
constructor(props) {
|
|
3361
|
+
this.props = props;
|
|
3362
|
+
}
|
|
3363
|
+
render() {
|
|
3364
|
+
let hint = undefined;
|
|
3365
|
+
if (this.props.hint != null) {
|
|
3366
|
+
hint = Scribbler.createElement(HintText, null, this.props.hint);
|
|
3367
|
+
}
|
|
3368
|
+
return (Scribbler.createElement(Line, { indent: 1 },
|
|
3369
|
+
Scribbler.createElement(Text, { color: "34" }, this.props.text),
|
|
3370
|
+
hint));
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
class OptionDescriptionText {
|
|
3374
|
+
props;
|
|
3375
|
+
constructor(props) {
|
|
3376
|
+
this.props = props;
|
|
3377
|
+
}
|
|
3378
|
+
render() {
|
|
3379
|
+
return Scribbler.createElement(Line, { indent: 1 }, this.props.text);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
class CliUsageText {
|
|
3383
|
+
render() {
|
|
3384
|
+
const usageText = usageExamples.map(([commandText, descriptionText]) => (Scribbler.createElement(Text, null,
|
|
3385
|
+
Scribbler.createElement(CommandText, { text: commandText }),
|
|
3386
|
+
Scribbler.createElement(OptionDescriptionText, { text: descriptionText }),
|
|
3387
|
+
Scribbler.createElement(Line, null))));
|
|
3388
|
+
return Scribbler.createElement(Text, null, usageText);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
class OptionNameText {
|
|
3392
|
+
props;
|
|
3393
|
+
constructor(props) {
|
|
3394
|
+
this.props = props;
|
|
3395
|
+
}
|
|
3396
|
+
render() {
|
|
3397
|
+
return Scribbler.createElement(Text, null,
|
|
3398
|
+
"--",
|
|
3399
|
+
this.props.text);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
class OptionHintText {
|
|
3403
|
+
props;
|
|
3404
|
+
constructor(props) {
|
|
3405
|
+
this.props = props;
|
|
3406
|
+
}
|
|
3407
|
+
render() {
|
|
3408
|
+
if (this.props.definition.brand === "list") {
|
|
3409
|
+
return (Scribbler.createElement(Text, null,
|
|
3410
|
+
this.props.definition.brand,
|
|
3411
|
+
" of ",
|
|
3412
|
+
this.props.definition.items.brand,
|
|
3413
|
+
"s"));
|
|
3414
|
+
}
|
|
3415
|
+
return Scribbler.createElement(Text, null, this.props.definition.brand);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
class CliOptionsText {
|
|
3419
|
+
props;
|
|
3420
|
+
constructor(props) {
|
|
3421
|
+
this.props = props;
|
|
3422
|
+
}
|
|
3423
|
+
render() {
|
|
3424
|
+
const definitions = Array.from(this.props.optionDefinitions.values());
|
|
3425
|
+
const optionsText = definitions.map((definition) => (Scribbler.createElement(Text, null,
|
|
3426
|
+
Scribbler.createElement(CommandText, { text: Scribbler.createElement(OptionNameText, { text: definition.name }), hint: Scribbler.createElement(OptionHintText, { definition: definition }) }),
|
|
3427
|
+
Scribbler.createElement(OptionDescriptionText, { text: definition.description }),
|
|
3428
|
+
Scribbler.createElement(Line, null))));
|
|
3429
|
+
return (Scribbler.createElement(Text, null,
|
|
3430
|
+
Scribbler.createElement(Line, null, "CLI Options"),
|
|
3431
|
+
Scribbler.createElement(Line, null),
|
|
3432
|
+
optionsText));
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
class HelpFooterText {
|
|
3436
|
+
render() {
|
|
3437
|
+
return Scribbler.createElement(Line, null, "To learn more, visit https://tstyche.org");
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
function helpText(optionDefinitions, tstycheVersion) {
|
|
3441
|
+
return (Scribbler.createElement(Text, null,
|
|
3442
|
+
Scribbler.createElement(HelpHeaderText, { tstycheVersion: tstycheVersion }),
|
|
3443
|
+
Scribbler.createElement(Line, null),
|
|
3444
|
+
Scribbler.createElement(CliUsageText, null),
|
|
3445
|
+
Scribbler.createElement(Line, null),
|
|
3446
|
+
Scribbler.createElement(CliOptionsText, { optionDefinitions: optionDefinitions }),
|
|
3447
|
+
Scribbler.createElement(Line, null),
|
|
3448
|
+
Scribbler.createElement(HelpFooterText, null),
|
|
3449
|
+
Scribbler.createElement(Line, null)));
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
class Cli {
|
|
3453
|
+
#abortController = new AbortController();
|
|
3454
|
+
#logger;
|
|
3455
|
+
#process;
|
|
3456
|
+
#storeService;
|
|
3457
|
+
constructor(process) {
|
|
3458
|
+
this.#process = process;
|
|
3459
|
+
this.#logger = new Logger();
|
|
3460
|
+
this.#storeService = new StoreService();
|
|
3461
|
+
}
|
|
3462
|
+
#onStartupEvent = ([eventName, payload]) => {
|
|
3463
|
+
switch (eventName) {
|
|
3464
|
+
case "store:info":
|
|
3465
|
+
this.#logger.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
|
|
3466
|
+
break;
|
|
3467
|
+
case "config:error":
|
|
3468
|
+
case "store:error":
|
|
3469
|
+
for (const diagnostic of payload.diagnostics) {
|
|
3470
|
+
switch (diagnostic.category) {
|
|
3471
|
+
case "error":
|
|
3472
|
+
this.#abortController.abort();
|
|
3473
|
+
this.#process.exitCode = 1;
|
|
3474
|
+
this.#logger.writeError(diagnosticText(diagnostic));
|
|
3475
|
+
break;
|
|
3476
|
+
case "warning":
|
|
3477
|
+
this.#logger.writeWarning(diagnosticText(diagnostic));
|
|
3478
|
+
break;
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
break;
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3484
|
+
async run(commandLineArgs) {
|
|
3485
|
+
EventEmitter.addHandler(this.#onStartupEvent);
|
|
3486
|
+
await this.#storeService.open(this.#abortController.signal);
|
|
3487
|
+
if (this.#process.exitCode === 1) {
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
const compiler = await this.#storeService.loadCompilerModule("local", this.#abortController.signal);
|
|
3491
|
+
if (!compiler) {
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
const configService = new ConfigService(compiler, this.#storeService);
|
|
3495
|
+
configService.parseCommandLine(commandLineArgs);
|
|
3496
|
+
if (this.#process.exitCode === 1) {
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
if (configService.commandLineOptions.prune === true) {
|
|
3500
|
+
await this.#storeService.prune();
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
if (configService.commandLineOptions.help === true) {
|
|
3504
|
+
const commandLineOptionDefinitions = OptionDefinitionsMap.for(2);
|
|
3505
|
+
this.#logger.writeMessage(helpText(commandLineOptionDefinitions, TSTyche.version));
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
if (configService.commandLineOptions.update === true) {
|
|
3509
|
+
await this.#storeService.update();
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
if (configService.commandLineOptions.version === true) {
|
|
3513
|
+
this.#logger.writeMessage(formattedText(TSTyche.version));
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
await configService.readConfigFile();
|
|
3517
|
+
if (this.#process.exitCode === 1) {
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
const resolvedConfig = configService.resolveConfig();
|
|
3521
|
+
if (configService.commandLineOptions.showConfig === true) {
|
|
3522
|
+
this.#logger.writeMessage(formattedText({
|
|
3523
|
+
noColor: Environment.noColor,
|
|
3524
|
+
storePath: Environment.storePath,
|
|
3525
|
+
timeout: Environment.timeout,
|
|
3526
|
+
...resolvedConfig,
|
|
3527
|
+
}));
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
if (configService.commandLineOptions.install === true) {
|
|
3531
|
+
for (const tag of resolvedConfig.target) {
|
|
3532
|
+
await this.#storeService.prepareCompilerModule(tag, this.#abortController.signal);
|
|
3533
|
+
}
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
const testFiles = configService.selectTestFiles();
|
|
3537
|
+
if (this.#process.exitCode === 1) {
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
if (configService.commandLineOptions.listFiles === true) {
|
|
3541
|
+
this.#logger.writeMessage(formattedText(testFiles));
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
EventEmitter.removeHandler(this.#onStartupEvent);
|
|
3545
|
+
const tstyche = new TSTyche(resolvedConfig, this.#storeService);
|
|
3546
|
+
await tstyche.run(testFiles);
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
export { Assertion, AssertionSource, Checker, Cli, CollectService, Color, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, Environment, EventEmitter, ExpectResult, FileResult, Line, Logger, OptionBrand, OptionDefinitionsMap, OptionGroup, ProjectResult, ProjectService, Reporter, Result, ResultCount, ResultManager, ResultStatus, ResultTiming, Scribbler, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, Text, ThoroughReporter, addsPackageStepText, describeNameText, diagnosticText, fileStatusText, fileViewText, summaryText, testNameText, usesCompilerStepText };
|