@logtape/pretty 1.4.0-dev.409 → 1.4.0-dev.417

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/src/formatter.ts DELETED
@@ -1,1070 +0,0 @@
1
- import {
2
- getLogLevels,
3
- type LogLevel,
4
- type LogRecord,
5
- type TextFormatter,
6
- type TextFormatterOptions,
7
- } from "@logtape/logtape";
8
- import { inspect, type InspectOptions } from "#util";
9
- import { getOptimalWordWrapWidth } from "./terminal.ts";
10
- import { truncateCategory, type TruncationStrategy } from "./truncate.ts";
11
- import { getDisplayWidth, stripAnsi } from "./wcwidth.ts";
12
- import { wrapText } from "./wordwrap.ts";
13
-
14
- /**
15
- * ANSI escape codes for styling
16
- */
17
- const RESET = "\x1b[0m";
18
- const DIM = "\x1b[2m";
19
-
20
- // Default true color values (referenced in JSDoc)
21
- const defaultColors = {
22
- trace: "rgb(167,139,250)", // Light purple
23
- debug: "rgb(96,165,250)", // Light blue
24
- info: "rgb(52,211,153)", // Emerald
25
- warning: "rgb(251,191,36)", // Amber
26
- error: "rgb(248,113,113)", // Light red
27
- fatal: "rgb(220,38,38)", // Dark red
28
- category: "rgb(100,116,139)", // Slate
29
- message: "rgb(148,163,184)", // Light slate
30
- timestamp: "rgb(100,116,139)", // Slate
31
- } as const;
32
-
33
- /**
34
- * ANSI style codes
35
- */
36
- const styles = {
37
- reset: RESET,
38
- bold: "\x1b[1m",
39
- dim: DIM,
40
- italic: "\x1b[3m",
41
- underline: "\x1b[4m",
42
- strikethrough: "\x1b[9m",
43
- } as const;
44
-
45
- /**
46
- * Standard ANSI colors (16-color)
47
- */
48
- const ansiColors = {
49
- black: "\x1b[30m",
50
- red: "\x1b[31m",
51
- green: "\x1b[32m",
52
- yellow: "\x1b[33m",
53
- blue: "\x1b[34m",
54
- magenta: "\x1b[35m",
55
- cyan: "\x1b[36m",
56
- white: "\x1b[37m",
57
- } as const;
58
-
59
- /**
60
- * Color type definition
61
- */
62
- export type Color =
63
- | keyof typeof ansiColors
64
- | `rgb(${number},${number},${number})`
65
- | `#${string}`
66
- | null;
67
-
68
- /**
69
- * Category color mapping for prefix-based coloring.
70
- *
71
- * Maps category prefixes (as arrays) to colors. The formatter will match
72
- * categories against these prefixes and use the corresponding color.
73
- * Longer/more specific prefixes take precedence over shorter ones.
74
- *
75
- * @example
76
- * ```typescript
77
- * new Map([
78
- * [["app", "auth"], "#ff6b6b"], // app.auth.* -> red
79
- * [["app", "db"], "#4ecdc4"], // app.db.* -> teal
80
- * [["app"], "#45b7d1"], // app.* (fallback) -> blue
81
- * [["lib"], "#96ceb4"], // lib.* -> green
82
- * ])
83
- * ```
84
- */
85
- export type CategoryColorMap = Map<readonly string[], Color>;
86
-
87
- /**
88
- * Internal representation of category prefix patterns
89
- */
90
- type CategoryPattern = {
91
- prefix: readonly string[];
92
- color: Color;
93
- };
94
-
95
- /**
96
- * Style type definition - supports single styles, arrays of styles, or null
97
- */
98
- export type Style = keyof typeof styles | (keyof typeof styles)[] | null;
99
-
100
- // Pre-compiled regex patterns for color parsing
101
- const RGB_PATTERN = /^rgb\((\d+),(\d+),(\d+)\)$/;
102
- const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
103
-
104
- /**
105
- * Helper function to convert color to ANSI escape code
106
- */
107
- function colorToAnsi(color: Color): string {
108
- if (color === null) return "";
109
- if (color in ansiColors) {
110
- return ansiColors[color as keyof typeof ansiColors];
111
- }
112
-
113
- // Handle rgb() format
114
- const rgbMatch = color.match(RGB_PATTERN);
115
- if (rgbMatch) {
116
- const [, r, g, b] = rgbMatch;
117
- return `\x1b[38;2;${r};${g};${b}m`;
118
- }
119
-
120
- // Handle hex format (#rrggbb or #rgb)
121
- const hexMatch = color.match(HEX_PATTERN);
122
- if (hexMatch) {
123
- let hex = hexMatch[1];
124
- // Convert 3-digit hex to 6-digit
125
- if (hex.length === 3) {
126
- hex = hex.split("").map((c) => c + c).join("");
127
- }
128
- const r = parseInt(hex.substr(0, 2), 16);
129
- const g = parseInt(hex.substr(2, 2), 16);
130
- const b = parseInt(hex.substr(4, 2), 16);
131
- return `\x1b[38;2;${r};${g};${b}m`;
132
- }
133
-
134
- return "";
135
- }
136
-
137
- /**
138
- * Helper function to convert style to ANSI escape code
139
- */
140
- function styleToAnsi(style: Style): string {
141
- if (style === null) return "";
142
- if (Array.isArray(style)) {
143
- return style.map((s) => styles[s] || "").join("");
144
- }
145
- return styles[style] || "";
146
- }
147
-
148
- /**
149
- * Converts a category color map to internal patterns and sorts them by specificity.
150
- * More specific (longer) prefixes come first for proper matching precedence.
151
- */
152
- function prepareCategoryPatterns(
153
- categoryColorMap: CategoryColorMap,
154
- ): CategoryPattern[] {
155
- const patterns: CategoryPattern[] = [];
156
-
157
- for (const [prefix, color] of categoryColorMap) {
158
- patterns.push({ prefix, color });
159
- }
160
-
161
- // Sort by prefix length (descending) for most-specific-first matching
162
- return patterns.sort((a, b) => b.prefix.length - a.prefix.length);
163
- }
164
-
165
- /**
166
- * Matches a category against category color patterns.
167
- * Returns the color of the first matching pattern, or null if no match.
168
- */
169
- function matchCategoryColor(
170
- category: readonly string[],
171
- patterns: CategoryPattern[],
172
- ): Color {
173
- for (const pattern of patterns) {
174
- if (categoryMatches(category, pattern.prefix)) {
175
- return pattern.color;
176
- }
177
- }
178
- return null;
179
- }
180
-
181
- /**
182
- * Checks if a category matches a prefix pattern.
183
- * A category matches if it starts with all segments of the prefix.
184
- */
185
- function categoryMatches(
186
- category: readonly string[],
187
- prefix: readonly string[],
188
- ): boolean {
189
- if (prefix.length > category.length) {
190
- return false;
191
- }
192
-
193
- for (let i = 0; i < prefix.length; i++) {
194
- if (category[i] !== prefix[i]) {
195
- return false;
196
- }
197
- }
198
-
199
- return true;
200
- }
201
-
202
- /**
203
- * Default icons for each log level
204
- */
205
- const defaultIcons: Record<LogLevel, string> = {
206
- trace: "🔍",
207
- debug: "🐛",
208
- info: "✨",
209
- warning: "⚡",
210
- error: "❌",
211
- fatal: "💀",
212
- };
213
-
214
- /**
215
- * Normalize icon spacing to ensure consistent column alignment.
216
- *
217
- * All icons will be padded with spaces to match the width of the widest icon,
218
- * ensuring consistent prefix alignment across all log levels.
219
- *
220
- * @param iconMap The icon mapping to normalize
221
- * @returns A new icon map with consistent spacing
222
- */
223
- function normalizeIconSpacing(
224
- iconMap: Record<LogLevel, string>,
225
- ): Record<LogLevel, string> {
226
- const entries = Object.entries(iconMap) as Array<[LogLevel, string]>;
227
- const maxWidth = Math.max(
228
- ...entries.map(([, icon]) => getDisplayWidth(icon)),
229
- );
230
-
231
- return Object.fromEntries(
232
- entries.map(([level, icon]) => [
233
- level,
234
- icon + " ".repeat(maxWidth - getDisplayWidth(icon)),
235
- ]),
236
- ) as Record<LogLevel, string>;
237
- }
238
-
239
- /**
240
- * Configuration options for the pretty formatter.
241
- *
242
- * This interface extends the base text formatter options while providing
243
- * extensive customization options for visual styling, layout control, and
244
- * development-focused features. It offers granular control over colors,
245
- * styles, and formatting similar to the ANSI color formatter.
246
- *
247
- * @since 1.0.0
248
- */
249
- export interface PrettyFormatterOptions
250
- extends Omit<TextFormatterOptions, "category" | "value" | "format"> {
251
- /**
252
- * Color for timestamp display when timestamps are enabled.
253
- *
254
- * Supports true color RGB values, hex colors, or ANSI color names.
255
- * Set to `null` to disable timestamp coloring.
256
- *
257
- * @example
258
- * ```typescript
259
- * timestampColor: "#888888" // Hex color
260
- * timestampColor: "rgb(128,128,128)" // RGB color
261
- * timestampColor: "cyan" // ANSI color name
262
- * timestampColor: null // No color
263
- * ```
264
- *
265
- * @default `"rgb(100,116,139)"` (slate gray)
266
- */
267
- readonly timestampColor?: Color;
268
-
269
- /**
270
- * Visual style applied to timestamp text.
271
- *
272
- * Controls text appearance like boldness, dimming, etc.
273
- * Supports single styles, multiple styles combined, or no styling.
274
- * Combines with `timestampColor` for full styling control.
275
- *
276
- * @example
277
- * ```typescript
278
- * timestampStyle: "dim" // Single style: dimmed text
279
- * timestampStyle: "bold" // Single style: bold text
280
- * timestampStyle: ["bold", "underline"] // Multiple styles: bold + underlined
281
- * timestampStyle: ["dim", "italic"] // Multiple styles: dimmed + italic
282
- * timestampStyle: null // No styling
283
- * ```
284
- *
285
- * @default `"dim"`
286
- */
287
- readonly timestampStyle?: Style;
288
-
289
- /**
290
- * Custom colors for each log level.
291
- *
292
- * Allows fine-grained control over level appearance. Each level can have
293
- * its own color scheme. Unspecified levels use built-in defaults.
294
- * Set individual levels to `null` to disable coloring for that level.
295
- *
296
- * @example
297
- * ```typescript
298
- * levelColors: {
299
- * info: "#00ff00", // Bright green for info
300
- * error: "#ff0000", // Bright red for errors
301
- * warning: "orange", // ANSI orange for warnings
302
- * debug: null, // No color for debug
303
- * }
304
- * ```
305
- *
306
- * @default Built-in color scheme (purple trace, blue debug, green info, amber warning, red error, dark red fatal)
307
- */
308
- readonly levelColors?: Partial<Record<LogLevel, Color>>;
309
-
310
- /**
311
- * Visual style applied to log level text.
312
- *
313
- * Controls the appearance of the level indicator (e.g., "info", "error").
314
- * Supports single styles, multiple styles combined, or no styling.
315
- * Applied in addition to level-specific colors.
316
- *
317
- * @example
318
- * ```typescript
319
- * levelStyle: "bold" // Single style: bold level text
320
- * levelStyle: "underline" // Single style: underlined level text
321
- * levelStyle: ["bold", "underline"] // Multiple styles: bold + underlined
322
- * levelStyle: ["dim", "italic"] // Multiple styles: dimmed + italic
323
- * levelStyle: null // No additional styling
324
- * ```
325
- *
326
- * @default `"underline"`
327
- */
328
- readonly levelStyle?: Style;
329
-
330
- /**
331
- * Icon configuration for each log level.
332
- *
333
- * Controls the emoji/symbol displayed before each log entry.
334
- * Provides visual quick-identification of log severity.
335
- *
336
- * - `true`: Use built-in emoji set (🔍 trace, 🐛 debug, ✨ info, ⚠️ warning, ❌ error, 💀 fatal)
337
- * - `false`: Disable all icons for clean text-only output
338
- * - Object: Custom icon mapping, falls back to defaults for unspecified levels
339
- *
340
- * @example
341
- * ```typescript
342
- * icons: true // Use default emoji set
343
- * icons: false // No icons
344
- * icons: {
345
- * info: "ℹ️", // Custom info icon
346
- * error: "🔥", // Custom error icon
347
- * warning: "⚡", // Custom warning icon
348
- * }
349
- * ```
350
- *
351
- * @default `true` (use default emoji icons)
352
- */
353
- readonly icons?: boolean | Partial<Record<LogLevel, string>>;
354
-
355
- /**
356
- * Character(s) used to separate category hierarchy levels.
357
- *
358
- * Categories are hierarchical (e.g., ["app", "auth", "jwt"]) and this
359
- * separator joins them for display (e.g., "app.auth.jwt").
360
- *
361
- * @example
362
- * ```typescript
363
- * categorySeparator: "·" // app·auth·jwt
364
- * categorySeparator: "." // app.auth.jwt
365
- * categorySeparator: ":" // app:auth:jwt
366
- * categorySeparator: " > " // app > auth > jwt
367
- * categorySeparator: "::" // app::auth::jwt
368
- * ```
369
- *
370
- * @default `"·"` (interpunct)
371
- */
372
- readonly categorySeparator?: string;
373
-
374
- /**
375
- * Default color for category display.
376
- *
377
- * Used as fallback when no specific color is found in `categoryColorMap`.
378
- * Controls the visual appearance of the category hierarchy display.
379
- *
380
- * @example
381
- * ```typescript
382
- * categoryColor: "#666666" // Gray categories
383
- * categoryColor: "blue" // Blue categories
384
- * categoryColor: "rgb(100,150,200)" // Light blue categories
385
- * categoryColor: null // No coloring
386
- * ```
387
- *
388
- * @default `"rgb(100,116,139)"` (slate gray)
389
- */
390
- readonly categoryColor?: Color;
391
-
392
- /**
393
- * Category-specific color mapping based on prefixes.
394
- *
395
- * Maps category prefixes (as arrays) to colors for visual grouping.
396
- * More specific (longer) prefixes take precedence over shorter ones.
397
- * If no prefix matches, falls back to the default `categoryColor`.
398
- *
399
- * @example
400
- * ```typescript
401
- * new Map([
402
- * [["app", "auth"], "#ff6b6b"], // app.auth.* -> red
403
- * [["app", "db"], "#4ecdc4"], // app.db.* -> teal
404
- * [["app"], "#45b7d1"], // app.* (fallback) -> blue
405
- * [["lib"], "#96ceb4"], // lib.* -> green
406
- * ])
407
- * ```
408
- */
409
- readonly categoryColorMap?: CategoryColorMap;
410
-
411
- /**
412
- * Visual style applied to category text.
413
- *
414
- * Controls the appearance of the category hierarchy display.
415
- * Supports single styles, multiple styles combined, or no styling.
416
- * Applied in addition to category colors from `categoryColor` or `categoryColorMap`.
417
- *
418
- * @example
419
- * ```typescript
420
- * categoryStyle: "dim" // Single style: dimmed category text
421
- * categoryStyle: "italic" // Single style: italic category text
422
- * categoryStyle: ["dim", "italic"] // Multiple styles: dimmed + italic
423
- * categoryStyle: ["bold", "underline"] // Multiple styles: bold + underlined
424
- * categoryStyle: null // No additional styling
425
- * ```
426
- *
427
- * @default `["dim", "italic"]` (dimmed for subtle appearance)
428
- */
429
- readonly categoryStyle?: Style;
430
-
431
- /**
432
- * Maximum display width for category names.
433
- *
434
- * Controls layout consistency by limiting category width.
435
- * Long categories are truncated according to `categoryTruncate` strategy.
436
- *
437
- * @default `20`
438
- */
439
- readonly categoryWidth?: number;
440
-
441
- /**
442
- * Strategy for truncating long category names.
443
- *
444
- * When categories exceed `categoryWidth`, this controls how truncation works.
445
- * Smart truncation preserves important context while maintaining layout.
446
- *
447
- * - `"middle"`: Keep first and last parts (e.g., "app.server…auth.jwt")
448
- * - `"end"`: Truncate at the end (e.g., "app.server.middleware…")
449
- * - `false`: No truncation (ignores `categoryWidth`)
450
- *
451
- * @example
452
- * ```typescript
453
- * categoryTruncate: "middle" // app.server…jwt (preserves context)
454
- * categoryTruncate: "end" // app.server.midd… (linear truncation)
455
- * categoryTruncate: false // app.server.middleware.auth.jwt (full)
456
- * ```
457
- *
458
- * @default `"middle"` (smart context-preserving truncation)
459
- */
460
- readonly categoryTruncate?: TruncationStrategy;
461
-
462
- /**
463
- * Color for log message text content.
464
- *
465
- * Controls the visual appearance of the actual log message content.
466
- * Does not affect structured values, which use syntax highlighting.
467
- *
468
- * @example
469
- * ```typescript
470
- * messageColor: "#ffffff" // White message text
471
- * messageColor: "green" // Green message text
472
- * messageColor: "rgb(200,200,200)" // Light gray message text
473
- * messageColor: null // No coloring
474
- * ```
475
- *
476
- * @default `"rgb(148,163,184)"` (light slate gray)
477
- */
478
- readonly messageColor?: Color;
479
-
480
- /**
481
- * Visual style applied to log message text.
482
- *
483
- * Controls the appearance of the log message content.
484
- * Supports single styles, multiple styles combined, or no styling.
485
- * Applied in addition to `messageColor`.
486
- *
487
- * @example
488
- * ```typescript
489
- * messageStyle: "dim" // Single style: dimmed message text
490
- * messageStyle: "italic" // Single style: italic message text
491
- * messageStyle: ["dim", "italic"] // Multiple styles: dimmed + italic
492
- * messageStyle: ["bold", "underline"] // Multiple styles: bold + underlined
493
- * messageStyle: null // No additional styling
494
- * ```
495
- *
496
- * @default `"dim"` (dimmed for subtle readability)
497
- */
498
- readonly messageStyle?: Style;
499
-
500
- /**
501
- * Global color control for the entire formatter.
502
- *
503
- * Master switch to enable/disable all color output.
504
- * When disabled, produces clean monochrome output suitable for
505
- * non-color terminals or when colors are not desired.
506
- *
507
- * @example
508
- * ```typescript
509
- * colors: true // Full color output (default)
510
- * colors: false // Monochrome output only
511
- * ```
512
- *
513
- * @default `true` (colors enabled)
514
- */
515
- readonly colors?: boolean;
516
-
517
- /**
518
- * Column alignment for consistent visual layout.
519
- *
520
- * When enabled, ensures all log components (icons, levels, categories)
521
- * align consistently across multiple log entries, creating a clean
522
- * tabular appearance.
523
- *
524
- * @example
525
- * ```typescript
526
- * align: true // Aligned columns (default)
527
- * align: false // Compact, non-aligned output
528
- * ```
529
- *
530
- * @default `true` (alignment enabled)
531
- */
532
- readonly align?: boolean;
533
-
534
- /**
535
- * Configuration for structured value inspection and rendering.
536
- *
537
- * Controls how objects, arrays, and other complex values are displayed
538
- * within log messages. Uses Node.js `util.inspect()` style options.
539
- *
540
- * @example
541
- * ```typescript
542
- * inspectOptions: {
543
- * depth: 3, // Show 3 levels of nesting
544
- * colors: false, // Disable value syntax highlighting
545
- * compact: true, // Use compact object display
546
- * }
547
- * ```
548
- *
549
- * @default `{}` (use built-in defaults: depth=unlimited, colors=auto, compact=true)
550
- */
551
- readonly inspectOptions?: {
552
- /**
553
- * Maximum depth to traverse when inspecting nested objects.
554
- * @default Infinity (no depth limit)
555
- */
556
- readonly depth?: number;
557
-
558
- /**
559
- * Whether to use syntax highlighting colors for inspected values.
560
- * @default Inherited from global `colors` setting
561
- */
562
- readonly colors?: boolean;
563
-
564
- /**
565
- * Whether to use compact formatting for objects and arrays.
566
- * @default `true` (compact formatting)
567
- */
568
- readonly compact?: boolean;
569
-
570
- /**
571
- * Whether to invoke getter functions during inspection.
572
- * (Node.js, Deno, and Bun only; browsers fall back to default behavior.)
573
- *
574
- * @default Inherited from runtime defaults
575
- * @since 1.2.0
576
- */
577
- readonly getters?: boolean;
578
-
579
- /**
580
- * Whether to show Proxy objects with their target and handler.
581
- * (Node.js, Deno, and Bun only; browsers fall back to default behavior.)
582
- *
583
- * @default Inherited from runtime defaults
584
- * @since 1.2.0
585
- */
586
- readonly showProxy?: boolean;
587
- };
588
-
589
- /**
590
- * Configuration to always render structured data.
591
- *
592
- * If set to `true`, any structured data that is logged will
593
- * always be rendered. This can be very verbose. Make sure
594
- * to configure `inspectOptions` properly for your usecase.
595
- *
596
- * @default `false`
597
- * @since 1.1.0
598
- */
599
- readonly properties?: boolean;
600
-
601
- /**
602
- * Enable word wrapping for long messages.
603
- *
604
- * When enabled, long messages will be wrapped at the specified width,
605
- * with continuation lines aligned to the message column position.
606
- *
607
- * - `true`: Auto-detect terminal width when attached to a terminal,
608
- * fallback to 80 columns when not in a terminal or detection fails
609
- * - `number`: Use the specified width in columns
610
- * - `false`: Disable word wrapping
611
- *
612
- * @example
613
- * ```typescript
614
- * // Auto-detect terminal width (recommended)
615
- * wordWrap: true
616
- *
617
- * // Custom wrap width
618
- * wordWrap: 120
619
- *
620
- * // Disable word wrapping (default)
621
- * wordWrap: false
622
- * ```
623
- *
624
- * @default `true` (auto-detect terminal width)
625
- * @since 1.0.0
626
- */
627
- readonly wordWrap?: boolean | number;
628
- }
629
-
630
- /**
631
- * Creates a beautiful console formatter optimized for local development.
632
- *
633
- * This formatter provides a Signale-inspired visual design with colorful icons,
634
- * smart category truncation, dimmed styling, and perfect column alignment.
635
- * It's specifically designed for development environments that support true colors
636
- * and Unicode characters.
637
- *
638
- * The formatter features:
639
- * - Emoji icons for each log level (🔍 trace, 🐛 debug, ✨ info, etc.)
640
- * - True color support with rich color schemes
641
- * - Intelligent category truncation for long hierarchical categories
642
- * - Optional timestamp display with multiple formats
643
- * - Configurable alignment and styling options
644
- * - Enhanced value rendering with syntax highlighting
645
- *
646
- * @param options Configuration options for customizing the formatter behavior.
647
- * @returns A text formatter function that can be used with LogTape sinks.
648
- *
649
- * @example
650
- * ```typescript
651
- * import { configure } from "@logtape/logtape";
652
- * import { getConsoleSink } from "@logtape/logtape/sink";
653
- * import { getPrettyFormatter } from "@logtape/pretty";
654
- *
655
- * await configure({
656
- * sinks: {
657
- * console: getConsoleSink({
658
- * formatter: getPrettyFormatter({
659
- * timestamp: "time",
660
- * categoryWidth: 25,
661
- * icons: {
662
- * info: "📘",
663
- * error: "🔥"
664
- * }
665
- * })
666
- * })
667
- * }
668
- * });
669
- * ```
670
- *
671
- * @since 1.0.0
672
- */
673
- export function getPrettyFormatter(
674
- options: PrettyFormatterOptions = {},
675
- ): TextFormatter {
676
- // Extract options with defaults
677
- const {
678
- timestamp = "none",
679
- timestampColor = "rgb(100,116,139)",
680
- timestampStyle = "dim",
681
- level: levelFormat = "full",
682
- levelColors = {},
683
- levelStyle = "underline",
684
- icons = true,
685
- categorySeparator = "·",
686
- categoryColor = "rgb(100,116,139)",
687
- categoryColorMap = new Map(),
688
- categoryStyle = ["dim", "italic"],
689
- categoryWidth = 20,
690
- categoryTruncate = "middle",
691
- messageColor = "rgb(148,163,184)",
692
- messageStyle = "dim",
693
- colors: useColors = true,
694
- align = true,
695
- inspectOptions = {},
696
- properties = false,
697
- wordWrap = true,
698
- } = options;
699
-
700
- // Resolve icons
701
- const baseIconMap: Record<LogLevel, string> = icons === false
702
- ? { trace: "", debug: "", info: "", warning: "", error: "", fatal: "" }
703
- : icons === true
704
- ? defaultIcons
705
- : { ...defaultIcons, ...(icons as Partial<Record<LogLevel, string>>) };
706
-
707
- // Normalize icon spacing for consistent alignment
708
- const iconMap = normalizeIconSpacing(baseIconMap);
709
-
710
- // Resolve level colors with defaults
711
- const resolvedLevelColors: Record<LogLevel, Color> = {
712
- trace: defaultColors.trace,
713
- debug: defaultColors.debug,
714
- info: defaultColors.info,
715
- warning: defaultColors.warning,
716
- error: defaultColors.error,
717
- fatal: defaultColors.fatal,
718
- ...levelColors,
719
- };
720
-
721
- // Level formatter function with optimized mappings
722
- const levelMappings: Record<string, Record<LogLevel, string>> = {
723
- "ABBR": {
724
- trace: "TRC",
725
- debug: "DBG",
726
- info: "INF",
727
- warning: "WRN",
728
- error: "ERR",
729
- fatal: "FTL",
730
- },
731
- "L": {
732
- trace: "T",
733
- debug: "D",
734
- info: "I",
735
- warning: "W",
736
- error: "E",
737
- fatal: "F",
738
- },
739
- "abbr": {
740
- trace: "trc",
741
- debug: "dbg",
742
- info: "inf",
743
- warning: "wrn",
744
- error: "err",
745
- fatal: "ftl",
746
- },
747
- "l": {
748
- trace: "t",
749
- debug: "d",
750
- info: "i",
751
- warning: "w",
752
- error: "e",
753
- fatal: "f",
754
- },
755
- };
756
-
757
- const formatLevel = (level: LogLevel): string => {
758
- if (typeof levelFormat === "function") {
759
- return levelFormat(level);
760
- }
761
-
762
- if (levelFormat === "FULL") return level.toUpperCase();
763
- if (levelFormat === "full") return level;
764
-
765
- return levelMappings[levelFormat]?.[level] ?? level;
766
- };
767
-
768
- // Timestamp formatters lookup table
769
- const timestampFormatters: Record<string, (ts: number) => string> = {
770
- "date-time-timezone": (ts) => {
771
- const iso = new Date(ts).toISOString();
772
- return iso.replace("T", " ").replace("Z", " +00:00");
773
- },
774
- "date-time-tz": (ts) => {
775
- const iso = new Date(ts).toISOString();
776
- return iso.replace("T", " ").replace("Z", " +00");
777
- },
778
- "date-time": (ts) => {
779
- const iso = new Date(ts).toISOString();
780
- return iso.replace("T", " ").replace("Z", "");
781
- },
782
- "time-timezone": (ts) => {
783
- const iso = new Date(ts).toISOString();
784
- return iso.replace(/.*T/, "").replace("Z", " +00:00");
785
- },
786
- "time-tz": (ts) => {
787
- const iso = new Date(ts).toISOString();
788
- return iso.replace(/.*T/, "").replace("Z", " +00");
789
- },
790
- "time": (ts) => {
791
- const iso = new Date(ts).toISOString();
792
- return iso.replace(/.*T/, "").replace("Z", "");
793
- },
794
- "date": (ts) => new Date(ts).toISOString().replace(/T.*/, ""),
795
- "rfc3339": (ts) => new Date(ts).toISOString(),
796
- };
797
-
798
- // Resolve timestamp formatter
799
- let timestampFn: ((ts: number) => string | null) | null = null;
800
- if (timestamp === "none" || timestamp === "disabled") {
801
- timestampFn = null;
802
- } else if (typeof timestamp === "function") {
803
- timestampFn = timestamp;
804
- } else {
805
- timestampFn = timestampFormatters[timestamp as string] ?? null;
806
- }
807
-
808
- // Configure word wrap settings
809
- const wordWrapEnabled = wordWrap !== false;
810
- let wordWrapWidth: number;
811
-
812
- if (typeof wordWrap === "number") {
813
- wordWrapWidth = wordWrap;
814
- } else if (wordWrap === true) {
815
- // Auto-detect terminal width
816
- wordWrapWidth = getOptimalWordWrapWidth(80);
817
- } else {
818
- wordWrapWidth = 80; // Default fallback
819
- }
820
-
821
- // Prepare category color patterns for matching
822
- const categoryPatterns = prepareCategoryPatterns(categoryColorMap);
823
-
824
- // Calculate level width based on format
825
- const allLevels: LogLevel[] = [...getLogLevels()];
826
- const levelWidth = Math.max(...allLevels.map((l) => formatLevel(l).length));
827
-
828
- return (record: LogRecord): string => {
829
- // Calculate the prefix parts first to determine message column position
830
- const icon = iconMap[record.level] || "";
831
- const level = formatLevel(record.level);
832
- const categoryStr = truncateCategory(
833
- record.category,
834
- categoryWidth,
835
- categorySeparator,
836
- categoryTruncate,
837
- );
838
-
839
- // Format message with values - handle color reset/reapply for interpolated values
840
- let message = "";
841
- const messageColorCode = useColors ? colorToAnsi(messageColor) : "";
842
- const messageStyleCode = useColors ? styleToAnsi(messageStyle) : "";
843
- const messagePrefix = useColors
844
- ? `${messageStyleCode}${messageColorCode}`
845
- : "";
846
-
847
- for (let i = 0; i < record.message.length; i++) {
848
- if (i % 2 === 0) {
849
- message += record.message[i];
850
- } else {
851
- const value = record.message[i];
852
- const inspected = inspect(value, {
853
- colors: useColors,
854
- ...inspectOptions,
855
- });
856
-
857
- // Handle multiline interpolated values properly
858
- if (inspected.includes("\n")) {
859
- const lines = inspected.split("\n");
860
-
861
- const formattedLines = lines.map((line, index) => {
862
- if (index === 0) {
863
- // First line: reset formatting, add the line, then reapply
864
- if (useColors && (messageColorCode || messageStyleCode)) {
865
- return `${RESET}${line}${messagePrefix}`;
866
- } else {
867
- return line;
868
- }
869
- } else {
870
- // Continuation lines: just apply formatting, let wrapText handle indentation
871
- if (useColors && (messageColorCode || messageStyleCode)) {
872
- return `${line}${messagePrefix}`;
873
- } else {
874
- return line;
875
- }
876
- }
877
- });
878
- message += formattedLines.join("\n");
879
- } else {
880
- // Single line - handle normally
881
- if (useColors && (messageColorCode || messageStyleCode)) {
882
- message += `${RESET}${inspected}${messagePrefix}`;
883
- } else {
884
- message += inspected;
885
- }
886
- }
887
- }
888
- }
889
-
890
- // Parts are already calculated above
891
-
892
- // Determine category color (with prefix matching)
893
- const finalCategoryColor = useColors
894
- ? (matchCategoryColor(record.category, categoryPatterns) || categoryColor)
895
- : null;
896
-
897
- // Apply colors and styling
898
- const formattedIcon = icon;
899
- let formattedLevel = level;
900
- let formattedCategory = categoryStr;
901
- let formattedMessage = message;
902
- let formattedTimestamp = "";
903
-
904
- if (useColors) {
905
- // Apply level color and style
906
- const levelColorCode = colorToAnsi(resolvedLevelColors[record.level]);
907
- const levelStyleCode = styleToAnsi(levelStyle);
908
- formattedLevel = `${levelStyleCode}${levelColorCode}${level}${RESET}`;
909
-
910
- // Apply category color and style (with prefix matching)
911
- const categoryColorCode = colorToAnsi(finalCategoryColor);
912
- const categoryStyleCode = styleToAnsi(categoryStyle);
913
- formattedCategory =
914
- `${categoryStyleCode}${categoryColorCode}${categoryStr}${RESET}`;
915
-
916
- // Apply message color and style (already handled in message building above)
917
- formattedMessage = `${messagePrefix}${message}${RESET}`;
918
- }
919
-
920
- // Format timestamp if needed
921
- if (timestampFn) {
922
- const ts = timestampFn(record.timestamp);
923
- if (ts !== null) {
924
- if (useColors) {
925
- const timestampColorCode = colorToAnsi(timestampColor);
926
- const timestampStyleCode = styleToAnsi(timestampStyle);
927
- formattedTimestamp =
928
- `${timestampStyleCode}${timestampColorCode}${ts}${RESET} `;
929
- } else {
930
- formattedTimestamp = `${ts} `;
931
- }
932
- }
933
- }
934
-
935
- // Build the final output with alignment
936
- if (align) {
937
- // Calculate padding accounting for ANSI escape sequences
938
- const levelColorLength = useColors
939
- ? (colorToAnsi(resolvedLevelColors[record.level]).length +
940
- styleToAnsi(levelStyle).length + RESET.length)
941
- : 0;
942
- const categoryColorLength = useColors
943
- ? (colorToAnsi(finalCategoryColor).length +
944
- styleToAnsi(categoryStyle).length + RESET.length)
945
- : 0;
946
-
947
- const paddedLevel = formattedLevel.padEnd(levelWidth + levelColorLength);
948
- const paddedCategory = formattedCategory.padEnd(
949
- categoryWidth + categoryColorLength,
950
- );
951
-
952
- let result =
953
- `${formattedTimestamp}${formattedIcon} ${paddedLevel} ${paddedCategory} ${formattedMessage}`;
954
- const indentWidth = getDisplayWidth(
955
- stripAnsi(
956
- `${formattedTimestamp}${formattedIcon} ${paddedLevel} ${paddedCategory} `,
957
- ),
958
- );
959
-
960
- // Apply word wrapping if enabled, or if there are multiline interpolated values
961
- if (wordWrapEnabled || message.includes("\n")) {
962
- result = wrapText(
963
- result,
964
- wordWrapEnabled ? wordWrapWidth : Infinity,
965
- indentWidth,
966
- );
967
- }
968
-
969
- if (properties) {
970
- result += formatProperties(
971
- record,
972
- indentWidth,
973
- wordWrapEnabled ? wordWrapWidth : Infinity,
974
- useColors,
975
- inspectOptions,
976
- );
977
- }
978
-
979
- return result + "\n";
980
- } else {
981
- let result =
982
- `${formattedTimestamp}${formattedIcon} ${formattedLevel} ${formattedCategory} ${formattedMessage}`;
983
- const indentWidth = getDisplayWidth(
984
- stripAnsi(
985
- `${formattedTimestamp}${formattedIcon} ${formattedLevel} ${formattedCategory} `,
986
- ),
987
- );
988
-
989
- // Apply word wrapping if enabled, or if there are multiline interpolated values
990
- if (wordWrapEnabled || message.includes("\n")) {
991
- result = wrapText(
992
- result,
993
- wordWrapEnabled ? wordWrapWidth : Infinity,
994
- indentWidth,
995
- );
996
- }
997
-
998
- if (properties) {
999
- result += formatProperties(
1000
- record,
1001
- indentWidth,
1002
- wordWrapEnabled ? wordWrapWidth : Infinity,
1003
- useColors,
1004
- inspectOptions,
1005
- );
1006
- }
1007
-
1008
- return result + "\n";
1009
- }
1010
- };
1011
- }
1012
-
1013
- function formatProperties(
1014
- record: LogRecord,
1015
- indentWidth: number,
1016
- maxWidth: number,
1017
- useColors: boolean,
1018
- inspectOptions: InspectOptions,
1019
- ): string {
1020
- let result = "";
1021
- for (const prop in record.properties) {
1022
- const propValue = record.properties[prop];
1023
- // Ensure padding is never negative
1024
- const pad = Math.max(0, indentWidth - getDisplayWidth(prop) - 2);
1025
- result += "\n" + wrapText(
1026
- `${" ".repeat(pad)}${useColors ? DIM : ""}${prop}:${
1027
- useColors ? RESET : ""
1028
- } ${inspect(propValue, { colors: useColors, ...inspectOptions })}`,
1029
- maxWidth,
1030
- indentWidth,
1031
- );
1032
- }
1033
- return result;
1034
- }
1035
-
1036
- /**
1037
- * A pre-configured beautiful console formatter for local development.
1038
- *
1039
- * This is a ready-to-use instance of the pretty formatter with sensible defaults
1040
- * for most development scenarios. It provides immediate visual enhancement to
1041
- * your logs without requiring any configuration.
1042
- *
1043
- * Features enabled by default:
1044
- * - Emoji icons for all log levels
1045
- * - True color support with rich color schemes
1046
- * - Dimmed text styling for better readability
1047
- * - Smart category truncation (20 characters max)
1048
- * - Perfect column alignment
1049
- * - No timestamp display (cleaner for development)
1050
- *
1051
- * For custom configuration, use {@link getPrettyFormatter} instead.
1052
- *
1053
- * @example
1054
- * ```typescript
1055
- * import { configure } from "@logtape/logtape";
1056
- * import { getConsoleSink } from "@logtape/logtape/sink";
1057
- * import { prettyFormatter } from "@logtape/pretty";
1058
- *
1059
- * await configure({
1060
- * sinks: {
1061
- * console: getConsoleSink({
1062
- * formatter: prettyFormatter
1063
- * })
1064
- * }
1065
- * });
1066
- * ```
1067
- *
1068
- * @since 1.0.0
1069
- */
1070
- export const prettyFormatter: TextFormatter = getPrettyFormatter();