tstyche 2.0.0-rc.2 → 2.0.0

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