Skip to content

Commit c54d1e9

Browse files
authored
fix: progress bar code cleanup (#8633)
- 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 - Minor comment syntax cleanup 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 e8de81b commit c54d1e9

File tree

1 file changed

+36
-34
lines changed

1 file changed

+36
-34
lines changed

lib/utils/display.js

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,11 @@ class Display {
216216
timing,
217217
unicode,
218218
}) {
219-
// get createSupportsColor from chalk directly if this lands
220-
// https://github.com/chalk/chalk/pull/600
221219
const [{ Chalk }, { createSupportsColor }] = await Promise.all([
222220
import('chalk'),
223221
import('supports-color'),
224222
])
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.
223+
// 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.
226224
const level = Math.max(createSupportsColor(null).level, 1)
227225
this.#noColorChalk = new Chalk({ level: 0 })
228226
this.#stdoutColor = stdoutColor
@@ -307,14 +305,14 @@ class Display {
307305
if (this.#outputState.buffering) {
308306
this.#outputState.buffer.push([level, meta, ...args])
309307
} else {
310-
// HACK: Check if the argument looks like a run-script banner. This can be replaced with proc-log.META in @npmcli/run-script
308+
// XXX: Check if the argument looks like a run-script banner. This should be replaced with proc-log.META in @npmcli/run-script
311309
if (typeof args[0] === 'string' && args[0].startsWith('\n> ') && args[0].endsWith('\n')) {
312310
if (this.#silent || ['exec', 'explore'].includes(this.#command)) {
313311
// Silent mode and some specific commands always hide run script banners
314312
break
315313
} else if (this.#json) {
316314
// 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"
315+
// 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".
318316
level = output.KEYS.error
319317
}
320318
}
@@ -339,12 +337,12 @@ class Display {
339337
break
340338

341339
case input.KEYS.read: {
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
340+
// 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.
343341
const [res, rej, p] = args
344342
return input.start(() => p()
345343
.then(res)
346344
.catch(rej)
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
345+
// 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.
348346
.finally(() => output.standard('')))
349347
}
350348
}
@@ -419,16 +417,17 @@ class Progress {
419417
static dots = { duration: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] }
420418
static lines = { duration: 130, frames: ['-', '\\', '|', '/'] }
421419

422-
#stream
423-
#spinner
424420
#enabled = false
425-
426421
#frameIndex = 0
427-
#lastUpdate = 0
428422
#interval
423+
#lastUpdate = 0
424+
#spinner
425+
#stream
426+
// Initial timeout to wait to start rendering
429427
#timeout
428+
#rendered = false
430429

431-
// We are rendering is enabled option is set and we are not waiting for the render timeout
430+
// We are rendering if enabled option is set and we are not waiting for the render timeout
432431
get #rendering () {
433432
return this.#enabled && !this.#timeout
434433
}
@@ -445,8 +444,13 @@ class Progress {
445444
load ({ enabled, unicode }) {
446445
this.#enabled = enabled
447446
this.#spinner = unicode ? Progress.dots : Progress.lines
448-
// Dont render the spinner for short durations
449-
this.#render(200)
447+
// Wait 200 ms so we don't render the spinner for short durations
448+
this.#timeout = setTimeout(() => {
449+
this.#timeout = null
450+
this.#render()
451+
}, 200)
452+
// Make sure this timeout does not keep the process open
453+
this.#timeout.unref()
450454
}
451455

452456
off () {
@@ -463,7 +467,7 @@ class Progress {
463467
}
464468

465469
resume () {
466-
this.#render()
470+
this.#render(true)
467471
}
468472

469473
// If we are currently rendering the spinner we clear it before writing our line and then re-render the spinner after.
@@ -478,45 +482,43 @@ class Progress {
478482
}
479483
}
480484

481-
#render (ms) {
482-
if (ms) {
483-
this.#timeout = setTimeout(() => {
484-
this.#timeout = null
485-
this.#renderSpinner()
486-
}, ms)
487-
// Make sure this timeout does not keep the process open
488-
this.#timeout.unref()
489-
} else {
490-
this.#renderSpinner()
491-
}
492-
}
493-
494-
#renderSpinner () {
485+
#render (resuming) {
495486
if (!this.#rendering) {
496487
return
497488
}
498489
// 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
499-
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration)
500-
clearInterval(this.#interval)
501-
this.#interval = setInterval(() => this.#renderFrame(true), this.#spinner.duration)
490+
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration, resuming)
491+
if (!this.#interval) {
492+
this.#interval = setInterval(() => this.#renderFrame(true), this.#spinner.duration)
493+
// Make sure this timeout does not keep the process open
494+
this.#interval.unref()
495+
}
496+
this.#interval.refresh()
502497
}
503498

504-
#renderFrame (next) {
499+
#renderFrame (next, resuming) {
505500
if (next) {
506501
this.#lastUpdate = Date.now()
507502
this.#frameIndex++
508503
if (this.#frameIndex >= this.#spinner.frames.length) {
509504
this.#frameIndex = 0
510505
}
511506
}
512-
this.#clearSpinner()
507+
if (!resuming) {
508+
this.#clearSpinner()
509+
}
513510
this.#stream.write(this.#spinner.frames[this.#frameIndex])
511+
this.#rendered = true
514512
}
515513

516514
#clearSpinner () {
515+
if (!this.#rendered) {
516+
return
517+
}
517518
// Move to the start of the line and clear the rest of the line
518519
this.#stream.cursorTo(0)
519520
this.#stream.clearLine(1)
521+
this.#rendered = false
520522
}
521523
}
522524

0 commit comments

Comments
 (0)