ForestAdmin/toolbelt

View on GitHub
src/services/logger.js

Summary

Maintainability
A
35 mins
Test Coverage
A
100%
const chalk = require('chalk');

const ALLOWED_OPTION_KEYS = ['color', 'prefix', 'std', 'lineColor'];
const DEFAULT_OPTION_VALUES = ALLOWED_OPTION_KEYS.reduce((options, key) => {
  options[key] = undefined;
  return options;
}, {});

class Logger {
  constructor({ assertPresent, env, stderr, stdout }) {
    assertPresent({ env, stderr, stdout });
    this.env = env;
    this.stderr = stderr;
    this.stdout = stdout;

    // FIXME: Silent was not used before as no "silent" value was in context.
    this.silent = !!this.env.SILENT && this.env.SILENT !== '0';
  }

  _logLine(message, options) {
    if (this.silent) return;

    options = {
      ...DEFAULT_OPTION_VALUES,
      ...options,
    };

    let actualPrefix = '';
    if ([undefined, null, ''].indexOf(options.prefix) === -1) actualPrefix = `${options.prefix} `;
    if (actualPrefix && options.color) {
      actualPrefix = Logger._setBoldColor(options.color, actualPrefix);
    }

    let actualMessage = Logger._stringifyIfObject(message);
    if (options.lineColor) {
      actualMessage = `${Logger._setColor(options.lineColor, actualMessage)}`;
    }
    actualMessage = `${actualPrefix}${actualMessage}\n`;

    if (options.std === 'err') {
      this.stderr.write(actualMessage);
    } else {
      this.stdout.write(actualMessage);
    }
  }

  _logLines(messagesWithPotentialGivenOptions, baseOptions) {
    const { options, messages } = Logger._extractGivenOptionsFromMessages(
      messagesWithPotentialGivenOptions,
    );
    messages.forEach(message => this._logLine(message, { ...baseOptions, ...options }));
  }

  static _stringifyIfObject(message) {
    if (typeof message === 'object') {
      return JSON.stringify(message);
    }

    return message;
  }

  static _setColor(color, message) {
    return chalk[color](message);
  }

  static _setBoldColor(color, message) {
    return chalk.bold[color](message);
  }

  static _isObjectKeysMatchAlwaysTheGivenKeys(object) {
    if (typeof object !== 'object') {
      return false;
    }

    return Object.keys(object).every(key => ALLOWED_OPTION_KEYS.includes(key));
  }

  // This is a hack to keep the current signature of Logger methods.
  // Last `message` is considered an option object if its keys are in `ALLOWED_OPTION_KEYS`.
  static _extractGivenOptionsFromMessages(messages) {
    let options = {};

    const potentialGivenOptions = messages[messages.length - 1];
    const hasOptions = Logger._isObjectKeysMatchAlwaysTheGivenKeys(potentialGivenOptions);

    if (hasOptions) {
      messages = messages.slice(0, -1);
      options = { ...options, ...potentialGivenOptions };
    }

    return { messages, options };
  }

  /**
   *  Allows to log one ore more messages, with option object as last optional parameter.
   *  @example logger.log('message')
   *  @example logger.log('message', { color: 'blue', colorLine: 'green' })
   *  @example logger.log('message 1', 'message 2')
   *  @example logger.log('message 1', 'message 2',  { color: 'blue', colorLine: 'green' })
   */
  log(...messagesAndOptions) {
    this._logLines(messagesAndOptions);
  }

  error(...messagesAndOptions) {
    this._logLines(messagesAndOptions, { color: 'red', prefix: '×', std: 'err' });
  }

  info(...messagesAndOptions) {
    this._logLines(messagesAndOptions, { color: 'blue', prefix: '>' });
  }

  success(...messagesAndOptions) {
    this._logLines(messagesAndOptions, { color: 'green', prefix: '√' });
  }

  warn(...messagesAndOptions) {
    this._logLines(messagesAndOptions, { color: 'yellow', prefix: 'Δ' });
  }
}

if (process.env.NODE_ENV === 'test') {
  Logger.DEFAULT_OPTION_VALUES = DEFAULT_OPTION_VALUES;
}
module.exports = Logger;