Skip to content

Commit

Permalink
feat: add spinner
Browse files Browse the repository at this point in the history
Closes #7425
  • Loading branch information
lukekarrys committed Apr 27, 2024
1 parent 762888a commit debba88
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 104 deletions.
8 changes: 2 additions & 6 deletions lib/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const npa = require('npm-package-arg')
const libexec = require('libnpmexec')
const mapWorkspaces = require('@npmcli/map-workspaces')
const PackageJson = require('@npmcli/package-json')
const { log, output } = require('proc-log')
const { log, output, input } = require('proc-log')
const updateWorkspaces = require('../utils/update-workspaces.js')
const BaseCommand = require('../base-cmd.js')

Expand Down Expand Up @@ -148,8 +148,6 @@ class Init extends BaseCommand {
}

async template (path = process.cwd()) {
log.pause()

const initFile = this.npm.config.get('init-module')
if (!this.npm.config.get('yes') && !this.npm.config.get('force')) {
output.standard([
Expand All @@ -167,7 +165,7 @@ class Init extends BaseCommand {
}

try {
const data = await initJson(path, initFile, this.npm.config)
const data = await input.start(() => initJson(path, initFile, this.npm.config))
log.silly('package data', data)
return data
} catch (er) {
Expand All @@ -176,8 +174,6 @@ class Init extends BaseCommand {
} else {
throw er
}
} finally {
log.resume()
}
}

Expand Down
4 changes: 4 additions & 0 deletions lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ class Npm {
return flat
}

get interactive () {
return process.stdin.isTTY === true && process.stdout.isTTY === true
}

// color and logColor are a special derived values that takes into
// consideration not only the config, but whether or not we are operating
// in a tty with the associated output (stdout/stderr)
Expand Down
8 changes: 4 additions & 4 deletions lib/utils/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ const adduser = async (npm, { creds, ...opts }) => {
let res
if (authType === 'web') {
try {
res = await profile.adduserWeb((url, emitter) => {
res = await profile.adduserWeb((url, signal) => {
openUrlPrompt(
npm,
url,
'Create your account at',
'Press ENTER to open in the browser...',
emitter
signal
)
}, opts)
} catch (err) {
Expand Down Expand Up @@ -56,13 +56,13 @@ const login = async (npm, { creds, ...opts }) => {
let res
if (authType === 'web') {
try {
res = await profile.loginWeb((url, emitter) => {
res = await profile.loginWeb((url, signal) => {
openUrlPrompt(
npm,
url,
'Login at',
'Press ENTER to open in the browser...',
emitter
signal
)
}, opts)
} catch (err) {
Expand Down
165 changes: 141 additions & 24 deletions lib/utils/display.js
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')

Expand Down Expand Up @@ -137,6 +137,7 @@ 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)
}

off () {
Expand All @@ -146,9 +147,8 @@ 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()

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - macOS - 18.x

Cannot read properties of undefined (reading 'off')

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - macOS - 20.x

Cannot read properties of undefined (reading 'off')

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - macOS - 18.17.0

Cannot read properties of undefined (reading 'off')

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - macOS - 20.5.0

Cannot read properties of undefined (reading 'off')

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - Linux - 20.x

Cannot read properties of undefined (reading 'off')

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - Linux - 18.x

Cannot read properties of undefined (reading 'off')

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - Linux - 18.17.0

Cannot read properties of undefined (reading 'off')

Check failure on line 151 in lib/utils/display.js

View workflow job for this annotation

GitHub Actions / Test - Linux - 20.5.0

Cannot read properties of undefined (reading 'off')
}

get chalk () {
Expand Down Expand Up @@ -207,12 +207,12 @@ class Display {
// 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) {
this.#progress.clear()
stream.write(formatWithOptions({
colors: stream === this.#stdout ? this.#stdoutColor : this.#stderrColor,
...options,
}, ...args))
}

// HANDLERS
Expand Down Expand Up @@ -259,6 +259,9 @@ class Display {
)
} else {
this.#outputState.buffer.forEach((item) => this.#writeOutput(...item))
if (args.length) {
this.#outputState.buffer.push([output.KEYS.standard, meta, ...args])
}
}

this.#outputState.buffer.length = 0
Expand All @@ -277,10 +280,8 @@ class Display {

// 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 arg = args[0]
const isBanner = typeof arg === 'string' && arg.startsWith('\n> ') && arg.endsWith('\n')
const hideBanner = this.#silent || ['exec', 'explore'].includes(this.#command)
if (isBanner && hideBanner) {
return
Expand All @@ -289,16 +290,31 @@ class Display {
this.#writeOutput(level, meta, ...args)
})

#inputHandler = withMeta((level, meta, ...args) => {
if (level === input.KEYS.start) {
log.pause()
this.#outputState.buffering = true
this.#progress.pause()
return
}

if (level === input.KEYS.end) {
log.resume()
output.flush()
this.#progress.resume()
}
})

// OUTPUT

#writeOutput (level, meta, ...args) {
if (level === output.KEYS.standard) {
this.#stdoutWrite({}, ...args)
this.#write(this.#stdout, {}, ...args)
return
}

if (level === output.KEYS.error) {
this.#stderrWrite({}, ...args)
this.#write(this.#stderr, {}, ...args)
}
}

Expand Down Expand Up @@ -344,22 +360,123 @@ 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)
}
}

// PROGRESS

#startProgress ({ progress, unicode }) {
if (!progress || this.#silent) {
this.#progress = new Progress({
enabled: !!progress && !this.#silent,
unicode,
stream: this.#stderr,
})
}
}

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

#frame = 0
#running = false
#timeout
#interval

constructor ({ enabled, stream, unicode }) {
if (!enabled) {
return
}

this.#client = proggy.createClient({ normalize: true })
this.#stream = stream
this.#spinner = unicode ? Progress.dots : Progress.lines
this.#render({ delay: 500 })
}

off () {
if (!this.#stream) {
return
}
this.#clear()
}

pause () {
if (!this.#stream) {
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
this.#clear({ clearLine: true })
}

resume () {
if (!this.#stream) {
return
}
this.#render()
}

persist (stream, value) {
if (this.#stream) {
this.#stream.cursorTo(0)
}
stream.write(value)
if (this.#stream) {
this.#render({ delay: 10 })
}
}

clear () {
if (!this.#stream) {
return
}
this.#stream.cursorTo(0)
}

#clear ({ clearLine } = {}) {
this.#running = false
this.#stream.cursorTo(0)
if (clearLine) {
this.#stream.clearLine(1)
}
clearTimeout(this.#timeout)
clearInterval(this.#interval)
}

#render ({ delay } = {}) {
if (delay) {
this.#stream.cursorTo(0)
this.#stream.clearLine(1)
clearTimeout(this.#timeout)
this.#timeout = setTimeout(() => this.#render(), delay)
return
}
this.#running = true
this.#renderFrame()
clearInterval(this.#interval)
this.#interval = setInterval(() => this.#renderFrame(), this.#spinner.duration)
// this.#interval.unref()
}

#renderFrame () {
if (this.#running) {
this.#stream.cursorTo(0)
this.#stream.write(this.#spinner.frames[this.#nextFrame()])
}
}

#nextFrame () {
if (this.#frame >= this.#spinner.frames.length) {
this.#frame = 0
}
return this.#frame++
}
}

Expand Down
5 changes: 5 additions & 0 deletions lib/utils/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ function STRIP_C01 (str) {

const formatWithOptions = ({ prefix: prefixes = [], eol = '\n', ...options }, ...args) => {
const prefix = prefixes.filter(p => p != null).join(' ')
// We output an empty string in some places which means newline. This made sense when we
// wrote via console.log as that would always output a newline.
if (args.length === 1 && args[0] === '') {
return eol
}
const formatted = STRIP_C01(baseFormatWithOptions(options, ...args))
// Splitting could be changed to only `\n` once we are sure we only emit unix newlines.
// The eol param to this function will put the correct newlines in place for the returned string.
Expand Down

0 comments on commit debba88

Please sign in to comment.