tstyche 2.0.0-rc.1 → 2.0.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/build/tstyche.js CHANGED
@@ -111,938 +111,118 @@ class ExitCodeHandler {
111
111
  }
112
112
  }
113
113
 
114
- class Environment {
115
- static #isCi = Environment.#resolveIsCi();
116
- static #noColor = Environment.#resolveNoColor();
117
- static #noInteractive = Environment.#resolveNoInteractive();
118
- static #storePath = Environment.#resolveStorePath();
119
- static #timeout = Environment.#resolveTimeout();
120
- static #typescriptPath = Environment.#resolveTypeScriptPath();
121
- static get isCi() {
122
- return Environment.#isCi;
123
- }
124
- static get noColor() {
125
- return Environment.#noColor;
126
- }
127
- static get noInteractive() {
128
- return Environment.#noInteractive;
129
- }
130
- static get storePath() {
131
- return Environment.#storePath;
132
- }
133
- static get timeout() {
134
- return Environment.#timeout;
135
- }
136
- static get typescriptPath() {
137
- return Environment.#typescriptPath;
138
- }
139
- static #resolveIsCi() {
140
- if (process.env["CI"] != null) {
141
- return process.env["CI"] !== "";
142
- }
143
- return false;
144
- }
145
- static #resolveNoColor() {
146
- if (process.env["TSTYCHE_NO_COLOR"] != null) {
147
- return process.env["TSTYCHE_NO_COLOR"] !== "";
148
- }
149
- if (process.env["NO_COLOR"] != null) {
150
- return process.env["NO_COLOR"] !== "";
151
- }
152
- return false;
153
- }
154
- static #resolveNoInteractive() {
155
- if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) {
156
- return process.env["TSTYCHE_NO_INTERACTIVE"] !== "";
157
- }
158
- return !process.stdout.isTTY;
159
- }
160
- static #resolveStorePath() {
161
- if (process.env["TSTYCHE_STORE_PATH"] != null) {
162
- return Path.resolve(process.env["TSTYCHE_STORE_PATH"]);
163
- }
164
- if (process.platform === "darwin") {
165
- return Path.resolve(os.homedir(), "Library", "TSTyche");
166
- }
167
- if (process.env["LocalAppData"] != null) {
168
- return Path.resolve(process.env["LocalAppData"], "TSTyche");
169
- }
170
- if (process.env["XDG_DATA_HOME"] != null) {
171
- return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
172
- }
173
- return Path.resolve(os.homedir(), ".local", "share", "TSTyche");
174
- }
175
- static #resolveTimeout() {
176
- if (process.env["TSTYCHE_TIMEOUT"] != null) {
177
- return Number(process.env["TSTYCHE_TIMEOUT"]);
178
- }
179
- return 30;
180
- }
181
- static #resolveTypeScriptPath() {
182
- let moduleId = "typescript";
183
- if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) {
184
- moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"];
185
- }
186
- let resolvedPath;
187
- try {
188
- resolvedPath = Path.normalizeSlashes(createRequire(import.meta.url).resolve(moduleId));
189
- }
190
- catch {
191
- }
192
- return resolvedPath;
114
+ class ResultTiming {
115
+ end = Number.NaN;
116
+ start = Number.NaN;
117
+ get duration() {
118
+ return this.end - this.start;
193
119
  }
194
120
  }
195
121
 
