UNPKG

@loglayer/transport-simple-pretty-terminal

Version:

Pretty log output in the terminal / browser / Next.js for the LogLayer logging library.

697 lines (696 loc) 25.6 kB
import { LoggerlessTransport } from "@loglayer/transport"; import chalk from "chalk"; import { sprintf } from "sprintf-js"; import { format } from "date-fns"; export * from "chalk"; //#region src/themes.ts /** * Moonlight - A dark theme with cool blue tones * Inspired by moonlit nights and modern IDEs * * Color Palette: * - Primary: Cool blues and soft greens * - Accents: Warm yellows and soft reds * - Background: Assumes dark terminal (black or very dark grey) * * Best used with: * - Dark terminal themes * - Night-time coding sessions * - Environments where eye strain is a concern */ const moonlight = { colors: { trace: chalk.rgb(114, 135, 153), debug: chalk.rgb(130, 170, 255), info: chalk.rgb(195, 232, 141), warn: chalk.rgb(255, 203, 107), error: chalk.rgb(247, 118, 142), fatal: chalk.bgRgb(247, 118, 142).white }, logIdColor: chalk.rgb(84, 98, 117), dataValueColor: chalk.rgb(209, 219, 231), dataKeyColor: chalk.rgb(130, 170, 255) }; /** * Sunlight - A light theme with warm tones * Inspired by daylight reading and paper documentation * * Color Palette: * - Primary: Deep, rich colors that contrast well with white * - Accents: Earth tones and deep jewel tones * - Background: Assumes light terminal (white or very light grey) * * Best used with: * - Light terminal themes * - Daytime coding sessions * - High-glare environments * - Printed documentation */ const sunlight = { colors: { trace: chalk.rgb(110, 110, 110), debug: chalk.rgb(32, 96, 159), info: chalk.rgb(35, 134, 54), warn: chalk.rgb(176, 95, 0), error: chalk.rgb(191, 0, 0), fatal: chalk.bgRgb(191, 0, 0).white }, logIdColor: chalk.rgb(110, 110, 110), dataValueColor: chalk.rgb(0, 0, 0), dataKeyColor: chalk.rgb(32, 96, 159) }; /** * Neon - A dark theme with vibrant cyberpunk colors * Inspired by neon-lit cityscapes and retro-futuristic aesthetics * * Color Palette: * - Primary: Electric blues and hot pinks * - Accents: Bright purples and cyber greens * - Background: Assumes dark terminal (black or very dark grey) * * Best used with: * - Dark terminal themes * - High-contrast preferences * - Modern, tech-focused applications */ const neon = { colors: { trace: chalk.rgb(108, 108, 255), debug: chalk.rgb(255, 82, 246), info: chalk.rgb(0, 255, 163), warn: chalk.rgb(255, 231, 46), error: chalk.rgb(255, 53, 91), fatal: chalk.bgRgb(255, 53, 91).rgb(0, 255, 163) }, logIdColor: chalk.rgb(187, 134, 252), dataValueColor: chalk.rgb(255, 255, 255), dataKeyColor: chalk.rgb(0, 255, 240) }; /** * Nature - A light theme with organic, earthy colors * Inspired by forest landscapes and natural elements * * Color Palette: * - Primary: Deep forest greens and rich browns * - Accents: Autumn reds and golden yellows * - Background: Assumes light terminal (white or very light grey) * * Best used with: * - Light terminal themes * - Nature-inspired interfaces * - Applications focusing on readability */ const nature = { colors: { trace: chalk.rgb(101, 115, 126), debug: chalk.rgb(34, 139, 34), info: chalk.rgb(46, 139, 87), warn: chalk.rgb(218, 165, 32), error: chalk.rgb(139, 69, 19), fatal: chalk.bgRgb(139, 69, 19).white }, logIdColor: chalk.rgb(101, 115, 126), dataValueColor: chalk.rgb(0, 0, 0), dataKeyColor: chalk.rgb(34, 139, 34) }; /** * Pastel - A soft, calming theme with gentle colors * Inspired by watercolor paintings and soft aesthetics * * Color Palette: * - Primary: Soft pastels and muted tones * - Accents: Gentle pinks and light blues * - Background: Assumes light terminal (white or very light grey) * * Best used with: * - Light terminal themes * - Long coding sessions * - Reduced visual stress */ const pastel = { colors: { trace: chalk.rgb(200, 200, 200), debug: chalk.rgb(173, 216, 230), info: chalk.rgb(144, 238, 144), warn: chalk.rgb(255, 218, 185), error: chalk.rgb(255, 182, 193), fatal: chalk.bgRgb(255, 182, 193).rgb(105, 105, 105) }, logIdColor: chalk.rgb(200, 200, 200), dataValueColor: chalk.rgb(105, 105, 105), dataKeyColor: chalk.rgb(173, 216, 230) }; //#endregion //#region src/vendor/utils.js /** * Creates a string with the same length as `numSpaces` parameter **/ function indent(numSpaces) { return new Array(numSpaces + 1).join(" "); } /** * Gets the string length of the longer index in a hash **/ function getMaxIndexLength(input) { var maxWidth = 0; Object.getOwnPropertyNames(input).forEach((key) => { if (input[key] === void 0) return; maxWidth = Math.max(maxWidth, key.length); }); return maxWidth; } function isIsoStringDate(isoString) { if (typeof isoString !== "string") return false; if (!/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/.test(isoString)) return false; const testDate = new Date(isoString); return !Number.isNaN(testDate.getTime()); } //#endregion //#region src/vendor/prettyjson.js const conflictChars = /[^\w\s\n\r\v\t.,]/i; const isPrintable = (input, options) => input !== void 0 || options.renderUndefined; const isSerializable = (input, onlyPrimitives, options) => { if (typeof input === "boolean" || typeof input === "number" || typeof input === "function" || input === null || input === void 0 || input instanceof Date) return true; if (typeof input === "string" && input.indexOf("\n") === -1) return true; if (options.inlineArrays && !onlyPrimitives) { if (Array.isArray(input) && isSerializable(input[0], true, options)) return true; } return false; }; /** * @param {Function} colorFn * @param {PrettyJSONOptions} options */ const getColorRenderer = (colorFn, options) => { if (options.noColor) return (text) => text; if (typeof colorFn !== "function") return (text) => text; return colorFn; }; const addColorToData = (input, options) => { if (options.noColor) return input; if (input instanceof Date) return getColorRenderer(options.dateColor, options)(input.toISOString()); if (typeof input === "string") { if (isIsoStringDate(input)) return getColorRenderer(options.dateColor, options)(input); return getColorRenderer(options.stringColor, options)(input); } const sInput = `${input}`; if (typeof input === "boolean") return getColorRenderer(options.booleanColor, options)(sInput); if (input === null || input === void 0) return getColorRenderer(options.nullUndefinedColor, options)(sInput); if (typeof input === "number") { if (input >= 0) return getColorRenderer(options.positiveNumberColor, options)(sInput); return getColorRenderer(options.negativeNumberColor, options)(sInput); } if (typeof input === "function") return "function() {}"; if (Array.isArray(input)) return input.join(", "); return sInput; }; const colorMultilineString = (options, line) => getColorRenderer(options.multilineStringColor, options)(line); const indentLines = (string, spaces, options) => { let lines = string.split("\n"); lines = lines.map((line) => indent(spaces) + colorMultilineString(options, line)); return lines.join("\n"); }; const renderToArray = (data, options, indentation) => { if (typeof data === "string" && data.match(conflictChars) && options.escape) data = JSON.stringify(data); if (!isPrintable(data, options)) return []; if (isSerializable(data, false, options)) return [indent(indentation) + addColorToData(data, options)]; if (typeof data === "string") return [ indent(indentation) + colorMultilineString(options, "\"\"\""), indentLines(data, indentation + options.defaultIndentation, options), indent(indentation) + colorMultilineString(options, "\"\"\"") ]; if (Array.isArray(data)) { if (data.length === 0) return [indent(indentation) + options.emptyArrayMsg]; if (options.collapseArrays && data.length > 0) return [`${indent(indentation)}[... ${data.length} items]`]; const outputArray = []; data.forEach((element) => { if (!isPrintable(element, options)) return; let line = "- "; line = getColorRenderer(options.dashColor, options)(line); line = indent(indentation) + line; if (isSerializable(element, false, options)) { line += renderToArray(element, options, 0)[0]; outputArray.push(line); } else { outputArray.push(line); outputArray.push.apply(outputArray, renderToArray(element, options, indentation + options.defaultIndentation)); } }); return outputArray; } if (data instanceof Error) return renderToArray({ message: data.message, stack: data.stack.split("\n") }, options, indentation); const maxIndexLength = options.noAlign ? 0 : getMaxIndexLength(data); let key; const output = []; Object.getOwnPropertyNames(data).forEach((i) => { if (!isPrintable(data[i], options)) return; key = `${i}: `; key = getColorRenderer(options.keysColor, options)(key); key = indent(indentation) + key; if (isSerializable(data[i], false, options)) { const nextIndentation = options.noAlign ? 0 : maxIndexLength - i.length; key += renderToArray(data[i], options, nextIndentation)[0]; output.push(key); } else { output.push(key); output.push.apply(output, renderToArray(data[i], options, indentation + options.defaultIndentation)); } }); return output; }; /** * @typedef {Object} PrettyJSONOptions * @property {Function} [stringColor=null] Chalk color function for strings * @property {Function} [multilineStringColor=null] Chalk color function for multiline strings * @property {Function} [keysColor=chalk.green] Chalk color function for keys in hashes * @property {Function} [dashColor=chalk.green] Chalk color function for dashes in arrays * @property {Function} [numberColor=chalk.blue] Default Chalk color function for numbers * @property {Function} [positiveNumberColor=numberColor] Chalk color function for positive numbers * @property {Function} [negativeNumberColor=numberColor] Chalk color function for negative numbers * @property {Function} [booleanColor=chalk.cyan] Chalk color function for boolean values * @property {Function} [nullUndefinedColor=chalk.grey] Chalk color function for null || undefined * @property {Function} [dateColor=chalk.magenta] Chalk color function for Date objects * @property {number} [defaultIndentation=2] Indentation spaces per object level * @property {string} [emptyArrayMsg="(empty array)"] Replace empty strings with * @property {boolean} [noColor] Flag to disable colors * @property {boolean} [noAlign] Flag to disable alignment * @property {boolean} [escape] Flag to escape printed content * @property {boolean} [collapseArrays] Flag to collapse arrays */ /** * Mutating function that ensures we have a valid options object * @param {PrettyJSONOptions} options * @returns PrettyJSONOptions */ const validateOptionsAndSetDefaults = (options) => { options = options || {}; options.emptyArrayMsg = options.emptyArrayMsg || "(empty array)"; options.keysColor = options.keysColor || chalk.green; options.dashColor = options.dashColor || chalk.green; options.booleanColor = options.booleanColor || chalk.cyan; options.nullUndefinedColor = options.nullUndefinedColor || chalk.grey; options.numberColor = options.numberColor || chalk.blue; options.positiveNumberColor = options.positiveNumberColor || options.numberColor; options.negativeNumberColor = options.negativeNumberColor || options.numberColor; options.dateColor = options.dateColor || chalk.magenta; options.defaultIndentation = options.defaultIndentation || 2; options.noColor = !!options.noColor; options.noAlign = !!options.noAlign; options.escape = !!options.escape; options.renderUndefined = !!options.renderUndefined; options.collapseArrays = !!options.collapseArrays; options.stringColor = options.stringColor || null; options.multilineStringColor = options.multilineStringColor || options.stringColor || null; return options; }; /** * ### Render function * *Parameters:* * * @param {*} data: Data to render * @param {PrettyJSONOptions} options: Hash of different options * @param {*} indentation **`indentation`**: Base indentation of the output * * *Example of options hash:* * * { * emptyArrayMsg: '(empty)', // Rendered message on empty strings * keysColor: 'blue', // Color for keys in hashes * dashColor: 'red', // Color for the dashes in arrays * stringColor: 'grey', // Color for strings * multilineStringColor: 'cyan' // Color for multiline strings * defaultIndentation: 2 // Indentation on nested objects * } * @returns string with the rendered data */ function render(data, options, indentation) { indentation = indentation || 0; options = validateOptionsAndSetDefaults(options); return renderToArray(data, options, indentation).join("\n"); } //#endregion //#region src/views/utils.ts /** * Formats a timestamp into a readable string. * * @param timestamp - Unix timestamp in milliseconds * @param colorFn - Chalk function to color the timestamp * @param timestampFormat - Custom format string (date-fns) or function. Defaults to "HH:mm:ss.SSS" * @returns Formatted timestamp string */ function formatTimestamp(timestamp, colorFn, timestampFormat = "HH:mm:ss.SSS") { let formattedTime; if (typeof timestampFormat === "function") formattedTime = timestampFormat(timestamp); else formattedTime = format(new Date(timestamp), timestampFormat); return colorFn(`[${formattedTime}]`); } /** * Gets the appropriate color function for a log level. * * @param level - Log level string * @param colors - Color configuration object * @returns Chalk function for the log level */ function getLevelColor(level, colors) { switch (level.toLowerCase()) { case "trace": return colors.trace || chalk.white; case "debug": return colors.debug || chalk.white; case "info": return colors.info || chalk.white; case "warn": return colors.warn || chalk.white; case "error": return colors.error || chalk.white; case "fatal": return colors.fatal || chalk.white; default: return colors.info || chalk.white; } } /** * Formats a value for display. */ function formatValue(value, expanded = false, collapseArrays = true) { if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return value.toString(); if (value === null) return "null"; if (value === void 0) return "undefined"; if (Array.isArray(value)) return expanded ? JSON.stringify(value) : collapseArrays ? "[...]" : value.toString(); if (typeof value === "object") return expanded ? JSON.stringify(value) : "{...}"; return value.toString(); } /** * Formats structured data for inline display with depth limiting. * * @param data - The data to format * @param config - View configuration for colors * @param maxDepth - Maximum depth for nested objects * @param expanded - Whether to show full depth (for inline view mode) * @param collapseArrays - Whether to collapse arrays to summary format * @returns Formatted data string */ function formatInlineData(data, config, maxDepth, expanded = false, collapseArrays = true) { if (!data) return ""; const pairs = []; const traverse = (obj, prefix = "", depth = 0) => { if (!expanded && depth >= maxDepth) return; for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; if (value && typeof value === "object" && !Array.isArray(value)) if (expanded) pairs.push(`${config.dataKeyColor(fullKey)}=${config.dataValueColor(JSON.stringify(value))}`); else traverse(value, fullKey, depth + 1); else pairs.push(`${config.dataKeyColor(fullKey)}=${config.dataValueColor(formatValue(value, expanded, collapseArrays))}`); } }; traverse(data); return pairs.join(" "); } //#endregion //#region src/views/SimpleView.ts /** * Handles rendering of the simple view mode. * Supports three display modes: * - Message-only: Shows timestamp, level and message * - Inline: Shows all details with complete data inline * - Expanded: Shows timestamp, level, and message on first line, with data on indented separate lines */ var SimpleView = class { config; viewMode; maxInlineDepth; showLogId; timestampFormat; collapseArrays; flattenNestedObjects; runtime; includeDataInBrowserConsole; constructor(config) { this.config = config; this.viewMode = config.viewMode; this.maxInlineDepth = config.maxInlineDepth; this.showLogId = config.showLogId; this.timestampFormat = config.timestampFormat; this.collapseArrays = config.collapseArrays !== false; this.flattenNestedObjects = config.flattenNestedObjects !== false; this.runtime = config.runtime; this.includeDataInBrowserConsole = config.includeDataInBrowserConsole || false; } getConfig() { return this.config; } getViewMode() { return this.viewMode; } /** * Writes a message to the appropriate output based on runtime */ writeMessage(message, level, data) { if (this.runtime === "node") process?.stdout?.write?.(`${message}\n`); else { const shouldIncludeData = this.includeDataInBrowserConsole && data !== void 0; switch (level) { case "trace": if (shouldIncludeData) console.debug(message, data); else console.debug(message); break; case "debug": if (shouldIncludeData) console.debug(message, data); else console.debug(message); break; case "info": if (shouldIncludeData) console.info(message, data); else console.info(message); break; case "warn": if (shouldIncludeData) console.warn(message, data); else console.warn(message); break; case "error": case "fatal": if (shouldIncludeData) console.error(message, data); else console.error(message); break; default: if (shouldIncludeData) console.log(message, data); else console.log(message); break; } } } /** * Renders a single log entry based on the current view mode */ renderLogLine(entry) { const chevron = getLevelColor(entry.level, this.config.config.colors)(`▶ ${entry.level.toUpperCase()} `); const message = entry.message || "(no message)"; const timestamp = formatTimestamp(entry.timestamp, this.config.config.logIdColor, this.timestampFormat); const parsedData = entry.data ? JSON.parse(entry.data) : void 0; switch (this.viewMode) { case "message-only": { const condensedLine = `${timestamp} ${chevron}${message}`; this.writeMessage(condensedLine, entry.level, parsedData); break; } case "inline": { const logId = this.showLogId ? this.config.config.logIdColor(`[${entry.id}]`) : ""; const fullData = entry.data ? formatInlineData(JSON.parse(entry.data), this.config.config, this.maxInlineDepth, !this.flattenNestedObjects, this.collapseArrays) : ""; const expandedLine = `${timestamp} ${chevron}${logId ? `${logId} ` : ""}${message}${fullData ? ` ${fullData}` : ""}`; this.writeMessage(expandedLine, entry.level, parsedData); break; } case "expanded": { const logId = this.showLogId ? this.config.config.logIdColor(`[${entry.id}]`) : ""; const firstLine = `${timestamp} ${chevron}${logId ? `${logId} ` : ""}${message}`; this.writeMessage(firstLine, entry.level, parsedData); if (entry.data) { const jsonLines = render(JSON.parse(entry.data), { defaultIndentation: 2, keysColor: this.config.config.dataKeyColor, dashColor: this.config.config.dataKeyColor, numberColor: this.config.config.dataValueColor, stringColor: this.config.config.dataValueColor, multilineStringColor: this.config.config.dataValueColor, positiveNumberColor: this.config.config.dataValueColor, negativeNumberColor: this.config.config.dataValueColor, booleanColor: this.config.config.dataValueColor, nullUndefinedColor: this.config.config.dataValueColor, dateColor: this.config.config.dataValueColor, collapseArrays: this.collapseArrays }).split("\n"); for (const line of jsonLines) if (line.trim() !== "") this.writeMessage(` ${line}`, entry.level); } break; } default: { const logId = this.showLogId ? this.config.config.logIdColor(`[${entry.id}]`) : ""; const fullData = entry.data ? formatInlineData(JSON.parse(entry.data), this.config.config, this.maxInlineDepth, true, this.collapseArrays) : ""; const expandedLine = `${timestamp} ${chevron}${logId ? `${logId} ` : ""}${message}${fullData ? ` ${fullData}` : ""}`; this.writeMessage(expandedLine, entry.level, parsedData); break; } } } }; //#endregion //#region src/SimplePrettyTerminalTransport.ts /** * Main transport class that handles simple pretty terminal output. * This class provides the display functionality of PrettyTerminal without interactive features. * * The transport supports three view modes: * 1. Inline: Shows all information with complete data structures inline (no truncation) * 2. Message-only: Shows only the timestamp, log level and message for a cleaner output (no data shown) * 3. Expanded: Shows timestamp, level, and message on first line, with data on indented separate lines */ var SimplePrettyTerminalTransport = class extends LoggerlessTransport { /** Handles rendering and formatting of logs */ renderer; /** Configuration options */ config; /** * Creates a new SimplePrettyTerminalTransport instance. * * @param config - Configuration options for the transport */ constructor(config) { super(config); this.config = config; if (config.enabled === false) return; const maxInlineDepth = config.maxInlineDepth || 4; const theme = config.theme || moonlight; const viewMode = config.viewMode || "inline"; const showLogId = config.showLogId || false; const timestampFormat = config.timestampFormat || "HH:mm:ss.SSS"; const collapseArrays = config.collapseArrays !== false; const flattenNestedObjects = config.flattenNestedObjects !== false; const runtime = config.runtime; const includeDataInBrowserConsole = config.includeDataInBrowserConsole || false; const viewConfig = { colors: { trace: chalk.gray, debug: chalk.blue, info: chalk.green, warn: chalk.yellow, error: chalk.red, fatal: chalk.bgRed.white, ...theme.colors }, logIdColor: theme.logIdColor || chalk.dim, dataValueColor: theme.dataValueColor || chalk.white, dataKeyColor: theme.dataKeyColor || chalk.dim }; this.renderer = new SimpleView({ viewMode, maxInlineDepth, showLogId, timestampFormat, collapseArrays, flattenNestedObjects, runtime, includeDataInBrowserConsole, config: viewConfig }); } /** * Generates a random ID for each log entry. * Uses base36 encoding for compact, readable IDs. * * @returns A 6-character string ID * @example * "a1b2c3" // Example generated ID */ generateId() { return Math.random().toString(36).substring(2, 8); } /** * Applies sprintf formatting to messages if enabled and applicable. * If the first message is a format string and there are additional arguments, * formats them using sprintf-js. * * @param messages - Array of message strings * @returns Formatted message string */ formatMessages(messages) { if (!this.config.enableSprintf || messages.length < 2) return messages.join(" "); const [format, ...args] = messages; if (typeof format !== "string") return messages.join(" "); try { return sprintf(format, ...args); } catch (_error) { return messages.join(" "); } } /** * Main transport method that receives logs from LogLayer. * This method is called for each log event and handles: * - Generating a unique ID for the log * - Converting the log data to a storable format * - Rendering the log using the SimpleView renderer * * @param logLevel - The severity level of the log * @param messages - Array of message strings to be joined * @param data - Additional structured data to be logged * @param hasData - Whether the log includes additional data * @returns The original messages array */ shipToLogger({ logLevel, messages, data, hasData }) { if (this.config.enabled === false) return messages; const entry = { id: this.generateId(), timestamp: Date.now(), level: logLevel, message: this.formatMessages(messages), data: hasData ? JSON.stringify(data) : null }; this.renderer.renderLogLine(entry); return messages; } /** * Changes the view mode for log display. * * @param viewMode - The new view mode to use */ setViewMode(viewMode) { if (this.config.enabled === false) return; const theme = this.config.theme || moonlight; const runtime = this.config.runtime; const viewConfig = { colors: { trace: chalk.gray, debug: chalk.blue, info: chalk.green, warn: chalk.yellow, error: chalk.red, fatal: chalk.bgRed.white, ...theme.colors }, logIdColor: theme.logIdColor || chalk.dim, dataValueColor: theme.dataValueColor || chalk.white, dataKeyColor: theme.dataKeyColor || chalk.dim }; this.renderer = new SimpleView({ viewMode, maxInlineDepth: this.config.maxInlineDepth || 4, showLogId: this.config.showLogId || false, timestampFormat: this.config.timestampFormat || "HH:mm:ss.SSS", collapseArrays: this.config.collapseArrays !== false, flattenNestedObjects: this.config.flattenNestedObjects !== false, runtime, includeDataInBrowserConsole: this.config.includeDataInBrowserConsole || false, config: viewConfig }); } /** * Gets the current view mode. * * @returns The current view mode */ getViewMode() { return this.renderer.getViewMode(); } }; //#endregion //#region src/index.ts function getSimplePrettyTerminal(config) { return new SimplePrettyTerminalTransport(config); } //#endregion export { SimplePrettyTerminalTransport, getSimplePrettyTerminal, moonlight, nature, neon, pastel, sunlight }; //# sourceMappingURL=index.js.map