@quenty/cli-output-helpers 1.18.0 → 1.19.1
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/CHANGELOG.md +12 -0
- package/dist/outputHelper.d.ts +7 -0
- package/dist/outputHelper.d.ts.map +1 -1
- package/dist/outputHelper.js +23 -7
- package/dist/outputHelper.js.map +1 -1
- package/dist/outputHelper.test.js +25 -1
- package/dist/outputHelper.test.js.map +1 -1
- package/dist/reporting/github/annotations.d.ts +5 -2
- package/dist/reporting/github/annotations.d.ts.map +1 -1
- package/dist/reporting/github/annotations.js +31 -6
- package/dist/reporting/github/annotations.js.map +1 -1
- package/dist/reporting/github/annotations.test.js +45 -1
- package/dist/reporting/github/annotations.test.js.map +1 -1
- package/dist/reporting/github/index.d.ts +1 -1
- package/dist/reporting/github/index.d.ts.map +1 -1
- package/dist/reporting/github/index.js +1 -1
- package/dist/reporting/github/index.js.map +1 -1
- package/dist/reporting/index.d.ts +1 -1
- package/dist/reporting/index.d.ts.map +1 -1
- package/dist/reporting/index.js +1 -1
- package/dist/reporting/index.js.map +1 -1
- package/dist/reporting/stdout-result-reporter.d.ts.map +1 -1
- package/dist/reporting/stdout-result-reporter.js +4 -1
- package/dist/reporting/stdout-result-reporter.js.map +1 -1
- package/dist/reporting/stdout-result-reporter.test.js +5 -0
- package/dist/reporting/stdout-result-reporter.test.js.map +1 -1
- package/package.json +2 -2
- package/src/outputHelper.test.ts +32 -1
- package/src/outputHelper.ts +31 -7
- package/src/reporting/github/annotations.test.ts +54 -0
- package/src/reporting/github/annotations.ts +35 -9
- package/src/reporting/github/index.ts +1 -0
- package/src/reporting/index.ts +1 -0
- package/src/reporting/stdout-result-reporter.test.ts +8 -0
- package/src/reporting/stdout-result-reporter.ts +4 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/outputHelper.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
2
|
import { OutputHelper } from './outputHelper.js';
|
|
3
3
|
|
|
4
4
|
describe('OutputHelper.formatErrorChain', () => {
|
|
@@ -68,3 +68,34 @@ describe('OutputHelper.formatErrorChain', () => {
|
|
|
68
68
|
expect(out).toContain(err.stack!);
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
describe('OutputHelper.setDiagnosticsToStderr', () => {
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
OutputHelper.setDiagnosticsToStderr(false);
|
|
75
|
+
vi.restoreAllMocks();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('writes diagnostics to stdout by default', () => {
|
|
79
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
80
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
81
|
+
|
|
82
|
+
OutputHelper.info('hello');
|
|
83
|
+
|
|
84
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(error).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('moves info, warnings, hints and boxes to stderr', () => {
|
|
89
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
90
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
91
|
+
OutputHelper.setDiagnosticsToStderr(true);
|
|
92
|
+
|
|
93
|
+
OutputHelper.info('info');
|
|
94
|
+
OutputHelper.warn('warn');
|
|
95
|
+
OutputHelper.hint('hint');
|
|
96
|
+
OutputHelper.box('box');
|
|
97
|
+
|
|
98
|
+
expect(log).not.toHaveBeenCalled();
|
|
99
|
+
expect(error).toHaveBeenCalledTimes(4);
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/outputHelper.ts
CHANGED
|
@@ -17,6 +17,16 @@ const _outputStorage = new AsyncLocalStorage<OutputBuffer>();
|
|
|
17
17
|
*/
|
|
18
18
|
export class OutputHelper {
|
|
19
19
|
private static _verbose: boolean = true;
|
|
20
|
+
private static _diagnosticsToStderr: boolean = false;
|
|
21
|
+
|
|
22
|
+
private static _writeDiagnostic(text: string): void {
|
|
23
|
+
if (this._diagnosticsToStderr) {
|
|
24
|
+
console.error(text);
|
|
25
|
+
} else {
|
|
26
|
+
console.log(text);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
/**
|
|
21
31
|
* Formats the error with markup
|
|
22
32
|
* @param message Message to format
|
|
@@ -169,7 +179,9 @@ export class OutputHelper {
|
|
|
169
179
|
* @param message Message to write
|
|
170
180
|
*/
|
|
171
181
|
public static info(message: string): void {
|
|
172
|
-
|
|
182
|
+
this._writeDiagnostic(
|
|
183
|
+
this._hasAnsi(message) ? message : this.formatInfo(message)
|
|
184
|
+
);
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
/**
|
|
@@ -189,6 +201,14 @@ export class OutputHelper {
|
|
|
189
201
|
return this._verbose;
|
|
190
202
|
}
|
|
191
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Sends info, warnings, hints, verbose output and boxes to stderr, for CLIs
|
|
206
|
+
* whose stdout carries only the command result.
|
|
207
|
+
*/
|
|
208
|
+
public static setDiagnosticsToStderr(toStderr: boolean): void {
|
|
209
|
+
this._diagnosticsToStderr = toStderr;
|
|
210
|
+
}
|
|
211
|
+
|
|
192
212
|
/**
|
|
193
213
|
* Logs a verbose/intermediate message. Suppressed when verbose is false.
|
|
194
214
|
* When running inside a buffered context (see runBuffered), messages are
|
|
@@ -206,7 +226,7 @@ export class OutputHelper {
|
|
|
206
226
|
if (buffer) {
|
|
207
227
|
buffer.lines.push(formatted);
|
|
208
228
|
} else {
|
|
209
|
-
|
|
229
|
+
this._writeDiagnostic(formatted);
|
|
210
230
|
}
|
|
211
231
|
}
|
|
212
232
|
|
|
@@ -228,7 +248,9 @@ export class OutputHelper {
|
|
|
228
248
|
* @param message Message to write
|
|
229
249
|
*/
|
|
230
250
|
public static warn(message: string): void {
|
|
231
|
-
|
|
251
|
+
this._writeDiagnostic(
|
|
252
|
+
this._hasAnsi(message) ? message : this.formatWarning(message)
|
|
253
|
+
);
|
|
232
254
|
}
|
|
233
255
|
|
|
234
256
|
/**
|
|
@@ -240,7 +262,7 @@ export class OutputHelper {
|
|
|
240
262
|
* after it.
|
|
241
263
|
*/
|
|
242
264
|
public static startGroup(title: string, useGroups = isCI()): void {
|
|
243
|
-
|
|
265
|
+
this._writeDiagnostic(
|
|
244
266
|
useGroups ? `::group::${title}` : this.formatDim(`── ${title} ──`)
|
|
245
267
|
);
|
|
246
268
|
}
|
|
@@ -248,7 +270,7 @@ export class OutputHelper {
|
|
|
248
270
|
/** Close the section opened by {@link startGroup}. */
|
|
249
271
|
public static endGroup(useGroups = isCI()): void {
|
|
250
272
|
if (useGroups) {
|
|
251
|
-
|
|
273
|
+
this._writeDiagnostic('::endgroup::');
|
|
252
274
|
}
|
|
253
275
|
}
|
|
254
276
|
|
|
@@ -257,7 +279,9 @@ export class OutputHelper {
|
|
|
257
279
|
* @param message Message to write
|
|
258
280
|
*/
|
|
259
281
|
public static hint(message: string): void {
|
|
260
|
-
|
|
282
|
+
this._writeDiagnostic(
|
|
283
|
+
this._hasAnsi(message) ? message : this.formatHint(message)
|
|
284
|
+
);
|
|
261
285
|
}
|
|
262
286
|
|
|
263
287
|
/**
|
|
@@ -265,6 +289,6 @@ export class OutputHelper {
|
|
|
265
289
|
* @param message
|
|
266
290
|
*/
|
|
267
291
|
public static box(message: string, options?: BoxOptions): void {
|
|
268
|
-
|
|
292
|
+
this._writeDiagnostic(this.formatBox(message, options));
|
|
269
293
|
}
|
|
270
294
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
type Diagnostic,
|
|
4
|
+
dedupeDiagnostics,
|
|
4
5
|
formatAnnotation,
|
|
5
6
|
emitAnnotations,
|
|
6
7
|
summarizeDiagnostics,
|
|
@@ -18,6 +19,24 @@ function makeDiagnostic(overrides: Partial<Diagnostic> = {}): Diagnostic {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
describe('formatAnnotation', () => {
|
|
22
|
+
it('omits file and line when the location is unknown', () => {
|
|
23
|
+
const result = formatAnnotation(
|
|
24
|
+
makeDiagnostic({
|
|
25
|
+
file: undefined,
|
|
26
|
+
line: undefined,
|
|
27
|
+
title: 'Runtime error',
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
expect(result).toBe('::error title=Runtime error::Something went wrong');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('formats an annotation with no properties at all', () => {
|
|
34
|
+
const result = formatAnnotation(
|
|
35
|
+
makeDiagnostic({ file: undefined, line: undefined })
|
|
36
|
+
);
|
|
37
|
+
expect(result).toBe('::error::Something went wrong');
|
|
38
|
+
});
|
|
39
|
+
|
|
21
40
|
it('formats a basic error annotation', () => {
|
|
22
41
|
const result = formatAnnotation(makeDiagnostic());
|
|
23
42
|
expect(result).toBe(
|
|
@@ -116,7 +135,42 @@ describe('summarizeDiagnostics', () => {
|
|
|
116
135
|
});
|
|
117
136
|
});
|
|
118
137
|
|
|
138
|
+
describe('dedupeDiagnostics', () => {
|
|
139
|
+
it('drops identical repeats', () => {
|
|
140
|
+
const result = dedupeDiagnostics([
|
|
141
|
+
makeDiagnostic({ title: 'Runtime error' }),
|
|
142
|
+
makeDiagnostic({ title: 'Runtime error' }),
|
|
143
|
+
makeDiagnostic({ file: undefined, line: undefined }),
|
|
144
|
+
makeDiagnostic({ file: undefined, line: undefined }),
|
|
145
|
+
]);
|
|
146
|
+
expect(result).toHaveLength(2);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('keeps distinct errors at the same file, line and title', () => {
|
|
150
|
+
const result = dedupeDiagnostics([
|
|
151
|
+
makeDiagnostic({ title: 'Runtime error', message: 'first' }),
|
|
152
|
+
makeDiagnostic({ title: 'Runtime error', message: 'second' }),
|
|
153
|
+
makeDiagnostic({ title: 'Runtime error', message: 'first', line: 11 }),
|
|
154
|
+
]);
|
|
155
|
+
expect(result.map((d) => [d.line, d.message])).toEqual([
|
|
156
|
+
[10, 'first'],
|
|
157
|
+
[10, 'second'],
|
|
158
|
+
[11, 'first'],
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
119
163
|
describe('formatAnnotationSummaryMarkdown', () => {
|
|
164
|
+
it('counts only real files and groups file-less diagnostics', () => {
|
|
165
|
+
const md = formatAnnotationSummaryMarkdown('Test Failures', [
|
|
166
|
+
makeDiagnostic({ file: 'src/a.lua' }),
|
|
167
|
+
makeDiagnostic({ file: undefined, line: undefined }),
|
|
168
|
+
]);
|
|
169
|
+
expect(md).toContain('across 1 file:');
|
|
170
|
+
expect(md).toContain('<summary>No file (1)</summary>');
|
|
171
|
+
expect(md).not.toContain('undefined');
|
|
172
|
+
});
|
|
173
|
+
|
|
120
174
|
it('returns "no issues" for empty diagnostics', () => {
|
|
121
175
|
const md = formatAnnotationSummaryMarkdown('luau-lsp', []);
|
|
122
176
|
expect(md).toContain('### luau-lsp');
|
|
@@ -16,8 +16,9 @@ import { isCI } from '../../cli-utils.js';
|
|
|
16
16
|
export type DiagnosticSeverity = 'error' | 'warning' | 'notice';
|
|
17
17
|
|
|
18
18
|
export interface Diagnostic {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Repo-relative path. Omit when the location is unknown. */
|
|
20
|
+
file?: string;
|
|
21
|
+
line?: number;
|
|
21
22
|
endLine?: number;
|
|
22
23
|
column?: number;
|
|
23
24
|
endColumn?: number;
|
|
@@ -61,8 +62,12 @@ function _escapeMessage(value: string): string {
|
|
|
61
62
|
export function formatAnnotation(diagnostic: Diagnostic): string {
|
|
62
63
|
const props: string[] = [];
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
if (diagnostic.file !== undefined) {
|
|
66
|
+
props.push(`file=${_escapeProperty(diagnostic.file)}`);
|
|
67
|
+
}
|
|
68
|
+
if (diagnostic.line !== undefined) {
|
|
69
|
+
props.push(`line=${diagnostic.line}`);
|
|
70
|
+
}
|
|
66
71
|
|
|
67
72
|
if (diagnostic.endLine !== undefined) {
|
|
68
73
|
props.push(`endLine=${diagnostic.endLine}`);
|
|
@@ -78,7 +83,25 @@ export function formatAnnotation(diagnostic: Diagnostic): string {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
const message = _escapeMessage(diagnostic.message);
|
|
81
|
-
|
|
86
|
+
const properties = props.length > 0 ? ` ${props.join(',')}` : '';
|
|
87
|
+
return `::${diagnostic.severity}${properties}::${message}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Drop exact repeats; GitHub shows only 10 error annotations per step. */
|
|
91
|
+
export function dedupeDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] {
|
|
92
|
+
const seen = new Set<string>();
|
|
93
|
+
return diagnostics.filter((d) => {
|
|
94
|
+
const key = JSON.stringify([
|
|
95
|
+
d.severity,
|
|
96
|
+
d.file,
|
|
97
|
+
d.line,
|
|
98
|
+
d.title,
|
|
99
|
+
d.message,
|
|
100
|
+
]);
|
|
101
|
+
if (seen.has(key)) return false;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
/**
|
|
@@ -103,7 +126,9 @@ export function summarizeDiagnostics(
|
|
|
103
126
|
const files = new Set<string>();
|
|
104
127
|
|
|
105
128
|
for (const d of diagnostics) {
|
|
106
|
-
|
|
129
|
+
if (d.file !== undefined) {
|
|
130
|
+
files.add(d.file);
|
|
131
|
+
}
|
|
107
132
|
switch (d.severity) {
|
|
108
133
|
case 'error':
|
|
109
134
|
errors++;
|
|
@@ -158,7 +183,7 @@ export function formatAnnotationSummaryMarkdown(
|
|
|
158
183
|
} file${summary.fileCount !== 1 ? 's' : ''}: ${parts.join(', ')}\n\n`;
|
|
159
184
|
|
|
160
185
|
// Group diagnostics by file
|
|
161
|
-
const byFile = new Map<string, Diagnostic[]>();
|
|
186
|
+
const byFile = new Map<string | undefined, Diagnostic[]>();
|
|
162
187
|
for (const d of diagnostics) {
|
|
163
188
|
const list = byFile.get(d.file) ?? [];
|
|
164
189
|
list.push(d);
|
|
@@ -176,7 +201,8 @@ export function formatAnnotationSummaryMarkdown(
|
|
|
176
201
|
break;
|
|
177
202
|
}
|
|
178
203
|
|
|
179
|
-
|
|
204
|
+
const label = file === undefined ? 'No file' : `<code>${file}</code>`;
|
|
205
|
+
md += `<details><summary>${label} (${diags.length})</summary>\n\n`;
|
|
180
206
|
md += '| Line | Severity | Message |\n';
|
|
181
207
|
md += '|------|----------|---------|\n';
|
|
182
208
|
|
|
@@ -189,7 +215,7 @@ export function formatAnnotationSummaryMarkdown(
|
|
|
189
215
|
? '`warning`'
|
|
190
216
|
: '`notice`';
|
|
191
217
|
const escapedMsg = d.message.replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
192
|
-
md += `| ${d.line} | ${sev} | ${escapedMsg} |\n`;
|
|
218
|
+
md += `| ${d.line ?? ''} | ${sev} | ${escapedMsg} |\n`;
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
if (diags.length > MAX_PER_FILE) {
|
package/src/reporting/index.ts
CHANGED
|
@@ -25,6 +25,14 @@ describe('StdoutResultReporter', () => {
|
|
|
25
25
|
expect(logSpy).toHaveBeenNthCalledWith(2, 'name=second');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
it('prints nothing when the result renders empty', () => {
|
|
29
|
+
const reporter = new StdoutResultReporter<unknown>({ render: () => '' });
|
|
30
|
+
|
|
31
|
+
reporter.onResult({});
|
|
32
|
+
|
|
33
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
28
36
|
it('startAsync and stopAsync are no-ops', async () => {
|
|
29
37
|
const reporter = new StdoutResultReporter<unknown>({ render: () => '' });
|
|
30
38
|
await reporter.startAsync();
|
|
@@ -18,6 +18,9 @@ export class StdoutResultReporter<T = unknown> extends BaseResultReporter<T> {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
override onResult(result: T): void {
|
|
21
|
-
|
|
21
|
+
const text = this._render(result);
|
|
22
|
+
if (text !== '') {
|
|
23
|
+
console.log(text);
|
|
24
|
+
}
|
|
22
25
|
}
|
|
23
26
|
}
|