blob: 52d34edd7236005f5748193cde3e42e14313758d [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001/*
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.
13var util = require('util');
14
15// External libs.
16var hooker = require('hooker');
17// Requiring this here modifies the String prototype!
18var colors = require('colors');
19var _ = require('lodash');
20// TODO: ADD CHALK
21
22var logUtils = require('grunt-legacy-log-utils');
23
24function 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}
80exports.Log = Log;
81
82// Am I doing it wrong? :P
83function 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}
89util.inherits(VerboseLog, Log);
90
91VerboseLog.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.
100function 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}
116makeSmartAccessor('options');
117makeSmartAccessor('hasLogged');
118makeSmartAccessor('muted', true);
119
120// Disable colors if --no-colors was passed.
121Log.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.
137Log.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.
146Log.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.
157Log.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
165Log.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
178Log.prototype._writeln = function(msg) {
179 // Write blank line if no msg is passed in.
180 this._write((msg || '') + '\n');
181};
182
183// Write output.
184Log.prototype.write = function() {
185 this._write(this._format(arguments));
186 return this;
187};
188
189// Write a line of output.
190Log.prototype.writeln = function() {
191 this._writeln(this._format(arguments));
192 return this;
193};
194
195Log.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};
204Log.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};
211Log.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};
220Log.prototype.errorlns = function() {
221 var msg = this._format(arguments);
222 this.error(this.wraptext(this.options.maxCols || 77, msg));
223 return this;
224};
225Log.prototype.oklns = function() {
226 var msg = this._format(arguments);
227 this.ok(this.wraptext(this.options.maxCols || 77, msg));
228 return this;
229};
230Log.prototype.success = function() {
231 var msg = this._format(arguments);
232 this._writeln(msg.green);
233 return this;
234};
235Log.prototype.fail = function() {
236 var msg = this._format(arguments);
237 this._writeln(msg.red);
238 return this;
239};
240Log.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};
247Log.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.
255Log.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.
264Log.prototype.writetableln = function(widths, texts) {
265 this._writeln(this.table(widths, texts));
266 return this;
267};
268
269// Wrap a long line of text.
270Log.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.
277Log.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});