UNPKG

easy-cli-framework

Version:

A framework for building CLI applications that are robust and easy to maintain. Supports theming, configuration files, interactive prompts, and more.

387 lines (358 loc) 11.1 kB
import { DisplayOptions, EasyCLITheme } from '..'; /** * Supported log types * @typedef {'log' | 'info' | 'warn' | 'error' | 'success'} SupportedLogType * * @type {string} */ export type SupportedLogType = 'log' | 'info' | 'warn' | 'error' | 'success'; /** * Removes circular references from an object recursively * * @param {Object} obj - The object to remove circular references from * @returns {Object} - The object with circular references removed */ const removeCircularReferences = (obj: Object) => { const seen = new Map(); const recurse = (obj: any) => { seen.set(obj, true); if (Array.isArray(obj)) { return obj.map((v: any): any => { if (typeof v !== 'object') { return v; } if (seen.has(v)) { return '[Circular Reference]'; } else { return recurse(v); } }) as any[]; } return Object.entries(obj).reduce((acc, [k, v]) => { if (typeof v !== 'object') { acc[k] = v; return acc; } if (seen.has(v)) { acc[k] = '[Circular Reference]'; return acc; } else { acc[k] = recurse(v); return acc; } }, {} as any); }; return recurse(obj); }; /** * Outputs a log to the console * * @param {SupportedLogType} type - The type of log to output * @param {string} log - The log to output */ const outputLog = (type: SupportedLogType, log: string) => { if (type === 'success') { console.log(log); return; } console[type](log); }; /** * A response from a logger * This is used to allow for forcing a log to be output using the `force` method * * @class EasyCLILoggerResponses * @property {string} log - The log that was output * @property {SupportedLogType} type - The type of log that was output * @property {boolean} logged - Whether the log was output * @property {function} force - Forces the log to be output * * @example * ```typescript * const logger = new EasyCLILogger({ theme: new EasyCLITheme(), verbosity: 0 }); * logger.log('Hello, world!'); // Won't be logged because verbosity is 0 * logger.log('Hello, world!').force(); // Will be logged * ``` */ export class EasyCLILoggerResponse { constructor( private log: string, private type: SupportedLogType, private logged: boolean = false ) {} /** * Forces the log to be output. This is useful if you want to output a log even if the verbosity is too low. * * @example * ```typescript * const logger = new EasyCLILogger({ theme: new EasyCLITheme(), verbosity: 0 }); * logger.log('Hello, world!'); // Won't be logged because verbosity is 0 * logger.log('Hello, world!').force(); // Will be logged * ``` */ public force = () => { if (this.logged) return; // Already logged outputLog(this.type, this.log); }; } /** * The properties for the logger * @param {EasyCLITheme} options.theme - The theme to use for the logger * @param {number} [options.verbosity=0] - The verbosity level to use (Default: 0) * @param {Record<SupportedLogType, number>} [options.verbosityThresholds] - The verbosity thresholds to use * @param {boolean} [options.timestamp=true] - Whether to include a timestamp in the execution logs (Default: true) * * @example * ```typescript * { * theme: new EasyCLITheme(), * verbosity?: 0, * verbosityThresholds?: { * error: 0, * success: 0, * warn: 1, * log: 2, * info: 3, * }, * timestamp?: true, * } * ``` */ export type EasyCLILoggerProps = { theme: EasyCLITheme; verbosity?: number; verbosityThresholds?: Record<SupportedLogType, number>; timestamp?: boolean; }; /** * A logger for use with CLI applicatiions. This logger allows for logging with different verbosity levels and themes * * @class EasyCLILogger * * @example * ```typescript * const logger = new EasyCLILogger({ theme: new EasyCLITheme(), verbosity: 0, timestamp: false }); * logger.log('Hello, world!'); // Won't be logged because verbosity is 0 * logger.log('Hello, world!').force(); // Will be logged due to force * logger.warn('This is a warning!'); // Won't be logged because verbosity is 0 * logger.error('This is an error!') // Will be logged * * const logs = logger.getExecutionLogs(); * * ``` */ export class EasyCLILogger { private logs: string[] = []; private verbosity: number; private verbosityThresholds: Record<SupportedLogType, number>; private theme: EasyCLITheme; private timestamp: boolean; /** * Instantiates a new logger with the given theme and verbosity level. * * @param {EasyCLILoggerProps} options - The configuration props for the logger * * @example * ```typescript * { * theme: new EasyCLITheme(), * verbosity?: 0, * verbosityThresholds?: { * error: 0, // Always log errors * success: 0, // Always log success * warn: 1, // Log warnings when verbosity is 1 or higher * log: 2, // Log logs when verbosity is 2 or higher * info: 3, // Log info when verbosity is 3 or higher * }, * } * ``` */ constructor({ theme, verbosity = 0, verbosityThresholds = { error: 0, success: 0, warn: 1, log: 2, info: 3, }, timestamp = true, }: EasyCLILoggerProps) { this.theme = theme; this.verbosity = verbosity; this.verbosityThresholds = verbosityThresholds; this.timestamp = timestamp; } /** * Converts the arguments to strings, to be able to handle similar to how console.log works. * * @param args Converts the arguments to a string, removing circular references. * @returns The arguments as a string. */ private convertArgArrayToString(args: unknown[]): string { return args .map(arg => { if (typeof arg === 'object') { // Removes circular references so it's safe to log const cleanObj = removeCircularReferences(arg as Object); return JSON.stringify(cleanObj, null, 2); // Pretty print } return `${arg}`; }) .join(', '); } /** * Saves a log to the internal log array for use with the `getExecutionLogs` method. * * @param {SupportedLogType} type - The type of log to save * @param {string} message - The message to save */ private saveLog(type: SupportedLogType, message: string) { const timestamp = this.timestamp ? ` ${new Date().toISOString()}:` : ''; this.logs.push(`[${type.toLocaleUpperCase()}]${timestamp} ${message}`); } /** * An internal method to process a log, saving it and outputting it to the console if the verbosity is high enough. * * @param {SupportedLogType} type - The type of log to process * @param {unknown[]} args - The arguments to process * @returns {EasyCLILoggerResponse} - The response from the logger */ private processLog(type: SupportedLogType, ...args: unknown[]) { const clean = this.convertArgArrayToString(args); this.saveLog(type, clean); const formatted = this.theme.formattedString(clean, type); const shouldLog = this.verbosity >= this.verbosityThresholds[type]; if (shouldLog) { outputLog(type, formatted); } return new EasyCLILoggerResponse(formatted, type, shouldLog); } /** * Writes a log to the console depending on the verbosity level, using the log display options. * * @param {unknown[]} args - The arguments to log * @returns {EasyCLILoggerResponse} - The response from the logger * * @example * ```typescript * logger.log('Hello, world!'); * ``` */ log(...args: unknown[]): EasyCLILoggerResponse { return this.processLog('log', ...args); } /** *Writes a warning to the console depending on the verbosity level, using the log display options. * * @param {unknown[]} args - The arguments to log * * @returns {EasyCLILoggerResponse} - The response from the logger * @example * ```typescript * logger.warn('Hello, world!'); * ``` */ warn(...args: unknown[]): EasyCLILoggerResponse { return this.processLog('warn', ...args); } /** * Writes an info message to the console depending on the verbosity level, using the log display options. * * @param {unknown[]} args - The arguments to log * * @returns {EasyCLILoggerResponse} - The response from the logger * * @example * ```typescript * logger.info('Hello, world!'); * ``` */ info(...args: unknown[]): EasyCLILoggerResponse { return this.processLog('info', ...args); } /** * Writes an error to the console depending on the verbosity level, using the log display options. * * @param {unknown[]} args - The arguments to log * * @returns {EasyCLILoggerResponse} - The response from the logger * * @example * ```typescript * logger.error('Hello, world!'); * ``` */ error(...args: unknown[]): EasyCLILoggerResponse { return this.processLog('error', ...args); } /** * Writes a success to the console depending on the verbosity level, using the log display options. * * @param {unknown[]} args - The arguments to log * * @returns {EasyCLILoggerResponse} - The response from the logger * * @example * ```typescript * logger.success('Hello, world!'); * ``` */ success(...args: unknown[]): EasyCLILoggerResponse { return this.processLog('success', ...args); } /** * Takes a list of arguments and prints them to the console in the format provided. * * @param {(string | { text: string; format: DisplayOptions })[]} args - The arguments to print * * @example * ```typescript * // Prints Hello World! in the default format and then in the info format * logger.printFormattedString('Hello, world!', { text: 'Hello, world!', format: 'info' }); * ``` */ printFormattedString( ...args: ( | string | { text: string; format: DisplayOptions; } )[] ) { console.log( args .map(arg => typeof arg === 'string' ? this.theme.formattedString(arg, 'default') : this.theme.formattedString(arg.text, arg.format) ) .join('') ); } /** * Gets the execution logs, including logs that were not output due to verbosity. * This is useful for debugging and logging to a file after execution. * * @returns {string[]} - The execution logs * * @example * ```typescript * const logger = new EasyCLILogger({ theme: new EasyCLITheme(), verbosity: 0 }); * logger.log('Hello, world!'); // Won't be logged because verbosity is 0 * logger.log('Hello, world!').force(); // Will be logged * logger.warn('This is a warning!'); // Won't be logged because verbosity is 0 * logger.error('This is an error!') // Will be logged * * const logs = logger.getExecutionLogs(); * * console.log(logs); * // Will display, all logs, including those that weren't output. * ``` */ getExecutionLogs(): string[] { return this.logs; } }