From f26cae5b268cfd7953c1a5f3f677d9987c5577fd Mon Sep 17 00:00:00 2001 From: neru Date: Sat, 10 Jan 2026 05:20:49 -0300 Subject: [PATCH] feat: add logger module --- src/utils/log.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/utils/log.ts diff --git a/src/utils/log.ts b/src/utils/log.ts new file mode 100644 index 0000000..dd861c4 --- /dev/null +++ b/src/utils/log.ts @@ -0,0 +1,115 @@ +import * as util from 'util'; +import * as path from 'path'; + +import 'colorts/lib/string'; + +interface CallSite { + getThis(): unknown; + getTypeName(): string | null; + getFunction(): Function | undefined; + getFunctionName(): string | null; + getMethodName(): string | null; + getFileName(): string | null; + getLineNumber(): number | null; + getColumnNumber(): number | null; + getEvalOrigin(): string | undefined; + isToplevel(): boolean; + isEval(): boolean; + isNative(): boolean; + isConstructor(): boolean; +} + +/* + helper fns +*/ +function getCallerFrame(shift: number = 0): NodeJS.CallSite | undefined { + const prepareStackTrace = Error.prepareStackTrace; + let loggedStack: NodeJS.CallSite[] | undefined; + + Error.prepareStackTrace = (_, stackTraces: NodeJS.CallSite[]) => { + loggedStack = stackTraces; + stackTraces.shift(); // discard curr frame + return stackTraces; + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + new Error().stack; + + Error.prepareStackTrace = prepareStackTrace; + + if (!loggedStack) return undefined; + + if (shift > 0) + for (let i = 0; i < shift; i++) + loggedStack.shift(); + + return loggedStack[1]; +} + +/* + logger +*/ +const LOG_TYPES = { + Info: "[Info]".green, + Verbose: "[Verbose]".blue, + Warning: "[Warning]".yellow, + Error: "[Error]".red, +} as const; + +export class Logger { + private readonly name: string; + + constructor(name: string) { + this.name = name; + } + + public info(fmt: string, ...args: unknown[]): void { + this.log(LOG_TYPES.Info, fmt, args); + } + + public verbose(fmt: string, ...args: unknown[]): void { + this.log(LOG_TYPES.Verbose, fmt, args); + } + + public warning(fmt: string, ...args: unknown[]): void { + this.log(LOG_TYPES.Warning, fmt, args); + } + + public error(fmt: string, ...args: unknown[]): void { + this.log(LOG_TYPES.Error, fmt, args); + } + + private log(type: string, message: string, args: unknown[]): void { + const caller = getCallerFrame(1); + if (!caller) { + console.error('Failed to determine caller information'); + return; + } + + const timestamp = this.getFormattedTime(); + const callerInfo = this.getCallerInfo(caller); + const formattedMessage = util.format(message, ...args); + + console.log( + `${timestamp} - ${callerInfo} [${this.name.magenta}] ${type} ${formattedMessage}` + ); + } + + private getFormattedTime(): string { + const now = new Date(); + return now.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + } + + private getCallerInfo(caller: NodeJS.CallSite): string { + const functionName = caller.getFunctionName()?.replace(/\[.*\]/, '') || ''; + const fileName = caller.getFileName() || 'unknown'; + const relativePath = path.relative(process.cwd(), fileName); + + return `${functionName.cyan} @ ${relativePath.dim}`; + } +}