Skip to content
Permalink
Browse files
Refactor CordovaLogger to singleton class (#53)
  • Loading branch information
dpogue authored and Chris Brody committed Dec 18, 2018
1 parent f26ffc4 commit 6667cfcf1a131b80e09e1410e5166e5f0a420f59
Showing 2 changed files with 225 additions and 169 deletions.
@@ -17,204 +17,207 @@
under the License.
*/

var ansi = require('ansi');
var EventEmitter = require('events').EventEmitter;
var CordovaError = require('./CordovaError/CordovaError');
var EOL = require('os').EOL;
const ansi = require('ansi');
const EventEmitter = require('events').EventEmitter;
const EOL = require('os').EOL;
const formatError = require('./util/formatError');

var INSTANCE;
const INSTANCE_KEY = Symbol.for('org.apache.cordova.common.CordovaLogger');

/**
* @class CordovaLogger
*
* Implements logging facility that anybody could use. Should not be
* instantiated directly, `CordovaLogger.get()` method should be used instead
* to acquire logger instance
*/
function CordovaLogger () {
this.levels = {};
this.colors = {};
this.stdout = process.stdout;
this.stderr = process.stderr;

this.stdoutCursor = ansi(this.stdout);
this.stderrCursor = ansi(this.stderr);

this.addLevel('verbose', 1000, 'grey');
this.addLevel('normal', 2000);
this.addLevel('warn', 2000, 'yellow');
this.addLevel('info', 3000, 'blue');
this.addLevel('error', 5000, 'red');
this.addLevel('results', 10000);

this.setLevel('normal');
}

/**
* Static method to create new or acquire existing instance.
*
* @return {CordovaLogger} Logger instance
* @typedef {'verbose'|'normal'|'warn'|'info'|'error'|'results'} CordovaLoggerLevel
*/
CordovaLogger.get = function () {
return INSTANCE || (INSTANCE = new CordovaLogger());
};

CordovaLogger.VERBOSE = 'verbose';
CordovaLogger.NORMAL = 'normal';
CordovaLogger.WARN = 'warn';
CordovaLogger.INFO = 'info';
CordovaLogger.ERROR = 'error';
CordovaLogger.RESULTS = 'results';

/**
* Emits log message to process' stdout/stderr depending on message's severity
* and current log level. If severity is less than current logger's level,
* then the message is ignored.
*
* @param {String} logLevel The message's log level. The logger should have
* corresponding level added (via logger.addLevel), otherwise
* `CordovaLogger.NORMAL` level will be used.
* @param {String} message The message, that should be logged to process'
* stdio
* Implements logging facility that anybody could use.
*
* @return {CordovaLogger} Current instance, to allow calls chaining.
* Should not be instantiated directly! `CordovaLogger.get()` method should be
* used instead to acquire the logger instance.
*/
CordovaLogger.prototype.log = function (logLevel, message) {
// if there is no such logLevel defined, or provided level has
// less severity than active level, then just ignore this call and return
if (!this.levels[logLevel] || this.levels[logLevel] < this.levels[this.logLevel]) {
// return instance to allow to chain calls
return this;
class CordovaLogger {
// Encapsulate the default logging level values with constants:
static get VERBOSE () { return 'verbose'; }
static get NORMAL () { return 'normal'; }
static get WARN () { return 'warn'; }
static get INFO () { return 'info'; }
static get ERROR () { return 'error'; }
static get RESULTS () { return 'results'; }

/**
* Static method to create new or acquire existing instance.
*
* @returns {CordovaLogger} Logger instance
*/
static get () {
// This singleton instance pattern is based on the ideas from
// https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
if (Object.getOwnPropertySymbols(global).indexOf(INSTANCE_KEY) === -1) {
global[INSTANCE_KEY] = new CordovaLogger();
}
return global[INSTANCE_KEY];
}

var isVerbose = this.logLevel === 'verbose';
var cursor = this.stdoutCursor;

if (message instanceof Error || logLevel === CordovaLogger.ERROR) {
message = formatError(message, isVerbose);
cursor = this.stderrCursor;
constructor () {
/** @private */
this.levels = {};
/** @private */
this.colors = {};
/** @private */
this.stdout = process.stdout;
/** @private */
this.stderr = process.stderr;

/** @private */
this.stdoutCursor = ansi(this.stdout);
/** @private */
this.stderrCursor = ansi(this.stderr);

this.addLevel(CordovaLogger.VERBOSE, 1000, 'grey');
this.addLevel(CordovaLogger.NORMAL, 2000);
this.addLevel(CordovaLogger.WARN, 2000, 'yellow');
this.addLevel(CordovaLogger.INFO, 3000, 'blue');
this.addLevel(CordovaLogger.ERROR, 5000, 'red');
this.addLevel(CordovaLogger.RESULTS, 10000);

this.setLevel(CordovaLogger.NORMAL);
}

var color = this.colors[logLevel];
if (color) {
cursor.bold().fg[color]();
}
/**
* Emits log message to process' stdout/stderr depending on message's
* severity and current log level. If severity is less than current
* logger's level, then the message is ignored.
*
* @param {CordovaLoggerLevel} logLevel - The message's log level. The
* logger should have corresponding level added (via logger.addLevel),
* otherwise `CordovaLogger.NORMAL` level will be used.
*
* @param {string} message - The message, that should be logged to
* process's stdio.
*
* @returns {CordovaLogger} Return the current instance, to allow chaining.
*/
log (logLevel, message) {
// if there is no such logLevel defined, or provided level has
// less severity than active level, then just ignore this call and return
if (!this.levels[logLevel] || this.levels[logLevel] < this.levels[this.logLevel]) {
// return instance to allow to chain calls
return this;
}

cursor.write(message).reset().write(EOL);
var isVerbose = this.logLevel === CordovaLogger.VERBOSE;
var cursor = this.stdoutCursor;

return this;
};
if (message instanceof Error || logLevel === CordovaLogger.ERROR) {
message = formatError(message, isVerbose);
cursor = this.stderrCursor;
}

/**
* Adds a new level to logger instance. This method also creates a shortcut
* method to log events with the level provided (i.e. after adding new level
* 'debug', the method `debug(message)`, equal to logger.log('debug', message),
* will be added to logger instance)
*
* @param {String} level A log level name. The levels with the following
* names added by default to every instance: 'verbose', 'normal', 'warn',
* 'info', 'error', 'results'
* @param {Number} severity A number that represents level's severity.
* @param {String} color A valid color name, that will be used to log
* messages with this level. Any CSS color code or RGB value is allowed
* (according to ansi documentation:
* https://github.com/TooTallNate/ansi.js#features)
*
* @return {CordovaLogger} Current instance, to allow calls chaining.
*/
CordovaLogger.prototype.addLevel = function (level, severity, color) {
var color = this.colors[logLevel];
if (color) {
cursor.bold().fg[color]();
}

this.levels[level] = severity;
cursor.write(message).reset().write(EOL);

if (color) {
this.colors[level] = color;
return this;
}

// Define own method with corresponding name
if (!this[level]) {
this[level] = this.log.bind(this, level);
}
/**
* Adds a new level to logger instance.
*
* This method also creates a shortcut method to log events with the level
* provided.
* (i.e. after adding new level 'debug', the method `logger.debug(message)`
* will exist, equal to `logger.log('debug', message)`)
*
* @param {CordovaLoggerLevel} level - A log level name. The levels with
* the following names are added by default to every instance: 'verbose',
* 'normal', 'warn', 'info', 'error', 'results'.
*
* @param {number} severity - A number that represents level's severity.
*
* @param {string} color - A valid color name, that will be used to log
* messages with this level. Any CSS color code or RGB value is allowed
* (according to ansi documentation:
* https://github.com/TooTallNate/ansi.js#features).
*
* @returns {CordovaLogger} Return the current instance, to allow chaining.
*/
addLevel (level, severity, color) {
this.levels[level] = severity;

if (color) {
this.colors[level] = color;
}

return this;
};
// Define own method with corresponding name
if (!this[level]) {
Object.defineProperty(this, level, {
get () { return this.log.bind(this, level); }
});
}

/**
* Sets the current logger level to provided value. If logger doesn't have level
* with this name, `CordovaLogger.NORMAL` will be used.
*
* @param {String} logLevel Level name. The level with this name should be
* added to logger before.
*
* @return {CordovaLogger} Current instance, to allow calls chaining.
*/
CordovaLogger.prototype.setLevel = function (logLevel) {
this.logLevel = this.levels[logLevel] ? logLevel : CordovaLogger.NORMAL;
return this;
}

return this;
};
/**
* Sets the current logger level to provided value.
*
* If logger doesn't have level with this name, `CordovaLogger.NORMAL` will
* be used.
*
* @param {CordovaLoggerLevel} logLevel - Level name. The level with this
* name should be added to logger before.
*
* @returns {CordovaLogger} Current instance, to allow chaining.
*/
setLevel (logLevel) {
this.logLevel = this.levels[logLevel] ? logLevel : CordovaLogger.NORMAL;

/**
* Adjusts the current logger level according to the passed options.
*
* @param {Object|Array} opts An object or args array with options
*
* @return {CordovaLogger} Current instance, to allow calls chaining.
*/
CordovaLogger.prototype.adjustLevel = function (opts) {
if (opts.verbose || (Array.isArray(opts) && opts.includes('--verbose'))) {
this.setLevel('verbose');
} else if (opts.silent || (Array.isArray(opts) && opts.includes('--silent'))) {
this.setLevel('error');
return this;
}

return this;
};

/**
* Attaches logger to EventEmitter instance provided.
*
* @param {EventEmitter} eventEmitter An EventEmitter instance to attach
* logger to.
*
* @return {CordovaLogger} Current instance, to allow calls chaining.
*/
CordovaLogger.prototype.subscribe = function (eventEmitter) {

if (!(eventEmitter instanceof EventEmitter)) { throw new Error('Subscribe method only accepts an EventEmitter instance as argument'); }

eventEmitter.on('verbose', this.verbose)
.on('log', this.normal)
.on('info', this.info)
.on('warn', this.warn)
.on('warning', this.warn)
// Set up event handlers for logging and results emitted as events.
.on('results', this.results);

return this;
};

function formatError (error, isVerbose) {
var message = '';

if (error instanceof CordovaError) {
message = error.toString(isVerbose);
} else if (error instanceof Error) {
if (isVerbose) {
message = error.stack;
} else {
message = error.message;
/**
* Adjusts the current logger level according to the passed options.
*
* @param {Object|Array<string>} opts - An object or args array with
* options.
*
* @returns {CordovaLogger} Current instance, to allow chaining.
*/
adjustLevel (opts) {
if (opts.verbose || (Array.isArray(opts) && opts.includes('--verbose'))) {
this.setLevel('verbose');
} else if (opts.silent || (Array.isArray(opts) && opts.includes('--silent'))) {
this.setLevel('error');
}
} else {
// Plain text error message
message = error;
}

if (typeof message === 'string' && !message.toUpperCase().startsWith('ERROR:')) {
// Needed for backward compatibility with external tools
message = 'Error: ' + message;
return this;
}

return message;
/**
* Attaches logger to EventEmitter instance provided.
*
* @param {EventEmitter} eventEmitter - An EventEmitter instance to attach
* the logger to.
*
* @returns {CordovaLogger} Current instance, to allow chaining.
*/
subscribe (eventEmitter) {
if (!(eventEmitter instanceof EventEmitter)) {
throw new Error('Subscribe method only accepts an EventEmitter instance as argument');
}

eventEmitter.on('verbose', this.verbose)
.on('log', this.normal)
.on('info', this.info)
.on('warn', this.warn)
.on('warning', this.warn)
// Set up event handlers for logging and results emitted as events.
.on('results', this.results);

return this;
}
}

module.exports = CordovaLogger;

0 comments on commit 6667cfc

Please sign in to comment.