Skip to content

Commit

Permalink
Support {encoding: 'buffer'} (#572)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored Aug 19, 2023
1 parent e7dee28 commit 89c69fe
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 36 deletions.
48 changes: 34 additions & 14 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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'
*/
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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>;

/**
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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[]
Expand Down Expand Up @@ -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>;
62 changes: 44 additions & 18 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: ''});
Expand Down Expand Up @@ -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}),
);
Expand All @@ -219,47 +224,67 @@ 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}),
);

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'));
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}),
);
Expand All @@ -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'}`);
Expand Down
2 changes: 1 addition & 1 deletion lib/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}

Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 20 additions & 2 deletions test/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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');
Expand All @@ -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')});
Expand Down

0 comments on commit 89c69fe

Please sign in to comment.