tstyche
Version:
The Essential Type Testing Tool.
1,391 lines (1,354 loc) • 141 kB
JavaScript
import process from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import { existsSync, writeFileSync, rmSync } from 'node:fs';
import fs from 'node:fs/promises';
import vm from 'node:vm';
import https from 'node:https';
import { spawn } from 'node:child_process';
class EventEmitter {
static #handlers = new Set();
static addHandler(handler) {
EventEmitter.#handlers.add(handler);
}
static dispatch(event) {
for (const handler of EventEmitter.#handlers) {
handler(event);
}
}
static removeHandler(handler) {
EventEmitter.#handlers.delete(handler);
}
}
class Path {
static dirname(filePath) {
return Path.normalizeSlashes(path.dirname(filePath));
}
static join(...filePaths) {
return Path.normalizeSlashes(path.join(...filePaths));
}
static normalizeSlashes(filePath) {
if (path.sep === "/") {
return filePath;
}
return filePath.replaceAll("\\", "/");
}
static relative(from, to) {
let relativePath = path.relative(from, to);
if (!relativePath.startsWith(".")) {
relativePath = `./${relativePath}`;
}
return Path.normalizeSlashes(relativePath);
}
static resolve(...filePaths) {
return Path.normalizeSlashes(path.resolve(...filePaths));
}
}
class Environment {
static #noColor = Environment.#resolveNoColor();
static #noInteractive = Environment.#resolveNoInteractive();
static #storePath = Environment.#resolveStorePath();
static #timeout = Environment.#resolveTimeout();
static #typescriptPath = Environment.#resolveTypeScriptPath();
static get noColor() {
return Environment.#noColor;
}
static get noInteractive() {
return Environment.#noInteractive;
}
static get storePath() {
return Environment.#storePath;
}
static get timeout() {
return Environment.#timeout;
}
static get typescriptPath() {
return Environment.#typescriptPath;
}
static #resolveNoColor() {
if (process.env["TSTYCHE_NO_COLOR"] != null) {
return process.env["TSTYCHE_NO_COLOR"] !== "";
}
if (process.env["NO_COLOR"] != null) {
return process.env["NO_COLOR"] !== "";
}
return false;
}
static #resolveNoInteractive() {
if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) {
return process.env["TSTYCHE_NO_INTERACTIVE"] !== "";
}
return !process.stdout.isTTY;
}
static #resolveStorePath() {
if (process.env["TSTYCHE_STORE_PATH"] != null) {
return Path.resolve(process.env["TSTYCHE_STORE_PATH"]);
}
if (process.platform === "darwin") {
return Path.resolve(os.homedir(), "Library", "TSTyche");
}
if (process.env["LocalAppData"] != null) {
return Path.resolve(process.env["LocalAppData"], "TSTyche");
}
if (process.env["XDG_DATA_HOME"] != null) {
return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
}
return Path.resolve(os.homedir(), ".local", "share", "TSTyche");
}
static #resolveTimeout() {
if (process.env["TSTYCHE_TIMEOUT"] != null) {
return Number(process.env["TSTYCHE_TIMEOUT"]);
}
return 30;
}
static #resolveTypeScriptPath() {
let moduleId = "typescript";
if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) {
moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"];
}
let resolvedPath;
try {
resolvedPath = createRequire(import.meta.url).resolve(moduleId);
}
catch {
}
return resolvedPath;
}
}
var Color;
(function (Color) {
Color["Reset"] = "0";
Color["Red"] = "31";
Color["Green"] = "32";
Color["Yellow"] = "33";
Color["Blue"] = "34";
Color["Magenta"] = "35";
Color["Cyan"] = "36";
Color["Gray"] = "90";
})(Color || (Color = {}));
class Scribbler {
#newLine;
#noColor;
constructor(options) {
this.#newLine = options?.newLine ?? "\n";
this.#noColor = options?.noColor ?? false;
}
static createElement(type, props, ...children) {
return {
$$typeof: Symbol.for("tstyche:scribbler"),
children: children.length > 1 ? children : children[0],
props,
type,
};
}
#escapeSequence(attributes) {
return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
}
#indentEachLine(lines, level) {
const indentStep = " ";
const notEmptyLineRegExp = /^(?!$)/gm;
return lines.replaceAll(notEmptyLineRegExp, indentStep.repeat(level));
}
render(element) {
if (element != null) {
if (typeof element.type === "function") {
const instance = new element.type({
...element.props,
children: element.children,
});
return this.render(instance.render());
}
if (element.type === "ansi" && !this.#noColor) {
const flags = typeof element.props?.["escapes"] === "string" || Array.isArray(element.props?.["escapes"])
? element.props["escapes"]
: undefined;
if (flags != null) {
return this.#escapeSequence(flags);
}
}
if (element.type === "newLine") {
return this.#newLine;
}
if (element.type === "text") {
const indentLevel = typeof element.props?.["indent"] === "number" ? element.props["indent"] : 0;
let text = this.#visitElementChildren(element.children);
if (indentLevel > 0) {
text = this.#indentEachLine(text, indentLevel);
}
return text;
}
}
return "";
}
#visitElementChildren(children) {
if (typeof children === "string") {
return children;
}
if (Array.isArray(children)) {
const text = [];
for (const child of children) {
if (typeof child === "string") {
text.push(child);
continue;
}
if (Array.isArray(child)) {
text.push(this.#visitElementChildren(child));
continue;
}
if (child != null && typeof child === "object") {
text.push(this.render(child));
continue;
}
}
return text.join("");
}
if (children != null && typeof children === "object") {
return this.render(children);
}
return "";
}
}
class Text {
props;
constructor(props) {
this.props = props;
}
render() {
const ansiEscapes = [];
if (this.props.color != null) {
ansiEscapes.push(this.props.color);
}
return (Scribbler.createElement("text", { indent: this.props.indent },
ansiEscapes.length > 0 ? Scribbler.createElement("ansi", { escapes: ansiEscapes }) : undefined,
this.props.children,
ansiEscapes.length > 0 ? Scribbler.createElement("ansi", { escapes: "0" }) : undefined));
}
}
class Line {
props;
constructor(props) {
this.props = props;
}
render() {
return (Scribbler.createElement("text", null,
Scribbler.createElement(Text, { color: this.props.color, indent: this.props.indent }, this.props.children),
Scribbler.createElement("newLine", null)));
}
}
class Logger {
#noColor;
#scribbler;
#stderr;
#stdout;
constructor(options) {
this.#noColor = options?.noColor ?? Environment.noColor;
this.#stderr = options?.stderr ?? process.stderr;
this.#stdout = options?.stdout ?? process.stdout;
this.#scribbler = new Scribbler({ noColor: this.#noColor });
}
eraseLastLine() {
this.#stdout.write("\u001B[1A\u001B[0K");
}
#write(stream, body) {
const elements = Array.isArray(body) ? body : [body];
for (const element of elements) {
if (element.$$typeof !== Symbol.for("tstyche:scribbler")) {
return;
}
stream.write(this.#scribbler.render(element));
}
}
writeError(body) {
this.#write(this.#stderr, body);
}
writeMessage(body) {
this.#write(this.#stdout, body);
}
writeWarning(body) {
this.#write(this.#stderr, body);
}
}
class Reporter {
resolvedConfig;
logger;
constructor(resolvedConfig) {
this.resolvedConfig = resolvedConfig;
this.logger = new Logger();
}
}
function addsPackageStepText(compilerVersion, installationPath) {
return (Scribbler.createElement(Line, null,
Scribbler.createElement(Text, { color: "90" }, "adds"),
" TypeScript ",
compilerVersion,
Scribbler.createElement(Text, { color: "90" },
" to ",
installationPath)));
}
function describeNameText(name, indent = 0) {
return Scribbler.createElement(Line, { indent: indent + 1 }, name);
}
class CodeSpanText {
props;
constructor(props) {
this.props = props;
}
render() {
const lastLineInFile = this.props.file.getLineAndCharacterOfPosition(this.props.file.text.length).line;
const { character: markedCharacter, line: markedLine } = this.props.file.getLineAndCharacterOfPosition(this.props.start);
const firstLine = Math.max(markedLine - 2, 0);
const lastLine = Math.min(firstLine + 5, lastLineInFile);
const lineNumberMaxWidth = `${lastLine + 1}`.length;
const codeSpan = [];
for (let index = firstLine; index <= lastLine; index++) {
const lineStart = this.props.file.getPositionOfLineAndCharacter(index, 0);
const lineEnd = index === lastLineInFile
? this.props.file.text.length
: this.props.file.getPositionOfLineAndCharacter(index + 1, 0);
const lineNumberText = String(index + 1);
const lineText = this.props.file.text.slice(lineStart, lineEnd).trimEnd().replaceAll("\t", " ");
if (index === markedLine) {
codeSpan.push(Scribbler.createElement(Line, null,
Scribbler.createElement(Text, { color: "31" }, ">"),
" ",
lineNumberText.padStart(lineNumberMaxWidth),
" ",
Scribbler.createElement(Text, { color: "90" }, "|"),
" ",
lineText), Scribbler.createElement(Line, null,
" ".repeat(lineNumberMaxWidth + 3),
Scribbler.createElement(Text, { color: "90" }, "|"),
" ".repeat(markedCharacter + 1),
Scribbler.createElement(Text, { color: "31" }, "^")));
}
else {
codeSpan.push(Scribbler.createElement(Line, null,
" ".repeat(2),
Scribbler.createElement(Text, { color: "90" },
lineNumberText.padStart(lineNumberMaxWidth),
" | ",
lineText || "")));
}
}
const breadcrumbs = this.props.breadcrumbs?.flatMap((ancestor) => [
Scribbler.createElement(Text, { color: "90" }, " ❭ "),
Scribbler.createElement(Text, null, ancestor),
]);
const location = (Scribbler.createElement(Line, null,
" ".repeat(lineNumberMaxWidth + 5),
Scribbler.createElement(Text, { color: "90" }, "at"),
" ",
Scribbler.createElement(Text, { color: "36" }, Path.relative("", this.props.file.fileName)),
Scribbler.createElement(Text, { color: "90" },
":",
String(markedLine + 1),
":",
String(markedCharacter + 1)),
breadcrumbs));
return (Scribbler.createElement(Text, null,
codeSpan,
Scribbler.createElement(Line, null),
location));
}
}
class DiagnosticText {
props;
constructor(props) {
this.props = props;
}
render() {
const code = typeof this.props.diagnostic.code === "string"
? Scribbler.createElement(Text, { color: "90" },
" ",
this.props.diagnostic.code)
: undefined;
const text = Array.isArray(this.props.diagnostic.text) ? this.props.diagnostic.text : [this.props.diagnostic.text];
const message = text.map((text, index) => (Scribbler.createElement(Text, null,
index === 1 ? Scribbler.createElement(Line, null) : undefined,
Scribbler.createElement(Line, null,
text,
code))));
const related = this.props.diagnostic.related?.map((relatedDiagnostic) => (Scribbler.createElement(DiagnosticText, { diagnostic: relatedDiagnostic })));
const codeSpan = this.props.diagnostic.origin
? (Scribbler.createElement(Text, null,
Scribbler.createElement(Line, null),
Scribbler.createElement(CodeSpanText, { ...this.props.diagnostic.origin })))
: undefined;
return (Scribbler.createElement(Text, null,
message,
codeSpan,
Scribbler.createElement(Line, null),
Scribbler.createElement(Text, { indent: 2 }, related)));
}
}
function diagnosticText(diagnostic) {
let prefix;
switch (diagnostic.category) {
case "error":
prefix = Scribbler.createElement(Text, { color: "31" }, "Error: ");
break;
case "warning":
prefix = Scribbler.createElement(Text, { color: "33" }, "Warning: ");
break;
}
return (Scribbler.createElement(Text, null,
prefix,
Scribbler.createElement(DiagnosticText, { diagnostic: diagnostic })));
}
class FileNameText {
props;
constructor(props) {
this.props = props;
}
render() {
const relativePath = Path.relative("", this.props.filePath);
const lastPathSeparator = relativePath.lastIndexOf("/");
const directoryNameText = relativePath.slice(0, lastPathSeparator + 1);
const fileNameText = relativePath.slice(lastPathSeparator + 1);
return (Scribbler.createElement(Text, null,
Scribbler.createElement(Text, { color: "90" }, directoryNameText),
fileNameText));
}
}
function fileStatusText(status, testFile) {
let statusColor;
let statusText;
switch (status) {
case "runs":
statusColor = "33";
statusText = "runs";
break;
case "passed":
statusColor = "32";
statusText = "pass";
break;
case "failed":
statusColor = "31";
statusText = "fail";
break;
}
return (Scribbler.createElement(Line, null,
Scribbler.createElement(Text, { color: statusColor }, statusText),
" ",
Scribbler.createElement(FileNameText, { filePath: fileURLToPath(testFile) })));
}
function fileViewText(lines, addEmptyFinalLine) {
return (Scribbler.createElement(Text, null,
[...lines],
addEmptyFinalLine ? Scribbler.createElement(Line, null) : undefined));
}
class JsonText {
props;
constructor(props) {
this.props = props;
}
render() {
return Scribbler.createElement(Line, null, JSON.stringify(this.#sortObject(this.props.input), null, 2));
}
#sortObject(target) {
if (Array.isArray(target)) {
return target;
}
return Object.keys(target)
.sort()
.reduce((result, key) => {
result[key] = target[key];
return result;
}, {});
}
}
function formattedText(input) {
if (typeof input === "string") {
return Scribbler.createElement(Line, null, input);
}
return Scribbler.createElement(JsonText, { input: input });
}
const usageExamples = [
["tstyche", "Run all tests."],
["tstyche path/to/first.test.ts", "Only run the test files with matching path."],
["tstyche --target 4.9,5.3.2,current", "Test on all specified versions of TypeScript."],
];
class HintText {
props;
constructor(props) {
this.props = props;
}
render() {
return (Scribbler.createElement(Text, { indent: 1, color: "90" }, this.props.children));
}
}
class HelpHeaderText {
props;
constructor(props) {
this.props = props;
}
render() {
const hint = (Scribbler.createElement(HintText, null,
Scribbler.createElement(Text, null, this.props.tstycheVersion)));
return (Scribbler.createElement(Line, null,
Scribbler.createElement(Text, null, "The TSTyche Type Test Runner"),
hint));
}
}
class CommandText {
props;
constructor(props) {
this.props = props;
}
render() {
let hint;
if (this.props.hint != null) {
hint = Scribbler.createElement(HintText, null, this.props.hint);
}
return (Scribbler.createElement(Line, { indent: 1 },
Scribbler.createElement(Text, { color: "34" }, this.props.text),
hint));
}
}
class OptionDescriptionText {
props;
constructor(props) {
this.props = props;
}
render() {
return Scribbler.createElement(Line, { indent: 1 }, this.props.text);
}
}
class CommandLineUsageText {
render() {
const usageText = usageExamples.map(([commandText, descriptionText]) => (Scribbler.createElement(Text, null,
Scribbler.createElement(CommandText, { text: commandText }),
Scribbler.createElement(OptionDescriptionText, { text: descriptionText }),
Scribbler.createElement(Line, null))));
return Scribbler.createElement(Text, null, usageText);
}
}
class CommandLineOptionNameText {
props;
constructor(props) {
this.props = props;
}
render() {
return Scribbler.createElement(Text, null,
"--",
this.props.text);
}
}
class CommandLineOptionHintText {
props;
constructor(props) {
this.props = props;
}
render() {
if (this.props.definition.brand === "list") {
return (Scribbler.createElement(Text, null,
this.props.definition.brand,
" of ",
this.props.definition.items.brand,
"s"));
}
return Scribbler.createElement(Text, null, this.props.definition.brand);
}
}
class CommandLineOptionsText {
props;
constructor(props) {
this.props = props;
}
render() {
const definitions = [...this.props.optionDefinitions.values()];
const optionsText = definitions.map((definition) => {
let hint;
if (definition.brand !== "true") {
hint = Scribbler.createElement(CommandLineOptionHintText, { definition: definition });
}
return (Scribbler.createElement(Text, null,
Scribbler.createElement(CommandText, { text: Scribbler.createElement(CommandLineOptionNameText, { text: definition.name }), hint: hint }),
Scribbler.createElement(OptionDescriptionText, { text: definition.description }),
Scribbler.createElement(Line, null)));
});
return (Scribbler.createElement(Text, null,
Scribbler.createElement(Line, null, "Command Line Options"),
Scribbler.createElement(Line, null),
optionsText));
}
}
class HelpFooterText {
render() {
return Scribbler.createElement(Line, null, "To learn more, visit https://tstyche.org");
}
}
function helpText(optionDefinitions, tstycheVersion) {
return (Scribbler.createElement(Text, null,
Scribbler.createElement(HelpHeaderText, { tstycheVersion: tstycheVersion }),
Scribbler.createElement(Line, null),
Scribbler.createElement(CommandLineUsageText, null),
Scribbler.createElement(Line, null),
Scribbler.createElement(CommandLineOptionsText, { optionDefinitions: optionDefinitions }),
Scribbler.createElement(Line, null),
Scribbler.createElement(HelpFooterText, null),
Scribbler.createElement(Line, null)));
}
class RowText {
props;
constructor(props) {
this.props = props;
}
render() {
return (Scribbler.createElement(Line, null,
`${this.props.label}:`.padEnd(12),
this.props.text));
}
}
class CountText {
props;
constructor(props) {
this.props = props;
}
render() {
return (Scribbler.createElement(Text, null,
this.props.failed > 0
? (Scribbler.createElement(Text, null,
Scribbler.createElement(Text, { color: "31" },
String(this.props.failed),
" failed"),
Scribbler.createElement(Text, null, ", ")))
: undefined,
this.props.skipped > 0
? (Scribbler.createElement(Text, null,
Scribbler.createElement(Text, { color: "33" },
String(this.props.skipped),
" skipped"),
Scribbler.createElement(Text, null, ", ")))
: undefined,
this.props.todo > 0
? (Scribbler.createElement(Text, null,
Scribbler.createElement(Text, { color: "35" },
String(this.props.todo),
" todo"),
Scribbler.createElement(Text, null, ", ")))
: undefined,
this.props.passed > 0
? (Scribbler.createElement(Text, null,
Scribbler.createElement(Text, { color: "32" },
String(this.props.passed),
" passed"),
Scribbler.createElement(Text, null, ", ")))
: undefined,
Scribbler.createElement(Text, null,
String(this.props.total),
Scribbler.createElement(Text, null, " total"))));
}
}
class DurationText {
props;
constructor(props) {
this.props = props;
}
render() {
const duration = this.props.duration / 1000;
const minutes = Math.floor(duration / 60);
const seconds = duration % 60;
return (Scribbler.createElement(Text, null,
minutes > 0 ? `${minutes}m ` : undefined,
`${Math.round(seconds * 10) / 10}s`));
}
}
class MatchText {
props;
constructor(props) {
this.props = props;
}
render() {
if (typeof this.props.text === "string") {
return Scribbler.createElement(Text, null,
"'",
this.props.text,
"'");
}
if (this.props.text.length <= 1) {
return Scribbler.createElement(Text, null,
"'",
...this.props.text,
"'");
}
const lastItem = this.props.text.pop();
return (Scribbler.createElement(Text, null,
this.props.text.map((match, index, list) => (Scribbler.createElement(Text, null,
"'",
match,
"'",
index === list.length - 1 ? Scribbler.createElement(Text, null, " ") : Scribbler.createElement(Text, { color: "90" }, ", ")))),
Scribbler.createElement(Text, { color: "90" }, "or"),
" '",
lastItem,
"'"));
}
}
class RanFilesText {
props;
constructor(props) {
this.props = props;
}
render() {
const testNameMatch = [];
if (this.props.onlyMatch != null) {
testNameMatch.push(Scribbler.createElement(Text, null,
Scribbler.createElement(Text, { color: "90" }, "matching "),
Scribbler.createElement(MatchText, { text: this.props.onlyMatch })));
}
if (this.props.skipMatch != null) {
testNameMatch.push(Scribbler.createElement(Text, null,
this.props.onlyMatch == null ? undefined : Scribbler.createElement(Text, { color: "90" }, " and "),
Scribbler.createElement(Text, { color: "90" }, "not matching "),
Scribbler.createElement(MatchText, { text: this.props.skipMatch })));
}
let pathMatch;
if (this.props.pathMatch.length > 0) {
pathMatch = (Scribbler.createElement(Text, null,
Scribbler.createElement(Text, { color: "90" }, "test files matching "),
Scribbler.createElement(MatchText, { text: this.props.pathMatch }),
Scribbler.createElement(Text, { color: "90" }, ".")));
}
else {
pathMatch = Scribbler.createElement(Text, { color: "90" }, "all test files.");
}
return (Scribbler.createElement(Line, null,
Scribbler.createElement(Text, { color: "90" }, "Ran "),
testNameMatch.length > 0 ? Scribbler.createElement(Text, { color: "90" }, "tests ") : undefined,
testNameMatch,
testNameMatch.length > 0 ? Scribbler.createElement(Text, { color: "90" }, " in ") : undefined,
pathMatch));
}
}
function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) {
const targetCountText = (Scribbler.createElement(RowText, { label: "Targets", text: Scribbler.createElement(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) }));
const fileCountText = (Scribbler.createElement(RowText, { label: "Test files", text: Scribbler.createElement(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) }));
const testCountText = (Scribbler.createElement(RowText, { label: "Tests", text: Scribbler.createElement(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) }));
const assertionCountText = (Scribbler.createElement(RowText, { label: "Assertions", text: Scribbler.createElement(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) }));
return (Scribbler.createElement(Text, null,
targetCountText,
fileCountText,
testCount.total > 0 ? testCountText : undefined,
expectCount.total > 0 ? assertionCountText : undefined,
Scribbler.createElement(RowText, { label: "Duration", text: Scribbler.createElement(DurationText, { duration: duration }) }),
Scribbler.createElement(Line, null),
Scribbler.createElement(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch })));
}
class StatusText {
props;
constructor(props) {
this.props = props;
}
render() {
switch (this.props.status) {
case "fail":
return Scribbler.createElement(Text, { color: "31" }, "\u00D7");
case "pass":
return Scribbler.createElement(Text, { color: "32" }, "+");
case "skip":
return Scribbler.createElement(Text, { color: "33" }, "- skip");
case "todo":
return Scribbler.createElement(Text, { color: "35" }, "- todo");
}
}
}
function testNameText(status, name, indent = 0) {
return (Scribbler.createElement(Line, { indent: indent + 1 },
Scribbler.createElement(StatusText, { status: status }),
" ",
Scribbler.createElement(Text, { color: "90" }, name)));
}
class ProjectNameText {
props;
constructor(props) {
this.props = props;
}
render() {
return (Scribbler.createElement(Text, { color: "90" },
" with ",
Path.relative("", this.props.filePath)));
}
}
function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) {
let projectPathText;
if (tsconfigFilePath != null) {
projectPathText = Scribbler.createElement(ProjectNameText, { filePath: tsconfigFilePath });
}
return (Scribbler.createElement(Text, null,
options?.prependEmptyLine === true ? Scribbler.createElement(Line, null) : undefined,
Scribbler.createElement(Line, null,
Scribbler.createElement(Text, { color: "34" }, "uses"),
" TypeScript ",
compilerVersion,
projectPathText),
Scribbler.createElement(Line, null)));
}
class SummaryReporter extends Reporter {
handleEvent([eventName, payload]) {
switch (eventName) {
case "end":
this.logger.writeMessage(summaryText({
duration: payload.result.timing.duration,
expectCount: payload.result.expectCount,
fileCount: payload.result.fileCount,
onlyMatch: payload.result.resolvedConfig.only,
pathMatch: payload.result.resolvedConfig.pathMatch,
skipMatch: payload.result.resolvedConfig.skip,
targetCount: payload.result.targetCount,
testCount: payload.result.testCount,
}));
break;
}
}
}
class FileViewService {
#indent = 0;
#lines = [];
#messages = [];
get hasErrors() {
return this.#messages.length > 0;
}
addMessage(message) {
this.#messages.push(message);
}
addTest(status, name) {
this.#lines.push(testNameText(status, name, this.#indent));
}
beginDescribe(name) {
this.#lines.push(describeNameText(name, this.#indent));
this.#indent++;
}
endDescribe() {
this.#indent--;
}
getMessages() {
return this.#messages;
}
getViewText(options) {
return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
}
reset() {
this.#indent = 0;
this.#lines = [];
this.#messages = [];
}
}
class ThoroughReporter extends Reporter {
#currentCompilerVersion;
#currentProjectConfigFilePath;
#fileCount = 0;
#fileView = new FileViewService();
#hasReportedAdds = false;
#hasReportedError = false;
#isFileViewExpanded = false;
get #isLastFile() {
return this.#fileCount === 0;
}
handleEvent([eventName, payload]) {
switch (eventName) {
case "start":
this.#isFileViewExpanded = payload.result.testFiles.length === 1;
break;
case "store:info":
this.logger.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
this.#hasReportedAdds = true;
break;
case "store:error":
for (const diagnostic of payload.diagnostics) {
this.logger.writeError(diagnosticText(diagnostic));
}
break;
case "target:start":
this.#fileCount = payload.result.testFiles.length;
break;
case "target:end":
this.#currentCompilerVersion = undefined;
this.#currentProjectConfigFilePath = undefined;
break;
case "project:info":
if (this.#currentCompilerVersion !== payload.compilerVersion
|| this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) {
this.logger.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds
&& !this.#hasReportedError,
}));
this.#hasReportedAdds = false;
this.#currentCompilerVersion = payload.compilerVersion;
this.#currentProjectConfigFilePath = payload.projectConfigFilePath;
}
break;
case "project:error":
for (const diagnostic of payload.diagnostics) {
this.logger.writeError(diagnosticText(diagnostic));
}
break;
case "file:start":
if (!Environment.noInteractive) {
this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
}
this.#fileCount--;
this.#hasReportedError = false;
break;
case "file:error":
for (const diagnostic of payload.diagnostics) {
this.#fileView.addMessage(diagnosticText(diagnostic));
}
break;
case "file:end":
if (!Environment.noInteractive) {
this.logger.eraseLastLine();
}
this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
this.logger.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
if (this.#fileView.hasErrors) {
this.logger.writeError(this.#fileView.getMessages());
this.#hasReportedError = true;
}
this.#fileView.reset();
break;
case "describe:start":
if (this.#isFileViewExpanded) {
this.#fileView.beginDescribe(payload.result.describe.name);
}
break;
case "describe:end":
if (this.#isFileViewExpanded) {
this.#fileView.endDescribe();
}
break;
case "test:skip":
if (this.#isFileViewExpanded) {
this.#fileView.addTest("skip", payload.result.test.name);
}
break;
case "test:todo":
if (this.#isFileViewExpanded) {
this.#fileView.addTest("todo", payload.result.test.name);
}
break;
case "test:error":
if (this.#isFileViewExpanded) {
this.#fileView.addTest("fail", payload.result.test.name);
}
for (const diagnostic of payload.diagnostics) {
this.#fileView.addMessage(diagnosticText(diagnostic));
}
break;
case "test:fail":
if (this.#isFileViewExpanded) {
this.#fileView.addTest("fail", payload.result.test.name);
}
break;
case "test:pass":
if (this.#isFileViewExpanded) {
this.#fileView.addTest("pass", payload.result.test.name);
}
break;
case "expect:error":
case "expect:fail":
for (const diagnostic of payload.diagnostics) {
this.#fileView.addMessage(diagnosticText(diagnostic));
}
break;
}
}
}
class ResultTiming {
end = Date.now();
start = Date.now();
get duration() {
return this.end - this.start;
}
}
class DescribeResult {
describe;
parent;
results = [];
timing = new ResultTiming();
constructor(describe, parent) {
this.describe = describe;
this.parent = parent;
}
}
class ExpectResult {
assertion;
parent;
diagnostics = [];
status = "runs";
timing = new ResultTiming();
constructor(assertion, parent) {
this.assertion = assertion;
this.parent = parent;
}
}
class ResultCount {
failed = 0;
passed = 0;
skipped = 0;
todo = 0;
get total() {
return this.failed + this.passed + this.skipped + this.todo;
}
}
class FileResult {
testFile;
diagnostics = [];
expectCount = new ResultCount();
results = [];
status = "runs";
testCount = new ResultCount();
timing = new ResultTiming();
constructor(testFile) {
this.testFile = testFile;
}
}
class ProjectResult {
compilerVersion;
projectConfigFilePath;
diagnostics = [];
results = [];
constructor(compilerVersion, projectConfigFilePath) {
this.compilerVersion = compilerVersion;
this.projectConfigFilePath = projectConfigFilePath;
}
}
class Result {
resolvedConfig;
testFiles;
expectCount = new ResultCount();
fileCount = new ResultCount();
results = [];
targetCount = new ResultCount();
testCount = new ResultCount();
timing = new ResultTiming();
constructor(resolvedConfig, testFiles) {
this.resolvedConfig = resolvedConfig;
this.testFiles = testFiles;
}
}
class ResultManager {
#describeResult;
#expectResult;
#fileResult;
#projectResult;
#result;
#targetResult;
#testResult;
handleEvent([eventName, payload]) {
switch (eventName) {
case "start":
this.#result = payload.result;
this.#result.timing.start = Date.now();
break;
case "end":
this.#result.timing.end = Date.now();
this.#result = undefined;
break;
case "target:start":
this.#result.results.push(payload.result);
this.#targetResult = payload.result;
this.#targetResult.timing.start = Date.now();
break;
case "target:end":
if (this.#targetResult.status === "failed") {
this.#result.targetCount.failed++;
}
else {
this.#result.targetCount.passed++;
this.#targetResult.status = "passed";
}
this.#targetResult.timing.end = Date.now();
this.#targetResult = undefined;
break;
case "store:error": {
if (payload.diagnostics.some(({ category }) => category === "error")) {
this.#targetResult.status = "failed";
}
break;
}
case "project:info":
{
let projectResult = this.#targetResult.results.get(payload.projectConfigFilePath);
if (!projectResult) {
projectResult = new ProjectResult(payload.compilerVersion, payload.projectConfigFilePath);
this.#targetResult.results.set(payload.projectConfigFilePath, projectResult);
}
this.#projectResult = projectResult;
}
break;
case "project:error":
this.#targetResult.status = "failed";
this.#projectResult.diagnostics.push(...payload.diagnostics);
break;
case "file:start":
this.#projectResult.results.push(payload.result);
this.#fileResult = payload.result;
this.#fileResult.timing.start = Date.now();
break;
case "file:error":
this.#targetResult.status = "failed";
this.#fileResult.status = "failed";
this.#fileResult.diagnostics.push(...payload.diagnostics);
break;
case "file:end":
if (this.#fileResult.status === "failed"
|| this.#fileResult.expectCount.failed > 0
|| this.#fileResult.testCount.failed > 0) {
this.#result.fileCount.failed++;
this.#targetResult.status = "failed";
this.#fileResult.status = "failed";
}
else {
this.#result.fileCount.passed++;
this.#fileResult.status = "passed";
}
this.#fileResult.timing.end = Date.now();
this.#fileResult = undefined;
break;
case "describe:start":
if (this.#describeResult) {
this.#describeResult.results.push(payload.result);
}
else {
this.#fileResult.results.push(payload.result);
}
this.#describeResult = payload.result;
this.#describeResult.timing.start = Date.now();
break;
case "describe:end":
this.#describeResult.timing.end = Date.now();
this.#describeResult = this.#describeResult.parent;
break;
case "test:start":
if (this.#describeResult) {
this.#describeResult.results.push(payload.result);
}
else {
this.#fileResult.results.push(payload.result);
}
this.#testResult = payload.result;
this.#testResult.timing.start = Date.now();
break;
case "test:error":
this.#result.testCount.failed++;
this.#fileResult.testCount.failed++;
this.#testResult.status = "failed";
this.#testResult.diagnostics.push(...payload.diagnostics);
this.#testResult.timing.end = Date.now();
this.#testResult = undefined;
break;
case "test:fail":
this.#result.testCount.failed++;
this.#fileResult.testCount.failed++;
this.#testResult.status = "failed";
this.#testResult.timing.end = Date.now();
this.#testResult = undefined;
break;
case "test:pass":
this.#result.testCount.passed++;
this.#fileResult.testCount.passed++;
this.#testResult.status = "passed";
this.#testResult.timing.end = Date.now();
this.#testResult = undefined;
break;
case "test:skip":
this.#result.testCount.skipped++;
this.#fileResult.testCount.skipped++;
this.#testResult.status = "skipped";
this.#testResult.timing.end = Date.now();
this.#testResult = undefined;
break;
case "test:todo":
this.#result.testCount.todo++;
this.#fileResult.testCount.todo++;
this.#testResult.status = "todo";
this.#testResult.timing.end = Date.now();
this.#testResult = undefined;
break;
case "expect:start":
if (this.#testResult) {
this.#testResult.results.push(payload.result);
}
else {
this.#fileResult.results.push(payload.result);
}
this.#expectResult = payload.result;
this.#expectResult.timing.start = Date.now();
break;
case "expect:error":
this.#result.expectCount.failed++;
this.#fileResult.expectCount.failed++;
if (this.#testResult) {
this.#testResult.expectCount.failed++;
}
this.#expectResult.status = "failed";
this.#expectResult.diagnostics.push(...payload.diagnostics);
this.#expectResult.timing.end = Date.now();
this.#expectResult = undefined;
break;
case "expect:fail":
this.#result.expectCount.failed++;
this.#fileResult.expectCount.failed++;
if (this.#testResult) {
this.#testResult.expectCount.failed++;
}
this.#expectResult.status = "failed";
this.#expectResult.timing.end = Date.now();
this.#expectResult = undefined;
break;
case "expect:pass":
this.#result.expectCount.passed++;
this.#fileResult.expectCount.passed++;
if (this.#testResult) {
this.#testResult.expectCount.passed++;
}
this.#expectResult.status = "passed";
this.#expectResult.timing.end = Date.now();
this.#expectResult = undefined;
break;
case "expect:skip":
this.#result.expectCount.skipped++;
this.#fileResult.expectCount.skipped++;
if (this.#testResult) {
this.#testResult.expectCount.skipped++;
}
this.#expectResult.status = "skipped";
this.#expectResult.timing.end = Date.now();
this.#expectResult = undefined;
break;
}
}
}
var ResultStatus;
(function (ResultStatus) {
ResultStatus["Runs"] = "runs";
ResultStatus["Passed"] = "passed";
ResultStatus["Failed"] = "failed";
ResultStatus["Skipped"] = "skipped";
ResultStatus["Todo"] = "todo";
})(ResultStatus || (ResultStatus = {}));
class TargetResult {
versionTag;
testFiles;
results = new Map();
status = "runs";
timing = new ResultTiming();
constructor(versionTag, testFiles) {
this.versionTag = versionTag;
this.testFiles = testFiles;
}
}
class TestResult {
test;
parent;
diagnostics = [];
expectCount = new ResultCount();
results = [];
status = "runs";
timing = new ResultTiming();
constructor(test, parent) {
this.test = test;
this.parent = parent;
}
}
class Diagnostic {
text;
category;
origin;
code;
related;
constructor(text, category, origin) {
this.text = text;
this.category = category;
this.origin = origin;
}
add(options) {
if (options.code != null) {
this.code = options.code;
}
if (options.origin != null) {
this.origin = options.origin;
}
if (options.related != null) {
this.related = options.related;
}
return this;
}
static error(text, origin) {
return new Diagnostic(text, "error", origin);
}
static fromDiagnostics(diagnostics, compiler) {
return diagnostics.map((diagnostic) => {
const category = "error";
const code = `ts(${diagnostic.code})`;
let origin;
const text = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
if (Diagnostic.isTsDiagnosticWithLocation(diagnostic)) {
origin = {
end: diagnostic.start + diagnostic.length,
file: diagnostic.file,
start: diagnostic.start,
};
}
return new Diagnostic(text, category, origin).add({ code });
});
}
static fromError(text, error) {
const messageText = Array.isArray(text) ? text : [text];
if (error instanceof Error) {
if (error.cause != null) {
messageText.push(this.#normalizeMessage(String(error.cause)));
}
messageText.push(this.#normalizeMessage(String(error.message)));
if (error.stack != null) {
const stackLines = error.stack
.split("\n")
.slice(1)
.map((line) => line.trimStart());
messageText.push(...stackLines);
}
}
return Diagnostic.error(messageText);
}
static isTsDiagnosticWithLocation(diagnostic) {
return diagnostic.file != null && diagnostic.start != null && diagnostic.length != null;
}
static #normalizeMessage(text) {
if (text.endsWith(".")) {
return text;
}
return `${text}.`;
}
static warning(text, origin) {
return new Diagnostic(text, "warning", origin);
}
}
var DiagnosticCategory;
(function (DiagnosticCategory) {
DiagnosticCategory["Error"] = "error";
DiagnosticCategory["Warning"] = "warning";
})(DiagnosticCategory || (DiagnosticCategory = {}));
class TestMember {
brand;
node;
parent;
flags;
compiler;
diagnostics = new Set();
members = [];
name = "";
constructor(brand, node, parent, flags) {
this.brand = brand;
this.node = node;
this.parent = parent;
this.flags = flags;
this.compiler = parent.compiler;
if (node.arguments[0] != null && this.compiler.isStringLiteralLike(node.arguments[0])) {
this.name = node.arguments[0].text;
}
if (node.arguments[1] != null
&& parent.compiler.isFunctionLike(node.arguments[1])
&& parent.