@untools/logger
Version:
An enhanced logger for JavaScript/TypeScript that handles DOM elements and circular references
441 lines (440 loc) • 17.9 kB
JavaScript
"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;