Skip to content

Commit 63b462a

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 9390158 commit 63b462a

File tree

2 files changed

+30
-79
lines changed

2 files changed

+30
-79
lines changed

lib/utils/display.js

Lines changed: 30 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,17 +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
430428
#rendered = false
431429

432-
// 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
433431
get #rendering () {
434432
return this.#enabled && !this.#timeout
435433
}
@@ -446,8 +444,13 @@ class Progress {
446444
load ({ enabled, unicode }) {
447445
this.#enabled = enabled
448446
this.#spinner = unicode ? Progress.dots : Progress.lines
449-
// Dont render the spinner for short durations
450-
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()
451454
}
452455

453456
off () {
@@ -464,7 +467,7 @@ class Progress {
464467
}
465468

466469
resume () {
467-
this.#render()
470+
this.#render(true)
468471
}
469472

470473
// If we are currently rendering the spinner we clear it before writing our line and then re-render the spinner after.
@@ -479,38 +482,31 @@ class Progress {
479482
}
480483
}
481484

482-
#render (ms) {
483-
if (ms) {
484-
this.#timeout = setTimeout(() => {
485-
this.#timeout = null
486-
this.#renderSpinner()
487-
}, ms)
488-
// Make sure this timeout does not keep the process open
489-
this.#timeout.unref()
490-
} else {
491-
this.#renderSpinner()
492-
}
493-
}
494-
495-
#renderSpinner () {
485+
#render (resuming) {
496486
if (!this.#rendering) {
497487
return
498488
}
499489
// 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
500-
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration)
501-
clearInterval(this.#interval)
502-
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()
503497
}
504498

505-
#renderFrame (next) {
499+
#renderFrame (next, resuming) {
506500
if (next) {
507501
this.#lastUpdate = Date.now()
508502
this.#frameIndex++
509503
if (this.#frameIndex >= this.#spinner.frames.length) {
510504
this.#frameIndex = 0
511505
}
512506
}
513-
this.#clearSpinner()
507+
if (!resuming) {
508+
this.#clearSpinner()
509+
}
514510
this.#stream.write(this.#spinner.frames[this.#frameIndex])
515511
this.#rendered = true
516512
}

test/lib/utils/display.js

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const mockDisplay = async (t, { mocks, load } = {}) => {
2929
...procLog,
3030
display,
3131
displayLoad,
32-
streams: logs.streams,
3332
...logs.logs,
3433
}
3534
}
@@ -102,50 +101,6 @@ t.test('can do progress', async (t) => {
102101
t.strictSame(outputs, ['before input', 'during input', 'after input'])
103102
})
104103

105-
t.test('progress resume does not clear output when spinner inactive', async (t) => {
106-
const { input, output, outputs, streams } = await mockDisplay(t, {
107-
load: {
108-
progress: true,
109-
},
110-
})
111-
112-
const origClearLine = streams.stderr.clearLine
113-
const origCursorTo = streams.stderr.cursorTo
114-
let clearCalls = 0
115-
let cursorCalls = 0
116-
streams.stderr.clearLine = (...args) => {
117-
clearCalls++
118-
return origClearLine.call(streams.stderr, ...args)
119-
}
120-
streams.stderr.cursorTo = (...args) => {
121-
cursorCalls++
122-
return origCursorTo.call(streams.stderr, ...args)
123-
}
124-
125-
t.teardown(() => {
126-
streams.stderr.clearLine = origClearLine
127-
streams.stderr.cursorTo = origCursorTo
128-
})
129-
130-
// ensure the spinner has rendered at least once so progress.off clears it
131-
await timers.setTimeout(300)
132-
133-
const endInput = input.start()
134-
await timers.setTimeout(0)
135-
136-
clearCalls = 0
137-
cursorCalls = 0
138-
139-
output.standard('no trailing newline')
140-
141-
endInput()
142-
await timers.setTimeout(0)
143-
144-
t.equal(clearCalls, 0, 'resume does not clear the line when spinner inactive')
145-
t.equal(cursorCalls, 0, 'resume does not reposition cursor when spinner inactive')
146-
t.equal(outputs.at(-1), 'no trailing newline', 'output remains visible')
147-
})
148-
149104
t.test('handles log throwing', async (t) => {
150105
class ThrowInspect {
151106
#crashes = 0;

0 commit comments

Comments
 (0)