Refactor CordovaLogger to singleton class (#53)
authorDarryl Pogue <darryl@dpogue.ca>
Tue, 18 Dec 2018 13:05:01 +0000 (05:05 -0800)
committerChris Brody <chris.brody@gmail.com>
Tue, 18 Dec 2018 13:05:01 +0000 (08:05 -0500)
src/CordovaLogger.js
src/util/formatError.js [new file with mode: 0644]

index b6564d2..631070a 100644 (file)
  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;
diff --git a/src/util/formatError.js b/src/util/formatError.js
new file mode 100644 (file)
index 0000000..c080ad2
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+*/
+
+const CordovaError = require('../CordovaError/CordovaError');
+
+/**
+ * Formats an error for logging.
+ *
+ * @param {Error} error - The error to be formatted.
+ * @param {boolean} isVerbose - Whether the include additional debugging
+ *   information when formatting the error.
+ *
+ * @returns {string} The formatted error message.
+ */
+module.exports = 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;
+        }
+    } 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 message;
+};