UNPKG

logsdx

Version:

log streaming with dx on the 🧠

945 lines (901 loc) • 29.4 kB
import chalk, { type ChalkInstance } from "chalk"; import JSON5 from "json5"; import type { LogLevel } from "@/src/types"; import { loadConfig } from "./loader"; // Force chalk to use colors (level 3) for consistent testing chalk.level = 3; // Theme configuration type export type ThemeConfig = { name: string; levels: { [key in LogLevel]?: { color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; }; fields: { [key: string]: { color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; }; status: { [key: string]: { color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; }; patterns: { [key: string]: { regex: string | RegExp; color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; }; elements: { brackets: { color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; keyValue: { key: { color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; separator: { color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; value: { color: string; bold?: boolean; dim?: boolean; italic?: boolean; }; }; }; }; // Built-in themes export const THEMES = { default: { name: "default", levels: { error: { color: "red", bold: true }, warn: { color: "yellow" }, info: { color: "blue" }, debug: { color: "gray", dim: true }, success: { color: "green" }, trace: { color: "white", dim: true }, }, fields: { timestamp: { color: "gray" }, service: { color: "cyan" }, action: { color: "magenta" }, user: { color: "blue" }, duration: { color: "yellow" }, requestId: { color: "gray" }, correlationId: { color: "gray" }, environment: { color: "green" }, version: { color: "magenta" }, method: { color: "cyan" }, path: { color: "white" }, statusCode: { color: "yellow" }, ip: { color: "gray" }, userAgent: { color: "gray", dim: true }, }, status: { success: { color: "green" }, failure: { color: "red" }, error: { color: "red" }, pending: { color: "yellow" }, cancelled: { color: "gray" }, timeout: { color: "red" }, }, patterns: { error: { regex: /\b(error|exception|failed|failure)\b/i, color: "red", bold: true, }, warning: { regex: /\b(warning|warn|caution)\b/i, color: "yellow" }, success: { regex: /\b(success|succeeded|completed)\b/i, color: "green" }, http: { regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/, color: "cyan", }, url: { regex: /https?:\/\/[^\s]+/, color: "blue", italic: true }, ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "gray" }, email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "blue" }, uuid: { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, color: "magenta", }, timestamp: { regex: /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/, color: "gray", }, json: { regex: /\{.*\}|\[.*\]/, color: "white" }, }, elements: { brackets: { color: "yellow" }, keyValue: { key: { color: "cyan" }, separator: { color: "gray" }, value: { color: "white" }, }, }, }, dark: { name: "dark", levels: { error: { color: "#ff5555", bold: true }, warn: { color: "#ffb86c" }, info: { color: "#8be9fd" }, debug: { color: "#6272a4", dim: true }, success: { color: "#50fa7b" }, trace: { color: "#f8f8f2", dim: true }, }, fields: { timestamp: { color: "#6272a4" }, service: { color: "#8be9fd" }, action: { color: "#bd93f9" }, user: { color: "#8be9fd" }, duration: { color: "#ffb86c" }, requestId: { color: "#6272a4" }, correlationId: { color: "#6272a4" }, environment: { color: "#50fa7b" }, version: { color: "#bd93f9" }, method: { color: "#8be9fd" }, path: { color: "#f8f8f2" }, statusCode: { color: "#ffb86c" }, ip: { color: "#6272a4" }, userAgent: { color: "#6272a4", dim: true }, }, status: { success: { color: "#50fa7b" }, failure: { color: "#ff5555" }, error: { color: "#ff5555" }, pending: { color: "#ffb86c" }, cancelled: { color: "#6272a4" }, timeout: { color: "#ff5555" }, }, patterns: { error: { regex: /\b(error|exception|failed|failure)\b/i, color: "#ff5555", bold: true, }, warning: { regex: /\b(warning|warn|caution)\b/i, color: "#ffb86c" }, success: { regex: /\b(success|succeeded|completed)\b/i, color: "#50fa7b", }, http: { regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/, color: "#8be9fd", }, url: { regex: /https?:\/\/[^\s]+/, color: "#8be9fd", italic: true }, ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#6272a4" }, email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#8be9fd" }, uuid: { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, color: "#bd93f9", }, timestamp: { regex: /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/, color: "#6272a4", }, json: { regex: /\{.*\}|\[.*\]/, color: "#f8f8f2" }, }, elements: { brackets: { color: "#6272a4" }, keyValue: { key: { color: "#8be9fd" }, separator: { color: "#6272a4" }, value: { color: "#f8f8f2" }, }, }, }, light: { name: "light", levels: { error: { color: "#d32f2f", bold: true }, warn: { color: "#f57c00" }, info: { color: "#1976d2" }, debug: { color: "#757575", dim: true }, success: { color: "#388e3c" }, trace: { color: "#212121", dim: true }, }, fields: { timestamp: { color: "#757575" }, service: { color: "#0097a7" }, action: { color: "#7b1fa2" }, user: { color: "#1976d2" }, duration: { color: "#f57c00" }, requestId: { color: "#757575" }, correlationId: { color: "#757575" }, environment: { color: "#388e3c" }, version: { color: "#7b1fa2" }, method: { color: "#0097a7" }, path: { color: "#212121" }, statusCode: { color: "#f57c00" }, ip: { color: "#757575" }, userAgent: { color: "#757575", dim: true }, }, status: { success: { color: "#388e3c" }, failure: { color: "#d32f2f" }, error: { color: "#d32f2f" }, pending: { color: "#f57c00" }, cancelled: { color: "#757575" }, timeout: { color: "#d32f2f" }, }, patterns: { error: { regex: /\b(error|exception|failed|failure)\b/i, color: "#d32f2f", bold: true, }, warning: { regex: /\b(warning|warn|caution)\b/i, color: "#f57c00" }, success: { regex: /\b(success|succeeded|completed)\b/i, color: "#388e3c", }, http: { regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/, color: "#0097a7", }, url: { regex: /https?:\/\/[^\s]+/, color: "#1976d2", italic: true }, ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#757575" }, email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#1976d2" }, uuid: { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, color: "#7b1fa2", }, timestamp: { regex: /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/, color: "#757575", }, json: { regex: /\{.*\}|\[.*\]/, color: "#212121" }, }, elements: { brackets: { color: "#757575" }, keyValue: { key: { color: "#0097a7" }, separator: { color: "#757575" }, value: { color: "#212121" }, }, }, }, minimal: { name: "minimal", levels: { error: { color: "red" }, warn: { color: "yellow" }, info: { color: "white" }, debug: { color: "gray", dim: true }, success: { color: "green" }, trace: { color: "gray", dim: true }, }, fields: { timestamp: { color: "gray" }, service: { color: "white" }, action: { color: "white" }, user: { color: "white" }, duration: { color: "white" }, requestId: { color: "gray" }, correlationId: { color: "gray" }, environment: { color: "white" }, version: { color: "white" }, method: { color: "white" }, path: { color: "white" }, statusCode: { color: "white" }, ip: { color: "gray" }, userAgent: { color: "gray", dim: true }, }, status: { success: { color: "green" }, failure: { color: "red" }, error: { color: "red" }, pending: { color: "yellow" }, cancelled: { color: "gray" }, timeout: { color: "red" }, }, patterns: { error: { regex: /\b(error|exception|failed|failure)\b/i, color: "red" }, warning: { regex: /\b(warning|warn|caution)\b/i, color: "yellow" }, success: { regex: /\b(success|succeeded|completed)\b/i, color: "green" }, http: { regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/, color: "white", }, url: { regex: /https?:\/\/[^\s]+/, color: "white" }, ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "gray" }, email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "white" }, uuid: { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, color: "white", }, timestamp: { regex: /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/, color: "gray", }, json: { regex: /\{.*\}|\[.*\]/, color: "white" }, }, elements: { brackets: { color: "gray" }, keyValue: { key: { color: "white" }, separator: { color: "gray" }, value: { color: "white" }, }, }, }, dracula: { name: "dracula", levels: { error: { color: "#ff5555", bold: true }, // Red warn: { color: "#f1fa8c" }, // Yellow info: { color: "#8be9fd" }, // Cyan debug: { color: "#6272a4", dim: true }, // Comment Purple success: { color: "#50fa7b" }, // Green trace: { color: "#f8f8f2", dim: true }, // Foreground }, fields: { timestamp: { color: "#6272a4" }, service: { color: "#ff79c6" }, // Pink action: { color: "#bd93f9" }, // Purple user: { color: "#8be9fd" }, // Cyan (Placeholder - adjust if needed) duration: { color: "#f1fa8c" }, // Yellow requestId: { color: "#6272a4" }, correlationId: { color: "#6272a4" }, environment: { color: "#50fa7b" }, // Green version: { color: "#bd93f9" }, // Purple method: { color: "#8be9fd" }, // Cyan path: { color: "#f8f8f2" }, // Foreground statusCode: { color: "#ffb86c" }, // Orange ip: { color: "#6272a4" }, userAgent: { color: "#6272a4", dim: true }, }, status: { success: { color: "#50fa7b" }, failure: { color: "#ff5555" }, error: { color: "#ff5555" }, pending: { color: "#f1fa8c" }, cancelled: { color: "#6272a4" }, timeout: { color: "#ff5555" }, }, patterns: { // Colors adjusted to match Dracula error: { regex: /\b(error|exception|failed|failure)\b/i, color: "#ff5555", bold: true, }, warning: { regex: /\b(warning|warn|caution)\b/i, color: "#f1fa8c" }, success: { regex: /\b(success|succeeded|completed)\b/i, color: "#50fa7b", }, http: { regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/, color: "#8be9fd", }, url: { regex: /https?:\/\/[^\s]+/, color: "#8be9fd", italic: true }, ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#6272a4" }, email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#8be9fd" }, uuid: { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, color: "#bd93f9", }, timestamp: { regex: /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/, color: "#6272a4", }, json: { regex: /\{.*\}|\[.*\]/, color: "#f8f8f2" }, }, elements: { brackets: { color: "#f8f8f2" }, // Foreground for {}, [], " keyValue: { key: { color: "#8be9fd" }, // Cyan for keys separator: { color: "#ff79c6" }, // Pink for : value: { color: "#f8f8f2" }, // Foreground for values }, }, }, githubLight: { name: "githubLight", levels: { error: { color: "#d73a49", bold: true }, // Red-600 warn: { color: "#b08800" }, // Yellow-700 info: { color: "#0366d6" }, // Blue-600 debug: { color: "#586069", dim: true }, // Gray-600 success: { color: "#22863a" }, // Green-600 trace: { color: "#24292e", dim: true }, // Gray-900 (Default Text) }, fields: { timestamp: { color: "#586069" }, service: { color: "#6f42c1" }, // Purple-600 action: { color: "#005cc5" }, // Blue-700 user: { color: "#0366d6" }, // Blue-600 duration: { color: "#e36209" }, // Orange-600 requestId: { color: "#586069" }, correlationId: { color: "#586069" }, environment: { color: "#22863a" }, // Green-600 version: { color: "#6f42c1" }, // Purple-600 method: { color: "#005cc5" }, // Blue-700 path: { color: "#24292e" }, // Gray-900 statusCode: { color: "#b08800" }, // Yellow-700 ip: { color: "#586069" }, userAgent: { color: "#586069", dim: true }, }, status: { success: { color: "#22863a" }, failure: { color: "#d73a49" }, error: { color: "#d73a49" }, pending: { color: "#b08800" }, cancelled: { color: "#586069" }, timeout: { color: "#d73a49" }, }, patterns: { error: { regex: /\b(error|exception|failed|failure)\b/i, color: "#d73a49", bold: true, }, warning: { regex: /\b(warning|warn|caution)\b/i, color: "#b08800" }, success: { regex: /\b(success|succeeded|completed)\b/i, color: "#22863a", }, http: { regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/, color: "#005cc5", }, url: { regex: /https?:\/\/[^\s]+/, color: "#0366d6", italic: true }, ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#586069" }, email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#0366d6" }, uuid: { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, color: "#6f42c1", }, timestamp: { regex: /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/, color: "#586069", }, json: { regex: /\{.*\}|\[.*\]/, color: "#24292e" }, }, elements: { brackets: { color: "#586069" }, keyValue: { key: { color: "#005cc5" }, separator: { color: "#586069" }, value: { color: "#24292e" }, }, }, }, githubDark: { name: "githubDark", levels: { error: { color: "#f97583", bold: true }, warn: { color: "#dbab09" }, info: { color: "#79b8ff" }, debug: { color: "#6a737d", dim: true }, success: { color: "#85e89d" }, trace: { color: "#d1d5da", dim: true }, }, fields: { timestamp: { color: "#6a737d" }, service: { color: "#b392f0" }, action: { color: "#58a6ff" }, user: { color: "#79b8ff" }, duration: { color: "#ffab70" }, requestId: { color: "#6a737d" }, correlationId: { color: "#6a737d" }, environment: { color: "#85e89d" }, version: { color: "#b392f0" }, method: { color: "#58a6ff" }, path: { color: "#d1d5da" }, statusCode: { color: "#dbab09" }, ip: { color: "#6a737d" }, userAgent: { color: "#6a737d", dim: true }, }, status: { success: { color: "#85e89d" }, failure: { color: "#f97583" }, error: { color: "#f97583" }, pending: { color: "#dbab09" }, cancelled: { color: "#6a737d" }, timeout: { color: "#f97583" }, }, patterns: { error: { regex: /\b(error|exception|failed|failure)\b/i, color: "#f97583", bold: true, }, warning: { regex: /\b(warning|warn|caution)\b/i, color: "#dbab09" }, success: { regex: /\b(success|succeeded|completed)\b/i, color: "#85e89d", }, http: { regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/, color: "#58a6ff", }, url: { regex: /https?:\/\/[^\s]+/, color: "#79b8ff", italic: true }, ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#6a737d" }, email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#79b8ff" }, uuid: { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, color: "#b392f0", }, timestamp: { regex: /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/, color: "#6a737d", }, json: { regex: /\{.*\}|\[.*\]/, color: "#d1d5da" }, }, elements: { brackets: { color: "#6a737d" }, keyValue: { key: { color: "#79b8ff" }, separator: { color: "#6a737d" }, value: { color: "#d1d5da" }, }, }, }, } as const; // Create a chalk style from theme config export function createStyle( config: | ThemeConfig["levels"][LogLevel] | ThemeConfig["fields"][string] | ThemeConfig["status"][string] | ThemeConfig["patterns"][string] | ThemeConfig["elements"]["brackets"] | ThemeConfig["elements"]["keyValue"]["key"] | ThemeConfig["elements"]["keyValue"]["separator"] | ThemeConfig["elements"]["keyValue"]["value"], ): ChalkInstance { if (!config) return chalk.white; const color = config.color; let style: ChalkInstance; // Handle hex colors if (color.startsWith("#")) { style = chalk.hex(color); } else { // Handle chalk colors style = (chalk[color as keyof typeof chalk] as ChalkInstance) || chalk.white; } if (config.bold) style = style.bold; if (config.dim) style = style.dim; if (config.italic) style = style.italic; return style; } // Style manager class for handling themes and caching export class StyleManager { private static instance: StyleManager; public currentTheme: ThemeConfig; private styleCache: Map<string, ChalkInstance>; private patternCache: Map<string, { regex: RegExp; style: ChalkInstance }>; private constructor() { this.currentTheme = THEMES.default; this.styleCache = new Map(); this.patternCache = new Map(); this.initializeCache(); } public static getInstance(): StyleManager { if (!StyleManager.instance) { StyleManager.instance = new StyleManager(); } return StyleManager.instance; } private initializeCache(): void { // Cache level styles Object.entries(this.currentTheme.levels).forEach(([level, config]) => { this.styleCache.set(`level:${level}`, createStyle(config)); }); // Cache field styles Object.entries(this.currentTheme.fields).forEach(([field, config]) => { this.styleCache.set(`field:${field}`, createStyle(config)); }); // Cache status styles Object.entries(this.currentTheme.status).forEach(([status, config]) => { this.styleCache.set(`status:${status}`, createStyle(config)); }); // Cache element styles this.styleCache.set( "brackets", createStyle(this.currentTheme.elements.brackets), ); this.styleCache.set( "keyValue:key", createStyle(this.currentTheme.elements.keyValue.key), ); this.styleCache.set( "keyValue:separator", createStyle(this.currentTheme.elements.keyValue.separator), ); this.styleCache.set( "keyValue:value", createStyle(this.currentTheme.elements.keyValue.value), ); // Cache pattern styles Object.entries(this.currentTheme.patterns).forEach(([pattern, config]) => { this.patternCache.set(pattern, { regex: typeof config.regex === "string" ? new RegExp(config.regex) : config.regex, style: createStyle(config), }); }); } public setTheme( themeName: keyof typeof THEMES | Partial<ThemeConfig> | string, ): void { if (typeof themeName === "string") { // Check if it's a built-in theme if (themeName in THEMES) { this.currentTheme = THEMES[themeName as keyof typeof THEMES]; } else { // Try to load from config const config = loadConfig(); if ( config?.customThemes && themeName in config.customThemes && config.customThemes[themeName] ) { // Ensure we have a valid theme config by merging with default const customTheme = { ...THEMES.default, ...config.customThemes[themeName], name: themeName, }; this.currentTheme = customTheme; } else { // Fall back to default theme if not found this.currentTheme = THEMES.default; } } } else { // Merge with default theme to ensure all required properties are present this.currentTheme = { name: themeName.name || "custom", levels: { ...THEMES.default.levels, ...themeName.levels }, fields: { ...THEMES.default.fields, ...themeName.fields }, status: { ...THEMES.default.status, ...themeName.status }, patterns: { ...THEMES.default.patterns, ...themeName.patterns }, elements: { brackets: { ...THEMES.default.elements.brackets, ...themeName.elements?.brackets, }, keyValue: { key: { ...THEMES.default.elements.keyValue.key, ...themeName.elements?.keyValue?.key, }, separator: { ...THEMES.default.elements.keyValue.separator, ...themeName.elements?.keyValue?.separator, }, value: { ...THEMES.default.elements.keyValue.value, ...themeName.elements?.keyValue?.value, }, }, }, }; } this.styleCache.clear(); this.patternCache.clear(); this.initializeCache(); } public getLevelStyle(level: LogLevel): ChalkInstance { return this.styleCache.get(`level:${level}`) || chalk.white; } public getFieldStyle(field: string): ChalkInstance { return this.styleCache.get(`field:${field}`) || chalk.white; } public getStatusStyle(status: string): ChalkInstance { return this.styleCache.get(`status:${status}`) || chalk.white; } public getBracketStyle(): ChalkInstance { return this.styleCache.get("brackets") || chalk.gray; } public getKeyValueStyles(): { key: ChalkInstance; separator: ChalkInstance; value: ChalkInstance; } { return { key: this.styleCache.get("keyValue:key") || chalk.cyan, separator: this.styleCache.get("keyValue:separator") || chalk.gray, value: this.styleCache.get("keyValue:value") || chalk.white, }; } public applyPatternStyles(text: string): string { let result = text; // Sort patterns by length (longest first) to avoid overlapping matches const sortedPatterns = Array.from(this.patternCache.entries()).sort( (a, b) => { const aLength = a[1].regex.toString().length; const bLength = b[1].regex.toString().length; return bLength - aLength; }, ); for (const [_, { regex, style }] of sortedPatterns) { result = result.replace(regex, (match) => style(match)); } return result; } public formatJson(obj: Record<string, any>): string { const lines: string[] = []; const bracketStyle = this.getBracketStyle(); const { key: keyStyle, separator: separatorStyle, value: valueStyle, } = this.getKeyValueStyles(); lines.push(bracketStyle("{")); const entries = Object.entries(obj); const lastEntry = entries[entries.length - 1]; for (const [key, val] of entries) { const formattedValue = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val); const isLast = lastEntry && lastEntry[0] === key; lines.push( ` ${bracketStyle('"')}${keyStyle(key)}${bracketStyle('"')}${separatorStyle(":")} ${valueStyle(formattedValue)}${isLast ? "" : separatorStyle(",")}`, ); } lines.push(bracketStyle("}")); return lines.join("\n"); } styleLine( line: string, parsed?: { level?: LogLevel; [key: string]: any }, parserName?: string, ): string { // If parser is json, try to format it compactly if (parserName === "json") { try { const json = JSON5.parse(line); return formatJsonCompact(json, this); // Use the compact formatter } catch (error) { // JSON parsing failed, fall back to default styling for the raw line // Use level from parsed object if available, otherwise default to 'info' const level = parsed?.level || "info"; const style = this.getLevelStyle(level); return this.applyPatternStyles(style(line)); } } // Fallback for non-JSON parsers or if parsing failed above const level = parsed?.level || "info"; const style = this.getLevelStyle(level); const styledLine = style(line); return this.applyPatternStyles(styledLine); } } // Export singleton instance export const styleManager = StyleManager.getInstance(); // UPDATE: Accept optional parserName and pass it to the manager's method export const styleLine = ( line: string, parsed?: { level?: LogLevel; [key: string]: any }, parserName?: string, // Added parserName argument ): string => { // Remove the previous logic, delegate entirely to the manager instance return styleManager.styleLine(line, parsed, parserName); // OLD Logic moved/modified into styleManager.styleLine: // if (parsed.language === "json") { ... } // Apply level-based styling // const level = parsed.level || "info"; // const styledLine = styleManager.getLevelStyle(level)(line); // Apply pattern-based styling // return styleManager.applyPatternStyles(styledLine); }; // Export theme-related functions export const setTheme = ( theme: keyof typeof THEMES | Partial<ThemeConfig> | string, ): void => { if (typeof theme === "string") { // Check if it's a built-in theme if (theme in THEMES) { styleManager.setTheme(theme as keyof typeof THEMES); } else { // Try to load from config const config = loadConfig(); if ( config?.customThemes && theme in config.customThemes && config.customThemes[theme] ) { // Ensure we have a valid theme config by merging with default const customTheme = { ...THEMES.default, ...config.customThemes[theme], name: theme, }; styleManager.setTheme(customTheme); } else { // Fall back to default theme if not found styleManager.setTheme("default"); } } } else { styleManager.setTheme(theme); } }; export const getTheme = () => styleManager.currentTheme; // NEW function: Format JSON compactly on a single line with colors export function formatJsonCompact( obj: any, styleManager: StyleManager, isRoot = true, ): string { const bracketStyle = styleManager.getBracketStyle(); const { key: keyStyle, separator: separatorStyle, value: valueStyle, } = styleManager.getKeyValueStyles(); if (obj === null) { return valueStyle("null"); } if (typeof obj === "string") { // NEW: Style quotes with bracketStyle, content with valueStyle return `${bracketStyle('"')}${valueStyle(obj)}${bracketStyle('"')}`; } if (typeof obj === "number" || typeof obj === "boolean") { return valueStyle(String(obj)); } if (Array.isArray(obj)) { const items = obj .map((item) => formatJsonCompact(item, styleManager, false)) .join(separatorStyle(", ")); return `${bracketStyle("[")}${items}${bracketStyle("]")}`; } if (typeof obj === "object") { const entries = Object.entries(obj) .map(([key, value]) => { const formattedKey = `${bracketStyle('"')}${keyStyle(key)}${bracketStyle('"')}`; const formattedValue = formatJsonCompact(value, styleManager, false); return `${formattedKey}${separatorStyle(":")}${formattedValue}`; }) .join(separatorStyle(", ")); return `${bracketStyle("{")}${entries}${bracketStyle("}")}`; } // Fallback for unexpected types return valueStyle(String(obj)); }