const colors = require('colors/safe');
const { providerCreator } = require('../shared/jimpleFns');
const { deepAssign } = require('../shared/deepAssign');
/**
* @module node/logger
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreator<O>} ProviderCreator
* @template O
*/
/**
* @typedef {Object} PackageInfo
* @property {string} [nameForCLI] A specific name to use on the logger; it overwrites
* `name`.
* @property {string} name The package name.
*/
/**
* @typedef {Object} AppLoggerServiceMap
* @property {string | PackageInfo} [packageInfo] The name of the service that containers
* the information of the `package.json`.
* `packageInfo` by default.
* @parent module:node/logger
*/
/**
* @typedef {Object} AppLoggerProviderOptions
* @property {string} serviceName The name that will be used to register an
* instance of {@link Logger} with the
* package name as prefix. Its default value
* is `appLogger`.
* @property {AppLoggerServiceMap} services A dictionary with the services that need
* to be injected.
* @property {boolean} [showTime] Whether or not to show the time on each
* message.
* @parent module:node/logger
*/
/**
* @typedef {Object} LoggerProviderOptions
* @property {string} serviceName The name that will be used to register an
* instance of {@link Logger}. Its default value is
* `logger`.
* @property {string} [messagesPrefix] A prefix to include in front of all the messages.
* @property {boolean} [showTime] Whether or not to show the time on each message.
* @parent module:node/logger
*/
/**
* This can be either a message to log, or an array where the first item is the message
* and the second one is the color it should be used to log it.
*
* @typedef {string | string[]} LoggerLine
* @example
*
* logger.log('hello world');
* // It will log 'hello world' with the default color.
* logger.log(['hello world', 'red']);
* // It will log 'hello world' in red.
*
* @parent module:node/logger
*/
/**
* @typedef {string | LoggerLine[]} LoggerMessage
* @parent module:node/logger
*/
/**
* A utility service to log messages on the console.
*
* @parent module:node/logger
* @tutorial logger
*/
class Logger {
/**
* @param {string} [messagesPrefix=''] A prefix to include in front of all the
* messages.
* @param {boolean} [showTime=false] Whether or not to show the time on each
* message.
*/
constructor(messagesPrefix = '', showTime = false) {
/**
* The prefix to include in front of all the messages.
*
* @type {string}
* @access protected
* @ignore
*/
this._messagesPrefix = messagesPrefix;
/**
* Whether or not to show the time on each message.
*
* @type {boolean}
* @access protected
* @ignore
*/
this._showTime = showTime;
/**
* An alias for the {@link Logger#warning} method.
*
* @type {Function}
* @see {@link Logger#warning} .
*/
this.warn = this.warning.bind(this);
}
/**
* Logs an error (red) message or messages on the console.
*
* @param {LoggerMessage | Error} message A single message of a list of them.
* See the `log()` documentation to see
* all the supported properties for the
* `message` parameter. Different from
* the other log methods, you can use
* an `Error` object and the method
* will take care of extracting the
* message and the stack information.
* @param {Object} [exception=null] If the exception has a `stack`
* property, the method will log each
* of the stack calls using `info()`.
*/
error(message, exception = null) {
if (message instanceof Error) {
this.error(message.message, message);
} else {
this.log(message, 'red');
if (exception) {
if (exception.stack) {
const stack = exception.stack.split('\n').map((line) => line.trim());
stack.splice(0, 1);
this.info(stack);
} else {
this.log(exception);
}
}
}
}
/**
* Logs an information (gray) message or messages on the console.
*
* @param {LoggerMessage} message A single message of a list of them.
* @see {@link Logger#log} .
*/
info(message) {
this.log(message, 'grey');
}
/**
* Logs a message with an specific color on the console.
*
* @param {LoggerMessage} message A text message to log or a list of them.
* @param {string} [color='raw'] Optional. The color of the message (the default
* is the terminal default). This can be
* overwritten line by line when the message is an
* array, take a look at the example.
* @example
*
* // Simple
* CLILogger.log('hello world');
* // Custom color
* CLILogger.log('It was the shadow who did it', 'red');
* // A list of messages all the same color
* CLILogger.log(["Ph'nglu", "mglw'nafh"], 'grey');
* // A list of messages with different colors per line
* CLILogger.log(
* [
* "Ph'nglu",
* "mglw'nafh",
* ['Cthulhu', 'green'],
* ["R'lyeh wgah'nagl fhtagn", 'red'],
* ],
* 'grey',
* );
*
*/
log(message, color = 'raw') {
const lines = [];
if (Array.isArray(message)) {
message.forEach((line) => {
if (Array.isArray(line)) {
lines.push(this._color(line[1])(this.prefix(line[0])));
} else {
lines.push(this._color(color)(this.prefix(line)));
}
});
} else {
lines.push(this._color(color)(this.prefix(message)));
}
// eslint-disable-next-line no-console
lines.forEach((line) => console.log(line));
}
/**
* Prefixes a message with the text sent to the constructor and, if enabled, the current
* time.
*
* @param {string} text The text that needs the prefix.
* @returns {string}
*/
prefix(text) {
// Define the list of things that will compose the formatted text.
const parts = [];
// If a prefix was set on the constructor...
if (this.messagesPrefix) {
// ...add it as first element.
parts.push(`[${this.messagesPrefix}]`);
}
// If the `showTime` setting is enabled...
if (this.showTime) {
// ...add the current time to the list.
const time = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
parts.push(`[${time}]`);
}
// Add the original text.
parts.push(text);
// Join the list into a single text message.
return parts.join(' ').trim();
}
/**
* Logs a success (green) message or messages on the console.
*
* @param {LoggerMessage} message A single message of a list of them.
* @see {@link Logger#log} .
*/
success(message) {
this.log(message, 'green');
}
/**
* Logs a warning (yellow) message or messages on the console.
*
* @param {LoggerMessage} message A single message of a list of them.
* @see {@link Logger#log} .
*/
warning(message) {
this.log(message, 'yellow');
}
/**
* The prefix to include in front of all the messages.
*
* @type {string}
*/
get messagesPrefix() {
return this._messagesPrefix;
}
/**
* Whether or not to show the time on each message.
*
* @type {boolean}
*/
get showTime() {
return this._showTime;
}
/**
* Gets a function to modify the color of a string. The reason for this _"proxy method"_
* is that the `colors` module doesn't have a `raw` option and the alternative would've
* been adding a few `if`s on the `log` method.
*
* @param {string} name The name of the color.
* @returns {Function} A function that receives a string and returns it colored.
* @access protected
* @ignore
*/
_color(name) {
return name === 'raw' ? (str) => str : colors[name];
}
}
/**
* The service provider to register an instance of {@link Logger} on the container.
*
* @type {ProviderCreator<LoggerProviderOptions>}
* @tutorial logger
*/
const logger = providerCreator((options = {}) => (app) => {
app.set(
options.serviceName || 'logger',
() => new Logger(options.messagesPrefix, options.showTime),
);
});
/**
* The service provider to register an instance of {@link Logger} with the package name as
* messages prefix on the container.
*
* @type {ProviderCreator<AppLoggerProviderOptions>}
* @tutorial logger
*/
const appLogger = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'appLogger', () => {
/**
* @type {AppLoggerProviderOptions}
* @ignore
*/
const useOptions = deepAssign(
{
services: {
packageInfo: 'packageInfo',
},
},
options,
);
const { packageInfo } = useOptions.services;
/**
* @type {PackageInfo}
* @ignore
*/
const usePackageInfo =
typeof packageInfo === 'string' ? app.get(packageInfo) : packageInfo;
const prefix = usePackageInfo.nameForCLI || usePackageInfo.name;
return new Logger(prefix, useOptions.showTime);
});
});
module.exports.Logger = Logger;
module.exports.logger = logger;
module.exports.appLogger = appLogger;