diff --git a/lib/log.js b/lib/log.js index f02e2eb..aeeb48e 100644 --- a/lib/log.js +++ b/lib/log.js @@ -1,400 +1,476 @@ -'use strict' -var Progress = require('are-we-there-yet') -var Gauge = require('gauge') -var EE = require('events').EventEmitter -var log = exports = module.exports = new EE() -var util = require('util') - +var { EventEmitter } = require('events') var setBlocking = require('set-blocking') var consoleControl = require('console-control-strings') +var util = require('util') setBlocking(true) -var stream = process.stderr -Object.defineProperty(log, 'stream', { - set: function (newStream) { - stream = newStream - if (this.gauge) { - this.gauge.setWriteTo(stream, stream) - } - }, - get: function () { - return stream - }, -}) - -// by default, decide based on tty-ness. -var colorEnabled -log.useColor = function () { - return colorEnabled != null ? colorEnabled : stream.isTTY -} - -log.enableColor = function () { - colorEnabled = true - this.gauge.setTheme({ hasColor: colorEnabled, hasUnicode: unicodeEnabled }) -} -log.disableColor = function () { - colorEnabled = false - this.gauge.setTheme({ hasColor: colorEnabled, hasUnicode: unicodeEnabled }) -} - -// default level -log.level = 'info' -log.gauge = new Gauge(stream, { - enabled: false, // no progress bars unless asked - theme: { hasColor: log.useColor() }, - template: [ - { type: 'progressbar', length: 20 }, - { type: 'activityIndicator', kerning: 1, length: 1 }, - { type: 'section', default: '' }, - ':', - { type: 'logline', kerning: 1, default: '' }, - ], -}) - -log.tracker = new Progress.TrackerGroup() - -// we track this separately as we may need to temporarily disable the -// display of the status bar for our own loggy purposes. -log.progressEnabled = log.gauge.isEnabled() - -var unicodeEnabled - -log.enableUnicode = function () { - unicodeEnabled = true - this.gauge.setTheme({ hasColor: this.useColor(), hasUnicode: unicodeEnabled }) -} +var trackerConstructors = ['newGroup', 'newItem', 'newStream'] -log.disableUnicode = function () { - unicodeEnabled = false - this.gauge.setTheme({ hasColor: this.useColor(), hasUnicode: unicodeEnabled }) -} +class Log extends EventEmitter { + #stream = process.stderr + #gauge = undefined + #colorEnabled = false + #unicodeEnabled = false + #id = 0 + #tracker = undefined + #hasColor = undefined + #hasUnicode = undefined + + constructor () { + super() + + // default level + this.level = 'info' + // we track this separately as we may need to temporarily disable the + // display of the status bar for our own loggy purposes. + this.progressEnabled = false + this._paused = false + // bind for use in tracker's on-change listener + this.showProgress = this.showProgress.bind(this) + this._buffer = [] + this.record = [] + this.maxRecordSize = 10000 + this.log = this.log.bind(this) + + this.prefixStyle = { fg: 'magenta' } + this.headingStyle = { fg: 'white', bg: 'black' } + + this.style = {} + this.levels = {} + this.disp = {} + + this.addLevel('silly', -Infinity, { inverse: true }, 'sill') + this.addLevel('verbose', 1000, { fg: 'cyan', bg: 'black' }, 'verb') + this.addLevel('info', 2000, { fg: 'green' }) + this.addLevel('timing', 2500, { fg: 'green', bg: 'black' }) + this.addLevel('http', 3000, { fg: 'green', bg: 'black' }) + this.addLevel('notice', 3500, { fg: 'cyan', bg: 'black' }) + this.addLevel('warn', 4000, { fg: 'black', bg: 'yellow' }, 'WARN') + this.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!') + this.addLevel('silent', Infinity) + + // allow 'error' prefix + this.on('error', function () { }) + + Object.defineProperty(this, 'tracker', { + get: () => { + if (!this.#tracker) { + const Progress = require('are-we-there-yet') + this.#tracker = new Progress.TrackerGroup() + } + + return this.#tracker + }, + set: (tracker) => { + this.#tracker = tracker + }, + enumerable: true, + configurable: true, + }) + } -log.setGaugeThemeset = function (themes) { - this.gauge.setThemeset(themes) -} + set stream (newStream) { + this.#stream = newStream -log.setGaugeTemplate = function (template) { - this.gauge.setTemplate(template) -} + if (this.#gauge) { + this.#gauge.setWriteTo(newStream, newStream) + } + } -log.enableProgress = function () { - if (this.progressEnabled || this._paused) { - return + get stream () { + return this.#stream } - this.progressEnabled = true - this.tracker.on('change', this.showProgress) - this.gauge.enable() -} + get gauge () { + if (!this.#gauge) { + const Gauge = require('gauge') + this.#gauge = new Gauge(this.#stream, { + enabled: false, // no progress bars unless asked + theme: { + hasColor: this.#hasColor !== undefined + ? this.#hasColor + : this.useColor(), + hasUnicode: this.#hasUnicode, + }, + template: [ + { type: 'progressbar', length: 20 }, + { type: 'activityIndicator', kerning: 1, length: 1 }, + { type: 'section', default: '' }, + ':', + { type: 'logline', kerning: 1, default: '' }, + ], + }) + } -log.disableProgress = function () { - if (!this.progressEnabled) { - return + return this.#gauge } - this.progressEnabled = false - this.tracker.removeListener('change', this.showProgress) - this.gauge.disable() -} -var trackerConstructors = ['newGroup', 'newItem', 'newStream'] + set gauge (newGauge) { + this.#gauge = newGauge + } -var mixinLog = function (tracker) { - // mixin the public methods from log into the tracker - // (except: conflicts and one's we handle specially) - Object.keys(log).forEach(function (P) { - if (P[0] === '_') { - return - } + // by default, decide based on tty-ness. + useColor () { + return this.#colorEnabled != null ? this.#colorEnabled : this.#stream.isTTY + } - if (trackerConstructors.filter(function (C) { - return C === P - }).length) { - return - } + enableColor () { + this.#colorEnabled = true + this.#gaugeSetTheme(this.#colorEnabled, this.#unicodeEnabled) + } - if (tracker[P]) { - return - } + disableColor () { + this.#colorEnabled = false + this.#gaugeSetTheme(this.#colorEnabled, this.#unicodeEnabled) + } - if (typeof log[P] !== 'function') { + #gaugeSetTheme (hasColor, hasUnicode) { + if (!this.#gauge) { + this.#hasColor = hasColor + this.#hasUnicode = hasUnicode return } - var func = log[P] - tracker[P] = function () { - return func.apply(log, arguments) - } - }) - // if the new tracker is a group, make sure any subtrackers get - // mixed in too - if (tracker instanceof Progress.TrackerGroup) { - trackerConstructors.forEach(function (C) { - var func = tracker[C] - tracker[C] = function () { - return mixinLog(func.apply(tracker, arguments)) - } + this.#gauge.setTheme({ + hasColor, + hasUnicode, }) } - return tracker -} -// Add tracker constructors to the top level log object -trackerConstructors.forEach(function (C) { - log[C] = function () { - return mixinLog(this.tracker[C].apply(this.tracker, arguments)) + enableUnicode () { + this.#unicodeEnabled = true + this.#gaugeSetTheme(this.useColor(), this.#unicodeEnabled) } -}) -log.clearProgress = function (cb) { - if (!this.progressEnabled) { - return cb && process.nextTick(cb) + disableUnicode () { + this.#unicodeEnabled = false + this.#gaugeSetTheme(this.useColor(), this.#unicodeEnabled) } - this.gauge.hide(cb) -} + setGaugeThemeset (themes) { + this.gauge.setThemeset(themes) + } -log.showProgress = function (name, completed) { - if (!this.progressEnabled) { - return + setGaugeTemplate (template) { + this.gauge.setTemplate(template) } - var values = {} - if (name) { - values.section = name + trackerRemoveAllListeners () { + if (!this.#tracker) { + return + } + + this.#tracker.removeAllListeners() } - var last = log.record[log.record.length - 1] - if (last) { - values.subsection = last.prefix - var disp = log.disp[last.level] - var logline = this._format(disp, log.style[last.level]) - if (last.prefix) { - logline += ' ' + this._format(last.prefix, this.prefixStyle) + enableProgress () { + if (this.progressEnabled || this._paused) { + return } - logline += ' ' + last.message.split(/\r?\n/)[0] - values.logline = logline + this.progressEnabled = true + this.tracker.on('change', this.showProgress) + this.gauge.enable() } - values.completed = completed || this.tracker.completed() - this.gauge.show(values) -}.bind(log) // bind for use in tracker's on-change listener - -// temporarily stop emitting, but don't drop -log.pause = function () { - this._paused = true - if (this.progressEnabled) { + + disableProgress () { + if (!this.progressEnabled) { + return + } + + this.progressEnabled = false + this.tracker.removeListener('change', this.showProgress) this.gauge.disable() } -} -log.resume = function () { - if (!this._paused) { - return + newGroup (groupname, weight) { + return this.#mixingLog( + this.tracker.newGroup(groupname, weight) + ) } - this._paused = false + newItem (name, todo, weight) { + return this.#mixingLog( + this.tracker.newItem(name, todo, weight) + ) + } - var b = this._buffer - this._buffer = [] - b.forEach(function (m) { - this.emitLog(m) - }, this) - if (this.progressEnabled) { - this.gauge.enable() + newStream (name, todo, weight) { + return this.#mixingLog( + this.tracker.newStream(name, todo, weight) + ) } -} -log._buffer = [] + #mixingLog (tracker) { + // mixin the public methods from log into the tracker + // (except: conflicts and one's we handle specially) + Object.keys(this).forEach((P) => { + if (P[0] === '_') { + return + } -var id = 0 -log.record = [] -log.maxRecordSize = 10000 -log.log = function (lvl, prefix, message) { - var l = this.levels[lvl] - if (l === undefined) { - return this.emit('error', new Error(util.format( - 'Undefined log level: %j', lvl))) - } + if (trackerConstructors.filter(function (C) { + return C === P + }).length) { + return + } - var a = new Array(arguments.length - 2) - var stack = null - for (var i = 2; i < arguments.length; i++) { - var arg = a[i - 2] = arguments[i] - - // resolve stack traces to a plain string. - if (typeof arg === 'object' && arg instanceof Error && arg.stack) { - Object.defineProperty(arg, 'stack', { - value: stack = arg.stack + '', - enumerable: true, - writable: true, + if (tracker[P]) { + return + } + + if (typeof this[P] !== 'function') { + return + } + + var func = this[P] + tracker[P] = (...args) => { + return func.apply(this, args) + } + }) + + const Progress = require('are-we-there-yet') + // if the new tracker is a group, make sure any subtrackers get + // mixed in too + if (tracker instanceof Progress.TrackerGroup) { + trackerConstructors.forEach((C) => { + var func = tracker[C] + tracker[C] = (...args) => { + return this.#mixingLog(func.apply(tracker, args)) + } }) } - } - if (stack) { - a.unshift(stack + '\n') - } - message = util.format.apply(util, a) - - var m = { - id: id++, - level: lvl, - prefix: String(prefix || ''), - message: message, - messageRaw: a, - } - this.emit('log', m) - this.emit('log.' + lvl, m) - if (m.prefix) { - this.emit(m.prefix, m) + return tracker } - this.record.push(m) - var mrs = this.maxRecordSize - var n = this.record.length - mrs - if (n > mrs / 10) { - var newSize = Math.floor(mrs * 0.9) - this.record = this.record.slice(-1 * newSize) + clearProgress (cb) { + if (!this.progressEnabled) { + return cb && process.nextTick(cb) + } + + this.gauge.hide(cb) } - this.emitLog(m) -}.bind(log) + showProgress (name, completed) { + if (!this.progressEnabled) { + return + } -log.emitLog = function (m) { - if (this._paused) { - this._buffer.push(m) - return - } - if (this.progressEnabled) { - this.gauge.pulse(m.prefix) - } + var values = {} + if (name) { + values.section = name + } - var l = this.levels[m.level] - if (l === undefined) { - return - } + var last = this.record[this.record.length - 1] + if (last) { + values.subsection = last.prefix + var disp = this.disp[last.level] + var logline = this._format(disp, this.style[last.level]) + if (last.prefix) { + logline += ' ' + this._format(last.prefix, this.prefixStyle) + } - if (l < this.levels[this.level]) { - return + logline += ' ' + last.message.split(/\r?\n/)[0] + values.logline = logline + } + values.completed = completed || this.tracker.completed() + this.gauge.show(values) } - if (l > 0 && !isFinite(l)) { - return + pause () { + this._paused = true + if (this.progressEnabled) { + this.gauge.disable() + } } - // If 'disp' is null or undefined, use the lvl as a default - // Allows: '', 0 as valid disp - var disp = log.disp[m.level] - this.clearProgress() - m.message.split(/\r?\n/).forEach(function (line) { - var heading = this.heading - if (heading) { - this.write(heading, this.headingStyle) - this.write(' ') - } - this.write(disp, log.style[m.level]) - var p = m.prefix || '' - if (p) { - this.write(' ') + resume () { + if (!this._paused) { + return } - this.write(p, this.prefixStyle) - this.write(' ' + line + '\n') - }, this) - this.showProgress() -} + this._paused = false + + var b = this._buffer + this._buffer = [] -log._format = function (msg, style) { - if (!stream) { - return + b.forEach(function (m) { + this.emitLog(m) + }, this) + + if (this.progressEnabled) { + this.gauge.enable() + } } - var output = '' - if (this.useColor()) { - style = style || {} - var settings = [] - if (style.fg) { - settings.push(style.fg) + log (lvl, prefix, ...args) { + var l = this.levels[lvl] + if (l === undefined) { + return this.emit('error', new Error(util.format( + 'Undefined log level: %j', lvl))) + } + + var message = args[2] + var stack = null + for (var i = 0; i < args.length; i++) { + var arg = args[i] + + // resolve stack traces to a plain string. + if (typeof arg === 'object' && arg instanceof Error && arg.stack) { + Object.defineProperty(arg, 'stack', { + value: stack = arg.stack + '', + enumerable: true, + writable: true, + }) + } + } + if (stack) { + args.unshift(stack + '\n') + } + message = util.format.apply(util, args) + + var m = { + id: this.#id++, + level: lvl, + prefix: String(prefix || ''), + message: message, + messageRaw: args, } - if (style.bg) { - settings.push('bg' + style.bg[0].toUpperCase() + style.bg.slice(1)) + this.emit('log', m) + this.emit('log.' + lvl, m) + if (m.prefix) { + this.emit(m.prefix, m) } - if (style.bold) { - settings.push('bold') + this.record.push(m) + var mrs = this.maxRecordSize + var n = this.record.length - mrs + if (n > mrs / 10) { + var newSize = Math.floor(mrs * 0.9) + this.record = this.record.slice(-1 * newSize) } - if (style.underline) { - settings.push('underline') + this.emitLog(m) + } + + emitLog (m) { + if (this._paused) { + this._buffer.push(m) + return + } + if (this.progressEnabled) { + this.gauge.pulse(m.prefix) } - if (style.inverse) { - settings.push('inverse') + var l = this.levels[m.level] + if (l === undefined) { + return } - if (settings.length) { - output += consoleControl.color(settings) + if (l < this.levels[this.level]) { + return } - if (style.beep) { - output += consoleControl.beep() + if (l > 0 && !isFinite(l)) { + return } - } - output += msg - if (this.useColor()) { - output += consoleControl.color('reset') + + // If 'disp' is null or undefined, use the lvl as a default + // Allows: '', 0 as valid disp + var disp = this.disp[m.level] + this.clearProgress() + m.message.split(/\r?\n/).forEach((line) => { + var heading = this.heading + if (heading) { + this.write(heading, this.headingStyle) + this.write(' ') + } + this.write(disp, this.style[m.level]) + var p = m.prefix || '' + if (p) { + this.write(' ') + } + + this.write(p, this.prefixStyle) + this.write(' ' + line + '\n') + }) + this.showProgress() } - return output -} + _format (msg, style) { + if (!this.#stream) { + return + } -log.write = function (msg, style) { - if (!stream) { - return + var output = '' + if (this.useColor()) { + style = style || {} + var settings = [] + if (style.fg) { + settings.push(style.fg) + } + + if (style.bg) { + settings.push('bg' + style.bg[0].toUpperCase() + style.bg.slice(1)) + } + + if (style.bold) { + settings.push('bold') + } + + if (style.underline) { + settings.push('underline') + } + + if (style.inverse) { + settings.push('inverse') + } + + if (settings.length) { + output += consoleControl.color(settings) + } + + if (style.beep) { + output += consoleControl.beep() + } + } + output += msg + if (this.useColor()) { + output += consoleControl.color('reset') + } + + return output } - stream.write(this._format(msg, style)) -} + write (msg, style) { + if (!this.#stream) { + return + } -log.addLevel = function (lvl, n, style, disp) { - // If 'disp' is null or undefined, use the lvl as a default - if (disp == null) { - disp = lvl + this.#stream.write(this._format(msg, style)) } - this.levels[lvl] = n - this.style[lvl] = style - if (!this[lvl]) { - this[lvl] = function () { - var a = new Array(arguments.length + 1) - a[0] = lvl - for (var i = 0; i < arguments.length; i++) { - a[i + 1] = arguments[i] - } + addLevel (lvl, n, style, disp) { + // If 'disp' is null or undefined, use the lvl as a default + if (disp == null) { + disp = lvl + } - return this.log.apply(this, a) - }.bind(this) + this.levels[lvl] = n + this.style[lvl] = style + + if (!this[lvl]) { + this[lvl] = (...args) => { + args.unshift(lvl) + + return this.log.apply(this, args) + } + } + this.disp[lvl] = disp } - this.disp[lvl] = disp } -log.prefixStyle = { fg: 'magenta' } -log.headingStyle = { fg: 'white', bg: 'black' } - -log.style = {} -log.levels = {} -log.disp = {} -log.addLevel('silly', -Infinity, { inverse: true }, 'sill') -log.addLevel('verbose', 1000, { fg: 'cyan', bg: 'black' }, 'verb') -log.addLevel('info', 2000, { fg: 'green' }) -log.addLevel('timing', 2500, { fg: 'green', bg: 'black' }) -log.addLevel('http', 3000, { fg: 'green', bg: 'black' }) -log.addLevel('notice', 3500, { fg: 'cyan', bg: 'black' }) -log.addLevel('warn', 4000, { fg: 'black', bg: 'yellow' }, 'WARN') -log.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!') -log.addLevel('silent', Infinity) - -// allow 'error' prefix -log.on('error', function () {}) +module.exports = new Log() diff --git a/test/progress.js b/test/progress.js index 5669432..ed2d9b2 100644 --- a/test/progress.js +++ b/test/progress.js @@ -251,3 +251,14 @@ test('pause while enableProgress', function (t) { log.resume() didActions(t, 'enableProgress', [['enable']]) }) + +test('trackerRemoveAllListeners', function (t) { + t.plan(2) + resetTracker() + log.disableProgress() + actions = [] + log.enableProgress() + t.equal(log.tracker.listenerCount('change'), 1) + log.trackerRemoveAllListeners() + t.equal(log.tracker.listenerCount('change'), 0) +})