@git.zone/tstest 1.0.96 → 1.2.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/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +5 -0
- package/dist_ts/index.js +61 -4
- package/dist_ts/tstest.classes.tap.combinator.d.ts +3 -0
- package/dist_ts/tstest.classes.tap.combinator.js +14 -40
- package/dist_ts/tstest.classes.tap.parser.d.ts +3 -1
- package/dist_ts/tstest.classes.tap.parser.js +42 -18
- package/dist_ts/tstest.classes.tap.testresult.d.ts +1 -1
- package/dist_ts/tstest.classes.testdirectory.d.ts +9 -7
- package/dist_ts/tstest.classes.testdirectory.js +47 -8
- package/dist_ts/tstest.classes.tstest.d.ts +8 -3
- package/dist_ts/tstest.classes.tstest.js +35 -34
- package/dist_ts/tstest.logging.d.ts +48 -0
- package/dist_ts/tstest.logging.js +228 -0
- package/package.json +12 -12
- package/readme.plan.md +199 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +65 -3
- package/ts/tstest.classes.tap.combinator.ts +19 -41
- package/ts/tstest.classes.tap.parser.ts +44 -47
- package/ts/tstest.classes.testdirectory.ts +59 -14
- package/ts/tstest.classes.tstest.ts +42 -36
- package/ts/tstest.logging.ts +285 -0
|
@@ -7,9 +7,14 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
|
|
|
7
7
|
import { TestDirectory } from './tstest.classes.testdirectory.js';
|
|
8
8
|
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
|
9
9
|
import { TapParser } from './tstest.classes.tap.parser.js';
|
|
10
|
+
import { TestExecutionMode } from './index.js';
|
|
11
|
+
import { TsTestLogger } from './tstest.logging.js';
|
|
12
|
+
import type { LogOptions } from './tstest.logging.js';
|
|
10
13
|
|
|
11
14
|
export class TsTest {
|
|
12
15
|
public testDir: TestDirectory;
|
|
16
|
+
public executionMode: TestExecutionMode;
|
|
17
|
+
public logger: TsTestLogger;
|
|
13
18
|
|
|
14
19
|
public smartshellInstance = new plugins.smartshell.Smartshell({
|
|
15
20
|
executor: 'bash',
|
|
@@ -20,61 +25,57 @@ export class TsTest {
|
|
|
20
25
|
|
|
21
26
|
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
|
22
27
|
|
|
23
|
-
constructor(cwdArg: string,
|
|
24
|
-
this.
|
|
28
|
+
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}) {
|
|
29
|
+
this.executionMode = executionModeArg;
|
|
30
|
+
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
|
31
|
+
this.logger = new TsTestLogger(logOptions);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
async run() {
|
|
28
35
|
const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.log(''); // force new line
|
|
36
|
+
|
|
37
|
+
// Log test discovery
|
|
38
|
+
this.logger.testDiscovery(
|
|
39
|
+
fileNamesToRun.length,
|
|
40
|
+
this.testDir.testPath,
|
|
41
|
+
this.executionMode
|
|
42
|
+
);
|
|
37
43
|
|
|
38
|
-
const tapCombinator = new TapCombinator(); // lets create the TapCombinator
|
|
44
|
+
const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator
|
|
45
|
+
let fileIndex = 0;
|
|
39
46
|
for (const fileNameArg of fileNamesToRun) {
|
|
47
|
+
fileIndex++;
|
|
40
48
|
switch (true) {
|
|
41
49
|
case process.env.CI && fileNameArg.includes('.nonci.'):
|
|
42
|
-
|
|
43
|
-
console.log(
|
|
44
|
-
`not running testfile ${fileNameArg}, since we are CI and file name includes '.nonci.' tag`
|
|
45
|
-
);
|
|
46
|
-
console.log('!!!!!!!!!!!');
|
|
50
|
+
this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
|
|
47
51
|
break;
|
|
48
52
|
case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
|
|
49
|
-
const tapParserBrowser = await this.runInChrome(fileNameArg);
|
|
53
|
+
const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
|
|
50
54
|
tapCombinator.addTapParser(tapParserBrowser);
|
|
51
55
|
break;
|
|
52
56
|
case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
|
|
53
|
-
|
|
54
|
-
const tapParserBothBrowser = await this.runInChrome(fileNameArg);
|
|
57
|
+
this.logger.sectionStart('Part 1: Chrome');
|
|
58
|
+
const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
|
|
55
59
|
tapCombinator.addTapParser(tapParserBothBrowser);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const tapParserBothNode = await this.runInNode(fileNameArg);
|
|
60
|
+
this.logger.sectionEnd();
|
|
61
|
+
|
|
62
|
+
this.logger.sectionStart('Part 2: Node');
|
|
63
|
+
const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
|
|
60
64
|
tapCombinator.addTapParser(tapParserBothNode);
|
|
65
|
+
this.logger.sectionEnd();
|
|
61
66
|
break;
|
|
62
67
|
default:
|
|
63
|
-
const tapParserNode = await this.runInNode(fileNameArg);
|
|
68
|
+
const tapParserNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
|
|
64
69
|
tapCombinator.addTapParser(tapParserNode);
|
|
65
70
|
break;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
console.log(cs(`^`.repeat(16), 'cyan'));
|
|
69
|
-
console.log(''); // force new line
|
|
70
72
|
}
|
|
71
73
|
tapCombinator.evaluate();
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
public async runInNode(fileNameArg: string): Promise<TapParser> {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const tapParser = new TapParser(fileNameArg + ':node');
|
|
76
|
+
public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> {
|
|
77
|
+
this.logger.testFileStart(fileNameArg, 'node.js', index, total);
|
|
78
|
+
const tapParser = new TapParser(fileNameArg + ':node', this.logger);
|
|
78
79
|
|
|
79
80
|
// tsrun options
|
|
80
81
|
let tsrunOptions = '';
|
|
@@ -89,9 +90,8 @@ export class TsTest {
|
|
|
89
90
|
return tapParser;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
public async runInChrome(fileNameArg: string): Promise<TapParser> {
|
|
93
|
-
|
|
94
|
-
console.log(`${cs(`= `.repeat(32), 'cyan')}`);
|
|
93
|
+
public async runInChrome(fileNameArg: string, index: number, total: number): Promise<TapParser> {
|
|
94
|
+
this.logger.testFileStart(fileNameArg, 'chromium', index, total);
|
|
95
95
|
|
|
96
96
|
// lets get all our paths sorted
|
|
97
97
|
const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
|
|
@@ -130,11 +130,17 @@ export class TsTest {
|
|
|
130
130
|
await server.start();
|
|
131
131
|
|
|
132
132
|
// lets handle realtime comms
|
|
133
|
-
const tapParser = new TapParser(fileNameArg + ':chrome');
|
|
133
|
+
const tapParser = new TapParser(fileNameArg + ':chrome', this.logger);
|
|
134
134
|
const wss = new plugins.ws.WebSocketServer({ port: 8080 });
|
|
135
135
|
wss.on('connection', (ws) => {
|
|
136
136
|
ws.on('message', (message) => {
|
|
137
|
-
|
|
137
|
+
const messageStr = message.toString();
|
|
138
|
+
if (messageStr.startsWith('console:')) {
|
|
139
|
+
const [, level, ...messageParts] = messageStr.split(':');
|
|
140
|
+
this.logger.browserConsole(messageParts.join(':'), level);
|
|
141
|
+
} else {
|
|
142
|
+
tapParser.handleTapLog(messageStr);
|
|
143
|
+
}
|
|
138
144
|
});
|
|
139
145
|
});
|
|
140
146
|
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { coloredString as cs } from '@push.rocks/consolecolor';
|
|
2
|
+
import * as plugins from './tstest.plugins.js';
|
|
3
|
+
|
|
4
|
+
export interface LogOptions {
|
|
5
|
+
quiet?: boolean;
|
|
6
|
+
verbose?: boolean;
|
|
7
|
+
noColor?: boolean;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
logFile?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TestFileResult {
|
|
13
|
+
file: string;
|
|
14
|
+
passed: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
total: number;
|
|
17
|
+
duration: number;
|
|
18
|
+
tests: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
passed: boolean;
|
|
21
|
+
duration: number;
|
|
22
|
+
error?: string;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TestSummary {
|
|
27
|
+
totalFiles: number;
|
|
28
|
+
totalTests: number;
|
|
29
|
+
totalPassed: number;
|
|
30
|
+
totalFailed: number;
|
|
31
|
+
totalDuration: number;
|
|
32
|
+
fileResults: TestFileResult[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class TsTestLogger {
|
|
36
|
+
private options: LogOptions;
|
|
37
|
+
private startTime: number;
|
|
38
|
+
private fileResults: TestFileResult[] = [];
|
|
39
|
+
private currentFileResult: TestFileResult | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(options: LogOptions = {}) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
this.startTime = Date.now();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private format(text: string, color?: string): string {
|
|
47
|
+
if (this.options.noColor || !color) {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
return cs(text, color as any);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private log(message: string) {
|
|
54
|
+
if (this.options.json) return;
|
|
55
|
+
console.log(message);
|
|
56
|
+
|
|
57
|
+
if (this.options.logFile) {
|
|
58
|
+
// TODO: Implement file logging
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Section separators
|
|
63
|
+
sectionStart(title: string) {
|
|
64
|
+
if (this.options.quiet || this.options.json) return;
|
|
65
|
+
this.log(this.format(`\n━━━ ${title} ━━━`, 'cyan'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
sectionEnd() {
|
|
69
|
+
if (this.options.quiet || this.options.json) return;
|
|
70
|
+
this.log(this.format('─'.repeat(50), 'dim'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Progress indication
|
|
74
|
+
progress(current: number, total: number, message: string) {
|
|
75
|
+
if (this.options.quiet || this.options.json) return;
|
|
76
|
+
const percentage = Math.round((current / total) * 100);
|
|
77
|
+
const filled = Math.round((current / total) * 20);
|
|
78
|
+
const empty = 20 - filled;
|
|
79
|
+
|
|
80
|
+
this.log(this.format(`\n📊 Progress: ${current}/${total} (${percentage}%)`, 'cyan'));
|
|
81
|
+
this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Test discovery
|
|
85
|
+
testDiscovery(count: number, pattern: string, executionMode: string) {
|
|
86
|
+
if (this.options.json) {
|
|
87
|
+
console.log(JSON.stringify({ event: 'discovery', count, pattern, executionMode }));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.options.quiet) {
|
|
92
|
+
this.log(`Found ${count} tests`);
|
|
93
|
+
} else {
|
|
94
|
+
this.log(this.format(`\n🔍 Test Discovery`, 'bold'));
|
|
95
|
+
this.log(this.format(` Mode: ${executionMode}`, 'dim'));
|
|
96
|
+
this.log(this.format(` Pattern: ${pattern}`, 'dim'));
|
|
97
|
+
this.log(this.format(` Found: ${count} test file(s)`, 'green'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Test execution
|
|
102
|
+
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
|
103
|
+
this.currentFileResult = {
|
|
104
|
+
file: filename,
|
|
105
|
+
passed: 0,
|
|
106
|
+
failed: 0,
|
|
107
|
+
total: 0,
|
|
108
|
+
duration: 0,
|
|
109
|
+
tests: []
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (this.options.json) {
|
|
113
|
+
console.log(JSON.stringify({ event: 'fileStart', filename, runtime, index, total }));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.options.quiet) return;
|
|
118
|
+
|
|
119
|
+
this.log(this.format(`\n▶️ ${filename} (${index}/${total})`, 'blue'));
|
|
120
|
+
this.log(this.format(` Runtime: ${runtime}`, 'dim'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
testResult(testName: string, passed: boolean, duration: number, error?: string) {
|
|
124
|
+
if (this.currentFileResult) {
|
|
125
|
+
this.currentFileResult.tests.push({ name: testName, passed, duration, error });
|
|
126
|
+
this.currentFileResult.total++;
|
|
127
|
+
if (passed) {
|
|
128
|
+
this.currentFileResult.passed++;
|
|
129
|
+
} else {
|
|
130
|
+
this.currentFileResult.failed++;
|
|
131
|
+
}
|
|
132
|
+
this.currentFileResult.duration += duration;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.options.json) {
|
|
136
|
+
console.log(JSON.stringify({ event: 'testResult', testName, passed, duration, error }));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const icon = passed ? '✅' : '❌';
|
|
141
|
+
const color = passed ? 'green' : 'red';
|
|
142
|
+
|
|
143
|
+
if (this.options.quiet) {
|
|
144
|
+
this.log(`${icon} ${testName}`);
|
|
145
|
+
} else {
|
|
146
|
+
this.log(this.format(` ${icon} ${testName} (${duration}ms)`, color));
|
|
147
|
+
if (error && !passed) {
|
|
148
|
+
this.log(this.format(` ${error}`, 'red'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
testFileEnd(passed: number, failed: number, duration: number) {
|
|
154
|
+
if (this.currentFileResult) {
|
|
155
|
+
this.fileResults.push(this.currentFileResult);
|
|
156
|
+
this.currentFileResult = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.options.json) {
|
|
160
|
+
console.log(JSON.stringify({ event: 'fileEnd', passed, failed, duration }));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!this.options.quiet) {
|
|
165
|
+
const total = passed + failed;
|
|
166
|
+
const status = failed === 0 ? 'PASSED' : 'FAILED';
|
|
167
|
+
const color = failed === 0 ? 'green' : 'red';
|
|
168
|
+
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// TAP output forwarding
|
|
173
|
+
tapOutput(message: string, isError: boolean = false) {
|
|
174
|
+
if (this.options.json) return;
|
|
175
|
+
|
|
176
|
+
if (this.options.verbose || isError) {
|
|
177
|
+
const prefix = isError ? ' ⚠️ ' : ' ';
|
|
178
|
+
const color = isError ? 'red' : 'dim';
|
|
179
|
+
this.log(this.format(`${prefix}${message}`, color));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Browser console
|
|
184
|
+
browserConsole(message: string, level: string = 'log') {
|
|
185
|
+
if (this.options.json) {
|
|
186
|
+
console.log(JSON.stringify({ event: 'browserConsole', message, level }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!this.options.quiet) {
|
|
191
|
+
const prefix = level === 'error' ? '🌐❌' : '🌐';
|
|
192
|
+
const color = level === 'error' ? 'red' : 'magenta';
|
|
193
|
+
this.log(this.format(` ${prefix} ${message}`, color));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Final summary
|
|
198
|
+
summary() {
|
|
199
|
+
const totalDuration = Date.now() - this.startTime;
|
|
200
|
+
const summary: TestSummary = {
|
|
201
|
+
totalFiles: this.fileResults.length,
|
|
202
|
+
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
|
|
203
|
+
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
|
|
204
|
+
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
|
|
205
|
+
totalDuration,
|
|
206
|
+
fileResults: this.fileResults
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (this.options.json) {
|
|
210
|
+
console.log(JSON.stringify({ event: 'summary', summary }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (this.options.quiet) {
|
|
215
|
+
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
|
|
216
|
+
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Detailed summary
|
|
221
|
+
this.log(this.format('\n📊 Test Summary', 'bold'));
|
|
222
|
+
this.log(this.format('┌────────────────────────────────┐', 'dim'));
|
|
223
|
+
this.log(this.format(`│ Total Files: ${summary.totalFiles.toString().padStart(14)} │`, 'white'));
|
|
224
|
+
this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
|
|
225
|
+
this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
|
|
226
|
+
this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
|
|
227
|
+
this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
|
|
228
|
+
this.log(this.format('└────────────────────────────────┘', 'dim'));
|
|
229
|
+
|
|
230
|
+
// File results
|
|
231
|
+
if (summary.totalFailed > 0) {
|
|
232
|
+
this.log(this.format('\n❌ Failed Tests:', 'red'));
|
|
233
|
+
this.fileResults.forEach(fileResult => {
|
|
234
|
+
if (fileResult.failed > 0) {
|
|
235
|
+
this.log(this.format(`\n ${fileResult.file}`, 'yellow'));
|
|
236
|
+
fileResult.tests.filter(t => !t.passed).forEach(test => {
|
|
237
|
+
this.log(this.format(` ❌ ${test.name}`, 'red'));
|
|
238
|
+
if (test.error) {
|
|
239
|
+
this.log(this.format(` ${test.error}`, 'dim'));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Performance metrics
|
|
247
|
+
if (this.options.verbose) {
|
|
248
|
+
const avgDuration = Math.round(totalDuration / summary.totalTests);
|
|
249
|
+
const slowestTest = this.fileResults
|
|
250
|
+
.flatMap(r => r.tests)
|
|
251
|
+
.sort((a, b) => b.duration - a.duration)[0];
|
|
252
|
+
|
|
253
|
+
this.log(this.format('\n⏱️ Performance Metrics:', 'cyan'));
|
|
254
|
+
this.log(this.format(` Average per test: ${avgDuration}ms`, 'white'));
|
|
255
|
+
if (slowestTest) {
|
|
256
|
+
this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Final status
|
|
261
|
+
const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
|
|
262
|
+
const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
|
|
263
|
+
this.log(this.format(`\n${status}`, statusColor));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Error display
|
|
267
|
+
error(message: string, file?: string, stack?: string) {
|
|
268
|
+
if (this.options.json) {
|
|
269
|
+
console.log(JSON.stringify({ event: 'error', message, file, stack }));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (this.options.quiet) {
|
|
274
|
+
console.error(`ERROR: ${message}`);
|
|
275
|
+
} else {
|
|
276
|
+
this.log(this.format('\n⚠️ Error', 'red'));
|
|
277
|
+
if (file) this.log(this.format(` File: ${file}`, 'yellow'));
|
|
278
|
+
this.log(this.format(` ${message}`, 'red'));
|
|
279
|
+
if (stack && this.options.verbose) {
|
|
280
|
+
this.log(this.format(` Stack:`, 'dim'));
|
|
281
|
+
this.log(this.format(stack.split('\n').map(line => ` ${line}`).join('\n'), 'dim'));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|