UNPKG

@untools/logger

Version:

An enhanced logger for JavaScript/TypeScript that handles DOM elements and circular references

441 lines (440 loc) 17.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.logger = exports.Logger = void 0; class Logger { constructor(options = {}) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; this.showInProd = (_a = options.showInProd) !== null && _a !== void 0 ? _a : false; this.includeTimestamp = (_b = options.includeTimestamp) !== null && _b !== void 0 ? _b : true; this.maxDepth = (_c = options.maxDepth) !== null && _c !== void 0 ? _c : 5; this.maxStringLength = (_d = options.maxStringLength) !== null && _d !== void 0 ? _d : 10000; this.enableCircularHandling = (_e = options.enableCircularHandling) !== null && _e !== void 0 ? _e : true; this.domElementFormat = (_f = options.domElementFormat) !== null && _f !== void 0 ? _f : "summary"; this.prettyPrint = (_g = options.prettyPrint) !== null && _g !== void 0 ? _g : true; this.indentSize = (_h = options.indentSize) !== null && _h !== void 0 ? _h : 2; this.useColors = (_j = options.colors) !== null && _j !== void 0 ? _j : true; this.env = this.getEnvironment(); // Console color schemes for Node.js this.consoleColors = { string: "\x1b[32m", number: "\x1b[36m", boolean: "\x1b[35m", null: "\x1b[90m", undefined: "\x1b[90m", function: "\x1b[36m", symbol: "\x1b[35m", date: "\x1b[34m", regexp: "\x1b[35m", key: "\x1b[33m", bracket: "\x1b[90m", circular: "\x1b[31m", truncated: "\x1b[90m", maxDepth: "\x1b[90m", reset: "\x1b[0m", // Reset }; // CSS color schemes for browser this.browserColors = { string: "color: green;", number: "color: cyan;", boolean: "color: magenta;", null: "color: gray;", undefined: "color: gray;", function: "color: cyan;", symbol: "color: magenta;", date: "color: blue;", regexp: "color: magenta;", key: "color: #b58900;", bracket: "color: gray;", circular: "color: red;", truncated: "color: gray;", maxDepth: "color: gray;", reset: "", }; } getEnvironment() { let isNode = false; let isBrowser = false; let isEdgeRuntime = false; let isDevelopment = true; // First, check for Edge Runtime as it's the most restrictive if (typeof process !== "undefined" && typeof process.env === "object" && process.env.NEXT_RUNTIME === "edge") { isEdgeRuntime = true; isDevelopment = process.env.NODE_ENV !== "production"; } // Then check for browser else if (typeof window !== "undefined") { isBrowser = true; isDevelopment = !window.location.hostname.includes("production") && (typeof process === "undefined" || (typeof process.env === "object" && process.env.NODE_ENV !== "production")); } // Lastly check for Node.js (but only if not in Edge Runtime) else if (!isEdgeRuntime && typeof process !== "undefined") { try { // This block won't be executed in Edge Runtime isNode = typeof process.versions === "object" && process.versions != null; isDevelopment = typeof process.env === "object" && process.env.NODE_ENV !== "production"; } catch (e) { // Fallback for environments where process exists but versions is not accessible isNode = false; } } return { isDevelopment, isNode, isBrowser, isEdgeRuntime }; } getCallerInfo() { var _a, _b; const callerInfo = { file: "unknown", line: "unknown", column: "unknown" }; try { const stack = new Error().stack; if (stack) { // Split the stack trace into lines const lines = stack.split("\n"); // Find the first line that doesn't include our logger file let callerLine = ""; for (let i = 1; i < lines.length; i++) { if (!lines[i].includes("debugLogger.ts") && !lines[i].includes("at Logger.")) { callerLine = lines[i]; break; } } if (this.env.isNode) { // Node.js style stack trace const matches = callerLine === null || callerLine === void 0 ? void 0 : callerLine.match(/\((.*):(\d+):(\d+)\)$/); if (matches) { callerInfo.file = ((_a = matches[1]) === null || _a === void 0 ? void 0 : _a.split("/").pop()) || "unknown"; callerInfo.line = matches[2] || "unknown"; callerInfo.column = matches[3] || "unknown"; } } else { // Browser style stack trace const matches = callerLine === null || callerLine === void 0 ? void 0 : callerLine.match(/at (?:.*? \()?(.+):(\d+):(\d+)/); if (matches) { callerInfo.file = ((_b = matches[1]) === null || _b === void 0 ? void 0 : _b.split("/").pop()) || "unknown"; callerInfo.line = matches[2] || "unknown"; callerInfo.column = matches[3] || "unknown"; } } } } catch (_c) { // If stack trace parsing fails, we'll use default unknown values } return callerInfo; } formatMetadata(level) { const callerInfo = this.getCallerInfo(); const timestamp = this.includeTimestamp ? new Date().toISOString() : ""; return [ `[${level.toUpperCase()}]`, this.includeTimestamp ? `[${timestamp}]` : "", `[${callerInfo.file}:${callerInfo.line}]`, ].filter(Boolean); } formatDOMElement(element) { var _a, _b; if (this.domElementFormat === "disabled") { return "[DOM Element]"; } // Safety check - ensure we're dealing with a DOM element if (!this.env.isBrowser || !element || typeof element.tagName !== "string") { return "[DOM Element]"; } if (this.domElementFormat === "inspect") { let attributes = ""; if (element.attributes) { for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; attributes += ` ${attr.name}="${attr.value}"`; } } let children = ""; if (element.children && element.children.length > 0) { children = ` (${element.children.length} children)`; } return `<${element.tagName.toLowerCase()}${attributes}>${children}`; } // Default: summary return `<${element.tagName.toLowerCase()}> with ${((_a = element.attributes) === null || _a === void 0 ? void 0 : _a.length) || 0} attributes and ${((_b = element.children) === null || _b === void 0 ? void 0 : _b.length) || 0} children`; } colorizeText(text, type) { if (!this.useColors) return text; if (this.env.isNode && !this.env.isEdgeRuntime) { return `${this.consoleColors[type]}${text}${this.consoleColors.reset}`; } return text; // In browser we handle coloring differently via CSS } getIndentation(depth) { return " ".repeat(depth * this.indentSize); } formatArgument(arg, options = {}) { var _a, _b, _c; const depth = (_a = options.depth) !== null && _a !== void 0 ? _a : 0; const seen = (_b = options.seen) !== null && _b !== void 0 ? _b : new WeakMap(); const indentation = (_c = options.indentation) !== null && _c !== void 0 ? _c : ""; // Check for max depth to prevent call stack overflow if (depth > this.maxDepth) { return this.colorizeText("[Max Depth Reached]", "maxDepth"); } if (arg === null) { return this.colorizeText("null", "null"); } if (arg === undefined) { return this.colorizeText("undefined", "undefined"); } if (typeof arg === "string") { if (arg.length > this.maxStringLength) { const truncated = `${arg.substring(0, this.maxStringLength)}...`; const truncatedMsg = this.colorizeText(` [truncated, ${arg.length} chars total]`, "truncated"); return this.colorizeText(`"${truncated}"`, "string") + truncatedMsg; } return this.colorizeText(`"${arg}"`, "string"); } if (typeof arg === "number") { return this.colorizeText(String(arg), "number"); } if (typeof arg === "boolean") { return this.colorizeText(String(arg), "boolean"); } if (typeof arg === "symbol") { return this.colorizeText(String(arg), "symbol"); } if (typeof arg === "bigint") { return this.colorizeText(`${String(arg)}n`, "number"); } if (typeof arg === "function") { return this.colorizeText(`[Function: ${arg.name || "anonymous"}]`, "function"); } // Safely check if the argument is a DOM Element if (this.env.isBrowser && typeof window !== "undefined" && typeof Element !== "undefined" && arg instanceof Element) { return this.formatDOMElement(arg); } if (arg instanceof Error) { return `${arg.name}: ${arg.message}\n${arg.stack || ""}`; } if (arg instanceof Date) { return this.colorizeText(arg.toISOString(), "date"); } if (arg instanceof RegExp) { return this.colorizeText(arg.toString(), "regexp"); } if (Array.isArray(arg)) { if (this.enableCircularHandling && seen.has(arg)) { return this.colorizeText("[Circular Reference]", "circular"); } if (this.enableCircularHandling) { seen.set(arg, true); } if (arg.length === 0) { return this.colorizeText("[]", "bracket"); } if (!this.prettyPrint) { const items = arg .map((item) => this.formatArgument(item, { depth: depth + 1, seen })) .join(", "); return (this.colorizeText("[", "bracket") + items + this.colorizeText("]", "bracket")); } const nextIndent = indentation + this.getIndentation(1); const items = arg .map((item) => nextIndent + this.formatArgument(item, { depth: depth + 1, seen, indentation: nextIndent, })) .join(",\n"); return (this.colorizeText("[", "bracket") + "\n" + items + "\n" + indentation + this.colorizeText("]", "bracket")); } if (typeof arg === "object" && arg !== null) { if (this.enableCircularHandling && seen.has(arg)) { return this.colorizeText("[Circular Reference]", "circular"); } if (this.enableCircularHandling) { seen.set(arg, true); } try { const entries = Object.entries(arg); if (entries.length === 0) { return this.colorizeText("{}", "bracket"); } if (!this.prettyPrint) { const formattedEntries = entries.map(([key, value]) => this.colorizeText(key, "key") + ": " + this.formatArgument(value, { depth: depth + 1, seen })); return (this.colorizeText("{", "bracket") + formattedEntries.join(", ") + this.colorizeText("}", "bracket")); } const nextIndent = indentation + this.getIndentation(1); const formattedEntries = entries .map(([key, value]) => nextIndent + this.colorizeText(key, "key") + ": " + this.formatArgument(value, { depth: depth + 1, seen, indentation: nextIndent, })) .join(",\n"); return (this.colorizeText("{", "bracket") + "\n" + formattedEntries + "\n" + indentation + this.colorizeText("}", "bracket")); } catch (error) { // Type-safe error handling const errorMessage = error instanceof Error ? error.message : "unknown error"; return `[Object: failed to stringify - ${errorMessage}]`; } } return String(arg); } _log(level, ...args) { // Check if we should show logs in current environment if (!this.env.isDevelopment && !this.showInProd) { return; } const metadata = this.formatMetadata(level); // Browser-specific styling if (this.env.isBrowser && !this.env.isNode) { const styles = { log: "color: #0099cc", debug: "color: #0099cc", info: "color: #00cc00", warn: "color: #cccc00", error: "color: #cc0000", }; // Special handling for objects in browser const formattedArgs = []; const rawObjects = []; args.forEach((arg) => { if ((typeof window !== "undefined" && typeof Element !== "undefined" && arg instanceof Element) || (typeof arg === "object" && arg !== null)) { // We'll pretty-print complex objects but also log raw objects for inspection formattedArgs.push(this.formatArgument(arg)); if (this.prettyPrint) { rawObjects.push(arg); } } else { formattedArgs.push(this.formatArgument(arg)); } }); // Log with formatting console[level](`%c${[...metadata, ...formattedArgs].join(" ")}`, styles[level]); // Additionally log raw objects for better browser inspection if they exist if (this.prettyPrint && rawObjects.length > 0) { console.groupCollapsed("Raw Objects (for inspection)"); rawObjects.forEach((obj) => console[level](obj)); console.groupEnd(); } return; } // Node.js or Edge Runtime output with colors const colors = { log: "\x1b[36m", debug: "\x1b[36m", info: "\x1b[32m", warn: "\x1b[33m", error: "\x1b[31m", reset: "\x1b[0m", // Reset }; // In Edge Runtime, we might want to skip fancy colors if (this.env.isEdgeRuntime || !this.useColors) { const formattedArgs = args.map((arg) => this.formatArgument(arg)); console[level](...metadata, ...formattedArgs); } else { const formattedArgs = args.map((arg) => this.formatArgument(arg)); console[level](colors[level], ...metadata, ...formattedArgs, colors.reset); } } log(...args) { this._log("log", ...args); } debug(...args) { this._log("debug", ...args); } info(...args) { this._log("info", ...args); } warn(...args) { this._log("warn", ...args); } error(...args) { this._log("error", ...args); } // Group logging for better organization - safe for all environments group(label) { if (!this.env.isDevelopment && !this.showInProd) { return; } if (typeof console.group === "function") { console.group(label); } else { console.log(`=== GROUP START: ${label} ===`); } } groupEnd() { if (!this.env.isDevelopment && !this.showInProd) { return; } if (typeof console.groupEnd === "function") { console.groupEnd(); } else { console.log("=== GROUP END ==="); } } // Utility method to time operations time(label) { if (!this.env.isDevelopment && !this.showInProd) { return; } if (typeof console.time === "function") { console.time(label); } } timeEnd(label) { if (!this.env.isDevelopment && !this.showInProd) { return; } if (typeof console.timeEnd === "function") { console.timeEnd(label); } } } exports.Logger = Logger; // Create default instance with pretty print enabled const logger = new Logger({ prettyPrint: true, colors: true, }); exports.logger = logger;