/* @flow */
import type {LintError, ConsoleStyleConfig, PostCSSWarning, NodeSassError} from './typedef';
import forEach from 'lodash/forEach';
import map from 'lodash/map';
import transform from 'lodash/transform';
import ErrorStackParser from 'error-stack-parser';
import cleanStack from 'clean-stack';
import {isNode} from './util';
import {relative} from 'path';
/* eslint-disable no-console */
let i = 0;
/**
* Dead-simple, composable, isomorphic, cross-browser wrapper for `console.log`.
*
* <style>
* .console {
* box-shadow: 0 6px 10px 0 rgba(0,0,0,0.14), 0 1px 18px 0 rgba(0,0,0,0.12), 0 3px 5px -1px rgba(0,0,0,0.3);
* font-family: Consolas, Monaco, 'Andale Mono', monospace;
* font-size: 18px;
* }
* .console, .console .line { padding: 5px; }
* .console .line .in { color: #909090; }
* .console .line .out { color: #b3b3b3; }
* .console .line .string { color: #c6211d; }
* .console .line .bold { font-weight: bold; }
* .console .line .underline { text-decoration: underline; }
* .console .line .red { color: red; }
* .console .line .green { color: green; }
* .console .line .blue { color: blue; }
* </style>
* <div class="console"><div class="line"><span class="in">></span> log(<span
* class="string">'Colorful '</span>, bold(red(<span class="string">'R'</span>), green(<span
* class="string">'G'</span>), blue(<span class="string">'B'</span>)), <span
* class="string">' logs are '</span>, underline(<span class="string">'very'</span>), <span
* class="string">' easy!'</span>);</div><div class="line"><span class="out"><</span> Colorful <span class="bold"><span
* class="red">R</span><span class="green">G</span><span class="blue">B</span></span> logs are <span
* class="underline">very</span> easy!</div></div>
*
* @module logger
*/
/**
* Provides the means for isomorphic styled output to the console.
*
* @class Message
* @memberof module:logger
* @protected
* @param {ConsoleStyleConfig} style - a style config
*/
export class Message {
/**
* a style config
*
* @member {ConsoleStyleConfig} style
* @memberof module:logger.Message
* @private
* @instance
*/
style: ConsoleStyleConfig;
/**
* a message string
*
* @member {string} message
* @memberof module:logger.Message
* @instance
*/
message: string = '';
/**
* css style strings
*
* @member {Array<string>} styles
* @memberof module:logger.Message
* @instance
*/
styles: string[] = [];
// eslint-disable-next-line require-jsdoc
constructor(style: ConsoleStyleConfig) {
this.style = style;
}
/**
* Adds a message to be printed to the console on Node.js
*
* @memberof module:logger.Message
* @instance
* @private
* @method addBEMessage
* @param {string | number | module:logger.Message} msg - a message, either plain text or the response if one of the
* style functions
*/
addBEMessage(msg: string | number | Message) {
const message = msg instanceof Message ? msg.message : msg,
[start, end] = this.style.ansi;
this.message += `${start}${message}${end}`;
}
/**
* Adds a Message instance to be printed to a browser console
*
* @memberof module:logger.Message
* @instance
* @private
* @method addFEMessageInstance
* @param {module:logger.Message} msg - a message object
*/
addFEMessageInstance(msg: Message) {
const {css} = this.style;
this.message += msg.message;
this.styles.push(...map(msg.styles, style => `${style}${css}`));
}
/**
* Adds a plain text message to be printed to a browser console
*
* @memberof module:logger.Message
* @instance
* @private
* @method addFEMessageString
* @param {string | number} msg - a plain text message
*/
addFEMessageString(msg: string | number) {
this.message += `%c${msg}`;
this.styles.push(this.style.css);
}
/**
* Adds a message
*
* @memberof module:logger.Message
* @instance
* @method addMessage
* @param {string | number | module:logger.Message} msg - a message
*/
addMessage(msg: string | number | Message) {
if (isNode) {
this.addBEMessage(msg);
} else if (msg instanceof Message) {
this.addFEMessageInstance(msg);
} else {
this.addFEMessageString(msg);
}
}
/**
* Adds messages
*
* @memberof module:logger.Message
* @instance
* @method addMessages
* @param {Array<string | number | module:logger.Message>} messages - a collection of messages
* @return {module:logger.Message} returns self
*/
addMessages(messages: Array<string | number | Message>): Message {
forEach(messages, message => this.addMessage(message));
return this;
}
}
/**
* @callback ConsoleStyle
* @memberof module:logger
* @param {...(string | number | module:logger.Message)} messages - messages to log
* @return {module:logger.Message} a styled message
*/
/**
* Console style helpers.
*
* @memberof module:logger
* @member {Object} consoleStyles
* @property {module:logger.ConsoleStyle} bold - bold text
* @property {module:logger.ConsoleStyle} dim - text dimmed by 20%
* @property {module:logger.ConsoleStyle} italic - italic text
* @property {module:logger.ConsoleStyle} underline - underlined text
* @property {module:logger.ConsoleStyle} inverse - inverted colors
* @property {module:logger.ConsoleStyle} hidden - invisible
* @property {module:logger.ConsoleStyle} strikethrough - strikethrough
* @property {module:logger.ConsoleStyle} black - black text color
* @property {module:logger.ConsoleStyle} red - red text color
* @property {module:logger.ConsoleStyle} green - green text color
* @property {module:logger.ConsoleStyle} yellow - yellow text color
* @property {module:logger.ConsoleStyle} blue - blue text color
* @property {module:logger.ConsoleStyle} magenta - magenta text color
* @property {module:logger.ConsoleStyle} cyan - cyan text color
* @property {module:logger.ConsoleStyle} white - white text color
* @property {module:logger.ConsoleStyle} gray - gray text color
* @property {module:logger.ConsoleStyle} grey - gray text color
* @property {module:logger.ConsoleStyle} bgBlack - black background color
* @property {module:logger.ConsoleStyle} bgRed - red background color
* @property {module:logger.ConsoleStyle} bgGreen - green background color
* @property {module:logger.ConsoleStyle} bgYellow - yellow background color
* @property {module:logger.ConsoleStyle} bgBlue - blue background color
* @property {module:logger.ConsoleStyle} bgMagenta - magenta background color
* @property {module:logger.ConsoleStyle} bgCyan - cyan background color
* @property {module:logger.ConsoleStyle} bgWhite - white background color
*/
export const consoleStyles = transform({
bold: {ansi: ['\u001b[1m', '\u001b[22m'], css: 'font-weight: bold;'},
dim: {ansi: ['\u001b[2m', '\u001b[22m'], css: 'opacity: .8;'},
italic: {ansi: ['\u001b[3m', '\u001b[23m'], css: 'font-style: italic;'},
underline: {ansi: ['\u001b[4m', '\u001b[24m'], css: 'text-decoration: underline;'},
inverse: {ansi: ['\u001b[7m', '\u001b[27m'],
css: '-moz-filter: invert(100%); -webkit-filter: invert(100%); filter: invert(100%);'},
hidden: {ansi: ['\u001b[8m', '\u001b[28m'], css: 'visibility: hidden;'},
strikethrough: {ansi: ['\u001b[9m', '\u001b[29m'], css: 'text-decoration: line-through;'},
black: {ansi: ['\u001b[30m', '\u001b[39m'], css: 'color: black;'},
red: {ansi: ['\u001b[31m', '\u001b[39m'], css: 'color: red;'},
green: {ansi: ['\u001b[32m', '\u001b[39m'], css: 'color: green;'},
yellow: {ansi: ['\u001b[33m', '\u001b[39m'], css: 'color: yellow;'},
blue: {ansi: ['\u001b[34m', '\u001b[39m'], css: 'color: blue;'},
magenta: {ansi: ['\u001b[35m', '\u001b[39m'], css: 'color: magenta;'},
cyan: {ansi: ['\u001b[36m', '\u001b[39m'], css: 'color: cyan;'},
white: {ansi: ['\u001b[37m', '\u001b[39m'], css: 'color: white;'},
gray: {ansi: ['\u001b[90m', '\u001b[39m'], css: 'color: gray;'},
grey: {ansi: ['\u001b[90m', '\u001b[39m'], css: 'color: gray;'},
bgBlack: {ansi: ['\u001b[40m', '\u001b[49m'], css: 'background-color: black;'},
bgRed: {ansi: ['\u001b[41m', '\u001b[49m'], css: 'background-color: red;'},
bgGreen: {ansi: ['\u001b[42m', '\u001b[49m'], css: 'background-color: green;'},
bgYellow: {ansi: ['\u001b[43m', '\u001b[49m'], css: 'background-color: yellow;'},
bgBlue: {ansi: ['\u001b[44m', '\u001b[49m'], css: 'background-color: blue;'},
bgMagenta: {ansi: ['\u001b[45m', '\u001b[49m'], css: 'background-color: magenta;'},
bgCyan: {ansi: ['\u001b[46m', '\u001b[49m'], css: 'background-color: cyan;'},
bgWhite: {ansi: ['\u001b[47m', '\u001b[49m'], css: 'background-color: white;'}
}, (result, value, key) => {
result[key] = (...messages) => new Message(value).addMessages(messages);
}, {});
/**
* Formats an error line with colors.
*
* @memberof module:logger
* @private
* @function formatLine
* @param {string} message - error message
* @param {string} file - offending file
* @param {number | string} line - offending line
* @param {number | string} column - offending column
* @return {Array<string | module:logger.Message>} styled messages
*/
export function formatLine(message: string, file: string, line: number | string,
column: number | string): Array<string | Message> {
const {yellow, cyan, magenta} = consoleStyles;
return ['"', yellow(message), '" in ', cyan(relative('', file)), ' on ', magenta(line), ':', magenta(column)];
}
/**
* Returns a string "Error" styled as a bold white text on a red background, ready to be printed to the console.
*
* @memberof module:logger
* @private
* @function formatErrorMarker
* @param {string} [message="Error"] - a message to apply styles to
* @return {module:logger.Message} a styled message
*/
export function formatErrorMarker(message: string = 'Error') {
const {bold, bgRed, white} = consoleStyles;
return bgRed(bold(white(message)));
}
/**
* Log colorful messages out to the console on Node.js as well as in a browser in the simplest and most composable way.
*
* @memberof module:logger
* @function log
* @param {...(string | number | module:logger.Message)} messages - messages to log
* @example
* import {log, consoleStyles} from 'webcompiler';
* // or - import {log, consoleStyles} from 'webcompiler/lib/logger';
* // or - var webcompiler = require('webcompiler');
* // var log = webcompiler.log;
* // var consoleStyles = webcompiler.consoleStyles;
* // or - var logger = require('webcompiler/lib/logger');
* // var log = logger.log;
* // var consoleStyles = logger.consoleStyles;
*
* const {red, green, blue, bold, underline} = consoleStyles;
*
* log('Colorful ', bold(red('R'), green('G'), blue('B')), ' logs are ', underline('very'), ' easy!');
*/
export function log(...messages: Array<string | number | Message>) {
const {message, styles} = new Message({ansi: ['', ''], css: ''}).addMessages(messages);
console.log(message, ...styles);
}
/**
* Prints an Error object out to the console.
*
* Removes irrelevant entries from its stack trace that point to Node.js system files that you would never debug anyway
* and hence are irrelevant.
*
* @memberof module:logger
* @function logError
* @param {Error} error - an error object
* @example
* import {logError} from 'webcompiler';
* // or - import {logError} from 'webcompiler/lib/logger';
* // or - var logError = require('webcompiler').logError;
* // or - var logError = require('webcompiler/lib/logger').logError;
*
* logError(new Error('Some error message'));
*/
export function logError(error: Error) {
const {name, message, stack} = error;
log(formatErrorMarker(name), ': ', message);
error.stack = cleanStack(stack);
forEach(ErrorStackParser.parse(error), ({functionName = 'unknown', fileName, lineNumber, columnNumber}) => {
log(' • ', ...formatLine(functionName, fileName, lineNumber, columnNumber));
});
}
/**
* Prints PostCSS Warning objects out to the console.
*
* @memberof module:logger
* @function logPostCSSWarnings
* @param {Array<PostCSSWarning>} warnings - warning objects
* @example
* import {logPostCSSWarnings} from 'webcompiler';
* // or - import {logPostCSSWarnings} from 'webcompiler/lib/logger';
* // or - var logPostCSSWarnings = require('webcompiler').logPostCSSWarnings;
* // or - var logPostCSSWarnings = require('webcompiler/lib/logger').logPostCSSWarnings;
* import postcss from 'postcss';
*
* postcss(...).process(...).then(result => {
* const warnings = result.warnings();
*
* if (warnings.length) {
* return logPostCSSWarnings(warnings);
* }
* ...
* });
*/
export function logPostCSSWarnings(warnings: PostCSSWarning[]) {
forEach(warnings, ({text, plugin, node: {source: {input: {file}}}, line, column}) => {
log(formatErrorMarker('Warning'), ': ', ...formatLine(`${text}(${plugin})`, file, line, column));
});
log('PostCSS warnings: ', warnings.length);
}
/**
* Prints a Node SASS error object out to the console.
*
* @memberof module:logger
* @function logSASSError
* @param {NodeSassError} error - error object
* @example
* import {logSASSError} from 'webcompiler';
* // or - import {logSASSError} from 'webcompiler/lib/logger';
* // or - var logSASSError = require('webcompiler').logSASSError;
* // or - var logSASSError = require('webcompiler/lib/logger').logSASSError;
* import {render} from 'node-sass';
*
* render(..., (error, result) => {
* if (error) {
* return logSASSError(error);
* }
* ...
* });
*/
export function logSASSError({message, file, line, column}: NodeSassError) {
log(formatErrorMarker('SASS error'), ': ', ...formatLine(message, file, line, column));
}
/**
* Prints linting errors out to the console.
*
* @memberof module:logger
* @function logLintingErrors
* @param {Array<LintError>} errors - error objects
* @param {string} [prefix=null] - will be printed on the last line along with the total number of messages
* @example
* import {logLintingErrors} from 'webcompiler';
* // or - import {logLintingErrors} from 'webcompiler/lib/logger';
* // or - var logLintingErrors = require('webcompiler').logLintingErrors;
* // or - var logLintingErrors = require('webcompiler/lib/logger').logLintingErrors;
*/
export function logLintingErrors(errors: LintError[], prefix: ?string = null) {
forEach(errors, ({message, rule, file, line, column}) => {
log(formatErrorMarker(), ': ', ...formatLine(`${message}${rule ? ` (${rule})` : ''}`, file, line, column));
});
log(prefix ? `${prefix} l` : 'L', 'inting errors: ', errors.length);
}
/**
* Within the current process logs green colored messages out to the console prepending a sequential number starting
* with "1." to the message, to make it easier to distinguish messages containing the same text.
*
* @memberof module:logger
* @function logSequentialSuccessMessage
* @param {string} message - a message to log
* @example
* import {logSequentialSuccessMessage} from 'webcompiler';
* // or - import {logSequentialSuccessMessage} from 'webcompiler/lib/logger';
* // or - var logSequentialSuccessMessage = require('webcompiler').logSequentialSuccessMessage;
* // or - var logSequentialSuccessMessage = require('webcompiler/lib/logger').logSequentialSuccessMessage;
*/
export function logSequentialSuccessMessage(message: string) {
const {green} = consoleStyles;
log(green(++i, '. ', message));
}