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
text/typescript
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;
}
}