import { ConsoleAdapter } from "./consolewriter.js"; import inspect from "./inspect.js"; import { Adapter, DefaultFormatConfig, Format, FormatConfig, Formatted, LoggingTypes, Message, } from "./types.js"; const browser = typeof window !== "undefined"; export interface ILoggingTimer { end: () => number; } export interface ILoggingInterface { debug(...message: any[]): void; log(...message: any[]): void; warn(...message: any[]): void; error(...message: any[]): void; time(name?: string): ILoggingTimer; timeEnd(id: string): number; getChild(name: string): ILoggingInterface; } export interface ILoggingOptions { /** * Name will be prefixed on Console output and added to logfiles, if not specified here */ name: string; /** * Prints output to console */ console: boolean; /** * Enables printing of calling file */ resolve_filename: boolean; } export interface INativeFunctions { startTimer(): any; endTimer(start: any): number; } export const DefaultNativeFunctions = { startTimer: () => { if (browser && window.performance && window.performance.now) { return window.performance.now(); } else { return Date.now(); } }, endTimer: (start: any) => { if (browser && window.performance && window.performance.now) { return window.performance.now() - start; } else { return Date.now() - start; } }, } as INativeFunctions; const consoleAdapter = new ConsoleAdapter(); declare var process: { cwd: () => string }; const PROJECT_ROOT = typeof process !== "undefined" ? process.cwd() : undefined; const InitialisedAdapters = Symbol("@hibas123/logging:initialisedAdapters"); export abstract class LoggingInterface implements ILoggingInterface { #names: string[]; #timerMap = new Map(); get names() { return [...this.#names]; } protected abstract message( type: LoggingTypes, names: string[], message: any[], caller?: { file: string; line: number; column?: number } ): void; constructor(names: string[]) { this.#names = names; } debug(...message: any[]) { this.message(LoggingTypes.Debug, this.#names, message); } log(...message: any[]) { this.message(LoggingTypes.Log, this.#names, message); } warning(...message: any[]) { this.message(LoggingTypes.Warn, this.#names, message); } warn(...message: any[]) { this.message(LoggingTypes.Warn, this.#names, message); } error(error: Error | string, ...message: any[]) { if (!error) error = "Empty ERROR was passed, so no informations available"; if (typeof error === "string") { let e = new Error("This is a fake error, to get a stack trace"); this.message(LoggingTypes.Error, this.#names, [ error, ...message, "\n", e.stack, "\n", ]); } else { this.message( LoggingTypes.Error, this.#names, [error.message, "\n", error.stack, "\n", ...message], getCallerFromExisting(error) ); } } time(id?: string) { if (!id) id = Math.floor(Math.random() * 899999 + 100000).toString(); this.#timerMap.set(id, { name: id, start: LoggingBase.nativeFunctions.startTimer(), }); return { id, end: () => this.timeEnd(id), }; } timeEnd(id: string) { let timer = this.#timerMap.get(id); if (timer) { let diff = LoggingBase.nativeFunctions.endTimer(timer.start); this.message(LoggingTypes.Debug, this.#names, [ Format.green(`[${timer.name}]`), `->`, Format.blue(diff.toFixed(4)), "ms", ]); return diff; } return -1; } abstract getChild(name: string): ILoggingInterface; } export class LoggingBase extends LoggingInterface { private static [InitialisedAdapters] = new Map(); public static nativeFunctions: INativeFunctions = DefaultNativeFunctions; static DecoupledLogging = class extends LoggingInterface { #lg: LoggingBase; constructor(names: string[], lg: LoggingBase) { super(names); this.#lg = lg; } message(...params: [any, any, any, any]) { this.#lg.message(...params); } getChild(name: string) { return new LoggingBase.DecoupledLogging([this.names, name], this.#lg); } }; #closed = false; #logLevel = LoggingTypes.Debug; #resolve_filename: boolean; #format_map: FormatConfig = new DefaultFormatConfig(); #adapters = new Set(); set logLevel(level: LoggingTypes) { this.#logLevel = level; } get logLevel() { return this.#logLevel; } constructor(options?: Partial) { super(options?.name ? [options.name] : []); options = { console: true, resolve_filename: true, ...options, }; if (options.console) { this.addAdapter(consoleAdapter); } for (const key in this) { if (typeof this[key] === "function") { this[key] = ((this[key] as never) as Function).bind(this); } } } async addAdapter(adapter: Adapter) { const init = adapter.init(); const add = () => { this.#adapters.add(adapter); if (LoggingBase[InitialisedAdapters].has(adapter)) { LoggingBase[InitialisedAdapters].set( adapter, LoggingBase[InitialisedAdapters].get(adapter) + 1 ); } else { LoggingBase[InitialisedAdapters].set(adapter, 1); } }; if (!init) { add(); } else { Promise.resolve(init).then(add); } } getChild(name: string): ILoggingInterface { return new LoggingBase.DecoupledLogging([...this.names, name], this); } protected message( type: LoggingTypes, names: string[], message: any[], caller?: { file: string; line: number; column?: number } ) { if (this.#logLevel > type) return; if (this.#closed) { //TODO: Maybe error? return; } const date = new Date(); const date_str = date.toISOString().replace(/T/, " ").replace(/\..+/, ""); let file: string | undefined = undefined; if (this.#resolve_filename) { let file_raw = caller; if (!file_raw) { try { file_raw = getCallerFile(); } catch (err) { file_raw = { file: "", line: 0, column: 0, }; } } if ( PROJECT_ROOT && file_raw.file && file_raw.file.startsWith(PROJECT_ROOT) ) { let newF = file_raw.file.substring(PROJECT_ROOT.length); if (newF.startsWith("/") || newF.startsWith("\\")) newF = newF.substring(1); file_raw.file = newF; } file = `${file_raw.file || ""}:${file_raw.line}:${ file_raw.column || 0 }`; } let type_str = LoggingTypes[type].toUpperCase().padEnd(5, " "); let type_format: Formatted; switch (type) { case LoggingTypes.Log: type_format = this.#format_map.log; break; case LoggingTypes.Error: type_format = this.#format_map.error; break; case LoggingTypes.Debug: type_format = this.#format_map.debug; break; case LoggingTypes.Warn: type_format = this.#format_map.warning; break; } const nameFormatted: Formatted[] = []; if (names.length > 0) { nameFormatted.push(new Formatted("[")); for (let i = 0; i < names.length; i++) { nameFormatted.push(new Formatted(names[i], this.#format_map.names)); if (i < names.length - 1) { nameFormatted.push(this.#format_map.names_delimiter); } } nameFormatted.push(new Formatted("]")); } let linePrefix = [ new Formatted("["), new Formatted(date_str, this.#format_map.date), new Formatted("]["), new Formatted(type_str, type_format), ...(file ? [new Formatted("]["), new Formatted(file, this.#format_map.file)] : []), new Formatted("]"), ...nameFormatted, new Formatted(": "), ]; // let linePrefix = [ // ...namePrefix, // new Formatted(date_str, this.#format_map.date), // new Formatted(" "), // new Formatted(type_str, type_format), // ...(file // ? [new Formatted(" "), new Formatted(file, this.#format_map.file)] // : []), // new Formatted("| "), // ]; let formattedMessage: Formatted[] = [...linePrefix]; message.forEach((msg, idx) => { let format = new Formatted(); if (msg instanceof Formatted) { format = msg; msg = msg.content; } if (typeof msg !== "string") { msg = inspect(msg, { colors: false, //TODO: Maybe change when changing the removeColors to return formatted text? showHidden: true, depth: 3, }) as string; } removeColors(msg) .split("\n") .forEach((text, index, { length }) => { if (index != length - 1) { formattedMessage.push( new Formatted(text + "\n", format), ...linePrefix ); } else { formattedMessage.push(new Formatted(text, format)); } }); formattedMessage.push(new Formatted(" ")); }); let msg: Message = { date, file, names, text: formattedMessage, type, }; this.#adapters.forEach((adapter) => adapter.onMessage(msg)); } async close() { if (this.#closed) return; this.#closed = true; this.#adapters.forEach((adapter) => { const cnt = LoggingBase[InitialisedAdapters].get(adapter); if (!cnt) { //TODO: should not happen! } else { if (cnt <= 1) { adapter.close(); LoggingBase[InitialisedAdapters].delete(adapter); } else { LoggingBase[InitialisedAdapters].set(adapter, cnt - 1); } } }); } } function getStack() { // Save original Error.prepareStackTrace let origPrepareStackTrace = (Error).prepareStackTrace; try { // Override with function that just returns `stack` (Error).prepareStackTrace = function (_, stack) { return stack; }; // Create a new `Error`, which automatically gets `stack` let err = new Error(); // Evaluate `err.stack`, which calls our new `Error.prepareStackTrace` let stack: any[] = err.stack; return stack; } finally { // Restore original `Error.prepareStackTrace` (Error).prepareStackTrace = origPrepareStackTrace; } } function getCallerFile() { try { let stack = getStack(); let current_file = stack.shift().getFileName(); while (stack.length) { let caller_file = stack.shift(); if (current_file !== caller_file.getFileName()) { return { file: caller_file.getFileName(), line: caller_file.getLineNumber(), column: caller_file.getColumnNumber(), }; } } } catch (err) {} return { file: undefined, line: 0 }; } function getCallerFromExisting( err: Error ): { file: string; line: number; column?: number } { if (!err || !err.stack) return { file: "NOFILE", line: 0 }; let lines = err.stack.split("\n"); lines.shift(); // removing first line while (lines.length > 0) { let line = lines.shift(); let matches = line.match( /[<]?([a-zA-Z]:)?([\/\\]?[a-zA-Z_-])+[.>][a-zA-Z_-]*([:][0-9]+)+/g ); if (matches && matches.length > 0) { let match = matches[0].trim(); let locationString = match.match(/([:][0-9]+)+$/gm)[0]; let line: number; let column: number; if (locationString) { match = match.slice(0, match.length - locationString.length); locationString = locationString.substring(1); [line, column] = locationString.split(":").map(Number); } let file = match; return { file, line, column, }; } } } export function removeColors(text: string) { text = text.replace( /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "" ); return text; }