/* * grunt * http://gruntjs.com/ * * Copyright (c) 2018 "Cowboy" Ben Alman * Licensed under the MIT license. * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT */ 'use strict'; // Nodejs libs. var util = require('util'); // External libs. var hooker = require('hooker'); // Requiring this here modifies the String prototype! var colors = require('colors'); var _ = require('lodash'); // TODO: ADD CHALK var logUtils = require('grunt-legacy-log-utils'); function Log(options) { // This property always refers to the "base" logger. this.always = this; // Extend options. this.options = _.extend({}, { // Show colors in output? color: true, // Enable verbose-mode logging? verbose: false, // Enable debug logging statement? debug: false, // Where should messages be output? outStream: process.stdout, // NOTE: the color, verbose, debug options will be ignored if the // "grunt" option is specified! See the Log.prototype.option and // the Log.prototype.error methods for more info. grunt: null, // Where should output wrap? If null, use legacy Grunt defaults. maxCols: null, // Should logger start muted? muted: false, }, options); // True once anything has actually been logged. this.hasLogged = false; // Related verbose / notverbose loggers. this.verbose = new VerboseLog(this, true); this.notverbose = new VerboseLog(this, false); this.verbose.or = this.notverbose; this.notverbose.or = this.verbose; // Apparently, people have using grunt.log in interesting ways. Just bind // all methods so that "this" is irrelevant. if (this.options.grunt) { var properties = [ 'write', 'writeln', 'writetableln', 'writelns', 'writeflags', 'warn', 'error', 'ok', 'errorlns', 'oklns', 'success', 'fail', 'header', 'subhead', 'debug' ]; _.bindAll(this, properties); _.bindAll(this.verbose, properties); _.bindAll(this.notverbose, properties); } } exports.Log = Log; // Am I doing it wrong? :P function VerboseLog(parentLog, verbose) { // Keep track of the original, base "Log" instance. this.always = parentLog; // This logger is either verbose (true) or notverbose (false). this._isVerbose = verbose; } util.inherits(VerboseLog, Log); VerboseLog.prototype._write = function() { // Abort if not in correct verbose mode. if (Boolean(this.option('verbose')) !== this._isVerbose) { return; } // Otherwise... log! return VerboseLog.super_.prototype._write.apply(this, arguments); }; // Create read/write accessors that prefer the parent log's properties (in // the case of verbose/notverbose) to the current log's properties. function makeSmartAccessor(name, isOption) { Object.defineProperty(Log.prototype, name, { enumerable: true, configurable: true, get: function() { return isOption ? this.always._options[name] : this.always['_' + name]; }, set: function(value) { if (isOption) { this.always._options[name] = value; } else { this.always['_' + name] = value; } }, }); } makeSmartAccessor('options'); makeSmartAccessor('hasLogged'); makeSmartAccessor('muted', true); // Disable colors if --no-colors was passed. Log.prototype.initColors = function() { if (this.option('no-color')) { // String color getters should just return the string. colors.mode = 'none'; // Strip colors from strings passed to console.log. hooker.hook(console, 'log', function() { var args = _.toArray(arguments); return hooker.filter(this, args.map(function(arg) { return typeof arg === 'string' ? colors.stripColors(arg) : arg; })); }); } }; // Check for color, verbose, debug options through Grunt if specified, // otherwise defer to options object properties. Log.prototype.option = function(name) { if (this.options.grunt && this.options.grunt.option) { return this.options.grunt.option(name); } var no = name.match(/^no-(.+)$/); return no ? !this.options[no[1]] : this.options[name]; }; // Parse certain markup in strings to be logged. Log.prototype._markup = function(str) { str = str || ''; // Make _foo_ underline. str = str.replace(/(\s|^)_(\S|\S[\s\S]+?\S)_(?=[\s,.!?]|$)/g, '$1' + '$2'.underline); // Make *foo* bold. str = str.replace(/(\s|^)\*(\S|\S[\s\S]+?\S)\*(?=[\s,.!?]|$)/g, '$1' + '$2'.bold); return str; }; // Similar to util.format in the standard library, however it'll always // convert the first argument to a string and treat it as the format string. Log.prototype._format = function(args) { args = _.toArray(args); if (args.length > 0) { args[0] = String(args[0]); } return util.format.apply(util, args); }; Log.prototype._write = function(msg) { // Abort if muted. if (this.muted) { return; } // Actually write output. this.hasLogged = true; msg = msg || ''; // Users should probably use the colors-provided methods, but if they // don't, this should strip extraneous color codes. if (this.option('no-color')) { msg = colors.stripColors(msg); } // Actually write to stdout. this.options.outStream.write(this._markup(msg)); }; Log.prototype._writeln = function(msg) { // Write blank line if no msg is passed in. this._write((msg || '') + '\n'); }; // Write output. Log.prototype.write = function() { this._write(this._format(arguments)); return this; }; // Write a line of output. Log.prototype.writeln = function() { this._writeln(this._format(arguments)); return this; }; Log.prototype.warn = function() { var msg = this._format(arguments); if (arguments.length > 0) { this._writeln('>> '.red + _.trim(msg).replace(/\n/g, '\n>> '.red)); } else { this._writeln('ERROR'.red); } return this; }; Log.prototype.error = function() { if (this.options.grunt && this.options.grunt.fail) { this.options.grunt.fail.errorcount++; } this.warn.apply(this, arguments); return this; }; Log.prototype.ok = function() { var msg = this._format(arguments); if (arguments.length > 0) { this._writeln('>> '.green + _.trim(msg).replace(/\n/g, '\n>> '.green)); } else { this._writeln('OK'.green); } return this; }; Log.prototype.errorlns = function() { var msg = this._format(arguments); this.error(this.wraptext(this.options.maxCols || 77, msg)); return this; }; Log.prototype.oklns = function() { var msg = this._format(arguments); this.ok(this.wraptext(this.options.maxCols || 77, msg)); return this; }; Log.prototype.success = function() { var msg = this._format(arguments); this._writeln(msg.green); return this; }; Log.prototype.fail = function() { var msg = this._format(arguments); this._writeln(msg.red); return this; }; Log.prototype.header = function() { var msg = this._format(arguments); // Skip line before header, but not if header is the very first line output. if (this.hasLogged) { this._writeln(); } this._writeln(msg.underline); return this; }; Log.prototype.subhead = function() { var msg = this._format(arguments); // Skip line before subhead, but not if subhead is the very first line output. if (this.hasLogged) { this._writeln(); } this._writeln(msg.bold); return this; }; // For debugging. Log.prototype.debug = function() { var msg = this._format(arguments); if (this.option('debug')) { this._writeln('[D] ' + msg.magenta); } return this; }; // Write a line of a table. Log.prototype.writetableln = function(widths, texts) { this._writeln(this.table(widths, texts)); return this; }; // Wrap a long line of text. Log.prototype.writelns = function() { var msg = this._format(arguments); this._writeln(this.wraptext(this.options.maxCols || 80, msg)); return this; }; // Display flags in verbose mode. Log.prototype.writeflags = function(obj, prefix) { var wordlist; if (Array.isArray(obj)) { wordlist = this.wordlist(obj); } else if (typeof obj === 'object' && obj) { wordlist = this.wordlist(Object.keys(obj).map(function(key) { var val = obj[key]; return key + (val === true ? '' : '=' + JSON.stringify(val)); })); } this._writeln((prefix || 'Flags') + ': ' + (wordlist || '(none)'.cyan)); return this; }; // Add static methods. [ 'wordlist', 'uncolor', 'wraptext', 'table', ].forEach(function(prop) { Log.prototype[prop] = exports[prop] = logUtils[prop]; });