Repository: sindresorhus/execa
Pull request #670 (2024-01-10 03:06:47)

Allow `stdin: uint8Array` option
This PR allows the `stdin` option to be an `Uint8Array`, to pass any input directly.

sindresorhus: Merge conflict
ehmicky: Fixed.

Patch:
diff --git a/index.d.ts b/index.d.ts
index ca8b7cb..bbba21e 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -18,6 +18,7 @@ type CommonStdioOption =
 type InputStdioOption =
 	| Iterable<string | Uint8Array>
 	| AsyncIterable<string | Uint8Array>
+	| Uint8Array
 	| Readable
 	| ReadableStream;
 
@@ -122,6 +123,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	- 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)
+	- an `Uint8Array`.
 
 	This can be an [array of values](https://github.com/sindresorhus/execa#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 
diff --git a/lib/stdio/async.js b/lib/stdio/async.js
index e9e30ae..6d067bd 100644
--- a/lib/stdio/async.js
+++ b/lib/stdio/async.js
@@ -3,10 +3,15 @@ import {Buffer} from 'node:buffer';
 import {Readable, Writable} from 'node:stream';
 import mergeStreams from '@sindresorhus/merge-streams';
 import {handleInput} from './handle.js';
+import {TYPE_TO_MESSAGE} from './type.js';
 
 // Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async mode
 export const handleInputAsync = options => handleInput(addPropertiesAsync, options);
 
+const forbiddenIfAsync = ({type, optionName}) => {
+	throw new TypeError(`The \`${optionName}\` option cannot be ${TYPE_TO_MESSAGE[type]}.`);
+};
+
 const addPropertiesAsync = {
 	input: {
 		filePath: ({value}) => ({value: createReadStream(value)}),
@@ -18,9 +23,8 @@ const addPropertiesAsync = {
 	output: {
 		filePath: ({value}) => ({value: createWriteStream(value)}),
 		webStream: ({value}) => ({value: Writable.fromWeb(value)}),
-		iterable({optionName}) {
-			throw new TypeError(`The \`${optionName}\` option cannot be an iterable.`);
-		},
+		iterable: forbiddenIfAsync,
+		uint8Array: forbiddenIfAsync,
 	},
 };
 
diff --git a/lib/stdio/direction.js b/lib/stdio/direction.js
index 4ecb169..d155b0f 100644
--- a/lib/stdio/direction.js
+++ b/lib/stdio/direction.js
@@ -26,10 +26,11 @@ const getStreamDirection = stdioStream => KNOWN_DIRECTIONS[stdioStream.index] ??
 // `stdin`/`stdout`/`stderr` have a known direction
 const KNOWN_DIRECTIONS = ['input', 'output', 'output'];
 
-// `string` and `uint8Array` types always applies to `stdin`, i.e. does not need to be handled here
+// `string` can only be added through the `input` option, i.e. does not need to be handled here
 const guessStreamDirection = {
 	filePath: () => undefined,
 	iterable: () => 'input',
+	uint8Array: () => 'input',
 	webStream: stdioOption => isWritableStream(stdioOption) ? 'output' : 'input',
 	nodeStream(stdioOption) {
 		if (isNodeReadableStream(stdioOption)) {
diff --git a/lib/stdio/input.js b/lib/stdio/input.js
index e452262..8572761 100644
--- a/lib/stdio/input.js
+++ b/lib/stdio/input.js
@@ -1,5 +1,5 @@
-import {Buffer} from 'node:buffer';
 import {isReadableStream} from 'is-stream';
+import {isUint8Array} from './utils.js';
 
 // Append the `stdin` option with the `input` and `inputFile` options
 export const handleInputOptions = ({input, inputFile}) => [
@@ -23,7 +23,7 @@ const getType = input => {
 		return 'string';
 	}
 
-	if (Object.prototype.toString.call(input) === '[object Uint8Array]' && !Buffer.isBuffer(input)) {
+	if (isUint8Array(input)) {
 		return 'uint8Array';
 	}
 
diff --git a/lib/stdio/sync.js b/lib/stdio/sync.js
index 751878f..7bb8772 100644
--- a/lib/stdio/sync.js
+++ b/lib/stdio/sync.js
@@ -35,6 +35,7 @@ const addPropertiesSync = {
 		webStream: forbiddenIfSync,
 		nodeStream: forbiddenIfSync,
 		iterable: forbiddenIfSync,
+		uint8Array: forbiddenIfSync,
 		native: forbiddenIfStreamSync,
 	},
 };
diff --git a/lib/stdio/type.js b/lib/stdio/type.js
index 711e3ce..b3e5945 100644
--- a/lib/stdio/type.js
+++ b/lib/stdio/type.js
@@ -1,5 +1,6 @@
 import {isAbsolute} from 'node:path';
 import {isStream as isNodeStream} from 'is-stream';
+import {isUint8Array} from './utils.js';
 
 // The `stdin`/`stdout`/`stderr` option can be of many types. This detects it.
 export const getStdioOptionType = stdioOption => {
@@ -15,6 +16,10 @@ export const getStdioOptionType = stdioOption => {
 		return 'native';
 	}
 
+	if (isUint8Array(stdioOption)) {
+		return 'uint8Array';
+	}
+
 	if (isIterableObject(stdioOption)) {
 		return 'iterable';
 	}
diff --git a/lib/stdio/utils.js b/lib/stdio/utils.js
index 1cda911..8269328 100644
--- a/lib/stdio/utils.js
+++ b/lib/stdio/utils.js
@@ -1,1 +1,5 @@
+import {Buffer} from 'node:buffer';
+
 export const bufferToUint8Array = buffer => new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
+
+export const isUint8Array = value => Object.prototype.toString.call(value) === '[object Uint8Array]' && !Buffer.isBuffer(value);
diff --git a/readme.md b/readme.md
index 5d8d167..1c55e1d 100644
--- a/readme.md
+++ b/readme.md
@@ -572,7 +572,7 @@ See also the [`input`](#input) and [`stdin`](#stdin) options.
 
 #### stdin
 
-Type: `string | number | stream.Readable | ReadableStream | URL | Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>` (or a tuple of those types)\
+Type: `string | number | stream.Readable | ReadableStream | URL | Uint8Array | Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>` (or a tuple of those types)\
 Default: `inherit` with [`$`](#command), `pipe` otherwise
 
 [How to setup](https://nodejs.org/api/child_process.html#child_process_options_stdio) the child process' standard input. This can be:
@@ -587,6 +587,7 @@ Default: `inherit` with [`$`](#command), `pipe` otherwise
 - 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)
+- an `Uint8Array`.
 
 This can be an [array of values](#redirect-stdinstdoutstderr-to-multiple-destinations) such as `['inherit', 'pipe']` or `[filePath, 'pipe']`.
 
@@ -630,7 +631,7 @@ This can be an [array of values](#redirect-stdinstdoutstderr-to-multiple-destina
 
 #### stdio
 
-Type: `string | Array<string | number | stream.Readable | stream.Writable | ReadableStream | WritableStream | URL | Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>>` (or a tuple of those types)\
+Type: `string | Array<string | number | stream.Readable | stream.Writable | ReadableStream | WritableStream | URL | Uint8Array | Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>>` (or a tuple of those types)\
 Default: `pipe`
 
 Like the [`stdin`](#stdin), [`stdout`](#stdout-1) and [`stderr`](#stderr-1) options but for all file descriptors at once. For example, `{stdio: ['ignore', 'pipe', 'pipe']}` is the same as `{stdin: 'ignore', stdout: 'pipe', stderr: 'pipe'}`.


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 15363b7..415c9ed 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -179,6 +179,7 @@ execa('unicorns', {stdin: new ReadableStream()});
 execa('unicorns', {stdin: [new ReadableStream()]});
 expectError(execa('unicorns', {stdin: new WritableStream()}));
 expectError(execa('unicorns', {stdin: [new WritableStream()]}));
+execa('unicorns', {stdin: new Uint8Array()});
 execa('unicorns', {stdin: stringGenerator()});
 execa('unicorns', {stdin: [stringGenerator()]});
 execa('unicorns', {stdin: binaryGenerator()});
@@ -306,6 +307,7 @@ execa('unicorns', {
 		new Readable(),
 		new WritableStream(),
 		new ReadableStream(),
+		new Uint8Array(),
 		stringGenerator(),
 		asyncStringGenerator(),
 	],
diff --git a/test/stdio/typed-array.js b/test/stdio/typed-array.js
new file mode 100644
index 0000000..82b6fa7
--- /dev/null
+++ b/test/stdio/typed-array.js
@@ -0,0 +1,29 @@
+import test from 'ava';
+import {execa, execaSync} from '../../index.js';
+import {setFixtureDir} from '../helpers/fixtures-dir.js';
+import {getStdinOption, getStdoutOption, getStderrOption, getStdioOption} from '../helpers/stdio.js';
+
+setFixtureDir();
+
+const uint8ArrayFoobar = new TextEncoder().encode('foobar');
+
+const testUint8Array = async (t, fixtureName, getOptions) => {
+	const {stdout} = await execa(fixtureName, getOptions(uint8ArrayFoobar));
+	t.is(stdout, 'foobar');
+};
+
+test('stdin option can be a Uint8Array', testUint8Array, 'stdin.js', getStdinOption);
+test('stdio[*] option can be a Uint8Array', testUint8Array, 'stdin-fd3.js', getStdioOption);
+test('stdin option can be a Uint8Array - sync', testUint8Array, 'stdin.js', getStdinOption);
+test('stdio[*] option can be a Uint8Array - sync', testUint8Array, 'stdin-fd3.js', getStdioOption);
+
+const testNoUint8ArrayOutput = (t, getOptions, execaMethod) => {
+	t.throws(() => {
+		execaMethod('noop.js', getOptions(uint8ArrayFoobar));
+	}, {message: /cannot be a Uint8Array/});
+};
+
+test('stdout option cannot be a Uint8Array', testNoUint8ArrayOutput, getStdoutOption, execa);
+test('stderr option cannot be a Uint8Array', testNoUint8ArrayOutput, getStderrOption, execa);
+test('stdout option cannot be a Uint8Array - sync', testNoUint8ArrayOutput, getStdoutOption, execaSync);
+test('stderr option cannot be a Uint8Array - sync', testNoUint8ArrayOutput, getStderrOption, execaSync);