Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export async function read<T extends string | number = string> ({
}
/* c8 ignore stop */

let finished = false

const done = () => {
finished = true
rl.close()
clearTimeout(timer)
m.mute()
Expand Down Expand Up @@ -121,6 +124,19 @@ export async function read<T extends string | number = string> ({
/* c8 ignore stop */
})

// readline emits 'close' without ever emitting 'line' when the input stream
// ends first, which is what happens any time stdin is not something a user
// can type into: /dev/null, an already closed pipe, a child process spawned
// without stdin. Settle the promise here or it never settles at all, and the
// caller waits on it until the event loop empties and the process exits with
// no error to report. Treated as a cancel, same as SIGINT, since both mean no
// answer is coming.
rl.on('close', () => {
if (!finished) {
onError(new Error('canceled'))
}
})

// TODO: add tests for sigint
/* c8 ignore start */
rl.on('SIGINT', () => {
Expand Down
20 changes: 20 additions & 0 deletions test/basic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test } from 'node:test'
import { strict as assert } from 'node:assert'
import { PassThrough } from 'node:stream'
import { read } from '../src/read.ts'
import spawnRead from './fixtures/setup.ts'

Expand Down Expand Up @@ -73,6 +74,25 @@ const main = () => {
// @ts-expect-error
await assert.rejects(() => read({ default: {} }))
})

test('input stream ends without a line', async () => {
const input = new PassThrough()
const output = new PassThrough()
input.end()

await assert.rejects(
() => read({ prompt: 'Username:', input, output }),
/canceled/
)
})

test('input stream ends after a line still resolves', async () => {
const input = new PassThrough()
const output = new PassThrough()
input.end('a user\n')

assert.equal(await read({ prompt: 'Username:', input, output }), 'a user')
})
}

if (process.argv[2] === 'child') {
Expand Down