Repository: sindresorhus/execa
Pull request #638 (2023-12-22 23:05:12)

Refactor `stdio` normalization logic
When a user passes `stdio: undefined` or `stdio: 'pipe'`, we currently keep it as is.
This PR normalizes it instead to `stdio: [undefined, undefined, undefined]` and `stdio: ['pipe', 'pipe', 'pipe']`.

According to [Node.js documentation](https://nodejs.org/api/child_process.html#optionsstdio), this is the same:

> For convenience, options.stdio may be one of the following strings:
>    'pipe': equivalent to ['pipe', 'pipe', 'pipe'] (the default)

I also checked the Node.js source code. Our tests also pass.

Finally, users cannot access the actual options being passed, so if the behavior is the same, there is no difference from a user standpoint.

Normalizing `stdio` to be always an array is helping working with that option without having duplicate logic (one for when it is an array, one for when it is not). 

I.e. this PR does not change any behavior, it is just meant to simplify working on the `stdio` option.


Patch:
diff --git a/index.d.ts b/index.d.ts
index 5b5ac94..6f1b4be 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -125,7 +125,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 
 	/**
 	[How to setup](https://nodejs.org/api/child_process.html#child_process_options_stdio) the child process' standard output. This can be:
-	- `'pipe'`: Sets [`childProcess.stdout`](https://nodejs.org/api/child_process.html#subprocessstdout) stream.
+	- `'pipe'`: Sets `childProcessResult.stdout` (as a string or `Uint8Array`) and [`childProcess.stdout`](https://nodejs.org/api/child_process.html#subprocessstdout) (as a stream).
 	- `'overlapped'`: Like `'pipe'` but asynchronous on Windows.
 	- `'ignore'`: Do not use `stdout`.
 	- `'ipc'`: Sets an [IPC channel](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback). You can also use `execaNode()` instead.
@@ -144,7 +144,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 
 	/**
 	[How to setup](https://nodejs.org/api/child_process.html#child_process_options_stdio) the child process' standard error. This can be:
-	- `'pipe'`: Sets [`childProcess.stderr`](https://nodejs.org/api/child_process.html#subprocessstderr) stream.
+	- `'pipe'`: Sets `childProcessResult.stderr` (as a string or `Uint8Array`) and [`childProcess.stderr`](https://nodejs.org/api/child_process.html#subprocessstderr) (as a stream).
 	- `'overlapped'`: Like `'pipe'` but asynchronous on Windows.
 	- `'ignore'`: Do not use `stderr`.
 	- `'ipc'`: Sets an [IPC channel](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback). You can also use `execaNode()` instead.
diff --git a/lib/stdio/handle.js b/lib/stdio/handle.js
index a1b601e..0e59023 100644
--- a/lib/stdio/handle.js
+++ b/lib/stdio/handle.js
@@ -5,14 +5,11 @@ import {handleInputOption, handleInputFileOption} from './input.js';
 // Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async/sync mode
 export const handleInput = (addProperties, options) => {
 	const stdio = normalizeStdio(options);
-	const stdioArray = arrifyStdio(stdio);
-	const stdioStreams = stdioArray.map((stdioOption, index) => getStdioStream(stdioOption, index, addProperties, options));
-	options.stdio = transformStdio(stdio, stdioStreams);
+	const stdioStreams = stdio.map((stdioOption, index) => getStdioStream(stdioOption, index, addProperties, options));
+	options.stdio = transformStdio(stdioStreams);
 	return stdioStreams;
 };
 
-const arrifyStdio = (stdio = []) => Array.isArray(stdio) ? stdio : [stdio, stdio, stdio];
-
 const getStdioStream = (stdioOption, index, addProperties, {input, inputFile}) => {
 	let stdioStream = getInitialStdioStream(stdioOption, index);
 
@@ -55,8 +52,6 @@ For example, you can use the \`pathToFileURL()\` method of the \`url\` core modu
 
 // When the `std*: Iterable | WebStream | URL | filePath`, `input` or `inputFile` option is used, we pipe to `spawned.std*`.
 // Therefore the `std*` options must be either `pipe` or `overlapped`. Other values do not set `spawned.std*`.
-const transformStdio = (stdio, stdioStreams) => Array.isArray(stdio)
-	? stdio.map((stdioItem, index) => transformStdioItem(stdioItem, stdioStreams[index]))
-	: stdio;
+const transformStdio = stdioStreams => stdioStreams.map(stdioStream => transformStdioItem(stdioStream));
 
-const transformStdioItem = (stdioItem, {type}) => type !== 'native' && stdioItem !== 'overlapped' ? 'pipe' : stdioItem;
+const transformStdioItem = stdioStream => stdioStream.type !== 'native' && stdioStream.value !== 'overlapped' ? 'pipe' : stdioStream.value;
diff --git a/lib/stdio/normalize.js b/lib/stdio/normalize.js
index 2a1bf64..c95179c 100644
--- a/lib/stdio/normalize.js
+++ b/lib/stdio/normalize.js
@@ -1,7 +1,7 @@
 // Add support for `stdin`/`stdout`/`stderr` as an alias for `stdio`
 export const normalizeStdio = options => {
 	if (!options) {
-		return;
+		return [undefined, undefined, undefined];
 	}
 
 	const {stdio} = options;
@@ -15,7 +15,7 @@ export const normalizeStdio = options => {
 	}
 
 	if (typeof stdio === 'string') {
-		return stdio;
+		return [stdio, stdio, stdio];
 	}
 
 	if (!Array.isArray(stdio)) {
@@ -33,18 +33,5 @@ const aliases = ['stdin', 'stdout', 'stderr'];
 // Same but for `execaNode()`, i.e. push `ipc` unless already present
 export const normalizeStdioNode = options => {
 	const stdio = normalizeStdio(options);
-
-	if (stdio === 'ipc') {
-		return 'ipc';
-	}
-
-	if (stdio === undefined || typeof stdio === 'string') {
-		return [stdio, stdio, stdio, 'ipc'];
-	}
-
-	if (stdio.includes('ipc')) {
-		return stdio;
-	}
-
-	return [...stdio, 'ipc'];
+	return stdio.includes('ipc') ? stdio : [...stdio, 'ipc'];
 };
diff --git a/readme.md b/readme.md
index 7e83d1e..0eceef0 100644
--- a/readme.md
+++ b/readme.md
@@ -593,7 +593,7 @@ Type: `string | number | stream.Writable | WritableStream | URL`\
 Default: `pipe`
 
 [How to setup](https://nodejs.org/api/child_process.html#child_process_options_stdio) the child process' standard output. This can be:
-- `'pipe'`: Sets [`childProcess.stdout`](https://nodejs.org/api/child_process.html#subprocessstdout) stream.
+- `'pipe'`: Sets [`childProcessResult.stdout`](#stdout) (as a string or `Uint8Array`) and [`childProcess.stdout`](https://nodejs.org/api/child_process.html#subprocessstdout) (as a stream).
 - `'overlapped'`: Like `'pipe'` but asynchronous on Windows.
 - `'ignore'`: Do not use `stdout`.
 - `'ipc'`: Sets an [IPC channel](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback). You can also use [`execaNode()`](#execanodescriptpath-arguments-options) instead.
@@ -612,7 +612,7 @@ Type: `string | number | stream.Writable | WritableStream | URL`\
 Default: `pipe`
 
 [How to setup](https://nodejs.org/api/child_process.html#child_process_options_stdio) the child process' standard error. This can be:
-- `'pipe'`: Sets [`childProcess.stderr`](https://nodejs.org/api/child_process.html#subprocessstderr) stream.
+- `'pipe'`: Sets [`childProcessResult.stderr`](#stderr) (as a string or `Uint8Array`) and [`childProcess.stderr`](https://nodejs.org/api/child_process.html#subprocessstderr) (as a stream).
 - `'overlapped'`: Like `'pipe'` but asynchronous on Windows.
 - `'ignore'`: Do not use `stderr`.
 - `'ipc'`: Sets an [IPC channel](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback). You can also use [`execaNode()`](#execanodescriptpath-arguments-options) instead.


Test patch:
diff --git a/test/stdio/normalize.js b/test/stdio/normalize.js
index c9bb3a2..1b7a4d6 100644
--- a/test/stdio/normalize.js
+++ b/test/stdio/normalize.js
@@ -18,12 +18,12 @@ const macroTitle = name => (title, input) => `${name} ${(inspect(input))}`;
 const stdioMacro = (...args) => macro(...args, normalizeStdio);
 stdioMacro.title = macroTitle('execa()');
 
-test(stdioMacro, undefined, undefined);
-test(stdioMacro, null, undefined);
+test(stdioMacro, undefined, [undefined, undefined, undefined]);
+test(stdioMacro, null, [undefined, undefined, undefined]);
 
-test(stdioMacro, {stdio: 'inherit'}, 'inherit');
-test(stdioMacro, {stdio: 'pipe'}, 'pipe');
-test(stdioMacro, {stdio: 'ignore'}, 'ignore');
+test(stdioMacro, {stdio: 'inherit'}, ['inherit', 'inherit', 'inherit']);
+test(stdioMacro, {stdio: 'pipe'}, ['pipe', 'pipe', 'pipe']);
+test(stdioMacro, {stdio: 'ignore'}, ['ignore', 'ignore', 'ignore']);
 test(stdioMacro, {stdio: [0, 1, 2]}, [0, 1, 2]);
 
 test(stdioMacro, {}, [undefined, undefined, undefined]);
@@ -52,7 +52,7 @@ forkMacro.title = macroTitle('execaNode()');
 
 test(forkMacro, undefined, [undefined, undefined, undefined, 'ipc']);
 test(forkMacro, {stdio: 'ignore'}, ['ignore', 'ignore', 'ignore', 'ipc']);
-test(forkMacro, {stdio: 'ipc'}, 'ipc');
+test(forkMacro, {stdio: 'ipc'}, ['ipc', 'ipc', 'ipc']);
 test(forkMacro, {stdio: [0, 1, 2]}, [0, 1, 2, 'ipc']);
 test(forkMacro, {stdio: [0, 1, 2, 3]}, [0, 1, 2, 3, 'ipc']);
 test(forkMacro, {stdio: [0, 1, 2, 'ipc']}, [0, 1, 2, 'ipc']);