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.
309 lines (306 loc) • 10.2 kB
JavaScript
'use strict';
/**
* 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) => {
const seen = new Map();
const recurse = (obj) => {
seen.set(obj, true);
if (Array.isArray(obj)) {
return obj.map((v) => {
if (typeof v !== 'object') {
return v;
}
if (seen.has(v)) {
return '[Circular Reference]';
}
else {
return recurse(v);
}
});
}
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;
}
}, {});
};
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, log) => {
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
* ```
*/
class EasyCLILoggerResponse {
constructor(log, type, logged = false) {
this.log = log;
this.type = type;
this.logged = logged;
/**
* 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
* ```
*/
this.force = () => {
if (this.logged)
return; // Already logged
outputLog(this.type, this.log);
};
}
}
/**
* 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();
*
* ```
*/
class EasyCLILogger {
/**
* 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, }) {
this.logs = [];
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.
*/
convertArgArrayToString(args) {
return args
.map(arg => {
if (typeof arg === 'object') {
// Removes circular references so it's safe to log
const cleanObj = removeCircularReferences(arg);
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
*/
saveLog(type, message) {
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
*/
processLog(type, ...args) {
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) {
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) {
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) {
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) {
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) {
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) {
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() {
return this.logs;
}
}
exports.EasyCLILogger = EasyCLILogger;
exports.EasyCLILoggerResponse = EasyCLILoggerResponse;