import chalk from "chalk"; import { setImmediate } from "node:timers"; import process from "node:process"; type Message = unknown; type ChalkFunction = (...msgs: Message[]) => string; type PrimitiveItems = Message[]; interface UnknownConversion { condition: (arg: Message) => boolean; converter: (arg: T) => string; } interface WebLike { url?: string, headers?: Headers, method?: string } /** The first log message across all sources will not contain carriage return + newline control codes */ let first = true; /** * 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; /** Control when logs are handled for this source - similar (defaults to) `LoggingConfiguration.logTiming` */ logTiming: LogTiming = LoggingConfiguration.logTiming; /** Control how time is displayed for this source - similar (defaults to) `LoggingConfiguration.timeFormat` */ timeFormat: TimeFormat = LoggingConfiguration.timeFormat; /** * 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. May be useful when debugging. * @returns A source for logging messages to the console (stdout). 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}`); } #conversions(...msgs: PrimitiveItems) { 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 => arg == undefined, converter: _arg => 'undefined' } as UnknownConversion, { condition: arg => arg instanceof Response, converter: arg => { try { const url = arg.url.toString().length == 0 ? '(unknown origin) ' : new URL(arg.url); const statusText = arg.statusText.length > 0 ? `${arg.statusText} ` : ''; const shouldNewline = Array.from(arg.headers.keys()).length > 0; const entries = Array.from(arg.headers.entries()); return ( `${arg.status} ${statusText}${url}${shouldNewline ? '\n ' : ''}` + entries.map(val => `${val[0]}: ${val[1]}`).join(`\n `) ); } catch { return String(arg); } } } as UnknownConversion, { condition: arg => arg instanceof Request, converter: arg => { try { const url = new URL(arg.url); const shouldNewline = Array.from(arg.headers.keys()).length > 0; const entries = Array.from(arg.headers.entries()); return `${arg.method} ${url}${shouldNewline ? '\n ' : ''}${entries.map(val => `${val[0]}: ${val[1]}`).join(`\n `)}`; } catch { return String(arg); } } } as UnknownConversion, { condition: arg => typeof arg == 'object', converter: arg => JSON.stringify(arg) } as UnknownConversion, ]; function convertToString(arg: Message) { 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 } return msgs.map(val => convertToString(val)).join(' '); } #typeStr(type: MessageType) { switch (type) { case MessageType.Info: return '[INFO]'; case MessageType.Warn: return '[WARN]'; case MessageType.Error: return '[ERROR]'; case MessageType.Debug: return '[DEBUG]'; case MessageType.Network: return '[NETWORK]'; default: return '[UNKNOWN]'; } } #timeStr(time: Date) { switch (this.timeFormat) { case TimeFormat.Unix: return time.getTime(); case TimeFormat.Local: return performance.now(); case TimeFormat.RoundedLocal: return Math.round(performance.now()); default: return time.toISOString(); } } /** * Don't use this unless you're benchmarking. * * Constructs a message. Does not do anything with it, or even return it. * * Logging messages during [Deno] benchmarks causes problems. */ bench(type: MessageType, chalkColor: ChalkFunction, ...msgs: PrimitiveItems) { `${chalk.gray(`${this.#timeStr(new Date())} `)} ${chalkColor(chalk.inverse(`${this.source} ${this.#typeStr(type)}`) + ` ${this.#conversions(...msgs)}`)}`; } #log(type: MessageType, chalkColor: ChalkFunction, ...msgs: PrimitiveItems) { if (!this.visible) return; const func = () => { const time = new Date(); const str = this.#conversions(...msgs); const msg = `${chalk.gray(this.#timeStr(time))} ${chalkColor(chalk.inverse(`${this.source} ${this.#typeStr(type)}`) + ` ${str}`)}` process.stdout.write(`${first ? '' : '\r\n'}${msg}`); // I don't know if promise-ifying this helps or not. // deno-lint-ignore require-await (async () => { first = false; })(); try { LoggingListeners.emit('basic', msg); LoggingListeners.emit('type', str, type, this.source, time); } catch (err) { process.stderr.write(`\r\n(FALLBACK ERROR) Callback failed! Stack: ${(err as Error).stack}`); } } if (this.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) { switch (type) { case MessageType.Info: this.info(...msgs); break; case MessageType.Warn: this.warn(...msgs); break; case MessageType.Error: this.error(...msgs); break; case MessageType.Debug: this.debug(...msgs); break; case MessageType.Network: this.network(...msgs); break; default: this.info(...msgs); break; } } /** 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 { /** Register listener callback */ 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); } /** Remove listener callback */ 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 a log line. Usually not needed outside this module. */ 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); } } /** * Add/remove callbacks: run when something is logged anywhere */ const LoggingListeners: ListenersBase = new ListenersBase(); /** * Specify when, in/around the event loop, logs are sent */ enum LogTiming { Sync, Deferred } /** * Specify the format that loggers use to display time */ enum TimeFormat { Utc, Unix, Local, RoundedLocal } 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 } } /** * Configure time display format and log timing here */ const LoggingConfiguration: LoggingConfigurationBase = new LoggingConfigurationBase(); export { MessageType, TimeFormat, LogTiming, MessageTypeVisibility, LoggingListeners, LoggingConfiguration }; export default Logging;