| Leo Repp | 58b9f11 | 2021-11-22 11:57:47 +0100 | [diff] [blame^] | 1 | /* |
| 2 | * grunt |
| 3 | * http://gruntjs.com/ |
| 4 | * |
| 5 | * Copyright (c) 2018 "Cowboy" Ben Alman |
| 6 | * Licensed under the MIT license. |
| 7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT |
| 8 | */ |
| 9 | |
| 10 | 'use strict'; |
| 11 | |
| 12 | // Nodejs libs. |
| 13 | var util = require('util'); |
| 14 | |
| 15 | // External libs. |
| 16 | var hooker = require('hooker'); |
| 17 | // Requiring this here modifies the String prototype! |
| 18 | var colors = require('colors'); |
| 19 | var _ = require('lodash'); |
| 20 | // TODO: ADD CHALK |
| 21 | |
| 22 | var logUtils = require('grunt-legacy-log-utils'); |
| 23 | |
| 24 | function Log(options) { |
| 25 | // This property always refers to the "base" logger. |
| 26 | this.always = this; |
| 27 | // Extend options. |
| 28 | this.options = _.extend({}, { |
| 29 | // Show colors in output? |
| 30 | color: true, |
| 31 | // Enable verbose-mode logging? |
| 32 | verbose: false, |
| 33 | // Enable debug logging statement? |
| 34 | debug: false, |
| 35 | // Where should messages be output? |
| 36 | outStream: process.stdout, |
| 37 | // NOTE: the color, verbose, debug options will be ignored if the |
| 38 | // "grunt" option is specified! See the Log.prototype.option and |
| 39 | // the Log.prototype.error methods for more info. |
| 40 | grunt: null, |
| 41 | // Where should output wrap? If null, use legacy Grunt defaults. |
| 42 | maxCols: null, |
| 43 | // Should logger start muted? |
| 44 | muted: false, |
| 45 | }, options); |
| 46 | // True once anything has actually been logged. |
| 47 | this.hasLogged = false; |
| 48 | |
| 49 | // Related verbose / notverbose loggers. |
| 50 | this.verbose = new VerboseLog(this, true); |
| 51 | this.notverbose = new VerboseLog(this, false); |
| 52 | this.verbose.or = this.notverbose; |
| 53 | this.notverbose.or = this.verbose; |
| 54 | |
| 55 | // Apparently, people have using grunt.log in interesting ways. Just bind |
| 56 | // all methods so that "this" is irrelevant. |
| 57 | if (this.options.grunt) { |
| 58 | var properties = [ |
| 59 | 'write', |
| 60 | 'writeln', |
| 61 | 'writetableln', |
| 62 | 'writelns', |
| 63 | 'writeflags', |
| 64 | 'warn', |
| 65 | 'error', |
| 66 | 'ok', |
| 67 | 'errorlns', |
| 68 | 'oklns', |
| 69 | 'success', |
| 70 | 'fail', |
| 71 | 'header', |
| 72 | 'subhead', |
| 73 | 'debug' |
| 74 | ]; |
| 75 | _.bindAll(this, properties); |
| 76 | _.bindAll(this.verbose, properties); |
| 77 | _.bindAll(this.notverbose, properties); |
| 78 | } |
| 79 | } |
| 80 | exports.Log = Log; |
| 81 | |
| 82 | // Am I doing it wrong? :P |
| 83 | function VerboseLog(parentLog, verbose) { |
| 84 | // Keep track of the original, base "Log" instance. |
| 85 | this.always = parentLog; |
| 86 | // This logger is either verbose (true) or notverbose (false). |
| 87 | this._isVerbose = verbose; |
| 88 | } |
| 89 | util.inherits(VerboseLog, Log); |
| 90 | |
| 91 | VerboseLog.prototype._write = function() { |
| 92 | // Abort if not in correct verbose mode. |
| 93 | if (Boolean(this.option('verbose')) !== this._isVerbose) { return; } |
| 94 | // Otherwise... log! |
| 95 | return VerboseLog.super_.prototype._write.apply(this, arguments); |
| 96 | }; |
| 97 | |
| 98 | // Create read/write accessors that prefer the parent log's properties (in |
| 99 | // the case of verbose/notverbose) to the current log's properties. |
| 100 | function makeSmartAccessor(name, isOption) { |
| 101 | Object.defineProperty(Log.prototype, name, { |
| 102 | enumerable: true, |
| 103 | configurable: true, |
| 104 | get: function() { |
| 105 | return isOption ? this.always._options[name] : this.always['_' + name]; |
| 106 | }, |
| 107 | set: function(value) { |
| 108 | if (isOption) { |
| 109 | this.always._options[name] = value; |
| 110 | } else { |
| 111 | this.always['_' + name] = value; |
| 112 | } |
| 113 | }, |
| 114 | }); |
| 115 | } |
| 116 | makeSmartAccessor('options'); |
| 117 | makeSmartAccessor('hasLogged'); |
| 118 | makeSmartAccessor('muted', true); |
| 119 | |
| 120 | // Disable colors if --no-colors was passed. |
| 121 | Log.prototype.initColors = function() { |
| 122 | if (this.option('no-color')) { |
| 123 | // String color getters should just return the string. |
| 124 | colors.mode = 'none'; |
| 125 | // Strip colors from strings passed to console.log. |
| 126 | hooker.hook(console, 'log', function() { |
| 127 | var args = _.toArray(arguments); |
| 128 | return hooker.filter(this, args.map(function(arg) { |
| 129 | return typeof arg === 'string' ? colors.stripColors(arg) : arg; |
| 130 | })); |
| 131 | }); |
| 132 | } |
| 133 | }; |
| 134 | |
| 135 | // Check for color, verbose, debug options through Grunt if specified, |
| 136 | // otherwise defer to options object properties. |
| 137 | Log.prototype.option = function(name) { |
| 138 | if (this.options.grunt && this.options.grunt.option) { |
| 139 | return this.options.grunt.option(name); |
| 140 | } |
| 141 | var no = name.match(/^no-(.+)$/); |
| 142 | return no ? !this.options[no[1]] : this.options[name]; |
| 143 | }; |
| 144 | |
| 145 | // Parse certain markup in strings to be logged. |
| 146 | Log.prototype._markup = function(str) { |
| 147 | str = str || ''; |
| 148 | // Make _foo_ underline. |
| 149 | str = str.replace(/(\s|^)_(\S|\S[\s\S]+?\S)_(?=[\s,.!?]|$)/g, '$1' + '$2'.underline); |
| 150 | // Make *foo* bold. |
| 151 | str = str.replace(/(\s|^)\*(\S|\S[\s\S]+?\S)\*(?=[\s,.!?]|$)/g, '$1' + '$2'.bold); |
| 152 | return str; |
| 153 | }; |
| 154 | |
| 155 | // Similar to util.format in the standard library, however it'll always |
| 156 | // convert the first argument to a string and treat it as the format string. |
| 157 | Log.prototype._format = function(args) { |
| 158 | args = _.toArray(args); |
| 159 | if (args.length > 0) { |
| 160 | args[0] = String(args[0]); |
| 161 | } |
| 162 | return util.format.apply(util, args); |
| 163 | }; |
| 164 | |
| 165 | Log.prototype._write = function(msg) { |
| 166 | // Abort if muted. |
| 167 | if (this.muted) { return; } |
| 168 | // Actually write output. |
| 169 | this.hasLogged = true; |
| 170 | msg = msg || ''; |
| 171 | // Users should probably use the colors-provided methods, but if they |
| 172 | // don't, this should strip extraneous color codes. |
| 173 | if (this.option('no-color')) { msg = colors.stripColors(msg); } |
| 174 | // Actually write to stdout. |
| 175 | this.options.outStream.write(this._markup(msg)); |
| 176 | }; |
| 177 | |
| 178 | Log.prototype._writeln = function(msg) { |
| 179 | // Write blank line if no msg is passed in. |
| 180 | this._write((msg || '') + '\n'); |
| 181 | }; |
| 182 | |
| 183 | // Write output. |
| 184 | Log.prototype.write = function() { |
| 185 | this._write(this._format(arguments)); |
| 186 | return this; |
| 187 | }; |
| 188 | |
| 189 | // Write a line of output. |
| 190 | Log.prototype.writeln = function() { |
| 191 | this._writeln(this._format(arguments)); |
| 192 | return this; |
| 193 | }; |
| 194 | |
| 195 | Log.prototype.warn = function() { |
| 196 | var msg = this._format(arguments); |
| 197 | if (arguments.length > 0) { |
| 198 | this._writeln('>> '.red + _.trim(msg).replace(/\n/g, '\n>> '.red)); |
| 199 | } else { |
| 200 | this._writeln('ERROR'.red); |
| 201 | } |
| 202 | return this; |
| 203 | }; |
| 204 | Log.prototype.error = function() { |
| 205 | if (this.options.grunt && this.options.grunt.fail) { |
| 206 | this.options.grunt.fail.errorcount++; |
| 207 | } |
| 208 | this.warn.apply(this, arguments); |
| 209 | return this; |
| 210 | }; |
| 211 | Log.prototype.ok = function() { |
| 212 | var msg = this._format(arguments); |
| 213 | if (arguments.length > 0) { |
| 214 | this._writeln('>> '.green + _.trim(msg).replace(/\n/g, '\n>> '.green)); |
| 215 | } else { |
| 216 | this._writeln('OK'.green); |
| 217 | } |
| 218 | return this; |
| 219 | }; |
| 220 | Log.prototype.errorlns = function() { |
| 221 | var msg = this._format(arguments); |
| 222 | this.error(this.wraptext(this.options.maxCols || 77, msg)); |
| 223 | return this; |
| 224 | }; |
| 225 | Log.prototype.oklns = function() { |
| 226 | var msg = this._format(arguments); |
| 227 | this.ok(this.wraptext(this.options.maxCols || 77, msg)); |
| 228 | return this; |
| 229 | }; |
| 230 | Log.prototype.success = function() { |
| 231 | var msg = this._format(arguments); |
| 232 | this._writeln(msg.green); |
| 233 | return this; |
| 234 | }; |
| 235 | Log.prototype.fail = function() { |
| 236 | var msg = this._format(arguments); |
| 237 | this._writeln(msg.red); |
| 238 | return this; |
| 239 | }; |
| 240 | Log.prototype.header = function() { |
| 241 | var msg = this._format(arguments); |
| 242 | // Skip line before header, but not if header is the very first line output. |
| 243 | if (this.hasLogged) { this._writeln(); } |
| 244 | this._writeln(msg.underline); |
| 245 | return this; |
| 246 | }; |
| 247 | Log.prototype.subhead = function() { |
| 248 | var msg = this._format(arguments); |
| 249 | // Skip line before subhead, but not if subhead is the very first line output. |
| 250 | if (this.hasLogged) { this._writeln(); } |
| 251 | this._writeln(msg.bold); |
| 252 | return this; |
| 253 | }; |
| 254 | // For debugging. |
| 255 | Log.prototype.debug = function() { |
| 256 | var msg = this._format(arguments); |
| 257 | if (this.option('debug')) { |
| 258 | this._writeln('[D] ' + msg.magenta); |
| 259 | } |
| 260 | return this; |
| 261 | }; |
| 262 | |
| 263 | // Write a line of a table. |
| 264 | Log.prototype.writetableln = function(widths, texts) { |
| 265 | this._writeln(this.table(widths, texts)); |
| 266 | return this; |
| 267 | }; |
| 268 | |
| 269 | // Wrap a long line of text. |
| 270 | Log.prototype.writelns = function() { |
| 271 | var msg = this._format(arguments); |
| 272 | this._writeln(this.wraptext(this.options.maxCols || 80, msg)); |
| 273 | return this; |
| 274 | }; |
| 275 | |
| 276 | // Display flags in verbose mode. |
| 277 | Log.prototype.writeflags = function(obj, prefix) { |
| 278 | var wordlist; |
| 279 | if (Array.isArray(obj)) { |
| 280 | wordlist = this.wordlist(obj); |
| 281 | } else if (typeof obj === 'object' && obj) { |
| 282 | wordlist = this.wordlist(Object.keys(obj).map(function(key) { |
| 283 | var val = obj[key]; |
| 284 | return key + (val === true ? '' : '=' + JSON.stringify(val)); |
| 285 | })); |
| 286 | } |
| 287 | this._writeln((prefix || 'Flags') + ': ' + (wordlist || '(none)'.cyan)); |
| 288 | return this; |
| 289 | }; |
| 290 | |
| 291 | // Add static methods. |
| 292 | [ |
| 293 | 'wordlist', |
| 294 | 'uncolor', |
| 295 | 'wraptext', |
| 296 | 'table', |
| 297 | ].forEach(function(prop) { |
| 298 | Log.prototype[prop] = exports[prop] = logUtils[prop]; |
| 299 | }); |