Skip to content

Commit 0c3bfde

Browse files
committed
refactor: cleanup string and structured console formatters
1 parent e9a1dea commit 0c3bfde

3 files changed

Lines changed: 460 additions & 349 deletions

File tree

src/formatters/ConsoleFormatter.ts

Lines changed: 150 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,21 @@ export type IgnoreCheck = (
2727
frame: DevTools.DevTools.StackTrace.StackTrace.Frame,
2828
) => boolean;
2929

30-
export class ConsoleFormatter {
31-
static readonly #STACK_TRACE_MAX_LINES = 50;
30+
interface ConsoleMessageConcise {
31+
type: string;
32+
text: string;
33+
argsCount: number;
34+
id: number;
35+
}
36+
37+
interface ConsoleMessageDetailed extends ConsoleMessageConcise {
38+
// pre-formatted args.
39+
args: string[];
40+
// pre-formatted stacktrace.
41+
stackTrace?: string;
42+
}
3243

44+
export class ConsoleFormatter {
3345
readonly #id: number;
3446
readonly #type: string;
3547
readonly #text: string;
@@ -40,7 +52,7 @@ export class ConsoleFormatter {
4052
readonly #stack?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
4153
readonly #cause?: SymbolizedError;
4254

43-
readonly #isIgnored: IgnoreCheck;
55+
readonly isIgnored: IgnoreCheck;
4456

4557
private constructor(params: {
4658
id: number;
@@ -59,7 +71,7 @@ export class ConsoleFormatter {
5971
this.#resolvedArgs = params.resolvedArgs ?? [];
6072
this.#stack = params.stack;
6173
this.#cause = params.cause;
62-
this.#isIgnored = params.isIgnored;
74+
this.isIgnored = params.isIgnored;
6375
}
6476

6577
static async from(
@@ -160,20 +172,14 @@ export class ConsoleFormatter {
160172

161173
// The short format for a console message.
162174
toString(): string {
163-
return `msgid=${this.#id} [${this.#type}] ${this.#text} (${this.#argCount} args)`;
175+
return convertConsoleMessageConciseToString(this.toJSON());
164176
}
165177

166178
// The verbose format for a console message, including all details.
167179
toStringDetailed(): string {
168-
const result = [
169-
`ID: ${this.#id}`,
170-
`Message: ${this.#type}> ${this.#text}`,
171-
this.#formatArgs(),
172-
this.#formatStackTrace(this.#stack, this.#cause, {
173-
includeHeading: true,
174-
}),
175-
].filter(line => !!line);
176-
return result.join('\n');
180+
return convertConsoleMessageConciseDetailedToString(
181+
this.toJSONDetailed(),
182+
);
177183
}
178184

179185
#getArgs(): unknown[] {
@@ -188,140 +194,162 @@ export class ConsoleFormatter {
188194
return [];
189195
}
190196

191-
#formatArg(arg: unknown) {
192-
if (arg instanceof SymbolizedError) {
193-
return [
194-
arg.message,
195-
this.#formatStackTrace(arg.stackTrace, arg.cause, {
196-
includeHeading: false,
197-
}),
198-
]
199-
.filter(line => !!line)
200-
.join('\n');
201-
}
202-
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
197+
toJSON(): ConsoleMessageConcise {
198+
return {
199+
type: this.#type,
200+
text: this.#text,
201+
argsCount: this.#argCount,
202+
id: this.#id,
203+
};
203204
}
204205

205-
#formatArgs(): string {
206-
const args = this.#getArgs();
206+
toJSONDetailed(): ConsoleMessageDetailed {
207+
return {
208+
id: this.#id,
209+
type: this.#type,
210+
text: this.#text,
211+
argsCount: this.#argCount,
212+
args: this.#getArgs().map(arg => formatArg(arg, this)),
213+
stackTrace: this.#stack ? formatStackTrace(this.#stack, this.#cause, this) : undefined,
214+
};
215+
}
216+
}
207217

208-
if (!args.length) {
209-
return '';
210-
}
218+
function convertConsoleMessageConciseToString(msg: ConsoleMessageConcise) {
219+
return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)`;
220+
}
211221

212-
const result = ['### Arguments'];
222+
function convertConsoleMessageConciseDetailedToString(
223+
msg: ConsoleMessageDetailed,
224+
) {
225+
const result = [
226+
`ID: ${msg.id}`,
227+
`Message: ${msg.type}> ${msg.text}`,
228+
formatArgs(msg),
229+
...(msg.stackTrace ? [
230+
'### Stack trace',
231+
msg.stackTrace,
232+
]: [])
233+
].filter(line => !!line);
234+
return result.join('\n');
235+
}
213236

214-
for (const [key, arg] of args.entries()) {
215-
result.push(`Arg #${key}: ${this.#formatArg(arg)}`);
216-
}
237+
function formatArgs(msg: ConsoleMessageDetailed): string {
238+
const args = msg.args;
217239

218-
return result.join('\n');
240+
if (!args.length) {
241+
return '';
219242
}
220243

221-
#formatStackTrace(
222-
stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined,
223-
cause: SymbolizedError | undefined,
224-
opts: {includeHeading: boolean},
225-
): string {
226-
if (!stackTrace) {
227-
return '';
228-
}
244+
const result = ['### Arguments'];
229245

230-
const lines = this.#formatStackTraceInner(stackTrace, cause);
231-
const includedLines = lines.slice(
232-
0,
233-
ConsoleFormatter.#STACK_TRACE_MAX_LINES,
234-
);
235-
const reminderCount = lines.length - includedLines.length;
246+
for (const [key, arg] of args.entries()) {
247+
result.push(`Arg #${key}: ${arg}`);
248+
}
249+
250+
return result.join('\n');
251+
}
236252

253+
function formatArg(arg: unknown, formatter: {isIgnored: IgnoreCheck}) {
254+
if (arg instanceof SymbolizedError) {
237255
return [
238-
opts.includeHeading ? '### Stack trace' : '',
239-
...includedLines,
240-
reminderCount > 0 ? `... and ${reminderCount} more frames` : '',
241-
'Note: line and column numbers use 1-based indexing',
256+
arg.message,
257+
arg.stackTrace ? formatStackTrace(
258+
arg.stackTrace,
259+
arg.cause,
260+
formatter,
261+
) : undefined,
242262
]
243263
.filter(line => !!line)
244264
.join('\n');
245265
}
266+
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
267+
}
246268

247-
#formatStackTraceInner(
248-
stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined,
249-
cause: SymbolizedError | undefined,
250-
): string[] {
251-
if (!stackTrace) {
252-
return [];
253-
}
254-
255-
return [
256-
...this.#formatFragment(stackTrace.syncFragment),
257-
...stackTrace.asyncFragments
258-
.map(this.#formatAsyncFragment.bind(this))
259-
.flat(),
260-
...this.#formatCause(cause),
261-
];
262-
}
269+
const STACK_TRACE_MAX_LINES = 50;
270+
271+
function formatStackTrace(
272+
stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace,
273+
cause: SymbolizedError | undefined,
274+
formatter: {isIgnored: IgnoreCheck},
275+
): string {
276+
const lines = formatStackTraceInner(stackTrace, cause, formatter);
277+
const includedLines = lines.slice(0, STACK_TRACE_MAX_LINES);
278+
const reminderCount = lines.length - includedLines.length;
279+
280+
return [
281+
...includedLines,
282+
reminderCount > 0 ? `... and ${reminderCount} more frames` : '',
283+
'Note: line and column numbers use 1-based indexing',
284+
]
285+
.filter(line => !!line)
286+
.join('\n');
287+
}
263288

264-
#formatFragment(
265-
fragment: DevTools.DevTools.StackTrace.StackTrace.Fragment,
266-
): string[] {
267-
const frames = fragment.frames.filter(frame => !this.#isIgnored(frame));
268-
return frames.map(this.#formatFrame.bind(this));
289+
function formatStackTraceInner(
290+
stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined,
291+
cause: SymbolizedError | undefined,
292+
formatter: {isIgnored: IgnoreCheck},
293+
): string[] {
294+
if (!stackTrace) {
295+
return [];
269296
}
270297

271-
#formatAsyncFragment(
272-
fragment: DevTools.DevTools.StackTrace.StackTrace.AsyncFragment,
273-
): string[] {
274-
const formattedFrames = this.#formatFragment(fragment);
275-
if (formattedFrames.length === 0) {
276-
return [];
277-
}
298+
return [
299+
...formatFragment(stackTrace.syncFragment, formatter),
300+
...stackTrace.asyncFragments
301+
.map(item => formatAsyncFragment(item, formatter))
302+
.flat(),
303+
...formatCause(cause, formatter),
304+
];
305+
}
278306

279-
const separatorLineLength = 40;
280-
const prefix = `--- ${fragment.description || 'async'} `;
281-
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
282-
return [separator, ...formattedFrames];
283-
}
307+
function formatFragment(
308+
fragment: DevTools.DevTools.StackTrace.StackTrace.Fragment,
309+
formatter: {isIgnored: IgnoreCheck},
310+
): string[] {
311+
const frames = fragment.frames.filter(frame => !formatter.isIgnored(frame));
312+
return frames.map(formatFrame);
313+
}
284314

285-
#formatFrame(frame: DevTools.DevTools.StackTrace.StackTrace.Frame): string {
286-
let result = `at ${frame.name ?? '<anonymous>'}`;
287-
if (frame.uiSourceCode) {
288-
const location = frame.uiSourceCode.uiLocation(frame.line, frame.column);
289-
result += ` (${location.linkText(/* skipTrim */ false, /* showColumnNumber */ true)})`;
290-
} else if (frame.url) {
291-
result += ` (${frame.url}:${frame.line}:${frame.column})`;
292-
}
293-
return result;
315+
function formatAsyncFragment(
316+
fragment: DevTools.DevTools.StackTrace.StackTrace.AsyncFragment,
317+
formatter: {isIgnored: IgnoreCheck},
318+
): string[] {
319+
const formattedFrames = formatFragment(fragment, formatter);
320+
if (formattedFrames.length === 0) {
321+
return [];
294322
}
295323

296-
#formatCause(cause: SymbolizedError | undefined): string[] {
297-
if (!cause) {
298-
return [];
299-
}
324+
const separatorLineLength = 40;
325+
const prefix = `--- ${fragment.description || 'async'} `;
326+
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
327+
return [separator, ...formattedFrames];
328+
}
300329

301-
return [
302-
`Caused by: ${cause.message}`,
303-
...this.#formatStackTraceInner(cause.stackTrace, cause.cause),
304-
];
330+
function formatFrame(
331+
frame: DevTools.DevTools.StackTrace.StackTrace.Frame,
332+
): string {
333+
let result = `at ${frame.name ?? '<anonymous>'}`;
334+
if (frame.uiSourceCode) {
335+
const location = frame.uiSourceCode.uiLocation(frame.line, frame.column);
336+
result += ` (${location.linkText(/* skipTrim */ false, /* showColumnNumber */ true)})`;
337+
} else if (frame.url) {
338+
result += ` (${frame.url}:${frame.line}:${frame.column})`;
305339
}
340+
return result;
341+
}
306342

307-
toJSON(): object {
308-
return {
309-
type: this.#type,
310-
text: this.#text,
311-
argsCount: this.#argCount,
312-
id: this.#id,
313-
};
343+
function formatCause(
344+
cause: SymbolizedError | undefined,
345+
formatter: {isIgnored: IgnoreCheck},
346+
): string[] {
347+
if (!cause) {
348+
return [];
314349
}
315350

316-
toJSONDetailed(): object {
317-
return {
318-
id: this.#id,
319-
type: this.#type,
320-
text: this.#text,
321-
args: this.#getArgs().map(arg =>
322-
typeof arg === 'object' ? arg : String(arg),
323-
),
324-
stackTrace: this.#stack,
325-
};
326-
}
351+
return [
352+
`Caused by: ${cause.message}`,
353+
...formatStackTraceInner(cause.stackTrace, cause.cause, formatter),
354+
];
327355
}

0 commit comments

Comments
 (0)