tstyche 1.0.0 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/tstyche.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import process from 'node:process';
2
2
  import { fileURLToPath, pathToFileURL } from 'node:url';
3
+ import path from 'node:path';
3
4
  import { createRequire } from 'node:module';
4
5
  import os from 'node:os';
5
- import path from 'node:path';
6
6
  import { existsSync, writeFileSync, rmSync } from 'node:fs';
7
7
  import fs from 'node:fs/promises';
8
8
  import vm from 'node:vm';
@@ -24,103 +24,6 @@ class EventEmitter {
24
24
  }
25
25
  }
26
26
 
27
- class Path {
28
- static dirname(filePath) {
29
- return Path.normalizeSlashes(path.dirname(filePath));
30
- }
31
- static join(...filePaths) {
32
- return Path.normalizeSlashes(path.join(...filePaths));
33
- }
34
- static normalizeSlashes(filePath) {
35
- if (path.sep === "/") {
36
- return filePath;
37
- }
38
- return filePath.replaceAll("\\", "/");
39
- }
40
- static relative(from, to) {
41
- let relativePath = path.relative(from, to);
42
- if (!relativePath.startsWith(".")) {
43
- relativePath = `./${relativePath}`;
44
- }
45
- return Path.normalizeSlashes(relativePath);
46
- }
47
- static resolve(...filePaths) {
48
- return Path.normalizeSlashes(path.resolve(...filePaths));
49
- }
50
- }
51
-
52
- class Environment {
53
- static #noColor = Environment.#resolveNoColor();
54
- static #noInteractive = Environment.#resolveNoInteractive();
55
- static #storePath = Environment.#resolveStorePath();
56
- static #timeout = Environment.#resolveTimeout();
57
- static #typescriptPath = Environment.#resolveTypeScriptPath();
58
- static get noColor() {
59
- return Environment.#noColor;
60
- }
61
- static get noInteractive() {
62
- return Environment.#noInteractive;
63
- }
64
- static get storePath() {
65
- return Environment.#storePath;
66
- }
67
- static get timeout() {
68
- return Environment.#timeout;
69
- }
70
- static get typescriptPath() {
71
- return Environment.#typescriptPath;
72
- }
73
- static #resolveNoColor() {
74
- if (process.env["TSTYCHE_NO_COLOR"] != null) {
75
- return process.env["TSTYCHE_NO_COLOR"] !== "";
76
- }
77
- if (process.env["NO_COLOR"] != null) {
78
- return process.env["NO_COLOR"] !== "";
79
- }
80
- return false;
81
- }
82
- static #resolveNoInteractive() {
83
- if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) {
84
- return process.env["TSTYCHE_NO_INTERACTIVE"] !== "";
85
- }
86
- return !process.stdout.isTTY;
87
- }
88
- static #resolveStorePath() {
89
- if (process.env["TSTYCHE_STORE_PATH"] != null) {
90
- return Path.resolve(process.env["TSTYCHE_STORE_PATH"]);
91
- }
92
- if (process.platform === "darwin") {
93
- return Path.resolve(os.homedir(), "Library", "TSTyche");
94
- }
95
- if (process.env["LocalAppData"] != null) {
96
- return Path.resolve(process.env["LocalAppData"], "TSTyche");
97
- }
98
- if (process.env["XDG_DATA_HOME"] != null) {
99
- return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
100
- }
101
- return Path.resolve(os.homedir(), ".local", "share", "TSTyche");
102
- }
103
- static #resolveTimeout() {
104
- if (process.env["TSTYCHE_TIMEOUT"] != null) {
105
- return Number(process.env["TSTYCHE_TIMEOUT"]);
106
- }
107
- return 30;
108
- }
109
- static #resolveTypeScriptPath() {
110
- let moduleId = "typescript";
111
- if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) {
112
- moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"];
113
- }
114
- let resolvedPath;
115
- try {
116
- resolvedPath = createRequire(import.meta.url).resolve(moduleId);
117
- }
118
- catch {
119
- }
120
- return resolvedPath;
121
- }
122
- }
123
-
124
27
  var Color;
125
28
  (function (Color) {
126
29
  Color["Reset"] = "0";
@@ -134,8 +37,10 @@ var Color;
134
37
  })(Color || (Color = {}));
135
38
 
