Skip to content

Commit a0a9995

Browse files
committed
fix: progress bar code cleanup
- inline this.#render logic. The initial setTimeout is only used during load(), so run it there instead. - .unref() the progress bar's recurring timeout object - reuse the progress bar's timeout object with .refresh() instead of making a new one every frame - consolidate comments into one line and remove stale comments - skip clearing the line on the first frame when resuming the spinner - removed the test from $8631 as it was only testing that the very first frame of the spinner didn't clear the line, not any subsequent frames This is built off of #8631, moving the "skip clearing the line..." logic into a flag that's passed during .resume()
1 parent ce41348 commit a0a9995

File tree

1 file changed

+43
-66
lines changed

1 file changed

+43
-66
lines changed

lib/utils/display.js

Lines changed: 43 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ const setBlocking = (stream) => {
7878
// These are important
7979
// This is the key that is returned to the user for errors
8080
const ERROR_KEY = 'error'
81-
// This is the key producers use to indicate that there
82-
// is a json error that should be merged into the finished output
81+
// This is the key producers use to indicate that there is a json error that should be merged into the finished output
8382
const JSON_ERROR_KEY = 'jsonError'
8483

8584
const isPlainObject = (v) => v && typeof v === 'object' && !Array.isArray(v)
@@ -120,15 +119,13 @@ const getJsonBuffer = ({ [JSON_ERROR_KEY]: metaError }, buffer) => {
120119

121120
const res = getArrayOrObject(items)
122121

123-
// This skips any error checking since we can only set an error property
124-
// on an object that can be stringified
122+
// This skips any error checking since we can only set an error property on an object that can be stringified
125123
// XXX(BREAKING_CHANGE): remove this in favor of always returning an object with result and error keys
126124
if (isPlainObject(res) && errors.length) {
127-
// This is not ideal. JSON output has always been keyed at the root with an `error`
128-
// key, so we cant change that without it being a breaking change. At the same time
129-
// some commands output arbitrary keys at the top level of the output, such as package
130-
// names. So the output could already have the same key. The choice here is to overwrite
131-
// it with our error since that is (probably?) more important.
125+
// This is not ideal.
126+
// JSON output has always been keyed at the root with an `error` key, so we cant change that without it being a breaking change.
127+
// At the same time some commands output arbitrary keys at the top level of the output, such as package names.
128+
// So the output could already have the same key. The choice here is to overwrite it with our error since that is (probably?) more important.
132129
// XXX(BREAKING_CHANGE): all json output should be keyed under well known keys, eg `result` and `error`
133130
if (res[ERROR_KEY]) {
134131
log.warn('', `overwriting existing ${ERROR_KEY} on json output`)
@@ -221,15 +218,11 @@ class Display {
221218
timing,
222219
unicode,
223220
}) {
224-
// get createSupportsColor from chalk directly if this lands
225-
// https://github.com/chalk/chalk/pull/600
226221
const [{ Chalk }, { createSupportsColor }] = await Promise.all([
227222
import('chalk'),
228223
import('supports-color'),
229224
])
230-
// we get the chalk level based on a null stream meaning chalk will only use
231-
// what it knows about the environment to get color support since we already
232-
// determined in our definitions that we want to show colors.
225+
// We get the chalk level based on a null stream, meaning chalk will only use what it knows about the environment to get color support since we already determined in our definitions that we want to show colors.
233226
const level = Math.max(createSupportsColor(null).level, 1)
234227
this.#noColorChalk = new Chalk({ level: 0 })
235228
this.#stdoutColor = stdoutColor
@@ -265,8 +258,7 @@ class Display {
265258

266259
// HANDLERS
267260

268-
// Arrow function assigned to a private class field so it can be passed
269-
// directly as a listener and still reference "this"
261+
// Arrow function assigned to a private class field so it can be passed directly as a listener and still reference "this"
270262
#logHandler = withMeta((level, meta, ...args) => {
271263
switch (level) {
272264
case log.KEYS.resume:
@@ -289,8 +281,7 @@ class Display {
289281
}
290282
})
291283

292-
// Arrow function assigned to a private class field so it can be passed
293-
// directly as a listener and still reference "this"
284+
// Arrow function assigned to a private class field so it can be passed directly as a listener and still reference "this"
294285
#outputHandler = withMeta((level, meta, ...args) => {
295286
this.#json = typeof meta.json === 'boolean' ? meta.json : this.#json
296287
switch (level) {
@@ -316,18 +307,14 @@ class Display {
316307
if (this.#outputState.buffering) {
317308
this.#outputState.buffer.push([level, meta, ...args])
318309
} else {
319-
// HACK: Check if the argument looks like a run-script banner. This can be
320-
// replaced with proc-log.META in @npmcli/run-script
310+
// XXX: Check if the argument looks like a run-script banner. This should be replaced with proc-log.META in @npmcli/run-script
321311
if (typeof args[0] === 'string' && args[0].startsWith('\n> ') && args[0].endsWith('\n')) {
322312
if (this.#silent || ['exec', 'explore'].includes(this.#command)) {
323313
// Silent mode and some specific commands always hide run script banners
324314
break
325315
} else if (this.#json) {
326-
// In json mode, change output to stderr since we don't want to break json
327-
// parsing on stdout if the user is piping to jq or something.
328-
// XXX: in a future (breaking?) change it might make sense for run-script to
329-
// always output these banners with proc-log.output.error if we think they
330-
// align closer with "logging" instead of "output"
316+
// In json mode, change output to stderr since we don't want to break json parsing on stdout if the user is piping to jq or something.
317+
// XXX: in a future (breaking?) change it might make sense for run-script to always output these banners with proc-log.output.error if we think they align closer with "logging" instead of "output".
331318
level = output.KEYS.error
332319
}
333320
}
@@ -352,14 +339,12 @@ class Display {
352339
break
353340

354341
case input.KEYS.read: {
355-
// The convention when calling input.read is to pass in a single fn that returns
356-
// the promise to await. resolve and reject are provided by proc-log
342+
// The convention when calling input.read is to pass in a single fn that returns the promise to await. resolve and reject are provided by proc-log.
357343
const [res, rej, p] = args
358344
return input.start(() => p()
359345
.then(res)
360346
.catch(rej)
361-
// Any call to procLog.input.read will render a prompt to the user, so we always
362-
// add a single newline of output to stdout to move the cursor to the next line
347+
// Any call to procLog.input.read will render a prompt to the user, so we always add a single newline of output to stdout to move the cursor to the next line.
363348
.finally(() => output.standard('')))
364349
}
365350
}
@@ -383,10 +368,7 @@ class Display {
383368

384369
#tryWriteLog (level, meta, ...args) {
385370
try {
386-
// Also (and this is a really inexcusable kludge), we patch the
387-
// log.warn() method so that when we see a peerDep override
388-
// explanation from Arborist, we can replace the object with a
389-
// highly abbreviated explanation of what's being overridden.
371+
// Also (and this is a really inexcusable kludge), we patch the log.warn() method so that when we see a peerDep override explanation from Arborist, we can replace the object with a highly abbreviated explanation of what's being overridden.
390372
// TODO: this could probably be moved to arborist now that display is refactored
391373
const [heading, message, expl] = args
392374
if (level === log.KEYS.warn && heading === 'ERESOLVE' && expl && typeof expl === 'object') {
@@ -400,8 +382,7 @@ class Display {
400382
// if it crashed once, it might again!
401383
this.#writeLog(log.KEYS.verbose, meta, '', `attempt to log crashed`, ...args, ex)
402384
} catch (ex2) {
403-
// This happens if the object has an inspect method that crashes so just console.error
404-
// with the errors but don't do anything else that might error again.
385+
// This happens if the object has an inspect method that crashes so just console.error with the errors but don't do anything else that might error again.
405386
// eslint-disable-next-line no-console
406387
console.error(`attempt to log crashed`, ex, ex2)
407388
}
@@ -433,17 +414,17 @@ class Progress {
433414
static dots = { duration: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] }
434415
static lines = { duration: 130, frames: ['-', '\\', '|', '/'] }
435416

436-
#stream
437-
#spinner
438417
#enabled = false
439-
440418
#frameIndex = 0
441-
#lastUpdate = 0
442419
#interval
420+
#lastUpdate = 0
421+
#spinner
422+
#stream
423+
// Initial timeout to wait to start rendering
443424
#timeout
444425
#rendered = false
445426

446-
// We are rendering is enabled option is set and we are not waiting for the render timeout
427+
// We are rendering if enabled option is set and we are not waiting for the render timeout
447428
get #rendering () {
448429
return this.#enabled && !this.#timeout
449430
}
@@ -460,8 +441,13 @@ class Progress {
460441
load ({ enabled, unicode }) {
461442
this.#enabled = enabled
462443
this.#spinner = unicode ? Progress.dots : Progress.lines
463-
// Dont render the spinner for short durations
464-
this.#render(200)
444+
// Wait 200 ms so we don't render the spinner for short durations
445+
this.#timeout = setTimeout(() => {
446+
this.#timeout = null
447+
this.#render()
448+
}, 200)
449+
// Make sure this timeout does not keep the process open
450+
this.#timeout.unref()
465451
}
466452

467453
off () {
@@ -478,11 +464,10 @@ class Progress {
478464
}
479465

480466
resume () {
481-
this.#render()
467+
this.#render(true)
482468
}
483469

484-
// If we are currently rendering the spinner we clear it
485-
// before writing our line and then re-render the spinner after.
470+
// If we are currently rendering the spinner we clear it before writing our line and then re-render the spinner after.
486471
// If not then all we need to do is write the line
487472
write (write) {
488473
if (this.#spinning) {
@@ -494,39 +479,31 @@ class Progress {
494479
}
495480
}
496481

497-
#render (ms) {
498-
if (ms) {
499-
this.#timeout = setTimeout(() => {
500-
this.#timeout = null
501-
this.#renderSpinner()
502-
}, ms)
503-
// Make sure this timeout does not keep the process open
504-
this.#timeout.unref()
505-
} else {
506-
this.#renderSpinner()
507-
}
508-
}
509-
510-
#renderSpinner () {
482+
#render (resuming) {
511483
if (!this.#rendering) {
512484
return
513485
}
514-
// We always attempt to render immediately but we only request to move to the next
515-
// frame if it has been longer than our spinner frame duration since our last update
516-
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration)
517-
clearInterval(this.#interval)
518-
this.#interval = setInterval(() => this.#renderFrame(true), this.#spinner.duration)
486+
// We always attempt to render immediately but we only request to move to the next frame if it has been longer than our spinner frame duration since our last update
487+
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration, resuming)
488+
if (!this.#interval) {
489+
this.#interval = setInterval(() => this.#renderFrame(true), this.#spinner.duration)
490+
// Make sure this timeout does not keep the process open
491+
this.#interval.unref()
492+
}
493+
this.#interval.refresh()
519494
}
520495

521-
#renderFrame (next) {
496+
#renderFrame (next, resuming) {
522497
if (next) {
523498
this.#lastUpdate = Date.now()
524499
this.#frameIndex++
525500
if (this.#frameIndex >= this.#spinner.frames.length) {
526501
this.#frameIndex = 0
527502
}
528503
}
529-
this.#clearSpinner()
504+
if (!resuming) {
505+
this.#clearSpinner()
506+
}
530507
this.#stream.write(this.#spinner.frames[this.#frameIndex])
531508
this.#rendered = true
532509
}

0 commit comments

Comments
 (0)