UNPKG

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