1'use strict' 2var Progress = require('are-we-there-yet') 3var Gauge = require('gauge') 4var EE = require('events').EventEmitter 5var log = exports = module.exports = new EE() 6var util = require('util') 7 8var setBlocking = require('set-blocking') 9var consoleControl = require('console-control-strings') 10 11setBlocking(true) 12var stream = process.stderr 13Object.defineProperty(log, 'stream', { 14 set: function (newStream) { 15 stream = newStream 16 if (this.gauge) { 17 this.gauge.setWriteTo(stream, stream) 18 } 19 }, 20 get: function () { 21 return stream 22 }, 23}) 24 25// by default, decide based on tty-ness. 26var colorEnabled 27log.useColor = function () { 28 return colorEnabled != null ? colorEnabled : stream.isTTY 29} 30 31log.enableColor = function () { 32 colorEnabled = true 33 this.gauge.setTheme({ hasColor: colorEnabled, hasUnicode: unicodeEnabled }) 34} 35log.disableColor = function () { 36 colorEnabled = false 37 this.gauge.setTheme({ hasColor: colorEnabled, hasUnicode: unicodeEnabled }) 38} 39 40// default level 41log.level = 'info' 42 43log.gauge = new Gauge(stream, { 44 enabled: false, // no progress bars unless asked 45 theme: { hasColor: log.useColor() }, 46 template: [ 47 { type: 'progressbar', length: 20 }, 48 { type: 'activityIndicator', kerning: 1, length: 1 }, 49 { type: 'section', default: '' }, 50 ':', 51 { type: 'logline', kerning: 1, default: '' }, 52 ], 53}) 54 55log.tracker = new Progress.TrackerGroup() 56 57// we track this separately as we may need to temporarily disable the 58// display of the status bar for our own loggy purposes. 59log.progressEnabled = log.gauge.isEnabled() 60 61var unicodeEnabled 62 63log.enableUnicode = function () { 64 unicodeEnabled = true 65 this.gauge.setTheme({ hasColor: this.useColor(), hasUnicode: unicodeEnabled }) 66} 67 68log.disableUnicode = function () { 69 unicodeEnabled = false 70 this.gauge.setTheme({ hasColor: this.useColor(), hasUnicode: unicodeEnabled }) 71} 72 73log.setGaugeThemeset = function (themes) { 74 this.gauge.setThemeset(themes) 75} 76 77log.setGaugeTemplate = function (template) { 78 this.gauge.setTemplate(template) 79} 80 81log.enableProgress = function () { 82 if (this.progressEnabled || this._paused) { 83 return 84 } 85 86 this.progressEnabled = true 87 this.tracker.on('change', this.showProgress) 88 this.gauge.enable() 89} 90 91log.disableProgress = function () { 92 if (!this.progressEnabled) { 93 return 94 } 95 this.progressEnabled = false 96 this.tracker.removeListener('change', this.showProgress) 97 this.gauge.disable() 98} 99 100var trackerConstructors = ['newGroup', 'newItem', 'newStream'] 101 102var mixinLog = function (tracker) { 103 // mixin the public methods from log into the tracker 104 // (except: conflicts and one's we handle specially) 105 Object.keys(log).forEach(function (P) { 106 if (P[0] === '_') { 107 return 108 } 109 110 if (trackerConstructors.filter(function (C) { 111 return C === P 112 }).length) { 113 return 114 } 115 116 if (tracker[P]) { 117 return 118 } 119 120 if (typeof log[P] !== 'function') { 121 return 122 } 123 124 var func = log[P] 125 tracker[P] = function () { 126 return func.apply(log, arguments) 127 } 128 }) 129 // if the new tracker is a group, make sure any subtrackers get 130 // mixed in too 131 if (tracker instanceof Progress.TrackerGroup) { 132 trackerConstructors.forEach(function (C) { 133 var func = tracker[C] 134 tracker[C] = function () { 135 return mixinLog(func.apply(tracker, arguments)) 136 } 137 }) 138 } 139 return tracker 140} 141 142// Add tracker constructors to the top level log object 143trackerConstructors.forEach(function (C) { 144 log[C] = function () { 145 return mixinLog(this.tracker[C].apply(this.tracker, arguments)) 146 } 147}) 148 149log.clearProgress = function (cb) { 150 if (!this.progressEnabled) { 151 return cb && process.nextTick(cb) 152 } 153 154 this.gauge.hide(cb) 155} 156 157log.showProgress = function (name, completed) { 158 if (!this.progressEnabled) { 159 return 160 } 161 162 var values = {} 163 if (name) { 164 values.section = name 165 } 166 167 var last = log.record[log.record.length - 1] 168 if (last) { 169 values.subsection = last.prefix 170 var disp = log.disp[last.level] || last.level 171 var logline = this._format(disp, log.style[last.level]) 172 if (last.prefix) { 173 logline += ' ' + this._format(last.prefix, this.prefixStyle) 174 } 175 176 logline += ' ' + last.message.split(/\r?\n/)[0] 177 values.logline = logline 178 } 179 values.completed = completed || this.tracker.completed() 180 this.gauge.show(values) 181}.bind(log) // bind for use in tracker's on-change listener 182 183// temporarily stop emitting, but don't drop 184log.pause = function () { 185 this._paused = true 186 if (this.progressEnabled) { 187 this.gauge.disable() 188 } 189} 190 191log.resume = function () { 192 if (!this._paused) { 193 return 194 } 195 196 this._paused = false 197 198 var b = this._buffer 199 this._buffer = [] 200 b.forEach(function (m) { 201 this.emitLog(m) 202 }, this) 203 if (this.progressEnabled) { 204 this.gauge.enable() 205 } 206} 207 208log._buffer = [] 209 210var id = 0 211log.record = [] 212log.maxRecordSize = 10000 213log.log = function (lvl, prefix, message) { 214 var l = this.levels[lvl] 215 if (l === undefined) { 216 return this.emit('error', new Error(util.format( 217 'Undefined log level: %j', lvl))) 218 } 219 220 var a = new Array(arguments.length - 2) 221 var stack = null 222 for (var i = 2; i < arguments.length; i++) { 223 var arg = a[i - 2] = arguments[i] 224 225 // resolve stack traces to a plain string. 226 if (typeof arg === 'object' && arg instanceof Error && arg.stack) { 227 Object.defineProperty(arg, 'stack', { 228 value: stack = arg.stack + '', 229 enumerable: true, 230 writable: true, 231 }) 232 } 233 } 234 if (stack) { 235 a.unshift(stack + '\n') 236 } 237 message = util.format.apply(util, a) 238 239 var m = { 240 id: id++, 241 level: lvl, 242 prefix: String(prefix || ''), 243 message: message, 244 messageRaw: a, 245 } 246 247 this.emit('log', m) 248 this.emit('log.' + lvl, m) 249 if (m.prefix) { 250 this.emit(m.prefix, m) 251 } 252 253 this.record.push(m) 254 var mrs = this.maxRecordSize 255 var n = this.record.length - mrs 256 if (n > mrs / 10) { 257 var newSize = Math.floor(mrs * 0.9) 258 this.record = this.record.slice(-1 * newSize) 259 } 260 261 this.emitLog(m) 262}.bind(log) 263 264log.emitLog = function (m) { 265 if (this._paused) { 266 this._buffer.push(m) 267 return 268 } 269 if (this.progressEnabled) { 270 this.gauge.pulse(m.prefix) 271 } 272 273 var l = this.levels[m.level] 274 if (l === undefined) { 275 return 276 } 277 278 if (l < this.levels[this.level]) { 279 return 280 } 281 282 if (l > 0 && !isFinite(l)) { 283 return 284 } 285 286 // If 'disp' is null or undefined, use the lvl as a default 287 // Allows: '', 0 as valid disp 288 var disp = log.disp[m.level] != null ? log.disp[m.level] : m.level 289 this.clearProgress() 290 m.message.split(/\r?\n/).forEach(function (line) { 291 var heading = this.heading 292 if (heading) { 293 this.write(heading, this.headingStyle) 294 this.write(' ') 295 } 296 this.write(disp, log.style[m.level]) 297 var p = m.prefix || '' 298 if (p) { 299 this.write(' ') 300 } 301 302 this.write(p, this.prefixStyle) 303 this.write(' ' + line + '\n') 304 }, this) 305 this.showProgress() 306} 307 308log._format = function (msg, style) { 309 if (!stream) { 310 return 311 } 312 313 var output = '' 314 if (this.useColor()) { 315 style = style || {} 316 var settings = [] 317 if (style.fg) { 318 settings.push(style.fg) 319 } 320 321 if (style.bg) { 322 settings.push('bg' + style.bg[0].toUpperCase() + style.bg.slice(1)) 323 } 324 325 if (style.bold) { 326 settings.push('bold') 327 } 328 329 if (style.underline) { 330 settings.push('underline') 331 } 332 333 if (style.inverse) { 334 settings.push('inverse') 335 } 336 337 if (settings.length) { 338 output += consoleControl.color(settings) 339 } 340 341 if (style.beep) { 342 output += consoleControl.beep() 343 } 344 } 345 output += msg 346 if (this.useColor()) { 347 output += consoleControl.color('reset') 348 } 349 350 return output 351} 352 353log.write = function (msg, style) { 354 if (!stream) { 355 return 356 } 357 358 stream.write(this._format(msg, style)) 359} 360 361log.addLevel = function (lvl, n, style, disp) { 362 // If 'disp' is null or undefined, use the lvl as a default 363 if (disp == null) { 364 disp = lvl 365 } 366 367 this.levels[lvl] = n 368 this.style[lvl] = style 369 if (!this[lvl]) { 370 this[lvl] = function () { 371 var a = new Array(arguments.length + 1) 372 a[0] = lvl 373 for (var i = 0; i < arguments.length; i++) { 374 a[i + 1] = arguments[i] 375 } 376 377 return this.log.apply(this, a) 378 }.bind(this) 379 } 380 this.disp[lvl] = disp 381} 382 383log.prefixStyle = { fg: 'magenta' } 384log.headingStyle = { fg: 'white', bg: 'black' } 385 386log.style = {} 387log.levels = {} 388log.disp = {} 389log.addLevel('silly', -Infinity, { inverse: true }, 'sill') 390log.addLevel('verbose', 1000, { fg: 'cyan', bg: 'black' }, 'verb') 391log.addLevel('info', 2000, { fg: 'green' }) 392log.addLevel('timing', 2500, { fg: 'green', bg: 'black' }) 393log.addLevel('http', 3000, { fg: 'green', bg: 'black' }) 394log.addLevel('notice', 3500, { fg: 'cyan', bg: 'black' }) 395log.addLevel('warn', 4000, { fg: 'black', bg: 'yellow' }, 'WARN') 396log.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!') 397log.addLevel('silent', Infinity) 398 399// allow 'error' prefix 400log.on('error', function () {}) 401