407 lines
9.9 KiB
JavaScript
407 lines
9.9 KiB
JavaScript
|
'use strict';
|
||
|
const util = require('util');
|
||
|
const path = require('path');
|
||
|
const readline = require('readline');
|
||
|
const chalk = require('chalk');
|
||
|
const figures = require('figures');
|
||
|
const pkgConf = require('pkg-conf');
|
||
|
const pkg = require('./package.json');
|
||
|
const defaultTypes = require('./types');
|
||
|
|
||
|
const {green, grey, red, underline, yellow} = chalk;
|
||
|
|
||
|
let isPreviousLogInteractive = false;
|
||
|
const defaults = pkg.options.default;
|
||
|
const namespace = pkg.name;
|
||
|
|
||
|
class Signale {
|
||
|
constructor(options = {}) {
|
||
|
this._interactive = options.interactive || false;
|
||
|
this._config = Object.assign(this.packageConfiguration, options.config);
|
||
|
this._customTypes = Object.assign({}, options.types);
|
||
|
this._disabled = options.disabled || false;
|
||
|
this._scopeName = options.scope || '';
|
||
|
this._timers = options.timers || new Map();
|
||
|
this._types = this._mergeTypes(defaultTypes, this._customTypes);
|
||
|
this._stream = options.stream || process.stdout;
|
||
|
this._longestLabel = this._getLongestLabel();
|
||
|
this._secrets = options.secrets || [];
|
||
|
this._generalLogLevel = this._validateLogLevel(options.logLevel);
|
||
|
|
||
|
Object.keys(this._types).forEach(type => {
|
||
|
this[type] = this._logger.bind(this, type);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
get _now() {
|
||
|
return Date.now();
|
||
|
}
|
||
|
|
||
|
get scopeName() {
|
||
|
return this._scopeName;
|
||
|
}
|
||
|
|
||
|
get currentOptions() {
|
||
|
return Object.assign({}, {
|
||
|
config: this._config,
|
||
|
disabled: this._disabled,
|
||
|
types: this._customTypes,
|
||
|
interactive: this._interactive,
|
||
|
timers: this._timers,
|
||
|
stream: this._stream,
|
||
|
secrets: this._secrets,
|
||
|
logLevel: this._generalLogLevel
|
||
|
});
|
||
|
}
|
||
|
|
||
|
get date() {
|
||
|
return new Date().toLocaleDateString();
|
||
|
}
|
||
|
|
||
|
get timestamp() {
|
||
|
return new Date().toLocaleTimeString();
|
||
|
}
|
||
|
|
||
|
get filename() {
|
||
|
const _ = Error.prepareStackTrace;
|
||
|
Error.prepareStackTrace = (error, stack) => stack;
|
||
|
const {stack} = new Error();
|
||
|
Error.prepareStackTrace = _;
|
||
|
|
||
|
const callers = stack.map(x => x.getFileName());
|
||
|
|
||
|
const firstExternalFilePath = callers.find(x => {
|
||
|
return x !== callers[0];
|
||
|
});
|
||
|
|
||
|
return firstExternalFilePath ? path.basename(firstExternalFilePath) : 'anonymous';
|
||
|
}
|
||
|
|
||
|
get packageConfiguration() {
|
||
|
return pkgConf.sync(namespace, {defaults});
|
||
|
}
|
||
|
|
||
|
get _longestUnderlinedLabel() {
|
||
|
return underline(this._longestLabel);
|
||
|
}
|
||
|
|
||
|
get _logLevels() {
|
||
|
return {
|
||
|
info: 0,
|
||
|
timer: 1,
|
||
|
debug: 2,
|
||
|
warn: 3,
|
||
|
error: 4
|
||
|
};
|
||
|
}
|
||
|
|
||
|
set configuration(configObj) {
|
||
|
this._config = Object.assign(this.packageConfiguration, configObj);
|
||
|
}
|
||
|
|
||
|
_arrayify(x) {
|
||
|
return Array.isArray(x) ? x : [x];
|
||
|
}
|
||
|
|
||
|
_timeSpan(then) {
|
||
|
return (this._now - then);
|
||
|
}
|
||
|
|
||
|
_getLongestLabel() {
|
||
|
const {_types} = this;
|
||
|
const labels = Object.keys(_types).map(x => _types[x].label);
|
||
|
return labels.reduce((x, y) => x.length > y.length ? x : y);
|
||
|
}
|
||
|
|
||
|
_validateLogLevel(level) {
|
||
|
return Object.keys(this._logLevels).includes(level) ? level : 'info';
|
||
|
}
|
||
|
|
||
|
_mergeTypes(standard, custom) {
|
||
|
const types = Object.assign({}, standard);
|
||
|
|
||
|
Object.keys(custom).forEach(type => {
|
||
|
types[type] = Object.assign({}, types[type], custom[type]);
|
||
|
});
|
||
|
|
||
|
return types;
|
||
|
}
|
||
|
|
||
|
_filterSecrets(message) {
|
||
|
const {_secrets} = this;
|
||
|
|
||
|
if (_secrets.length === 0) {
|
||
|
return message;
|
||
|
}
|
||
|
|
||
|
let safeMessage = message;
|
||
|
|
||
|
_secrets.forEach(secret => {
|
||
|
safeMessage = safeMessage.replace(new RegExp(secret, 'g'), '[secure]');
|
||
|
});
|
||
|
|
||
|
return safeMessage;
|
||
|
}
|
||
|
|
||
|
_formatStream(stream) {
|
||
|
return this._arrayify(stream);
|
||
|
}
|
||
|
|
||
|
_formatDate() {
|
||
|
return `[${this.date}]`;
|
||
|
}
|
||
|
|
||
|
_formatFilename() {
|
||
|
return `[${this.filename}]`;
|
||
|
}
|
||
|
|
||
|
_formatScopeName() {
|
||
|
if (Array.isArray(this._scopeName)) {
|
||
|
const scopes = this._scopeName.filter(x => x.length !== 0);
|
||
|
return `${scopes.map(x => `[${x.trim()}]`).join(' ')}`;
|
||
|
}
|
||
|
|
||
|
return `[${this._scopeName}]`;
|
||
|
}
|
||
|
|
||
|
_formatTimestamp() {
|
||
|
return `[${this.timestamp}]`;
|
||
|
}
|
||
|
|
||
|
_formatMessage(str) {
|
||
|
return util.format(...this._arrayify(str));
|
||
|
}
|
||
|
|
||
|
_meta() {
|
||
|
const meta = [];
|
||
|
|
||
|
if (this._config.displayDate) {
|
||
|
meta.push(this._formatDate());
|
||
|
}
|
||
|
|
||
|
if (this._config.displayTimestamp) {
|
||
|
meta.push(this._formatTimestamp());
|
||
|
}
|
||
|
|
||
|
if (this._config.displayFilename) {
|
||
|
meta.push(this._formatFilename());
|
||
|
}
|
||
|
|
||
|
if (this._scopeName.length !== 0 && this._config.displayScope) {
|
||
|
meta.push(this._formatScopeName());
|
||
|
}
|
||
|
|
||
|
if (meta.length !== 0) {
|
||
|
meta.push(`${figures.pointerSmall}`);
|
||
|
return meta.map(item => grey(item));
|
||
|
}
|
||
|
|
||
|
return meta;
|
||
|
}
|
||
|
|
||
|
_hasAdditional({suffix, prefix}, args) {
|
||
|
return (suffix || prefix) ? '' : this._formatMessage(args);
|
||
|
}
|
||
|
|
||
|
_buildSignale(type, ...args) {
|
||
|
let [msg, additional] = [{}, {}];
|
||
|
|
||
|
if (args.length === 1 && typeof (args[0]) === 'object' && args[0] !== null) {
|
||
|
if (args[0] instanceof Error) {
|
||
|
[msg] = args;
|
||
|
} else {
|
||
|
const [{prefix, message, suffix}] = args;
|
||
|
additional = Object.assign({}, {suffix, prefix});
|
||
|
msg = message ? this._formatMessage(message) : this._hasAdditional(additional, args);
|
||
|
}
|
||
|
} else {
|
||
|
msg = this._formatMessage(args);
|
||
|
}
|
||
|
|
||
|
const signale = this._meta();
|
||
|
|
||
|
if (additional.prefix) {
|
||
|
if (this._config.underlinePrefix) {
|
||
|
signale.push(underline(additional.prefix));
|
||
|
} else {
|
||
|
signale.push(additional.prefix);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this._config.displayBadge && type.badge) {
|
||
|
signale.push(chalk[type.color](this._padEnd(type.badge, type.badge.length + 1)));
|
||
|
}
|
||
|
|
||
|
if (this._config.displayLabel && type.label) {
|
||
|
const label = this._config.uppercaseLabel ? type.label.toUpperCase() : type.label;
|
||
|
if (this._config.underlineLabel) {
|
||
|
signale.push(chalk[type.color](this._padEnd(underline(label), this._longestUnderlinedLabel.length + 1)));
|
||
|
} else {
|
||
|
signale.push(chalk[type.color](this._padEnd(label, this._longestLabel.length + 1)));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (msg instanceof Error && msg.stack) {
|
||
|
const [name, ...rest] = msg.stack.split('\n');
|
||
|
if (this._config.underlineMessage) {
|
||
|
signale.push(underline(name));
|
||
|
} else {
|
||
|
signale.push(name);
|
||
|
}
|
||
|
|
||
|
signale.push(grey(rest.map(l => l.replace(/^/, '\n')).join('')));
|
||
|
return signale.join(' ');
|
||
|
}
|
||
|
|
||
|
if (this._config.underlineMessage) {
|
||
|
signale.push(underline(msg));
|
||
|
} else {
|
||
|
signale.push(msg);
|
||
|
}
|
||
|
|
||
|
if (additional.suffix) {
|
||
|
if (this._config.underlineSuffix) {
|
||
|
signale.push(underline(additional.suffix));
|
||
|
} else {
|
||
|
signale.push(additional.suffix);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return signale.join(' ');
|
||
|
}
|
||
|
|
||
|
_write(stream, message) {
|
||
|
if (this._interactive && stream.isTTY && isPreviousLogInteractive) {
|
||
|
readline.moveCursor(stream, 0, -1);
|
||
|
readline.clearLine(stream);
|
||
|
readline.cursorTo(stream, 0);
|
||
|
}
|
||
|
|
||
|
stream.write(message + '\n');
|
||
|
isPreviousLogInteractive = this._interactive;
|
||
|
}
|
||
|
|
||
|
_log(message, streams = this._stream, logLevel) {
|
||
|
if (this.isEnabled() && this._logLevels[logLevel] >= this._logLevels[this._generalLogLevel]) {
|
||
|
this._formatStream(streams).forEach(stream => {
|
||
|
this._write(stream, message);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_logger(type, ...messageObj) {
|
||
|
const {stream, logLevel} = this._types[type];
|
||
|
const message = this._buildSignale(this._types[type], ...messageObj);
|
||
|
this._log(this._filterSecrets(message), stream, this._validateLogLevel(logLevel));
|
||
|
}
|
||
|
|
||
|
_padEnd(str, targetLength) {
|
||
|
str = String(str);
|
||
|
targetLength = parseInt(targetLength, 10) || 0;
|
||
|
|
||
|
if (str.length >= targetLength) {
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
if (String.prototype.padEnd) {
|
||
|
return str.padEnd(targetLength);
|
||
|
}
|
||
|
|
||
|
targetLength -= str.length;
|
||
|
return str + ' '.repeat(targetLength);
|
||
|
}
|
||
|
|
||
|
addSecrets(secrets) {
|
||
|
if (!Array.isArray(secrets)) {
|
||
|
throw new TypeError('Argument must be an array.');
|
||
|
}
|
||
|
|
||
|
this._secrets.push(...secrets);
|
||
|
}
|
||
|
|
||
|
clearSecrets() {
|
||
|
this._secrets = [];
|
||
|
}
|
||
|
|
||
|
config(configObj) {
|
||
|
this.configuration = configObj;
|
||
|
}
|
||
|
|
||
|
disable() {
|
||
|
this._disabled = true;
|
||
|
}
|
||
|
|
||
|
enable() {
|
||
|
this._disabled = false;
|
||
|
}
|
||
|
|
||
|
isEnabled() {
|
||
|
return !this._disabled;
|
||
|
}
|
||
|
|
||
|
scope(...name) {
|
||
|
if (name.length === 0) {
|
||
|
throw new Error('No scope name was defined.');
|
||
|
}
|
||
|
|
||
|
return new Signale(Object.assign(this.currentOptions, {scope: name}));
|
||
|
}
|
||
|
|
||
|
unscope() {
|
||
|
this._scopeName = '';
|
||
|
}
|
||
|
|
||
|
time(label) {
|
||
|
if (!label) {
|
||
|
label = `timer_${this._timers.size}`;
|
||
|
}
|
||
|
|
||
|
this._timers.set(label, this._now);
|
||
|
|
||
|
const message = this._meta();
|
||
|
message.push(green(this._padEnd(this._types.start.badge, 2)));
|
||
|
|
||
|
if (this._config.underlineLabel) {
|
||
|
message.push(green(this._padEnd(underline(label), this._longestUnderlinedLabel.length + 1)));
|
||
|
} else {
|
||
|
message.push(green(this._padEnd(label, this._longestLabel.length + 1)));
|
||
|
}
|
||
|
|
||
|
message.push('Initialized timer...');
|
||
|
this._log(message.join(' '), this._stream, 'timer');
|
||
|
|
||
|
return label;
|
||
|
}
|
||
|
|
||
|
timeEnd(label) {
|
||
|
if (!label && this._timers.size) {
|
||
|
const is = x => x.includes('timer_');
|
||
|
label = [...this._timers.keys()].reduceRight((x, y) => {
|
||
|
return is(x) ? x : (is(y) ? y : null);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this._timers.has(label)) {
|
||
|
const span = this._timeSpan(this._timers.get(label));
|
||
|
this._timers.delete(label);
|
||
|
|
||
|
const message = this._meta();
|
||
|
message.push(red(this._padEnd(this._types.pause.badge, 2)));
|
||
|
|
||
|
if (this._config.underlineLabel) {
|
||
|
message.push(red(this._padEnd(underline(label), this._longestUnderlinedLabel.length + 1)));
|
||
|
} else {
|
||
|
message.push(red(this._padEnd(label, this._longestLabel.length + 1)));
|
||
|
}
|
||
|
|
||
|
message.push('Timer run for:');
|
||
|
message.push(yellow(span < 1000 ? span + 'ms' : (span / 1000).toFixed(2) + 's'));
|
||
|
this._log(message.join(' '), this._stream, 'timer');
|
||
|
|
||
|
return {label, span};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Signale;
|