Repository: sindresorhus/execa
Pull request #669 (2024-01-09 23:42:53)

Improve documentation of synchronous methods
The synchronous methods have multiple limitations. At the moment, those are documented in each method or option that has the limitation. 

This PR lists all the limitations directly in the documentation of the synchronous methods instead. This makes the documentation of the methods and options simpler. This also makes it more discouraging to use the synchronous methods by listing their limitations upfront.


Patch:
diff --git a/index.d.ts b/index.d.ts
index 9175f5a..ca8b7cb 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -118,12 +118,10 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	- `'inherit'`: Re-use the current process' `stdin`.
 	- an integer: Re-use a specific file descriptor from the current process.
 	- a Node.js `Readable` stream.
-
-	Unless the synchronous methods are used, the value can also be a:
-	- file path. If relative, it must start with `.`.
-	- file URL.
-	- web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
-	- [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) or [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
+	- a file path. If relative, it must start with `.`.
+	- a file URL.
+	- a web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
+	- an [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) or an [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
 
 	This can be an [array of values](https://github.com/sindresorhus/execa#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 
@@ -140,11 +138,9 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	- `'inherit'`: Re-use the current process' `stdout`.
 	- an integer: Re-use a specific file descriptor from the current process.
 	- a Node.js `Writable` stream.
-
-	Unless the synchronous methods are used, the value can also be a:
-	- file path. If relative, it must start with `.`.
-	- file URL.
-	- web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
+	- a file path. If relative, it must start with `.`.
+	- a file URL.
+	- a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 
 	This can be an [array of values](https://github.com/sindresorhus/execa#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 
@@ -161,11 +157,9 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	- `'inherit'`: Re-use the current process' `stderr`.
 	- an integer: Re-use a specific file descriptor from the current process.
 	- a Node.js `Writable` stream.
-
-	Unless the synchronous methods are used, the value can also be a:
-	- file path. If relative, it must start with `.`.
-	- file URL.
-	- web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
+	- a file path. If relative, it must start with `.`.
+	- a file URL.
+	- a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 
 	This can be an [array of values](https://github.com/sindresorhus/execa#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 
@@ -351,8 +345,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 
 export type Options<EncodingType extends EncodingOption = DefaultEncodingOption> = {
 	/**
-	Write some input to the child process' `stdin`.\
-	Streams are not allowed when using the synchronous methods.
+	Write some input to the child process' `stdin`.
 
 	See also the `inputFile` and `stdin` options.
 	*/
@@ -493,7 +486,6 @@ export type ExecaReturnValue<StdoutStderrType extends StdoutStderrAll = string>
 
 	This is `undefined` if either:
 	- the `all` option is `false` (default value)
-	- the synchronous methods are used
   - both `stdout` and `stderr` options are set to [`'inherit'`, `'ipc'`, `'ignore'`, `Stream` or `integer`](https://nodejs.org/api/child_process.html#child_process_options_stdio)
 	*/
 	all?: StdoutStderrType;
@@ -560,7 +552,6 @@ export type ExecaChildPromise<StdoutStderrType extends StdoutStderrAll> = {
 
 	This is `undefined` if either:
 	- the `all` option is `false` (the default value)
-	- the synchronous methods are used
 	- both `stdout` and `stderr` options are set to [`'inherit'`, `'ipc'`, `'ignore'`, `Stream` or `integer`](https://nodejs.org/api/child_process.html#child_process_options_stdio)
 	*/
 	all?: Readable;
@@ -737,6 +728,8 @@ export function execa<EncodingType extends EncodingOption = DefaultEncodingOptio
 /**
 Same as `execa()` but synchronous.
 
+Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `serialization` and `signal`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be a stream nor an iterable.
+
 @param file - The program/script to execute, as a string or file URL
 @param arguments - Arguments to pass to `file` on execution.
 @returns A `childProcessResult` object
@@ -833,6 +826,8 @@ export function execaCommand<EncodingType extends EncodingOption = DefaultEncodi
 /**
 Same as `execaCommand()` but synchronous.
 
+Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `serialization` and `signal`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be a stream nor an iterable.
+
 @param command - The program/script to execute and its arguments.
 @returns A `childProcessResult` object
 @throws A `childProcessResult` error
@@ -892,6 +887,8 @@ type Execa$<StdoutStderrType extends StdoutStderrAll = string> = {
 	/**
 	Same as $\`command\` but synchronous.
 
+	Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `serialization` and `signal`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be a stream nor an iterable.
+
 	@returns A `childProcessResult` object
 	@throws A `childProcessResult` error
 
@@ -942,6 +939,8 @@ type Execa$<StdoutStderrType extends StdoutStderrAll = string> = {
 	/**
 	Same as $\`command\` but synchronous.
 
+	Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `serialization` and `signal`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be a stream nor an iterable.
+
 	@returns A `childProcessResult` object
 	@throws A `childProcessResult` error
 
diff --git a/index.js b/index.js
index 8bc872c..b702988 100644
--- a/index.js
+++ b/index.js
@@ -16,6 +16,7 @@ import {getSpawnedResult, makeAllStream} from './lib/stream.js';
 import {mergePromise} from './lib/promise.js';
 import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js';
 import {logCommand, verboseDefault} from './lib/verbose.js';
+import {bufferToUint8Array} from './lib/stdio/utils.js';
 
 const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100;
 
@@ -81,7 +82,7 @@ const handleArguments = (rawFile, rawArgs, rawOptions = {}) => {
 
 const handleOutputSync = (options, value, error) => {
 	if (Buffer.isBuffer(value)) {
-		value = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
+		value = bufferToUint8Array(value);
 	}
 
 	return handleOutput(options, value, error);
diff --git a/lib/stdio/async.js b/lib/stdio/async.js
index f09079b..e9e30ae 100644
--- a/lib/stdio/async.js
+++ b/lib/stdio/async.js
@@ -12,7 +12,8 @@ const addPropertiesAsync = {
 		filePath: ({value}) => ({value: createReadStream(value)}),
 		webStream: ({value}) => ({value: Readable.fromWeb(value)}),
 		iterable: ({value}) => ({value: Readable.from(value)}),
-		stringOrBuffer: ({value}) => ({value: Readable.from(ArrayBuffer.isView(value) ? Buffer.from(value) : value)}),
+		string: ({value}) => ({value: Readable.from(value)}),
+		uint8Array: ({value}) => ({value: Readable.from(Buffer.from(value))}),
 	},
 	output: {
 		filePath: ({value}) => ({value: createWriteStream(value)}),
diff --git a/lib/stdio/direction.js b/lib/stdio/direction.js
index c3b4e96..4ecb169 100644
--- a/lib/stdio/direction.js
+++ b/lib/stdio/direction.js
@@ -26,7 +26,7 @@ const getStreamDirection = stdioStream => KNOWN_DIRECTIONS[stdioStream.index] ??
 // `stdin`/`stdout`/`stderr` have a known direction
 const KNOWN_DIRECTIONS = ['input', 'output', 'output'];
 
-// `stringOrBuffer` type always applies to `stdin`, i.e. does not need to be handled here
+// `string` and `uint8Array` types always applies to `stdin`, i.e. does not need to be handled here
 const guessStreamDirection = {
 	filePath: () => undefined,
 	iterable: () => 'input',
diff --git a/lib/stdio/input.js b/lib/stdio/input.js
index b94ff6f..e452262 100644
--- a/lib/stdio/input.js
+++ b/lib/stdio/input.js
@@ -1,4 +1,5 @@
-import {isStream as isNodeStream} from 'is-stream';
+import {Buffer} from 'node:buffer';
+import {isReadableStream} from 'is-stream';
 
 // Append the `stdin` option with the `input` and `inputFile` options
 export const handleInputOptions = ({input, inputFile}) => [
@@ -6,14 +7,30 @@ export const handleInputOptions = ({input, inputFile}) => [
 	handleInputFileOption(inputFile),
 ].filter(Boolean);
 
-const handleInputOption = input => input && {
-	type: isNodeStream(input) ? 'nodeStream' : 'stringOrBuffer',
+const handleInputOption = input => input === undefined ? undefined : {
+	type: getType(input),
 	value: input,
 	optionName: 'input',
 	index: 0,
 };
 
-const handleInputFileOption = inputFile => inputFile && {
+const getType = input => {
+	if (isReadableStream(input)) {
+		return 'nodeStream';
+	}
+
+	if (typeof input === 'string') {
+		return 'string';
+	}
+
+	if (Object.prototype.toString.call(input) === '[object Uint8Array]' && !Buffer.isBuffer(input)) {
+		return 'uint8Array';
+	}
+
+	throw new Error('The `input` option must be a string, a Uint8Array or a Node.js Readable stream.');
+};
+
+const handleInputFileOption = inputFile => inputFile === undefined ? undefined : {
 	type: 'filePath',
 	value: inputFile,
 	optionName: 'inputFile',
diff --git a/lib/stdio/sync.js b/lib/stdio/sync.js
index 311c051..751878f 100644
--- a/lib/stdio/sync.js
+++ b/lib/stdio/sync.js
@@ -2,6 +2,7 @@ import {readFileSync, writeFileSync} from 'node:fs';
 import {isStream as isNodeStream} from 'is-stream';
 import {handleInput} from './handle.js';
 import {TYPE_TO_MESSAGE} from './type.js';
+import {bufferToUint8Array} from './utils.js';
 
 // Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in sync mode
 export const handleInputSync = options => {
@@ -24,7 +25,7 @@ const forbiddenIfSync = ({type, optionName}) => {
 
 const addPropertiesSync = {
 	input: {
-		filePath: ({value}) => ({value: readFileSync(value, 'utf8'), type: 'stringOrBuffer'}),
+		filePath: ({value}) => ({value: bufferToUint8Array(readFileSync(value)), type: 'uint8Array'}),
 		webStream: forbiddenIfSync,
 		nodeStream: forbiddenIfSync,
 		iterable: forbiddenIfSync,
@@ -39,17 +40,17 @@ const addPropertiesSync = {
 };
 
 const addInputOptionSync = (stdioStreams, options) => {
-	const inputs = stdioStreams.filter(({type}) => type === 'stringOrBuffer');
+	const inputs = stdioStreams.filter(({type}) => type === 'string' || type === 'uint8Array');
 	if (inputs.length === 0) {
 		return;
 	}
 
 	options.input = inputs.length === 1
 		? inputs[0].value
-		: inputs.map(({value}) => serializeInput(value)).join('');
+		: inputs.map(stdioStream => serializeInput(stdioStream)).join('');
 };
 
-const serializeInput = value => typeof value === 'string' ? value : new TextDecoder().decode(value);
+const serializeInput = ({type, value}) => type === 'string' ? value : new TextDecoder().decode(value);
 
 // Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, after spawning, in sync mode
 export const pipeOutputSync = (stdioStreams, result) => {
diff --git a/lib/stdio/type.js b/lib/stdio/type.js
index 47838a0..711e3ce 100644
--- a/lib/stdio/type.js
+++ b/lib/stdio/type.js
@@ -49,5 +49,6 @@ export const TYPE_TO_MESSAGE = {
 	nodeStream: 'a Node.js stream',
 	native: 'any value',
 	iterable: 'an iterable',
-	stringOrBuffer: 'a string or Uint8Array',
+	string: 'a string',
+	uint8Array: 'a Uint8Array',
 };
diff --git a/lib/stdio/utils.js b/lib/stdio/utils.js
new file mode 100644
index 0000000..1cda911
--- /dev/null
+++ b/lib/stdio/utils.js
@@ -0,0 +1,1 @@
+export const bufferToUint8Array = buffer => new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
diff --git a/readme.md b/readme.md
index 5a10fa0..5d8d167 100644
--- a/readme.md
+++ b/readme.md
@@ -289,6 +289,8 @@ This is the preferred method when executing a user-supplied `command` string, su
 
 Same as [`execa()`](#execacommandcommand-options) but synchronous.
 
+Cannot use the following options: [`all`](#all-2), [`cleanup`](#cleanup), [`buffer`](#buffer), [`detached`](#detached), [`serialization`](#serialization) and [`signal`](#signal). Also, the [`stdin`](#stdin), [`stdout`](#stdout-1), [`stderr`](#stderr-1), [`stdio`](#stdio) and [`input`](#input) options cannot be a stream nor an iterable.
+
 Returns or throws a [`childProcessResult`](#childProcessResult).
 
 ### $.sync\`command\`
@@ -296,12 +298,16 @@ Returns or throws a [`childProcessResult`](#childProcessResult).
 
 Same as [$\`command\`](#command) but synchronous.
 
+Cannot use the following options: [`all`](#all-2), [`cleanup`](#cleanup), [`buffer`](#buffer), [`detached`](#detached), [`serialization`](#serialization) and [`signal`](#signal). Also, the [`stdin`](#stdin), [`stdout`](#stdout-1), [`stderr`](#stderr-1), [`stdio`](#stdio) and [`input`](#input) options cannot be a stream nor an iterable.
+
 Returns or throws a [`childProcessResult`](#childProcessResult).
 
 ### execaCommandSync(command, options?)
 
 Same as [`execaCommand()`](#execacommand-command-options) but synchronous.
 
+Cannot use the following options: [`all`](#all-2), [`cleanup`](#cleanup), [`buffer`](#buffer), [`detached`](#detached), [`serialization`](#serialization) and [`signal`](#signal). Also, the [`stdin`](#stdin), [`stdout`](#stdout-1), [`stderr`](#stderr-1), [`stdio`](#stdio) and [`input`](#input) options cannot be a stream nor an iterable.
+
 Returns or throws a [`childProcessResult`](#childProcessResult).
 
 ### Shell syntax
@@ -337,7 +343,6 @@ Stream [combining/interleaving](#ensuring-all-output-is-interleaved) [`stdout`](
 
 This is `undefined` if either:
 - the [`all` option](#all-2) is `false` (the default value)
-- the [synchronous methods](#execasyncfile-arguments-options) are used
 - both [`stdout`](#stdout-1) and [`stderr`](#stderr-1) options are set to [`'inherit'`, `'ipc'`, `'ignore'`, `Stream` or `integer`](https://nodejs.org/api/child_process.html#child_process_options_stdio)
 
 #### pipeStdout(target)
@@ -423,7 +428,6 @@ The output of the process with `stdout` and `stderr` [interleaved](#ensuring-all
 
 This is `undefined` if either:
 - the [`all` option](#all-2) is `false` (the default value)
-- the [synchronous methods](#execasyncfile-arguments-options) are used
 - both [`stdout`](#stdout-1) and [`stderr`](#stderr-1) options are set to [`'inherit'`, `'ipc'`, `'ignore'`, `Stream` or `integer`](https://nodejs.org/api/child_process.html#child_process_options_stdio)
 
 #### failed
@@ -554,8 +558,7 @@ If the spawned process fails, [`error.stdout`](#stdout), [`error.stderr`](#stder
 
 Type: `string | Uint8Array | stream.Readable`
 
-Write some input to the child process' `stdin`.\
-Streams are not allowed when using the [synchronous methods](#execasyncfile-arguments-options).
+Write some input to the child process' `stdin`.
 
 See also the [`inputFile`](#inputfile) and [`stdin`](#stdin) options.
 
@@ -580,12 +583,10 @@ Default: `inherit` with [`$`](#command), `pipe` otherwise
 - `'inherit'`: Re-use the current process' `stdin`.
 - an integer: Re-use a specific file descriptor from the current process.
 - a [Node.js `Readable` stream](#redirect-a-nodejs-stream-fromto-stdinstdoutstderr).
-
-Unless the [synchronous methods](#execasyncfile-arguments-options) are used, the value can also be a:
-- file path. If relative, it must start with `.`.
-- file URL.
-- web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
-- [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) or [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
+- a file path. If relative, it must start with `.`.
+- a file URL.
+- a web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
+- an [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) or an [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
 
 This can be an [array of values](#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 
@@ -602,11 +603,9 @@ Default: `pipe`
 - `'inherit'`: Re-use the current process' `stdout`.
 - an integer: Re-use a specific file descriptor from the current process.
 - a [Node.js `Writable` stream](#redirect-a-nodejs-stream-fromto-stdinstdoutstderr).
-
-Unless the [synchronous methods](#execasyncfile-arguments-options) are used, the value can also be a:
-- file path. If relative, it must start with `.`.
-- file URL.
-- web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
+- a file path. If relative, it must start with `.`.
+- a file URL.
+- a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 
 This can be an [array of values](#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 
@@ -623,11 +622,9 @@ Default: `pipe`
 - `'inherit'`: Re-use the current process' `stderr`.
 - an integer: Re-use a specific file descriptor from the current process.
 - a [Node.js `Writable` stream](#redirect-a-nodejs-stream-fromto-stdinstdoutstderr).
-
-Unless the [synchronous methods](#execasyncfile-arguments-options) are used, the value can also be a:
-- file path. If relative, it must start with `.`.
-- file URL.
-- web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
+- a file path. If relative, it must start with `.`.
+- a file URL.
+- a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 
 This can be an [array of values](#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 


Test patch:
diff --git a/test/stdio/input.js b/test/stdio/input.js
index 4a53df4..7247915 100644
--- a/test/stdio/input.js
+++ b/test/stdio/input.js
@@ -1,3 +1,5 @@
+import {Buffer} from 'node:buffer';
+import {Writable} from 'node:stream';
 import test from 'ava';
 import {execa, execaSync, $} from '../../index.js';
 import {setFixtureDir} from '../helpers/fixtures-dir.js';
@@ -7,6 +9,10 @@ setFixtureDir();
 
 const textEncoder = new TextEncoder();
 const binaryFoobar = textEncoder.encode('foobar');
+const bufferFoobar = Buffer.from(binaryFoobar);
+const arrayBufferFoobar = binaryFoobar.buffer;
+const dataViewFoobar = new DataView(arrayBufferFoobar);
+const uint16ArrayFoobar = new Uint16Array(arrayBufferFoobar);
 
 const testInput = async (t, input, execaMethod) => {
 	const {stdout} = await execaMethod('stdin.js', {input});
@@ -25,3 +31,26 @@ const testInputScript = async (t, getExecaMethod) => {
 
 test('input option can be used with $', testInputScript, identity);
 test('input option can be used with $.sync', testInputScript, getScriptSync);
+
+const testInvalidInput = async (t, input, execaMethod) => {
+	t.throws(() => {
+		execaMethod('noop.js', {input});
+	}, {message: /a string, a Uint8Array/});
+};
+
+test('input option cannot be a Buffer', testInvalidInput, bufferFoobar, execa);
+test('input option cannot be an ArrayBuffer', testInvalidInput, arrayBufferFoobar, execa);
+test('input option cannot be a DataView', testInvalidInput, dataViewFoobar, execa);
+test('input option cannot be a Uint16Array', testInvalidInput, uint16ArrayFoobar, execa);
+test('input option cannot be 0', testInvalidInput, 0, execa);
+test('input option cannot be false', testInvalidInput, false, execa);
+test('input option cannot be null', testInvalidInput, null, execa);
+test('input option cannot be a non-Readable stream', testInvalidInput, new Writable(), execa);
+test('input option cannot be a Buffer - sync', testInvalidInput, bufferFoobar, execaSync);
+test('input option cannot be an ArrayBuffer - sync', testInvalidInput, arrayBufferFoobar, execaSync);
+test('input option cannot be a DataView - sync', testInvalidInput, dataViewFoobar, execaSync);
+test('input option cannot be a Uint16Array - sync', testInvalidInput, uint16ArrayFoobar, execaSync);
+test('input option cannot be 0 - sync', testInvalidInput, 0, execaSync);
+test('input option cannot be false - sync', testInvalidInput, false, execaSync);
+test('input option cannot be null - sync', testInvalidInput, null, execaSync);
+test('input option cannot be a non-Readable stream - sync', testInvalidInput, new Writable(), execaSync);