consola
Version:
Elegant Console Logger for Node.js and Browser
679 lines (545 loc) • 15.8 kB
JavaScript
import util from 'util';
import { sep } from 'path';
import { writeSync } from 'fs';
import dayjs from 'dayjs';
import stringWidth from 'string-width';
import figures from 'figures';
import chalk from 'chalk';
var Types = {
// Level 0
fatal: {
level: 0
},
error: {
level: 0
},
// Level 1
warn: {
level: 1
},
// Level 2
log: {
level: 2
},
// Level 3
info: {
level: 3
},
success: {
level: 3
},
// Level 4
debug: {
level: 4
},
// Level 5
trace: {
level: 5
},
// Silent
silent: {
level: Infinity
},
// Legacy
ready: {
level: 3
},
start: {
level: 3
}
};
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
} // TODO: remove for consola@3
function isLogObj(arg) {
// Should be plain object
if (!isPlainObject(arg)) {
return false;
} // Should contains either 'message' or 'args' field
if (!arg.message && !arg.args) {
return false;
} // Handle non-standard error objects
if (arg.stack) {
return false;
}
return true;
}
let paused = false;
const queue = [];
class Consola {
constructor(options = {}) {
this._reporters = options.reporters || [];
this._types = options.types || Types;
this._level = options.level != null ? options.level : 3;
this._defaults = options.defaults || {};
this._async = typeof options.async !== 'undefined' ? options.async : null;
this._stdout = options.stdout;
this._stderr = options.stdout;
this._mockFn = options.mockFn; // Create logger functions for current instance
for (const type in this._types) {
this[type] = this._wrapLogFn(Object.assign({
type
}, this._types[type], this._defaults));
} // Use _mockFn if is set
if (this._mockFn) {
this.mockTypes();
}
}
get level() {
return this._level;
}
set level(newLevel) {
// Ensure that newLevel does not exceeds type level boundaries
let min = 0;
let max = 0;
for (const typeName in this._types) {
const type = this._types[typeName];
if (type.level > max) {
max = type.level;
} else if (type.level < min) {
min = type.level;
}
} // Set level
this._level = Math.min(max, Math.max(min, newLevel));
}
get stdout() {
return this._stdout || console._stdout; // eslint-disable-line no-console
}
get stderr() {
return this._stderr || console._stderr; // eslint-disable-line no-console
}
create(options) {
return new Consola(Object.assign({
reporters: this._reporters,
level: this._level,
types: this._types,
defaults: this._defaults,
stdout: this._stdout,
stderr: this._stderr,
mockFn: this._mockFn
}, options));
}
withDefaults(defaults) {
return this.create({
defaults: Object.assign({}, this._defaults, defaults)
});
}
withTag(tag) {
return this.withDefaults({
tag: this._defaults.tag ? this._defaults.tag + ':' + tag : tag
});
}
addReporter(reporter) {
this._reporters.push(reporter);
return this;
}
removeReporter(reporter) {
if (reporter) {
const i = this._reporters.indexOf(reporter);
if (i >= 0) {
return this._reporters.splice(i, 1);
}
} else {
this._reporters.splice(0);
}
return this;
}
setReporters(reporters) {
this._reporters = Array.isArray(reporters) ? reporters : [reporters];
return this;
}
wrapAll() {
this.wrapConsole();
this.wrapStd();
}
restoreAll() {
this.restoreConsole();
this.restoreStd();
}
wrapConsole() {
for (const type in this._types) {
// Backup original value
if (!console['__' + type]) {
// eslint-disable-line no-console
console['__' + type] = console[type]; // eslint-disable-line no-console
} // Override
console[type] = this[type]; // eslint-disable-line no-console
}
}
restoreConsole() {
for (const type in this._types) {
// Restore if backup is available
if (console['__' + type]) {
// eslint-disable-line no-console
console[type] = console['__' + type]; // eslint-disable-line no-console
delete console['__' + type]; // eslint-disable-line no-console
}
}
}
wrapStd() {
this._wrapStream(this.stdout, 'log');
this._wrapStream(this.stderr, 'log');
}
_wrapStream(stream, type) {
if (!stream) {
return;
} // Backup original value
if (!stream.__write) {
stream.__write = stream.write;
} // Override
stream.write = data => {
this[type](String(data).trim());
};
}
restoreStd() {
this._restoreStream(this.stdout);
this._restoreStream(this.stderr);
}
_restoreStream(stream) {
if (!stream) {
return;
}
if (stream.__write) {
stream.write = stream.__write;
delete stream.__write;
}
}
pauseLogs() {
paused = true;
}
resumeLogs() {
paused = false; // Process queue
const _queue = queue.splice(0);
for (const item of _queue) {
item[0]._logFn(item[1], item[2]);
}
}
mockTypes(mockFn) {
this._mockFn = mockFn || this._mockFn;
if (typeof this._mockFn !== 'function') {
return;
}
for (const type in this._types) {
this[type] = this._mockFn(type, this._types[type]) || this[type];
}
}
_wrapLogFn(defaults) {
function logFn() {
if (paused) {
queue.push([this, defaults, arguments]);
return;
}
return this._logFn(defaults, arguments);
}
return logFn.bind(this);
}
_logFn(defaults, args) {
if (defaults.level > this._level) {
return this._async ? Promise.resolve(false) : false;
} // Construct a new log object
const logObj = Object.assign({
date: new Date(),
args: []
}, defaults); // Consume arguments
if (args.length === 1 && isLogObj(args[0])) {
Object.assign(logObj, args[0]);
} else {
logObj.args = Array.from(args);
} // Aliases
if (logObj.message) {
logObj.args.unshift(logObj.message);
delete logObj.message;
}
if (logObj.additional) {
if (!Array.isArray(logObj.additional)) {
logObj.additional = logObj.additional.split('\n');
}
logObj.args.push('\n' + logObj.additional.join('\n'));
delete logObj.additional;
} // Log
if (this._async) {
return this._logAsync(logObj);
} else {
this._log(logObj);
}
}
_log(logObj) {
for (const reporter of this._reporters) {
reporter.log(logObj, {
async: false,
stdout: this.stdout,
stderr: this.stderr
});
}
}
_logAsync(logObj) {
return Promise.all(this._reporters.map(reporter => reporter.log(logObj, {
async: true,
stdout: this.stdout,
stderr: this.stderr
})));
}
} // Legacy support
Consola.prototype.add = Consola.prototype.addReporter;
Consola.prototype.remove = Consola.prototype.removeReporter;
Consola.prototype.clear = Consola.prototype.removeReporter;
Consola.prototype.withScope = Consola.prototype.withTag;
Consola.prototype.mock = Consola.prototype.mockTypes;
Consola.prototype.pause = Consola.prototype.pauseLogs;
Consola.prototype.resume = Consola.prototype.resumeLogs; // Export class
function assignGlobalReference(newInstance, referenceKey) {
if (!newInstance.constructor || global[referenceKey] && !global[referenceKey].constructor) {
throw new Error('Assigning to global reference is only supported for class instances');
} else if (newInstance.constructor && !global[referenceKey]) {
global[referenceKey] = newInstance;
} else if (!(newInstance instanceof global[referenceKey].constructor || global[referenceKey] instanceof newInstance.constructor)) {
throw new Error(`Not a ${global[referenceKey].constructor.name} instance`);
}
const oldInstance = Object.create(global[referenceKey]);
for (let prop in global[referenceKey]) {
oldInstance[prop] = global[referenceKey][prop];
delete global[referenceKey][prop];
}
for (let prop of Object.getOwnPropertySymbols(global[referenceKey])) {
oldInstance[prop] = global[referenceKey][prop];
delete global[referenceKey][prop];
}
for (let prop in newInstance) {
global[referenceKey][prop] = newInstance[prop];
}
for (let prop of Object.getOwnPropertySymbols(newInstance)) {
global[referenceKey][prop] = newInstance[prop];
}
return oldInstance;
}
function assignGlobalConsola(newConsola) {
return assignGlobalReference(newConsola, 'consola');
}
function parseStack(stack) {
const cwd = process.cwd() + sep;
let lines = stack.split('\n').splice(1).map(l => l.trim().replace('file://', '').replace(cwd, ''));
return lines;
}
function writeStream(data, stream, mode = 'default') {
const write = stream.__write || stream.write;
switch (mode) {
case 'async':
return new Promise(resolve => {
if (write.call(stream, data) === true) {
resolve();
} else {
stream.once('drain', () => {
resolve();
});
}
});
case 'sync':
return writeSync(stream.fd, data);
default:
return write.call(stream, data);
}
}
function formatDate(timeFormat, date) {
return dayjs(date).format(timeFormat);
}
const DEFAULTS = {
dateFormat: 'HH:mm:ss',
formatOptions: {
colors: false,
compact: true
}
};
const bracket = x => x ? `[${x}]` : '';
class BasicReporter {
constructor(options) {
this.options = Object.assign({}, DEFAULTS, options);
}
formatStack(stack) {
return ' ' + parseStack(stack).join('\n ');
}
formatArgs(args) {
const _args = args.map(arg => {
if (arg && typeof arg.stack === 'string') {
return arg.message + '\n' + this.formatStack(arg.stack);
}
return arg;
}); // Only supported with Node >= 10
// https://nodejs.org/api/util.html#util_util_inspect_object_options
if (typeof util.formatWithOptions === 'function') {
return util.formatWithOptions(this.options.formatOptions, ..._args);
} else {
return util.format(..._args);
}
}
formatDate(date) {
return formatDate(this.options.dateFormat, date);
}
filterAndJoin(arr) {
return arr.filter(x => x).join(' ');
}
formatLogObj(logObj) {
const message = this.formatArgs(logObj.args);
const date = this.formatDate(logObj.date);
const type = logObj.type.toUpperCase();
return this.filterAndJoin([bracket(date), bracket(logObj.tag), bracket(type), message]);
}
log(logObj, {
async,
stdout,
stderr
} = {}) {
const line = this.formatLogObj(logObj, {
width: stdout.columns ? stdout.columns - 1 : 80
});
return writeStream(line + '\n', logObj.level < 2 ? stderr : stdout, async ? 'async' : 'default');
}
}
const TYPE_COLOR_MAP = {
'info': 'cyan'
};
const LEVEL_COLOR_MAP = {
0: 'red',
1: 'yellow',
2: 'white',
3: 'green'
};
class BrowserReporter {
constructor(options) {
this.options = Object.assign({}, options);
}
log(logObj) {
const consoleLogFn = logObj.level < 1 // eslint-disable-next-line no-console
? console.__error || console.error : console.__log || console.log; // Type
const type = logObj.type !== 'log' ? `[${logObj.type.toUpperCase()}]` : ''; // Tag
const tag = logObj.tag ? `[${logObj.tag.toUpperCase()}]` : ''; // Date
const date = `[${new Date(logObj.date).toLocaleTimeString()}]`; // Styles
const color = TYPE_COLOR_MAP[logObj.type] || LEVEL_COLOR_MAP[logObj.level];
const styleColor = `color: ${color}; background-color: inherit;`;
const styleGrey = `color: ${logObj.additionalColor || 'grey'}; background-color: inherit;`; // Log to the console
consoleLogFn('%c' + date + tag + '%c' + type, styleGrey, styleColor, ...logObj.args);
}
}
const _colorCache = {};
function chalkColor(name) {
let color = _colorCache[name];
if (color) {
return color;
}
if (name[0] === '#') {
color = chalk.hex(name);
} else {
color = chalk[name] || chalk.keyword(name);
}
_colorCache[name] = color;
return color;
}
const _bgColorCache = {};
function chalkBgColor(name) {
let color = _bgColorCache[name];
if (color) {
return color;
}
if (name[0] === '#') {
color = chalk.bgHex(name);
} else {
color = chalk['bg' + name[0].toUpperCase() + name.slice(1)] || chalk.bgKeyword(name);
}
_bgColorCache[name] = color;
return color;
}
const DEFAULTS$1 = {
secondaryColor: 'grey',
formatOptions: {
colors: true,
compact: false
}
};
const TYPE_ICONS = {
info: figures('ℹ'),
success: figures('✔'),
debug: figures('›'),
trace: figures('›'),
log: ''
};
class FancyReporter extends BasicReporter {
constructor(options) {
super(Object.assign({}, DEFAULTS$1, options));
}
formatStack(stack) {
const grey = chalkColor('grey');
const cyan = chalkColor('cyan');
return '\n' + parseStack(stack).map(line => ' ' + line.replace(/^at +/, m => grey(m)).replace(/\((.+)\)/, (_, m) => `(${cyan(m)})`)).join('\n');
}
formatType(logObj, isBadge) {
const typeColor = TYPE_COLOR_MAP[logObj.type] || LEVEL_COLOR_MAP[logObj.level] || this.options.secondaryColor;
if (isBadge) {
return chalkBgColor(typeColor).black(` ${logObj.type.toUpperCase()} `);
}
const _type = typeof TYPE_ICONS[logObj.type] === 'string' ? TYPE_ICONS[logObj.type] : logObj.icon || logObj.type;
return _type ? chalkColor(typeColor)(_type) : '';
}
formatLogObj(logObj, {
width
}) {
const [message, ...additional] = this.formatArgs(logObj.args).split('\n');
const isBadge = typeof logObj.badge !== 'undefined' ? Boolean(logObj.badge) : logObj.level < 2;
const secondaryColor = chalkColor(this.options.secondaryColor);
const date = secondaryColor(this.formatDate(logObj.date));
const type = this.formatType(logObj, isBadge);
const tag = logObj.tag ? secondaryColor(logObj.tag) : '';
let left = this.filterAndJoin([type, message]);
let right = this.filterAndJoin([tag, date]);
const space = width - stringWidth(left) - stringWidth(right) - 2;
let line = space > 0 ? left + ' '.repeat(space) + right : left;
line += additional.length ? '\n' + additional.join('\n') : '';
return isBadge ? '\n' + line + '\n' : line;
}
}
class JSONReporter {
constructor({
stream
} = {}) {
this.stream = stream || process.stdout;
}
log(logObj) {
this.stream.write(JSON.stringify(logObj) + '\n');
}
}
// This reporter is compatible with Winston 3
// https://github.com/winstonjs/winston
// eslint-disable-next-line
const _require = typeof __non_webpack_require__ !== 'undefined' ? __non_webpack_require__ : require; // bypass webpack
class WinstonReporter {
constructor(logger) {
if (logger && logger.log) {
this.logger = logger;
} else {
const winston = _require('winston');
this.logger = winston.createLogger(Object.assign({
level: 'info',
format: winston.format.simple(),
transports: [new winston.transports.Console()]
}, logger));
}
}
log(logObj) {
const args = [].concat(logObj.args);
const arg0 = args.shift();
this.logger.log({
level: levels[logObj.level] || 'info',
label: logObj.tag,
message: arg0,
args: args,
timestamp: logObj.date.getTime() / 1000
});
}
}
const levels = {
0: 'error',
1: 'warn',
2: 'info',
3: 'verbose',
4: 'debug',
5: 'silly'
};
export { Consola, Types, isLogObj, assignGlobalConsola, BasicReporter, BrowserReporter, FancyReporter, JSONReporter, WinstonReporter };