import chalk from "chalk"; import { setImmediate } from "node:timers"; type ChalkFunction = (...msgs: unknown[]) => string; type PrimitiveItems = unknown[]; interface UnknownConversion { condition: (arg: unknown) => boolean; converter: (arg: T) => string; } /** * A source for pretty and cool and fun logging */ class Logging { /** Can I be seen by the console and listeners? */ visible: boolean /** What is my name? */ source: string /** Should I use bright colors over dull ones? */ bright: boolean = true; /** * Create a logging source * ```ts * const log = new Logging("Main"); * * log.i("Hello World!"); * ``` * @param source Module identifier. Used in every line to identify the module that sent the message. * @param silent Set to false to log a message when the logger instantiates. Useful for debugging. * @returns A source for logging messages to the console. Functions for info, warnings, errors, debug statements, and network events are provided and have shorthands. */ constructor(source: string, silent?: boolean, bright?: boolean) { this.visible = true; this.source = source; if (typeof bright == 'boolean') this.bright = bright; if (typeof silent == 'boolean' && !silent) this.info(`Instantiated logging for ${this.source}`); } #log(type: MessageType, chalkColor: ChalkFunction, ...msgs: PrimitiveItems) { if (!this.visible) return; const func = () => { let typestr: string; switch (type) { case MessageType.Info: typestr = '[INFO]'; break; case MessageType.Warn: typestr = '[WARN]'; break; case MessageType.Error: typestr = '[ERROR]'; break; case MessageType.Debug: typestr = '[DEBUG]'; break; case MessageType.Network: typestr = '[NETWORK]'; break; default: typestr = '[UNKNOWN]'; break; } const time = new Date(); let timeFormatted; switch (LoggingConfiguration.timeFormat) { case TimeFormat.Unix: timeFormatted = time.getTime(); break; case TimeFormat.Local: timeFormatted = performance.now(); break; default: timeFormatted = time.toISOString(); } const conversions = [ { condition: arg => arg instanceof Error, converter: arg => arg.stack || arg.message } as UnknownConversion, { condition: arg => arg == null, converter: arg => JSON.stringify(arg) } as UnknownConversion, { condition: arg => typeof arg == 'object', converter: arg => JSON.stringify(arg) } as UnknownConversion, ]; function convertToString(arg: unknown) { for (const conversion of conversions) if (conversion.condition(arg)) return (conversion.converter as (arg: unknown) => string)(arg); // gpt-4o idea cuz my brain has the `stupid` type return String(arg); // fallback } const str = msgs.map(val => convertToString(val)).join(' '); const msg = chalk.gray(`${timeFormatted} `) + chalkColor(chalk.inverse(`${this.source} ${typestr}`) + ` ${str}`); console.log(msg); try { LoggingListeners.emit('basic', msg); LoggingListeners.emit('type', str, type, this.source, time); } catch (err) { console.error(`(FALLBACK ERROR) Callback failed! Stack: ${(err as Error).stack}`); } } if (LoggingConfiguration.logTiming == LogTiming.Sync) func(); else setImmediate(func); } /** * * @param type The kind of message to log * @param msgs Your message(s) */ log(type: MessageType, ...msgs: PrimitiveItems) { if (type == MessageType.Info) this.i(...msgs); if (type == MessageType.Warn) this.w(...msgs); if (type == MessageType.Error) this.e(...msgs); if (type == MessageType.Debug) this.d(...msgs); if (type == MessageType.Network) this.n(...msgs); } /** Logging Function */ i(...msgs: PrimitiveItems) {this.info(...msgs);} /** Logging Function */ info(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Info) return; this.#log(MessageType.Info, chalk.whiteBright, ...msgs); } /** Logging Function */ w(...msgs: PrimitiveItems) {this.warn(...msgs);} /** Logging Function */ warn(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Warn) return; this.#log(MessageType.Warn, chalk.yellowBright, ...msgs); } /** Logging Function */ e(...msgs: PrimitiveItems) {this.error(...msgs);} /** Logging Function */ error(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Error) return; this.#log(MessageType.Error, chalk.redBright, ...msgs); } /** Logging Function */ d(...msgs: PrimitiveItems) {this.debug(...msgs);} /** Logging Function */ debug(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Debug) return; this.#log(MessageType.Debug, chalk.greenBright, ...msgs); } /** Logging Function */ n(...msgs: PrimitiveItems) {this.network(...msgs);} /** Logging Function */ network(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Network) return; this.#log(MessageType.Network, chalk.cyanBright, ...msgs); } } /** Useful for conditional logging with the `log` function */ enum MessageType { Info, Warn, Error, Debug, Network } /** Enable/disable logging of certain message types across all logging sources */ const MessageTypeVisibility = { Info: true, Warn: true, Error: true, Debug: true, Network: true } // 'On Message' event listeners type BasicListener = (msg: string) => unknown; type TypeListener = (msg: string, type: MessageType, source: string, time: Date) => unknown; type Listener = BasicListener | TypeListener; type ListenerType = 'basic' | 'type'; const basicListeners = new Set(); const typeListeners = new Set(); class ListenersBase { onmsg(type: 'basic', cb: BasicListener): void onmsg(type: 'type', cb: TypeListener): void onmsg(type: ListenerType, cb: Listener) { if (type == 'basic') basicListeners.add(cb); else if (type == 'type') typeListeners.add(cb); } offmsg(type: 'basic', cb: BasicListener): void offmsg(type: 'type', cb: TypeListener): void offmsg(type: ListenerType, cb: Listener) { if (type == 'basic') basicListeners.delete(cb); else if (type == 'type') typeListeners.delete(cb); } emit(type: 'basic', msg: string, msgtype?: MessageType, source?: string, time?: Date): void emit(type: 'type', msg: string, msgtype: MessageType, source: string, time: Date): void emit(type: ListenerType, msg: string, msgtype: MessageType, source: string, time: Date) { if (type == 'basic') for (const cb of basicListeners) (cb as BasicListener)(msg); if (type == 'type') for (const cb of typeListeners) (cb as TypeListener)(msg, msgtype, source, time); } } const LoggingListeners: ListenersBase = new ListenersBase(); enum LogTiming { Sync, Deferred } enum TimeFormat { Utc, Unix, Local } class LoggingConfigurationBase { #timing: LogTiming = LogTiming.Sync; #timeType: TimeFormat = TimeFormat.Utc; get timeFormat(): TimeFormat { return this.#timeType } set timeFormat(data: TimeFormat) { this.#timeType = data } get logTiming(): LogTiming { return this.#timing; } set logTiming(data: LogTiming) { this.#timing = data } } const LoggingConfiguration: LoggingConfigurationBase = new LoggingConfigurationBase(); export { MessageType, TimeFormat, LogTiming, MessageTypeVisibility, LoggingListeners, LoggingConfiguration }; export default Logging;