import type { LogLevel, LogRecord, TextFormatter, TextFormatterOptions, } from "@logtape/logtape"; import { inspect } from "#util"; import { getOptimalWordWrapWidth } from "./terminal.ts"; import { truncateCategory, type TruncationStrategy } from "./truncate.ts"; import { getDisplayWidth } from "./wcwidth.ts"; import { wrapText } from "./wordwrap.ts"; /** * ANSI escape codes for styling */ const RESET = "\x1b[0m"; const DIM = "\x1b[2m"; // Default true color values (referenced in JSDoc) const defaultColors = { trace: "rgb(167,139,250)", // Light purple debug: "rgb(96,165,250)", // Light blue info: "rgb(52,211,153)", // Emerald warning: "rgb(251,191,36)", // Amber error: "rgb(248,113,113)", // Light red fatal: "rgb(220,38,38)", // Dark red category: "rgb(100,116,139)", // Slate message: "rgb(148,163,184)", // Light slate timestamp: "rgb(100,116,139)", // Slate } as const; /** * ANSI style codes */ const styles = { reset: RESET, bold: "\x1b[1m", dim: DIM, italic: "\x1b[3m", underline: "\x1b[4m", strikethrough: "\x1b[9m", } as const; /** * Standard ANSI colors (16-color) */ const ansiColors = { black: "\x1b[30m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", white: "\x1b[37m", } as const; /** * Color type definition */ export type Color = | keyof typeof ansiColors | `rgb(${number},${number},${number})` | `#${string}` | null; /** * Category color mapping for prefix-based coloring. * * Maps category prefixes (as arrays) to colors. The formatter will match * categories against these prefixes and use the corresponding color. * Longer/more specific prefixes take precedence over shorter ones. * * @example * ```typescript * new Map([ * [["app", "auth"], "#ff6b6b"], // app.auth.* -> red * [["app", "db"], "#4ecdc4"], // app.db.* -> teal * [["app"], "#45b7d1"], // app.* (fallback) -> blue * [["lib"], "#96ceb4"], // lib.* -> green * ]) * ``` */ export type CategoryColorMap = Map; /** * Internal representation of category prefix patterns */ type CategoryPattern = { prefix: readonly string[]; color: Color; }; /** * Style type definition - supports single styles, arrays of styles, or null */ export type Style = keyof typeof styles | (keyof typeof styles)[] | null; // Pre-compiled regex patterns for color parsing const RGB_PATTERN = /^rgb\((\d+),(\d+),(\d+)\)$/; const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; /** * Helper function to convert color to ANSI escape code */ function colorToAnsi(color: Color): string { if (color === null) return ""; if (color in ansiColors) { return ansiColors[color as keyof typeof ansiColors]; } // Handle rgb() format const rgbMatch = color.match(RGB_PATTERN); if (rgbMatch) { const [, r, g, b] = rgbMatch; return `\x1b[38;2;${r};${g};${b}m`; } // Handle hex format (#rrggbb or #rgb) const hexMatch = color.match(HEX_PATTERN); if (hexMatch) { let hex = hexMatch[1]; // Convert 3-digit hex to 6-digit if (hex.length === 3) { hex = hex.split("").map((c) => c + c).join(""); } const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); return `\x1b[38;2;${r};${g};${b}m`; } return ""; } /** * Helper function to convert style to ANSI escape code */ function styleToAnsi(style: Style): string { if (style === null) return ""; if (Array.isArray(style)) { return style.map((s) => styles[s] || "").join(""); } return styles[style] || ""; } /** * Converts a category color map to internal patterns and sorts them by specificity. * More specific (longer) prefixes come first for proper matching precedence. */ function prepareCategoryPatterns( categoryColorMap: CategoryColorMap, ): CategoryPattern[] { const patterns: CategoryPattern[] = []; for (const [prefix, color] of categoryColorMap) { patterns.push({ prefix, color }); } // Sort by prefix length (descending) for most-specific-first matching return patterns.sort((a, b) => b.prefix.length - a.prefix.length); } /** * Matches a category against category color patterns. * Returns the color of the first matching pattern, or null if no match. */ function matchCategoryColor( category: readonly string[], patterns: CategoryPattern[], ): Color { for (const pattern of patterns) { if (categoryMatches(category, pattern.prefix)) { return pattern.color; } } return null; } /** * Checks if a category matches a prefix pattern. * A category matches if it starts with all segments of the prefix. */ function categoryMatches( category: readonly string[], prefix: readonly string[], ): boolean { if (prefix.length > category.length) { return false; } for (let i = 0; i < prefix.length; i++) { if (category[i] !== prefix[i]) { return false; } } return true; } /** * Default icons for each log level */ const defaultIcons: Record = { trace: "🔍", debug: "🐛", info: "✨", warning: "⚡", error: "❌", fatal: "💀", }; /** * Normalize icon spacing to ensure consistent column alignment. * * All icons will be padded with spaces to match the width of the widest icon, * ensuring consistent prefix alignment across all log levels. * * @param iconMap The icon mapping to normalize * @returns A new icon map with consistent spacing */ function normalizeIconSpacing( iconMap: Record, ): Record { const entries = Object.entries(iconMap) as Array<[LogLevel, string]>; const maxWidth = Math.max( ...entries.map(([, icon]) => getDisplayWidth(icon)), ); return Object.fromEntries( entries.map(([level, icon]) => [ level, icon + " ".repeat(maxWidth - getDisplayWidth(icon)), ]), ) as Record; } /** * Configuration options for the pretty formatter. * * This interface extends the base text formatter options while providing * extensive customization options for visual styling, layout control, and * development-focused features. It offers granular control over colors, * styles, and formatting similar to the ANSI color formatter. * * @since 1.0.0 */ export interface PrettyFormatterOptions extends Omit { /** * Color for timestamp display when timestamps are enabled. * * Supports true color RGB values, hex colors, or ANSI color names. * Set to `null` to disable timestamp coloring. * * @example * ```typescript * timestampColor: "#888888" // Hex color * timestampColor: "rgb(128,128,128)" // RGB color * timestampColor: "cyan" // ANSI color name * timestampColor: null // No color * ``` * * @default `"rgb(100,116,139)"` (slate gray) */ readonly timestampColor?: Color; /** * Visual style applied to timestamp text. * * Controls text appearance like boldness, dimming, etc. * Supports single styles, multiple styles combined, or no styling. * Combines with `timestampColor` for full styling control. * * @example * ```typescript * timestampStyle: "dim" // Single style: dimmed text * timestampStyle: "bold" // Single style: bold text * timestampStyle: ["bold", "underline"] // Multiple styles: bold + underlined * timestampStyle: ["dim", "italic"] // Multiple styles: dimmed + italic * timestampStyle: null // No styling * ``` * * @default `"dim"` */ readonly timestampStyle?: Style; /** * Custom colors for each log level. * * Allows fine-grained control over level appearance. Each level can have * its own color scheme. Unspecified levels use built-in defaults. * Set individual levels to `null` to disable coloring for that level. * * @example * ```typescript * levelColors: { * info: "#00ff00", // Bright green for info * error: "#ff0000", // Bright red for errors * warning: "orange", // ANSI orange for warnings * debug: null, // No color for debug * } * ``` * * @default Built-in color scheme (purple trace, blue debug, green info, amber warning, red error, dark red fatal) */ readonly levelColors?: Partial>; /** * Visual style applied to log level text. * * Controls the appearance of the level indicator (e.g., "info", "error"). * Supports single styles, multiple styles combined, or no styling. * Applied in addition to level-specific colors. * * @example * ```typescript * levelStyle: "bold" // Single style: bold level text * levelStyle: "underline" // Single style: underlined level text * levelStyle: ["bold", "underline"] // Multiple styles: bold + underlined * levelStyle: ["dim", "italic"] // Multiple styles: dimmed + italic * levelStyle: null // No additional styling * ``` * * @default `"underline"` */ readonly levelStyle?: Style; /** * Icon configuration for each log level. * * Controls the emoji/symbol displayed before each log entry. * Provides visual quick-identification of log severity. * * - `true`: Use built-in emoji set (🔍 trace, 🐛 debug, ✨ info, ⚠️ warning, ❌ error, 💀 fatal) * - `false`: Disable all icons for clean text-only output * - Object: Custom icon mapping, falls back to defaults for unspecified levels * * @example * ```typescript * icons: true // Use default emoji set * icons: false // No icons * icons: { * info: "ℹ️", // Custom info icon * error: "🔥", // Custom error icon * warning: "⚡", // Custom warning icon * } * ``` * * @default `true` (use default emoji icons) */ readonly icons?: boolean | Partial>; /** * Character(s) used to separate category hierarchy levels. * * Categories are hierarchical (e.g., ["app", "auth", "jwt"]) and this * separator joins them for display (e.g., "app.auth.jwt"). * * @example * ```typescript * categorySeparator: "·" // app·auth·jwt * categorySeparator: "." // app.auth.jwt * categorySeparator: ":" // app:auth:jwt * categorySeparator: " > " // app > auth > jwt * categorySeparator: "::" // app::auth::jwt * ``` * * @default `"·"` (interpunct) */ readonly categorySeparator?: string; /** * Default color for category display. * * Used as fallback when no specific color is found in `categoryColorMap`. * Controls the visual appearance of the category hierarchy display. * * @example * ```typescript * categoryColor: "#666666" // Gray categories * categoryColor: "blue" // Blue categories * categoryColor: "rgb(100,150,200)" // Light blue categories * categoryColor: null // No coloring * ``` * * @default `"rgb(100,116,139)"` (slate gray) */ readonly categoryColor?: Color; /** * Category-specific color mapping based on prefixes. * * Maps category prefixes (as arrays) to colors for visual grouping. * More specific (longer) prefixes take precedence over shorter ones. * If no prefix matches, falls back to the default `categoryColor`. * * @example * ```typescript * new Map([ * [["app", "auth"], "#ff6b6b"], // app.auth.* -> red * [["app", "db"], "#4ecdc4"], // app.db.* -> teal * [["app"], "#45b7d1"], // app.* (fallback) -> blue * [["lib"], "#96ceb4"], // lib.* -> green * ]) * ``` */ readonly categoryColorMap?: CategoryColorMap; /** * Visual style applied to category text. * * Controls the appearance of the category hierarchy display. * Supports single styles, multiple styles combined, or no styling. * Applied in addition to category colors from `categoryColor` or `categoryColorMap`. * * @example * ```typescript * categoryStyle: "dim" // Single style: dimmed category text * categoryStyle: "italic" // Single style: italic category text * categoryStyle: ["dim", "italic"] // Multiple styles: dimmed + italic * categoryStyle: ["bold", "underline"] // Multiple styles: bold + underlined * categoryStyle: null // No additional styling * ``` * * @default `["dim", "italic"]` (dimmed for subtle appearance) */ readonly categoryStyle?: Style; /** * Maximum display width for category names. * * Controls layout consistency by limiting category width. * Long categories are truncated according to `categoryTruncate` strategy. * * @default `20` */ readonly categoryWidth?: number; /** * Strategy for truncating long category names. * * When categories exceed `categoryWidth`, this controls how truncation works. * Smart truncation preserves important context while maintaining layout. * * - `"middle"`: Keep first and last parts (e.g., "app.server…auth.jwt") * - `"end"`: Truncate at the end (e.g., "app.server.middleware…") * - `false`: No truncation (ignores `categoryWidth`) * * @example * ```typescript * categoryTruncate: "middle" // app.server…jwt (preserves context) * categoryTruncate: "end" // app.server.midd… (linear truncation) * categoryTruncate: false // app.server.middleware.auth.jwt (full) * ``` * * @default `"middle"` (smart context-preserving truncation) */ readonly categoryTruncate?: TruncationStrategy; /** * Color for log message text content. * * Controls the visual appearance of the actual log message content. * Does not affect structured values, which use syntax highlighting. * * @example * ```typescript * messageColor: "#ffffff" // White message text * messageColor: "green" // Green message text * messageColor: "rgb(200,200,200)" // Light gray message text * messageColor: null // No coloring * ``` * * @default `"rgb(148,163,184)"` (light slate gray) */ readonly messageColor?: Color; /** * Visual style applied to log message text. * * Controls the appearance of the log message content. * Supports single styles, multiple styles combined, or no styling. * Applied in addition to `messageColor`. * * @example * ```typescript * messageStyle: "dim" // Single style: dimmed message text * messageStyle: "italic" // Single style: italic message text * messageStyle: ["dim", "italic"] // Multiple styles: dimmed + italic * messageStyle: ["bold", "underline"] // Multiple styles: bold + underlined * messageStyle: null // No additional styling * ``` * * @default `"dim"` (dimmed for subtle readability) */ readonly messageStyle?: Style; /** * Global color control for the entire formatter. * * Master switch to enable/disable all color output. * When disabled, produces clean monochrome output suitable for * non-color terminals or when colors are not desired. * * @example * ```typescript * colors: true // Full color output (default) * colors: false // Monochrome output only * ``` * * @default `true` (colors enabled) */ readonly colors?: boolean; /** * Column alignment for consistent visual layout. * * When enabled, ensures all log components (icons, levels, categories) * align consistently across multiple log entries, creating a clean * tabular appearance. * * @example * ```typescript * align: true // Aligned columns (default) * align: false // Compact, non-aligned output * ``` * * @default `true` (alignment enabled) */ readonly align?: boolean; /** * Configuration for structured value inspection and rendering. * * Controls how objects, arrays, and other complex values are displayed * within log messages. Uses Node.js `util.inspect()` style options. * * @example * ```typescript * inspectOptions: { * depth: 3, // Show 3 levels of nesting * colors: false, // Disable value syntax highlighting * compact: true, // Use compact object display * } * ``` * * @default `{}` (use built-in defaults: depth=unlimited, colors=auto, compact=true) */ readonly inspectOptions?: { /** * Maximum depth to traverse when inspecting nested objects. * @default Infinity (no depth limit) */ readonly depth?: number; /** * Whether to use syntax highlighting colors for inspected values. * @default Inherited from global `colors` setting */ readonly colors?: boolean; /** * Whether to use compact formatting for objects and arrays. * @default `true` (compact formatting) */ readonly compact?: boolean; }; /** * Enable word wrapping for long messages. * * When enabled, long messages will be wrapped at the specified width, * with continuation lines aligned to the message column position. * * - `true`: Auto-detect terminal width when attached to a terminal, * fallback to 80 columns when not in a terminal or detection fails * - `number`: Use the specified width in columns * - `false`: Disable word wrapping * * @example * ```typescript * // Auto-detect terminal width (recommended) * wordWrap: true * * // Custom wrap width * wordWrap: 120 * * // Disable word wrapping (default) * wordWrap: false * ``` * * @default `true` (auto-detect terminal width) * @since 1.0.0 */ readonly wordWrap?: boolean | number; } /** * Creates a beautiful console formatter optimized for local development. * * This formatter provides a Signale-inspired visual design with colorful icons, * smart category truncation, dimmed styling, and perfect column alignment. * It's specifically designed for development environments that support true colors * and Unicode characters. * * The formatter features: * - Emoji icons for each log level (🔍 trace, 🐛 debug, ✨ info, etc.) * - True color support with rich color schemes * - Intelligent category truncation for long hierarchical categories * - Optional timestamp display with multiple formats * - Configurable alignment and styling options * - Enhanced value rendering with syntax highlighting * * @param options Configuration options for customizing the formatter behavior. * @returns A text formatter function that can be used with LogTape sinks. * * @example * ```typescript * import { configure } from "@logtape/logtape"; * import { getConsoleSink } from "@logtape/logtape/sink"; * import { getPrettyFormatter } from "@logtape/pretty"; * * await configure({ * sinks: { * console: getConsoleSink({ * formatter: getPrettyFormatter({ * timestamp: "time", * categoryWidth: 25, * icons: { * info: "📘", * error: "🔥" * } * }) * }) * } * }); * ``` * * @since 1.0.0 */ export function getPrettyFormatter( options: PrettyFormatterOptions = {}, ): TextFormatter { // Extract options with defaults 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 = new Map(), categoryStyle = ["dim", "italic"], categoryWidth = 20, categoryTruncate = "middle", messageColor = "rgb(148,163,184)", messageStyle = "dim", colors: useColors = true, align = true, inspectOptions = {}, wordWrap = true, } = options; // Resolve icons const baseIconMap: Record = icons === false ? { trace: "", debug: "", info: "", warning: "", error: "", fatal: "" } : icons === true ? defaultIcons : { ...defaultIcons, ...(icons as Partial>) }; // Normalize icon spacing for consistent alignment const iconMap = normalizeIconSpacing(baseIconMap); // Resolve level colors with defaults const resolvedLevelColors: Record = { trace: defaultColors.trace, debug: defaultColors.debug, info: defaultColors.info, warning: defaultColors.warning, error: defaultColors.error, fatal: defaultColors.fatal, ...levelColors, }; // Level formatter function with optimized mappings const levelMappings: Record> = { "ABBR": { trace: "TRC", debug: "DBG", info: "INF", warning: "WRN", error: "ERR", fatal: "FTL", }, "L": { trace: "T", debug: "D", info: "I", warning: "W", error: "E", fatal: "F", }, "abbr": { trace: "trc", debug: "dbg", info: "inf", warning: "wrn", error: "err", fatal: "ftl", }, "l": { trace: "t", debug: "d", info: "i", warning: "w", error: "e", fatal: "f", }, }; const formatLevel = (level: LogLevel): string => { if (typeof levelFormat === "function") { return levelFormat(level); } if (levelFormat === "FULL") return level.toUpperCase(); if (levelFormat === "full") return level; return levelMappings[levelFormat]?.[level] ?? level; }; // Timestamp formatters lookup table const timestampFormatters: Record string> = { "date-time-timezone": (ts) => { const iso = new Date(ts).toISOString(); return iso.replace("T", " ").replace("Z", " +00:00"); }, "date-time-tz": (ts) => { const iso = new Date(ts).toISOString(); return iso.replace("T", " ").replace("Z", " +00"); }, "date-time": (ts) => { const iso = new Date(ts).toISOString(); return iso.replace("T", " ").replace("Z", ""); }, "time-timezone": (ts) => { const iso = new Date(ts).toISOString(); return iso.replace(/.*T/, "").replace("Z", " +00:00"); }, "time-tz": (ts) => { const iso = new Date(ts).toISOString(); return iso.replace(/.*T/, "").replace("Z", " +00"); }, "time": (ts) => { const iso = new Date(ts).toISOString(); return iso.replace(/.*T/, "").replace("Z", ""); }, "date": (ts) => new Date(ts).toISOString().replace(/T.*/, ""), "rfc3339": (ts) => new Date(ts).toISOString(), }; // Resolve timestamp formatter let timestampFn: ((ts: number) => string | null) | null = null; if (timestamp === "none" || timestamp === "disabled") { timestampFn = null; } else if (typeof timestamp === "function") { timestampFn = timestamp; } else { timestampFn = timestampFormatters[timestamp as string] ?? null; } // Configure word wrap settings const wordWrapEnabled = wordWrap !== false; let wordWrapWidth: number; if (typeof wordWrap === "number") { wordWrapWidth = wordWrap; } else if (wordWrap === true) { // Auto-detect terminal width wordWrapWidth = getOptimalWordWrapWidth(80); } else { wordWrapWidth = 80; // Default fallback } // Prepare category color patterns for matching const categoryPatterns = prepareCategoryPatterns(categoryColorMap); // Calculate level width based on format const allLevels: LogLevel[] = [ "trace", "debug", "info", "warning", "error", "fatal", ]; const levelWidth = Math.max(...allLevels.map((l) => formatLevel(l).length)); return (record: LogRecord): string => { // Calculate the prefix parts first to determine message column position const icon = iconMap[record.level] || ""; const level = formatLevel(record.level); const categoryStr = truncateCategory( record.category, categoryWidth, categorySeparator, categoryTruncate, ); // Format message with values - handle color reset/reapply for interpolated values let message = ""; const messageColorCode = useColors ? colorToAnsi(messageColor) : ""; const messageStyleCode = useColors ? styleToAnsi(messageStyle) : ""; const messagePrefix = useColors ? `${messageStyleCode}${messageColorCode}` : ""; for (let i = 0; i < record.message.length; i++) { if (i % 2 === 0) { message += record.message[i]; } else { const value = record.message[i]; const inspected = inspect(value, { colors: useColors, ...inspectOptions, }); // Handle multiline interpolated values properly if (inspected.includes("\n")) { const lines = inspected.split("\n"); const formattedLines = lines.map((line, index) => { if (index === 0) { // First line: reset formatting, add the line, then reapply if (useColors && (messageColorCode || messageStyleCode)) { return `${RESET}${line}${messagePrefix}`; } else { return line; } } else { // Continuation lines: just apply formatting, let wrapText handle indentation if (useColors && (messageColorCode || messageStyleCode)) { return `${line}${messagePrefix}`; } else { return line; } } }); message += formattedLines.join("\n"); } else { // Single line - handle normally if (useColors && (messageColorCode || messageStyleCode)) { message += `${RESET}${inspected}${messagePrefix}`; } else { message += inspected; } } } } // Parts are already calculated above // Determine category color (with prefix matching) const finalCategoryColor = useColors ? (matchCategoryColor(record.category, categoryPatterns) || categoryColor) : null; // Apply colors and styling const formattedIcon = icon; let formattedLevel = level; let formattedCategory = categoryStr; let formattedMessage = message; let formattedTimestamp = ""; if (useColors) { // Apply level color and style const levelColorCode = colorToAnsi(resolvedLevelColors[record.level]); const levelStyleCode = styleToAnsi(levelStyle); formattedLevel = `${levelStyleCode}${levelColorCode}${level}${RESET}`; // Apply category color and style (with prefix matching) const categoryColorCode = colorToAnsi(finalCategoryColor); const categoryStyleCode = styleToAnsi(categoryStyle); formattedCategory = `${categoryStyleCode}${categoryColorCode}${categoryStr}${RESET}`; // Apply message color and style (already handled in message building above) formattedMessage = `${messagePrefix}${message}${RESET}`; } // Format timestamp if needed if (timestampFn) { const ts = timestampFn(record.timestamp); if (ts !== null) { if (useColors) { const timestampColorCode = colorToAnsi(timestampColor); const timestampStyleCode = styleToAnsi(timestampStyle); formattedTimestamp = `${timestampStyleCode}${timestampColorCode}${ts}${RESET} `; } else { formattedTimestamp = `${ts} `; } } } // Build the final output with alignment if (align) { // Calculate padding accounting for ANSI escape sequences const levelColorLength = useColors ? (colorToAnsi(resolvedLevelColors[record.level]).length + styleToAnsi(levelStyle).length + RESET.length) : 0; const categoryColorLength = useColors ? (colorToAnsi(finalCategoryColor).length + styleToAnsi(categoryStyle).length + RESET.length) : 0; const paddedLevel = formattedLevel.padEnd(levelWidth + levelColorLength); const paddedCategory = formattedCategory.padEnd( categoryWidth + categoryColorLength, ); let result = `${formattedTimestamp}${formattedIcon} ${paddedLevel} ${paddedCategory} ${formattedMessage}`; // Apply word wrapping if enabled, or if there are multiline interpolated values if (wordWrapEnabled || message.includes("\n")) { result = wrapText( result, wordWrapEnabled ? wordWrapWidth : Infinity, message, ); } return result + "\n"; } else { let result = `${formattedTimestamp}${formattedIcon} ${formattedLevel} ${formattedCategory} ${formattedMessage}`; // Apply word wrapping if enabled, or if there are multiline interpolated values if (wordWrapEnabled || message.includes("\n")) { result = wrapText( result, wordWrapEnabled ? wordWrapWidth : Infinity, message, ); } return result + "\n"; } }; } /** * A pre-configured beautiful console formatter for local development. * * This is a ready-to-use instance of the pretty formatter with sensible defaults * for most development scenarios. It provides immediate visual enhancement to * your logs without requiring any configuration. * * Features enabled by default: * - Emoji icons for all log levels * - True color support with rich color schemes * - Dimmed text styling for better readability * - Smart category truncation (20 characters max) * - Perfect column alignment * - No timestamp display (cleaner for development) * * For custom configuration, use {@link getPrettyFormatter} instead. * * @example * ```typescript * import { configure } from "@logtape/logtape"; * import { getConsoleSink } from "@logtape/logtape/sink"; * import { prettyFormatter } from "@logtape/pretty"; * * await configure({ * sinks: { * console: getConsoleSink({ * formatter: prettyFormatter * }) * } * }); * ``` * * @since 1.0.0 */ export const prettyFormatter: TextFormatter = getPrettyFormatter();