tstyche 1.1.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.replace(/\\/g, "/");
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 = Path.normalizeSlashes(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.replace(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);
@@ -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)) {
@@ -1400,10 +1445,8 @@ class TestMember {
1400
1445
  }
1401
1446
  validate() {
1402
1447
  const diagnostics = [];
1403
- 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()}()'.`;
1404
1449
  switch (this.brand) {
1405
- case "expect":
1406
- break;
1407
1450
  case "describe":
1408
1451
  for (const member of this.members) {
1409
1452
  if (member.brand === "expect") {
@@ -1416,6 +1459,7 @@ class TestMember {
1416
1459
  }
1417
1460
  break;
1418
1461
  case "test":
1462
+ case "expect":
1419
1463
  for (const member of this.members) {
1420
1464
  if (member.brand !== "expect") {
1421
1465
  diagnostics.push(Diagnostic.error(getText(member.node), {
@@ -1599,8 +1643,11 @@ class TestTree {
1599
1643
  class CollectService {
1600
1644
  compiler;
1601
1645
  matcherIdentifiers = [
1646
+ "toBe",
1602
1647
  "toBeAny",
1603
1648
  "toBeAssignable",
1649
+ "toBeAssignableTo",
1650
+ "toBeAssignableWith",
1604
1651
  "toBeBigInt",
1605
1652
  "toBeBoolean",
1606
1653
  "toBeNever",
@@ -1649,7 +1696,11 @@ class CollectService {
1649
1696
  if (matcherNode == null || !this.#isMatcherNode(matcherNode)) {
1650
1697
  return;
1651
1698
  }
1652
- 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
+ });
1653
1704
  return;
1654
1705
  }
1655
1706
  }
@@ -1716,6 +1767,7 @@ class PrimitiveTypeMatcher {
1716
1767
 
1717
1768
  class RelationMatcherBase {
1718
1769
  typeChecker;
1770
+ relationExplanationVerb = "is";
1719
1771
  constructor(typeChecker) {
1720
1772
  this.typeChecker = typeChecker;
1721
1773
  }
@@ -1723,8 +1775,12 @@ class RelationMatcherBase {
1723
1775
  const sourceTypeText = this.typeChecker.typeToString(sourceType);
1724
1776
  const targetTypeText = this.typeChecker.typeToString(targetType);
1725
1777
  return isNot
1726
- ? [Diagnostic.error(`Type '${targetTypeText}' is ${this.relationExplanationText} type '${sourceTypeText}'.`)]
1727
- : [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
+ ];
1728
1784
  }
1729
1785
  match(sourceType, targetType, isNot) {
1730
1786
  const isMatch = this.typeChecker.isTypeRelatedTo(sourceType, targetType, this.relation);
@@ -1735,9 +1791,19 @@ class RelationMatcherBase {
1735
1791
  }
1736
1792
  }
1737
1793
 
1738
- 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 {
1739
1800
  relation = this.typeChecker.relation.assignable;
1740
1801
  relationExplanationText = "assignable to";
1802
+ }
1803
+
1804
+ class ToBeAssignableWith extends RelationMatcherBase {
1805
+ relation = this.typeChecker.relation.assignable;
1806
+ relationExplanationText = "assignable with";
1741
1807
  match(sourceType, targetType, isNot) {
1742
1808
  const isMatch = this.typeChecker.isTypeRelatedTo(targetType, sourceType, this.relation);
1743
1809
  return {
@@ -1747,11 +1813,6 @@ class ToBeAssignable extends RelationMatcherBase {
1747
1813
  }
1748
1814
  }
1749
1815
 
1750
- class ToEqual extends RelationMatcherBase {
1751
- relation = this.typeChecker.relation.identity;
1752
- relationExplanationText = "identical to";
1753
- }
1754
-
1755
1816
  class ToHaveProperty {
1756
1817
  compiler;
1757
1818
  typeChecker;
@@ -1795,7 +1856,8 @@ class ToHaveProperty {
1795
1856
 
1796
1857
  class ToMatch extends RelationMatcherBase {
1797
1858
  relation = this.typeChecker.relation.subtype;
1798
- relationExplanationText = "a subtype of";
1859
+ relationExplanationText = "match";
1860
+ relationExplanationVerb = "does";
1799
1861
  }
1800
1862
 
1801
1863
  class ToRaiseError {
@@ -1815,16 +1877,16 @@ class ToRaiseError {
1815
1877
  Diagnostic.error(`The raised type error${source.diagnostics.length === 1 ? "" : "s"}:`),
1816
1878
  ...Diagnostic.fromDiagnostics(source.diagnostics, this.compiler),
1817
1879
  ];
1818
- 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"}.`;
1819
1881
  return [Diagnostic.error(text).add({ related })];
1820
1882
  }
1821
1883
  if (source.diagnostics.length !== targetTypes.length) {
1822
1884
  const expectedText = source.diagnostics.length > targetTypes.length
1823
- ? `only ${targetTypes.length} type error${targetTypes.length === 1 ? "" : "s"}`
1824
- : `${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"}`;
1825
1887
  const foundText = source.diagnostics.length > targetTypes.length
1826
- ? `${source.diagnostics.length}`
1827
- : `only ${source.diagnostics.length}`;
1888
+ ? String(source.diagnostics.length)
1889
+ : `only ${String(source.diagnostics.length)}`;
1828
1890
  const related = [
1829
1891
  Diagnostic.error(`The raised type error${source.diagnostics.length === 1 ? "" : "s"}:`),
1830
1892
  ...Diagnostic.fromDiagnostics(source.diagnostics, this.compiler),
@@ -1842,7 +1904,7 @@ class ToRaiseError {
1842
1904
  if (!isNot && !isMatch) {
1843
1905
  const expectedText = this.#isStringLiteralType(argument)
1844
1906
  ? `matching substring '${argument.value}'`
1845
- : `with code ${argument.value}`;
1907
+ : `with code ${String(argument.value)}`;
1846
1908
  const related = [
1847
1909
  Diagnostic.error("The raised type error:"),
1848
1910
  ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
@@ -1853,7 +1915,7 @@ class ToRaiseError {
1853
1915
  if (isNot && isMatch) {
1854
1916
  const expectedText = this.#isStringLiteralType(argument)
1855
1917
  ? `matching substring '${argument.value}'`
1856
- : `with code ${argument.value}`;
1918
+ : `with code ${String(argument.value)}`;
1857
1919
  const related = [
1858
1920
  Diagnostic.error("The raised type error:"),
1859
1921
  ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
@@ -1897,8 +1959,11 @@ class ToRaiseError {
1897
1959
  class Expect {
1898
1960
  compiler;
1899
1961
  typeChecker;
1962
+ toBe;
1900
1963
  toBeAny;
1901
1964
  toBeAssignable;
1965
+ toBeAssignableTo;
1966
+ toBeAssignableWith;
1902
1967
  toBeBigInt;
1903
1968
  toBeBoolean;
1904
1969
  toBeNever;
@@ -1917,8 +1982,11 @@ class Expect {
1917
1982
  constructor(compiler, typeChecker) {
1918
1983
  this.compiler = compiler;
1919
1984
  this.typeChecker = typeChecker;
1985
+ this.toBe = new ToBe(this.typeChecker);
1920
1986
  this.toBeAny = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Any);
1921
- 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);
1922
1990
  this.toBeBigInt = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.BigInt);
1923
1991
  this.toBeBoolean = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Boolean);
1924
1992
  this.toBeNever = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Never);
@@ -1930,7 +1998,7 @@ class Expect {
1930
1998
  this.toBeUniqueSymbol = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.UniqueESSymbol);
1931
1999
  this.toBeUnknown = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Unknown);
1932
2000
  this.toBeVoid = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Void);
1933
- this.toEqual = new ToEqual(this.typeChecker);
2001
+ this.toEqual = new ToBe(this.typeChecker);
1934
2002
  this.toHaveProperty = new ToHaveProperty(this.compiler, this.typeChecker);
1935
2003
  this.toMatch = new ToMatch(this.typeChecker);
1936
2004
  this.toRaiseError = new ToRaiseError(this.compiler, this.typeChecker);
@@ -1960,6 +2028,10 @@ class Expect {
1960
2028
  switch (matcherNameText) {
1961
2029
  case "toBeAssignable":
1962
2030
  case "toEqual":
2031
+ this.#onDeprecatedMatcher(assertion);
2032
+ case "toBe":
2033
+ case "toBeAssignableTo":
2034
+ case "toBeAssignableWith":
1963
2035
  case "toMatch":
1964
2036
  if (assertion.source[0] == null) {
1965
2037
  this.#onSourceArgumentMustBeProvided(assertion, expectResult);
@@ -2027,6 +2099,22 @@ class Expect {
2027
2099
  return;
2028
2100
  }
2029
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
+ }
2030
2118
  #onKeyArgumentMustBeOfType(node, expectResult) {
2031
2119
  const receivedTypeText = this.typeChecker.typeToString(this.#getType(node));
2032
2120
  const text = `An argument for 'key' must be of type 'string | number | symbol', received: '${receivedTypeText}'.`;
@@ -2320,6 +2408,7 @@ class TestTreeWorker {
2320
2408
  }
2321
2409
  }
2322
2410
  #visitAssertion(assertion, runMode, parentResult) {
2411
+ this.visit(assertion.members, runMode, parentResult);
2323
2412
  const expectResult = new ExpectResult(assertion, parentResult);
2324
2413
  EventEmitter.dispatch(["expect:start", { result: expectResult }]);
2325
2414
  runMode = this.#resolveRunMode(runMode, assertion);
@@ -2522,7 +2611,7 @@ class TaskRunner {
2522
2611
  }
2523
2612
  async run(testFiles, target, cancellationToken) {
2524
2613
  const result = new Result(this.resolvedConfig, testFiles);
2525
- EventEmitter.dispatch(["start", { result }]);
2614
+ EventEmitter.dispatch(["run:start", { result }]);
2526
2615
  for (const versionTag of target) {
2527
2616
  const targetResult = new TargetResult(versionTag, testFiles);
2528
2617
  EventEmitter.dispatch(["target:start", { result: targetResult }]);
@@ -2535,7 +2624,7 @@ class TaskRunner {
2535
2624
  }
2536
2625
  EventEmitter.dispatch(["target:end", { result: targetResult }]);
2537
2626
  }
2538
- EventEmitter.dispatch(["end", { result }]);
2627
+ EventEmitter.dispatch(["run:end", { result }]);
2539
2628
  return result;
2540
2629
  }
2541
2630
  }
@@ -2552,12 +2641,128 @@ class CancellationToken {
2552
2641
  }
2553
2642
  }
2554
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 {
2661
+ resolvedConfig;
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
+
2555
2760
  class TSTyche {
2556
2761
  resolvedConfig;
2557
2762
  #cancellationToken = new CancellationToken();
2558
2763
  #storeService;
2559
2764
  #taskRunner;
2560
- static version = "1.1.0";
2765
+ static version = "2.0.0-beta.0";
2561
2766
  constructor(resolvedConfig, storeService) {
2562
2767
  this.resolvedConfig = resolvedConfig;
2563
2768
  this.#storeService = storeService;
@@ -2569,17 +2774,27 @@ class TSTyche {
2569
2774
  if (eventName.includes("error") || eventName.includes("fail")) {
2570
2775
  if ("diagnostics" in payload
2571
2776
  && payload.diagnostics.some((diagnostic) => diagnostic.category === "error")) {
2572
- process.exitCode = 1;
2777
+ if (this.resolvedConfig.watch !== true) {
2778
+ process.exitCode = 1;
2779
+ }
2573
2780
  if (this.resolvedConfig.failFast) {
2574
2781
  this.#cancellationToken.cancel();
2575
2782
  }
2576
2783
  }
2577
2784
  }
2578
2785
  });
2579
- const outputHandlers = [new ThoroughReporter(this.resolvedConfig), new SummaryReporter(this.resolvedConfig)];
2580
- 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) {
2581
2796
  EventEmitter.addHandler((event) => {
2582
- outputHandler.handleEvent(event);
2797
+ reporter.handleEvent(event);
2583
2798
  });
2584
2799
  }
2585
2800
  }
@@ -2597,6 +2812,11 @@ class TSTyche {
2597
2812
  async run(testFiles) {
2598
2813
  await this.#taskRunner.run(this.#normalizePaths(testFiles), this.resolvedConfig.target, this.#cancellationToken);
2599
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();
2819
+ }
2600
2820
  }
2601
2821
 
2602
2822
  class OptionDefinitionsMap {
@@ -2694,6 +2914,12 @@ class OptionDefinitionsMap {
2694
2914
  group: 2,
2695
2915
  name: "version",
2696
2916
  },
2917
+ {
2918
+ brand: "boolean",
2919
+ description: "Watch for changes and rerun related tests files.",
2920
+ group: 2,
2921
+ name: "watch",
2922
+ },
2697
2923
  ];
2698
2924
  static for(optionGroup) {
2699
2925
  const definitionMap = new Map();
@@ -2714,13 +2940,13 @@ class OptionDiagnosticText {
2714
2940
  doubleQuotesExpected() {
2715
2941
  return "String literal with double quotes expected.";
2716
2942
  }
2717
- expectsArgument(optionName) {
2718
- optionName = this.#optionName(optionName);
2719
- return `Option '${optionName}' expects an argument.`;
2720
- }
2721
2943
  expectsListItemType(optionName, optionBrand) {
2722
2944
  return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
2723
2945
  }
2946
+ expectsValue(optionName) {
2947
+ optionName = this.#optionName(optionName);
2948
+ return `Option '${optionName}' expects a value.`;
2949
+ }
2724
2950
  fileDoesNotExist(filePath) {
2725
2951
  return `The specified path '${filePath}' does not exist.`;
2726
2952
  }
@@ -2732,9 +2958,9 @@ class OptionDiagnosticText {
2732
2958
  return optionName;
2733
2959
  }
2734
2960
  }
2735
- requiresArgumentType(optionName, optionBrand) {
2961
+ requiresValueType(optionName, optionBrand) {
2736
2962
  optionName = this.#optionName(optionName);
2737
- return `Option '${optionName}' requires an argument of type ${optionBrand}.`;
2963
+ return `Option '${optionName}' requires a value of type ${optionBrand}.`;
2738
2964
  }
2739
2965
  unknownOption(optionName) {
2740
2966
  return `Unknown option '${optionName}'.`;
@@ -2745,6 +2971,9 @@ class OptionDiagnosticText {
2745
2971
  }
2746
2972
  return `TypeScript version '${value}' is not supported.`;
2747
2973
  }
2974
+ watchCannotBeEnabledInCiEnvironment() {
2975
+ return "The watch mode cannot be enabled in a continuous integration environment.";
2976
+ }
2748
2977
  }
2749
2978
 
2750
2979
  class OptionUsageText {
@@ -2764,7 +2993,7 @@ class OptionUsageText {
2764
2993
  const supportedTagsText = `Supported tags: ${["'", supportedTags.join("', '"), "'"].join("")}.`;
2765
2994
  switch (this.#optionGroup) {
2766
2995
  case 2:
2767
- 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);
2768
2997
  break;
2769
2998
  case 4:
2770
2999
  usageText.push("Item of the 'target' list must be a supported version tag.", supportedTagsText);
@@ -2773,7 +3002,7 @@ class OptionUsageText {
2773
3002
  break;
2774
3003
  }
2775
3004
  default:
2776
- usageText.push(this.#optionDiagnosticText.requiresArgumentType(optionName, optionBrand));
3005
+ usageText.push(this.#optionDiagnosticText.requiresValueType(optionName, optionBrand));
2777
3006
  }
2778
3007
  return usageText;
2779
3008
  }
@@ -2797,8 +3026,9 @@ class OptionValidator {
2797
3026
  case "config":
2798
3027
  case "rootPath":
2799
3028
  if (!existsSync(optionValue)) {
2800
- const text = [this.#optionDiagnosticText.fileDoesNotExist(optionValue)];
2801
- this.#onDiagnostic(Diagnostic.error(text, origin));
3029
+ this.#onDiagnostic(Diagnostic.error([
3030
+ this.#optionDiagnosticText.fileDoesNotExist(optionValue),
3031
+ ], origin));
2802
3032
  }
2803
3033
  break;
2804
3034
  case "target":
@@ -2809,6 +3039,13 @@ class OptionValidator {
2809
3039
  ], origin));
2810
3040
  }
2811
3041
  break;
3042
+ case "watch":
3043
+ if (Environment.isCi) {
3044
+ this.#onDiagnostic(Diagnostic.error([
3045
+ this.#optionDiagnosticText.watchCannotBeEnabledInCiEnvironment(),
3046
+ ], origin));
3047
+ }
3048
+ break;
2812
3049
  }
2813
3050
  }
2814
3051
  }
@@ -2832,9 +3069,9 @@ class CommandLineOptionsWorker {
2832
3069
  this.#optionUsageText = new OptionUsageText(2, this.#storeService);
2833
3070
  this.#optionValidator = new OptionValidator(2, this.#storeService, this.#onDiagnostic);
2834
3071
  }
2835
- async #onExpectsArgumentDiagnostic(optionDefinition) {
3072
+ async #onExpectsValue(optionDefinition) {
2836
3073
  const text = [
2837
- this.#optionDiagnosticText.expectsArgument(optionDefinition.name),
3074
+ this.#optionDiagnosticText.expectsValue(optionDefinition.name),
2838
3075
  ...(await this.#optionUsageText.get(optionDefinition.name, optionDefinition.brand)),
2839
3076
  ];
2840
3077
  this.#onDiagnostic(Diagnostic.error(text));
@@ -2870,13 +3107,14 @@ class CommandLineOptionsWorker {
2870
3107
  this.#commandLineOptions[optionDefinition.name] = true;
2871
3108
  break;
2872
3109
  case "boolean":
3110
+ await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
2873
3111
  this.#commandLineOptions[optionDefinition.name] = optionValue !== "false";
2874
3112
  if (optionValue === "false" || optionValue === "true") {
2875
3113
  index++;
2876
3114
  }
2877
3115
  break;
2878
3116
  case "list":
2879
- if (optionValue != null) {
3117
+ if (optionValue !== "") {
2880
3118
  const optionValues = optionValue
2881
3119
  .split(",")
2882
3120
  .map((value) => value.trim())
@@ -2888,10 +3126,10 @@ class CommandLineOptionsWorker {
2888
3126
  index++;
2889
3127
  break;
2890
3128
  }
2891
- await this.#onExpectsArgumentDiagnostic(optionDefinition);
3129
+ await this.#onExpectsValue(optionDefinition);
2892
3130
  break;
2893
3131
  case "string":
2894
- if (optionValue != null) {
3132
+ if (optionValue !== "") {
2895
3133
  if (optionDefinition.name === "config") {
2896
3134
  optionValue = Path.resolve(optionValue);
2897
3135
  }
@@ -2900,16 +3138,13 @@ class CommandLineOptionsWorker {
2900
3138
  index++;
2901
3139
  break;
2902
3140
  }
2903
- await this.#onExpectsArgumentDiagnostic(optionDefinition);
3141
+ await this.#onExpectsValue(optionDefinition);
2904
3142
  break;
2905
3143
  }
2906
3144
  return index;
2907
3145
  }
2908
- #resolveOptionValue(optionValue) {
2909
- if (optionValue == null || optionValue.startsWith("-")) {
2910
- return;
2911
- }
2912
- return optionValue;
3146
+ #resolveOptionValue(target = "") {
3147
+ return target.startsWith("-") ? "" : target;
2913
3148
  }
2914
3149
  }
2915
3150
 
@@ -3034,7 +3269,7 @@ class ConfigFileOptionsWorker {
3034
3269
  };
3035
3270
  const text = isListItem
3036
3271
  ? this.#optionDiagnosticText.expectsListItemType(optionDefinition.name, optionDefinition.brand)
3037
- : this.#optionDiagnosticText.requiresArgumentType(optionDefinition.name, optionDefinition.brand);
3272
+ : this.#optionDiagnosticText.requiresValueType(optionDefinition.name, optionDefinition.brand);
3038
3273
  this.#onDiagnostic(Diagnostic.error(text, origin));
3039
3274
  return;
3040
3275
  }
@@ -3107,9 +3342,9 @@ class ConfigService {
3107
3342
  static get defaultOptions() {
3108
3343
  return ConfigService.#defaultOptions;
3109
3344
  }
3110
- #onDiagnostic = (diagnostic) => {
3345
+ #onDiagnostic(diagnostic) {
3111
3346
  EventEmitter.dispatch(["config:error", { diagnostics: [diagnostic] }]);
3112
- };
3347
+ }
3113
3348
  async parseCommandLine(commandLineArgs) {
3114
3349
  this.#commandLineOptions = {};
3115
3350
  this.#pathMatch = [];
@@ -3139,28 +3374,6 @@ class ConfigService {
3139
3374
  };
3140
3375
  return mergedOptions;
3141
3376
  }
3142
- selectTestFiles() {
3143
- const { pathMatch, rootPath, testFileMatch } = this.resolveConfig();
3144
- let testFilePaths = this.compiler.sys.readDirectory(rootPath, ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"], undefined, testFileMatch);
3145
- if (pathMatch.length > 0) {
3146
- testFilePaths = testFilePaths.filter((testFilePath) => pathMatch.some((match) => {
3147
- const relativeTestFilePath = Path.relative("", testFilePath);
3148
- return relativeTestFilePath.toLowerCase().includes(match.toLowerCase());
3149
- }));
3150
- }
3151
- if (testFilePaths.length === 0) {
3152
- const text = [
3153
- "No test files were selected using current configuration.",
3154
- `Root path: ${rootPath}`,
3155
- `Test file match: ${testFileMatch.join(", ")}`,
3156
- ];
3157
- if (pathMatch.length > 0) {
3158
- text.push(`Path match: ${pathMatch.join(", ")}`);
3159
- }
3160
- this.#onDiagnostic(Diagnostic.error(text));
3161
- }
3162
- return testFilePaths;
3163
- }
3164
3377
  }
3165
3378
 
3166
3379
  var OptionBrand;
@@ -3177,20 +3390,134 @@ var OptionGroup;
3177
3390
  OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
3178
3391
  })(OptionGroup || (OptionGroup = {}));
3179
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
+
3180
3509
  class ManifestWorker {
3181
3510
  #manifestFileName = "store-manifest.json";
3182
3511
  #manifestFilePath;
3183
3512
  #onDiagnostic;
3184
- #prune;
3185
3513
  #registryUrl = new URL("https://registry.npmjs.org");
3186
3514
  #storePath;
3187
3515
  #timeout = Environment.timeout * 1000;
3188
3516
  #version = "1";
3189
- constructor(storePath, onDiagnostic, prune) {
3517
+ constructor(storePath, onDiagnostic) {
3190
3518
  this.#storePath = storePath;
3191
3519
  this.#onDiagnostic = onDiagnostic;
3192
3520
  this.#manifestFilePath = Path.join(storePath, this.#manifestFileName);
3193
- this.#prune = prune;
3194
3521
  }
3195
3522
  async #create() {
3196
3523
  const manifest = await this.#load();
@@ -3251,7 +3578,7 @@ class ManifestWorker {
3251
3578
  }
3252
3579
  const text = [`Failed to fetch metadata of the 'typescript' package from '${this.#registryUrl.toString()}'.`];
3253
3580
  if (error instanceof Error && "code" in error && error.code === "ECONNRESET") {
3254
- text.push(`Setup timeout of ${this.#timeout / 1000}s was exceeded.`);
3581
+ text.push(`Setup timeout of ${String(this.#timeout / 1000)}s was exceeded.`);
3255
3582
  }
3256
3583
  else {
3257
3584
  text.push("Might be there is an issue with the registry or the network connection.");
@@ -3298,7 +3625,7 @@ class ManifestWorker {
3298
3625
  catch {
3299
3626
  }
3300
3627
  if (manifest == null || manifest.$version !== this.#version) {
3301
- await this.#prune();
3628
+ await fs.rm(this.#storePath, { force: true, recursive: true });
3302
3629
  return this.#create();
3303
3630
  }
3304
3631
  if (this.isOutdated(manifest) || options?.refresh === true) {
@@ -3346,7 +3673,7 @@ class Lock {
3346
3673
  break;
3347
3674
  }
3348
3675
  if (Date.now() - waitStartTime > options.timeout) {
3349
- 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.`);
3350
3677
  break;
3351
3678
  }
3352
3679
  await Lock.#sleep(1000);
@@ -3431,7 +3758,7 @@ class PackageInstaller {
3431
3758
  resolve();
3432
3759
  }
3433
3760
  if (signal != null) {
3434
- 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`));
3435
3762
  }
3436
3763
  reject(new Error(`process exited with code ${String(code)}`));
3437
3764
  });
@@ -3448,7 +3775,7 @@ class StoreService {
3448
3775
  constructor() {
3449
3776
  this.#storePath = Environment.storePath;
3450
3777
  this.#packageInstaller = new PackageInstaller(this.#storePath, this.#onDiagnostic);
3451
- this.#manifestWorker = new ManifestWorker(this.#storePath, this.#onDiagnostic, this.prune);
3778
+ this.#manifestWorker = new ManifestWorker(this.#storePath, this.#onDiagnostic);
3452
3779
  }
3453
3780
  async getSupportedTags() {
3454
3781
  await this.open();
@@ -3516,18 +3843,15 @@ class StoreService {
3516
3843
  }
3517
3844
  return module.exports;
3518
3845
  }
3519
- #onDiagnostic = (diagnostic) => {
3846
+ #onDiagnostic(diagnostic) {
3520
3847
  EventEmitter.dispatch(["store:error", { diagnostics: [diagnostic] }]);
3521
- };
3848
+ }
3522
3849
  async open() {
3523
3850
  if (this.#manifest) {
3524
3851
  return;
3525
3852
  }
3526
3853
  this.#manifest = await this.#manifestWorker.open();
3527
3854
  }
3528
- prune = async () => {
3529
- await fs.rm(this.#storePath, { force: true, recursive: true });
3530
- };
3531
3855
  async resolveTag(tag) {
3532
3856
  if (tag === "current") {
3533
3857
  return tag;
@@ -3567,28 +3891,29 @@ class StoreService {
3567
3891
 
3568
3892
  class Cli {
3569
3893
  #cancellationToken = new CancellationToken();
3570
- #logger;
3894
+ #outputService;
3571
3895
  #storeService;
3572
3896
  constructor() {
3573
- this.#logger = new Logger();
3897
+ this.#outputService = new OutputService();
3574
3898
  this.#storeService = new StoreService();
3575
3899
  }
3576
3900
  #onStartupEvent = ([eventName, payload]) => {
3577
3901
  switch (eventName) {
3578
3902
  case "store:info":
3579
- this.#logger.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
3903
+ this.#outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
3580
3904
  break;
3581
3905
  case "config:error":
3906
+ case "select:error":
3582
3907
  case "store:error":
3583
3908
  for (const diagnostic of payload.diagnostics) {
3584
3909
  switch (diagnostic.category) {
3585
3910
  case "error":
3586
3911
  this.#cancellationToken.cancel();
3587
- this.#logger.writeError(diagnosticText(diagnostic));
3912
+ this.#outputService.writeError(diagnosticText(diagnostic));
3588
3913
  process.exitCode = 1;
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")) {
@@ -3628,8 +3953,8 @@ class Cli {
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, CancellationToken, 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 };