Skip to content

Commit 82eaf3c

Browse files
committed
feat: better console formatting
1 parent 1f70eef commit 82eaf3c

3 files changed

Lines changed: 197 additions & 3 deletions

File tree

.husky/pre-commit

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/transports/console.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface ConsoleTransportOptions {
66
showTimestamps?: boolean
77
/** Whether to disable colors */
88
noColor?: boolean
9+
/** Custom formatter for message parts */
10+
formatMessagePart?: (value: any) => string
911
}
1012

1113
/**
@@ -19,6 +21,7 @@ export class ConsoleTransport implements Transport {
1921
this.options = {
2022
showTimestamps: false,
2123
noColor: false,
24+
formatMessagePart: undefined,
2225
...options,
2326
}
2427
}
@@ -52,13 +55,61 @@ export class ConsoleTransport implements Transport {
5255
? `<${this.options.noColor ? entry.category : pc.bold(entry.category)}> `
5356
: ""
5457

58+
// Format message parts
59+
const formattedMessage = this.formatMessageParts(entry.messageParts || [entry.message])
60+
5561
// Combine all parts
56-
const fullMessage = `${timestamp}${levelStr} ${category}${entry.message}`
62+
const fullMessage = `${timestamp}${levelStr} ${category}${formattedMessage}`
5763

5864
// Apply color to the entire log line based on severity
5965
return this.colorize(levelColor, fullMessage)
6066
}
6167

68+
/**
69+
* Format message parts according to their types
70+
*/
71+
private formatMessageParts(messageParts: any[]): string {
72+
if (!messageParts || messageParts.length === 0) {
73+
return ""
74+
}
75+
76+
return messageParts
77+
.map((part) => {
78+
// Use custom formatter if provided
79+
if (this.options.formatMessagePart) {
80+
return this.options.formatMessagePart(part)
81+
}
82+
83+
// Default formatting
84+
if (part === null) {
85+
return "null"
86+
}
87+
88+
if (part === undefined) {
89+
return "undefined"
90+
}
91+
92+
if (typeof part === "boolean") {
93+
return part ? "true" : "false"
94+
}
95+
96+
if (typeof part === "string" || typeof part === "number") {
97+
return String(part)
98+
}
99+
100+
if (typeof part === "object") {
101+
// Check if object has a non-default toString method
102+
if (part.toString && part.toString !== Object.prototype.toString) {
103+
return part.toString()
104+
}
105+
return JSON.stringify(part)
106+
}
107+
108+
return String(part)
109+
})
110+
.join(" ")
111+
}
112+
62113
/**
63114
* Apply color function if colors are enabled
64115
*/

test/console-transport.test.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,149 @@ describe("ConsoleTransport", () => {
148148
expect(formattedMsg).toContain("test-category");
149149
expect(formattedMsg).toContain(">");
150150
});
151+
152+
test("formats multiple message parts with spaces between them", () => {
153+
const entry: LogEntry = {
154+
timestamp: new Date(),
155+
level: LogLevel.INFO,
156+
message: "",
157+
messageParts: ["Hello", "world", 123]
158+
};
159+
160+
transport.write(entry);
161+
162+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
163+
expect(formattedMsg).toContain("Hello world 123");
164+
});
165+
166+
test("formats boolean values as 'true' or 'false'", () => {
167+
const entry: LogEntry = {
168+
timestamp: new Date(),
169+
level: LogLevel.INFO,
170+
message: "",
171+
messageParts: ["Status:", true, "Enabled:", false]
172+
};
173+
174+
transport.write(entry);
175+
176+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
177+
expect(formattedMsg).toContain("Status: true Enabled: false");
178+
});
179+
180+
test("formats objects using JSON.stringify", () => {
181+
const testObj = { name: "test", value: 42 };
182+
const entry: LogEntry = {
183+
timestamp: new Date(),
184+
level: LogLevel.INFO,
185+
message: "",
186+
messageParts: ["Object:", testObj]
187+
};
188+
189+
transport.write(entry);
190+
191+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
192+
expect(formattedMsg).toContain(`Object: ${JSON.stringify(testObj)}`);
193+
});
194+
195+
test("formats arrays using JSON.stringify", () => {
196+
const testArray = [1, 2, 3];
197+
const entry: LogEntry = {
198+
timestamp: new Date(),
199+
level: LogLevel.INFO,
200+
message: "",
201+
messageParts: ["Array:", testArray]
202+
};
203+
204+
transport.write(entry);
205+
206+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
207+
expect(formattedMsg).toContain(`Array: ${JSON.stringify(testArray)}`);
208+
});
209+
210+
test("handles null and undefined values", () => {
211+
const entry: LogEntry = {
212+
timestamp: new Date(),
213+
level: LogLevel.INFO,
214+
message: "",
215+
messageParts: ["Null:", null, "Undefined:", undefined]
216+
};
217+
218+
transport.write(entry);
219+
220+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
221+
expect(formattedMsg).toContain("Null: null Undefined: undefined");
222+
});
223+
224+
test("uses custom formatMessagePart function when provided", () => {
225+
transport = new ConsoleTransport({
226+
formatMessagePart: (value) => {
227+
if (typeof value === 'object' && value !== null) {
228+
return "OBJECT";
229+
}
230+
return String(value).toUpperCase();
231+
}
232+
});
233+
234+
const entry: LogEntry = {
235+
timestamp: new Date(),
236+
level: LogLevel.INFO,
237+
message: "",
238+
messageParts: ["test", 123, { a: 1 }]
239+
};
240+
241+
transport.write(entry);
242+
243+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
244+
expect(formattedMsg).toContain("TEST 123 OBJECT");
245+
});
246+
247+
test("falls back to message if messageParts is not provided", () => {
248+
const entry: LogEntry = {
249+
timestamp: new Date(),
250+
level: LogLevel.INFO,
251+
message: "Legacy message",
252+
messageParts: []
253+
};
254+
255+
transport.write(entry);
256+
257+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
258+
expect(formattedMsg).toContain("Legacy message");
259+
});
260+
261+
test("uses toString method when available on objects", () => {
262+
class CustomObject {
263+
toString() {
264+
return "CUSTOM_STRING_REPRESENTATION";
265+
}
266+
}
267+
268+
const customObj = new CustomObject();
269+
const entry: LogEntry = {
270+
timestamp: new Date(),
271+
level: LogLevel.INFO,
272+
message: "",
273+
messageParts: ["Custom object:", customObj]
274+
};
275+
276+
transport.write(entry);
277+
278+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
279+
expect(formattedMsg).toContain("Custom object: CUSTOM_STRING_REPRESENTATION");
280+
});
281+
282+
test("falls back to JSON.stringify when toString is not customized", () => {
283+
const regularObj = { id: 123, name: "test" };
284+
const entry: LogEntry = {
285+
timestamp: new Date(),
286+
level: LogLevel.INFO,
287+
message: "",
288+
messageParts: ["Regular object:", regularObj]
289+
};
290+
291+
transport.write(entry);
292+
293+
const [formattedMsg] = consoleLogSpy.mock.calls[0];
294+
expect(formattedMsg).toContain(`Regular object: ${JSON.stringify(regularObj)}`);
295+
});
151296
});

0 commit comments

Comments
 (0)