Skip to content

Commit 35e8d38

Browse files
authored
fix: properly handle newlines with input when using the spinner (#8322)
1 parent ffc9b71 commit 35e8d38

File tree

6 files changed

+103
-11
lines changed

6 files changed

+103
-11
lines changed

lib/utils/display.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -333,20 +333,37 @@ class Display {
333333
this.#progress.off()
334334
break
335335

336-
case input.KEYS.end:
336+
case input.KEYS.end: {
337337
log.resume()
338+
// For silent prompts (like password), add newline to preserve output
339+
if (meta?.silent) {
340+
output.standard('')
341+
}
338342
output.flush()
339343
this.#progress.resume()
340344
break
345+
}
341346

342347
case input.KEYS.read: {
343-
// 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.
348+
// 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.
344349
const [res, rej, p] = args
345-
return input.start(() => p()
346-
.then(res)
347-
.catch(rej)
348-
// 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.
349-
.finally(() => output.standard('')))
350+
351+
// Use sequential input management to avoid race condition which causes issues with spinner and adding newlines.
352+
input.start()
353+
354+
return p()
355+
.then((result) => {
356+
// If user hits enter, process end event and return input.
357+
input.end({ [META]: true, silent: meta?.silent })
358+
res(result)
359+
return result
360+
})
361+
.catch((error) => {
362+
// If user hits ctrl+c, add newline to preserve output.
363+
output.standard('')
364+
input.end()
365+
rej(error)
366+
})
350367
}
351368
}
352369
})

lib/utils/read-user-info.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { read: _read } = require('read')
22
const userValidate = require('npm-user-validate')
3-
const { log, input } = require('proc-log')
3+
const { log, input, META } = require('proc-log')
44

55
const otpPrompt = `This command requires a one-time password (OTP) from your authenticator app.
66
Enter one below. You can also pass one on the command line by appending --otp=123456.
@@ -11,7 +11,9 @@ const passwordPrompt = 'npm password: '
1111
const usernamePrompt = 'npm username: '
1212
const emailPrompt = 'email (this will be public): '
1313

14-
const read = (...args) => input.read(() => _read(...args))
14+
// Pass options through so we can differentiate between regular and silent prompts
15+
const read = (options) =>
16+
input.read(() => _read(options), { [META]: true, silent: options?.silent })
1517

1618
function readOTP (msg = otpPrompt, otp, isRetry) {
1719
if (isRetry && otp && /^[\d ]+$|^[A-Fa-f0-9]{64,64}$/.test(otp)) {

tap-snapshots/test/lib/commands/init.js.test.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,5 @@ Press ^C at any time to quit.
2020

2121
exports[`test/lib/commands/init.js TAP workspaces no args -- yes > should print helper info 1`] = `
2222
23-
2423
added 1 package in {TIME}
2524
`

tap-snapshots/test/lib/utils/open-url.js.test.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ https://www.npmjs.com
2525
exports[`test/lib/utils/open-url.js TAP open url prompt does not error when opener cannot find command > Outputs extra Browser unavailable message and url 1`] = `
2626
npm home:
2727
https://www.npmjs.com
28-
2928
Browser unavailable. Please open the URL manually:
3029
https://www.npmjs.com
3130
`

test/lib/utils/display.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,46 @@ t.test('Display.clean', async (t) => {
270270
clearOutput()
271271
}
272272
})
273+
274+
t.test('prompt functionality', async t => {
275+
t.test('regular prompt completion works', async t => {
276+
const { input } = await mockDisplay(t)
277+
278+
const result = await input.read(() => Promise.resolve('user-input'))
279+
280+
t.equal(result, 'user-input', 'should return the input result')
281+
})
282+
283+
t.test('silent prompt completion works', async t => {
284+
const { input } = await mockDisplay(t)
285+
286+
const result = await input.read(
287+
() => Promise.resolve('secret-password'),
288+
{ silent: true }
289+
)
290+
291+
t.equal(result, 'secret-password', 'should return the input result for silent prompts')
292+
})
293+
294+
t.test('metadata is correctly passed through', async t => {
295+
const { input } = await mockDisplay(t)
296+
297+
await input.read(
298+
() => Promise.resolve('result1'),
299+
{ silent: false }
300+
)
301+
t.pass('should handle silent false option')
302+
303+
await input.read(
304+
() => Promise.resolve('result2'),
305+
{}
306+
)
307+
t.pass('should handle empty options')
308+
309+
await input.read(
310+
() => Promise.resolve('result3'),
311+
{ silent: true }
312+
)
313+
t.pass('should handle silent true option')
314+
})
315+
})

test/lib/utils/read-user-info.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,35 @@ t.test('email - invalid warns and retries', async (t) => {
118118
t.equal(result, 'foo@bar.baz', 'received the email')
119119
t.equal(logMsg, 'invalid email')
120120
})
121+
122+
t.test('read-user-info integration works', async (t) => {
123+
t.teardown(() => {
124+
readResult = null
125+
readOpts = null
126+
})
127+
128+
readResult = 'regular-input'
129+
const username = await readUserInfo.username('Username: ')
130+
t.equal(username, 'regular-input', 'should return username from regular prompt')
131+
t.notOk(readOpts.silent, 'username prompt should not set silent')
132+
133+
readResult = 'secret-password'
134+
const password = await readUserInfo.password('Password: ')
135+
t.equal(password, 'secret-password', 'should return password from silent prompt')
136+
t.match(readOpts, { silent: true }, 'password prompt should set silent: true')
137+
})
138+
139+
t.test('silent metadata is passed correctly by read-user-info', async (t) => {
140+
t.teardown(() => {
141+
readResult = null
142+
readOpts = null
143+
})
144+
145+
readResult = 'username'
146+
await readUserInfo.username('Username: ')
147+
t.notOk(readOpts?.silent, 'username prompt should not set silent')
148+
149+
readResult = 'password'
150+
await readUserInfo.password('Password: ')
151+
t.equal(readOpts?.silent, true, 'password prompt should set silent: true')
152+
})

0 commit comments

Comments
 (0)