Logger.js

/* globals Wsh: false */
/* globals process: false */

(function () {
  if (Wsh && Wsh.Logger) return;

  /**
   * The logger module for WSH (Windows Script Host) that writes logging messages in a console or a file or Windows-Event-Viewer.
   *
   * @namespace Logger
   * @memberof Wsh
   * @requires {@link https://github.com/tuckn/WshModeJs|tuckn/WshModeJs}
   */
  Wsh.Logger = {};

  // Shorthands
  var sh = Wsh.Shell;
  var util = Wsh.Util;
  var CD = Wsh.Constants;
  var path = Wsh.Path;
  var os = Wsh.OS;
  var fs = Wsh.FileSystem;
  var fse = Wsh.FileSystemExtra;

  var insp = util.inspect;
  var obtain = util.obtainPropVal;
  var isPlainObject = util.isPlainObject;
  var isSolidString = util.isSolidString;
  var hasIn = util.hasIn;
  var hasContent = util.hasContent;
  var parseDate = util.createDateString;
  var parseDateLiteral = util.parseDateLiteral;

  var logger = Wsh.Logger;

  /** @constant {string} */
  var MODULE_TITLE = 'WshLogger/Logger.js';

  var throwErrNonStr = function (functionName, typeErrVal) {
    util.throwTypeError('string', MODULE_TITLE, functionName, typeErrVal);
  };

  var lvPriority = {
    off: 0,
    error: 1,
    warn: 2,
    success: 3,
    info: 4,
    debug: 5
  };

  // _parseLogsToStr {{{
  /**
   * @private
   * @param {Array} logs - An Array of logs to parse.
   * @param {object} [options] - Optional parameters.
   * @param {boolean} [options.eolChar] - Default is {@link https://tuckn.net/docs/WshOS/Wsh.OS.html#.EOL|Wsh.OS.EOL}.
   * @returns {void}
   */
  function _parseLogsToStr (logs, options) {
    var eolChar = hasIn(options, 'eolChar') ? options.eolChar : os.EOL;

    var logStr = '';
    var log, lv; // Shorthand
    var lvDisp = {
      debug: 'debug   ',
      info: 'info    ',
      success: 'success ',
      warn: 'warn    ',
      error: 'error   '
    };

    for (var i = 0, I = logs.length; i < I; i++) {
      log = logs[i]; // shorthand
      logStr += '[' + log.timestamp + ']';
      lv = isSolidString(log.level) ? log.level : 'info';
      logStr += ' ' + lvDisp[lv] + log.message + eolChar;
    }

    return logStr;
  } // }}}

  // _createLogObj {{{
  /**
   * @private
   * @param {string} level
   * @param {string} message
   * @returns {object}
   */
  function _createLogObj (level, message) {
    return {
      timestamp: parseDate('yyyy-MM-ddTHH:mm:ss'),
      level: level,
      message: message
    };
  } // }}}

  // _showLogsWithConsole {{{
  /**
   * [W.I.P] @TODO Create a result console window when running wscript.exe
   *
   * @private
   * @param {string} logStr - A String of logs to output.
   * @returns {void}
   */
  function _showLogsWithConsole (logStr) {
    // Save the logStr on a temporary file.
    var tmpLog = os.makeTmpPath('WshLogWriter_', '.log');
    fs.writeFileSync(tmpLog, logStr, { encoding: os.cmdCodeset() });

    var cmdCode = 'TYPE "' + tmpLog + '"\r\n@PAUSE';
    var tmpCmd = tmpLog.replace('.log', '.cmd');
    fs.writeFileSync(tmpCmd, cmdCode, { encoding: CD.ado.charset.utf8 });

    // Show the file content with CMD.exe type
    sh.Run('CMD.EXE /S /K"' + tmpCmd + '"', CD.windowStyles.activeDef, false);
    // child_process.execFile(tmpCmd);

    /**
     * @FIXME Remove tmp. .log and .cmd
     * If you write the following code to remove the temporary files, not work.
     */
    // try {
    //   fse.removeSync(tmpLog);
    //   fse.removeSync(tmpCmd);
    // } catch (e) {
    //   // Ignore the error
    // }
  } // }}}

  // _Logger {{{
  /**
   * @typedef {object} typeLoggerCreateOptions
   * @property {string} [level=null] - debug, info, success, warn, error, off
   * @property {string|_Logger} transportation - console, popup, winEvent, `filepath`. Can use a date string (e.g. "C:\\My log\\#{yyyy-MM-dd}.log" -> 2019-10-29.log) or _Logger instance
   * @property {string} [encoding] - Default: {@link Wsh.Constants.UTF8}. A log file encoding
   */

  /**
   * @private
   * @class _Logger
   * @param {typeLoggerCreateOptions} params
   */
  function _Logger (params) {
    // Constructor
    this.level = obtain(params, 'level', 'info').toLowerCase();

    var dest = obtain(params, 'transportation', 'console');
    if (/^none$/i.test(dest)) {
      this.transportation = 'NONE';
    } else if (/^console$/i.test(dest)) {
      this.transportation = 'CONSOLE';
    } else if (/^popup$/i.test(dest)) {
      this.transportation = 'POPUP';
    } else if (/^winevent$/i.test(dest)) {
      this.transportation = 'WINEVENT';
    } else {
      this.transportation = 'FILE';
      dest = path.resolve(dest);

      this.logPath = parseDateLiteral(dest);
      if (fs.existsSync(this.logPath)) {
        if (fs.statSync(this.logPath).isDirectory()) {
          this.logPath = path.join(this.logPath, parseDate() + '.log');
        }
      }
    }

    // this.runsCscript = /cscript\.exe$/i.test(process.execPath);
    this.encoding = obtain(params, 'encoding', CD.ado.charset.utf8);
    this.logs = [];
    this.stackingLogs = [];
    // The level for a result (Enable on 'popup', 'winEvent')
    this.logLevelResult = 'info';

    // Methods

    // clear {{{
    /**
     * Clears all info of the logger instance.
     *
     * @memberof Wsh.Logger
     * @returns {void}
     */
    this.clear = function () {
      this.logs = [];
      this.logLevelResult = 'info';
    }; // }}}

    // setLevel {{{
    /**
     * Sets the logging level.
     *
     * @memberof Wsh.Logger
     * @param {string} level - The log level to set.
     * @returns {void}
     */
    this.setLevel = function (level) {
      this.level = level.toLowerCase();
    }; // }}}

    // stackLog {{{
    /**
     * Stores the log to output.
     *
     * @memberof Wsh.Logger
     * @param {object} obj
     * @param {string} object.level
     * @param {string} object.message
     * @returns {void}
     */
    this.stackLog = function (obj) {
      var level = obtain(obj, 'level', null).toLowerCase();
      if (!this.outputsLog(level)) return;

      this.stackingLogs.push(_createLogObj(level, obj.message));
    }; // }}}

    // clearStackingLogs {{{
    /**
     * Clears the stacking logs to output.
     *
     * @memberof Wsh.Logger
     * @returns {void}
     */
    this.clearStackingLogs = function () {
      this.stackingLogs = [];
    }; // }}}

    // outputsLog {{{
    /**
     * Checks the log whether the level to output.
     *
     * @memberof Wsh.Logger
     * @param {string} level
     * @returns {boolean}
     */
    this.outputsLog = function (level) {
      var functionName = 'outputsLog';
      if (!isSolidString(level)) throwErrNonStr(functionName, level);

      if (lvPriority[this.logLevelResult] > lvPriority[level]) {
        this.logLevelResult = level; // Update the result level
      }

      if (level === 'off') return false;

      if (level === 'debug' && process.env.WSH_ENV === 'development') {
        return true;
      }

      if (lvPriority[this.level] < lvPriority[level]) return false;
      return true;
    }; // }}}

    // log {{{
    /**
     * Adds the log.
     *
     * @example
     * var logger = Wsh.Logger.create();
     *
     * logger.log({ level: 'info', message: 'Information message' });
     * // The above is equal with `logger.info('Information message');`
     * @memberof Wsh.Logger
     * @param {object} obj - The object to log.
     * @param {string} obj.level - The logging level.
     * @param {string} obj.message - The logging message.
     * @returns {void}
     */
    this.log = function (obj) {
      var level = obtain(obj, 'level', null).toLowerCase();
      if (!this.outputsLog(level)) return;

      if (this.stackingLogs.length > 0) {
        this.logs = this.logs.concat(this.stackingLogs);
        this.clearStackingLogs();
      }

      var message = obtain(obj, 'message', '');

      this.logs.push(_createLogObj(level, message));

      // Realtime showing
      if (this.transportation === 'CONSOLE') {
        var parsedLog = _parseLogsToStr(this.logs, { eolChar: '' });

        if (level === 'error') {
          console.error(parsedLog);
        } else {
          console.log(parsedLog);
        }

        this.clear();
      }
    }; // }}}

    // debug {{{
    /**
     * If process.env.WSH_ENV is 'development', add the log of info level.
     *
     * @example
     * var logger = Wsh.Logger.create();
     *
     * logger.debug('Debug message as info');
     * // The above is equal with `logger.log('debug', 'Debug message as info');`
     * @memberof Wsh.Logger
     * @param {string} [message]
     * @returns {void}
     */
    this.debug = function (message) {
      this.log({ level: 'debug', message: message });
    }; // }}}

    // info {{{
    /**
     * Adds the log of info level.
     *
     * @example
     * var logger = Wsh.Logger.create();
     *
     * logger.info('Information message');
     * // The above is equal with `logger.log('info', 'Information message');`
     * @memberof Wsh.Logger
     * @param {string} [message]
     * @returns {void}
     */
    this.info = function (message) {
      this.log({ level: 'info', message: message });
    }; // }}}

    // success {{{
    /**
     * Adds the log of success level.
     *
     * @example
     * var logger = Wsh.Logger.create();
     *
     * logger.success('Success message');
     * // The above is equal with `logger.log('success', 'Success message');`
     * @memberof Wsh.Logger
     * @param {string} [message]
     * @returns {void}
     */
    this.success = function (message) {
      this.log({ level: 'success', message: message });
    }; // }}}

    // warn {{{
    /**
     * Adds the log of warn level.
     *
     * @example
     * var logger = Wsh.Logger.create();
     *
     * logger.warn('Warnning message');
     * // The above is equal with `logger.log('warn', 'Warnning message');`
     * @memberof Wsh.Logger
     * @param {string} [message]
     * @returns {void}
     */
    this.warn = function (message) {
      this.log({ level: 'warn', message: message });
    }; // }}}

    // error {{{
    /**
     * Adds the log of error level.
     *
     * @example
     * var logger = Wsh.Logger.create();
     *
     * logger.error('Error message');
     * // The above is equal with `logger.log('error', 'Error message');`
     * @memberof Wsh.Logger
     * @param {string} [message]
     * @returns {void}
     */
    this.error = function (message) {
      this.log({ level: 'error', message: message });
    }; // }}}

    // transport {{{
    /**
     * Outputs the storing logs and clears.
     *
     * @memberof Wsh.Logger
     * @returns {void}
     */
    this.transport = function () {
      var functionName = 'transport';

      if (this.transportation === null || this.transportation === 'NONE') {
        return;
      }

      var logStr = '';
      if (this.transportation === 'FILE') {
        try {
          logStr = fs.readFileSync(this.logPath, { encoding: this.encoding });
        } catch (e) {
          var errStr = insp(e);

          if (/no such file or directory/i.test(errStr)) {
            logStr = ''; // Ignores this error.
          } else {
            throw e;
          }
        }
      }

      logStr += _parseLogsToStr(this.logs);

      if (logStr == '') return this.clear();

      if (this.transportation === 'CONSOLE') {
        console.log(logStr);
      } else if (this.transportation === 'POPUP') {
        var ico = /^error$/i.test(this.logLevelResult)
          ? CD.iconTypes.stop : (
            /^warn$/i.test(this.logLevelResult)
            ? CD.iconTypes.excl : CD.iconTypes.infomaiton);

        /**
         * Displays text in a pop-up message box. {@link https://msdn.microsoft.com/ja-jp/library/cc364428.aspx|Microsoft Docs}
         *
         * @function Popup
         * @memberof Wsh.Shell
         * @param {string} strText
         * @param {number} [nSecondsToWait]
         * @param {string} [strTitle]
         * @param {number} [nType] The value of {@link Wsh.Constants.buttonTypes} and The value of {@link Wsh.Constants.iconTypes}
         * @returns {number} - A value of {@link Wsh.Constants.enterCodes}
         */
        sh.Popup(logStr, CD.nonAutoClosing, MODULE_TITLE, CD.buttonTypes.ok + ico);
      } else if (this.transportation === 'WINEVENT') {
        os.writeLogEvent[this.logLevelResult](logStr);
      } else if (this.transportation === 'FILE') {
        try {
          fse.ensureDirSync(path.dirname(this.logPath));
          fs.writeFileSync(this.logPath, logStr, { encoding: this.encoding });
        } catch (e) {
          throw new Error(insp(e) + '\n'
              + '  at ' + functionName + ' (' + MODULE_TITLE + ')\n'
              + '  Failed to write the log "' + this.logPath + '"');
        }
      }

      this.clear();
    }; // }}}
  } // }}}

  // logger.create {{{
  /**
   * Creates the new instance of Wsh.Logger.
   *
   * @example
   * // Ex.1 Default: Output Console
   * var logger = Wsh.Logger.create();
   * // or `var logger = Wsh.Logger.create('info/console');`
   * // or `var logger = Wsh.Logger.create({ level: 'info', transportation: 'console' });`
   *
   * logger.error('Error message');
   * logger.warn('Warn message');
   * logger.success('Success message');
   * logger.info('Info message');
   *
   * logger.transport();
   * // Displays the following logs in Commandprompt
   * // [2020-03-02T15:31:01] error   Error message
   * // [2020-03-02T15:31:02] warn    Warn message
   * // [2020-03-02T15:31:03] success Success message
   * // [2020-03-02T15:31:04] info    Info message
   * @example
   * // Ex.2 Output Popup Window
   * var logger = Wsh.Logger.create('warn/popup');
   *
   * logger.error('Error message');
   * logger.warn('Warn message');
   * logger.success('Success message');
   * logger.info('Info message');
   *
   * logger.transport();
   * // Displays the following logs in a popup window
   * // [2020-03-02T15:31:01] error   Error message
   * // [2020-03-02T15:31:02] warn    Warn message
   * @example
   * // Ex.3 Output File
   * var logger = Wsh.Logger.create('success/foo_{yyyy-MM-dd}.log');
   *
   * logger.error('Error message');
   * logger.warn('Warn message');
   * logger.success('Success message');
   * logger.info('Info message');
   *
   * logger.transport();
   * // Writes the logs into %CD%\foo_2020-03-02.log
   * // [2020-03-02T15:31:01] error   Error message
   * // [2020-03-02T15:31:02] warn    Warn message
   * // [2020-03-02T15:31:03] success Success message
   * @example
   * // Ex.4 Output Windows Event Viewer
   * var logger = Wsh.Logger.create('error/winEvent');
   *
   * logger.error('Error message');
   * logger.warn('Warn message');
   * logger.success('Success message');
   * logger.info('Info message');
   *
   * logger.transport();
   * // Adds the following log on your Windows Event Viewer
   * // [2020-03-02T15:31:01] error   Error message
   * @function create
   * @memberof Wsh.Logger
   * @param {(string|typeLoggerCreateOptions|_Logger)} [options] - "`level`/`transportation`" or options or _Logger instance
   * @returns {_Logger} - _Logger instance
   */
  logger.create = function (options) {
    var functionName = 'logger.create';
    var level, transportation, encoding;

    if (options instanceof _Logger) return options;

    if (isSolidString(options)) {
      var args = options.split('/');

      if (args.length > 1) {
        level = args[0];
        transportation = args[1];
      } else {
        level = args[0];
      }

      return new _Logger({ level: level, transportation: transportation });
    }

    if (isPlainObject(options)) {
      level = obtain(options, 'level', null);
      transportation = obtain(options, 'transportation', null);
      encoding = obtain(options, 'encoding', CD.ado.charset.utf8);

      if (transportation instanceof _Logger) {
        if (isSolidString(level)) transportation.setLevel(level);
        return transportation;
      }

      return new _Logger({
        level: level,
        transportation: transportation,
        encoding: encoding
      });
    }

    if (!hasContent(options)) {
      return new _Logger({
        level: null, transportation: null, encoding: null
      });
    }

    throw new Error('Error [ERR_INVALID_ARG_TYPE]\n'
      + '  at ' + functionName + ' (' + MODULE_TITLE + ')\n'
      + '  options: ' + insp(options));
  }; // }}}
})();

// vim:set foldmethod=marker commentstring=//%s :