/** * @file Console logger for browser environments. * * @remarks * The repetition of log levels, typedefs, and method signatures is intentional * to give IDEs and type checkers a mapping of log methods to the TypeScript * provided JSDoc comments. * * Additionally, no wrapper functions are used to avoid the browser's console * reported call site being the wrapper instead of the actual caller. * * @import { * ChildLoggerOptions, * LoggerOptions, * Logger as PinoLogger, * BaseLogger as PinoBaseLogger, * Level as PinoLevel, * LogFn as PinoLogFn * } from "pino" */ /* eslint-disable no-console */ /** * @typedef {PinoLogger} Logger */ /** * @typedef {PinoLevel} Level */ /** * @typedef {PinoLogFn} LogFn */ /** * @typedef {Pick>} BaseLogger */ //#region Constants /** * Labels log levels in the browser console. * @satisfies {Record} */ export const LogLevelLabel = /** @type {const} */ ({ info: "[INFO]", warn: "[WARN]", error: "[ERROR]", debug: "[DEBUG]", trace: "[TRACE]", fatal: "[FATAL]", }); /** * Predefined log levels. */ export const LogLevels = /** @type {Level[]} */ (Object.keys(LogLevelLabel)); /** * @callback LoggerFactory * @param {string | null} [prefix] * @param {...string} args * @returns {Logger} */ /** * Colors for log levels in the browser console. * * @remarks * * The colors are derived from Carbon Design System's palette to ensure * sufficient contrast and accessibility across light and dark themes. */ const LogLevelColors = /** @type {const} */ ({ info: `light-dark(#0043CE, #4589FF)`, warn: `light-dark(#F1C21B, #F1C21B)`, error: `light-dark(#DA1E28, #FA4D56)`, debug: `light-dark(#8A3FFC, #A56EFF)`, trace: `light-dark(#8A3FFC, #A56EFF)`, fatal: `light-dark(#DA1E28, #FA4D56)`, }); //#region Functions /** * Creates a logger with the given prefix. * * @type {LoggerFactory} */ function createConsoleLogger(prefix, ...args) { const msgPrefix = prefix ? `(${prefix}):` : ":"; /** * @type {Partial} */ const logger = { msgPrefix, }; for (const level of LogLevels) { const label = LogLevelLabel[level]; // @ts-expect-error Alias the log method to the appropriate console method, // defaulting to console.log if the level is not supported. const method = level in console ? console[level] : console.log; logger[level] = method.bind(console, `${label} ${msgPrefix}`, ...args); } return /** @type {Logger} */ (logger); } /** * @typedef {Logger} FixtureLogger */ /** * @this {Logger} * @param {string} fixtureName * @param {string} [testName] * @param {ChildLoggerOptions} [options] * @returns {FixtureLogger} */ export function fixture(fixtureName, testName, options) { return this.child( { name: fixtureName }, { msgPrefix: `[${testName}] `, ...options, }, ); } /** * @this {Logger} * @param {Record} bindings * @param {ChildLoggerOptions} [_options] * @returns {Logger} */ export function child(bindings, _options) { const prefix = typeof bindings.name === "string" ? bindings.name : null; return Object.assign(createConsoleLogger(prefix), { ...bindings }); } /** * @this {{ child: typeof child }} * @param {string} label * @returns {IConsoleLogger} */ export function prefix(label) { // @ts-expect-error Create a child logger with the given prefix. return this.child({ name: label }); } /** * @typedef {object} CustomLoggerMethods * @property {typeof fixture} fixture * @property {typeof prefix} prefix * @property {typeof child} child */ /** * @typedef {Record} BaseConsoleLogger */ /** * @typedef {BaseConsoleLogger & CustomLoggerMethods} IConsoleLogger */ /** * @type {CustomLoggerMethods} */ export const customLoggerMethods = { fixture, prefix, child, }; /** * Creates a lightweight logger that mimics the Pino API but falls back to * console methods when Pino is not available. * @param {LoggerOptions} options * @return {IConsoleLogger} */ export function pinoLight(options) { const baseLogger = createConsoleLogger(options.name); /** * @type {IConsoleLogger} */ const logger = { ...baseLogger, fixture, prefix, child, }; return logger; } //#endregion //#region Functions /** * Creates a logger with the given prefix. * * @param {string} [prefix] * @param {...string} args * @returns {Logger} * */ export function createLogger(prefix, ...args) { const msgPrefix = prefix ? `(${prefix}):` : ":"; /** * @type {Partial} */ const logger = { msgPrefix, }; for (const level of LogLevels) { const label = LogLevelLabel[level]; const color = LogLevelColors[level]; // @ts-expect-error Alias the log method to the appropriate console method, // defaulting to console.log if the level is not supported. const method = level in console ? console[level] : console.log; logger[level] = method.bind( console, `%c${label}%c ${msgPrefix}%c`, `font-weight: 700; color: ${color};`, `font-weight: 600; color: CanvasText;`, "", ...args, ); } return /** @type {Logger} */ (logger); } //#endregion //#region Console Logger /** * A singleton logger instance for the browser. * * ```js * import { ConsoleLogger } from "#logger/browser"; * * ConsoleLogger.info("Hello, world!"); * ``` * * @implements {IConsoleLogger} * @runtime browser */ // @ts-expect-error Logging properties are dynamically assigned. export class ConsoleLogger { /** @type {typeof console.info} */ static info; /** @type {typeof console.warn} */ static warn; /** @type {typeof console.error} */ static error; /** @type {typeof console.debug} */ static debug; /** @type {typeof console.trace} */ static trace; /** * Creates a logger with the given prefix. * @param {string} logPrefix */ static prefix(logPrefix) { return createLogger(logPrefix); } } Object.assign(ConsoleLogger, createLogger()); //#endregion