136
39
  class Scribbler {
40
+ #indentStep = " ";
137
41
  #newLine;
138
42
  #noColor;
43
+ #notEmptyLineRegex = /^(?!$)/gm;
139
44
  constructor(options) {
140
45
  this.#newLine = options?.newLine ?? "\n";
141
46
  this.#noColor = options?.noColor ?? false;
@@ -152,9 +57,7 @@ class Scribbler {
152
57
  return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
153
58
  }
154
59
  #indentEachLine(lines, level) {
155
- const indentStep = " ";
156
- const notEmptyLineRegExp = /^(?!$)/gm;
157
- return lines.replaceAll(notEmptyLineRegExp, indentStep.repeat(level));
60
+ return lines.replace(this.#notEmptyLineRegex, this.#indentStep.repeat(level));
158
61
  }
159
62
  render(element) {
160
63
  if (element != null) {
@@ -245,49 +148,6 @@ class Line {
245
148
  }
246
149
  }
247
150
 
248
- class Logger {
249
- #noColor;
250
- #scribbler;
251
- #stderr;
252
- #stdout;
253
- constructor(options) {
254
- this.#noColor = options?.noColor ?? Environment.noColor;
255
- this.#stderr = options?.stderr ?? process.stderr;
256
- this.#stdout = options?.stdout ?? process.stdout;
257
- this.#scribbler = new Scribbler({ noColor: this.#noColor });
258
- }
259
- eraseLastLine() {
260
- this.#stdout.write("\u001B[1A\u001B[0K");
261
- }
262
- #write(stream, body) {
263
- const elements = Array.isArray(body) ? body : [body];
264
- for (const element of elements) {
265
- if (element.$$typeof !== Symbol.for("tstyche:scribbler")) {
266
- return;
267
- }
268
- stream.write(this.#scribbler.render(element));
269
- }
270
- }
271
- writeError(body) {
272
- this.#write(this.#stderr, body);
273
- }
274
- writeMessage(body) {
275
- this.#write(this.#stdout, body);
276
- }
277
- writeWarning(body) {
278
- this.#write(this.#stderr, body);
279
- }
280
- }
281
-
282
- class Reporter {
283
- resolvedConfig;
284
- logger;
285
- constructor(resolvedConfig) {
286
- this.resolvedConfig = resolvedConfig;
287
- this.logger = new Logger();
288
- }
289
- }
290
-
291
151
  function addsPackageStepText(compilerVersion, installationPath) {
292
152
  return (Scribbler.createElement(Line, null,
293
153
  Scribbler.createElement(Text, { color: "90" }, "adds"),
@@ -302,6 +162,31 @@ function describeNameText(name, indent = 0) {
302
162
  return Scribbler.createElement(Line, { indent: indent + 1 }, name);
303
163
  }
304
164
 
165
+ class Path {
166
+ static dirname(filePath) {
167
+ return Path.normalizeSlashes(path.dirname(filePath));
168
+ }
169
+ static join(...filePaths) {
170
+ return Path.normalizeSlashes(path.join(...filePaths));
171
+ }
172
+ static normalizeSlashes(filePath) {
173
+ if (path.sep === "/") {
174
+ return filePath;
175
+ }
176
+ return filePath.replace(/\\/g, "/");
177
+ }
178
+ static relative(from, to) {
179
+ let relativePath = path.relative(from, to);
180
+ if (!relativePath.startsWith("./")) {
181
+ relativePath = `./${relativePath}`;
182
+ }
183
+ return Path.normalizeSlashes(relativePath);
184
+ }
185
+ static resolve(...filePaths) {
186
+ return Path.normalizeSlashes(path.resolve(...filePaths));
187
+ }
188
+ }
189
+
305
190
  class CodeSpanText {
306
191
  props;
307
192
  constructor(props) {
@@ -312,7 +197,7 @@ class CodeSpanText {
312
197
  const { character: markedCharacter, line: markedLine } = this.props.file.getLineAndCharacterOfPosition(this.props.start);
313
198
  const firstLine = Math.max(markedLine - 2, 0);
314
199
  const lastLine = Math.min(firstLine + 5, lastLineInFile);
315
- const lineNumberMaxWidth = `${lastLine + 1}`.length;
200
+ const lineNumberMaxWidth = String(lastLine + 1).length;
316
201
  const codeSpan = [];
317
202
  for (let index = firstLine; index <= lastLine; index++) {
318
203
  const lineStart = this.props.file.getPositionOfLineAndCharacter(index, 0);
@@ -320,7 +205,7 @@ class CodeSpanText {
320
205
  ? this.props.file.text.length
321
206
  : this.props.file.getPositionOfLineAndCharacter(index + 1, 0);
322
207
  const lineNumberText = String(index + 1);
323
- const lineText = this.props.file.text.slice(lineStart, lineEnd).trimEnd().replaceAll("\t", " ");
208
+ const lineText = this.props.file.text.slice(lineStart, lineEnd).trimEnd().replace(/\t/g, " ");
324
209
  if (index === markedLine) {
325
210
  codeSpan.push(Scribbler.createElement(Line, null,
326
211
  Scribbler.createElement(Text, { color: "31" }, ">"),
@@ -609,6 +494,125 @@ function helpText(optionDefinitions, tstycheVersion) {
609
494
  Scribbler.createElement(Line, null)));
610
495
  }
611
496
 
497
+ class Environment {
498
+ static #isCi = Environment.#resolveIsCi();
499
+ static #noColor = Environment.#resolveNoColor();
500
+ static #noInteractive = Environment.#resolveNoInteractive();
501
+ static #storePath = Environment.#resolveStorePath();
502
+ static #timeout = Environment.#resolveTimeout();
503
+ static #typescriptPath = Environment.#resolveTypeScriptPath();
504
+ static get isCi() {
505
+ return Environment.#isCi;
506
+ }
507
+ static get noColor() {
508
+ return Environment.#noColor;
509
+ }
510
+ static get noInteractive() {
511
+ return Environment.#noInteractive;
512
+ }
513
+ static get storePath() {
514
+ return Environment.#storePath;
515
+ }
516
+ static get timeout() {
517
+ return Environment.#timeout;
518
+ }
519
+ static get typescriptPath() {
520
+ return Environment.#typescriptPath;
521
+ }
522
+ static #resolveIsCi() {
523
+ if (process.env["CI"] != null) {
524
+ return process.env["CI"] !== "";
525
+ }
526
+ return false;
527
+ }
528
+ static #resolveNoColor() {
529
+ if (process.env["TSTYCHE_NO_COLOR"] != null) {
530
+ return process.env["TSTYCHE_NO_COLOR"] !== "";
531
+ }
532
+ if (process.env["NO_COLOR"] != null) {
533
+ return process.env["NO_COLOR"] !== "";
534
+ }
535
+ return false;
536
+ }
537
+ static #resolveNoInteractive() {
538
+ if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) {
539
+ return process.env["TSTYCHE_NO_INTERACTIVE"] !== "";
540
+ }
541
+ return !process.stdout.isTTY;
542
+ }
543
+ static #resolveStorePath() {
544
+ if (process.env["TSTYCHE_STORE_PATH"] != null) {
545
+ return Path.resolve(process.env["TSTYCHE_STORE_PATH"]);
546
+ }
547
+ if (process.platform === "darwin") {
548
+ return Path.resolve(os.homedir(), "Library", "TSTyche");
549
+ }
550
+ if (process.env["LocalAppData"] != null) {
551
+ return Path.resolve(process.env["LocalAppData"], "TSTyche");
552
+ }
553
+ if (process.env["XDG_DATA_HOME"] != null) {
554
+ return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
555
+ }
556
+ return Path.resolve(os.homedir(), ".local", "share", "TSTyche");
557
+ }
558
+ static #resolveTimeout() {
559
+ if (process.env["TSTYCHE_TIMEOUT"] != null) {
560
+ return Number(process.env["TSTYCHE_TIMEOUT"]);
561
+ }
562
+ return 30;
563
+ }
564
+ static #resolveTypeScriptPath() {
565
+ let moduleId = "typescript";
566
+ if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) {
567
+ moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"];
568
+ }
569
+ let resolvedPath;
570
+ try {
571
+ resolvedPath = Path.normalizeSlashes(createRequire(import.meta.url).resolve(moduleId));
572
+ }
573
+ catch {
574
+ }
575
+ return resolvedPath;
576
+ }
577
+ }
578
+
579
+ class OutputService {
580
+ #noColor;
581
+ #scribbler;
582
+ #stderr;
583
+ #stdout;
584
+ constructor(options) {
585
+ this.#noColor = options?.noColor ?? Environment.noColor;
586
+ this.#stderr = options?.stderr ?? process.stderr;
587
+ this.#stdout = options?.stdout ?? process.stdout;
588
+ this.#scribbler = new Scribbler({ noColor: this.#noColor });
589
+ }
590
+ clearTerminal() {
591
+ this.#stdout.write("\u001B[2J\u001B[3J\u001B[H");
592
+ }
593
+ eraseLastLine() {
594
+ this.#stdout.write("\u001B[1A\u001B[0K");
595
+ }
596
+ #write(stream, body) {
597
+ const elements = Array.isArray(body) ? body : [body];
598
+ for (const element of elements) {
599
+ if (element.$$typeof !== Symbol.for("tstyche:scribbler")) {
600
+ return;
601
+ }
602
+ stream.write(this.#scribbler.render(element));
603
+ }
604
+ }
605
+ writeError(body) {
606
+ this.#write(this.#stderr, body);
607
+ }
608
+ writeMessage(body) {
609
+ this.#write(this.#stdout, body);
610
+ }
611
+ writeWarning(body) {
612
+ this.#write(this.#stderr, body);
613
+ }
614
+ }
615
+
612
616
  class RowText {
613
617
  props;
614
618
  constructor(props) {
@@ -670,8 +674,8 @@ class DurationText {
670
674
  const minutes = Math.floor(duration / 60);
671
675
  const seconds = duration % 60;
672
676
  return (Scribbler.createElement(Text, null,
673
- minutes > 0 ? `${minutes}m ` : undefined,
674
- `${Math.round(seconds * 10) / 10}s`));
677
+ minutes > 0 ? `${String(minutes)}m ` : undefined,
678
+ `${String(Math.round(seconds * 10) / 10)}s`));
675
679
  }
676
680
  }
677
681
  class MatchText {
@@ -807,11 +811,30 @@ function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) {
807
811
  Scribbler.createElement(Line, null)));
808
812
  }
809
813
 
814
+ function watchModeUsageText() {
815
+ const usageText = Object.entries({ a: "run all tests", x: "exit" }).map(([key, action]) => {
816
+ return (Scribbler.createElement(Line, null,
817
+ Scribbler.createElement(Text, { color: "90" }, "Press"),
818
+ Scribbler.createElement(Text, null, ` ${key} `),
819
+ Scribbler.createElement(Text, { color: "90" }, `to ${action}.`)));
820
+ });
821
+ return (Scribbler.createElement(Text, null, usageText));
822
+ }
823
+
824
+ class Reporter {
825
+ resolvedConfig;
826
+ outputService;
827
+ constructor(resolvedConfig) {
828
+ this.resolvedConfig = resolvedConfig;
829
+ this.outputService = new OutputService();
830
+ }
831
+ }
832
+
810
833
  class SummaryReporter extends Reporter {
811
834
  handleEvent([eventName, payload]) {
812
835
  switch (eventName) {
813
- case "end":
814
- this.logger.writeMessage(summaryText({
836
+ case "run:end":
837
+ this.outputService.writeMessage(summaryText({
815
838
  duration: payload.result.timing.duration,
816
839
  expectCount: payload.result.expectCount,
817
840
  fileCount: payload.result.fileCount,
@@ -867,21 +890,30 @@ class ThoroughReporter extends Reporter {
867
890
  #hasReportedAdds = false;
868
891
  #hasReportedError = false;
869
892
  #isFileViewExpanded = false;
893
+ #seenDeprecations = new Set();
870
894
  get #isLastFile() {
871
895
  return this.#fileCount === 0;
872
896
  }
873
897
  handleEvent([eventName, payload]) {
874
898
  switch (eventName) {
875
- case "start":
876
- this.#isFileViewExpanded = payload.result.testFiles.length === 1;
899
+ case "deprecation:info":
900
+ for (const diagnostic of payload.diagnostics) {
901
+ if (!this.#seenDeprecations.has(diagnostic.text.toString())) {
902
+ this.#fileView.addMessage(diagnosticText(diagnostic));
903
+ this.#seenDeprecations.add(diagnostic.text.toString());
904
+ }
905
+ }
906
+ break;
907
+ case "run:start":
908
+ this.#isFileViewExpanded = payload.result.testFiles.length === 1 && this.resolvedConfig.watch !== true;
877
909
  break;
878
910
  case "store:info":
879
- this.logger.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
911
+ this.outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
880
912
  this.#hasReportedAdds = true;
881
913
  break;
882
914
  case "store:error":
883
915
  for (const diagnostic of payload.diagnostics) {
884
- this.logger.writeError(diagnosticText(diagnostic));
916
+ this.outputService.writeError(diagnosticText(diagnostic));
885
917
  }
886
918
  break;
887
919
  case "target:start":
@@ -894,7 +926,7 @@ class ThoroughReporter extends Reporter {
894
926
  case "project:info":
895
927
  if (this.#currentCompilerVersion !== payload.compilerVersion
896
928
  || this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) {
897
- this.logger.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
929
+ this.outputService.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
898
930
  prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds
899
931
  && !this.#hasReportedError,
900
932
  }));
@@ -905,12 +937,12 @@ class ThoroughReporter extends Reporter {
905
937
  break;
906
938
  case "project:error":
907
939
  for (const diagnostic of payload.diagnostics) {
908
- this.logger.writeError(diagnosticText(diagnostic));
940
+ this.outputService.writeError(diagnosticText(diagnostic));
909
941
  }
910
942
  break;
911
943
  case "file:start":
912
944
  if (!Environment.noInteractive) {
913
- this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
945
+ this.outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
914
946
  }
915
947
  this.#fileCount--;
916
948
  this.#hasReportedError = false;
@@ -922,12 +954,12 @@ class ThoroughReporter extends Reporter {
922
954
  break;
923
955
  case "file:end":
924
956
  if (!Environment.noInteractive) {
925
- this.logger.eraseLastLine();
957
+ this.outputService.eraseLastLine();
926
958
  }
927
- this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
928
- this.logger.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
959
+ this.outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
960
+ this.outputService.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
929
961
  if (this.#fileView.hasErrors) {
930
- this.logger.writeError(this.#fileView.getMessages());
962
+ this.outputService.writeError(this.#fileView.getMessages());
931
963
  this.#hasReportedError = true;
932
964
  }
933
965
  this.#fileView.reset();
@@ -980,6 +1012,19 @@ class ThoroughReporter extends Reporter {
980
1012
  }
981
1013
  }
982
1014
 
1015
+ class WatchModeReporter extends Reporter {
1016
+ handleEvent([eventName]) {
1017
+ switch (eventName) {
1018
+ case "run:start":
1019
+ this.outputService.clearTerminal();
1020
+ break;
1021
+ case "run:end":
1022
+ this.outputService.writeMessage(watchModeUsageText());
1023
+ break;
1024
+ }
1025
+ }
1026
+ }
1027
+
983
1028
  class ResultTiming {
984
1029
  end = Date.now();
985
1030
  start = Date.now();
@@ -1079,11 +1124,11 @@ class ResultManager {
1079
1124
  #testResult;
1080
1125
  handleEvent([eventName, payload]) {
1081
1126
  switch (eventName) {
1082
- case "start":
1127
+ case "run:start":
1083
1128
  this.#result = payload.result;
1084
1129
  this.#result.timing.start = Date.now();
1085
1130
  break;
1086
- case "end":
1131
+ case "run:end":
1087
1132
  this.#result.timing.end = Date.now();
1088
1133
  this.#result = undefined;
1089
1134
  break;
@@ -1318,7 +1363,7 @@ class Diagnostic {
1318
1363
  static fromDiagnostics(diagnostics, compiler) {
1319
1364
  return diagnostics.map((diagnostic) => {
1320
1365
  const category = "error";
1321
- const code = `ts(${diagnostic.code})`;
1366
+ const code = `ts(${String(diagnostic.code)})`;
1322
1367
  let origin;
1323
1368
  const text = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
1324
1369
  if (Diagnostic.isTsDiagnosticWithLocation(diagnostic)) {
@@ -1333,30 +1378,20 @@ class Diagnostic {
1333
1378
  }
1334
1379
  static fromError(text, error) {
1335
1380
  const messageText = Array.isArray(text) ? text : [text];
1336
- if (error instanceof Error) {
1337
- if (error.cause != null) {
1338
- messageText.push(this.#normalizeMessage(String(error.cause)));
1339
- }
1340
- messageText.push(this.#normalizeMessage(String(error.message)));
1341
- if (error.stack != null) {
1342
- const stackLines = error.stack
1343
- .split("\n")
1344
- .slice(1)
1345
- .map((line) => line.trimStart());
1346
- messageText.push(...stackLines);
1381
+ if (error instanceof Error && error.stack != null) {
1382
+ if (messageText.length > 1) {
1383
+ messageText.push("");
1347
1384
  }
1385
+ const stackLines = error.stack
1386
+ .split("\n")
1387
+ .map((line) => line.trimStart());
1388
+ messageText.push(...stackLines);
1348
1389
  }
1349
1390
  return Diagnostic.error(messageText);
1350
1391
  }
1351
1392
  static isTsDiagnosticWithLocation(diagnostic) {
1352
1393
  return diagnostic.file != null && diagnostic.start != null && diagnostic.length != null;
1353
1394
  }
1354
- static #normalizeMessage(text) {
1355
- if (text.endsWith(".")) {
1356
- return text;
1357
- }
1358
- return `${text}.`;
1359
- }
1360
1395
  static warning(text, origin) {
1361
1396
  return new Diagnostic(text, "warning", origin);
1362
1397
  }
@@ -1410,10 +1445,8 @@ class TestMember {
1410
1445
  }
1411
1446
  validate() {
1412
1447
  const diagnostics = [];
1413
- const getText = (node) => `'${node.expression.getText()}()' cannot be nested within '${this.node.expression.getText()}()' helper.`;
1448
+ const getText = (node) => `'${node.expression.getText()}()' cannot be nested within '${this.node.expression.getText()}()'.`;
1414
1449
  switch (this.brand) {
1415
- case "expect":
1416
- break;
1417
1450
  case "describe":
1418
1451
  for (const member of this.members) {
1419
1452
  if (member.brand === "expect") {
@@ -1426,6 +1459,7 @@ class TestMember {
1426
1459
  }
1427
1460
  break;
1428
1461
  case "test":
1462
+ case "expect":
1429
1463
  for (const member of this.members) {
1430
1464
  if (member.brand !== "expect") {
1431
1465
  diagnostics.push(Diagnostic.error(getText(member.node), {
@@ -1609,8 +1643,11 @@ class TestTree {
1609
1643
  class CollectService {
1610
1644
  compiler;
1611
1645
  matcherIdentifiers = [
1646
+ "toBe",
1612
1647
  "toBeAny",
1613
1648
  "toBeAssignable",
1649
+ "toBeAssignableTo",
1650
+ "toBeAssignableWith",
1614
1651
  "toBeBigInt",
1615
1652
  "toBeBoolean",
1616
1653
  "toBeNever",
@@ -1659,7 +1696,11 @@ class CollectService {
1659
1696
  if (matcherNode == null || !this.#isMatcherNode(matcherNode)) {
1660
1697
  return;
1661
1698
  }
1662
- parent.members.push(new Assertion(meta.brand, node, parent, meta.flags, matcherNode, modifierNode, notNode));
1699
+ const assertion = new Assertion(meta.brand, node, parent, meta.flags, matcherNode, modifierNode, notNode);
1700
+ parent.members.push(assertion);
1701
+ this.compiler.forEachChild(node, (node) => {
1702
+ this.#collectTestMembers(node, identifiers, assertion);
1703
+ });
1663
1704
  return;
1664
1705
  }
1665
1706
  }
@@ -1726,6 +1767,7 @@ class PrimitiveTypeMatcher {
1726
1767
 
1727
1768
  class RelationMatcherBase {
1728
1769
  typeChecker;
1770
+ relationExplanationVerb = "is";
1729
1771
  constructor(typeChecker) {
1730
1772
  this.typeChecker = typeChecker;
1731
1773
  }
@@ -1733,8 +1775,12 @@ class RelationMatcherBase {
1733
1775
  const sourceTypeText = this.typeChecker.typeToString(sourceType);
1734
1776
  const targetTypeText = this.typeChecker.typeToString(targetType);
1735
1777
  return isNot
1736
- ? [Diagnostic.error(`Type '${targetTypeText}' is ${this.relationExplanationText} type '${sourceTypeText}'.`)]
1737
- : [Diagnostic.error(`Type '${targetTypeText}' is not ${this.relationExplanationText} type '${sourceTypeText}'.`)];
1778
+ ? [
1779
+ Diagnostic.error(`Type '${sourceTypeText}' ${this.relationExplanationVerb} ${this.relationExplanationText} type '${targetTypeText}'.`),
1780
+ ]
1781
+ : [
1782
+ Diagnostic.error(`Type '${sourceTypeText}' ${this.relationExplanationVerb} not ${this.relationExplanationText} type '${targetTypeText}'.`),
1783
+ ];
1738
1784
  }
1739
1785
  match(sourceType, targetType, isNot) {
1740
1786
  const isMatch = this.typeChecker.isTypeRelatedTo(sourceType, targetType, this.relation);
@@ -1745,9 +1791,19 @@ class RelationMatcherBase {
1745
1791
  }
1746
1792
  }
1747
1793
 
1748
- class ToBeAssignable extends RelationMatcherBase {
1794
+ class ToBe extends RelationMatcherBase {
1795
+ relation = this.typeChecker.relation.identity;
1796
+ relationExplanationText = "identical to";
1797
+ }
1798
+
1799
+ class ToBeAssignableTo extends RelationMatcherBase {
1749
1800
  relation = this.typeChecker.relation.assignable;
1750
1801
  relationExplanationText = "assignable to";
1802
+ }
1803
+
1804
+ class ToBeAssignableWith extends RelationMatcherBase {
1805
+ relation = this.typeChecker.relation.assignable;
1806
+ relationExplanationText = "assignable with";
1751
1807
  match(sourceType, targetType, isNot) {
1752
1808
  const isMatch = this.typeChecker.isTypeRelatedTo(targetType, sourceType, this.relation);
1753
1809
  return {
@@ -1757,11 +1813,6 @@ class ToBeAssignable extends RelationMatcherBase {
1757
1813
  }
1758
1814
  }
1759
1815
 
1760
- class ToEqual extends RelationMatcherBase {
1761
- relation = this.typeChecker.relation.identity;
1762
- relationExplanationText = "identical to";
1763
- }
1764
-
1765
1816
  class ToHaveProperty {
1766
1817
  compiler;
1767
1818
  typeChecker;
@@ -1805,7 +1856,8 @@ class ToHaveProperty {
1805
1856
 
1806
1857
  class ToMatch extends RelationMatcherBase {
1807
1858
  relation = this.typeChecker.relation.subtype;
1808
- relationExplanationText = "a subtype of";
1859
+ relationExplanationText = "match";
1860
+ relationExplanationVerb = "does";
1809
1861
  }
1810
1862
 
1811
1863
  class ToRaiseError {
@@ -1825,16 +1877,16 @@ class ToRaiseError {
1825
1877
  Diagnostic.error(`The raised type error${source.diagnostics.length === 1 ? "" : "s"}:`),
1826
1878
  ...Diagnostic.fromDiagnostics(source.diagnostics, this.compiler),
1827
1879
  ];
1828
- const text = `${sourceText} raised ${source.diagnostics.length === 1 ? "a" : source.diagnostics.length} type error${source.diagnostics.length === 1 ? "" : "s"}.`;
1880
+ const text = `${sourceText} raised ${source.diagnostics.length === 1 ? "a" : String(source.diagnostics.length)} type error${source.diagnostics.length === 1 ? "" : "s"}.`;
1829
1881
  return [Diagnostic.error(text).add({ related })];
1830
1882
  }
1831
1883
  if (source.diagnostics.length !== targetTypes.length) {
1832
1884
  const expectedText = source.diagnostics.length > targetTypes.length
1833
- ? `only ${targetTypes.length} type error${targetTypes.length === 1 ? "" : "s"}`
1834
- : `${targetTypes.length} type error${targetTypes.length === 1 ? "" : "s"}`;
1885
+ ? `only ${String(targetTypes.length)} type error${targetTypes.length === 1 ? "" : "s"}`
1886
+ : `${String(targetTypes.length)} type error${targetTypes.length === 1 ? "" : "s"}`;
1835
1887
  const foundText = source.diagnostics.length > targetTypes.length
1836
- ? `${source.diagnostics.length}`
1837
- : `only ${source.diagnostics.length}`;
1888
+ ? String(source.diagnostics.length)
1889
+ : `only ${String(source.diagnostics.length)}`;
1838
1890
  const related = [
1839
1891
  Diagnostic.error(`The raised type error${source.diagnostics.length === 1 ? "" : "s"}:`),
1840
1892
  ...Diagnostic.fromDiagnostics(source.diagnostics, this.compiler),
@@ -1852,7 +1904,7 @@ class ToRaiseError {
1852
1904
  if (!isNot && !isMatch) {
1853
1905
  const expectedText = this.#isStringLiteralType(argument)
1854
1906
  ? `matching substring '${argument.value}'`
1855
- : `with code ${argument.value}`;
1907
+ : `with code ${String(argument.value)}`;
1856
1908
  const related = [
1857
1909
  Diagnostic.error("The raised type error:"),
1858
1910
  ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
@@ -1863,7 +1915,7 @@ class ToRaiseError {
1863
1915
  if (isNot && isMatch) {
1864
1916
  const expectedText = this.#isStringLiteralType(argument)
1865
1917
  ? `matching substring '${argument.value}'`
1866
- : `with code ${argument.value}`;
1918
+ : `with code ${String(argument.value)}`;
1867
1919
  const related = [
1868
1920
  Diagnostic.error("The raised type error:"),
1869
1921
  ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
@@ -1907,8 +1959,11 @@ class ToRaiseError {
1907
1959
  class Expect {
1908
1960
  compiler;
1909
1961
  typeChecker;
1962
+ toBe;
1910
1963
  toBeAny;
1911
1964
  toBeAssignable;
1965
+ toBeAssignableTo;
1966
+ toBeAssignableWith;
1912
1967
  toBeBigInt;
1913
1968
  toBeBoolean;
1914
1969
  toBeNever;
@@ -1927,8 +1982,11 @@ class Expect {
1927
1982
  constructor(compiler, typeChecker) {
1928
1983
  this.compiler = compiler;
1929
1984
  this.typeChecker = typeChecker;
1985
+ this.toBe = new ToBe(this.typeChecker);
1930
1986
  this.toBeAny = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Any);
1931
- this.toBeAssignable = new ToBeAssignable(this.typeChecker);
1987
+ this.toBeAssignable = new ToBeAssignableWith(this.typeChecker);
1988
+ this.toBeAssignableTo = new ToBeAssignableTo(this.typeChecker);
1989
+ this.toBeAssignableWith = new ToBeAssignableWith(this.typeChecker);
1932
1990
  this.toBeBigInt = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.BigInt);
1933
1991
  this.toBeBoolean = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Boolean);
1934
1992
  this.toBeNever = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Never);
@@ -1940,7 +1998,7 @@ class Expect {
1940
1998
  this.toBeUniqueSymbol = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.UniqueESSymbol);
1941
1999
  this.toBeUnknown = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Unknown);
1942
2000
  this.toBeVoid = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Void);
1943
- this.toEqual = new ToEqual(this.typeChecker);
2001
+ this.toEqual = new ToBe(this.typeChecker);
1944
2002
  this.toHaveProperty = new ToHaveProperty(this.compiler, this.typeChecker);
1945
2003
  this.toMatch = new ToMatch(this.typeChecker);
1946
2004
  this.toRaiseError = new ToRaiseError(this.compiler, this.typeChecker);
@@ -1970,6 +2028,10 @@ class Expect {
1970
2028
  switch (matcherNameText) {
1971
2029
  case "toBeAssignable":
1972
2030
  case "toEqual":
2031
+ this.#onDeprecatedMatcher(assertion);
2032
+ case "toBe":
2033
+ case "toBeAssignableTo":
2034
+ case "toBeAssignableWith":
1973
2035
  case "toMatch":
1974
2036
  if (assertion.source[0] == null) {
1975
2037
  this.#onSourceArgumentMustBeProvided(assertion, expectResult);
@@ -2037,6 +2099,22 @@ class Expect {
2037
2099
  return;
2038
2100
  }
2039
2101
  }
2102
+ #onDeprecatedMatcher(assertion) {
2103
+ const matcherNameText = assertion.matcherName.getText();
2104
+ const text = [
2105
+ `'.${matcherNameText}()' is deprecated and will be removed in TSTyche 3.`,
2106
+ "To learn more, visit https://tstyche.org/release-notes/tstyche-2",
2107
+ ];
2108
+ const origin = {
2109
+ end: assertion.matcherName.getEnd(),
2110
+ file: assertion.matcherName.getSourceFile(),
2111
+ start: assertion.matcherName.getStart(),
2112
+ };
2113
+ EventEmitter.dispatch([
2114
+ "deprecation:info",
2115
+ { diagnostics: [Diagnostic.warning(text, origin)] },
2116
+ ]);
2117
+ }
2040
2118
  #onKeyArgumentMustBeOfType(node, expectResult) {
2041
2119
  const receivedTypeText = this.typeChecker.typeToString(this.#getType(node));
2042
2120
  const text = `An argument for 'key' must be of type 'string | number | symbol', received: '${receivedTypeText}'.`;
@@ -2266,19 +2344,19 @@ class ProjectService {
2266
2344
  class TestTreeWorker {
2267
2345
  resolvedConfig;
2268
2346
  compiler;
2347
+ #cancellationToken;
2269
2348
  #expect;
2270
2349
  #fileResult;
2271
2350
  #hasOnly;
2272
2351
  #position;
2273
- #signal;
2274
2352
  constructor(resolvedConfig, compiler, expect, options) {
2275
2353
  this.resolvedConfig = resolvedConfig;
2276
2354
  this.compiler = compiler;
2355
+ this.#cancellationToken = options.cancellationToken;
2277
2356
  this.#expect = expect;
2278
2357
  this.#fileResult = options.fileResult;
2279
2358
  this.#hasOnly = options.hasOnly || resolvedConfig.only != null || options.position != null;
2280
2359
  this.#position = options.position;
2281
- this.#signal = options.signal;
2282
2360
  }
2283
2361
  #resolveRunMode(mode, member) {
2284
2362
  if (member.flags & 1) {
@@ -2302,7 +2380,7 @@ class TestTreeWorker {
2302
2380
  }
2303
2381
  visit(members, runMode, parentResult) {
2304
2382
  for (const member of members) {
2305
- if (this.#signal?.aborted === true) {
2383
+ if (this.#cancellationToken?.isCancellationRequested === true) {
2306
2384
  break;
2307
2385
  }
2308
2386
  const validationError = member.validate();
@@ -2330,6 +2408,7 @@ class TestTreeWorker {
2330
2408
  }
2331
2409
  }
2332
2410
  #visitAssertion(assertion, runMode, parentResult) {
2411
+ this.visit(assertion.members, runMode, parentResult);
2333
2412
  const expectResult = new ExpectResult(assertion, parentResult);
2334
2413
  EventEmitter.dispatch(["expect:start", { result: expectResult }]);
2335
2414
  runMode = this.#resolveRunMode(runMode, assertion);
@@ -2452,8 +2531,8 @@ class TestFileRunner {
2452
2531
  this.#collectService = new CollectService(compiler);
2453
2532
  this.#projectService = new ProjectService(compiler);
2454
2533
  }
2455
- run(testFile, signal) {
2456
- if (signal?.aborted === true) {
2534
+ run(testFile, cancellationToken) {
2535
+ if (cancellationToken?.isCancellationRequested === true) {
2457
2536
  return;
2458
2537
  }
2459
2538
  const testFilePath = fileURLToPath(testFile);
@@ -2461,11 +2540,11 @@ class TestFileRunner {
2461
2540
  this.#projectService.openFile(testFilePath, undefined, this.resolvedConfig.rootPath);
2462
2541
  const fileResult = new FileResult(testFile);
2463
2542
  EventEmitter.dispatch(["file:start", { result: fileResult }]);
2464
- this.#runFile(testFilePath, fileResult, position, signal);
2543
+ this.#runFile(testFilePath, fileResult, position, cancellationToken);
2465
2544
  EventEmitter.dispatch(["file:end", { result: fileResult }]);
2466
2545
  this.#projectService.closeFile(testFilePath);
2467
2546
  }
2468
- #runFile(testFilePath, fileResult, position, signal) {
2547
+ #runFile(testFilePath, fileResult, position, cancellationToken) {
2469
2548
  const languageService = this.#projectService.getLanguageService(testFilePath);
2470
2549
  if (!languageService) {
2471
2550
  return;
@@ -2509,10 +2588,10 @@ class TestFileRunner {
2509
2588
  }
2510
2589
  const expect = new Expect(this.compiler, typeChecker);
2511
2590
  const testTreeWorker = new TestTreeWorker(this.resolvedConfig, this.compiler, expect, {
2591
+ cancellationToken,
2512
2592
  fileResult,
2513
2593
  hasOnly: testTree.hasOnly,
2514
2594
  position,
2515
- signal,
2516
2595
  });
2517
2596
  testTreeWorker.visit(testTree.members, 0, undefined);
2518
2597
  }
@@ -2530,32 +2609,160 @@ class TaskRunner {
2530
2609
  this.#resultManager.handleEvent(event);
2531
2610
  });
2532
2611
  }
2533
- async run(testFiles, target, signal) {
2612
+ async run(testFiles, target, cancellationToken) {
2534
2613
  const result = new Result(this.resolvedConfig, testFiles);
2535
- EventEmitter.dispatch(["start", { result }]);
2614
+ EventEmitter.dispatch(["run:start", { result }]);
2536
2615
  for (const versionTag of target) {
2537
2616
  const targetResult = new TargetResult(versionTag, testFiles);
2538
2617
  EventEmitter.dispatch(["target:start", { result: targetResult }]);
2539
- const compiler = await this.#storeService.load(versionTag, signal);
2618
+ const compiler = await this.#storeService.load(versionTag, cancellationToken);
2540
2619
  if (compiler) {
2541
2620
  const testFileRunner = new TestFileRunner(this.resolvedConfig, compiler);
2542
2621
  for (const testFile of testFiles) {
2543
- testFileRunner.run(testFile, signal);
2622
+ testFileRunner.run(testFile, cancellationToken);
2544
2623
  }
2545
2624
  }
2546
2625
  EventEmitter.dispatch(["target:end", { result: targetResult }]);
2547
2626
  }
2548
- EventEmitter.dispatch(["end", { result }]);
2627
+ EventEmitter.dispatch(["run:end", { result }]);
2549
2628
  return result;
2550
2629
  }
2551
2630
  }
2552
2631
 
2553
- class TSTyche {
2632
+ class CancellationToken {
2633
+ #isCancelled = false;
2634
+ get isCancellationRequested() {
2635
+ return this.#isCancelled;
2636
+ }
2637
+ cancel() {
2638
+ if (!this.#isCancelled) {
2639
+ this.#isCancelled = true;
2640
+ }
2641
+ }
2642
+ }
2643
+
2644
+ class InputService {
2645
+ #stdin;
2646
+ constructor(options) {
2647
+ this.#stdin = options?.stdin ?? process.stdin;
2648
+ this.#stdin.setRawMode?.(true);
2649
+ this.#stdin.setEncoding("utf8");
2650
+ this.#stdin.on("data", (key) => {
2651
+ EventEmitter.dispatch(["input:info", { key }]);
2652
+ });
2653
+ this.#stdin.unref();
2654
+ }
2655
+ dispose() {
2656
+ this.#stdin.setRawMode?.(false);
2657
+ }
2658
+ }
2659
+
2660
+ class Watcher {
2554
2661
  resolvedConfig;
2555
2662
  #abortController = new AbortController();
2663
+ #changedTestFiles = new Set();
2664
+ #inputService;
2665
+ #runCallback;
2666
+ #runChangedDebounced;
2667
+ #selectService;
2668
+ #testFiles;
2669
+ #watcher;
2670
+ constructor(resolvedConfig, runCallback, selectService, testFiles) {
2671
+ this.resolvedConfig = resolvedConfig;
2672
+ this.#inputService = new InputService();
2673
+ this.#runCallback = runCallback;
2674
+ this.#runChangedDebounced = this.#debounce(this.#runChanged, 100);
2675
+ this.#selectService = selectService;
2676
+ this.#testFiles = new Set(testFiles);
2677
+ EventEmitter.addHandler(([eventName, payload]) => {
2678
+ if (eventName === "input:info") {
2679
+ switch (payload.key) {
2680
+ case "\u000D":
2681
+ case "\u0041":
2682
+ case "\u0061":
2683
+ void this.#runAll();
2684
+ break;
2685
+ case "\u0003":
2686
+ case "\u0004":
2687
+ case "\u001B":
2688
+ case "\u0058":
2689
+ case "\u0078":
2690
+ this.#abortController.abort();
2691
+ break;
2692
+ }
2693
+ }
2694
+ });
2695
+ }
2696
+ #debounce(target, delay) {
2697
+ let timeout;
2698
+ return function (...args) {
2699
+ clearTimeout(timeout);
2700
+ timeout = setTimeout(() => {
2701
+ target.apply(this, args);
2702
+ }, delay);
2703
+ };
2704
+ }
2705
+ #onChanged(fileName) {
2706
+ if (this.#testFiles.has(fileName)) {
2707
+ this.#changedTestFiles.add(fileName);
2708
+ return;
2709
+ }
2710
+ if (this.#selectService.isTestFile(fileName)) {
2711
+ this.#changedTestFiles.add(fileName);
2712
+ this.#testFiles.add(fileName);
2713
+ }
2714
+ }
2715
+ #onExit() {
2716
+ this.#inputService.dispose();
2717
+ this.#watcher = undefined;
2718
+ }
2719
+ #onRemoved(fileName) {
2720
+ this.#changedTestFiles.delete(fileName);
2721
+ this.#testFiles.delete(fileName);
2722
+ }
2723
+ async #runAll() {
2724
+ await this.#runCallback([...this.#testFiles].sort());
2725
+ }
2726
+ async #runChanged() {
2727
+ await this.#runCallback([...this.#changedTestFiles].sort());
2728
+ this.#changedTestFiles.clear();
2729
+ }
2730
+ async watch() {
2731
+ this.#watcher = fs.watch(this.resolvedConfig.rootPath, { recursive: true, signal: this.#abortController.signal });
2732
+ await this.#runAll();
2733
+ try {
2734
+ for await (const event of this.#watcher) {
2735
+ if (event.filename != null) {
2736
+ const filePath = Path.resolve(this.resolvedConfig.rootPath, event.filename);
2737
+ if (!existsSync(filePath)) {
2738
+ this.#onRemoved(filePath);
2739
+ continue;
2740
+ }
2741
+ this.#onChanged(filePath);
2742
+ }
2743
+ if (this.#changedTestFiles.size !== 0) {
2744
+ await this.#runChangedDebounced();
2745
+ }
2746
+ }
2747
+ }
2748
+ catch (error) {
2749
+ if (error != null && typeof error === "object" && "name" in error && error.name === "AbortError") ;
2750
+ else {
2751
+ throw error;
2752
+ }
2753
+ }
2754
+ finally {
2755
+ this.#onExit();
2756
+ }
2757
+ }
2758
+ }
2759
+
2760
+ class TSTyche {
2761
+ resolvedConfig;
2762
+ #cancellationToken = new CancellationToken();
2556
2763
  #storeService;
2557
2764
  #taskRunner;
2558
- static version = "1.0.0";
2765
+ static version = "2.0.0-beta.0";
2559
2766
  constructor(resolvedConfig, storeService) {
2560
2767
  this.resolvedConfig = resolvedConfig;
2561
2768
  this.#storeService = storeService;
@@ -2567,17 +2774,27 @@ class TSTyche {
2567
2774
  if (eventName.includes("error") || eventName.includes("fail")) {
2568
2775
  if ("diagnostics" in payload
2569
2776
  && payload.diagnostics.some((diagnostic) => diagnostic.category === "error")) {
2570
- process.exitCode = 1;
2777
+ if (this.resolvedConfig.watch !== true) {
2778
+ process.exitCode = 1;
2779
+ }
2571
2780
  if (this.resolvedConfig.failFast) {
2572
- this.#abortController.abort();
2781
+ this.#cancellationToken.cancel();
2573
2782
  }
2574
2783
  }
2575
2784
  }
2576
2785
  });
2577
- const outputHandlers = [new ThoroughReporter(this.resolvedConfig), new SummaryReporter(this.resolvedConfig)];
2578
- for (const outputHandler of outputHandlers) {
2786
+ const reporters = [
2787
+ new ThoroughReporter(this.resolvedConfig),
2788
+ ];
2789
+ if (this.resolvedConfig.watch === true) {
2790
+ reporters.push(new WatchModeReporter(this.resolvedConfig));
2791
+ }
2792
+ else {
2793
+ reporters.push(new SummaryReporter(this.resolvedConfig));
2794
+ }
2795
+ for (const reporter of reporters) {
2579
2796
  EventEmitter.addHandler((event) => {
2580
- outputHandler.handleEvent(event);
2797
+ reporter.handleEvent(event);
2581
2798
  });
2582
2799
  }
2583
2800
  }
@@ -2593,7 +2810,12 @@ class TSTyche {
2593
2810
  });
2594
2811
  }
2595
2812
  async run(testFiles) {
2596
- await this.#taskRunner.run(this.#normalizePaths(testFiles), this.resolvedConfig.target, this.#abortController.signal);
2813
+ await this.#taskRunner.run(this.#normalizePaths(testFiles), this.resolvedConfig.target, this.#cancellationToken);
2814
+ }
2815
+ async watch(testFiles, selectService) {
2816
+ const runCallback = async (testFiles) => this.run(testFiles);
2817
+ const watcher = new Watcher(this.resolvedConfig, runCallback, selectService, testFiles);
2818
+ await watcher.watch();
2597
2819
  }
2598
2820
  }
2599
2821
 
@@ -2692,6 +2914,12 @@ class OptionDefinitionsMap {
2692
2914
  group: 2,
2693
2915
  name: "version",
2694
2916
  },
2917
+ {
2918
+ brand: "boolean",
2919
+ description: "Watch for changes and rerun related tests files.",
2920
+ group: 2,
2921
+ name: "watch",
2922
+ },
2695
2923
  ];
2696
2924
  static for(optionGroup) {
2697
2925
  const definitionMap = new Map();
@@ -2712,13 +2940,13 @@ class OptionDiagnosticText {
2712
2940
  doubleQuotesExpected() {
2713
2941
  return "String literal with double quotes expected.";
2714
2942
  }
2715
- expectsArgument(optionName) {
2716
- optionName = this.#optionName(optionName);
2717
- return `Option '${optionName}' expects an argument.`;
2718
- }
2719
2943
  expectsListItemType(optionName, optionBrand) {
2720
2944
  return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
2721
2945
  }
2946
+ expectsValue(optionName) {
2947
+ optionName = this.#optionName(optionName);
2948
+ return `Option '${optionName}' expects a value.`;
2949
+ }
2722
2950
  fileDoesNotExist(filePath) {
2723
2951
  return `The specified path '${filePath}' does not exist.`;
2724
2952
  }
@@ -2730,9 +2958,9 @@ class OptionDiagnosticText {
2730
2958
  return optionName;
2731
2959
  }
2732
2960
  }
2733
- requiresArgumentType(optionName, optionBrand) {
2961
+ requiresValueType(optionName, optionBrand) {
2734
2962
  optionName = this.#optionName(optionName);
2735
- return `Option '${optionName}' requires an argument of type ${optionBrand}.`;
2963
+ return `Option '${optionName}' requires a value of type ${optionBrand}.`;
2736
2964
  }
2737
2965
  unknownOption(optionName) {
2738
2966
  return `Unknown option '${optionName}'.`;
@@ -2743,6 +2971,9 @@ class OptionDiagnosticText {
2743
2971
  }
2744
2972
  return `TypeScript version '${value}' is not supported.`;
2745
2973
  }
2974
+ watchCannotBeEnabledInCiEnvironment() {
2975
+ return "The watch mode cannot be enabled in a continuous integration environment.";
2976
+ }
2746
2977
  }
2747
2978
 
2748
2979
  class OptionUsageText {
@@ -2762,7 +2993,7 @@ class OptionUsageText {
2762
2993
  const supportedTagsText = `Supported tags: ${["'", supportedTags.join("', '"), "'"].join("")}.`;
2763
2994
  switch (this.#optionGroup) {
2764
2995
  case 2:
2765
- usageText.push("Argument 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);
2996
+ 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);
2766
2997
  break;
2767
2998
  case 4:
2768
2999
  usageText.push("Item of the 'target' list must be a supported version tag.", supportedTagsText);
@@ -2771,7 +3002,7 @@ class OptionUsageText {
2771
3002
  break;
2772
3003
  }
2773
3004
  default:
2774
- usageText.push(this.#optionDiagnosticText.requiresArgumentType(optionName, optionBrand));
3005
+ usageText.push(this.#optionDiagnosticText.requiresValueType(optionName, optionBrand));
2775
3006
  }
2776
3007
  return usageText;
2777
3008
  }
@@ -2795,18 +3026,26 @@ class OptionValidator {
2795
3026
  case "config":
2796
3027
  case "rootPath":
2797
3028
  if (!existsSync(optionValue)) {
2798
- const text = [this.#optionDiagnosticText.fileDoesNotExist(optionValue)];
2799
- this.#onDiagnostic(Diagnostic.error(text, origin));
3029
+ this.#onDiagnostic(Diagnostic.error([
3030
+ this.#optionDiagnosticText.fileDoesNotExist(optionValue),
3031
+ ], origin));
2800
3032
  }
2801
3033
  break;
2802
3034
  case "target":
2803
- if (!(await this.#storeService.validateTag(optionValue))) {
3035
+ if (await this.#storeService.validateTag(optionValue) === false) {
2804
3036
  this.#onDiagnostic(Diagnostic.error([
2805
3037
  this.#optionDiagnosticText.versionIsNotSupported(optionValue),
2806
3038
  ...(await this.#optionUsageText.get(optionName, optionBrand)),
2807
3039
  ], origin));
2808
3040
  }
2809
3041
  break;
3042
+ case "watch":
3043
+ if (Environment.isCi) {
3044
+ this.#onDiagnostic(Diagnostic.error([
3045
+ this.#optionDiagnosticText.watchCannotBeEnabledInCiEnvironment(),
3046
+ ], origin));
3047
+ }
3048
+ break;
2810
3049
  }
2811
3050
  }
2812
3051
  }
@@ -2830,9 +3069,9 @@ class CommandLineOptionsWorker {
2830
3069
  this.#optionUsageText = new OptionUsageText(2, this.#storeService);
2831
3070
  this.#optionValidator = new OptionValidator(2, this.#storeService, this.#onDiagnostic);
2832
3071
  }
2833
- async #onExpectsArgumentDiagnostic(optionDefinition) {
3072
+ async #onExpectsValue(optionDefinition) {
2834
3073
  const text = [
2835
- this.#optionDiagnosticText.expectsArgument(optionDefinition.name),
3074
+ this.#optionDiagnosticText.expectsValue(optionDefinition.name),
2836
3075
  ...(await this.#optionUsageText.get(optionDefinition.name, optionDefinition.brand)),
2837
3076
  ];
2838
3077
  this.#onDiagnostic(Diagnostic.error(text));
@@ -2868,13 +3107,14 @@ class CommandLineOptionsWorker {
2868
3107
  this.#commandLineOptions[optionDefinition.name] = true;
2869
3108
  break;
2870
3109
  case "boolean":
3110
+ await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
2871
3111
  this.#commandLineOptions[optionDefinition.name] = optionValue !== "false";
2872
3112
  if (optionValue === "false" || optionValue === "true") {
2873
3113
  index++;
2874
3114
  }
2875
3115
  break;
2876
3116
  case "list":
2877
- if (optionValue != null) {
3117
+ if (optionValue !== "") {
2878
3118
  const optionValues = optionValue
2879
3119
  .split(",")
2880
3120
  .map((value) => value.trim())
@@ -2886,10 +3126,10 @@ class CommandLineOptionsWorker {
2886
3126
  index++;
2887
3127
  break;
2888
3128
  }
2889
- await this.#onExpectsArgumentDiagnostic(optionDefinition);
3129
+ await this.#onExpectsValue(optionDefinition);
2890
3130
  break;
2891
3131
  case "string":
2892
- if (optionValue != null) {
3132
+ if (optionValue !== "") {
2893
3133
  if (optionDefinition.name === "config") {
2894
3134
  optionValue = Path.resolve(optionValue);
2895
3135
  }
@@ -2898,16 +3138,13 @@ class CommandLineOptionsWorker {
2898
3138
  index++;
2899
3139
  break;
2900
3140
  }
2901
- await this.#onExpectsArgumentDiagnostic(optionDefinition);
3141
+ await this.#onExpectsValue(optionDefinition);
2902
3142
  break;
2903
3143
  }
2904
3144
  return index;
2905
3145
  }
2906
- #resolveOptionValue(optionValue) {
2907
- if (optionValue == null || optionValue.startsWith("-")) {
2908
- return;
2909
- }
2910
- return optionValue;
3146
+ #resolveOptionValue(target = "") {
3147
+ return target.startsWith("-") ? "" : target;
2911
3148
  }
2912
3149
  }
2913
3150
 
@@ -3032,7 +3269,7 @@ class ConfigFileOptionsWorker {
3032
3269
  };
3033
3270
  const text = isListItem
3034
3271
  ? this.#optionDiagnosticText.expectsListItemType(optionDefinition.name, optionDefinition.brand)
3035
- : this.#optionDiagnosticText.requiresArgumentType(optionDefinition.name, optionDefinition.brand);
3272
+ : this.#optionDiagnosticText.requiresValueType(optionDefinition.name, optionDefinition.brand);
3036
3273
  this.#onDiagnostic(Diagnostic.error(text, origin));
3037
3274
  return;
3038
3275
  }
@@ -3105,9 +3342,9 @@ class ConfigService {
3105
3342
  static get defaultOptions() {
3106
3343
  return ConfigService.#defaultOptions;
3107
3344
  }
3108
- #onDiagnostic = (diagnostic) => {
3345
+ #onDiagnostic(diagnostic) {
3109
3346
  EventEmitter.dispatch(["config:error", { diagnostics: [diagnostic] }]);
3110
- };
3347
+ }
3111
3348
  async parseCommandLine(commandLineArgs) {
3112
3349
  this.#commandLineOptions = {};
3113
3350
  this.#pathMatch = [];
@@ -3137,28 +3374,6 @@ class ConfigService {
3137
3374
  };
3138
3375
  return mergedOptions;
3139
3376
  }
3140
- selectTestFiles() {
3141
- const { pathMatch, rootPath, testFileMatch } = this.resolveConfig();
3142
- let testFilePaths = this.compiler.sys.readDirectory(rootPath, ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"], undefined, testFileMatch);
3143
- if (pathMatch.length > 0) {
3144
- testFilePaths = testFilePaths.filter((testFilePath) => pathMatch.some((match) => {
3145
- const relativeTestFilePath = Path.relative("", testFilePath);
3146
- return relativeTestFilePath.toLowerCase().includes(match.toLowerCase());
3147
- }));
3148
- }
3149
- if (testFilePaths.length === 0) {
3150
- const text = [
3151
- "No test files were selected using current configuration.",
3152
- `Root path: ${rootPath}`,
3153
- `Test file match: ${testFileMatch.join(", ")}`,
3154
- ];
3155
- if (pathMatch.length > 0) {
3156
- text.push(`Path match: ${pathMatch.join(", ")}`);
3157
- }
3158
- this.#onDiagnostic(Diagnostic.error(text));
3159
- }
3160
- return testFilePaths;
3161
- }
3162
3377
  }
3163
3378
 
3164
3379
  var OptionBrand;
@@ -3175,57 +3390,169 @@ var OptionGroup;
3175
3390
  OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
3176
3391
  })(OptionGroup || (OptionGroup = {}));
3177
3392
 
3393
+ class GlobPattern {
3394
+ static #reservedCharacterRegex = /[^\w\s/]/g;
3395
+ static #parse(pattern, usageTarget) {
3396
+ const segments = pattern.split("/");
3397
+ let resultPattern = "\\.";
3398
+ let optionalSegmentCount = 0;
3399
+ for (const segment of segments) {
3400
+ if (segment === "**") {
3401
+ resultPattern += "(\\/(?!(node_modules)(\\/|$))[^./][^/]*)*?";
3402
+ continue;
3403
+ }
3404
+ if (usageTarget === "directories") {
3405
+ resultPattern += "(";
3406
+ optionalSegmentCount++;
3407
+ }
3408
+ resultPattern += `\\/`;
3409
+ const segmentPattern = segment.replace(GlobPattern.#reservedCharacterRegex, GlobPattern.#replaceReservedCharacter);
3410
+ if (segmentPattern !== segment) {
3411
+ resultPattern += "(?!(node_modules)(\\/|$))";
3412
+ }
3413
+ resultPattern += segmentPattern;
3414
+ }
3415
+ resultPattern += ")?".repeat(optionalSegmentCount);
3416
+ return resultPattern;
3417
+ }
3418
+ static #replaceReservedCharacter(match, offset) {
3419
+ switch (match) {
3420
+ case "*":
3421
+ return (offset === 0) ? "([^./][^/]*)?" : "([^/]*)?";
3422
+ case "?":
3423
+ return (offset === 0) ? "[^./]" : "[^/]";
3424
+ default:
3425
+ return `\\${match}`;
3426
+ }
3427
+ }
3428
+ static toRegex(patterns, usageTarget) {
3429
+ const patternText = patterns.map((pattern) => `(${GlobPattern.#parse(pattern, usageTarget)})`).join("|");
3430
+ return new RegExp(`^(${patternText})$`);
3431
+ }
3432
+ }
3433
+
3434
+ class SelectService {
3435
+ resolvedConfig;
3436
+ #includeDirectoryRegex;
3437
+ #includeFileRegex;
3438
+ constructor(resolvedConfig) {
3439
+ this.resolvedConfig = resolvedConfig;
3440
+ this.#includeDirectoryRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "directories");
3441
+ this.#includeFileRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "files");
3442
+ }
3443
+ #isDirectoryIncluded(directoryPath) {
3444
+ return this.#includeDirectoryRegex.test(directoryPath);
3445
+ }
3446
+ #isFileIncluded(filePath) {
3447
+ if (this.resolvedConfig.pathMatch.length > 0
3448
+ && !this.resolvedConfig.pathMatch.some((match) => filePath.toLowerCase().includes(match.toLowerCase()))) {
3449
+ return false;
3450
+ }
3451
+ return this.#includeFileRegex.test(filePath);
3452
+ }
3453
+ isTestFile(filePath) {
3454
+ return this.#isFileIncluded(Path.relative(this.resolvedConfig.rootPath, filePath));
3455
+ }
3456
+ #onDiagnostic(diagnostic) {
3457
+ EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
3458
+ }
3459
+ async selectFiles() {
3460
+ const currentPath = ".";
3461
+ const testFilePaths = [];
3462
+ await this.#visitDirectory(currentPath, testFilePaths);
3463
+ if (testFilePaths.length === 0) {
3464
+ const text = [
3465
+ "No test files were selected using current configuration.",
3466
+ `Root path: ${this.resolvedConfig.rootPath}`,
3467
+ `Test file match: ${this.resolvedConfig.testFileMatch.join(", ")}`,
3468
+ ];
3469
+ if (this.resolvedConfig.pathMatch.length > 0) {
3470
+ text.push(`Path match: ${this.resolvedConfig.pathMatch.join(", ")}`);
3471
+ }
3472
+ this.#onDiagnostic(Diagnostic.error(text));
3473
+ }
3474
+ return testFilePaths.sort();
3475
+ }
3476
+ async #visitDirectory(currentPath, testFilePaths) {
3477
+ const targetPath = Path.join(this.resolvedConfig.rootPath, currentPath);
3478
+ let entries = [];
3479
+ try {
3480
+ entries = await fs.readdir(targetPath, { withFileTypes: true });
3481
+ }
3482
+ catch {
3483
+ }
3484
+ for (const entry of entries) {
3485
+ let entryMeta;
3486
+ if (entry.isSymbolicLink()) {
3487
+ try {
3488
+ entryMeta = await fs.stat([targetPath, entry.name].join("/"));
3489
+ }
3490
+ catch {
3491
+ continue;
3492
+ }
3493
+ }
3494
+ else {
3495
+ entryMeta = entry;
3496
+ }
3497
+ const entryPath = [currentPath, entry.name].join("/");
3498
+ if (entryMeta.isDirectory() && this.#isDirectoryIncluded(entryPath)) {
3499
+ await this.#visitDirectory(entryPath, testFilePaths);
3500
+ continue;
3501
+ }
3502
+ if (entryMeta.isFile() && this.#isFileIncluded(entryPath)) {
3503
+ testFilePaths.push([targetPath, entry.name].join("/"));
3504
+ }
3505
+ }
3506
+ }
3507
+ }
3508
+
3178
3509
  class ManifestWorker {
3179
3510
  #manifestFileName = "store-manifest.json";
3180
3511
  #manifestFilePath;
3181
3512
  #onDiagnostic;
3182
- #prune;
3183
3513
  #registryUrl = new URL("https://registry.npmjs.org");
3184
3514
  #storePath;
3185
3515
  #timeout = Environment.timeout * 1000;
3186
3516
  #version = "1";
3187
- constructor(storePath, onDiagnostic, prune) {
3517
+ constructor(storePath, onDiagnostic) {
3188
3518
  this.#storePath = storePath;
3189
3519
  this.#onDiagnostic = onDiagnostic;
3190
3520
  this.#manifestFilePath = Path.join(storePath, this.#manifestFileName);
3191
- this.#prune = prune;
3192
3521
  }
3193
- async #create(signal) {
3194
- const manifest = await this.#load(signal);
3522
+ async #create() {
3523
+ const manifest = await this.#load();
3195
3524
  if (manifest != null) {
3196
3525
  await this.persist(manifest);
3197
3526
  }
3198
3527
  return manifest;
3199
3528
  }
3200
- async #fetch(signal) {
3529
+ async #fetch() {
3201
3530
  return new Promise((resolve, reject) => {
3202
3531
  const request = https.get(new URL("typescript", this.#registryUrl), {
3203
3532
  headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*" },
3204
- signal,
3205
- }, (result) => {
3206
- if (result.statusCode !== 200) {
3207
- reject(new Error(`Request failed with status code ${String(result.statusCode)}.`));
3208
- result.resume();
3533
+ timeout: this.#timeout,
3534
+ }, (response) => {
3535
+ if (response.statusCode !== 200) {
3536
+ reject(new Error(`Request failed with status code ${String(response.statusCode)}.`));
3537
+ response.resume();
3209
3538
  return;
3210
3539
  }
3211
- result.setEncoding("utf8");
3540
+ response.setEncoding("utf8");
3212
3541
  let rawData = "";
3213
- result.on("data", (chunk) => {
3542
+ response.on("data", (chunk) => {
3214
3543
  rawData += chunk;
3215
3544
  });
3216
- result.on("end", () => {
3217
- try {
3218
- const packageMetadata = JSON.parse(rawData);
3219
- resolve(packageMetadata);
3220
- }
3221
- catch (error) {
3222
- reject(error);
3223
- }
3545
+ response.on("end", () => {
3546
+ const packageMetadata = JSON.parse(rawData);
3547
+ resolve(packageMetadata);
3224
3548
  });
3225
3549
  });
3226
3550
  request.on("error", (error) => {
3227
3551
  reject(error);
3228
3552
  });
3553
+ request.on("timeout", () => {
3554
+ request.destroy();
3555
+ });
3229
3556
  });
3230
3557
  }
3231
3558
  isOutdated(manifest, ageTolerance = 0) {
@@ -3234,7 +3561,7 @@ class ManifestWorker {
3234
3561
  }
3235
3562
  return false;
3236
3563
  }
3237
- async #load(signal, options = { quite: false }) {
3564
+ async #load(options) {
3238
3565
  const manifest = {
3239
3566
  $version: this.#version,
3240
3567
  lastUpdated: Date.now(),
@@ -3242,27 +3569,21 @@ class ManifestWorker {
3242
3569
  versions: [],
3243
3570
  };
3244
3571
  let packageMetadata;
3245
- const abortController = new AbortController();
3246
- const timeoutSignal = AbortSignal.timeout(this.#timeout);
3247
- timeoutSignal.addEventListener("abort", () => {
3248
- abortController.abort(`Setup timeout of ${this.#timeout / 1000}s was exceeded.`);
3249
- }, { once: true });
3250
- signal?.addEventListener("abort", () => {
3251
- abortController.abort("Fetch got canceled by request.");
3252
- }, { once: true });
3253
3572
  try {
3254
- packageMetadata = await this.#fetch(abortController.signal);
3573
+ packageMetadata = await this.#fetch();
3255
3574
  }
3256
3575
  catch (error) {
3257
- if (!options.quite) {
3258
- const text = [`Failed to fetch metadata of the 'typescript' package from '${this.#registryUrl.toString()}'.`];
3259
- if (error instanceof Error && error.name !== "AbortError") {
3260
- text.push("Might be there is an issue with the registry or the network connection.");
3261
- }
3262
- this.#onDiagnostic(Diagnostic.fromError(text, error));
3576
+ if (options?.quite === true) {
3577
+ return;
3263
3578
  }
3264
- }
3265
- if (!packageMetadata) {
3579
+ const text = [`Failed to fetch metadata of the 'typescript' package from '${this.#registryUrl.toString()}'.`];
3580
+ if (error instanceof Error && "code" in error && error.code === "ECONNRESET") {
3581
+ text.push(`Setup timeout of ${String(this.#timeout / 1000)}s was exceeded.`);
3582
+ }
3583
+ else {
3584
+ text.push("Might be there is an issue with the registry or the network connection.");
3585
+ }
3586
+ this.#onDiagnostic(Diagnostic.fromError(text, error));
3266
3587
  return;
3267
3588
  }
3268
3589
  manifest.versions = Object.keys(packageMetadata.versions)
@@ -3283,10 +3604,10 @@ class ManifestWorker {
3283
3604
  }
3284
3605
  return manifest;
3285
3606
  }
3286
- async open(signal, options) {
3607
+ async open(options) {
3287
3608
  let manifest;
3288
3609
  if (!existsSync(this.#manifestFilePath)) {
3289
- return this.#create(signal);
3610
+ return this.#create();
3290
3611
  }
3291
3612
  let manifestText;
3292
3613
  try {
@@ -3304,12 +3625,12 @@ class ManifestWorker {
3304
3625
  catch {
3305
3626
  }
3306
3627
  if (manifest == null || manifest.$version !== this.#version) {
3307
- await this.#prune();
3308
- return this.#create(signal);
3628
+ await fs.rm(this.#storePath, { force: true, recursive: true });
3629
+ return this.#create();
3309
3630
  }
3310
3631
  if (this.isOutdated(manifest) || options?.refresh === true) {
3311
3632
  const quite = options?.refresh !== true;
3312
- const freshManifest = await this.#load(signal, { quite });
3633
+ const freshManifest = await this.#load({ quite });
3313
3634
  if (freshManifest != null) {
3314
3635
  await this.persist(freshManifest);
3315
3636
  return freshManifest;
@@ -3348,11 +3669,11 @@ class Lock {
3348
3669
  }
3349
3670
  const waitStartTime = Date.now();
3350
3671
  while (isLocked) {
3351
- if (options.signal?.aborted === true) {
3672
+ if (options.cancellationToken?.isCancellationRequested === true) {
3352
3673
  break;
3353
3674
  }
3354
3675
  if (Date.now() - waitStartTime > options.timeout) {
3355
- options.onDiagnostic?.(`Lock wait timeout of ${options.timeout / 1000}s was exceeded.`);
3676
+ options.onDiagnostic?.(`Lock wait timeout of ${String(options.timeout / 1000)}s was exceeded.`);
3356
3677
  break;
3357
3678
  }
3358
3679
  await Lock.#sleep(1000);
@@ -3377,51 +3698,55 @@ class PackageInstaller {
3377
3698
  this.#storePath = storePath;
3378
3699
  this.#onDiagnostic = onDiagnostic;
3379
3700
  }
3380
- async ensure(compilerVersion, signal) {
3701
+ async ensure(compilerVersion, cancellationToken) {
3381
3702
  const installationPath = Path.join(this.#storePath, compilerVersion);
3382
3703
  const readyFilePath = Path.join(installationPath, this.#readyFileName);
3704
+ const modulePath = Path.join(installationPath, "node_modules", "typescript", "lib", "typescript.js");
3705
+ if (existsSync(readyFilePath)) {
3706
+ return modulePath;
3707
+ }
3383
3708
  if (await Lock.isLocked(installationPath, {
3709
+ cancellationToken,
3384
3710
  onDiagnostic: (text) => {
3385
3711
  this.#onDiagnostic(Diagnostic.error([`Failed to install 'typescript@${compilerVersion}'.`, text]));
3386
3712
  },
3387
- signal,
3388
3713
  timeout: this.#timeout,
3389
3714
  })) {
3390
3715
  return;
3391
3716
  }
3392
- if (!existsSync(readyFilePath)) {
3393
- EventEmitter.dispatch(["store:info", { compilerVersion, installationPath }]);
3394
- try {
3395
- await fs.mkdir(installationPath, { recursive: true });
3396
- const lock = new Lock(installationPath);
3397
- const packageJson = {
3398
- name: "tstyche-typescript",
3399
- version: compilerVersion,
3400
- description: "Do not change. This package was generated by TSTyche",
3401
- private: true,
3402
- license: "MIT",
3403
- dependencies: {
3404
- typescript: compilerVersion,
3405
- },
3406
- };
3407
- await fs.writeFile(Path.join(installationPath, "package.json"), JSON.stringify(packageJson, null, 2));
3408
- await this.#install(installationPath, signal);
3409
- await fs.writeFile(readyFilePath, "");
3410
- lock.release();
3411
- }
3412
- catch (error) {
3413
- this.#onDiagnostic(Diagnostic.fromError(`Failed to install 'typescript@${compilerVersion}'.`, error));
3414
- }
3717
+ const lock = new Lock(installationPath);
3718
+ EventEmitter.dispatch(["store:info", { compilerVersion, installationPath }]);
3719
+ try {
3720
+ await fs.mkdir(installationPath, { recursive: true });
3721
+ const packageJson = {
3722
+ name: "tstyche-typescript",
3723
+ version: compilerVersion,
3724
+ description: "Do not change. This package was generated by TSTyche",
3725
+ private: true,
3726
+ license: "MIT",
3727
+ dependencies: {
3728
+ typescript: compilerVersion,
3729
+ },
3730
+ };
3731
+ await fs.writeFile(Path.join(installationPath, "package.json"), JSON.stringify(packageJson, null, 2));
3732
+ await this.#install(installationPath);
3733
+ await fs.writeFile(readyFilePath, "");
3734
+ return modulePath;
3735
+ }
3736
+ catch (error) {
3737
+ this.#onDiagnostic(Diagnostic.fromError(`Failed to install 'typescript@${compilerVersion}'.`, error));
3738
+ }
3739
+ finally {
3740
+ lock.release();
3415
3741
  }
3416
- return Path.join(installationPath, "node_modules", "typescript", "lib", "typescript.js");
3742
+ return;
3417
3743
  }
3418
- async #install(cwd, signal) {
3744
+ async #install(cwd) {
3419
3745
  const args = ["install", "--ignore-scripts", "--no-bin-links", "--no-package-lock"];
3420
3746
  return new Promise((resolve, reject) => {
3421
3747
  const spawnedNpm = spawn("npm", args, {
3422
3748
  cwd,
3423
3749
  shell: true,
3424
- signal,
3425
3750
  stdio: "ignore",
3426
3751
  timeout: this.#timeout,
3427
3752
  });
@@ -3433,9 +3758,9 @@ class PackageInstaller {
3433
3758
  resolve();
3434
3759
  }
3435
3760
  if (signal != null) {
3436
- reject(new Error(`Setup timeout of ${this.#timeout / 1000}s was exceeded.`));
3761
+ reject(new Error(`setup timeout of ${String(this.#timeout / 1000)}s was exceeded`));
3437
3762
  }
3438
- reject(new Error(`Process exited with code ${String(code)}.`));
3763
+ reject(new Error(`process exited with code ${String(code)}`));
3439
3764
  });
3440
3765
  });
3441
3766
  }
@@ -3450,27 +3775,27 @@ class StoreService {
3450
3775
  constructor() {
3451
3776
  this.#storePath = Environment.storePath;
3452
3777
  this.#packageInstaller = new PackageInstaller(this.#storePath, this.#onDiagnostic);
3453
- this.#manifestWorker = new ManifestWorker(this.#storePath, this.#onDiagnostic, this.prune);
3778
+ this.#manifestWorker = new ManifestWorker(this.#storePath, this.#onDiagnostic);
3454
3779
  }
3455
- async getSupportedTags(signal) {
3456
- await this.open(signal);
3780
+ async getSupportedTags() {
3781
+ await this.open();
3457
3782
  if (!this.#manifest) {
3458
3783
  return [];
3459
3784
  }
3460
3785
  return [...Object.keys(this.#manifest.resolutions), ...this.#manifest.versions, "current"].sort();
3461
3786
  }
3462
- async install(tag, signal) {
3787
+ async install(tag, cancellationToken) {
3463
3788
  if (tag === "current") {
3464
3789
  return;
3465
3790
  }
3466
- const version = await this.resolveTag(tag, signal);
3791
+ const version = await this.resolveTag(tag);
3467
3792
  if (version == null) {
3468
3793
  this.#onDiagnostic(Diagnostic.error(`Cannot add the 'typescript' package for the '${tag}' tag.`));
3469
3794
  return;
3470
3795
  }
3471
- return this.#packageInstaller.ensure(version, signal);
3796
+ return this.#packageInstaller.ensure(version, cancellationToken);
3472
3797
  }
3473
- async load(tag, signal) {
3798
+ async load(tag, cancellationToken) {
3474
3799
  let compilerInstance = this.#compilerInstanceCache.get(tag);
3475
3800
  if (compilerInstance != null) {
3476
3801
  return compilerInstance;
@@ -3480,7 +3805,7 @@ class StoreService {
3480
3805
  modulePath = Environment.typescriptPath;
3481
3806
  }
3482
3807
  else {
3483
- const version = await this.resolveTag(tag, signal);
3808
+ const version = await this.resolveTag(tag);
3484
3809
  if (version == null) {
3485
3810
  this.#onDiagnostic(Diagnostic.error(`Cannot add the 'typescript' package for the '${tag}' tag.`));
3486
3811
  return;
@@ -3489,7 +3814,7 @@ class StoreService {
3489
3814
  if (compilerInstance != null) {
3490
3815
  return compilerInstance;
3491
3816
  }
3492
- modulePath = await this.#packageInstaller.ensure(version, signal);
3817
+ modulePath = await this.#packageInstaller.ensure(version, cancellationToken);
3493
3818
  }
3494
3819
  if (modulePath != null) {
3495
3820
  compilerInstance = await this.#loadModule(modulePath);
@@ -3518,23 +3843,20 @@ class StoreService {
3518
3843
  }
3519
3844
  return module.exports;
3520
3845
  }
3521
- #onDiagnostic = (diagnostic) => {
3846
+ #onDiagnostic(diagnostic) {
3522
3847
  EventEmitter.dispatch(["store:error", { diagnostics: [diagnostic] }]);
3523
- };
3524
- async open(signal) {
3848
+ }
3849
+ async open() {
3525
3850
  if (this.#manifest) {
3526
3851
  return;
3527
3852
  }
3528
- this.#manifest = await this.#manifestWorker.open(signal);
3853
+ this.#manifest = await this.#manifestWorker.open();
3529
3854
  }
3530
- prune = async () => {
3531
- await fs.rm(this.#storePath, { force: true, recursive: true });
3532
- };
3533
- async resolveTag(tag, signal) {
3855
+ async resolveTag(tag) {
3534
3856
  if (tag === "current") {
3535
3857
  return tag;
3536
3858
  }
3537
- await this.open(signal);
3859
+ await this.open();
3538
3860
  if (!this.#manifest) {
3539
3861
  return;
3540
3862
  }
@@ -3543,16 +3865,16 @@ class StoreService {
3543
3865
  }
3544
3866
  return this.#manifest.resolutions[tag];
3545
3867
  }
3546
- async update(signal) {
3547
- await this.#manifestWorker.open(signal, { refresh: true });
3868
+ async update() {
3869
+ await this.#manifestWorker.open({ refresh: true });
3548
3870
  }
3549
- async validateTag(tag, signal) {
3871
+ async validateTag(tag) {
3550
3872
  if (tag === "current") {
3551
3873
  return Environment.typescriptPath != null;
3552
3874
  }
3553
- await this.open(signal);
3875
+ await this.open();
3554
3876
  if (!this.#manifest) {
3555
- return false;
3877
+ return undefined;
3556
3878
  }
3557
3879
  if (this.#manifestWorker.isOutdated(this.#manifest, 60)
3558
3880
  && (!Version.isVersionTag(tag)
@@ -3568,27 +3890,30 @@ class StoreService {
3568
3890
  }
3569
3891
 
3570
3892
  class Cli {
3571
- #logger;
3893
+ #cancellationToken = new CancellationToken();
3894
+ #outputService;
3572
3895
  #storeService;
3573
3896
  constructor() {
3574
- this.#logger = new Logger();
3897
+ this.#outputService = new OutputService();
3575
3898
  this.#storeService = new StoreService();
3576
3899
  }
3577
3900
  #onStartupEvent = ([eventName, payload]) => {
3578
3901
  switch (eventName) {
3579
3902
  case "store:info":
3580
- this.#logger.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
3903
+ this.#outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
3581
3904
  break;
3582
3905
  case "config:error":
3906
+ case "select:error":
3583
3907
  case "store:error":
3584
3908
  for (const diagnostic of payload.diagnostics) {
3585
3909
  switch (diagnostic.category) {
3586
3910
  case "error":
3911
+ this.#cancellationToken.cancel();
3912
+ this.#outputService.writeError(diagnosticText(diagnostic));
3587
3913
  process.exitCode = 1;
3588
- this.#logger.writeError(diagnosticText(diagnostic));
3589
3914
  break;
3590
3915
  case "warning":
3591
- this.#logger.writeWarning(diagnosticText(diagnostic));
3916
+ this.#outputService.writeWarning(diagnosticText(diagnostic));
3592
3917
  break;
3593
3918
  }
3594
3919
  }
@@ -3599,15 +3924,15 @@ class Cli {
3599
3924
  EventEmitter.addHandler(this.#onStartupEvent);
3600
3925
  if (commandLineArguments.includes("--help")) {
3601
3926
  const commandLineOptionDefinitions = OptionDefinitionsMap.for(2);
3602
- this.#logger.writeMessage(helpText(commandLineOptionDefinitions, TSTyche.version));
3927
+ this.#outputService.writeMessage(helpText(commandLineOptionDefinitions, TSTyche.version));
3603
3928
  return;
3604
3929
  }
3605
3930
  if (commandLineArguments.includes("--prune")) {
3606
- await this.#storeService.prune();
3931
+ await fs.rm(Environment.storePath, { force: true, recursive: true });
3607
3932
  return;
3608
3933
  }
3609
3934
  if (commandLineArguments.includes("--version")) {
3610
- this.#logger.writeMessage(formattedText(TSTyche.version));
3935
+ this.#outputService.writeMessage(formattedText(TSTyche.version));
3611
3936
  return;
3612
3937
  }
3613
3938
  if (commandLineArguments.includes("--update")) {
@@ -3620,16 +3945,16 @@ class Cli {
3620
3945
  }
3621
3946
  const configService = new ConfigService(compiler, this.#storeService);
3622
3947
  await configService.parseCommandLine(commandLineArguments);
3623
- if (process.exitCode === 1) {
3948
+ if (this.#cancellationToken.isCancellationRequested) {
3624
3949
  return;
3625
3950
  }
3626
3951
  await configService.readConfigFile();
3627
- if (process.exitCode === 1) {
3952
+ if (this.#cancellationToken.isCancellationRequested) {
3628
3953
  return;
3629
3954
  }
3630
3955
  const resolvedConfig = configService.resolveConfig();
3631
- if (configService.commandLineOptions.showConfig === true) {
3632
- this.#logger.writeMessage(formattedText({
3956
+ if (commandLineArguments.includes("--showConfig")) {
3957
+ this.#outputService.writeMessage(formattedText({
3633
3958
  noColor: Environment.noColor,
3634
3959
  noInteractive: Environment.noInteractive,
3635
3960
  storePath: Environment.storePath,
@@ -3639,27 +3964,33 @@ class Cli {
3639
3964
  }));
3640
3965
  return;
3641
3966
  }
3642
- if (configService.commandLineOptions.install === true) {
3967
+ if (commandLineArguments.includes("--install")) {
3643
3968
  for (const tag of resolvedConfig.target) {
3644
3969
  await this.#storeService.install(tag);
3645
3970
  }
3646
3971
  return;
3647
3972
  }
3973
+ const selectService = new SelectService(resolvedConfig);
3648
3974
  let testFiles = [];
3649
3975
  if (resolvedConfig.testFileMatch.length !== 0) {
3650
- testFiles = configService.selectTestFiles();
3976
+ testFiles = await selectService.selectFiles();
3651
3977
  if (testFiles.length === 0) {
3652
3978
  return;
3653
3979
  }
3654
- if (configService.commandLineOptions.listFiles === true) {
3655
- this.#logger.writeMessage(formattedText(testFiles));
3980
+ if (commandLineArguments.includes("--listFiles")) {
3981
+ this.#outputService.writeMessage(formattedText(testFiles));
3656
3982
  return;
3657
3983
  }
3658
3984
  }
3659
3985
  EventEmitter.removeHandler(this.#onStartupEvent);
3660
3986
  const tstyche = new TSTyche(resolvedConfig, this.#storeService);
3661
- await tstyche.run(testFiles);
3987
+ if (resolvedConfig.watch === true) {
3988
+ await tstyche.watch(testFiles, selectService);
3989
+ }
3990
+ else {
3991
+ await tstyche.run(testFiles);
3992
+ }
3662
3993
  }
3663
3994
  }
3664
3995
 
3665
- export { Assertion, Cli, CollectService, Color, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, Environment, EventEmitter, Expect, ExpectResult, FileResult, Line, Logger, OptionBrand, OptionDefinitionsMap, OptionGroup, Path, ProjectResult, ProjectService, Reporter, Result, ResultCount, ResultManager, ResultStatus, ResultTiming, Scribbler, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, Text, ThoroughReporter, Version, addsPackageStepText, describeNameText, diagnosticText, fileStatusText, fileViewText, formattedText, helpText, summaryText, testNameText, usesCompilerStepText };
3996
+ export { Assertion, CancellationToken, Cli, CollectService, Color, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, Environment, EventEmitter, Expect, ExpectResult, FileResult, InputService, Line, OptionBrand, OptionDefinitionsMap, OptionGroup, OutputService, Path, ProjectResult, ProjectService, Reporter, Result, ResultCount, ResultManager, ResultStatus, ResultTiming, Scribbler, SelectService, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, Text, ThoroughReporter, Version, WatchModeReporter, Watcher, addsPackageStepText, describeNameText, diagnosticText, fileStatusText, fileViewText, formattedText, helpText, summaryText, testNameText, usesCompilerStepText, watchModeUsageText };