196
- function jsx(type, props) {
197
- return { props, type };
122
+ class DescribeResult {
123
+ describe;
124
+ parent;
125
+ results = [];
126
+ timing = new ResultTiming();
127
+ constructor(describe, parent) {
128
+ this.describe = describe;
129
+ this.parent = parent;
130
+ }
198
131
  }
199
132
 
200
- var Color;
201
- (function (Color) {
202
- Color["Reset"] = "0";
203
- Color["Red"] = "31";
204
- Color["Green"] = "32";
205
- Color["Yellow"] = "33";
206
- Color["Blue"] = "34";
207
- Color["Magenta"] = "35";
208
- Color["Cyan"] = "36";
209
- Color["Gray"] = "90";
210
- })(Color || (Color = {}));
133
+ var ResultStatus;
134
+ (function (ResultStatus) {
135
+ ResultStatus["Runs"] = "runs";
136
+ ResultStatus["Passed"] = "passed";
137
+ ResultStatus["Failed"] = "failed";
138
+ ResultStatus["Skipped"] = "skipped";
139
+ ResultStatus["Todo"] = "todo";
140
+ })(ResultStatus || (ResultStatus = {}));
211
141
 
212
- class Text {
213
- props;
214
- constructor(props) {
215
- this.props = props;
216
- }
217
- render() {
218
- const ansiEscapes = [];
219
- if (this.props.color != null) {
220
- ansiEscapes.push(this.props.color);
221
- }
222
- return (jsx("text", { indent: this.props.indent ?? 0, children: [ansiEscapes.length > 0 ? jsx("ansi", { escapes: ansiEscapes }) : undefined, this.props.children, ansiEscapes.length > 0 ? jsx("ansi", { escapes: "0" }) : undefined] }));
142
+ class ExpectResult {
143
+ assertion;
144
+ diagnostics = [];
145
+ parent;
146
+ status = "runs";
147
+ timing = new ResultTiming();
148
+ constructor(assertion, parent) {
149
+ this.assertion = assertion;
150
+ this.parent = parent;
223
151
  }
224
152
  }
225
153
 
226
- class Line {
227
- props;
228
- constructor(props) {
229
- this.props = props;
230
- }
231
- render() {
232
- return (jsx(Text, { color: this.props.color, indent: this.props.indent, children: [this.props.children, jsx("newLine", {})] }));
154
+ class ResultCount {
155
+ failed = 0;
156
+ passed = 0;
157
+ skipped = 0;
158
+ todo = 0;
159
+ get total() {
160
+ return this.failed + this.passed + this.skipped + this.todo;
233
161
  }
234
162
  }
235
163
 
236
- class Scribbler {
237
- #indentStep = " ";
238
- #newLine;
239
- #noColor;
240
- #notEmptyLineRegex = /^(?!$)/gm;
241
- constructor(options) {
242
- this.#newLine = options?.newLine ?? "\n";
243
- this.#noColor = options?.noColor ?? false;
244
- }
245
- #escapeSequence(attributes) {
246
- return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
247
- }
248
- #indentEachLine(lines, level) {
249
- if (level === 0) {
250
- return lines;
251
- }
252
- return lines.replace(this.#notEmptyLineRegex, this.#indentStep.repeat(level));
253
- }
254
- render(element) {
255
- if (typeof element.type === "function") {
256
- const instance = new element.type({ ...element.props });
257
- return this.render(instance.render());
258
- }
259
- if (element.type === "ansi" && !this.#noColor) {
260
- return this.#escapeSequence(element.props.escapes);
261
- }
262
- if (element.type === "newLine") {
263
- return this.#newLine;
264
- }
265
- if (element.type === "text") {
266
- const text = this.#visitChildren(element.props.children);
267
- return this.#indentEachLine(text, element.props.indent);
268
- }
269
- return "";
270
- }
271
- #visitChildren(children) {
272
- const text = [];
273
- for (const child of children) {
274
- if (typeof child === "string") {
275
- text.push(child);
276
- continue;
277
- }
278
- if (Array.isArray(child)) {
279
- text.push(this.#visitChildren(child));
280
- continue;
281
- }
282
- if (child != null && typeof child === "object") {
283
- text.push(this.render(child));
284
- }
285
- }
286
- return text.join("");
164
+ class FileResult {
165
+ diagnostics = [];
166
+ expectCount = new ResultCount();
167
+ results = [];
168
+ status = "runs";
169
+ testCount = new ResultCount();
170
+ testFile;
171
+ timing = new ResultTiming();
172
+ constructor(testFile) {
173
+ this.testFile = testFile;
287
174
  }
288
175
  }
289
176
 
290
- function addsPackageStepText(compilerVersion, installationPath) {
291
- return (jsx(Line, { children: [jsx(Text, { color: "90", children: "adds" }), " TypeScript ", compilerVersion, jsx(Text, { color: "90", children: [" to ", installationPath] })] }));
177
+ class ProjectResult {
178
+ compilerVersion;
179
+ diagnostics = [];
180
+ projectConfigFilePath;
181
+ results = [];
182
+ constructor(compilerVersion, projectConfigFilePath) {
183
+ this.compilerVersion = compilerVersion;
184
+ this.projectConfigFilePath = projectConfigFilePath;
185
+ }
292
186
  }
293
187
 
294
- function describeNameText(name, indent = 0) {
295
- return jsx(Line, { indent: indent + 1, children: name });
188
+ class Result {
189
+ expectCount = new ResultCount();
190
+ fileCount = new ResultCount();
191
+ resolvedConfig;
192
+ results = [];
193
+ targetCount = new ResultCount();
194
+ testCount = new ResultCount();
195
+ testFiles;
196
+ timing = new ResultTiming();
197
+ constructor(resolvedConfig, testFiles) {
198
+ this.resolvedConfig = resolvedConfig;
199
+ this.testFiles = testFiles;
200
+ }
296
201
  }
297
202
 
298
- class CodeSpanText {
299
- props;
300
- constructor(props) {
301
- this.props = props;
302
- }
303
- render() {
304
- const lastLineInFile = this.props.sourceFile.getLineAndCharacterOfPosition(this.props.sourceFile.text.length).line;
305
- const { character: markedCharacter, line: markedLine } = this.props.sourceFile.getLineAndCharacterOfPosition(this.props.start);
306
- const firstLine = Math.max(markedLine - 2, 0);
307
- const lastLine = Math.min(firstLine + 5, lastLineInFile);
308
- const lineNumberMaxWidth = String(lastLine + 1).length;
309
- const codeSpan = [];
310
- for (let index = firstLine; index <= lastLine; index++) {
311
- const lineStart = this.props.sourceFile.getPositionOfLineAndCharacter(index, 0);
312
- const lineEnd = index === lastLineInFile
313
- ? this.props.sourceFile.text.length
314
- : this.props.sourceFile.getPositionOfLineAndCharacter(index + 1, 0);
315
- const lineNumberText = String(index + 1);
316
- const lineText = this.props.sourceFile.text.slice(lineStart, lineEnd).trimEnd().replace(/\t/g, " ");
317
- if (index === markedLine) {
318
- codeSpan.push(jsx(Line, { children: [jsx(Text, { color: "31", children: ">" }), jsx(Text, { children: " " }), lineNumberText.padStart(lineNumberMaxWidth), jsx(Text, { children: " " }), jsx(Text, { color: "90", children: "|" }), " ", lineText] }), jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 3), jsx(Text, { color: "90", children: "|" }), " ".repeat(markedCharacter + 1), jsx(Text, { color: "31", children: "^" })] }));
319
- }
320
- else {
321
- codeSpan.push(jsx(Line, { children: [" ".repeat(2), jsx(Text, { color: "90", children: [lineNumberText.padStart(lineNumberMaxWidth), " | ", lineText || ""] })] }));
322
- }
323
- }
324
- const breadcrumbs = this.props.breadcrumbs?.flatMap((ancestor) => [
325
- jsx(Text, { color: "90", children: " ❭ " }),
326
- jsx(Text, { children: ancestor }),
327
- ]);
328
- const location = (jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 5), jsx(Text, { color: "90", children: "at" }), jsx(Text, { children: " " }), jsx(Text, { color: "36", children: Path.relative("", this.props.sourceFile.fileName) }), jsx(Text, { color: "90", children: [":", String(markedLine + 1), ":", String(markedCharacter + 1)] }), breadcrumbs] }));
329
- return (jsx(Text, { children: [codeSpan, jsx(Line, {}), location] }));
203
+ class TargetResult {
204
+ results = new Map();
205
+ status = "runs";
206
+ testFiles;
207
+ timing = new ResultTiming();
208
+ versionTag;
209
+ constructor(versionTag, testFiles) {
210
+ this.versionTag = versionTag;
211
+ this.testFiles = testFiles;
330
212
  }
331
213
  }
332
214
 
333
- class DiagnosticText {
334
- props;
335
- constructor(props) {
336
- this.props = props;
337
- }
338
- render() {
339
- const code = typeof this.props.diagnostic.code === "string" ? (jsx(Text, { color: "90", children: [" ", this.props.diagnostic.code] })) : undefined;
340
- const text = Array.isArray(this.props.diagnostic.text) ? this.props.diagnostic.text : [this.props.diagnostic.text];
341
- const message = text.map((text, index) => (jsx(Text, { children: [index === 1 ? jsx(Line, {}) : undefined, jsx(Line, { children: [text, code] })] })));
342
- const related = this.props.diagnostic.related?.map((relatedDiagnostic) => (jsx(DiagnosticText, { diagnostic: relatedDiagnostic })));
343
- const codeSpan = this.props.diagnostic.origin ? (jsx(Text, { children: [jsx(Line, {}), jsx(CodeSpanText, { ...this.props.diagnostic.origin })] })) : undefined;
344
- return (jsx(Text, { children: [message, codeSpan, jsx(Line, {}), jsx(Text, { indent: 2, children: related })] }));
345
- }
346
- }
347
- function diagnosticText(diagnostic) {
348
- let prefix;
349
- switch (diagnostic.category) {
350
- case "error": {
351
- prefix = jsx(Text, { color: "31", children: "Error: " });
352
- break;
353
- }
354
- case "warning": {
355
- prefix = jsx(Text, { color: "33", children: "Warning: " });
356
- break;
357
- }
358
- }
359
- return (jsx(Text, { children: [prefix, jsx(DiagnosticText, { diagnostic: diagnostic })] }));
360
- }
361
-
362
- class FileNameText {
363
- props;
364
- constructor(props) {
365
- this.props = props;
366
- }
367
- render() {
368
- const relativePath = Path.relative("", this.props.filePath);
369
- const lastPathSeparator = relativePath.lastIndexOf("/");
370
- const directoryNameText = relativePath.slice(0, lastPathSeparator + 1);
371
- const fileNameText = relativePath.slice(lastPathSeparator + 1);
372
- return (jsx(Text, { children: [jsx(Text, { color: "90", children: directoryNameText }), fileNameText] }));
373
- }
374
- }
375
- function fileStatusText(status, testFile) {
376
- let statusColor;
377
- let statusText;
378
- switch (status) {
379
- case "runs": {
380
- statusColor = "33";
381
- statusText = "runs";
382
- break;
383
- }
384
- case "passed": {
385
- statusColor = "32";
386
- statusText = "pass";
387
- break;
388
- }
389
- case "failed": {
390
- statusColor = "31";
391
- statusText = "fail";
392
- break;
393
- }
394
- }
395
- return (jsx(Line, { children: [jsx(Text, { color: statusColor, children: statusText }), " ", jsx(FileNameText, { filePath: testFile.path })] }));
396
- }
397
-
398
- function fileViewText(lines, addEmptyFinalLine) {
399
- return (jsx(Text, { children: [[...lines], addEmptyFinalLine ? jsx(Line, {}) : undefined] }));
400
- }
401
-
402
- class JsonText {
403
- props;
404
- constructor(props) {
405
- this.props = props;
406
- }
407
- render() {
408
- return jsx(Line, { children: JSON.stringify(this.#sortObject(this.props.input), null, 2) });
409
- }
410
- #sortObject(target) {
411
- if (Array.isArray(target)) {
412
- return target;
413
- }
414
- return Object.keys(target)
415
- .sort()
416
- .reduce((result, key) => {
417
- result[key] = target[key];
418
- return result;
419
- }, {});
420
- }
421
- }
422
- function formattedText(input) {
423
- if (typeof input === "string") {
424
- return jsx(Line, { children: input });
425
- }
426
- return jsx(JsonText, { input: input });
427
- }
428
-
429
- const usageExamples = [
430
- ["tstyche", "Run all tests."],
431
- ["tstyche path/to/first.test.ts", "Only run the test files with matching path."],
432
- ["tstyche --target 4.9,5.3.2,current", "Test on all specified versions of TypeScript."],
433
- ];
434
- class HintText {
435
- props;
436
- constructor(props) {
437
- this.props = props;
438
- }
439
- render() {
440
- return (jsx(Text, { indent: 1, color: "90", children: this.props.children }));
441
- }
442
- }
443
- class HelpHeaderText {
444
- props;
445
- constructor(props) {
446
- this.props = props;
447
- }
448
- render() {
449
- const hint = (jsx(HintText, { children: jsx(Text, { children: this.props.tstycheVersion }) }));
450
- return (jsx(Line, { children: [jsx(Text, { children: "The TSTyche Type Test Runner" }), hint] }));
451
- }
452
- }
453
- class CommandText {
454
- props;
455
- constructor(props) {
456
- this.props = props;
457
- }
458
- render() {
459
- let hint;
460
- if (this.props.hint != null) {
461
- hint = jsx(HintText, { children: this.props.hint });
462
- }
463
- return (jsx(Line, { indent: 1, children: [jsx(Text, { color: "34", children: this.props.text }), hint] }));
464
- }
465
- }
466
- class OptionDescriptionText {
467
- props;
468
- constructor(props) {
469
- this.props = props;
470
- }
471
- render() {
472
- return jsx(Line, { indent: 1, children: this.props.text });
473
- }
474
- }
475
- class CommandLineUsageText {
476
- render() {
477
- const usageText = usageExamples.map(([commandText, descriptionText]) => (jsx(Text, { children: [jsx(CommandText, { text: commandText }), jsx(OptionDescriptionText, { text: descriptionText }), jsx(Line, {})] })));
478
- return jsx(Text, { children: usageText });
479
- }
480
- }
481
- class CommandLineOptionNameText {
482
- props;
483
- constructor(props) {
484
- this.props = props;
485
- }
486
- render() {
487
- return jsx(Text, { children: ["--", this.props.text] });
488
- }
489
- }
490
- class CommandLineOptionHintText {
491
- props;
492
- constructor(props) {
493
- this.props = props;
494
- }
495
- render() {
496
- if (this.props.definition.brand === "list") {
497
- return (jsx(Text, { children: [this.props.definition.brand, " of ", this.props.definition.items.brand, "s"] }));
498
- }
499
- return jsx(Text, { children: this.props.definition.brand });
500
- }
501
- }
502
- class CommandLineOptionsText {
503
- props;
504
- constructor(props) {
505
- this.props = props;
506
- }
507
- render() {
508
- const definitions = [...this.props.optionDefinitions.values()];
509
- const optionsText = definitions.map((definition) => {
510
- let hint;
511
- if (definition.brand !== "bareTrue") {
512
- hint = jsx(CommandLineOptionHintText, { definition: definition });
513
- }
514
- return (jsx(Text, { children: [jsx(CommandText, { text: jsx(CommandLineOptionNameText, { text: definition.name }), hint: hint }), jsx(OptionDescriptionText, { text: definition.description }), jsx(Line, {})] }));
515
- });
516
- return (jsx(Text, { children: [jsx(Line, { children: "Command Line Options" }), jsx(Line, {}), optionsText] }));
517
- }
518
- }
519
- class HelpFooterText {
520
- render() {
521
- return jsx(Line, { children: "To learn more, visit https://tstyche.org" });
522
- }
523
- }
524
- function helpText(optionDefinitions, tstycheVersion) {
525
- return (jsx(Text, { children: [jsx(HelpHeaderText, { tstycheVersion: tstycheVersion }), jsx(Line, {}), jsx(CommandLineUsageText, {}), jsx(Line, {}), jsx(CommandLineOptionsText, { optionDefinitions: optionDefinitions }), jsx(Line, {}), jsx(HelpFooterText, {}), jsx(Line, {})] }));
526
- }
527
-
528
- class OutputService {
529
- #isClear = false;
530
- #noColor;
531
- #scribbler;
532
- #stderr;
533
- #stdout;
534
- constructor(options) {
535
- this.#noColor = options?.noColor ?? Environment.noColor;
536
- this.#stderr = options?.stderr ?? process.stderr;
537
- this.#stdout = options?.stdout ?? process.stdout;
538
- this.#scribbler = new Scribbler({ noColor: this.#noColor });
539
- }
540
- clearTerminal() {
541
- if (!this.#isClear) {
542
- this.#stdout.write("\u001B[2J\u001B[3J\u001B[H");
543
- this.#isClear = true;
544
- }
545
- }
546
- eraseLastLine() {
547
- this.#stdout.write("\u001B[1A\u001B[0K");
548
- }
549
- #write(stream, body) {
550
- const elements = Array.isArray(body) ? body : [body];
551
- for (const element of elements) {
552
- stream.write(this.#scribbler.render(element));
553
- }
554
- this.#isClear = false;
555
- }
556
- writeError(body) {
557
- this.#write(this.#stderr, body);
558
- }
559
- writeMessage(body) {
560
- this.#write(this.#stdout, body);
561
- }
562
- writeWarning(body) {
563
- this.#write(this.#stderr, body);
564
- }
565
- }
566
-
567
- class RowText {
568
- props;
569
- constructor(props) {
570
- this.props = props;
571
- }
572
- render() {
573
- return (jsx(Line, { children: [`${this.props.label}:`.padEnd(12), this.props.text] }));
574
- }
575
- }
576
- class CountText {
577
- props;
578
- constructor(props) {
579
- this.props = props;
580
- }
581
- render() {
582
- return (jsx(Text, { children: [this.props.failed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "31", children: [String(this.props.failed), " failed"] }), jsx(Text, { children: ", " })] })) : undefined, this.props.skipped > 0 ? (jsx(Text, { children: [jsx(Text, { color: "33", children: [String(this.props.skipped), " skipped"] }), jsx(Text, { children: ", " })] })) : undefined, this.props.todo > 0 ? (jsx(Text, { children: [jsx(Text, { color: "35", children: [String(this.props.todo), " todo"] }), jsx(Text, { children: ", " })] })) : undefined, this.props.passed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "32", children: [String(this.props.passed), " passed"] }), jsx(Text, { children: ", " })] })) : undefined, jsx(Text, { children: [String(this.props.total), jsx(Text, { children: " total" })] })] }));
583
- }
584
- }
585
- class DurationText {
586
- props;
587
- constructor(props) {
588
- this.props = props;
589
- }
590
- render() {
591
- const duration = this.props.duration / 1000;
592
- const minutes = Math.floor(duration / 60);
593
- const seconds = duration % 60;
594
- return (jsx(Text, { children: [minutes > 0 ? `${String(minutes)}m ` : undefined, `${String(Math.round(seconds * 10) / 10)}s`] }));
595
- }
596
- }
597
- class MatchText {
598
- props;
599
- constructor(props) {
600
- this.props = props;
601
- }
602
- render() {
603
- if (typeof this.props.text === "string") {
604
- return jsx(Text, { children: ["'", this.props.text, "'"] });
605
- }
606
- if (this.props.text.length <= 1) {
607
- return jsx(Text, { children: ["'", ...this.props.text, "'"] });
608
- }
609
- const lastItem = this.props.text.pop();
610
- return (jsx(Text, { children: [this.props.text.map((match, index, list) => (jsx(Text, { children: ["'", match, "'", index === list.length - 1 ? jsx(Text, { children: " " }) : jsx(Text, { color: "90", children: ", " })] }))), jsx(Text, { color: "90", children: "or" }), " '", lastItem, "'"] }));
611
- }
612
- }
613
- class RanFilesText {
614
- props;
615
- constructor(props) {
616
- this.props = props;
617
- }
618
- render() {
619
- const testNameMatchText = [];
620
- if (this.props.onlyMatch != null) {
621
- testNameMatchText.push(jsx(Text, { children: [jsx(Text, { color: "90", children: "matching " }), jsx(MatchText, { text: this.props.onlyMatch })] }));
622
- }
623
- if (this.props.skipMatch != null) {
624
- testNameMatchText.push(jsx(Text, { children: [this.props.onlyMatch == null ? undefined : jsx(Text, { color: "90", children: " and " }), jsx(Text, { color: "90", children: "not matching " }), jsx(MatchText, { text: this.props.skipMatch })] }));
625
- }
626
- let pathMatchText;
627
- if (this.props.pathMatch.length > 0) {
628
- pathMatchText = (jsx(Text, { children: [jsx(Text, { color: "90", children: "test files matching " }), jsx(MatchText, { text: this.props.pathMatch }), jsx(Text, { color: "90", children: "." })] }));
629
- }
630
- else {
631
- pathMatchText = jsx(Text, { color: "90", children: "all test files." });
632
- }
633
- return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Ran " }), testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: "tests " }) : undefined, testNameMatchText, testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: " in " }) : undefined, pathMatchText] }));
634
- }
635
- }
636
- function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) {
637
- const targetCountText = (jsx(RowText, { label: "Targets", text: jsx(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) }));
638
- const fileCountText = (jsx(RowText, { label: "Test files", text: jsx(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) }));
639
- const testCountText = (jsx(RowText, { label: "Tests", text: jsx(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) }));
640
- const assertionCountText = (jsx(RowText, { label: "Assertions", text: jsx(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) }));
641
- return (jsx(Text, { children: [targetCountText, fileCountText, testCount.total > 0 ? testCountText : undefined, expectCount.total > 0 ? assertionCountText : undefined, jsx(RowText, { label: "Duration", text: jsx(DurationText, { duration: duration }) }), jsx(Line, {}), jsx(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch })] }));
642
- }
643
-
644
- class StatusText {
645
- props;
646
- constructor(props) {
647
- this.props = props;
648
- }
649
- render() {
650
- switch (this.props.status) {
651
- case "fail":
652
- return jsx(Text, { color: "31", children: "\u00D7" });
653
- case "pass":
654
- return jsx(Text, { color: "32", children: "+" });
655
- case "skip":
656
- return jsx(Text, { color: "33", children: "- skip" });
657
- case "todo":
658
- return jsx(Text, { color: "35", children: "- todo" });
659
- }
660
- }
661
- }
662
- function testNameText(status, name, indent = 0) {
663
- return (jsx(Line, { indent: indent + 1, children: [jsx(StatusText, { status: status }), " ", jsx(Text, { color: "90", children: name })] }));
664
- }
665
-
666
- class ProjectNameText {
667
- props;
668
- constructor(props) {
669
- this.props = props;
670
- }
671
- render() {
672
- return (jsx(Text, { color: "90", children: [" with ", Path.relative("", this.props.filePath)] }));
673
- }
674
- }
675
- function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) {
676
- let projectPathText;
677
- if (tsconfigFilePath != null) {
678
- projectPathText = jsx(ProjectNameText, { filePath: tsconfigFilePath });
679
- }
680
- return (jsx(Text, { children: [options?.prependEmptyLine === true ? jsx(Line, {}) : undefined, jsx(Line, { children: [jsx(Text, { color: "34", children: "uses" }), " TypeScript ", compilerVersion, projectPathText] }), jsx(Line, {})] }));
681
- }
682
-
683
- function waitingForFileChangesText() {
684
- return jsx(Line, { children: "Waiting for file changes." });
685
- }
686
-
687
- function watchUsageText() {
688
- const usageText = Object.entries({ a: "run all tests", x: "exit" }).map(([key, action]) => {
689
- return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Press" }), jsx(Text, { children: ` ${key} ` }), jsx(Text, { color: "90", children: `to ${action}.` })] }));
690
- });
691
- return jsx(Text, { children: usageText });
692
- }
693
-
694
- class FileViewService {
695
- #indent = 0;
696
- #lines = [];
697
- #messages = [];
698
- get hasErrors() {
699
- return this.#messages.length > 0;
700
- }
701
- addMessage(message) {
702
- this.#messages.push(message);
703
- }
704
- addTest(status, name) {
705
- this.#lines.push(testNameText(status, name, this.#indent));
706
- }
707
- beginDescribe(name) {
708
- this.#lines.push(describeNameText(name, this.#indent));
709
- this.#indent++;
710
- }
711
- clear() {
712
- this.#indent = 0;
713
- this.#lines = [];
714
- this.#messages = [];
715
- }
716
- endDescribe() {
717
- this.#indent--;
718
- }
719
- getMessages() {
720
- return this.#messages;
721
- }
722
- getViewText(options) {
723
- return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
724
- }
725
- }
726
-
727
- class RuntimeReporter {
728
- #currentCompilerVersion;
729
- #currentProjectConfigFilePath;
730
- #fileCount = 0;
731
- #fileView = new FileViewService();
732
- #hasReportedAdds = false;
733
- #hasReportedError = false;
734
- #isFileViewExpanded = false;
735
- #resolvedConfig;
736
- #outputService;
737
- #seenDeprecations = new Set();
738
- constructor(resolvedConfig, outputService) {
739
- this.#resolvedConfig = resolvedConfig;
740
- this.#outputService = outputService;
741
- }
742
- get #isLastFile() {
743
- return this.#fileCount === 0;
744
- }
745
- handleEvent([eventName, payload]) {
746
- switch (eventName) {
747
- case "deprecation:info": {
748
- for (const diagnostic of payload.diagnostics) {
749
- if (!this.#seenDeprecations.has(diagnostic.text.toString())) {
750
- this.#fileView.addMessage(diagnosticText(diagnostic));
751
- this.#seenDeprecations.add(diagnostic.text.toString());
752
- }
753
- }
754
- break;
755
- }
756
- case "run:start": {
757
- this.#isFileViewExpanded = payload.result.testFiles.length === 1 && this.#resolvedConfig.watch !== true;
758
- break;
759
- }
760
- case "store:info": {
761
- this.#outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
762
- this.#hasReportedAdds = true;
763
- break;
764
- }
765
- case "store:error": {
766
- for (const diagnostic of payload.diagnostics) {
767
- this.#outputService.writeError(diagnosticText(diagnostic));
768
- }
769
- break;
770
- }
771
- case "target:start": {
772
- this.#fileCount = payload.result.testFiles.length;
773
- break;
774
- }
775
- case "target:end": {
776
- this.#currentCompilerVersion = undefined;
777
- this.#currentProjectConfigFilePath = undefined;
778
- break;
779
- }
780
- case "project:info": {
781
- if (this.#currentCompilerVersion !== payload.compilerVersion ||
782
- this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) {
783
- this.#outputService.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
784
- prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds && !this.#hasReportedError,
785
- }));
786
- this.#hasReportedAdds = false;
787
- this.#currentCompilerVersion = payload.compilerVersion;
788
- this.#currentProjectConfigFilePath = payload.projectConfigFilePath;
789
- }
790
- break;
791
- }
792
- case "project:error": {
793
- for (const diagnostic of payload.diagnostics) {
794
- this.#outputService.writeError(diagnosticText(diagnostic));
795
- }
796
- break;
797
- }
798
- case "file:start": {
799
- if (!Environment.noInteractive) {
800
- this.#outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
801
- }
802
- this.#fileCount--;
803
- this.#hasReportedError = false;
804
- break;
805
- }
806
- case "file:error": {
807
- for (const diagnostic of payload.diagnostics) {
808
- this.#fileView.addMessage(diagnosticText(diagnostic));
809
- }
810
- break;
811
- }
812
- case "file:end": {
813
- if (!Environment.noInteractive) {
814
- this.#outputService.eraseLastLine();
815
- }
816
- this.#outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
817
- this.#outputService.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
818
- if (this.#fileView.hasErrors) {
819
- this.#outputService.writeError(this.#fileView.getMessages());
820
- this.#hasReportedError = true;
821
- }
822
- this.#fileView.clear();
823
- this.#seenDeprecations.clear();
824
- break;
825
- }
826
- case "describe:start": {
827
- if (this.#isFileViewExpanded) {
828
- this.#fileView.beginDescribe(payload.result.describe.name);
829
- }
830
- break;
831
- }
832
- case "describe:end": {
833
- if (this.#isFileViewExpanded) {
834
- this.#fileView.endDescribe();
835
- }
836
- break;
837
- }
838
- case "test:skip": {
839
- if (this.#isFileViewExpanded) {
840
- this.#fileView.addTest("skip", payload.result.test.name);
841
- }
842
- break;
843
- }
844
- case "test:todo": {
845
- if (this.#isFileViewExpanded) {
846
- this.#fileView.addTest("todo", payload.result.test.name);
847
- }
848
- break;
849
- }
850
- case "test:error": {
851
- if (this.#isFileViewExpanded) {
852
- this.#fileView.addTest("fail", payload.result.test.name);
853
- }
854
- for (const diagnostic of payload.diagnostics) {
855
- this.#fileView.addMessage(diagnosticText(diagnostic));
856
- }
857
- break;
858
- }
859
- case "test:fail": {
860
- if (this.#isFileViewExpanded) {
861
- this.#fileView.addTest("fail", payload.result.test.name);
862
- }
863
- break;
864
- }
865
- case "test:pass": {
866
- if (this.#isFileViewExpanded) {
867
- this.#fileView.addTest("pass", payload.result.test.name);
868
- }
869
- break;
870
- }
871
- case "expect:error":
872
- case "expect:fail": {
873
- for (const diagnostic of payload.diagnostics) {
874
- this.#fileView.addMessage(diagnosticText(diagnostic));
875
- }
876
- break;
877
- }
878
- }
879
- }
880
- }
881
-
882
- class SetupReporter {
883
- #outputService;
884
- constructor(outputService) {
885
- this.#outputService = outputService;
886
- }
887
- handleEvent([eventName, payload]) {
888
- if (eventName === "store:info") {
889
- this.#outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
890
- return;
891
- }
892
- if ("diagnostics" in payload) {
893
- for (const diagnostic of payload.diagnostics) {
894
- switch (diagnostic.category) {
895
- case "error": {
896
- this.#outputService.writeError(diagnosticText(diagnostic));
897
- break;
898
- }
899
- case "warning": {
900
- this.#outputService.writeWarning(diagnosticText(diagnostic));
901
- break;
902
- }
903
- }
904
- }
905
- }
906
- }
907
- }
908
-
909
- class SummaryReporter {
910
- #outputService;
911
- constructor(outputService) {
912
- this.#outputService = outputService;
913
- }
914
- handleEvent([eventName, payload]) {
915
- switch (eventName) {
916
- case "run:end": {
917
- this.#outputService.writeMessage(summaryText({
918
- duration: payload.result.timing.duration,
919
- expectCount: payload.result.expectCount,
920
- fileCount: payload.result.fileCount,
921
- onlyMatch: payload.result.resolvedConfig.only,
922
- pathMatch: payload.result.resolvedConfig.pathMatch,
923
- skipMatch: payload.result.resolvedConfig.skip,
924
- targetCount: payload.result.targetCount,
925
- testCount: payload.result.testCount,
926
- }));
927
- break;
928
- }
929
- }
930
- }
931
- }
932
-
933
- class WatchReporter {
934
- #outputService;
935
- constructor(outputService) {
936
- this.#outputService = outputService;
937
- }
938
- handleEvent([eventName, payload]) {
939
- switch (eventName) {
940
- case "run:start": {
941
- this.#outputService.clearTerminal();
942
- break;
943
- }
944
- case "run:end": {
945
- this.#outputService.writeMessage(watchUsageText());
946
- break;
947
- }
948
- case "watch:error": {
949
- this.#outputService.clearTerminal();
950
- for (const diagnostic of payload.diagnostics) {
951
- this.#outputService.writeError(diagnosticText(diagnostic));
952
- }
953
- this.#outputService.writeMessage(waitingForFileChangesText());
954
- break;
955
- }
956
- }
957
- }
958
- }
959
-
960
- class ResultTiming {
961
- end = Number.NaN;
962
- start = Number.NaN;
963
- get duration() {
964
- return this.end - this.start;
965
- }
966
- }
967
-
968
- class DescribeResult {
969
- describe;
970
- parent;
971
- results = [];
972
- timing = new ResultTiming();
973
- constructor(describe, parent) {
974
- this.describe = describe;
975
- this.parent = parent;
976
- }
977
- }
978
-
979
- var ResultStatus;
980
- (function (ResultStatus) {
981
- ResultStatus["Runs"] = "runs";
982
- ResultStatus["Passed"] = "passed";
983
- ResultStatus["Failed"] = "failed";
984
- ResultStatus["Skipped"] = "skipped";
985
- ResultStatus["Todo"] = "todo";
986
- })(ResultStatus || (ResultStatus = {}));
987
-
988
- class ExpectResult {
989
- assertion;
990
- diagnostics = [];
991
- parent;
992
- status = "runs";
993
- timing = new ResultTiming();
994
- constructor(assertion, parent) {
995
- this.assertion = assertion;
996
- this.parent = parent;
997
- }
998
- }
999
-
1000
- class ResultCount {
1001
- failed = 0;
1002
- passed = 0;
1003
- skipped = 0;
1004
- todo = 0;
1005
- get total() {
1006
- return this.failed + this.passed + this.skipped + this.todo;
1007
- }
1008
- }
1009
-
1010
- class FileResult {
1011
- diagnostics = [];
1012
- expectCount = new ResultCount();
1013
- results = [];
1014
- status = "runs";
1015
- testCount = new ResultCount();
1016
- testFile;
1017
- timing = new ResultTiming();
1018
- constructor(testFile) {
1019
- this.testFile = testFile;
1020
- }
1021
- }
1022
-
1023
- class ProjectResult {
1024
- compilerVersion;
1025
- diagnostics = [];
1026
- projectConfigFilePath;
1027
- results = [];
1028
- constructor(compilerVersion, projectConfigFilePath) {
1029
- this.compilerVersion = compilerVersion;
1030
- this.projectConfigFilePath = projectConfigFilePath;
1031
- }
1032
- }
1033
-
1034
- class Result {
1035
- expectCount = new ResultCount();
1036
- fileCount = new ResultCount();
1037
- resolvedConfig;
1038
- results = [];
1039
- targetCount = new ResultCount();
1040
- testCount = new ResultCount();
1041
- testFiles;
1042
- timing = new ResultTiming();
1043
- constructor(resolvedConfig, testFiles) {
1044
- this.resolvedConfig = resolvedConfig;
1045
- this.testFiles = testFiles;
215
+ class TestResult {
216
+ diagnostics = [];
217
+ expectCount = new ResultCount();
218
+ parent;
219
+ results = [];
220
+ status = "runs";
221
+ test;
222
+ timing = new ResultTiming();
223
+ constructor(test, parent) {
224
+ this.test = test;
225
+ this.parent = parent;
1046
226
  }
1047
227
  }
1048
228
 
@@ -1262,783 +442,883 @@ class ResultHandler {
1262
442
  }
1263
443
  }
1264
444
 
1265
- class TargetResult {
1266
- results = new Map();
1267
- status = "runs";
1268
- testFiles;
1269
- timing = new ResultTiming();
1270
- versionTag;
1271
- constructor(versionTag, testFiles) {
1272
- this.versionTag = versionTag;
1273
- this.testFiles = testFiles;
445
+ class Environment {
446
+ static #isCi = Environment.#resolveIsCi();
447
+ static #noColor = Environment.#resolveNoColor();
448
+ static #noInteractive = Environment.#resolveNoInteractive();
449
+ static #storePath = Environment.#resolveStorePath();
450
+ static #timeout = Environment.#resolveTimeout();
451
+ static #typescriptPath = Environment.#resolveTypeScriptPath();
452
+ static get isCi() {
453
+ return Environment.#isCi;
1274
454
  }
1275
- }
1276
-
1277
- class TestResult {
1278
- diagnostics = [];
1279
- expectCount = new ResultCount();
1280
- parent;
1281
- results = [];
1282
- status = "runs";
1283
- test;
1284
- timing = new ResultTiming();
1285
- constructor(test, parent) {
1286
- this.test = test;
1287
- this.parent = parent;
455
+ static get noColor() {
456
+ return Environment.#noColor;
1288
457
  }
1289
- }
1290
-
1291
- class CancellationToken {
1292
- #isCancelled = false;
1293
- #handlers = new Set();
1294
- #reason;
1295
- get isCancellationRequested() {
1296
- return this.#isCancelled;
458
+ static get noInteractive() {
459
+ return Environment.#noInteractive;
1297
460
  }
1298
- get reason() {
1299
- return this.#reason;
461
+ static get storePath() {
462
+ return Environment.#storePath;
1300
463
  }
1301
- cancel(reason) {
1302
- if (!this.#isCancelled) {
1303
- for (const handler of this.#handlers) {
1304
- handler(reason);
1305
- }
1306
- this.#isCancelled = true;
1307
- this.#reason = reason;
464
+ static get timeout() {
465
+ return Environment.#timeout;
466
+ }
467
+ static get typescriptPath() {
468
+ return Environment.#typescriptPath;
469
+ }
470
+ static #resolveIsCi() {
471
+ if (process.env["CI"] != null) {
472
+ return process.env["CI"] !== "";
1308
473
  }
474
+ return false;
1309
475
  }
1310
- onCancellationRequested(handler) {
1311
- this.#handlers.add(handler);
476
+ static #resolveNoColor() {
477
+ if (process.env["TSTYCHE_NO_COLOR"] != null) {
478
+ return process.env["TSTYCHE_NO_COLOR"] !== "";
479
+ }
480
+ if (process.env["NO_COLOR"] != null) {
481
+ return process.env["NO_COLOR"] !== "";
482
+ }
483
+ return false;
1312
484
  }
1313
- reset() {
1314
- if (this.#isCancelled) {
1315
- this.#isCancelled = false;
1316
- this.#reason = undefined;
485
+ static #resolveNoInteractive() {
486
+ if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) {
487
+ return process.env["TSTYCHE_NO_INTERACTIVE"] !== "";
488
+ }
489
+ return !process.stdout.isTTY;
490
+ }
491
+ static #resolveStorePath() {
492
+ if (process.env["TSTYCHE_STORE_PATH"] != null) {
493
+ return Path.resolve(process.env["TSTYCHE_STORE_PATH"]);
494
+ }
495
+ if (process.platform === "darwin") {
496
+ return Path.resolve(os.homedir(), "Library", "TSTyche");
497
+ }
498
+ if (process.env["LocalAppData"] != null) {
499
+ return Path.resolve(process.env["LocalAppData"], "TSTyche");
500
+ }
501
+ if (process.env["XDG_DATA_HOME"] != null) {
502
+ return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
503
+ }
504
+ return Path.resolve(os.homedir(), ".local", "share", "TSTyche");
505
+ }
506
+ static #resolveTimeout() {
507
+ if (process.env["TSTYCHE_TIMEOUT"] != null) {
508
+ return Number(process.env["TSTYCHE_TIMEOUT"]);
509
+ }
510
+ return 30;
511
+ }
512
+ static #resolveTypeScriptPath() {
513
+ let moduleId = "typescript";
514
+ if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) {
515
+ moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"];
516
+ }
517
+ let resolvedPath;
518
+ try {
519
+ resolvedPath = Path.normalizeSlashes(createRequire(import.meta.url).resolve(moduleId));
520
+ }
521
+ catch {
1317
522
  }
523
+ return resolvedPath;
1318
524
  }
1319
525
  }
1320
526
 
1321
- var CancellationReason;
1322
- (function (CancellationReason) {
1323
- CancellationReason["ConfigChange"] = "configChange";
1324
- CancellationReason["ConfigError"] = "configError";
1325
- CancellationReason["FailFast"] = "failFast";
1326
- })(CancellationReason || (CancellationReason = {}));
527
+ function jsx(type, props) {
528
+ return { props, type };
529
+ }
1327
530
 
1328
- class Watcher {
1329
- #abortController = new AbortController();
1330
- #onChanged;
1331
- #onRemoved;
1332
- #recursive;
1333
- #targetPath;
1334
- #watcher;
1335
- constructor(targetPath, onChanged, onRemoved, options) {
1336
- this.#targetPath = targetPath;
1337
- this.#onChanged = onChanged;
1338
- this.#onRemoved = onRemoved ?? onChanged;
1339
- this.#recursive = options?.recursive;
531
+ var Color;
532
+ (function (Color) {
533
+ Color["Reset"] = "0";
534
+ Color["Red"] = "31";
535
+ Color["Green"] = "32";
536
+ Color["Yellow"] = "33";
537
+ Color["Blue"] = "34";
538
+ Color["Magenta"] = "35";
539
+ Color["Cyan"] = "36";
540
+ Color["Gray"] = "90";
541
+ })(Color || (Color = {}));
542
+
543
+ function Text({ children, color, indent }) {
544
+ const ansiEscapes = [];
545
+ if (color != null) {
546
+ ansiEscapes.push(color);
1340
547
  }
1341
- close() {
1342
- this.#abortController.abort();
548
+ return (jsx("text", { indent: indent ?? 0, children: [ansiEscapes.length > 0 ? jsx("ansi", { escapes: ansiEscapes }) : undefined, children, ansiEscapes.length > 0 ? jsx("ansi", { escapes: "0" }) : undefined] }));
549
+ }
550
+
551
+ function Line({ children, color, indent }) {
552
+ return (jsx(Text, { color: color, indent: indent, children: [children, jsx("newLine", {})] }));
553
+ }
554
+
555
+ class Scribbler {
556
+ #indentStep = " ";
557
+ #newLine;
558
+ #noColor;
559
+ #notEmptyLineRegex = /^(?!$)/gm;
560
+ constructor(options) {
561
+ this.#newLine = options?.newLine ?? "\n";
562
+ this.#noColor = options?.noColor ?? false;
1343
563
  }
1344
- async watch() {
1345
- this.#watcher = fs.watch(this.#targetPath, { recursive: this.#recursive, signal: this.#abortController.signal });
1346
- try {
1347
- for await (const event of this.#watcher) {
1348
- if (event.filename != null) {
1349
- const filePath = Path.resolve(this.#targetPath, event.filename);
1350
- if (existsSync(filePath)) {
1351
- await this.#onChanged(filePath);
1352
- }
1353
- else {
1354
- await this.#onRemoved(filePath);
1355
- }
1356
- }
564
+ #escapeSequence(attributes) {
565
+ return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
566
+ }
567
+ #indentEachLine(lines, level) {
568
+ if (level === 0) {
569
+ return lines;
570
+ }
571
+ return lines.replace(this.#notEmptyLineRegex, this.#indentStep.repeat(level));
572
+ }
573
+ render(element) {
574
+ if (typeof element.type === "function") {
575
+ return this.render(element.type({ ...element.props }));
576
+ }
577
+ if (element.type === "ansi" && !this.#noColor) {
578
+ return this.#escapeSequence(element.props.escapes);
579
+ }
580
+ if (element.type === "newLine") {
581
+ return this.#newLine;
582
+ }
583
+ if (element.type === "text") {
584
+ const text = this.#visitChildren(element.props.children);
585
+ return this.#indentEachLine(text, element.props.indent);
586
+ }
587
+ return "";
588
+ }
589
+ #visitChildren(children) {
590
+ const text = [];
591
+ for (const child of children) {
592
+ if (typeof child === "string") {
593
+ text.push(child);
594
+ continue;
595
+ }
596
+ if (Array.isArray(child)) {
597
+ text.push(this.#visitChildren(child));
598
+ continue;
599
+ }
600
+ if (child != null && typeof child === "object") {
601
+ text.push(this.render(child));
1357
602
  }
1358
603
  }
1359
- catch (error) {
1360
- if (error instanceof Error && error.name === "AbortError") ;
1361
- }
604
+ return text.join("");
1362
605
  }
1363
606
  }
1364
607
 
1365
- class FileWatcher extends Watcher {
1366
- constructor(targetPath, onChanged) {
1367
- const onChangedFile = async (filePath) => {
1368
- if (filePath === targetPath) {
1369
- await onChanged();
1370
- }
1371
- };
1372
- super(Path.dirname(targetPath), onChangedFile);
1373
- }
608
+ function addsPackageStepText(compilerVersion, installationPath) {
609
+ return (jsx(Line, { children: [jsx(Text, { color: "90", children: "adds" }), " TypeScript ", compilerVersion, jsx(Text, { color: "90", children: [" to ", installationPath] })] }));
1374
610
  }
1375
611
 
1376
- class DiagnosticOrigin {
1377
- breadcrumbs;
1378
- end;
1379
- sourceFile;
1380
- start;
1381
- constructor(start, end, sourceFile, breadcrumbs) {
1382
- this.start = start;
1383
- this.end = end;
1384
- this.sourceFile = sourceFile;
1385
- this.breadcrumbs = breadcrumbs;
1386
- }
1387
- static fromJsonNode(node, sourceFile, skipTrivia) {
1388
- return new DiagnosticOrigin(skipTrivia(node.pos, sourceFile), node.end, sourceFile);
1389
- }
1390
- static fromNode(node, breadcrumbs) {
1391
- return new DiagnosticOrigin(node.getStart(), node.getEnd(), node.getSourceFile(), breadcrumbs);
612
+ function describeNameText(name, indent = 0) {
613
+ return jsx(Line, { indent: indent + 1, children: name });
614
+ }
615
+
616
+ function CodeSpanText(diagnosticOrigin) {
617
+ const lastLineInFile = diagnosticOrigin.sourceFile.getLineAndCharacterOfPosition(diagnosticOrigin.sourceFile.text.length).line;
618
+ const { character: markedCharacter, line: markedLine } = diagnosticOrigin.sourceFile.getLineAndCharacterOfPosition(diagnosticOrigin.start);
619
+ const firstLine = Math.max(markedLine - 2, 0);
620
+ const lastLine = Math.min(firstLine + 5, lastLineInFile);
621
+ const lineNumberMaxWidth = String(lastLine + 1).length;
622
+ const codeSpan = [];
623
+ for (let index = firstLine; index <= lastLine; index++) {
624
+ const lineStart = diagnosticOrigin.sourceFile.getPositionOfLineAndCharacter(index, 0);
625
+ const lineEnd = index === lastLineInFile
626
+ ? diagnosticOrigin.sourceFile.text.length
627
+ : diagnosticOrigin.sourceFile.getPositionOfLineAndCharacter(index + 1, 0);
628
+ const lineNumberText = String(index + 1);
629
+ const lineText = diagnosticOrigin.sourceFile.text.slice(lineStart, lineEnd).trimEnd().replace(/\t/g, " ");
630
+ if (index === markedLine) {
631
+ codeSpan.push(jsx(Line, { children: [jsx(Text, { color: "31", children: ">" }), jsx(Text, { children: " " }), lineNumberText.padStart(lineNumberMaxWidth), jsx(Text, { children: " " }), jsx(Text, { color: "90", children: "|" }), " ", lineText] }), jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 3), jsx(Text, { color: "90", children: "|" }), " ".repeat(markedCharacter + 1), jsx(Text, { color: "31", children: "^" })] }));
632
+ }
633
+ else {
634
+ codeSpan.push(jsx(Line, { children: [" ".repeat(2), jsx(Text, { color: "90", children: [lineNumberText.padStart(lineNumberMaxWidth), " | ", lineText || ""] })] }));
635
+ }
1392
636
  }
637
+ const breadcrumbs = diagnosticOrigin.breadcrumbs?.flatMap((ancestor) => [
638
+ jsx(Text, { color: "90", children: " ❭ " }),
639
+ jsx(Text, { children: ancestor }),
640
+ ]);
641
+ const location = (jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 5), jsx(Text, { color: "90", children: "at" }), jsx(Text, { children: " " }), jsx(Text, { color: "36", children: Path.relative("", diagnosticOrigin.sourceFile.fileName) }), jsx(Text, { color: "90", children: [":", String(markedLine + 1), ":", String(markedCharacter + 1)] }), breadcrumbs] }));
642
+ return (jsx(Text, { children: [codeSpan, jsx(Line, {}), location] }));
1393
643
  }
1394
644
 
1395
- class Diagnostic {
1396
- category;
1397
- code;
1398
- related;
1399
- origin;
1400
- text;
1401
- constructor(text, category, origin) {
1402
- this.text = text;
1403
- this.category = category;
1404
- this.origin = origin;
645
+ function DiagnosticText({ diagnostic }) {
646
+ const code = typeof diagnostic.code === "string" ? jsx(Text, { color: "90", children: [" ", diagnostic.code] }) : undefined;
647
+ const text = Array.isArray(diagnostic.text) ? diagnostic.text : [diagnostic.text];
648
+ const message = text.map((text, index) => (jsx(Text, { children: [index === 1 ? jsx(Line, {}) : undefined, jsx(Line, { children: [text, code] })] })));
649
+ const related = diagnostic.related?.map((relatedDiagnostic) => jsx(DiagnosticText, { diagnostic: relatedDiagnostic }));
650
+ const codeSpan = diagnostic.origin ? (jsx(Text, { children: [jsx(Line, {}), jsx(CodeSpanText, { ...diagnostic.origin })] })) : undefined;
651
+ return (jsx(Text, { children: [message, codeSpan, jsx(Line, {}), jsx(Text, { indent: 2, children: related })] }));
652
+ }
653
+ function diagnosticText(diagnostic) {
654
+ let prefix;
655
+ switch (diagnostic.category) {
656
+ case "error": {
657
+ prefix = jsx(Text, { color: "31", children: "Error: " });
658
+ break;
659
+ }
660
+ case "warning": {
661
+ prefix = jsx(Text, { color: "33", children: "Warning: " });
662
+ break;
663
+ }
1405
664
  }
1406
- add(options) {
1407
- if (options.code != null) {
1408
- this.code = options.code;
665
+ return (jsx(Text, { children: [prefix, jsx(DiagnosticText, { diagnostic: diagnostic })] }));
666
+ }
667
+
668
+ function FileNameText({ filePath }) {
669
+ const relativePath = Path.relative("", filePath);
670
+ const lastPathSeparator = relativePath.lastIndexOf("/");
671
+ const directoryNameText = relativePath.slice(0, lastPathSeparator + 1);
672
+ const fileNameText = relativePath.slice(lastPathSeparator + 1);
673
+ return (jsx(Text, { children: [jsx(Text, { color: "90", children: directoryNameText }), fileNameText] }));
674
+ }
675
+ function fileStatusText(status, testFile) {
676
+ let statusColor;
677
+ let statusText;
678
+ switch (status) {
679
+ case "runs": {
680
+ statusColor = "33";
681
+ statusText = "runs";
682
+ break;
1409
683
  }
1410
- if (options.origin != null) {
1411
- this.origin = options.origin;
684
+ case "passed": {
685
+ statusColor = "32";
686
+ statusText = "pass";
687
+ break;
1412
688
  }
1413
- if (options.related != null) {
1414
- this.related = options.related;
689
+ case "failed": {
690
+ statusColor = "31";
691
+ statusText = "fail";
692
+ break;
1415
693
  }
1416
- return this;
1417
- }
1418
- static error(text, origin) {
1419
- return new Diagnostic(text, "error", origin);
1420
- }
1421
- static fromDiagnostics(diagnostics, compiler) {
1422
- return diagnostics.map((diagnostic) => {
1423
- const category = "error";
1424
- const code = `ts(${String(diagnostic.code)})`;
1425
- let origin;
1426
- const text = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
1427
- if (Diagnostic.isTsDiagnosticWithLocation(diagnostic)) {
1428
- origin = new DiagnosticOrigin(diagnostic.start, diagnostic.start + diagnostic.length, diagnostic.file);
1429
- }
1430
- return new Diagnostic(text, category, origin).add({ code });
1431
- });
1432
694
  }
1433
- static fromError(text, error) {
1434
- const messageText = Array.isArray(text) ? text : [text];
1435
- if (error instanceof Error && error.stack != null) {
1436
- if (messageText.length > 1) {
1437
- messageText.push("");
1438
- }
1439
- const stackLines = error.stack.split("\n").map((line) => line.trimStart());
1440
- messageText.push(...stackLines);
1441
- }
1442
- return Diagnostic.error(messageText);
695
+ return (jsx(Line, { children: [jsx(Text, { color: statusColor, children: statusText }), " ", jsx(FileNameText, { filePath: testFile.path })] }));
696
+ }
697
+
698
+ function fileViewText(lines, addEmptyFinalLine) {
699
+ return (jsx(Text, { children: [[...lines], addEmptyFinalLine ? jsx(Line, {}) : undefined] }));
700
+ }
701
+
702
+ function formattedText(input) {
703
+ if (typeof input === "string") {
704
+ return jsx(Line, { children: input });
1443
705
  }
1444
- static isTsDiagnosticWithLocation(diagnostic) {
1445
- return diagnostic.file != null && diagnostic.start != null && diagnostic.length != null;
706
+ if (Array.isArray(input)) {
707
+ return jsx(Line, { children: JSON.stringify(input, null, 2) });
1446
708
  }
1447
- static warning(text, origin) {
1448
- return new Diagnostic(text, "warning", origin);
709
+ function sortObject(target) {
710
+ return Object.keys(target)
711
+ .sort()
712
+ .reduce((result, key) => {
713
+ result[key] = target[key];
714
+ return result;
715
+ }, {});
1449
716
  }
717
+ return jsx(Line, { children: JSON.stringify(sortObject(input), null, 2) });
1450
718
  }
1451
719
 
1452
- var DiagnosticCategory;
1453
- (function (DiagnosticCategory) {
1454
- DiagnosticCategory["Error"] = "error";
1455
- DiagnosticCategory["Warning"] = "warning";
1456
- })(DiagnosticCategory || (DiagnosticCategory = {}));
1457
-
1458
- class OptionDefinitionsMap {
1459
- static #definitions = [
1460
- {
1461
- brand: "string",
1462
- description: "The path to a TSTyche configuration file.",
1463
- group: 2,
1464
- name: "config",
1465
- },
1466
- {
1467
- brand: "boolean",
1468
- description: "Stop running tests after the first failed assertion.",
1469
- group: 4 | 2,
1470
- name: "failFast",
1471
- },
1472
- {
1473
- brand: "bareTrue",
1474
- description: "Print the list of command line options with brief descriptions and exit.",
1475
- group: 2,
1476
- name: "help",
1477
- },
1478
- {
1479
- brand: "bareTrue",
1480
- description: "Install specified versions of the 'typescript' package and exit.",
1481
- group: 2,
1482
- name: "install",
1483
- },
1484
- {
1485
- brand: "bareTrue",
1486
- description: "Print the list of the selected test files and exit.",
1487
- group: 2,
1488
- name: "listFiles",
1489
- },
1490
- {
1491
- brand: "string",
1492
- description: "Only run tests with matching name.",
1493
- group: 2,
1494
- name: "only",
1495
- },
1496
- {
1497
- brand: "bareTrue",
1498
- description: "Remove all installed versions of the 'typescript' package and exit.",
1499
- group: 2,
1500
- name: "prune",
1501
- },
1502
- {
1503
- brand: "string",
1504
- description: "The path to a directory containing files of a test project.",
1505
- group: 4,
1506
- name: "rootPath",
1507
- },
1508
- {
1509
- brand: "bareTrue",
1510
- description: "Print the resolved configuration and exit.",
1511
- group: 2,
1512
- name: "showConfig",
1513
- },
1514
- {
1515
- brand: "string",
1516
- description: "Skip tests with matching name.",
1517
- group: 2,
1518
- name: "skip",
1519
- },
1520
- {
1521
- brand: "list",
1522
- description: "The list of TypeScript versions to be tested on.",
1523
- group: 2 | 4,
1524
- items: {
1525
- brand: "string",
1526
- name: "target",
1527
- pattern: "^([45]\\.[0-9](\\.[0-9])?)|beta|current|latest|next|rc$",
1528
- },
1529
- name: "target",
1530
- },
1531
- {
1532
- brand: "list",
1533
- description: "The list of glob patterns matching the test files.",
1534
- group: 4,
1535
- items: {
1536
- brand: "string",
1537
- name: "testFileMatch",
1538
- },
1539
- name: "testFileMatch",
1540
- },
1541
- {
1542
- brand: "bareTrue",
1543
- description: "Fetch the 'typescript' package metadata from the registry and exit.",
1544
- group: 2,
1545
- name: "update",
1546
- },
1547
- {
1548
- brand: "bareTrue",
1549
- description: "Print the version number and exit.",
1550
- group: 2,
1551
- name: "version",
1552
- },
1553
- {
1554
- brand: "bareTrue",
1555
- description: "Watch for changes and rerun related test files.",
1556
- group: 2,
1557
- name: "watch",
1558
- },
1559
- ];
1560
- static for(optionGroup) {
1561
- const definitionMap = new Map();
1562
- for (const definition of OptionDefinitionsMap.#definitions) {
1563
- if (definition.group & optionGroup) {
1564
- definitionMap.set(definition.name, definition);
1565
- }
720
+ function HintText({ children }) {
721
+ return (jsx(Text, { indent: 1, color: "90", children: children }));
722
+ }
723
+ function HelpHeaderText({ tstycheVersion }) {
724
+ return (jsx(Line, { children: ["The TSTyche Type Test Runner", jsx(HintText, { children: tstycheVersion })] }));
725
+ }
726
+ function CommandText({ hint, text }) {
727
+ let hintText;
728
+ if (hint != null) {
729
+ hintText = jsx(HintText, { children: hint });
730
+ }
731
+ return (jsx(Line, { indent: 1, children: [jsx(Text, { color: "34", children: text }), hintText] }));
732
+ }
733
+ function OptionDescriptionText({ text }) {
734
+ return jsx(Line, { indent: 1, children: text });
735
+ }
736
+ function CommandLineUsageText() {
737
+ const usage = [
738
+ ["tstyche", "Run all tests."],
739
+ ["tstyche path/to/first.test.ts", "Only run the test files with matching path."],
740
+ ["tstyche --target 4.9,5.3.2,current", "Test on all specified versions of TypeScript."],
741
+ ];
742
+ const usageText = usage.map(([commandText, descriptionText]) => (jsx(Line, { children: [jsx(CommandText, { text: commandText }), jsx(OptionDescriptionText, { text: descriptionText })] })));
743
+ return jsx(Text, { children: usageText });
744
+ }
745
+ function CommandLineOptionNameText({ text }) {
746
+ return jsx(Text, { children: ["--", text] });
747
+ }
748
+ function CommandLineOptionHintText({ definition }) {
749
+ if (definition.brand === "list") {
750
+ return (jsx(Text, { children: [definition.brand, " of ", definition.items.brand, "s"] }));
751
+ }
752
+ return jsx(Text, { children: definition.brand });
753
+ }
754
+ function CommandLineOptionsText({ optionDefinitions }) {
755
+ const definitions = [...optionDefinitions.values()];
756
+ const optionsText = definitions.map((definition) => {
757
+ let hint;
758
+ if (definition.brand !== "bareTrue") {
759
+ hint = jsx(CommandLineOptionHintText, { definition: definition });
1566
760
  }
1567
- return definitionMap;
761
+ return (jsx(Text, { children: [jsx(CommandText, { text: jsx(CommandLineOptionNameText, { text: definition.name }), hint: hint }), jsx(OptionDescriptionText, { text: definition.description }), jsx(Line, {})] }));
762
+ });
763
+ return (jsx(Text, { children: [jsx(Line, { children: "Command Line Options" }), jsx(Line, {}), optionsText] }));
764
+ }
765
+ function HelpFooterText() {
766
+ return jsx(Line, { children: "To learn more, visit https://tstyche.org" });
767
+ }
768
+ function helpText(optionDefinitions, tstycheVersion) {
769
+ return (jsx(Text, { children: [jsx(HelpHeaderText, { tstycheVersion: tstycheVersion }), jsx(Line, {}), jsx(CommandLineUsageText, {}), jsx(Line, {}), jsx(CommandLineOptionsText, { optionDefinitions: optionDefinitions }), jsx(Line, {}), jsx(HelpFooterText, {}), jsx(Line, {})] }));
770
+ }
771
+
772
+ class OutputService {
773
+ #isClear = false;
774
+ #noColor;
775
+ #scribbler;
776
+ #stderr;
777
+ #stdout;
778
+ constructor(options) {
779
+ this.#noColor = options?.noColor ?? Environment.noColor;
780
+ this.#stderr = options?.stderr ?? process.stderr;
781
+ this.#stdout = options?.stdout ?? process.stdout;
782
+ this.#scribbler = new Scribbler({ noColor: this.#noColor });
783
+ }
784
+ clearTerminal() {
785
+ if (!this.#isClear) {
786
+ this.#stdout.write("\u001B[2J\u001B[3J\u001B[H");
787
+ this.#isClear = true;
788
+ }
789
+ }
790
+ eraseLastLine() {
791
+ this.#stdout.write("\u001B[1A\u001B[0K");
792
+ }
793
+ #write(stream, body) {
794
+ const elements = Array.isArray(body) ? body : [body];
795
+ for (const element of elements) {
796
+ stream.write(this.#scribbler.render(element));
797
+ }
798
+ this.#isClear = false;
799
+ }
800
+ writeError(body) {
801
+ this.#write(this.#stderr, body);
802
+ }
803
+ writeMessage(body) {
804
+ this.#write(this.#stdout, body);
805
+ }
806
+ writeWarning(body) {
807
+ this.#write(this.#stderr, body);
1568
808
  }
1569
809
  }
1570
810
 
1571
- class OptionDiagnosticText {
1572
- static doubleQuotesExpected() {
1573
- return "String literal with double quotes expected.";
811
+ function RowText({ label, text }) {
812
+ return (jsx(Line, { children: [`${label}:`.padEnd(12), text] }));
813
+ }
814
+ function CountText({ failed, passed, skipped, todo, total }) {
815
+ return (jsx(Text, { children: [failed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "31", children: [String(failed), " failed"] }), jsx(Text, { children: ", " })] })) : undefined, skipped > 0 ? (jsx(Text, { children: [jsx(Text, { color: "33", children: [String(skipped), " skipped"] }), jsx(Text, { children: ", " })] })) : undefined, todo > 0 ? (jsx(Text, { children: [jsx(Text, { color: "35", children: [String(todo), " todo"] }), jsx(Text, { children: ", " })] })) : undefined, passed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "32", children: [String(passed), " passed"] }), jsx(Text, { children: ", " })] })) : undefined, jsx(Text, { children: [String(total), jsx(Text, { children: " total" })] })] }));
816
+ }
817
+ function DurationText({ duration }) {
818
+ const minutes = Math.floor(duration / 60);
819
+ const seconds = duration % 60;
820
+ return (jsx(Text, { children: [minutes > 0 ? `${String(minutes)}m ` : undefined, `${String(Math.round(seconds * 10) / 10)}s`] }));
821
+ }
822
+ function MatchText({ text }) {
823
+ if (typeof text === "string") {
824
+ return jsx(Text, { children: ["'", text, "'"] });
1574
825
  }
1575
- static expectsListItemType(optionName, optionBrand) {
1576
- return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
826
+ if (text.length <= 1) {
827
+ return jsx(Text, { children: ["'", ...text, "'"] });
1577
828
  }
1578
- static expectsValue(optionName, optionGroup) {
1579
- optionName = OptionDiagnosticText.#optionName(optionName, optionGroup);
1580
- return `Option '${optionName}' expects a value.`;
829
+ const lastItem = text.pop();
830
+ return (jsx(Text, { children: [text.map((match, index, list) => (jsx(Text, { children: ["'", match, "'", index === list.length - 1 ? jsx(Text, { children: " " }) : jsx(Text, { color: "90", children: ", " })] }))), jsx(Text, { color: "90", children: "or" }), " '", lastItem, "'"] }));
831
+ }
832
+ function RanFilesText({ onlyMatch, pathMatch, skipMatch }) {
833
+ const testNameMatchText = [];
834
+ if (onlyMatch != null) {
835
+ testNameMatchText.push(jsx(Text, { children: [jsx(Text, { color: "90", children: "matching " }), jsx(MatchText, { text: onlyMatch })] }));
1581
836
  }
1582
- static fileDoesNotExist(filePath) {
1583
- return `The specified path '${filePath}' does not exist.`;
837
+ if (skipMatch != null) {
838
+ testNameMatchText.push(jsx(Text, { children: [onlyMatch == null ? undefined : jsx(Text, { color: "90", children: " and " }), jsx(Text, { color: "90", children: "not matching " }), jsx(MatchText, { text: skipMatch })] }));
1584
839
  }
1585
- static #pathSelectOptions(resolvedConfig) {
1586
- const text = [
1587
- `Root path: ${resolvedConfig.rootPath}`,
1588
- `Test file match: ${resolvedConfig.testFileMatch.join(", ")}`,
1589
- ];
1590
- if (resolvedConfig.pathMatch.length > 0) {
1591
- text.push(`Path match: ${resolvedConfig.pathMatch.join(", ")}`);
1592
- }
1593
- return text;
840
+ let pathMatchText;
841
+ if (pathMatch.length > 0) {
842
+ pathMatchText = (jsx(Text, { children: [jsx(Text, { color: "90", children: "test files matching " }), jsx(MatchText, { text: pathMatch }), jsx(Text, { color: "90", children: "." })] }));
1594
843
  }
1595
- static noTestFilesWereLeft(resolvedConfig) {
1596
- return [
1597
- "No test files were left to run using current configuration.",
1598
- ...OptionDiagnosticText.#pathSelectOptions(resolvedConfig),
1599
- ];
844
+ else {
845
+ pathMatchText = jsx(Text, { color: "90", children: "all test files." });
1600
846
  }
1601
- static noTestFilesWereSelected(resolvedConfig) {
1602
- return [
1603
- "No test files were selected using current configuration.",
1604
- ...OptionDiagnosticText.#pathSelectOptions(resolvedConfig),
1605
- ];
847
+ return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Ran " }), testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: "tests " }) : undefined, testNameMatchText, testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: " in " }) : undefined, pathMatchText] }));
848
+ }
849
+ function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) {
850
+ const targetCountText = (jsx(RowText, { label: "Targets", text: jsx(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) }));
851
+ const fileCountText = (jsx(RowText, { label: "Test files", text: jsx(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) }));
852
+ const testCountText = (jsx(RowText, { label: "Tests", text: jsx(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) }));
853
+ const assertionCountText = (jsx(RowText, { label: "Assertions", text: jsx(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) }));
854
+ return (jsx(Text, { children: [targetCountText, fileCountText, testCount.total > 0 ? testCountText : undefined, expectCount.total > 0 ? assertionCountText : undefined, jsx(RowText, { label: "Duration", text: jsx(DurationText, { duration: duration / 1000 }) }), jsx(Line, {}), jsx(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch })] }));
855
+ }
856
+
857
+ function StatusText({ status }) {
858
+ switch (status) {
859
+ case "fail":
860
+ return jsx(Text, { color: "31", children: "\u00D7" });
861
+ case "pass":
862
+ return jsx(Text, { color: "32", children: "+" });
863
+ case "skip":
864
+ return jsx(Text, { color: "33", children: "- skip" });
865
+ case "todo":
866
+ return jsx(Text, { color: "35", children: "- todo" });
1606
867
  }
1607
- static #optionName(optionName, optionGroup) {
1608
- switch (optionGroup) {
1609
- case 2:
1610
- return `--${optionName}`;
1611
- case 4:
1612
- return optionName;
1613
- }
868
+ }
869
+ function testNameText(status, name, indent = 0) {
870
+ return (jsx(Line, { indent: indent + 1, children: [jsx(StatusText, { status: status }), " ", jsx(Text, { color: "90", children: name })] }));
871
+ }
872
+
873
+ function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) {
874
+ let projectPathText;
875
+ if (tsconfigFilePath != null) {
876
+ projectPathText = (jsx(Text, { color: "90", children: [" with ", Path.relative("", tsconfigFilePath)] }));
1614
877
  }
1615
- static testFileMatchCannotStartWith(segment) {
1616
- return [
1617
- `A test file match pattern cannot start with '${segment}'.`,
1618
- "The test files are only collected within the 'rootPath' directory.",
1619
- ];
878
+ return (jsx(Text, { children: [options?.prependEmptyLine === true ? jsx(Line, {}) : undefined, jsx(Line, { children: [jsx(Text, { color: "34", children: "uses" }), " TypeScript ", compilerVersion, projectPathText] }), jsx(Line, {})] }));
879
+ }
880
+
881
+ function waitingForFileChangesText() {
882
+ return jsx(Line, { children: "Waiting for file changes." });
883
+ }
884
+
885
+ function watchUsageText() {
886
+ const usage = [
887
+ ["a", "to run all tests."],
888
+ ["x", "to exit."],
889
+ ];
890
+ const usageText = usage.map(([keyText, actionText]) => {
891
+ return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Press" }), jsx(Text, { children: ` ${keyText} ` }), jsx(Text, { color: "90", children: actionText })] }));
892
+ });
893
+ return jsx(Text, { children: usageText });
894
+ }
895
+
896
+ class FileViewService {
897
+ #indent = 0;
898
+ #lines = [];
899
+ #messages = [];
900
+ get hasErrors() {
901
+ return this.#messages.length > 0;
1620
902
  }
1621
- static requiresValueType(optionName, optionBrand, optionGroup) {
1622
- optionName = OptionDiagnosticText.#optionName(optionName, optionGroup);
1623
- return `Option '${optionName}' requires a value of type ${optionBrand}.`;
903
+ addMessage(message) {
904
+ this.#messages.push(message);
1624
905
  }
1625
- static unknownOption(optionName) {
1626
- return `Unknown option '${optionName}'.`;
906
+ addTest(status, name) {
907
+ this.#lines.push(testNameText(status, name, this.#indent));
1627
908
  }
1628
- static versionIsNotSupported(value) {
1629
- if (value === "current") {
1630
- return "Cannot use 'current' as a target. Failed to resolve the path to the currently installed TypeScript module.";
1631
- }
1632
- return `TypeScript version '${value}' is not supported.`;
909
+ beginDescribe(name) {
910
+ this.#lines.push(describeNameText(name, this.#indent));
911
+ this.#indent++;
1633
912
  }
1634
- static watchCannotBeEnabledInCiEnvironment() {
1635
- return "The watch mode cannot be enabled in a continuous integration environment.";
913
+ clear() {
914
+ this.#indent = 0;
915
+ this.#lines = [];
916
+ this.#messages = [];
917
+ }
918
+ endDescribe() {
919
+ this.#indent--;
920
+ }
921
+ getMessages() {
922
+ return this.#messages;
1636
923
  }
1637
- static watchIsNotAvailable() {
1638
- return "The watch mode is not available on this system.";
924
+ getViewText(options) {
925
+ return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
1639
926
  }
1640
927
  }
1641
928
 
1642
- class OptionUsageText {
1643
- #optionGroup;
1644
- #storeService;
1645
- constructor(optionGroup, storeService) {
1646
- this.#optionGroup = optionGroup;
1647
- this.#storeService = storeService;
929
+ class Reporter {
930
+ outputService;
931
+ constructor(outputService) {
932
+ this.outputService = outputService;
1648
933
  }
1649
- async get(optionName, optionBrand) {
1650
- const usageText = [];
1651
- switch (optionName) {
1652
- case "target": {
1653
- const supportedTags = await this.#storeService.getSupportedTags();
1654
- const supportedTagsText = `Supported tags: ${["'", supportedTags.join("', '"), "'"].join("")}.`;
1655
- switch (this.#optionGroup) {
1656
- case 2: {
1657
- usageText.push("Value for the '--target' option must be a single tag or a comma separated list.", "Usage examples: '--target 4.9', '--target latest', '--target 4.9,5.3.2,current'.", supportedTagsText);
1658
- break;
1659
- }
1660
- case 4: {
1661
- usageText.push("Item of the 'target' list must be a supported version tag.", supportedTagsText);
1662
- break;
934
+ }
935
+
936
+ class RunReporter extends Reporter {
937
+ #currentCompilerVersion;
938
+ #currentProjectConfigFilePath;
939
+ #fileCount = 0;
940
+ #fileView = new FileViewService();
941
+ #hasReportedAdds = false;
942
+ #hasReportedError = false;
943
+ #isFileViewExpanded = false;
944
+ #resolvedConfig;
945
+ #seenDeprecations = new Set();
946
+ constructor(resolvedConfig, outputService) {
947
+ super(outputService);
948
+ this.#resolvedConfig = resolvedConfig;
949
+ }
950
+ get #isLastFile() {
951
+ return this.#fileCount === 0;
952
+ }
953
+ handleEvent([eventName, payload]) {
954
+ switch (eventName) {
955
+ case "deprecation:info": {
956
+ for (const diagnostic of payload.diagnostics) {
957
+ if (!this.#seenDeprecations.has(diagnostic.text.toString())) {
958
+ this.#fileView.addMessage(diagnosticText(diagnostic));
959
+ this.#seenDeprecations.add(diagnostic.text.toString());
1663
960
  }
1664
961
  }
1665
962
  break;
1666
963
  }
1667
- default:
1668
- usageText.push(OptionDiagnosticText.requiresValueType(optionName, optionBrand, this.#optionGroup));
1669
- }
1670
- return usageText;
1671
- }
1672
- }
1673
-
1674
- class OptionValidator {
1675
- #onDiagnostic;
1676
- #optionGroup;
1677
- #optionUsageText;
1678
- #storeService;
1679
- constructor(optionGroup, storeService, onDiagnostic) {
1680
- this.#optionGroup = optionGroup;
1681
- this.#storeService = storeService;
1682
- this.#onDiagnostic = onDiagnostic;
1683
- this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
1684
- }
1685
- async check(optionName, optionValue, optionBrand, origin) {
1686
- switch (optionName) {
1687
- case "config":
1688
- case "rootPath": {
1689
- if (!existsSync(optionValue)) {
1690
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.fileDoesNotExist(optionValue), origin));
964
+ case "run:start": {
965
+ this.#isFileViewExpanded = payload.result.testFiles.length === 1 && this.#resolvedConfig.watch !== true;
966
+ break;
967
+ }
968
+ case "store:info": {
969
+ this.outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
970
+ this.#hasReportedAdds = true;
971
+ break;
972
+ }
973
+ case "store:error": {
974
+ for (const diagnostic of payload.diagnostics) {
975
+ this.outputService.writeError(diagnosticText(diagnostic));
976
+ }
977
+ break;
978
+ }
979
+ case "target:start": {
980
+ this.#fileCount = payload.result.testFiles.length;
981
+ break;
982
+ }
983
+ case "target:end": {
984
+ this.#currentCompilerVersion = undefined;
985
+ this.#currentProjectConfigFilePath = undefined;
986
+ break;
987
+ }
988
+ case "project:info": {
989
+ if (this.#currentCompilerVersion !== payload.compilerVersion ||
990
+ this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) {
991
+ this.outputService.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
992
+ prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds && !this.#hasReportedError,
993
+ }));
994
+ this.#hasReportedAdds = false;
995
+ this.#currentCompilerVersion = payload.compilerVersion;
996
+ this.#currentProjectConfigFilePath = payload.projectConfigFilePath;
1691
997
  }
1692
998
  break;
1693
999
  }
1694
- case "target": {
1695
- if ((await this.#storeService.validateTag(optionValue)) === false) {
1696
- this.#onDiagnostic(Diagnostic.error([
1697
- OptionDiagnosticText.versionIsNotSupported(optionValue),
1698
- ...(await this.#optionUsageText.get(optionName, optionBrand)),
1699
- ], origin));
1000
+ case "project:error": {
1001
+ for (const diagnostic of payload.diagnostics) {
1002
+ this.outputService.writeError(diagnosticText(diagnostic));
1700
1003
  }
1701
1004
  break;
1702
1005
  }
1703
- case "testFileMatch": {
1704
- for (const segment of ["/", "../"]) {
1705
- if (optionValue.startsWith(segment)) {
1706
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.testFileMatchCannotStartWith(segment), origin));
1707
- }
1006
+ case "file:start": {
1007
+ if (!Environment.noInteractive) {
1008
+ this.outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
1708
1009
  }
1010
+ this.#fileCount--;
1011
+ this.#hasReportedError = false;
1709
1012
  break;
1710
1013
  }
1711
- case "watch": {
1712
- if (Environment.isCi) {
1713
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.watchCannotBeEnabledInCiEnvironment(), origin));
1014
+ case "file:error": {
1015
+ for (const diagnostic of payload.diagnostics) {
1016
+ this.#fileView.addMessage(diagnosticText(diagnostic));
1714
1017
  }
1715
1018
  break;
1716
1019
  }
1717
- }
1718
- }
1719
- }
1720
-
1721
- class CommandLineOptionsWorker {
1722
- #commandLineOptionDefinitions;
1723
- #commandLineOptions;
1724
- #onDiagnostic;
1725
- #optionGroup = 2;
1726
- #optionUsageText;
1727
- #optionValidator;
1728
- #pathMatch;
1729
- #storeService;
1730
- constructor(commandLineOptions, pathMatch, storeService, onDiagnostic) {
1731
- this.#commandLineOptions = commandLineOptions;
1732
- this.#pathMatch = pathMatch;
1733
- this.#storeService = storeService;
1734
- this.#onDiagnostic = onDiagnostic;
1735
- this.#commandLineOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
1736
- this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
1737
- this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
1738
- }
1739
- async #onExpectsValue(optionDefinition) {
1740
- const text = [
1741
- OptionDiagnosticText.expectsValue(optionDefinition.name, this.#optionGroup),
1742
- ...(await this.#optionUsageText.get(optionDefinition.name, optionDefinition.brand)),
1743
- ];
1744
- this.#onDiagnostic(Diagnostic.error(text));
1745
- }
1746
- async parse(commandLineArgs) {
1747
- let index = 0;
1748
- let arg = commandLineArgs[index];
1749
- while (arg != null) {
1750
- index++;
1751
- if (arg.startsWith("--")) {
1752
- const optionName = arg.slice(2);
1753
- const optionDefinition = this.#commandLineOptionDefinitions.get(optionName);
1754
- if (optionDefinition) {
1755
- index = await this.#parseOptionValue(commandLineArgs, index, optionDefinition);
1020
+ case "file:end": {
1021
+ if (!Environment.noInteractive) {
1022
+ this.outputService.eraseLastLine();
1756
1023
  }
1757
- else {
1758
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.unknownOption(arg)));
1024
+ this.outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
1025
+ this.outputService.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
1026
+ if (this.#fileView.hasErrors) {
1027
+ this.outputService.writeError(this.#fileView.getMessages());
1028
+ this.#hasReportedError = true;
1759
1029
  }
1030
+ this.#fileView.clear();
1031
+ this.#seenDeprecations.clear();
1032
+ break;
1760
1033
  }
1761
- else if (arg.startsWith("-")) {
1762
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.unknownOption(arg)));
1034
+ case "describe:start": {
1035
+ if (this.#isFileViewExpanded) {
1036
+ this.#fileView.beginDescribe(payload.result.describe.name);
1037
+ }
1038
+ break;
1763
1039
  }
1764
- else {
1765
- this.#pathMatch.push(Path.normalizeSlashes(arg));
1040
+ case "describe:end": {
1041
+ if (this.#isFileViewExpanded) {
1042
+ this.#fileView.endDescribe();
1043
+ }
1044
+ break;
1766
1045
  }
1767
- arg = commandLineArgs[index];
1768
- }
1769
- }
1770
- async #parseOptionValue(commandLineArgs, index, optionDefinition) {
1771
- let optionValue = this.#resolveOptionValue(commandLineArgs[index]);
1772
- switch (optionDefinition.brand) {
1773
- case "bareTrue": {
1774
- await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
1775
- this.#commandLineOptions[optionDefinition.name] = true;
1046
+ case "test:skip": {
1047
+ if (this.#isFileViewExpanded) {
1048
+ this.#fileView.addTest("skip", payload.result.test.name);
1049
+ }
1776
1050
  break;
1777
1051
  }
1778
- case "boolean": {
1779
- await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
1780
- this.#commandLineOptions[optionDefinition.name] = optionValue !== "false";
1781
- if (optionValue === "false" || optionValue === "true") {
1782
- index++;
1052
+ case "test:todo": {
1053
+ if (this.#isFileViewExpanded) {
1054
+ this.#fileView.addTest("todo", payload.result.test.name);
1783
1055
  }
1784
1056
  break;
1785
1057
  }
1786
- case "list": {
1787
- if (optionValue !== "") {
1788
- const optionValues = optionValue
1789
- .split(",")
1790
- .map((value) => value.trim())
1791
- .filter((value) => value !== "");
1792
- for (const optionValue of optionValues) {
1793
- await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
1794
- }
1795
- this.#commandLineOptions[optionDefinition.name] = optionValues;
1796
- index++;
1797
- break;
1058
+ case "test:error": {
1059
+ if (this.#isFileViewExpanded) {
1060
+ this.#fileView.addTest("fail", payload.result.test.name);
1061
+ }
1062
+ for (const diagnostic of payload.diagnostics) {
1063
+ this.#fileView.addMessage(diagnosticText(diagnostic));
1798
1064
  }
1799
- await this.#onExpectsValue(optionDefinition);
1800
1065
  break;
1801
1066
  }
1802
- case "string": {
1803
- if (optionValue !== "") {
1804
- if (optionDefinition.name === "config") {
1805
- optionValue = Path.resolve(optionValue);
1806
- }
1807
- await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
1808
- this.#commandLineOptions[optionDefinition.name] = optionValue;
1809
- index++;
1810
- break;
1067
+ case "test:fail": {
1068
+ if (this.#isFileViewExpanded) {
1069
+ this.#fileView.addTest("fail", payload.result.test.name);
1070
+ }
1071
+ break;
1072
+ }
1073
+ case "test:pass": {
1074
+ if (this.#isFileViewExpanded) {
1075
+ this.#fileView.addTest("pass", payload.result.test.name);
1076
+ }
1077
+ break;
1078
+ }
1079
+ case "expect:error":
1080
+ case "expect:fail": {
1081
+ for (const diagnostic of payload.diagnostics) {
1082
+ this.#fileView.addMessage(diagnosticText(diagnostic));
1811
1083
  }
1812
- await this.#onExpectsValue(optionDefinition);
1813
1084
  break;
1814
1085
  }
1815
1086
  }
1816
- return index;
1817
- }
1818
- #resolveOptionValue(target = "") {
1819
- return target.startsWith("-") ? "" : target;
1820
1087
  }
1821
1088
  }
1822
1089
 
1823
- class ConfigFileOptionsWorker {
1824
- #compiler;
1825
- #configFileOptionDefinitions;
1826
- #configFileOptions;
1827
- #configFilePath;
1828
- #onDiagnostic;
1829
- #optionGroup = 4;
1830
- #optionValidator;
1831
- #storeService;
1832
- constructor(compiler, configFileOptions, configFilePath, storeService, onDiagnostic) {
1833
- this.#compiler = compiler;
1834
- this.#configFileOptions = configFileOptions;
1835
- this.#configFilePath = configFilePath;
1836
- this.#storeService = storeService;
1837
- this.#onDiagnostic = onDiagnostic;
1838
- this.#configFileOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
1839
- this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
1840
- }
1841
- #isDoubleQuotedString(node, sourceFile) {
1842
- return (node.kind === this.#compiler.SyntaxKind.StringLiteral &&
1843
- sourceFile.text.slice(this.#skipTrivia(node.pos, sourceFile), node.end).startsWith('"'));
1844
- }
1845
- async parse(sourceText) {
1846
- const sourceFile = this.#compiler.parseJsonText(this.#configFilePath, sourceText);
1847
- if (sourceFile.parseDiagnostics.length > 0) {
1848
- for (const diagnostic of Diagnostic.fromDiagnostics(sourceFile.parseDiagnostics, this.#compiler)) {
1849
- this.#onDiagnostic(diagnostic);
1850
- }
1851
- return;
1852
- }
1853
- const rootExpression = sourceFile.statements[0]?.expression;
1854
- if (rootExpression == null || !this.#compiler.isObjectLiteralExpression(rootExpression)) {
1855
- const origin = new DiagnosticOrigin(0, 0, sourceFile);
1856
- this.#onDiagnostic(Diagnostic.error("The root value of a configuration file must be an object literal.", origin));
1090
+ class SetupReporter extends Reporter {
1091
+ handleEvent([eventName, payload]) {
1092
+ if (eventName === "store:info") {
1093
+ this.outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
1857
1094
  return;
1858
1095
  }
1859
- for (const property of rootExpression.properties) {
1860
- if (this.#compiler.isPropertyAssignment(property)) {
1861
- if (!this.#isDoubleQuotedString(property.name, sourceFile)) {
1862
- const origin = DiagnosticOrigin.fromJsonNode(property, sourceFile, this.#skipTrivia);
1863
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.doubleQuotesExpected(), origin));
1864
- continue;
1865
- }
1866
- const optionName = this.#resolvePropertyName(property);
1867
- if (optionName === "$schema") {
1868
- continue;
1869
- }
1870
- const optionDefinition = this.#configFileOptionDefinitions.get(optionName);
1871
- if (optionDefinition) {
1872
- this.#configFileOptions[optionDefinition.name] = await this.#parseOptionValue(sourceFile, property.initializer, optionDefinition);
1873
- }
1874
- else {
1875
- const origin = DiagnosticOrigin.fromJsonNode(property, sourceFile, this.#skipTrivia);
1876
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.unknownOption(optionName), origin));
1096
+ if ("diagnostics" in payload) {
1097
+ for (const diagnostic of payload.diagnostics) {
1098
+ switch (diagnostic.category) {
1099
+ case "error": {
1100
+ this.outputService.writeError(diagnosticText(diagnostic));
1101
+ break;
1102
+ }
1103
+ case "warning": {
1104
+ this.outputService.writeWarning(diagnosticText(diagnostic));
1105
+ break;
1106
+ }
1877
1107
  }
1878
1108
  }
1879
1109
  }
1880
- return;
1881
1110
  }
1882
- async #parseOptionValue(sourceFile, valueExpression, optionDefinition, isListItem = false) {
1883
- switch (valueExpression.kind) {
1884
- case this.#compiler.SyntaxKind.TrueKeyword: {
1885
- if (optionDefinition.brand === "boolean") {
1886
- return true;
1887
- }
1888
- break;
1889
- }
1890
- case this.#compiler.SyntaxKind.FalseKeyword: {
1891
- if (optionDefinition.brand === "boolean") {
1892
- return false;
1893
- }
1111
+ }
1112
+
1113
+ class SummaryReporter extends Reporter {
1114
+ handleEvent([eventName, payload]) {
1115
+ switch (eventName) {
1116
+ case "run:end": {
1117
+ this.outputService.writeMessage(summaryText({
1118
+ duration: payload.result.timing.duration,
1119
+ expectCount: payload.result.expectCount,
1120
+ fileCount: payload.result.fileCount,
1121
+ onlyMatch: payload.result.resolvedConfig.only,
1122
+ pathMatch: payload.result.resolvedConfig.pathMatch,
1123
+ skipMatch: payload.result.resolvedConfig.skip,
1124
+ targetCount: payload.result.targetCount,
1125
+ testCount: payload.result.testCount,
1126
+ }));
1894
1127
  break;
1895
1128
  }
1896
- case this.#compiler.SyntaxKind.StringLiteral: {
1897
- if (!this.#isDoubleQuotedString(valueExpression, sourceFile)) {
1898
- const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
1899
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.doubleQuotesExpected(), origin));
1900
- return;
1901
- }
1902
- if (optionDefinition.brand === "string") {
1903
- let value = valueExpression.text;
1904
- if (optionDefinition.name === "rootPath") {
1905
- value = Path.resolve(Path.dirname(this.#configFilePath), value);
1906
- }
1907
- const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
1908
- await this.#optionValidator.check(optionDefinition.name, value, optionDefinition.brand, origin);
1909
- return value;
1910
- }
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ class WatchReporter extends Reporter {
1134
+ handleEvent([eventName, payload]) {
1135
+ switch (eventName) {
1136
+ case "run:start": {
1137
+ this.outputService.clearTerminal();
1911
1138
  break;
1912
1139
  }
1913
- case this.#compiler.SyntaxKind.ArrayLiteralExpression: {
1914
- if (optionDefinition.brand === "list") {
1915
- const value = [];
1916
- for (const element of valueExpression.elements) {
1917
- value.push(await this.#parseOptionValue(sourceFile, element, optionDefinition.items, true));
1918
- }
1919
- return value;
1140
+ case "run:end": {
1141
+ this.outputService.writeMessage(watchUsageText());
1142
+ break;
1143
+ }
1144
+ case "watch:error": {
1145
+ this.outputService.clearTerminal();
1146
+ for (const diagnostic of payload.diagnostics) {
1147
+ this.outputService.writeError(diagnosticText(diagnostic));
1920
1148
  }
1149
+ this.outputService.writeMessage(waitingForFileChangesText());
1921
1150
  break;
1922
1151
  }
1923
1152
  }
1924
- const text = isListItem
1925
- ? OptionDiagnosticText.expectsListItemType(optionDefinition.name, optionDefinition.brand)
1926
- : OptionDiagnosticText.requiresValueType(optionDefinition.name, optionDefinition.brand, this.#optionGroup);
1927
- const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
1928
- this.#onDiagnostic(Diagnostic.error(text, origin));
1929
- return;
1930
1153
  }
1931
- #resolvePropertyName({ name }) {
1932
- if ("text" in name) {
1933
- return name.text;
1934
- }
1935
- return "";
1154
+ }
1155
+
1156
+ class CancellationToken {
1157
+ #isCancelled = false;
1158
+ #handlers = new Set();
1159
+ #reason;
1160
+ get isCancellationRequested() {
1161
+ return this.#isCancelled;
1936
1162
  }
1937
- #skipTrivia(position, sourceFile) {
1938
- const { text } = sourceFile.getSourceFile();
1939
- while (position < text.length) {
1940
- if (/\s/.test(text.charAt(position))) {
1941
- position++;
1942
- continue;
1163
+ get reason() {
1164
+ return this.#reason;
1165
+ }
1166
+ cancel(reason) {
1167
+ if (!this.#isCancelled) {
1168
+ for (const handler of this.#handlers) {
1169
+ handler(reason);
1943
1170
  }
1944
- if (text.charAt(position) === "/") {
1945
- if (text.charAt(position + 1) === "/") {
1946
- position += 2;
1947
- while (position < text.length) {
1948
- if (text.charAt(position) === "\n") {
1949
- break;
1950
- }
1951
- position++;
1171
+ this.#isCancelled = true;
1172
+ this.#reason = reason;
1173
+ }
1174
+ }
1175
+ onCancellationRequested(handler) {
1176
+ this.#handlers.add(handler);
1177
+ }
1178
+ reset() {
1179
+ if (this.#isCancelled) {
1180
+ this.#isCancelled = false;
1181
+ this.#reason = undefined;
1182
+ }
1183
+ }
1184
+ }
1185
+
1186
+ var CancellationReason;
1187
+ (function (CancellationReason) {
1188
+ CancellationReason["ConfigChange"] = "configChange";
1189
+ CancellationReason["ConfigError"] = "configError";
1190
+ CancellationReason["FailFast"] = "failFast";
1191
+ })(CancellationReason || (CancellationReason = {}));
1192
+
1193
+ class Watcher {
1194
+ #abortController = new AbortController();
1195
+ #onChanged;
1196
+ #onRemoved;
1197
+ #recursive;
1198
+ #targetPath;
1199
+ #watcher;
1200
+ constructor(targetPath, onChanged, onRemoved, options) {
1201
+ this.#targetPath = targetPath;
1202
+ this.#onChanged = onChanged;
1203
+ this.#onRemoved = onRemoved ?? onChanged;
1204
+ this.#recursive = options?.recursive;
1205
+ }
1206
+ close() {
1207
+ this.#abortController.abort();
1208
+ }
1209
+ async watch() {
1210
+ this.#watcher = fs.watch(this.#targetPath, { recursive: this.#recursive, signal: this.#abortController.signal });
1211
+ try {
1212
+ for await (const event of this.#watcher) {
1213
+ if (event.filename != null) {
1214
+ const filePath = Path.resolve(this.#targetPath, event.filename);
1215
+ if (existsSync(filePath)) {
1216
+ await this.#onChanged(filePath);
1952
1217
  }
1953
- continue;
1954
- }
1955
- if (text.charAt(position + 1) === "*") {
1956
- position += 2;
1957
- while (position < text.length) {
1958
- if (text.charAt(position) === "*" && text.charAt(position + 1) === "/") {
1959
- position += 2;
1960
- break;
1961
- }
1962
- position++;
1218
+ else {
1219
+ await this.#onRemoved(filePath);
1963
1220
  }
1964
- continue;
1965
1221
  }
1966
- position++;
1967
- continue;
1968
1222
  }
1969
- break;
1970
1223
  }
1971
- return position;
1224
+ catch (error) {
1225
+ if (error instanceof Error && error.name === "AbortError") ;
1226
+ }
1972
1227
  }
1973
1228
  }
1974
1229
 
1975
- const defaultOptions = {
1976
- failFast: false,
1977
- rootPath: "./",
1978
- target: [Environment.typescriptPath == null ? "latest" : "current"],
1979
- testFileMatch: ["**/*.tst.*", "**/__typetests__/*.test.*", "**/typetests/*.test.*"],
1980
- };
1981
- class ConfigService {
1982
- #commandLineOptions = {};
1983
- #compiler;
1984
- #configFileOptions = {};
1985
- #configFilePath = Path.resolve(defaultOptions.rootPath, "./tstyche.config.json");
1986
- #pathMatch = [];
1987
- #storeService;
1988
- constructor(compiler, storeService) {
1989
- this.#compiler = compiler;
1990
- this.#storeService = storeService;
1230
+ class FileWatcher extends Watcher {
1231
+ constructor(targetPath, onChanged) {
1232
+ const onChangedFile = async (filePath) => {
1233
+ if (filePath === targetPath) {
1234
+ await onChanged();
1235
+ }
1236
+ };
1237
+ super(Path.dirname(targetPath), onChangedFile);
1991
1238
  }
1992
- #onDiagnostic(diagnostic) {
1993
- EventEmitter.dispatch(["config:error", { diagnostics: [diagnostic] }]);
1239
+ }
1240
+
1241
+ class DiagnosticOrigin {
1242
+ breadcrumbs;
1243
+ end;
1244
+ sourceFile;
1245
+ start;
1246
+ constructor(start, end, sourceFile, breadcrumbs) {
1247
+ this.start = start;
1248
+ this.end = end;
1249
+ this.sourceFile = sourceFile;
1250
+ this.breadcrumbs = breadcrumbs;
1994
1251
  }
1995
- async parseCommandLine(commandLineArgs) {
1996
- this.#commandLineOptions = {};
1997
- this.#pathMatch = [];
1998
- const commandLineWorker = new CommandLineOptionsWorker(this.#commandLineOptions, this.#pathMatch, this.#storeService, this.#onDiagnostic);
1999
- await commandLineWorker.parse(commandLineArgs);
2000
- if (this.#commandLineOptions.config != null) {
2001
- this.#configFilePath = this.#commandLineOptions.config;
2002
- delete this.#commandLineOptions.config;
2003
- }
1252
+ static fromJsonNode(node, sourceFile, skipTrivia) {
1253
+ return new DiagnosticOrigin(skipTrivia(node.pos, sourceFile), node.end, sourceFile);
2004
1254
  }
2005
- async readConfigFile() {
2006
- this.#configFileOptions = {
2007
- rootPath: Path.dirname(this.#configFilePath),
2008
- };
2009
- if (!existsSync(this.#configFilePath)) {
2010
- return;
1255
+ static fromNode(node, breadcrumbs) {
1256
+ return new DiagnosticOrigin(node.getStart(), node.getEnd(), node.getSourceFile(), breadcrumbs);
1257
+ }
1258
+ }
1259
+
1260
+ class Diagnostic {
1261
+ category;
1262
+ code;
1263
+ related;
1264
+ origin;
1265
+ text;
1266
+ constructor(text, category, origin) {
1267
+ this.text = text;
1268
+ this.category = category;
1269
+ this.origin = origin;
1270
+ }
1271
+ add(options) {
1272
+ if (options.code != null) {
1273
+ this.code = options.code;
2011
1274
  }
2012
- const configFileText = await fs.readFile(this.#configFilePath, {
2013
- encoding: "utf8",
1275
+ if (options.origin != null) {
1276
+ this.origin = options.origin;
1277
+ }
1278
+ if (options.related != null) {
1279
+ this.related = options.related;
1280
+ }
1281
+ return this;
1282
+ }
1283
+ static error(text, origin) {
1284
+ return new Diagnostic(text, "error", origin);
1285
+ }
1286
+ static fromDiagnostics(diagnostics, compiler) {
1287
+ return diagnostics.map((diagnostic) => {
1288
+ const category = "error";
1289
+ const code = `ts(${String(diagnostic.code)})`;
1290
+ let origin;
1291
+ const text = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
1292
+ if (Diagnostic.isTsDiagnosticWithLocation(diagnostic)) {
1293
+ origin = new DiagnosticOrigin(diagnostic.start, diagnostic.start + diagnostic.length, diagnostic.file);
1294
+ }
1295
+ return new Diagnostic(text, category, origin).add({ code });
2014
1296
  });
2015
- const configFileWorker = new ConfigFileOptionsWorker(this.#compiler, this.#configFileOptions, this.#configFilePath, this.#storeService, this.#onDiagnostic);
2016
- await configFileWorker.parse(configFileText);
2017
1297
  }
2018
- resolveConfig() {
2019
- return {
2020
- ...defaultOptions,
2021
- ...this.#configFileOptions,
2022
- ...this.#commandLineOptions,
2023
- configFilePath: this.#configFilePath,
2024
- pathMatch: this.#pathMatch,
2025
- };
1298
+ static fromError(text, error) {
1299
+ const messageText = Array.isArray(text) ? text : [text];
1300
+ if (error instanceof Error && error.stack != null) {
1301
+ if (messageText.length > 1) {
1302
+ messageText.push("");
1303
+ }
1304
+ const stackLines = error.stack.split("\n").map((line) => line.trimStart());
1305
+ messageText.push(...stackLines);
1306
+ }
1307
+ return Diagnostic.error(messageText);
1308
+ }
1309
+ static isTsDiagnosticWithLocation(diagnostic) {
1310
+ return diagnostic.file != null && diagnostic.start != null && diagnostic.length != null;
1311
+ }
1312
+ static warning(text, origin) {
1313
+ return new Diagnostic(text, "warning", origin);
2026
1314
  }
2027
1315
  }
2028
1316
 
2029
- var OptionBrand;
2030
- (function (OptionBrand) {
2031
- OptionBrand["String"] = "string";
2032
- OptionBrand["Number"] = "number";
2033
- OptionBrand["Boolean"] = "boolean";
2034
- OptionBrand["BareTrue"] = "bareTrue";
2035
- OptionBrand["List"] = "list";
2036
- })(OptionBrand || (OptionBrand = {}));
2037
- var OptionGroup;
2038
- (function (OptionGroup) {
2039
- OptionGroup[OptionGroup["CommandLine"] = 2] = "CommandLine";
2040
- OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
2041
- })(OptionGroup || (OptionGroup = {}));
1317
+ var DiagnosticCategory;
1318
+ (function (DiagnosticCategory) {
1319
+ DiagnosticCategory["Error"] = "error";
1320
+ DiagnosticCategory["Warning"] = "warning";
1321
+ })(DiagnosticCategory || (DiagnosticCategory = {}));
2042
1322
 
2043
1323
  class InputService {
2044
1324
  #onInput;
@@ -2047,6 +1327,7 @@ class InputService {
2047
1327
  this.#onInput = onInput;
2048
1328
  this.#stdin = options?.stdin ?? process.stdin;
2049
1329
  this.#stdin.setRawMode?.(true);
1330
+ this.#stdin.setEncoding("utf8");
2050
1331
  this.#stdin.unref();
2051
1332
  this.#stdin.addListener("data", this.#onInput);
2052
1333
  }
@@ -2056,6 +1337,142 @@ class InputService {
2056
1337
  }
2057
1338
  }
2058
1339
 
1340
+ class SelectDiagnosticText {
1341
+ static #pathSelectOptions(resolvedConfig) {
1342
+ const text = [
1343
+ `Root path: ${resolvedConfig.rootPath}`,
1344
+ `Test file match: ${resolvedConfig.testFileMatch.join(", ")}`,
1345
+ ];
1346
+ if (resolvedConfig.pathMatch.length > 0) {
1347
+ text.push(`Path match: ${resolvedConfig.pathMatch.join(", ")}`);
1348
+ }
1349
+ return text;
1350
+ }
1351
+ static noTestFilesWereLeft(resolvedConfig) {
1352
+ return [
1353
+ "No test files were left to run using current configuration.",
1354
+ ...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
1355
+ ];
1356
+ }
1357
+ static noTestFilesWereSelected(resolvedConfig) {
1358
+ return [
1359
+ "No test files were selected using current configuration.",
1360
+ ...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
1361
+ ];
1362
+ }
1363
+ }
1364
+
1365
+ class GlobPattern {
1366
+ static #reservedCharacterRegex = /[^\w\s/]/g;
1367
+ static #parse(pattern, usageTarget) {
1368
+ const segments = pattern.split("/");
1369
+ let resultPattern = "\\.";
1370
+ let optionalSegmentCount = 0;
1371
+ for (const segment of segments) {
1372
+ if (segment === ".") {
1373
+ continue;
1374
+ }
1375
+ if (segment === "**") {
1376
+ resultPattern += "(\\/(?!(node_modules)(\\/|$))[^./][^/]*)*?";
1377
+ continue;
1378
+ }
1379
+ if (usageTarget === "directories") {
1380
+ resultPattern += "(";
1381
+ optionalSegmentCount++;
1382
+ }
1383
+ resultPattern += "\\/";
1384
+ const segmentPattern = segment.replace(GlobPattern.#reservedCharacterRegex, GlobPattern.#replaceReservedCharacter);
1385
+ if (segmentPattern !== segment) {
1386
+ resultPattern += "(?!(node_modules)(\\/|$))";
1387
+ }
1388
+ resultPattern += segmentPattern;
1389
+ }
1390
+ resultPattern += ")?".repeat(optionalSegmentCount);
1391
+ return resultPattern;
1392
+ }
1393
+ static #replaceReservedCharacter(match, offset) {
1394
+ switch (match) {
1395
+ case "*":
1396
+ return offset === 0 ? "([^./][^/]*)?" : "([^/]*)?";
1397
+ case "?":
1398
+ return offset === 0 ? "[^./]" : "[^/]";
1399
+ default:
1400
+ return `\\${match}`;
1401
+ }
1402
+ }
1403
+ static toRegex(patterns, usageTarget) {
1404
+ const patternText = patterns.map((pattern) => `(${GlobPattern.#parse(pattern, usageTarget)})`).join("|");
1405
+ return new RegExp(`^(${patternText})$`);
1406
+ }
1407
+ }
1408
+
1409
+ class SelectService {
1410
+ #includeDirectoryRegex;
1411
+ #includeFileRegex;
1412
+ #resolvedConfig;
1413
+ constructor(resolvedConfig) {
1414
+ this.#resolvedConfig = resolvedConfig;
1415
+ this.#includeDirectoryRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "directories");
1416
+ this.#includeFileRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "files");
1417
+ }
1418
+ #isDirectoryIncluded(directoryPath) {
1419
+ return this.#includeDirectoryRegex.test(directoryPath);
1420
+ }
1421
+ #isFileIncluded(filePath) {
1422
+ if (this.#resolvedConfig.pathMatch.length > 0 &&
1423
+ !this.#resolvedConfig.pathMatch.some((match) => filePath.toLowerCase().includes(match.toLowerCase()))) {
1424
+ return false;
1425
+ }
1426
+ return this.#includeFileRegex.test(filePath);
1427
+ }
1428
+ isTestFile(filePath) {
1429
+ return this.#isFileIncluded(Path.relative(this.#resolvedConfig.rootPath, filePath));
1430
+ }
1431
+ #onDiagnostic(diagnostic) {
1432
+ EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
1433
+ }
1434
+ async selectFiles() {
1435
+ const currentPath = ".";
1436
+ const testFilePaths = [];
1437
+ await this.#visitDirectory(currentPath, testFilePaths);
1438
+ if (testFilePaths.length === 0) {
1439
+ this.#onDiagnostic(Diagnostic.error(SelectDiagnosticText.noTestFilesWereSelected(this.#resolvedConfig)));
1440
+ }
1441
+ return testFilePaths.sort();
1442
+ }
1443
+ async #visitDirectory(currentPath, testFilePaths) {
1444
+ const targetPath = Path.join(this.#resolvedConfig.rootPath, currentPath);
1445
+ let entries = [];
1446
+ try {
1447
+ entries = await fs.readdir(targetPath, { withFileTypes: true });
1448
+ }
1449
+ catch {
1450
+ }
1451
+ for (const entry of entries) {
1452
+ let entryMeta;
1453
+ if (entry.isSymbolicLink()) {
1454
+ try {
1455
+ entryMeta = await fs.stat([targetPath, entry.name].join("/"));
1456
+ }
1457
+ catch {
1458
+ continue;
1459
+ }
1460
+ }
1461
+ else {
1462
+ entryMeta = entry;
1463
+ }
1464
+ const entryPath = [currentPath, entry.name].join("/");
1465
+ if (entryMeta.isDirectory() && this.#isDirectoryIncluded(entryPath)) {
1466
+ await this.#visitDirectory(entryPath, testFilePaths);
1467
+ continue;
1468
+ }
1469
+ if (entryMeta.isFile() && this.#isFileIncluded(entryPath)) {
1470
+ testFilePaths.push([targetPath, entry.name].join("/"));
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+
2059
1476
  class Timer {
2060
1477
  #timeout;
2061
1478
  clear() {
@@ -2085,19 +1502,18 @@ class WatchService {
2085
1502
  this.#selectService = selectService;
2086
1503
  this.#watchedTestFiles = new Map(testFiles.map((testFile) => [testFile.path, testFile]));
2087
1504
  const onInput = (chunk) => {
2088
- switch (chunk.toString()) {
1505
+ switch (chunk.toLowerCase()) {
2089
1506
  case "\u0003":
2090
1507
  case "\u0004":
2091
1508
  case "\u001B":
2092
- case "\u0058":
2093
- case "\u0078": {
1509
+ case "q":
1510
+ case "x": {
2094
1511
  this.close();
2095
1512
  break;
2096
1513
  }
2097
1514
  case "\u000D":
2098
1515
  case "\u0020":
2099
- case "\u0041":
2100
- case "\u0061": {
1516
+ case "a": {
2101
1517
  this.#runAll();
2102
1518
  break;
2103
1519
  }
@@ -2148,7 +1564,7 @@ class WatchService {
2148
1564
  this.#changedTestFiles.delete(filePath);
2149
1565
  this.#watchedTestFiles.delete(filePath);
2150
1566
  if (this.#watchedTestFiles.size === 0) {
2151
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.noTestFilesWereLeft(this.#resolvedConfig)));
1567
+ this.#onDiagnostic(Diagnostic.error(SelectDiagnosticText.noTestFilesWereLeft(this.#resolvedConfig)));
2152
1568
  }
2153
1569
  };
2154
1570
  this.#watchers.push(new Watcher(this.#resolvedConfig.rootPath, onChangedFile, onRemovedFile, { recursive: true }));
@@ -3224,294 +2640,743 @@ class TestTreeWorker {
3224
2640
  if (testResult.expectCount.failed > 0) {
3225
2641
  EventEmitter.dispatch(["test:fail", { result: testResult }]);
3226
2642
  }
3227
- else {
3228
- EventEmitter.dispatch(["test:pass", { result: testResult }]);
2643
+ else {
2644
+ EventEmitter.dispatch(["test:pass", { result: testResult }]);
2645
+ }
2646
+ }
2647
+ }
2648
+
2649
+ class TestFileRunner {
2650
+ #compiler;
2651
+ #collectService;
2652
+ #resolvedConfig;
2653
+ #projectService;
2654
+ constructor(resolvedConfig, compiler) {
2655
+ this.#resolvedConfig = resolvedConfig;
2656
+ this.#compiler = compiler;
2657
+ this.#collectService = new CollectService(compiler);
2658
+ this.#projectService = new ProjectService(compiler);
2659
+ }
2660
+ run(testFile, cancellationToken) {
2661
+ if (cancellationToken?.isCancellationRequested === true) {
2662
+ return;
2663
+ }
2664
+ this.#projectService.openFile(testFile.path, undefined, this.#resolvedConfig.rootPath);
2665
+ const fileResult = new FileResult(testFile);
2666
+ EventEmitter.dispatch(["file:start", { result: fileResult }]);
2667
+ this.#runFile(testFile, fileResult, cancellationToken);
2668
+ EventEmitter.dispatch(["file:end", { result: fileResult }]);
2669
+ this.#projectService.closeFile(testFile.path);
2670
+ }
2671
+ #runFile(testFile, fileResult, cancellationToken) {
2672
+ const languageService = this.#projectService.getLanguageService(testFile.path);
2673
+ if (!languageService) {
2674
+ return;
2675
+ }
2676
+ const syntacticDiagnostics = languageService.getSyntacticDiagnostics(testFile.path);
2677
+ if (syntacticDiagnostics.length > 0) {
2678
+ EventEmitter.dispatch([
2679
+ "file:error",
2680
+ {
2681
+ diagnostics: Diagnostic.fromDiagnostics(syntacticDiagnostics, this.#compiler),
2682
+ result: fileResult,
2683
+ },
2684
+ ]);
2685
+ return;
2686
+ }
2687
+ const semanticDiagnostics = languageService.getSemanticDiagnostics(testFile.path);
2688
+ const program = languageService.getProgram();
2689
+ if (!program) {
2690
+ return;
2691
+ }
2692
+ const sourceFile = program.getSourceFile(testFile.path);
2693
+ if (!sourceFile) {
2694
+ return;
2695
+ }
2696
+ const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
2697
+ if (testTree.diagnostics.size > 0) {
2698
+ EventEmitter.dispatch([
2699
+ "file:error",
2700
+ {
2701
+ diagnostics: Diagnostic.fromDiagnostics([...testTree.diagnostics], this.#compiler),
2702
+ result: fileResult,
2703
+ },
2704
+ ]);
2705
+ return;
2706
+ }
2707
+ const typeChecker = program.getTypeChecker();
2708
+ if (!Expect.assertTypeChecker(typeChecker)) {
2709
+ const text = "The required 'isTypeRelatedTo()' method is missing in the provided type checker.";
2710
+ EventEmitter.dispatch(["file:error", { diagnostics: [Diagnostic.error(text)], result: fileResult }]);
2711
+ return;
2712
+ }
2713
+ const expect = new Expect(this.#compiler, typeChecker);
2714
+ const testTreeWorker = new TestTreeWorker(this.#resolvedConfig, this.#compiler, expect, {
2715
+ cancellationToken,
2716
+ fileResult,
2717
+ hasOnly: testTree.hasOnly,
2718
+ position: testFile.position,
2719
+ });
2720
+ testTreeWorker.visit(testTree.members, 0, undefined);
2721
+ }
2722
+ }
2723
+
2724
+ class TaskRunner {
2725
+ #eventEmitter = new EventEmitter();
2726
+ #resolvedConfig;
2727
+ #selectService;
2728
+ #storeService;
2729
+ constructor(resolvedConfig, selectService, storeService) {
2730
+ this.#resolvedConfig = resolvedConfig;
2731
+ this.#selectService = selectService;
2732
+ this.#storeService = storeService;
2733
+ this.#eventEmitter.addHandler(new ResultHandler());
2734
+ }
2735
+ close() {
2736
+ this.#eventEmitter.removeHandlers();
2737
+ }
2738
+ async run(testFiles, cancellationToken = new CancellationToken()) {
2739
+ let cancellationHandler;
2740
+ if (this.#resolvedConfig.failFast) {
2741
+ cancellationHandler = new CancellationHandler(cancellationToken, "failFast");
2742
+ this.#eventEmitter.addHandler(cancellationHandler);
2743
+ }
2744
+ if (this.#resolvedConfig.watch === true) {
2745
+ await this.#watch(testFiles, cancellationToken);
2746
+ }
2747
+ else {
2748
+ await this.#run(testFiles, cancellationToken);
2749
+ }
2750
+ if (cancellationHandler != null) {
2751
+ this.#eventEmitter.removeHandler(cancellationHandler);
2752
+ }
2753
+ }
2754
+ async #run(testFiles, cancellationToken) {
2755
+ const result = new Result(this.#resolvedConfig, testFiles);
2756
+ EventEmitter.dispatch(["run:start", { result }]);
2757
+ for (const versionTag of this.#resolvedConfig.target) {
2758
+ const targetResult = new TargetResult(versionTag, testFiles);
2759
+ EventEmitter.dispatch(["target:start", { result: targetResult }]);
2760
+ const compiler = await this.#storeService.load(versionTag, cancellationToken);
2761
+ if (compiler) {
2762
+ const testFileRunner = new TestFileRunner(this.#resolvedConfig, compiler);
2763
+ for (const testFile of testFiles) {
2764
+ testFileRunner.run(testFile, cancellationToken);
2765
+ }
2766
+ }
2767
+ EventEmitter.dispatch(["target:end", { result: targetResult }]);
2768
+ }
2769
+ EventEmitter.dispatch(["run:end", { result }]);
2770
+ if (cancellationToken?.reason === "failFast") {
2771
+ cancellationToken.reset();
3229
2772
  }
3230
2773
  }
2774
+ async #watch(testFiles, cancellationToken) {
2775
+ await this.#run(testFiles, cancellationToken);
2776
+ const runCallback = async (testFiles) => {
2777
+ await this.#run(testFiles, cancellationToken);
2778
+ };
2779
+ const watchModeManager = new WatchService(this.#resolvedConfig, runCallback, this.#selectService, testFiles);
2780
+ cancellationToken?.onCancellationRequested((reason) => {
2781
+ if (reason !== "failFast") {
2782
+ watchModeManager.close();
2783
+ }
2784
+ });
2785
+ await watchModeManager.watch(cancellationToken);
2786
+ }
3231
2787
  }
3232
2788
 
3233
- class TestFileRunner {
3234
- #compiler;
3235
- #collectService;
2789
+ class TSTyche {
2790
+ #eventEmitter = new EventEmitter();
2791
+ #outputService;
3236
2792
  #resolvedConfig;
3237
- #projectService;
3238
- constructor(resolvedConfig, compiler) {
2793
+ #selectService;
2794
+ #storeService;
2795
+ #taskRunner;
2796
+ static version = "2.0.0";
2797
+ constructor(resolvedConfig, outputService, selectService, storeService) {
3239
2798
  this.#resolvedConfig = resolvedConfig;
3240
- this.#compiler = compiler;
3241
- this.#collectService = new CollectService(compiler);
3242
- this.#projectService = new ProjectService(compiler);
2799
+ this.#outputService = outputService;
2800
+ this.#selectService = selectService;
2801
+ this.#storeService = storeService;
2802
+ this.#taskRunner = new TaskRunner(this.#resolvedConfig, this.#selectService, this.#storeService);
3243
2803
  }
3244
- run(testFile, cancellationToken) {
3245
- if (cancellationToken?.isCancellationRequested === true) {
3246
- return;
3247
- }
3248
- this.#projectService.openFile(testFile.path, undefined, this.#resolvedConfig.rootPath);
3249
- const fileResult = new FileResult(testFile);
3250
- EventEmitter.dispatch(["file:start", { result: fileResult }]);
3251
- this.#runFile(testFile, fileResult, cancellationToken);
3252
- EventEmitter.dispatch(["file:end", { result: fileResult }]);
3253
- this.#projectService.closeFile(testFile.path);
2804
+ close() {
2805
+ this.#taskRunner.close();
3254
2806
  }
3255
- #runFile(testFile, fileResult, cancellationToken) {
3256
- const languageService = this.#projectService.getLanguageService(testFile.path);
3257
- if (!languageService) {
3258
- return;
2807
+ async run(testFiles, cancellationToken = new CancellationToken()) {
2808
+ this.#eventEmitter.addHandler(new RunReporter(this.#resolvedConfig, this.#outputService));
2809
+ if (this.#resolvedConfig.watch === true) {
2810
+ this.#eventEmitter.addHandler(new WatchReporter(this.#outputService));
3259
2811
  }
3260
- const syntacticDiagnostics = languageService.getSyntacticDiagnostics(testFile.path);
3261
- if (syntacticDiagnostics.length > 0) {
3262
- EventEmitter.dispatch([
3263
- "file:error",
3264
- {
3265
- diagnostics: Diagnostic.fromDiagnostics(syntacticDiagnostics, this.#compiler),
3266
- result: fileResult,
3267
- },
3268
- ]);
3269
- return;
2812
+ else {
2813
+ this.#eventEmitter.addHandler(new SummaryReporter(this.#outputService));
3270
2814
  }
3271
- const semanticDiagnostics = languageService.getSemanticDiagnostics(testFile.path);
3272
- const program = languageService.getProgram();
3273
- if (!program) {
3274
- return;
2815
+ await this.#taskRunner.run(testFiles.map((testFile) => new TestFile(testFile)), cancellationToken);
2816
+ this.#eventEmitter.removeHandlers();
2817
+ }
2818
+ }
2819
+
2820
+ class ConfigDiagnosticText {
2821
+ static doubleQuotesExpected() {
2822
+ return "String literal with double quotes expected.";
2823
+ }
2824
+ static expectsListItemType(optionName, optionBrand) {
2825
+ return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
2826
+ }
2827
+ static expectsValue(optionName, optionGroup) {
2828
+ optionName = ConfigDiagnosticText.#optionName(optionName, optionGroup);
2829
+ return `Option '${optionName}' expects a value.`;
2830
+ }
2831
+ static fileDoesNotExist(filePath) {
2832
+ return `The specified path '${filePath}' does not exist.`;
2833
+ }
2834
+ static #optionName(optionName, optionGroup) {
2835
+ switch (optionGroup) {
2836
+ case 2:
2837
+ return `--${optionName}`;
2838
+ case 4:
2839
+ return optionName;
3275
2840
  }
3276
- const sourceFile = program.getSourceFile(testFile.path);
3277
- if (!sourceFile) {
3278
- return;
2841
+ }
2842
+ static testFileMatchCannotStartWith(segment) {
2843
+ return [
2844
+ `A test file match pattern cannot start with '${segment}'.`,
2845
+ "The test files are only collected within the 'rootPath' directory.",
2846
+ ];
2847
+ }
2848
+ static requiresValueType(optionName, optionBrand, optionGroup) {
2849
+ optionName = ConfigDiagnosticText.#optionName(optionName, optionGroup);
2850
+ return `Option '${optionName}' requires a value of type ${optionBrand}.`;
2851
+ }
2852
+ static unknownOption(optionName) {
2853
+ return `Unknown option '${optionName}'.`;
2854
+ }
2855
+ static versionIsNotSupported(value) {
2856
+ if (value === "current") {
2857
+ return "Cannot use 'current' as a target. Failed to resolve the path to the currently installed TypeScript module.";
3279
2858
  }
3280
- const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
3281
- if (testTree.diagnostics.size > 0) {
3282
- EventEmitter.dispatch([
3283
- "file:error",
3284
- {
3285
- diagnostics: Diagnostic.fromDiagnostics([...testTree.diagnostics], this.#compiler),
3286
- result: fileResult,
3287
- },
3288
- ]);
3289
- return;
2859
+ return `TypeScript version '${value}' is not supported.`;
2860
+ }
2861
+ static watchCannotBeEnabled() {
2862
+ return "The watch mode cannot be enabled in a continuous integration environment.";
2863
+ }
2864
+ }
2865
+
2866
+ class OptionDefinitionsMap {
2867
+ static #definitions = [
2868
+ {
2869
+ brand: "string",
2870
+ description: "The path to a TSTyche configuration file.",
2871
+ group: 2,
2872
+ name: "config",
2873
+ },
2874
+ {
2875
+ brand: "boolean",
2876
+ description: "Stop running tests after the first failed assertion.",
2877
+ group: 4 | 2,
2878
+ name: "failFast",
2879
+ },
2880
+ {
2881
+ brand: "bareTrue",
2882
+ description: "Print the list of command line options with brief descriptions and exit.",
2883
+ group: 2,
2884
+ name: "help",
2885
+ },
2886
+ {
2887
+ brand: "bareTrue",
2888
+ description: "Install specified versions of the 'typescript' package and exit.",
2889
+ group: 2,
2890
+ name: "install",
2891
+ },
2892
+ {
2893
+ brand: "bareTrue",
2894
+ description: "Print the list of the selected test files and exit.",
2895
+ group: 2,
2896
+ name: "listFiles",
2897
+ },
2898
+ {
2899
+ brand: "string",
2900
+ description: "Only run tests with matching name.",
2901
+ group: 2,
2902
+ name: "only",
2903
+ },
2904
+ {
2905
+ brand: "bareTrue",
2906
+ description: "Remove all installed versions of the 'typescript' package and exit.",
2907
+ group: 2,
2908
+ name: "prune",
2909
+ },
2910
+ {
2911
+ brand: "string",
2912
+ description: "The path to a directory containing files of a test project.",
2913
+ group: 4,
2914
+ name: "rootPath",
2915
+ },
2916
+ {
2917
+ brand: "bareTrue",
2918
+ description: "Print the resolved configuration and exit.",
2919
+ group: 2,
2920
+ name: "showConfig",
2921
+ },
2922
+ {
2923
+ brand: "string",
2924
+ description: "Skip tests with matching name.",
2925
+ group: 2,
2926
+ name: "skip",
2927
+ },
2928
+ {
2929
+ brand: "list",
2930
+ description: "The list of TypeScript versions to be tested on.",
2931
+ group: 2 | 4,
2932
+ items: {
2933
+ brand: "string",
2934
+ name: "target",
2935
+ pattern: "^([45]\\.[0-9](\\.[0-9])?)|beta|current|latest|next|rc$",
2936
+ },
2937
+ name: "target",
2938
+ },
2939
+ {
2940
+ brand: "list",
2941
+ description: "The list of glob patterns matching the test files.",
2942
+ group: 4,
2943
+ items: {
2944
+ brand: "string",
2945
+ name: "testFileMatch",
2946
+ },
2947
+ name: "testFileMatch",
2948
+ },
2949
+ {
2950
+ brand: "bareTrue",
2951
+ description: "Fetch the 'typescript' package metadata from the registry and exit.",
2952
+ group: 2,
2953
+ name: "update",
2954
+ },
2955
+ {
2956
+ brand: "bareTrue",
2957
+ description: "Print the version number and exit.",
2958
+ group: 2,
2959
+ name: "version",
2960
+ },
2961
+ {
2962
+ brand: "bareTrue",
2963
+ description: "Watch for changes and rerun related test files.",
2964
+ group: 2,
2965
+ name: "watch",
2966
+ },
2967
+ ];
2968
+ static for(optionGroup) {
2969
+ const definitionMap = new Map();
2970
+ for (const definition of OptionDefinitionsMap.#definitions) {
2971
+ if (definition.group & optionGroup) {
2972
+ definitionMap.set(definition.name, definition);
2973
+ }
2974
+ }
2975
+ return definitionMap;
2976
+ }
2977
+ }
2978
+
2979
+ class OptionUsageText {
2980
+ #optionGroup;
2981
+ #storeService;
2982
+ constructor(optionGroup, storeService) {
2983
+ this.#optionGroup = optionGroup;
2984
+ this.#storeService = storeService;
2985
+ }
2986
+ async get(optionName, optionBrand) {
2987
+ const usageText = [];
2988
+ switch (optionName) {
2989
+ case "target": {
2990
+ const supportedTags = await this.#storeService.getSupportedTags();
2991
+ const supportedTagsText = `Supported tags: ${["'", supportedTags.join("', '"), "'"].join("")}.`;
2992
+ switch (this.#optionGroup) {
2993
+ case 2: {
2994
+ usageText.push("Value for the '--target' option must be a single tag or a comma separated list.", "Usage examples: '--target 4.9', '--target latest', '--target 4.9,5.3.2,current'.", supportedTagsText);
2995
+ break;
2996
+ }
2997
+ case 4: {
2998
+ usageText.push("Item of the 'target' list must be a supported version tag.", supportedTagsText);
2999
+ break;
3000
+ }
3001
+ }
3002
+ break;
3003
+ }
3004
+ default:
3005
+ usageText.push(ConfigDiagnosticText.requiresValueType(optionName, optionBrand, this.#optionGroup));
3290
3006
  }
3291
- const typeChecker = program.getTypeChecker();
3292
- if (!Expect.assertTypeChecker(typeChecker)) {
3293
- const text = "The required 'isTypeRelatedTo()' method is missing in the provided type checker.";
3294
- EventEmitter.dispatch(["file:error", { diagnostics: [Diagnostic.error(text)], result: fileResult }]);
3295
- return;
3007
+ return usageText;
3008
+ }
3009
+ }
3010
+
3011
+ class OptionValidator {
3012
+ #onDiagnostic;
3013
+ #optionGroup;
3014
+ #optionUsageText;
3015
+ #storeService;
3016
+ constructor(optionGroup, storeService, onDiagnostic) {
3017
+ this.#optionGroup = optionGroup;
3018
+ this.#storeService = storeService;
3019
+ this.#onDiagnostic = onDiagnostic;
3020
+ this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
3021
+ }
3022
+ async check(optionName, optionValue, optionBrand, origin) {
3023
+ switch (optionName) {
3024
+ case "config":
3025
+ case "rootPath": {
3026
+ if (!existsSync(optionValue)) {
3027
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.fileDoesNotExist(optionValue), origin));
3028
+ }
3029
+ break;
3030
+ }
3031
+ case "target": {
3032
+ if ((await this.#storeService.validateTag(optionValue)) === false) {
3033
+ this.#onDiagnostic(Diagnostic.error([
3034
+ ConfigDiagnosticText.versionIsNotSupported(optionValue),
3035
+ ...(await this.#optionUsageText.get(optionName, optionBrand)),
3036
+ ], origin));
3037
+ }
3038
+ break;
3039
+ }
3040
+ case "testFileMatch": {
3041
+ for (const segment of ["/", "../"]) {
3042
+ if (optionValue.startsWith(segment)) {
3043
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.testFileMatchCannotStartWith(segment), origin));
3044
+ }
3045
+ }
3046
+ break;
3047
+ }
3048
+ case "watch": {
3049
+ if (Environment.isCi) {
3050
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.watchCannotBeEnabled(), origin));
3051
+ }
3052
+ break;
3053
+ }
3296
3054
  }
3297
- const expect = new Expect(this.#compiler, typeChecker);
3298
- const testTreeWorker = new TestTreeWorker(this.#resolvedConfig, this.#compiler, expect, {
3299
- cancellationToken,
3300
- fileResult,
3301
- hasOnly: testTree.hasOnly,
3302
- position: testFile.position,
3303
- });
3304
- testTreeWorker.visit(testTree.members, 0, undefined);
3305
3055
  }
3306
3056
  }
3307
3057
 
3308
- class TaskRunner {
3309
- #eventEmitter = new EventEmitter();
3310
- #resolvedConfig;
3311
- #selectService;
3058
+ class CommandLineOptionsWorker {
3059
+ #commandLineOptionDefinitions;
3060
+ #commandLineOptions;
3061
+ #onDiagnostic;
3062
+ #optionGroup = 2;
3063
+ #optionUsageText;
3064
+ #optionValidator;
3065
+ #pathMatch;
3312
3066
  #storeService;
3313
- constructor(resolvedConfig, selectService, storeService) {
3314
- this.#resolvedConfig = resolvedConfig;
3315
- this.#selectService = selectService;
3067
+ constructor(commandLineOptions, pathMatch, storeService, onDiagnostic) {
3068
+ this.#commandLineOptions = commandLineOptions;
3069
+ this.#pathMatch = pathMatch;
3316
3070
  this.#storeService = storeService;
3317
- this.#eventEmitter.addHandler(new ResultHandler());
3071
+ this.#onDiagnostic = onDiagnostic;
3072
+ this.#commandLineOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
3073
+ this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
3074
+ this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
3318
3075
  }
3319
- close() {
3320
- this.#eventEmitter.removeHandlers();
3076
+ async #onExpectsValue(optionDefinition) {
3077
+ const text = [
3078
+ ConfigDiagnosticText.expectsValue(optionDefinition.name, this.#optionGroup),
3079
+ ...(await this.#optionUsageText.get(optionDefinition.name, optionDefinition.brand)),
3080
+ ];
3081
+ this.#onDiagnostic(Diagnostic.error(text));
3321
3082
  }
3322
- async run(testFiles, cancellationToken = new CancellationToken()) {
3323
- let cancellationHandler;
3324
- if (this.#resolvedConfig.failFast) {
3325
- cancellationHandler = new CancellationHandler(cancellationToken, "failFast");
3326
- this.#eventEmitter.addHandler(cancellationHandler);
3327
- }
3328
- if (this.#resolvedConfig.watch === true) {
3329
- await this.#watch(testFiles, cancellationToken);
3330
- }
3331
- else {
3332
- await this.#run(testFiles, cancellationToken);
3333
- }
3334
- if (cancellationHandler != null) {
3335
- this.#eventEmitter.removeHandler(cancellationHandler);
3083
+ async parse(commandLineArgs) {
3084
+ let index = 0;
3085
+ let arg = commandLineArgs[index];
3086
+ while (arg != null) {
3087
+ index++;
3088
+ if (arg.startsWith("--")) {
3089
+ const optionName = arg.slice(2);
3090
+ const optionDefinition = this.#commandLineOptionDefinitions.get(optionName);
3091
+ if (optionDefinition) {
3092
+ index = await this.#parseOptionValue(commandLineArgs, index, optionDefinition);
3093
+ }
3094
+ else {
3095
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.unknownOption(arg)));
3096
+ }
3097
+ }
3098
+ else if (arg.startsWith("-")) {
3099
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.unknownOption(arg)));
3100
+ }
3101
+ else {
3102
+ this.#pathMatch.push(Path.normalizeSlashes(arg));
3103
+ }
3104
+ arg = commandLineArgs[index];
3336
3105
  }
3337
3106
  }
3338
- async #run(testFiles, cancellationToken) {
3339
- const result = new Result(this.#resolvedConfig, testFiles);
3340
- EventEmitter.dispatch(["run:start", { result }]);
3341
- for (const versionTag of this.#resolvedConfig.target) {
3342
- const targetResult = new TargetResult(versionTag, testFiles);
3343
- EventEmitter.dispatch(["target:start", { result: targetResult }]);
3344
- const compiler = await this.#storeService.load(versionTag, cancellationToken);
3345
- if (compiler) {
3346
- const testFileRunner = new TestFileRunner(this.#resolvedConfig, compiler);
3347
- for (const testFile of testFiles) {
3348
- testFileRunner.run(testFile, cancellationToken);
3107
+ async #parseOptionValue(commandLineArgs, index, optionDefinition) {
3108
+ let optionValue = this.#resolveOptionValue(commandLineArgs[index]);
3109
+ switch (optionDefinition.brand) {
3110
+ case "bareTrue": {
3111
+ await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
3112
+ this.#commandLineOptions[optionDefinition.name] = true;
3113
+ break;
3114
+ }
3115
+ case "boolean": {
3116
+ await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
3117
+ this.#commandLineOptions[optionDefinition.name] = optionValue !== "false";
3118
+ if (optionValue === "false" || optionValue === "true") {
3119
+ index++;
3349
3120
  }
3121
+ break;
3122
+ }
3123
+ case "list": {
3124
+ if (optionValue !== "") {
3125
+ const optionValues = optionValue
3126
+ .split(",")
3127
+ .map((value) => value.trim())
3128
+ .filter((value) => value !== "");
3129
+ for (const optionValue of optionValues) {
3130
+ await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
3131
+ }
3132
+ this.#commandLineOptions[optionDefinition.name] = optionValues;
3133
+ index++;
3134
+ break;
3135
+ }
3136
+ await this.#onExpectsValue(optionDefinition);
3137
+ break;
3138
+ }
3139
+ case "string": {
3140
+ if (optionValue !== "") {
3141
+ if (optionDefinition.name === "config") {
3142
+ optionValue = Path.resolve(optionValue);
3143
+ }
3144
+ await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
3145
+ this.#commandLineOptions[optionDefinition.name] = optionValue;
3146
+ index++;
3147
+ break;
3148
+ }
3149
+ await this.#onExpectsValue(optionDefinition);
3150
+ break;
3350
3151
  }
3351
- EventEmitter.dispatch(["target:end", { result: targetResult }]);
3352
- }
3353
- EventEmitter.dispatch(["run:end", { result }]);
3354
- if (cancellationToken?.reason === "failFast") {
3355
- cancellationToken.reset();
3356
3152
  }
3153
+ return index;
3357
3154
  }
3358
- async #watch(testFiles, cancellationToken) {
3359
- await this.#run(testFiles, cancellationToken);
3360
- const runCallback = async (testFiles) => {
3361
- await this.#run(testFiles, cancellationToken);
3362
- };
3363
- const watchModeManager = new WatchService(this.#resolvedConfig, runCallback, this.#selectService, testFiles);
3364
- cancellationToken?.onCancellationRequested((reason) => {
3365
- if (reason !== "failFast") {
3366
- watchModeManager.close();
3367
- }
3368
- });
3369
- await watchModeManager.watch(cancellationToken);
3155
+ #resolveOptionValue(target = "") {
3156
+ return target.startsWith("-") ? "" : target;
3370
3157
  }
3371
3158
  }
3372
3159
 
3373
- class TSTyche {
3374
- #eventEmitter = new EventEmitter();
3375
- #outputService;
3376
- #resolvedConfig;
3377
- #selectService;
3160
+ class ConfigFileOptionsWorker {
3161
+ #compiler;
3162
+ #configFileOptionDefinitions;
3163
+ #configFileOptions;
3164
+ #configFilePath;
3165
+ #onDiagnostic;
3166
+ #optionGroup = 4;
3167
+ #optionValidator;
3378
3168
  #storeService;
3379
- #taskRunner;
3380
- static version = "2.0.0-rc.1";
3381
- constructor(resolvedConfig, outputService, selectService, storeService) {
3382
- this.#resolvedConfig = resolvedConfig;
3383
- this.#outputService = outputService;
3384
- this.#selectService = selectService;
3169
+ constructor(compiler, configFileOptions, configFilePath, storeService, onDiagnostic) {
3170
+ this.#compiler = compiler;
3171
+ this.#configFileOptions = configFileOptions;
3172
+ this.#configFilePath = configFilePath;
3385
3173
  this.#storeService = storeService;
3386
- this.#taskRunner = new TaskRunner(this.#resolvedConfig, this.#selectService, this.#storeService);
3174
+ this.#onDiagnostic = onDiagnostic;
3175
+ this.#configFileOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
3176
+ this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
3387
3177
  }
3388
- close() {
3389
- this.#taskRunner.close();
3178
+ #isDoubleQuotedString(node, sourceFile) {
3179
+ return (node.kind === this.#compiler.SyntaxKind.StringLiteral &&
3180
+ sourceFile.text.slice(this.#skipTrivia(node.pos, sourceFile), node.end).startsWith('"'));
3390
3181
  }
3391
- async run(testFiles, cancellationToken = new CancellationToken()) {
3392
- this.#eventEmitter.addHandler(new RuntimeReporter(this.#resolvedConfig, this.#outputService));
3393
- if (this.#resolvedConfig.watch === true) {
3394
- this.#eventEmitter.addHandler(new WatchReporter(this.#outputService));
3182
+ async parse(sourceText) {
3183
+ const sourceFile = this.#compiler.parseJsonText(this.#configFilePath, sourceText);
3184
+ if (sourceFile.parseDiagnostics.length > 0) {
3185
+ for (const diagnostic of Diagnostic.fromDiagnostics(sourceFile.parseDiagnostics, this.#compiler)) {
3186
+ this.#onDiagnostic(diagnostic);
3187
+ }
3188
+ return;
3189
+ }
3190
+ const rootExpression = sourceFile.statements[0]?.expression;
3191
+ if (rootExpression == null || !this.#compiler.isObjectLiteralExpression(rootExpression)) {
3192
+ const origin = new DiagnosticOrigin(0, 0, sourceFile);
3193
+ this.#onDiagnostic(Diagnostic.error("The root value of a configuration file must be an object literal.", origin));
3194
+ return;
3395
3195
  }
3396
- else {
3397
- this.#eventEmitter.addHandler(new SummaryReporter(this.#outputService));
3196
+ for (const property of rootExpression.properties) {
3197
+ if (this.#compiler.isPropertyAssignment(property)) {
3198
+ if (!this.#isDoubleQuotedString(property.name, sourceFile)) {
3199
+ const origin = DiagnosticOrigin.fromJsonNode(property, sourceFile, this.#skipTrivia);
3200
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.doubleQuotesExpected(), origin));
3201
+ continue;
3202
+ }
3203
+ const optionName = this.#resolvePropertyName(property);
3204
+ if (optionName === "$schema") {
3205
+ continue;
3206
+ }
3207
+ const optionDefinition = this.#configFileOptionDefinitions.get(optionName);
3208
+ if (optionDefinition) {
3209
+ this.#configFileOptions[optionDefinition.name] = await this.#parseOptionValue(sourceFile, property.initializer, optionDefinition);
3210
+ }
3211
+ else {
3212
+ const origin = DiagnosticOrigin.fromJsonNode(property, sourceFile, this.#skipTrivia);
3213
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.unknownOption(optionName), origin));
3214
+ }
3215
+ }
3398
3216
  }
3399
- await this.#taskRunner.run(testFiles.map((testFile) => new TestFile(testFile)), cancellationToken);
3400
- this.#eventEmitter.removeHandlers();
3217
+ return;
3401
3218
  }
3402
- }
3403
-
3404
- class GlobPattern {
3405
- static #reservedCharacterRegex = /[^\w\s/]/g;
3406
- static #parse(pattern, usageTarget) {
3407
- const segments = pattern.split("/");
3408
- let resultPattern = "\\.";
3409
- let optionalSegmentCount = 0;
3410
- for (const segment of segments) {
3411
- if (segment === ".") {
3412
- continue;
3219
+ async #parseOptionValue(sourceFile, valueExpression, optionDefinition, isListItem = false) {
3220
+ switch (valueExpression.kind) {
3221
+ case this.#compiler.SyntaxKind.TrueKeyword: {
3222
+ if (optionDefinition.brand === "boolean") {
3223
+ return true;
3224
+ }
3225
+ break;
3413
3226
  }
3414
- if (segment === "**") {
3415
- resultPattern += "(\\/(?!(node_modules)(\\/|$))[^./][^/]*)*?";
3416
- continue;
3227
+ case this.#compiler.SyntaxKind.FalseKeyword: {
3228
+ if (optionDefinition.brand === "boolean") {
3229
+ return false;
3230
+ }
3231
+ break;
3417
3232
  }
3418
- if (usageTarget === "directories") {
3419
- resultPattern += "(";
3420
- optionalSegmentCount++;
3233
+ case this.#compiler.SyntaxKind.StringLiteral: {
3234
+ if (!this.#isDoubleQuotedString(valueExpression, sourceFile)) {
3235
+ const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
3236
+ this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.doubleQuotesExpected(), origin));
3237
+ return;
3238
+ }
3239
+ if (optionDefinition.brand === "string") {
3240
+ let value = valueExpression.text;
3241
+ if (optionDefinition.name === "rootPath") {
3242
+ value = Path.resolve(Path.dirname(this.#configFilePath), value);
3243
+ }
3244
+ const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
3245
+ await this.#optionValidator.check(optionDefinition.name, value, optionDefinition.brand, origin);
3246
+ return value;
3247
+ }
3248
+ break;
3421
3249
  }
3422
- resultPattern += "\\/";
3423
- const segmentPattern = segment.replace(GlobPattern.#reservedCharacterRegex, GlobPattern.#replaceReservedCharacter);
3424
- if (segmentPattern !== segment) {
3425
- resultPattern += "(?!(node_modules)(\\/|$))";
3250
+ case this.#compiler.SyntaxKind.ArrayLiteralExpression: {
3251
+ if (optionDefinition.brand === "list") {
3252
+ const value = [];
3253
+ for (const element of valueExpression.elements) {
3254
+ value.push(await this.#parseOptionValue(sourceFile, element, optionDefinition.items, true));
3255
+ }
3256
+ return value;
3257
+ }
3258
+ break;
3426
3259
  }
3427
- resultPattern += segmentPattern;
3428
3260
  }
3429
- resultPattern += ")?".repeat(optionalSegmentCount);
3430
- return resultPattern;
3261
+ const text = isListItem
3262
+ ? ConfigDiagnosticText.expectsListItemType(optionDefinition.name, optionDefinition.brand)
3263
+ : ConfigDiagnosticText.requiresValueType(optionDefinition.name, optionDefinition.brand, this.#optionGroup);
3264
+ const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
3265
+ this.#onDiagnostic(Diagnostic.error(text, origin));
3266
+ return;
3431
3267
  }
3432
- static #replaceReservedCharacter(match, offset) {
3433
- switch (match) {
3434
- case "*":
3435
- return offset === 0 ? "([^./][^/]*)?" : "([^/]*)?";
3436
- case "?":
3437
- return offset === 0 ? "[^./]" : "[^/]";
3438
- default:
3439
- return `\\${match}`;
3268
+ #resolvePropertyName({ name }) {
3269
+ if ("text" in name) {
3270
+ return name.text;
3440
3271
  }
3272
+ return "";
3441
3273
  }
3442
- static toRegex(patterns, usageTarget) {
3443
- const patternText = patterns.map((pattern) => `(${GlobPattern.#parse(pattern, usageTarget)})`).join("|");
3444
- return new RegExp(`^(${patternText})$`);
3274
+ #skipTrivia(position, sourceFile) {
3275
+ const { text } = sourceFile.getSourceFile();
3276
+ while (position < text.length) {
3277
+ if (/\s/.test(text.charAt(position))) {
3278
+ position++;
3279
+ continue;
3280
+ }
3281
+ if (text.charAt(position) === "/") {
3282
+ if (text.charAt(position + 1) === "/") {
3283
+ position += 2;
3284
+ while (position < text.length) {
3285
+ if (text.charAt(position) === "\n") {
3286
+ break;
3287
+ }
3288
+ position++;
3289
+ }
3290
+ continue;
3291
+ }
3292
+ if (text.charAt(position + 1) === "*") {
3293
+ position += 2;
3294
+ while (position < text.length) {
3295
+ if (text.charAt(position) === "*" && text.charAt(position + 1) === "/") {
3296
+ position += 2;
3297
+ break;
3298
+ }
3299
+ position++;
3300
+ }
3301
+ continue;
3302
+ }
3303
+ position++;
3304
+ continue;
3305
+ }
3306
+ break;
3307
+ }
3308
+ return position;
3445
3309
  }
3446
3310
  }
3447
3311
 
3448
- class SelectService {
3449
- #includeDirectoryRegex;
3450
- #includeFileRegex;
3451
- #resolvedConfig;
3452
- constructor(resolvedConfig) {
3453
- this.#resolvedConfig = resolvedConfig;
3454
- this.#includeDirectoryRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "directories");
3455
- this.#includeFileRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "files");
3456
- }
3457
- #isDirectoryIncluded(directoryPath) {
3458
- return this.#includeDirectoryRegex.test(directoryPath);
3459
- }
3460
- #isFileIncluded(filePath) {
3461
- if (this.#resolvedConfig.pathMatch.length > 0 &&
3462
- !this.#resolvedConfig.pathMatch.some((match) => filePath.toLowerCase().includes(match.toLowerCase()))) {
3463
- return false;
3464
- }
3465
- return this.#includeFileRegex.test(filePath);
3466
- }
3467
- isTestFile(filePath) {
3468
- return this.#isFileIncluded(Path.relative(this.#resolvedConfig.rootPath, filePath));
3312
+ const defaultOptions = {
3313
+ failFast: false,
3314
+ rootPath: "./",
3315
+ target: [Environment.typescriptPath == null ? "latest" : "current"],
3316
+ testFileMatch: ["**/*.tst.*", "**/__typetests__/*.test.*", "**/typetests/*.test.*"],
3317
+ };
3318
+ class ConfigService {
3319
+ #commandLineOptions = {};
3320
+ #compiler;
3321
+ #configFileOptions = {};
3322
+ #configFilePath = Path.resolve(defaultOptions.rootPath, "./tstyche.config.json");
3323
+ #pathMatch = [];
3324
+ #storeService;
3325
+ constructor(compiler, storeService) {
3326
+ this.#compiler = compiler;
3327
+ this.#storeService = storeService;
3469
3328
  }
3470
3329
  #onDiagnostic(diagnostic) {
3471
- EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
3330
+ EventEmitter.dispatch(["config:error", { diagnostics: [diagnostic] }]);
3472
3331
  }
3473
- async selectFiles() {
3474
- const currentPath = ".";
3475
- const testFilePaths = [];
3476
- await this.#visitDirectory(currentPath, testFilePaths);
3477
- if (testFilePaths.length === 0) {
3478
- this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.noTestFilesWereSelected(this.#resolvedConfig)));
3332
+ async parseCommandLine(commandLineArgs) {
3333
+ this.#commandLineOptions = {};
3334
+ this.#pathMatch = [];
3335
+ const commandLineWorker = new CommandLineOptionsWorker(this.#commandLineOptions, this.#pathMatch, this.#storeService, this.#onDiagnostic);
3336
+ await commandLineWorker.parse(commandLineArgs);
3337
+ if (this.#commandLineOptions.config != null) {
3338
+ this.#configFilePath = this.#commandLineOptions.config;
3339
+ delete this.#commandLineOptions.config;
3479
3340
  }
3480
- return testFilePaths.sort();
3481
3341
  }
3482
- async #visitDirectory(currentPath, testFilePaths) {
3483
- const targetPath = Path.join(this.#resolvedConfig.rootPath, currentPath);
3484
- let entries = [];
3485
- try {
3486
- entries = await fs.readdir(targetPath, { withFileTypes: true });
3487
- }
3488
- catch {
3489
- }
3490
- for (const entry of entries) {
3491
- let entryMeta;
3492
- if (entry.isSymbolicLink()) {
3493
- try {
3494
- entryMeta = await fs.stat([targetPath, entry.name].join("/"));
3495
- }
3496
- catch {
3497
- continue;
3498
- }
3499
- }
3500
- else {
3501
- entryMeta = entry;
3502
- }
3503
- const entryPath = [currentPath, entry.name].join("/");
3504
- if (entryMeta.isDirectory() && this.#isDirectoryIncluded(entryPath)) {
3505
- await this.#visitDirectory(entryPath, testFilePaths);
3506
- continue;
3507
- }
3508
- if (entryMeta.isFile() && this.#isFileIncluded(entryPath)) {
3509
- testFilePaths.push([targetPath, entry.name].join("/"));
3510
- }
3342
+ async readConfigFile() {
3343
+ this.#configFileOptions = {
3344
+ rootPath: Path.dirname(this.#configFilePath),
3345
+ };
3346
+ if (!existsSync(this.#configFilePath)) {
3347
+ return;
3511
3348
  }
3349
+ const configFileText = await fs.readFile(this.#configFilePath, {
3350
+ encoding: "utf8",
3351
+ });
3352
+ const configFileWorker = new ConfigFileOptionsWorker(this.#compiler, this.#configFileOptions, this.#configFilePath, this.#storeService, this.#onDiagnostic);
3353
+ await configFileWorker.parse(configFileText);
3354
+ }
3355
+ resolveConfig() {
3356
+ return {
3357
+ ...defaultOptions,
3358
+ ...this.#configFileOptions,
3359
+ ...this.#commandLineOptions,
3360
+ configFilePath: this.#configFilePath,
3361
+ pathMatch: this.#pathMatch,
3362
+ };
3512
3363
  }
3513
3364
  }
3514
3365
 
3366
+ var OptionBrand;
3367
+ (function (OptionBrand) {
3368
+ OptionBrand["String"] = "string";
3369
+ OptionBrand["Number"] = "number";
3370
+ OptionBrand["Boolean"] = "boolean";
3371
+ OptionBrand["BareTrue"] = "bareTrue";
3372
+ OptionBrand["List"] = "list";
3373
+ })(OptionBrand || (OptionBrand = {}));
3374
+ var OptionGroup;
3375
+ (function (OptionGroup) {
3376
+ OptionGroup[OptionGroup["CommandLine"] = 2] = "CommandLine";
3377
+ OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
3378
+ })(OptionGroup || (OptionGroup = {}));
3379
+
3515
3380
  class StoreDiagnosticText {
3516
3381
  static failedToFetchMetadata(registryUrl) {
3517
3382
  return `Failed to fetch metadata of the 'typescript' package from '${registryUrl.toString()}'.`;
@@ -4014,4 +3879,4 @@ class Cli {
4014
3879
  }
4015
3880
  }
4016
3881
 
4017
- export { Assertion, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, Environment, EventEmitter, ExitCodeHandler, Expect, ExpectResult, FileResult, FileWatcher, InputService, Line, OptionBrand, OptionDefinitionsMap, OptionDiagnosticText, OptionGroup, OutputService, Path, ProjectResult, ProjectService, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, RuntimeReporter, Scribbler, SelectService, SetupReporter, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestFile, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, Text, Version, WatchReporter, WatchService, Watcher, addsPackageStepText, defaultOptions, describeNameText, diagnosticText, fileStatusText, fileViewText, formattedText, helpText, summaryText, testNameText, usesCompilerStepText, waitingForFileChangesText, watchUsageText };
3882
+ export { Assertion, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, ConfigDiagnosticText, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, Environment, EventEmitter, ExitCodeHandler, Expect, ExpectResult, FileResult, FileWatcher, InputService, Line, OptionBrand, OptionDefinitionsMap, OptionGroup, OutputService, Path, ProjectResult, ProjectService, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, RunReporter, Scribbler, SelectDiagnosticText, SelectService, SetupReporter, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestFile, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, Text, Version, WatchReporter, WatchService, Watcher, addsPackageStepText, defaultOptions, describeNameText, diagnosticText, fileStatusText, fileViewText, formattedText, helpText, summaryText, testNameText, usesCompilerStepText, waitingForFileChangesText, watchUsageText };