prototypes/logger.js

var promptCallbacks = [];

process.stdin.on('readable', function () {
	var chunk = process.stdin.read();
	if (chunk !== null) {
		if (promptCallbacks.length) {
			var cbs = promptCallbacks;
			promptCallbacks = [];
			cbs.forEach(function (cb) {
				var er;
				try {
					cb(null, chunk);
				} catch (er) {
					console.error(er);
				}
			});
		}
	}
});


var path = require("path");



var stackList = function () {
	var stackReg = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/i;
	var stackReg2 = /at\s+()(.*):(\d*):(\d*)/i;
	var stackIndex = 1;
	var data = {
		method: "anonymous",
		path: "unknown",
		line: "?",
		pos: "?",
		file: "unknown",
		stack: ""
	};
	var errorStack = (new Error()).stack.split('\n').slice(3);
	var s = errorStack[stackIndex] || errorStack[0],
		sp = stackReg.exec(s) || stackReg2.exec(s);
	if (sp && sp.length === 5) {
		data.method = data.method || sp[1];
		data.path = path.relative(process.cwd(), sp[2]);
		data.line = sp[3];
		data.pos = sp[4];
		data.file = (data.path + '').replace(/^[\S\s]*\//, '');
		data.stack = errorStack.join('\n');
	}
	return data;
};

/**
 * @example
 * // =============================
 * //   Use Logger as 💻 instance
 * // =============================
 * 
 * const { LoggerBuilder } = require('@sgapps.io/server');
 * 
 * const logger = new LoggerBuilder();
 * 
 * logger.log("Hello world");
 * 
 * @example
 * // replace default console
 * 
 * const { LoggerBuilder } = require('@sgapps.io/server');
 * const logger = new LoggerBuilder();
 * logger.decorateGlobalLogger();
 * 
 * console.log("Console Messages are decorated now");
 * 
 * @class
 * @name LoggerBuilder
 * @description Pretty CLI Logger, with possibility to replace default nodejs' console logger
 */
function LoggerBuilder() {
	//@ts-ignore
	if (this === global) {
		throw Error("LoggerBuilder should be initialized with `new`");
	}

	//@ts-ignore
	this._console = global.console_original || global.console;
	/**
	 * @example
	 * // Insert an message in VT100 format
	 * logger._format = "\x1b[7m {{timestamp}} [{{TYPE}}] <{{title}}> {{file}}:{{line}} ({{method}}){{stack}}\x1b[7m";
	 * 
	 * @memberof LoggerBuilder#
	 * @description this parameter may be changed if you decide to change decoration schema
	 * @name _format
	 * @type {string}
	 */
	this._format = "\x1b[7m {{timestamp}} [{{TYPE}}] <{{title}}> {{file}}:{{line}} ({{method}}){{stack}}\x1b[7m";
	/**
	 * @memberof LoggerBuilder#
	 * @name _debug
	 * @type {boolean}
	 */
	this._debug  = true;

	/**
	 * @memberof LoggerBuilder
	 * @typedef {object} headerFormatterInfo
	 * @property {string} time
	 * @property {string} type
	 * @property {string} file
	 * @property {string} line
	 * @property {string} method
	 * @property {string} path
	 * @property {string} stack
	 */

	/**
	 * @memberof LoggerBuilder
	 * @callback headerFormatter
	 * @param {headerFormatterInfo} info
	 */

	/**
	 * @memberof LoggerBuilder#
	 * @name _headerFormatters
	 * @type {Array<headerFormatter>}
	 */
	this._headerFormatters  = [];
	return this;
}

LoggerBuilder.prototype.pushHeader = function (args, type, stack) {
	const data = stackList();
	data.time = new Date().toISOString();
	data.type = type.toUpperCase();
	if (this._headerFormatters.length) {
		this._headerFormatters.forEach(handler => {
			//@ts-ignore
			handler(data);
		});
	}
	let format = this._format;
	format = format.replace('{{timestamp}}', data.time);
	format = format.replace('{{TYPE}}', data.type);
	format = format.replace('{{file}}', data.file);
	format = format.replace('{{line}}', data.line);
	format = format.replace('{{method}}', data.method);
	format = format.replace('{{title}}', data.path);
	format = format.replace('{{stack}}', stack ? "\n" + data.stack + "\n" : "");
	args.unshift(format);
};

/**
 * @memberof LoggerBuilder#
 * @method prettyCli
 * @param {any} ref
 * @param {number} [indent]
 * @param {string} [separator="  "]
 */
LoggerBuilder.prototype.prettyCli = function (ref, indent, separator) {
	indent = indent || 0;
	if (separator === undefined) separator = "  ";
	let data = '';
	if (Array.isArray(ref)) {
		data += `\x1b[0m[${
			ref
				.map(item => this.prettyCli(item, indent + 1, separator))
				.map(
					(item, index) => `\n${separator}\x1b[0m${item}\x1b[0m${
						(index < ref.length - 1) ? ',' : '\n'
					}`
				).join('')
		}\x1b[0m]`;
	} else {
		switch (typeof(ref)) {
			case "boolean":
				data += `\x1b[0;34m${ref}\x1b[0m`;
			break;
			case "function":
				data += `\x1b[0;36m${ref}\x1b[0m`;
			break;
			case "number":
				data += `\x1b[0;33m${ref}\x1b[0m`;
			break;
			case "undefined":
				data += `\x1b[0;35m${ref}\x1b[0m`;
			break;
			case "object":
				if (ref === null) {
					data += `\x1b[0;35m${ref}\x1b[0m`;
				} else if (ref instanceof RegExp) {
					data += `\x1b[0;32mRegExp\x1b[34m(\x1b[32m${ref.toString()}\x1b[34m)\x1b[0m`;
				} else if (ref instanceof Buffer) {
					data += `\x1b[0;32mBuffer\x1b[34m(\x1b[32m${
							ref.slice(0,16)
								.toString('hex')
								.replace(/(.{2})/g, '$1, ')
								.replace(/\,\s+$/, '')
								.replace(/,/g, '\x1b[0m,\x1b[32m')
						}\x1b[0m${
							ref.byteLength > 16 ? '...' : ''
						}\x1b[34m)\x1b[0m`;
				} else {
					data += '\x1b[0m{\n';
					let prop, firstProp = true;
					for (prop in ref) {
						if (firstProp) {
							firstProp = false;
						} else {
							data += '\x1b[0m,\n';
						}
						if (`${prop}`.match(/^[\_\da-zA-Z]+$/)) {
							data += `\n${separator}\x1b[0m${prop} \: `;
						} else {
							data += `\n${separator}\x1b[32m${JSON.stringify(prop)}\x1b[0m] \: `;
						}
						data += ` ${this.prettyCli(ref[prop], indent + 1, separator)}`;
					}
					data += '}\x1b[0m';
				}
			break;
			case "string":
			default:
				data += `\x1b[0;32m${JSON.stringify(ref)}\x1b[0m`;
			break;
		}
	}
	if (indent) {
		const indentData = '\n' + new Array(indent).fill(separator).join('');
		data = indentData + data.replace(/\n/g, indentData);
	}

	return data;
};

/**
 * @memberof LoggerBuilder#
 * @method log
 * @param {...any} messages
 */
LoggerBuilder.prototype.log	= function () {
	if (this._debug) {
		var args = Array.prototype.slice.call(arguments);
		args.unshift("\x1b[0m");
		this.pushHeader(args, "log");
		args.push('\x1b[0m');
		this._console.log.apply(this._console, args);
	}
};

/**
 * @memberof LoggerBuilder#
 * @method info
 * @param {...any} messages
 */
LoggerBuilder.prototype.info = function () {
	if (this._debug) {
		var args = Array.prototype.slice.call(arguments);
		args.unshift("\x1b[0;36m ");
		this.pushHeader(args, "info");
		args.push("\x1b[0m");
		this._console.log.apply(this._console, args);
	}
};

/**
 * @memberof LoggerBuilder#
 * @method warn
 * @param {...any} messages
 */
LoggerBuilder.prototype.warn = function () {
	if (this._debug) {
		var args = Array.prototype.slice.call(arguments);
		args.unshift("\x1b[0;40;33m ");
		this.pushHeader(args, "warn");
		args.push("\x1b[0m");
		this._console.log.apply(this._console, args);
	}
};

/**
 * @memberof LoggerBuilder#
 * @method error
 * @param {...any} messages
 */
LoggerBuilder.prototype.error	= function () {
	var getStackTrace = function () {
		var obj = {};
		Error.captureStackTrace(obj, getStackTrace);
		return obj.stack;
	};
	var args = Array.prototype.slice.call(arguments);
	this._console.log("\x1b[0;40;31m");
	this.pushHeader(args, "error", true);
	this._console.error.apply(this._console, args);
	this._console.log(getStackTrace());
	this._console.log("\x1b[0m\n");
};

/**
 * @callback LoggerBuilderPrompt
 * @param {Buffer} message 
 */

/**
 * @example
 * logger.prompt("rerun tests? [y/n]: ", function (err, buffer) {
 * 	// trim spaces from response
 * 	var response = buffer.toString().replace(/^\s*(.*?)\s*$/, '$1');
 * 	if (response === 'y') {
 * 		// write your code
 * 	}
 * });
 * 
 * @memberof LoggerBuilder#
 * @method prompt
 * @param {LoggerBuilderPrompt} callback
 * @param {string|Buffer} message
 */
LoggerBuilder.prototype.prompt = function (callback, message) {
	if (typeof (message) !== "undefined")
		process.stdout.write(message);
	promptCallbacks.push(callback);
};
		
/**
 * @memberof LoggerBuilder#
 * @method decorateGlobalLogger
 */
LoggerBuilder.prototype.decorateGlobalLogger = function () {
	//@ts-ignore
	if (!global.console_original) {
		//@ts-ignore
		global.console_original = {
			log: this._console.log,
			info: this._console.info,
			warn: this._console.warn,
			error: this._console.error,
			dir: this._console.dir,
			time: this._console.time,
			timeEnd: this._console.timeEnd,
			trace: this._console.trace,
			assert: this._console.assert,
			Console: this._console.Console
		};
		global.console.log = this.log;
		global.console.info = this.info;
		global.console.warn = this.warn;
		global.console.error = this.error;
		//@ts-ignore
		global.console.prompt = this.prompt;
	}
};

module.exports = LoggerBuilder;