UNPKG

@noxfly/noxus

Version:

Simulate lightweight HTTP-like requests between renderer and main process in Electron applications with MessagePort, with structured and modular design.

431 lines (359 loc) 13.7 kB
/** * @copyright 2025 NoxFly * @license MIT * @author NoxFly */ import * as fs from 'fs'; import * as path from 'path'; /** * Logger is a utility class for logging messages to the console. */ export type LogLevel = | 'debug' | 'comment' | 'log' | 'info' | 'warn' | 'error' | 'critical' ; interface FileLogState { queue: string[]; isWriting: boolean; } /** * Returns a formatted timestamp for logging. */ function getPrettyTimestamp(): string { const now = new Date(); return `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}` + ` ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`; } /** * Generates a log prefix for the console output. * @param callee - The name of the function or class that is logging the message. * @param messageType - The type of message being logged (e.g., 'log', 'info', 'warn', 'error', 'debug'). * @param color - The color to use for the log message. * @returns A formatted string that includes the timestamp, process ID, message type, and callee name. */ function getLogPrefix(callee: string, messageType: string, color?: string): string { const timestamp = getPrettyTimestamp(); const spaces = " ".repeat(10 - messageType.length); let colReset = Logger.colors.initial; let colCallee = Logger.colors.yellow; if(color === undefined) { color = ""; colReset = ""; colCallee = ""; } return `${color}[APP] ${process.pid} - ${colReset}` + `${timestamp}${spaces}` + `${color}${messageType.toUpperCase()}${colReset} ` + `${colCallee}[${callee}]${colReset}`; } /** * Formats an object into a string representation for logging. * It converts the object to JSON and adds indentation for readability. * @param prefix - The prefix to use for the formatted object. * @param arg - The object to format. * @returns A formatted string representation of the object, with each line prefixed by the specified prefix. */ function formatObject(prefix: string, arg: object, enableColor: boolean = true): string { const json = JSON.stringify(arg, null, 2); let colStart = ""; let colLine = ""; let colReset = ""; if(enableColor) { colStart = Logger.colors.darkGrey; colLine = Logger.colors.grey; colReset = Logger.colors.initial; } const prefixedJson = json .split('\n') .map((line, idx) => idx === 0 ? `${colStart}${line}` : `${prefix} ${colLine}${line}`) .join('\n') + colReset; return prefixedJson; } /** * Formats the arguments for logging. * It colors strings and formats objects with indentation. * This function is used to prepare the arguments for console output. * @param prefix - The prefix to use for the formatted arguments. * @param args - The arguments to format. * @param color - The color to use for the formatted arguments. * @returns An array of formatted arguments, where strings are colored and objects are formatted with indentation. */ function formattedArgs(prefix: string, args: any[], color?: string): any[] { let colReset = Logger.colors.initial; if(color === undefined) { color = ""; colReset = ""; } return args.map(arg => { if(typeof arg === "string") { return `${color}${arg}${colReset}`; } else if(typeof arg === "object") { return formatObject(prefix, arg, color !== ""); } return arg; }); } /** * Gets the name of the caller function or class from the stack trace. * This function is used to determine the context of the log message. * @returns The name of the caller function or class. */ function getCallee(): string { const stack = new Error().stack?.split('\n') ?? []; const caller = stack[3] ?.trim() .match(/at (.+?)(?:\..+)? .+$/) ?.[1] ?.replace("Object", "") .replace(/^_/, "") || "App"; return caller; } /** * Checks if the current log level allows logging the specified level. * This function compares the current log level with the specified level to determine if logging should occur. * @param level - The log level to check. * @returns A boolean indicating whether the log level is enabled. */ function canLog(level: LogLevel): boolean { return logLevels.has(level); } /** * Writes a log message to a file asynchronously to avoid blocking the event loop. * It batches messages if writing is already in progress. * @param filepath - The path to the log file. */ function processLogQueue(filepath: string): void { const state = fileStates.get(filepath); if(!state || state.isWriting || state.queue.length === 0) { return; } state.isWriting = true; // Optimization: Grab all pending messages to write in one go const messagesToWrite = state.queue.join('\n') + '\n'; state.queue = []; // Clear the queue immediately const dir = path.dirname(filepath); // Using async IO to allow other operations fs.mkdir(dir, { recursive: true }, (err) => { if(err) { console.error(`[Logger] Failed to create directory ${dir}`, err); state.isWriting = false; return; } fs.appendFile(filepath, messagesToWrite, { encoding: "utf-8" }, (err) => { state.isWriting = false; if(err) { console.error(`[Logger] Failed to write log to ${filepath}`, err); } // If new messages arrived while we were writing, process them now if(state.queue.length > 0) { processLogQueue(filepath); } }); }); } /** * Adds a message to the file queue and triggers processing. */ function enqueue(filepath: string, message: string): void { if(!fileStates.has(filepath)) { fileStates.set(filepath, { queue: [], isWriting: false }); } const state = fileStates.get(filepath)!; state.queue.push(message); processLogQueue(filepath); } /** * */ function output(level: LogLevel, args: any[]): void { if(!canLog(level)) { return; } const callee = getCallee(); { const prefix = getLogPrefix(callee, level, logLevelColors[level]); const data = formattedArgs(prefix, args, logLevelColors[level]); logLevelChannel[level](prefix, ...data); } { const prefix = getLogPrefix(callee, level); const data = formattedArgs(prefix, args); const filepath = fileSettings.get(level)?.filepath; if(filepath) { const message = prefix + " " + data.join(" ").replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI codes enqueue(filepath, message); } } } export namespace Logger { /** * Sets the log level for the logger. * This function allows you to change the log level dynamically at runtime. * This won't affect the startup logs. * * If the parameter is a single LogLevel, all log levels with equal or higher severity will be enabled. * If the parameter is an array of LogLevels, only the specified levels will be enabled. * * @param level Sets the log level for the logger. */ export function setLogLevel(level: LogLevel | LogLevel[]): void { logLevels.clear(); if(Array.isArray(level)) { for(const lvl of level) { logLevels.add(lvl); } } else { const targetRank = logLevelRank[level]; for(const [lvl, rank] of Object.entries(logLevelRank) as [LogLevel, number][]) { if(rank >= targetRank) { logLevels.add(lvl); } } } } /** * Logs a message to the console with log level LOG. * This function formats the message with a timestamp, process ID, and the name of the caller function or class. * It uses different colors for different log levels to enhance readability. * @param args The arguments to log. */ export function log(...args: any[]): void { output("log", args); } /** * Logs a message to the console with log level INFO. * This function formats the message with a timestamp, process ID, and the name of the caller function or class. * It uses different colors for different log levels to enhance readability. * @param args The arguments to log. */ export function info(...args: any[]): void { output("info", args); } /** * Logs a message to the console with log level WARN. * This function formats the message with a timestamp, process ID, and the name of the caller function or class. * It uses different colors for different log levels to enhance readability. * @param args The arguments to log. */ export function warn(...args: any[]): void { output("warn", args); } /** * Logs a message to the console with log level ERROR. * This function formats the message with a timestamp, process ID, and the name of the caller function or class. * It uses different colors for different log levels to enhance readability. * @param args The arguments to log. */ export function error(...args: any[]): void { output("error", args); } /** * Logs a message to the console with log level ERROR and a grey color scheme. */ export function errorStack(...args: any[]): void { output("error", args); } /** * Logs a message to the console with log level DEBUG. * This function formats the message with a timestamp, process ID, and the name of the caller function or class. * It uses different colors for different log levels to enhance readability. * @param args The arguments to log. */ export function debug(...args: any[]): void { output("debug", args); } /** * Logs a message to the console with log level COMMENT. * This function formats the message with a timestamp, process ID, and the name of the caller function or class. * It uses different colors for different log levels to enhance readability. * @param args The arguments to log. */ export function comment(...args: any[]): void { output("comment", args); } /** * Logs a message to the console with log level CRITICAL. * This function formats the message with a timestamp, process ID, and the name of the caller function or class. * It uses different colors for different log levels to enhance readability. * @param args The arguments to log. */ export function critical(...args: any[]): void { output("critical", args); } /** * Enables logging to a file output for the specified log levels. * @param filepath The path to the log file. * @param levels The log levels to enable file logging for. Defaults to all levels. */ export function enableFileLogging(filepath: string, levels: LogLevel[] = ["debug", "comment", "log", "info", "warn", "error", "critical"]): void { for(const level of levels) { fileSettings.set(level, { filepath }); } } /** * Disables logging to a file output for the specified log levels. * @param levels The log levels to disable file logging for. Defaults to all levels. */ export function disableFileLogging(levels: LogLevel[] = ["debug", "comment", "log", "info", "warn", "error", "critical"]): void { for(const level of levels) { fileSettings.delete(level); } } export const colors = { black: "\x1b[0;30m", grey: "\x1b[0;37m", red: "\x1b[0;31m", green: "\x1b[0;32m", brown: "\x1b[0;33m", blue: "\x1b[0;34m", purple: "\x1b[0;35m", darkGrey: "\x1b[1;30m", lightRed: "\x1b[1;31m", lightGreen: "\x1b[1;32m", yellow: "\x1b[1;33m", lightBlue: "\x1b[1;34m", magenta: "\x1b[1;35m", cyan: "\x1b[1;36m", white: "\x1b[1;37m", initial: "\x1b[0m" }; } const fileSettings: Map<LogLevel, { filepath: string }> = new Map(); const fileStates: Map<string, FileLogState> = new Map(); // filepath -> state const logLevels: Set<LogLevel> = new Set(); const logLevelRank: Record<LogLevel, number> = { debug: 0, comment: 1, log: 2, info: 3, warn: 4, error: 5, critical: 6, }; const logLevelColors: Record<LogLevel, string> = { debug: Logger.colors.purple, comment: Logger.colors.grey, log: Logger.colors.green, info: Logger.colors.blue, warn: Logger.colors.brown, error: Logger.colors.red, critical: Logger.colors.lightRed, }; const logLevelChannel: Record<LogLevel, (message?: any, ...optionalParams: any[]) => void> = { debug: console.debug, comment: console.debug, log: console.log, info: console.info, warn: console.warn, error: console.error, critical: console.error, }; Logger.setLogLevel("debug");