-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Commit
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
const proggy = require('proggy') | ||
const { log, output, META } = require('proc-log') | ||
const { log, output, input, META } = require('proc-log') | ||
const { explain } = require('./explain-eresolve.js') | ||
const { formatWithOptions } = require('./format') | ||
|
||
|
@@ -137,6 +137,9 @@ class Display { | |
// Handlers are set immediately so they can buffer all events | ||
process.on('log', this.#logHandler) | ||
process.on('output', this.#outputHandler) | ||
process.on('input', this.#inputHandler) | ||
|
||
this.#progress = new Progress({ stream: stderr }) | ||
} | ||
|
||
off () { | ||
|
@@ -146,9 +149,9 @@ class Display { | |
process.off('output', this.#outputHandler) | ||
this.#outputState.buffer.length = 0 | ||
|
||
if (this.#progress) { | ||
this.#progress.stop() | ||
} | ||
process.off('input', this.#inputHandler) | ||
|
||
this.#progress.off() | ||
} | ||
|
||
get chalk () { | ||
|
@@ -171,6 +174,7 @@ class Display { | |
unicode, | ||
}) { | ||
this.#command = command | ||
|
||
// get createSupportsColor from chalk directly if this lands | ||
// https://github.com/chalk/chalk/pull/600 | ||
const [{ Chalk }, { createSupportsColor }] = await Promise.all([ | ||
|
@@ -201,104 +205,134 @@ class Display { | |
// Emit resume event on the logs which will flush output | ||
log.resume() | ||
output.flush() | ||
this.#startProgress({ progress, unicode }) | ||
this.#progress.load({ | ||
unicode, | ||
enabled: !!progress && !this.#silent, | ||
}) | ||
} | ||
|
||
// STREAM WRITES | ||
|
||
// Write formatted and (non-)colorized output to streams | ||
#stdoutWrite (options, ...args) { | ||
this.#stdout.write(formatWithOptions({ colors: this.#stdoutColor, ...options }, ...args)) | ||
} | ||
|
||
#stderrWrite (options, ...args) { | ||
this.#stderr.write(formatWithOptions({ colors: this.#stderrColor, ...options }, ...args)) | ||
#write (stream, options, ...args) { | ||
const colors = stream === this.#stdout ? this.#stdoutColor : this.#stderrColor | ||
this.#progress.write(stream, formatWithOptions({ colors, ...options }, ...args)) | ||
} | ||
|
||
// HANDLERS | ||
|
||
// Arrow function assigned to a private class field so it can be passed | ||
// directly as a listener and still reference "this" | ||
#logHandler = withMeta((level, meta, ...args) => { | ||
if (level === log.KEYS.resume) { | ||
this.#logState.buffering = false | ||
this.#logState.buffer.forEach((item) => this.#tryWriteLog(...item)) | ||
this.#logState.buffer.length = 0 | ||
return | ||
} | ||
|
||
if (level === log.KEYS.pause) { | ||
this.#logState.buffering = true | ||
return | ||
} | ||
|
||
if (this.#logState.buffering) { | ||
this.#logState.buffer.push([level, meta, ...args]) | ||
return | ||
switch (level) { | ||
case log.KEYS.resume: | ||
this.#logState.buffering = false | ||
this.#logState.buffer.forEach((item) => this.#tryWriteLog(...item)) | ||
this.#logState.buffer.length = 0 | ||
break | ||
|
||
case log.KEYS.pause: | ||
this.#logState.buffering = true | ||
break | ||
|
||
default: | ||
if (this.#logState.buffering) { | ||
this.#logState.buffer.push([level, meta, ...args]) | ||
} else { | ||
this.#tryWriteLog(level, meta, ...args) | ||
} | ||
break | ||
} | ||
|
||
this.#tryWriteLog(level, meta, ...args) | ||
}) | ||
|
||
// Arrow function assigned to a private class field so it can be passed | ||
// directly as a listener and still reference "this" | ||
#outputHandler = withMeta((level, meta, ...args) => { | ||
if (level === output.KEYS.flush) { | ||
this.#outputState.buffering = false | ||
|
||
if (meta.jsonError && this.#json) { | ||
const json = {} | ||
for (const item of this.#outputState.buffer) { | ||
// index 2 skips the level and meta | ||
Object.assign(json, tryJsonParse(item[2])) | ||
switch (level) { | ||
case output.KEYS.flush: | ||
this.#outputState.buffering = false | ||
if (meta.jsonError && this.#json) { | ||
const json = {} | ||
for (const item of this.#outputState.buffer) { | ||
// index 2 skips the level and meta | ||
Object.assign(json, tryJsonParse(item[2])) | ||
} | ||
this.#writeOutput( | ||
output.KEYS.standard, | ||
meta, | ||
JSON.stringify({ ...json, error: meta.jsonError }, null, 2) | ||
) | ||
} else { | ||
this.#outputState.buffer.forEach((item) => this.#writeOutput(...item)) | ||
if (args.length) { | ||
this.#writeOutput(output.KEYS.standard, meta, ...args) | ||
} | ||
} | ||
this.#writeOutput( | ||
output.KEYS.standard, | ||
meta, | ||
JSON.stringify({ ...json, error: meta.jsonError }, null, 2) | ||
) | ||
} else { | ||
this.#outputState.buffer.forEach((item) => this.#writeOutput(...item)) | ||
} | ||
|
||
this.#outputState.buffer.length = 0 | ||
return | ||
} | ||
|
||
if (level === output.KEYS.buffer) { | ||
this.#outputState.buffer.push([output.KEYS.standard, meta, ...args]) | ||
return | ||
} | ||
|
||
if (this.#outputState.buffering) { | ||
this.#outputState.buffer.push([level, meta, ...args]) | ||
return | ||
this.#outputState.buffer.length = 0 | ||
break | ||
|
||
case output.KEYS.buffer: | ||
this.#outputState.buffer.push([output.KEYS.standard, meta, ...args]) | ||
break | ||
|
||
default: | ||
if (this.#outputState.buffering) { | ||
this.#outputState.buffer.push([level, meta, ...args]) | ||
} else { | ||
// HACK: if it looks like the banner and we are in a state where we hide the | ||
// banner then dont write any output. This hack can be replaced with proc-log.META | ||
const isBanner = args.length === 1 && | ||
typeof args[0] === 'string' && | ||
args[0].startsWith('\n> ') && | ||
args[0].endsWith('\n') | ||
const hideBanner = this.#silent || ['exec', 'explore'].includes(this.#command) | ||
if (!(isBanner && hideBanner)) { | ||
this.#writeOutput(level, meta, ...args) | ||
} | ||
} | ||
break | ||
} | ||
}) | ||
|
||
// HACK: if it looks like the banner and we are in a state where we hide the | ||
// banner then dont write any output. This hack can be replaced with proc-log.META | ||
const isBanner = args.length === 1 && | ||
typeof args[0] === 'string' && | ||
args[0].startsWith('\n> ') && | ||
args[0].endsWith('\n') | ||
const hideBanner = this.#silent || ['exec', 'explore'].includes(this.#command) | ||
if (isBanner && hideBanner) { | ||
return | ||
#inputHandler = withMeta((level, meta, ...args) => { | ||
switch (level) { | ||
case input.KEYS.start: | ||
log.pause() | ||
this.#outputState.buffering = true | ||
this.#progress.pause() | ||
break | ||
|
||
case input.KEYS.end: | ||
log.resume() | ||
output.flush() | ||
this.#progress.resume() | ||
break | ||
|
||
case input.KEYS.read: { | ||
input.start() | ||
const [resolve, reject, promiseFn] = args | ||
return promiseFn() | ||
.then(resolve) | ||
.catch(reject) | ||
.finally(() => { | ||
output.standard('') | ||
input.end() | ||
}) | ||
} | ||
} | ||
|
||
this.#writeOutput(level, meta, ...args) | ||
}) | ||
|
||
// OUTPUT | ||
|
||
#writeOutput (level, meta, ...args) { | ||
if (level === output.KEYS.standard) { | ||
this.#stdoutWrite({}, ...args) | ||
return | ||
} | ||
|
||
if (level === output.KEYS.error) { | ||
this.#stderrWrite({}, ...args) | ||
switch (level) { | ||
case output.KEYS.standard: | ||
this.#write(this.#stdout, {}, ...args) | ||
break | ||
|
||
case output.KEYS.error: | ||
this.#write(this.#stderr, {}, ...args) | ||
break | ||
} | ||
} | ||
|
||
|
@@ -344,22 +378,107 @@ class Display { | |
this.#logColors[level](level), | ||
title ? this.#logColors.title(title) : null, | ||
] | ||
this.#stderrWrite({ prefix }, ...args) | ||
} else if (this.#progress) { | ||
// TODO: make this display a single log line of filtered messages | ||
this.#write(this.#stderr, { prefix }, ...args) | ||
} | ||
} | ||
} | ||
|
||
class Progress { | ||
// Taken from https://github.com/sindresorhus/cli-spinners | ||
// MIT License | ||
// Copyright (c) Sindre Sorhus <[email protected]> (https://sindresorhus.com) | ||
static dots = { duration: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] } | ||
static lines = { duration: 130, frames: ['-', '\\', '|', '/'] } | ||
|
||
#stream | ||
#spinner | ||
#client | ||
#enabled = false | ||
|
||
#frameIndex = 0 | ||
#lastUpdate = 0 | ||
#interval | ||
#initialTimeout | ||
|
||
constructor ({ stream }) { | ||
this.#client = proggy.createClient({ normalize: true }) | ||
this.#stream = stream | ||
} | ||
|
||
// PROGRESS | ||
load ({ enabled, unicode }) { | ||
this.#enabled = enabled | ||
this.#spinner = unicode ? Progress.dots : Progress.lines | ||
this.#delayRender(500) | ||
} | ||
|
||
#startProgress ({ progress, unicode }) { | ||
if (!progress || this.#silent) { | ||
off () { | ||
this.#clear() | ||
} | ||
|
||
pause () { | ||
this.#clear({ clearLine: true }) | ||
} | ||
|
||
#clear ({ clearLine } = {}) { | ||
if (!this.#enabled) { | ||
return | ||
} | ||
this.#progress = proggy.createClient({ normalize: true }) | ||
// TODO: implement proggy trackers in arborist/doctor | ||
// TODO: listen to progress events here and build progress UI | ||
// TODO: see deprecated gauge package for what unicode chars were used | ||
clearTimeout(this.#initialTimeout) | ||
this.#initialTimeout = null | ||
clearInterval(this.#interval) | ||
this.#interval = null | ||
this.#frameIndex = 0 | ||
this.#lastUpdate = 0 | ||
this.#stream.cursorTo(0) | ||
Check failure on line 432 in lib/utils/display.js
|
||
if (clearLine) { | ||
this.#stream.clearLine(1) | ||
} | ||
} | ||
|
||
resume () { | ||
this.#delayRender(10) | ||
} | ||
|
||
write (stream, str) { | ||
if (!this.#enabled || !this.#interval) { | ||
return stream.write(str) | ||
} | ||
this.#stream.cursorTo(0) | ||
if (str.startsWith('\n')) { | ||
this.#stream.write(' ') | ||
this.#stream.cursorTo(0) | ||
} | ||
stream.write(str) | ||
this.#render() | ||
} | ||
|
||
#delayRender (ms) { | ||
this.#initialTimeout = setTimeout(() => { | ||
this.#initialTimeout = null | ||
this.#render() | ||
}, ms) | ||
this.#initialTimeout.unref() | ||
} | ||
|
||
#render () { | ||
if (!this.#enabled || this.#initialTimeout) { | ||
return | ||
} | ||
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration) | ||
clearInterval(this.#interval) | ||
this.#interval = setInterval(() => this.#renderFrame(true), this.#spinner.duration) | ||
} | ||
|
||
#renderFrame (next) { | ||
if (next) { | ||
this.#lastUpdate = Date.now() | ||
this.#frameIndex++ | ||
if (this.#frameIndex >= this.#spinner.frames.length) { | ||
this.#frameIndex = 0 | ||
} | ||
} | ||
this.#stream.cursorTo(0) | ||
this.#stream.write(this.#spinner.frames[this.#frameIndex]) | ||
} | ||
} | ||
|
||
|