From 0726126d2837cd3a8f2115c4011456bfa9deeac5 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Thu, 9 Mar 2023 04:44:50 +0000 Subject: [PATCH] Add `inputFile` option (#542) --- docs/scripts.md | 2 +- index.d.ts | 18 ++++++++++++++++++ index.js | 8 ++++---- index.test-d.ts | 1 + lib/stream.js | 48 ++++++++++++++++++++++++++++++++++++++++-------- readme.md | 12 +++++++++++- test/stream.js | 26 ++++++++++++++++++++++++++ 7 files changed, 101 insertions(+), 14 deletions(-) diff --git a/docs/scripts.md b/docs/scripts.md index 6729c3aff5..a0af2d59b2 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -635,7 +635,7 @@ await cat ```js // Execa -await $({input: fs.createReadStream('file.txt')})`cat` +await $({inputFile: 'file.txt'})`cat` ``` ### Silent stderr diff --git a/index.d.ts b/index.d.ts index cb68179ffd..90d91f1272 100644 --- a/index.d.ts +++ b/index.d.ts @@ -258,15 +258,33 @@ export type CommonOptions = { export type Options = { /** Write some input to the `stdin` of your binary. + + If the input is a file, use the `inputFile` option instead. */ readonly input?: string | Buffer | ReadableStream; + + /** + Use a file as input to the the `stdin` of your binary. + + If the input is not a file, use the `input` option instead. + */ + readonly inputFile?: string; } & CommonOptions; export type SyncOptions = { /** Write some input to the `stdin` of your binary. + + If the input is a file, use the `inputFile` option instead. */ readonly input?: string | Buffer; + + /** + Use a file as input to the the `stdin` of your binary. + + If the input is not a file, use the `input` option instead. + */ + readonly inputFile?: string; } & CommonOptions; export type NodeOptions = { diff --git a/index.js b/index.js index e5525b9d4c..87c827624c 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ import {makeError} from './lib/error.js'; import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; import {addPipeMethods} from './lib/pipe.js'; -import {handleInput, getSpawnedResult, makeAllStream, validateInputSync} from './lib/stream.js'; +import {handleInput, getSpawnedResult, makeAllStream, handleInputSync} from './lib/stream.js'; import {mergePromise, getSpawnedPromise} from './lib/promise.js'; import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js'; import {logCommand, verboseDefault} from './lib/verbose.js'; @@ -159,7 +159,7 @@ export function execa(file, args, options) { const handlePromiseOnce = onetime(handlePromise); - handleInput(spawned, parsed.options.input); + handleInput(spawned, parsed.options); spawned.all = makeAllStream(spawned, parsed.options); @@ -174,11 +174,11 @@ export function execaSync(file, args, options) { const escapedCommand = getEscapedCommand(file, args); logCommand(escapedCommand, parsed.options); - validateInputSync(parsed.options); + const input = handleInputSync(parsed.options); let result; try { - result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options); + result = childProcess.spawnSync(parsed.file, parsed.args, {...parsed.options, input}); } catch (error) { throw makeError({ error, diff --git a/index.test-d.ts b/index.test-d.ts index 15208999d6..284545b4f9 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -134,6 +134,7 @@ execa('unicorns', {buffer: false}); execa('unicorns', {input: ''}); execa('unicorns', {input: Buffer.from('')}); execa('unicorns', {input: process.stdin}); +execa('unicorns', {inputFile: ''}); execa('unicorns', {stdin: 'pipe'}); execa('unicorns', {stdin: 'overlapped'}); execa('unicorns', {stdin: 'ipc'}); diff --git a/lib/stream.js b/lib/stream.js index 32bdefe9bc..5f79b791d9 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -1,9 +1,47 @@ +import {createReadStream, readFileSync} from 'node:fs'; import {isStream} from 'is-stream'; import getStream from 'get-stream'; import mergeStream from 'merge-stream'; -// `input` option -export const handleInput = (spawned, input) => { +const validateInputOptions = input => { + if (input !== undefined) { + throw new TypeError('The `input` and `inputFile` options cannot be both set.'); + } +}; + +const getInputSync = ({input, inputFile}) => { + if (typeof inputFile !== 'string') { + return input; + } + + validateInputOptions(input); + return readFileSync(inputFile); +}; + +// `input` and `inputFile` option in sync mode +export const handleInputSync = options => { + const input = getInputSync(options); + + if (isStream(input)) { + throw new TypeError('The `input` option cannot be a stream in sync mode'); + } + + return input; +}; + +const getInput = ({input, inputFile}) => { + if (typeof inputFile !== 'string') { + return input; + } + + validateInputOptions(input); + return createReadStream(inputFile); +}; + +// `input` and `inputFile` option in async mode +export const handleInput = (spawned, options) => { + const input = getInput(options); + if (input === undefined) { return; } @@ -79,9 +117,3 @@ export const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, ]); } }; - -export const validateInputSync = ({input}) => { - if (isStream(input)) { - throw new TypeError('The `input` option cannot be a stream in sync mode'); - } -}; diff --git a/readme.md b/readme.md index e51a3b6cfc..4f820e21c6 100644 --- a/readme.md +++ b/readme.md @@ -128,7 +128,7 @@ await execa('echo', ['unicorns'], {all:true}).pipeAll('all.txt'); import {execa} from 'execa'; // Similar to `cat < stdin.txt` in Bash -const {stdout} = await execa('cat', {input:fs.createReadStream('stdin.txt')}); +const {stdout} = await execa('cat', {inputFile:'stdin.txt'}); console.log(stdout); //=> 'unicorns' ``` @@ -497,6 +497,16 @@ Type: `string | Buffer | stream.Readable` Write some input to the `stdin` of your binary.\ Streams are not allowed when using the synchronous methods. +If the input is a file, use the [`inputFile` option](#inputfile) instead. + +#### inputFile + +Type: `string` + +Use a file as input to the the `stdin` of your binary. + +If the input is not a file, use the [`input` option](#input) instead. + #### stdin Type: `string | number | Stream | undefined`\ diff --git a/test/stream.js b/test/stream.js index aeceedb0d4..3d2c187fd2 100644 --- a/test/stream.js +++ b/test/stream.js @@ -71,6 +71,19 @@ test('input can be a Stream', async t => { t.is(stdout, 'howdy'); }); +test('inputFile can be set', async t => { + const inputFile = tempfile(); + fs.writeFileSync(inputFile, 'howdy'); + const {stdout} = await execa('stdin.js', {inputFile}); + t.is(stdout, 'howdy'); +}); + +test('inputFile and input cannot be both set', t => { + t.throws(() => execa('stdin.js', {inputFile: '', input: ''}), { + message: /cannot be both set/, + }); +}); + test('you can write to child.stdin', async t => { const subprocess = execa('stdin.js'); subprocess.stdin.end('unicorns'); @@ -105,6 +118,19 @@ test('helpful error trying to provide an input stream in sync mode', t => { ); }); +test('inputFile can be set - sync', t => { + const inputFile = tempfile(); + fs.writeFileSync(inputFile, 'howdy'); + const {stdout} = execaSync('stdin.js', {inputFile}); + t.is(stdout, 'howdy'); +}); + +test('inputFile and input cannot be both set - sync', t => { + t.throws(() => execaSync('stdin.js', {inputFile: '', input: ''}), { + message: /cannot be both set/, + }); +}); + test('maxBuffer affects stdout', async t => { await t.notThrowsAsync(execa('max-buffer.js', ['stdout', '10'], {maxBuffer: 10})); const {stdout, all} = await t.throwsAsync(execa('max-buffer.js', ['stdout', '11'], {maxBuffer: 10, all: true}), {message: /max-buffer.js stdout/});