Repository: sindresorhus/execa (js)

Issue:
Change the way you indicate you want a Buffer
Setting `{encoding: null}` is such a weird API I should never have inherited from `childProcess`.

We can introduce a new way while still supporting the old way.

Some possible solutions:

1. `{encoding: 'buffer'}` *(It's not technically an encoding, but who cares?)*
2. `{returnValue: 'string' | 'buffer'}`

In Got, we went with `{buffer: Boolean}`, but we already use that option name for something else.

Pull request description:
Support `encoding: 'buffer'`
Fixes #480.

Before [`8.0.0`](https://github.com/sindresorhus/execa/releases/tag/v8.0.0), the `encoding: 'buffer'` option was supported like in `child_process`, albeit it was undocumented. This PR re-adds support for it, documents it, adds some tests, and improve the related types.

Patch:
diff --git a/index.d.ts b/index.d.ts
index 2b61406..7cef754 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -12,7 +12,27 @@ export type StdioOption =
 	| number
 	| undefined;
 
-export type CommonOptions<EncodingType> = {
+type EncodingOption =
+  | 'utf8'
+  // eslint-disable-next-line unicorn/text-encoding-identifier-case
+  | 'utf-8'
+  | 'utf16le'
+  | 'utf-16le'
+  | 'ucs2'
+  | 'ucs-2'
+  | 'latin1'
+  | 'binary'
+  | 'ascii'
+  | 'hex'
+  | 'base64'
+  | 'base64url'
+  | 'buffer'
+  | null
+  | undefined;
+type DefaultEncodingOption = 'utf8';
+type BufferEncodingOption = 'buffer' | null;
+
+export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingOption> = {
 	/**
 	Kill the spawned process when the parent process exits unless either:
 		- the spawned process is [`detached`](https://nodejs.org/api/child_process.html#child_process_options_detached)
@@ -176,7 +196,7 @@ export type CommonOptions<EncodingType> = {
 	readonly shell?: boolean | string;
 
 	/**
-	Specify the character encoding used to decode the `stdout` and `stderr` output. If set to `null`, then `stdout` and `stderr` will be a `Buffer` instead of a string.
+	Specify the character encoding used to decode the `stdout` and `stderr` output. If set to `'buffer'` or `null`, then `stdout` and `stderr` will be a `Buffer` instead of a string.
 
 	@default 'utf8'
 	*/
@@ -253,7 +273,7 @@ export type CommonOptions<EncodingType> = {
 	readonly verbose?: boolean;
 };
 
-export type Options<EncodingType = string> = {
+export type Options<EncodingType extends EncodingOption = DefaultEncodingOption> = {
 	/**
 	Write some input to the `stdin` of your binary.
 
@@ -269,7 +289,7 @@ export type Options<EncodingType = string> = {
 	readonly inputFile?: string;
 } & CommonOptions<EncodingType>;
 
-export type SyncOptions<EncodingType = string> = {
+export type SyncOptions<EncodingType extends EncodingOption = DefaultEncodingOption> = {
 	/**
 	Write some input to the `stdin` of your binary.
 
@@ -285,7 +305,7 @@ export type SyncOptions<EncodingType = string> = {
 	readonly inputFile?: string;
 } & CommonOptions<EncodingType>;
 
-export type NodeOptions<EncodingType = string> = {
+export type NodeOptions<EncodingType extends EncodingOption = DefaultEncodingOption> = {
 	/**
 	The Node.js executable to use.
 
@@ -625,10 +645,10 @@ export function execa(
 export function execa(
 	file: string,
 	arguments?: readonly string[],
-	options?: Options<null>
+	options?: Options<BufferEncodingOption>
 ): ExecaChildProcess<Buffer>;
 export function execa(file: string, options?: Options): ExecaChildProcess;
-export function execa(file: string, options?: Options<null>): ExecaChildProcess<Buffer>;
+export function execa(file: string, options?: Options<BufferEncodingOption>): ExecaChildProcess<Buffer>;
 
 /**
 Same as `execa()` but synchronous.
@@ -698,12 +718,12 @@ export function execaSync(
 export function execaSync(
 	file: string,
 	arguments?: readonly string[],
-	options?: SyncOptions<null>
+	options?: SyncOptions<BufferEncodingOption>
 ): ExecaSyncReturnValue<Buffer>;
 export function execaSync(file: string, options?: SyncOptions): ExecaSyncReturnValue;
 export function execaSync(
 	file: string,
-	options?: SyncOptions<null>
+	options?: SyncOptions<BufferEncodingOption>
 ): ExecaSyncReturnValue<Buffer>;
 
 /**
@@ -729,7 +749,7 @@ console.log(stdout);
 ```
 */
 export function execaCommand(command: string, options?: Options): ExecaChildProcess;
-export function execaCommand(command: string, options?: Options<null>): ExecaChildProcess<Buffer>;
+export function execaCommand(command: string, options?: Options<BufferEncodingOption>): ExecaChildProcess<Buffer>;
 
 /**
 Same as `execaCommand()` but synchronous.
@@ -748,7 +768,7 @@ console.log(stdout);
 ```
 */
 export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue;
-export function execaCommandSync(command: string, options?: SyncOptions<null>): ExecaSyncReturnValue<Buffer>;
+export function execaCommandSync(command: string, options?: SyncOptions<BufferEncodingOption>): ExecaSyncReturnValue<Buffer>;
 
 type TemplateExpression =
 	| string
@@ -783,7 +803,7 @@ type Execa$<StdoutStderrType extends StdoutStderrAll = string> = {
 	*/
 	(options: Options<undefined>): Execa$<StdoutStderrType>;
 	(options: Options): Execa$;
-	(options: Options<null>): Execa$<Buffer>;
+	(options: Options<BufferEncodingOption>): Execa$<Buffer>;
 	(
 		templates: TemplateStringsArray,
 		...expressions: TemplateExpression[]
@@ -929,7 +949,7 @@ export function execaNode(
 export function execaNode(
 	scriptPath: string,
 	arguments?: readonly string[],
-	options?: NodeOptions<null>
+	options?: NodeOptions<BufferEncodingOption>
 ): ExecaChildProcess<Buffer>;
 export function execaNode(scriptPath: string, options?: NodeOptions): ExecaChildProcess;
-export function execaNode(scriptPath: string, options?: NodeOptions<null>): ExecaChildProcess<Buffer>;
+export function execaNode(scriptPath: string, options?: NodeOptions<BufferEncodingOption>): ExecaChildProcess<Buffer>;
diff --git a/lib/stream.js b/lib/stream.js
index 5182cd0..4e06459 100644
--- a/lib/stream.js
+++ b/lib/stream.js
@@ -102,7 +102,7 @@ const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => {
 		return getStream(stream, {maxBuffer});
 	}
 
-	if (encoding === null) {
+	if (encoding === null || encoding === 'buffer') {
 		return getStreamAsBuffer(stream, {maxBuffer});
 	}
 
diff --git a/readme.md b/readme.md
index 7d8b15d..1babbe5 100644
--- a/readme.md
+++ b/readme.md
@@ -682,7 +682,7 @@ We recommend against using this option since it is:
 Type: `string | null`\
 Default: `utf8`
 
-Specify the character encoding used to decode the `stdout` and `stderr` output. If set to `null`, then `stdout` and `stderr` will be a `Buffer` instead of a string.
+Specify the character encoding used to decode the `stdout` and `stderr` output. If set to `'buffer'` or `null`, then `stdout` and `stderr` will be a `Buffer` instead of a string.
 
 #### timeout
 


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 46e3572..1e1f764 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -25,7 +25,7 @@ try {
 	execaPromise.cancel();
 	expectType<ReadableStream | undefined>(execaPromise.all);
 
-	const execaBufferPromise = execa('unicorns', {encoding: null});
+	const execaBufferPromise = execa('unicorns', {encoding: 'buffer'});
 	const writeStream = createWriteStream('output.txt');
 
 	expectAssignable<Function | undefined>(execaPromise.pipeStdout);
@@ -133,6 +133,7 @@ execa('unicorns', {cleanup: false});
 execa('unicorns', {preferLocal: false});
 execa('unicorns', {localDir: '.'});
 execa('unicorns', {localDir: new URL('file:///test')});
+expectError(execa('unicorns', {encoding: 'unknownEncoding'}));
 execa('unicorns', {execPath: '/path'});
 execa('unicorns', {buffer: false});
 execa('unicorns', {input: ''});
@@ -207,10 +208,14 @@ expectType<ExecaReturnValue>(await execa('unicorns'));
 expectType<ExecaReturnValue>(
 	await execa('unicorns', {encoding: 'utf8'}),
 );
+expectType<ExecaReturnValue<Buffer>>(await execa('unicorns', {encoding: 'buffer'}));
 expectType<ExecaReturnValue<Buffer>>(await execa('unicorns', {encoding: null}));
 expectType<ExecaReturnValue>(
 	await execa('unicorns', ['foo'], {encoding: 'utf8'}),
 );
+expectType<ExecaReturnValue<Buffer>>(
+	await execa('unicorns', ['foo'], {encoding: 'buffer'}),
+);
 expectType<ExecaReturnValue<Buffer>>(
 	await execa('unicorns', ['foo'], {encoding: null}),
 );
@@ -219,12 +224,18 @@ expectType<ExecaSyncReturnValue>(execaSync('unicorns'));
 expectType<ExecaSyncReturnValue>(
 	execaSync('unicorns', {encoding: 'utf8'}),
 );
+expectType<ExecaSyncReturnValue<Buffer>>(
+	execaSync('unicorns', {encoding: 'buffer'}),
+);
 expectType<ExecaSyncReturnValue<Buffer>>(
 	execaSync('unicorns', {encoding: null}),
 );
 expectType<ExecaSyncReturnValue>(
 	execaSync('unicorns', ['foo'], {encoding: 'utf8'}),
 );
+expectType<ExecaSyncReturnValue<Buffer>>(
+	execaSync('unicorns', ['foo'], {encoding: 'buffer'}),
+);
 expectType<ExecaSyncReturnValue<Buffer>>(
 	execaSync('unicorns', ['foo'], {encoding: null}),
 );
@@ -232,14 +243,18 @@ expectType<ExecaSyncReturnValue<Buffer>>(
 expectType<ExecaChildProcess>(execaCommand('unicorns'));
 expectType<ExecaReturnValue>(await execaCommand('unicorns'));
 expectType<ExecaReturnValue>(await execaCommand('unicorns', {encoding: 'utf8'}));
+expectType<ExecaReturnValue<Buffer>>(await execaCommand('unicorns', {encoding: 'buffer'}));
 expectType<ExecaReturnValue<Buffer>>(await execaCommand('unicorns', {encoding: null}));
 expectType<ExecaReturnValue>(await execaCommand('unicorns foo', {encoding: 'utf8'}));
+expectType<ExecaReturnValue<Buffer>>(await execaCommand('unicorns foo', {encoding: 'buffer'}));
 expectType<ExecaReturnValue<Buffer>>(await execaCommand('unicorns foo', {encoding: null}));
 
 expectType<ExecaSyncReturnValue>(execaCommandSync('unicorns'));
 expectType<ExecaSyncReturnValue>(execaCommandSync('unicorns', {encoding: 'utf8'}));
+expectType<ExecaSyncReturnValue<Buffer>>(execaCommandSync('unicorns', {encoding: 'buffer'}));
 expectType<ExecaSyncReturnValue<Buffer>>(execaCommandSync('unicorns', {encoding: null}));
 expectType<ExecaSyncReturnValue>(execaCommandSync('unicorns foo', {encoding: 'utf8'}));
+expectType<ExecaSyncReturnValue<Buffer>>(execaCommandSync('unicorns foo', {encoding: 'buffer'}));
 expectType<ExecaSyncReturnValue<Buffer>>(execaCommandSync('unicorns foo', {encoding: null}));
 
 expectType<ExecaChildProcess>(execaNode('unicorns'));
@@ -247,19 +262,29 @@ expectType<ExecaReturnValue>(await execaNode('unicorns'));
 expectType<ExecaReturnValue>(
 	await execaNode('unicorns', {encoding: 'utf8'}),
 );
+expectType<ExecaReturnValue<Buffer>>(await execaNode('unicorns', {encoding: 'buffer'}));
 expectType<ExecaReturnValue<Buffer>>(await execaNode('unicorns', {encoding: null}));
 expectType<ExecaReturnValue>(
 	await execaNode('unicorns', ['foo'], {encoding: 'utf8'}),
 );
+expectType<ExecaReturnValue<Buffer>>(
+	await execaNode('unicorns', ['foo'], {encoding: 'buffer'}),
+);
 expectType<ExecaReturnValue<Buffer>>(
 	await execaNode('unicorns', ['foo'], {encoding: null}),
 );
 
 expectType<ExecaChildProcess>(execaNode('unicorns', {nodeOptions: ['--async-stack-traces']}));
 expectType<ExecaChildProcess>(execaNode('unicorns', ['foo'], {nodeOptions: ['--async-stack-traces']}));
+expectType<ExecaChildProcess<Buffer>>(
+	execaNode('unicorns', {nodeOptions: ['--async-stack-traces'], encoding: 'buffer'}),
+);
 expectType<ExecaChildProcess<Buffer>>(
 	execaNode('unicorns', {nodeOptions: ['--async-stack-traces'], encoding: null}),
 );
+expectType<ExecaChildProcess<Buffer>>(
+	execaNode('unicorns', ['foo'], {nodeOptions: ['--async-stack-traces'], encoding: 'buffer'}),
+);
 expectType<ExecaChildProcess<Buffer>>(
 	execaNode('unicorns', ['foo'], {nodeOptions: ['--async-stack-traces'], encoding: null}),
 );
@@ -277,28 +302,29 @@ expectType<ExecaReturnValue>(await $({encoding: 'utf8'})`unicorns foo`);
 expectType<ExecaSyncReturnValue>($({encoding: 'utf8'}).sync`unicorns foo`);
 
 expectType<ExecaChildProcess<Buffer>>($({encoding: null})`unicorns`);
-expectType<ExecaReturnValue<Buffer>>(await $({encoding: null})`unicorns`);
-expectType<ExecaSyncReturnValue<Buffer>>($({encoding: null}).sync`unicorns`);
+expectType<ExecaChildProcess<Buffer>>($({encoding: 'buffer'})`unicorns`);
+expectType<ExecaReturnValue<Buffer>>(await $({encoding: 'buffer'})`unicorns`);
+expectType<ExecaSyncReturnValue<Buffer>>($({encoding: 'buffer'}).sync`unicorns`);
 
-expectType<ExecaChildProcess<Buffer>>($({encoding: null})`unicorns foo`);
-expectType<ExecaReturnValue<Buffer>>(await $({encoding: null})`unicorns foo`);
-expectType<ExecaSyncReturnValue<Buffer>>($({encoding: null}).sync`unicorns foo`);
+expectType<ExecaChildProcess<Buffer>>($({encoding: 'buffer'})`unicorns foo`);
+expectType<ExecaReturnValue<Buffer>>(await $({encoding: 'buffer'})`unicorns foo`);
+expectType<ExecaSyncReturnValue<Buffer>>($({encoding: 'buffer'}).sync`unicorns foo`);
 
-expectType<ExecaChildProcess>($({encoding: null})({encoding: 'utf8'})`unicorns`);
-expectType<ExecaReturnValue>(await $({encoding: null})({encoding: 'utf8'})`unicorns`);
-expectType<ExecaSyncReturnValue>($({encoding: null})({encoding: 'utf8'}).sync`unicorns`);
+expectType<ExecaChildProcess>($({encoding: 'buffer'})({encoding: 'utf8'})`unicorns`);
+expectType<ExecaReturnValue>(await $({encoding: 'buffer'})({encoding: 'utf8'})`unicorns`);
+expectType<ExecaSyncReturnValue>($({encoding: 'buffer'})({encoding: 'utf8'}).sync`unicorns`);
 
-expectType<ExecaChildProcess>($({encoding: null})({encoding: 'utf8'})`unicorns foo`);
-expectType<ExecaReturnValue>(await $({encoding: null})({encoding: 'utf8'})`unicorns foo`);
-expectType<ExecaSyncReturnValue>($({encoding: null})({encoding: 'utf8'}).sync`unicorns foo`);
+expectType<ExecaChildProcess>($({encoding: 'buffer'})({encoding: 'utf8'})`unicorns foo`);
+expectType<ExecaReturnValue>(await $({encoding: 'buffer'})({encoding: 'utf8'})`unicorns foo`);
+expectType<ExecaSyncReturnValue>($({encoding: 'buffer'})({encoding: 'utf8'}).sync`unicorns foo`);
 
-expectType<ExecaChildProcess<Buffer>>($({encoding: null})({})`unicorns`);
-expectType<ExecaReturnValue<Buffer>>(await $({encoding: null})({})`unicorns`);
-expectType<ExecaSyncReturnValue<Buffer>>($({encoding: null})({}).sync`unicorns`);
+expectType<ExecaChildProcess<Buffer>>($({encoding: 'buffer'})({})`unicorns`);
+expectType<ExecaReturnValue<Buffer>>(await $({encoding: 'buffer'})({})`unicorns`);
+expectType<ExecaSyncReturnValue<Buffer>>($({encoding: 'buffer'})({}).sync`unicorns`);
 
-expectType<ExecaChildProcess<Buffer>>($({encoding: null})({})`unicorns foo`);
-expectType<ExecaReturnValue<Buffer>>(await $({encoding: null})({})`unicorns foo`);
-expectType<ExecaSyncReturnValue<Buffer>>($({encoding: null})({}).sync`unicorns foo`);
+expectType<ExecaChildProcess<Buffer>>($({encoding: 'buffer'})({})`unicorns foo`);
+expectType<ExecaReturnValue<Buffer>>(await $({encoding: 'buffer'})({})`unicorns foo`);
+expectType<ExecaSyncReturnValue<Buffer>>($({encoding: 'buffer'})({}).sync`unicorns foo`);
 
 expectType<ExecaReturnValue>(await $`unicorns ${'foo'}`);
 expectType<ExecaSyncReturnValue>($.sync`unicorns ${'foo'}`);
diff --git a/test/stream.js b/test/stream.js
index dec62b0..3911a01 100644
--- a/test/stream.js
+++ b/test/stream.js
@@ -11,6 +11,8 @@ import tempfile from 'tempfile';
 import {execa, execaSync, $} from '../index.js';
 import {setFixtureDir, FIXTURES_DIR} from './helpers/fixtures-dir.js';
 
+const pExec = promisify(exec);
+
 setFixtureDir();
 
 test('buffer', async t => {
@@ -21,14 +23,15 @@ test('buffer', async t => {
 
 const checkEncoding = async (t, encoding) => {
 	const {stdout} = await execa('noop-no-newline.js', [STRING_TO_ENCODE], {encoding});
-	t.is(stdout, Buffer.from(STRING_TO_ENCODE).toString(encoding));
+	t.is(stdout, BUFFER_TO_ENCODE.toString(encoding));
 
-	const {stdout: nativeStdout} = await promisify(exec)(`node noop-no-newline.js ${STRING_TO_ENCODE}`, {encoding, cwd: FIXTURES_DIR});
+	const {stdout: nativeStdout} = await pExec(`node noop-no-newline.js ${STRING_TO_ENCODE}`, {encoding, cwd: FIXTURES_DIR});
 	t.is(stdout, nativeStdout);
 };
 
 // This string gives different outputs with each encoding type
 const STRING_TO_ENCODE = '\u1000.';
+const BUFFER_TO_ENCODE = Buffer.from(STRING_TO_ENCODE);
 
 test('can pass encoding "utf8"', checkEncoding, 'utf8');
 test('can pass encoding "utf-8"', checkEncoding, 'utf8');
@@ -43,6 +46,21 @@ test('can pass encoding "hex"', checkEncoding, 'hex');
 test('can pass encoding "base64"', checkEncoding, 'base64');
 test('can pass encoding "base64url"', checkEncoding, 'base64url');
 
+const checkBufferEncoding = async (t, encoding) => {
+	const {stdout} = await execa('noop-no-newline.js', [STRING_TO_ENCODE], {encoding});
+	t.true(BUFFER_TO_ENCODE.equals(stdout));
+
+	const {stdout: nativeStdout} = await pExec(`node noop-no-newline.js ${STRING_TO_ENCODE}`, {encoding, cwd: FIXTURES_DIR});
+	t.true(BUFFER_TO_ENCODE.equals(nativeStdout));
+};
+
+test('can pass encoding "buffer"', checkBufferEncoding, 'buffer');
+test('can pass encoding null', checkBufferEncoding, null);
+
+test('validate unknown encodings', async t => {
+	await t.throwsAsync(execa('noop.js', {encoding: 'unknownEncoding'}), {code: 'ERR_UNKNOWN_ENCODING'});
+});
+
 test('pass `stdout` to a file descriptor', async t => {
 	const file = tempfile({extension: '.txt'});
 	await execa('noop.js', ['foo bar'], {stdout: fs.openSync(file, 'w')});