@logtape/pretty 1.0.0-dev.231

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.
@@ -0,0 +1,412 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const require_terminal = require('./terminal.cjs');
3
+ const require_truncate = require('./truncate.cjs');
4
+ const require_wcwidth = require('./wcwidth.cjs');
5
+ const require_wordwrap = require('./wordwrap.cjs');
6
+ const node_util = require_rolldown_runtime.__toESM(require("node:util"));
7
+
8
+ //#region formatter.ts
9
+ /**
10
+ * ANSI escape codes for styling
11
+ */
12
+ const RESET = "\x1B[0m";
13
+ const DIM = "\x1B[2m";
14
+ const defaultColors = {
15
+ trace: "rgb(167,139,250)",
16
+ debug: "rgb(96,165,250)",
17
+ info: "rgb(52,211,153)",
18
+ warning: "rgb(251,191,36)",
19
+ error: "rgb(248,113,113)",
20
+ fatal: "rgb(220,38,38)",
21
+ category: "rgb(100,116,139)",
22
+ message: "rgb(148,163,184)",
23
+ timestamp: "rgb(100,116,139)"
24
+ };
25
+ /**
26
+ * ANSI style codes
27
+ */
28
+ const styles = {
29
+ reset: RESET,
30
+ bold: "\x1B[1m",
31
+ dim: DIM,
32
+ italic: "\x1B[3m",
33
+ underline: "\x1B[4m",
34
+ strikethrough: "\x1B[9m"
35
+ };
36
+ /**
37
+ * Standard ANSI colors (16-color)
38
+ */
39
+ const ansiColors = {
40
+ black: "\x1B[30m",
41
+ red: "\x1B[31m",
42
+ green: "\x1B[32m",
43
+ yellow: "\x1B[33m",
44
+ blue: "\x1B[34m",
45
+ magenta: "\x1B[35m",
46
+ cyan: "\x1B[36m",
47
+ white: "\x1B[37m"
48
+ };
49
+ /**
50
+ * Helper function to convert color to ANSI escape code
51
+ */
52
+ function colorToAnsi(color) {
53
+ if (color === null) return "";
54
+ if (color in ansiColors) return ansiColors[color];
55
+ const rgbMatch = color.match(/^rgb\((\d+),(\d+),(\d+)\)$/);
56
+ if (rgbMatch) {
57
+ const [, r, g, b] = rgbMatch;
58
+ return `\x1b[38;2;${r};${g};${b}m`;
59
+ }
60
+ const hexMatch = color.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
61
+ if (hexMatch) {
62
+ let hex = hexMatch[1];
63
+ if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
64
+ const r = parseInt(hex.substr(0, 2), 16);
65
+ const g = parseInt(hex.substr(2, 2), 16);
66
+ const b = parseInt(hex.substr(4, 2), 16);
67
+ return `\x1b[38;2;${r};${g};${b}m`;
68
+ }
69
+ return "";
70
+ }
71
+ /**
72
+ * Helper function to convert style to ANSI escape code
73
+ */
74
+ function styleToAnsi(style) {
75
+ if (style === null) return "";
76
+ if (Array.isArray(style)) return style.map((s) => styles[s] || "").join("");
77
+ return styles[style] || "";
78
+ }
79
+ /**
80
+ * Converts a category color map to internal patterns and sorts them by specificity.
81
+ * More specific (longer) prefixes come first for proper matching precedence.
82
+ */
83
+ function prepareCategoryPatterns(categoryColorMap) {
84
+ const patterns = [];
85
+ for (const [prefix, color] of categoryColorMap) patterns.push({
86
+ prefix,
87
+ color
88
+ });
89
+ return patterns.sort((a, b) => b.prefix.length - a.prefix.length);
90
+ }
91
+ /**
92
+ * Matches a category against category color patterns.
93
+ * Returns the color of the first matching pattern, or null if no match.
94
+ */
95
+ function matchCategoryColor(category, patterns) {
96
+ for (const pattern of patterns) if (categoryMatches(category, pattern.prefix)) return pattern.color;
97
+ return null;
98
+ }
99
+ /**
100
+ * Checks if a category matches a prefix pattern.
101
+ * A category matches if it starts with all segments of the prefix.
102
+ */
103
+ function categoryMatches(category, prefix) {
104
+ if (prefix.length > category.length) return false;
105
+ for (let i = 0; i < prefix.length; i++) if (category[i] !== prefix[i]) return false;
106
+ return true;
107
+ }
108
+ /**
109
+ * Default icons for each log level
110
+ */
111
+ const defaultIcons = {
112
+ trace: "🔍",
113
+ debug: "🐛",
114
+ info: "✨",
115
+ warning: "⚡",
116
+ error: "❌",
117
+ fatal: "💀"
118
+ };
119
+ /**
120
+ * Normalize icon spacing to ensure consistent column alignment.
121
+ *
122
+ * All icons will be padded with spaces to match the width of the widest icon,
123
+ * ensuring consistent prefix alignment across all log levels.
124
+ *
125
+ * @param iconMap The icon mapping to normalize
126
+ * @returns A new icon map with consistent spacing
127
+ */
128
+ function normalizeIconSpacing(iconMap) {
129
+ const maxWidth = Math.max(...Object.values(iconMap).map((icon) => require_wcwidth.getDisplayWidth(icon)));
130
+ const normalizedMap = {};
131
+ for (const [level, icon] of Object.entries(iconMap)) {
132
+ const currentWidth = require_wcwidth.getDisplayWidth(icon);
133
+ const spacesToAdd = maxWidth - currentWidth;
134
+ normalizedMap[level] = icon + " ".repeat(spacesToAdd);
135
+ }
136
+ return normalizedMap;
137
+ }
138
+ /**
139
+ * Platform-specific inspect function. Uses Node.js `util.inspect()` which
140
+ * is available in both Deno and Node.js environments. For browser environments,
141
+ * it falls back to {@link JSON.stringify}.
142
+ *
143
+ * @param value The value to inspect.
144
+ * @param options The options for inspecting the value.
145
+ * @returns The string representation of the value.
146
+ */
147
+ const inspect = typeof document !== "undefined" || typeof navigator !== "undefined" && navigator.product === "ReactNative" ? (v) => JSON.stringify(v) : (v, opts) => (0, node_util.inspect)(v, {
148
+ maxArrayLength: 10,
149
+ maxStringLength: 80,
150
+ compact: true,
151
+ ...opts
152
+ });
153
+ /**
154
+ * Creates a beautiful console formatter optimized for local development.
155
+ *
156
+ * This formatter provides a Signale-inspired visual design with colorful icons,
157
+ * smart category truncation, dimmed styling, and perfect column alignment.
158
+ * It's specifically designed for development environments that support true colors
159
+ * and Unicode characters.
160
+ *
161
+ * The formatter features:
162
+ * - Emoji icons for each log level (🔍 trace, 🐛 debug, ✨ info, etc.)
163
+ * - True color support with rich color schemes
164
+ * - Intelligent category truncation for long hierarchical categories
165
+ * - Optional timestamp display with multiple formats
166
+ * - Configurable alignment and styling options
167
+ * - Enhanced value rendering with syntax highlighting
168
+ *
169
+ * @param options Configuration options for customizing the formatter behavior.
170
+ * @returns A text formatter function that can be used with LogTape sinks.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * import { configure } from "@logtape/logtape";
175
+ * import { getConsoleSink } from "@logtape/logtape";
176
+ * import { getPrettyFormatter } from "@logtape/pretty";
177
+ *
178
+ * await configure({
179
+ * sinks: {
180
+ * console: getConsoleSink({
181
+ * formatter: getPrettyFormatter({
182
+ * timestamp: "time",
183
+ * categoryWidth: 25,
184
+ * icons: {
185
+ * info: "📘",
186
+ * error: "🔥"
187
+ * }
188
+ * })
189
+ * })
190
+ * }
191
+ * });
192
+ * ```
193
+ *
194
+ * @since 1.0.0
195
+ */
196
+ function getPrettyFormatter(options = {}) {
197
+ const { timestamp = "none", timestampColor = "rgb(100,116,139)", timestampStyle = "dim", level: levelFormat = "full", levelColors = {}, levelStyle = "underline", icons = true, categorySeparator = "·", categoryColor = "rgb(100,116,139)", categoryColorMap = /* @__PURE__ */ new Map(), categoryStyle = ["dim", "italic"], categoryWidth = 20, categoryTruncate = "middle", messageColor = "rgb(148,163,184)", messageStyle = "dim", colors: useColors = true, align = true, inspectOptions = {}, wordWrap = false } = options;
198
+ const baseIconMap = icons === false ? {
199
+ trace: "",
200
+ debug: "",
201
+ info: "",
202
+ warning: "",
203
+ error: "",
204
+ fatal: ""
205
+ } : icons === true ? defaultIcons : {
206
+ ...defaultIcons,
207
+ ...icons
208
+ };
209
+ const iconMap = normalizeIconSpacing(baseIconMap);
210
+ const resolvedLevelColors = {
211
+ trace: defaultColors.trace,
212
+ debug: defaultColors.debug,
213
+ info: defaultColors.info,
214
+ warning: defaultColors.warning,
215
+ error: defaultColors.error,
216
+ fatal: defaultColors.fatal,
217
+ ...levelColors
218
+ };
219
+ const formatLevel = (level) => {
220
+ if (typeof levelFormat === "function") return levelFormat(level);
221
+ switch (levelFormat) {
222
+ case "ABBR": return {
223
+ trace: "TRC",
224
+ debug: "DBG",
225
+ info: "INF",
226
+ warning: "WRN",
227
+ error: "ERR",
228
+ fatal: "FTL"
229
+ }[level];
230
+ case "FULL": return level.toUpperCase();
231
+ case "L": return {
232
+ trace: "T",
233
+ debug: "D",
234
+ info: "I",
235
+ warning: "W",
236
+ error: "E",
237
+ fatal: "F"
238
+ }[level];
239
+ case "abbr": return {
240
+ trace: "trc",
241
+ debug: "dbg",
242
+ info: "inf",
243
+ warning: "wrn",
244
+ error: "err",
245
+ fatal: "ftl"
246
+ }[level];
247
+ case "full": return level;
248
+ case "l": return {
249
+ trace: "t",
250
+ debug: "d",
251
+ info: "i",
252
+ warning: "w",
253
+ error: "e",
254
+ fatal: "f"
255
+ }[level];
256
+ default: return level;
257
+ }
258
+ };
259
+ let timestampFn = null;
260
+ if (timestamp === "none" || timestamp === "disabled") timestampFn = null;
261
+ else if (timestamp === "date-time-timezone") timestampFn = (ts) => {
262
+ const date = new Date(ts);
263
+ return date.toISOString().replace("T", " ").replace("Z", " +00:00");
264
+ };
265
+ else if (timestamp === "date-time-tz") timestampFn = (ts) => {
266
+ const date = new Date(ts);
267
+ return date.toISOString().replace("T", " ").replace("Z", " +00");
268
+ };
269
+ else if (timestamp === "date-time") timestampFn = (ts) => {
270
+ const date = new Date(ts);
271
+ return date.toISOString().replace("T", " ").replace("Z", "");
272
+ };
273
+ else if (timestamp === "time-timezone") timestampFn = (ts) => {
274
+ const date = new Date(ts);
275
+ return date.toISOString().replace(/.*T/, "").replace("Z", " +00:00");
276
+ };
277
+ else if (timestamp === "time-tz") timestampFn = (ts) => {
278
+ const date = new Date(ts);
279
+ return date.toISOString().replace(/.*T/, "").replace("Z", " +00");
280
+ };
281
+ else if (timestamp === "time") timestampFn = (ts) => {
282
+ const date = new Date(ts);
283
+ return date.toISOString().replace(/.*T/, "").replace("Z", "");
284
+ };
285
+ else if (timestamp === "date") timestampFn = (ts) => {
286
+ const date = new Date(ts);
287
+ return date.toISOString().replace(/T.*/, "");
288
+ };
289
+ else if (timestamp === "rfc3339") timestampFn = (ts) => {
290
+ const date = new Date(ts);
291
+ return date.toISOString();
292
+ };
293
+ else if (typeof timestamp === "function") timestampFn = timestamp;
294
+ const wordWrapEnabled = wordWrap !== false;
295
+ let wordWrapWidth;
296
+ if (typeof wordWrap === "number") wordWrapWidth = wordWrap;
297
+ else if (wordWrap === true) wordWrapWidth = require_terminal.getOptimalWordWrapWidth(80);
298
+ else wordWrapWidth = 80;
299
+ const categoryPatterns = prepareCategoryPatterns(categoryColorMap);
300
+ const allLevels = [
301
+ "trace",
302
+ "debug",
303
+ "info",
304
+ "warning",
305
+ "error",
306
+ "fatal"
307
+ ];
308
+ const levelWidth = Math.max(...allLevels.map((l) => formatLevel(l).length));
309
+ return (record) => {
310
+ const icon = iconMap[record.level] || "";
311
+ const level = formatLevel(record.level);
312
+ const categoryStr = require_truncate.truncateCategory(record.category, typeof categoryWidth === "number" ? categoryWidth : 30, categorySeparator, categoryTruncate);
313
+ let message = "";
314
+ const messageColorCode = useColors ? colorToAnsi(messageColor) : "";
315
+ const messageStyleCode = useColors ? styleToAnsi(messageStyle) : "";
316
+ const messagePrefix = useColors ? `${messageStyleCode}${messageColorCode}` : "";
317
+ for (let i = 0; i < record.message.length; i++) if (i % 2 === 0) message += record.message[i];
318
+ else {
319
+ const value = record.message[i];
320
+ const inspected = inspect(value, {
321
+ colors: useColors,
322
+ ...inspectOptions
323
+ });
324
+ if (inspected.includes("\n")) {
325
+ const lines = inspected.split("\n");
326
+ const formattedLines = lines.map((line, index) => {
327
+ if (index === 0) if (useColors && (messageColorCode || messageStyleCode)) return `${RESET}${line}${messagePrefix}`;
328
+ else return line;
329
+ else if (useColors && (messageColorCode || messageStyleCode)) return `${line}${messagePrefix}`;
330
+ else return line;
331
+ });
332
+ message += formattedLines.join("\n");
333
+ } else if (useColors && (messageColorCode || messageStyleCode)) message += `${RESET}${inspected}${messagePrefix}`;
334
+ else message += inspected;
335
+ }
336
+ const finalCategoryColor = useColors ? matchCategoryColor(record.category, categoryPatterns) || categoryColor : null;
337
+ const formattedIcon = icon;
338
+ let formattedLevel = level;
339
+ let formattedCategory = categoryStr;
340
+ let formattedMessage = message;
341
+ let formattedTimestamp = "";
342
+ if (useColors) {
343
+ const levelColorCode = colorToAnsi(resolvedLevelColors[record.level]);
344
+ const levelStyleCode = styleToAnsi(levelStyle);
345
+ formattedLevel = `${levelStyleCode}${levelColorCode}${level}${RESET}`;
346
+ const categoryColorCode = colorToAnsi(finalCategoryColor);
347
+ const categoryStyleCode = styleToAnsi(categoryStyle);
348
+ formattedCategory = `${categoryStyleCode}${categoryColorCode}${categoryStr}${RESET}`;
349
+ formattedMessage = `${messagePrefix}${message}${RESET}`;
350
+ }
351
+ if (timestampFn) {
352
+ const ts = timestampFn(record.timestamp);
353
+ if (ts !== null) if (useColors) {
354
+ const timestampColorCode = colorToAnsi(timestampColor);
355
+ const timestampStyleCode = styleToAnsi(timestampStyle);
356
+ formattedTimestamp = `${timestampStyleCode}${timestampColorCode}${ts}${RESET} `;
357
+ } else formattedTimestamp = `${ts} `;
358
+ }
359
+ if (align) {
360
+ const levelColorLength = useColors ? colorToAnsi(resolvedLevelColors[record.level]).length + styleToAnsi(levelStyle).length + RESET.length : 0;
361
+ const categoryColorLength = useColors ? colorToAnsi(finalCategoryColor).length + styleToAnsi(categoryStyle).length + RESET.length : 0;
362
+ const paddedLevel = formattedLevel.padEnd(levelWidth + levelColorLength);
363
+ const paddedCategory = formattedCategory.padEnd((typeof categoryWidth === "number" ? categoryWidth : 30) + categoryColorLength);
364
+ let result = `${formattedTimestamp}${formattedIcon} ${paddedLevel} ${paddedCategory} ${formattedMessage}`;
365
+ if (wordWrapEnabled || message.includes("\n")) result = require_wordwrap.wrapText(result, wordWrapEnabled ? wordWrapWidth : Infinity, message);
366
+ return result + "\n";
367
+ } else {
368
+ let result = `${formattedTimestamp}${formattedIcon} ${formattedLevel} ${formattedCategory} ${formattedMessage}`;
369
+ if (wordWrapEnabled || message.includes("\n")) result = require_wordwrap.wrapText(result, wordWrapEnabled ? wordWrapWidth : Infinity, message);
370
+ return result + "\n";
371
+ }
372
+ };
373
+ }
374
+ /**
375
+ * A pre-configured beautiful console formatter for local development.
376
+ *
377
+ * This is a ready-to-use instance of the pretty formatter with sensible defaults
378
+ * for most development scenarios. It provides immediate visual enhancement to
379
+ * your logs without requiring any configuration.
380
+ *
381
+ * Features enabled by default:
382
+ * - Emoji icons for all log levels
383
+ * - True color support with rich color schemes
384
+ * - Dimmed text styling for better readability
385
+ * - Smart category truncation (20 characters max)
386
+ * - Perfect column alignment
387
+ * - No timestamp display (cleaner for development)
388
+ *
389
+ * For custom configuration, use {@link getPrettyFormatter} instead.
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * import { configure } from "@logtape/logtape";
394
+ * import { getConsoleSink } from "@logtape/logtape";
395
+ * import { prettyFormatter } from "@logtape/pretty";
396
+ *
397
+ * await configure({
398
+ * sinks: {
399
+ * console: getConsoleSink({
400
+ * formatter: prettyFormatter
401
+ * })
402
+ * }
403
+ * });
404
+ * ```
405
+ *
406
+ * @since 1.0.0
407
+ */
408
+ const prettyFormatter = getPrettyFormatter();
409
+
410
+ //#endregion
411
+ exports.getPrettyFormatter = getPrettyFormatter;
412
+ exports.prettyFormatter = prettyFormatter;