Skip to content

Commit cb8aaf6

Browse files
committed
Fix IPC types for option unions
When the options are a union, IPC is now resolved for each member, with its own `buffer` and `serialization` options. `subprocess.sendMessage()`, `result.ipcOutput` and `result.stdio` used the union as a whole instead, so they were typed as if IPC was disabled. The IPC methods are resolved per member on the subprocess type, not inside `HasIpc`, and `$`'s default stdin type no longer recomputes the `stdin` option. Otherwise, options from a generic type parameter made `$(...).stdin` fail with "Expression produces a union type that is too complex to represent".
1 parent 80b01e9 commit cb8aaf6

8 files changed

Lines changed: 165 additions & 14 deletions

File tree

‎test-d/ipc-options-union.test-d.ts‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {expectTypeOf, expectError} from 'nz';
2+
import {
3+
execa,
4+
execaNode,
5+
$,
6+
type Message,
7+
type Subprocess,
8+
type Result,
9+
type ExecaError,
10+
} from '../index.js';
11+
12+
type EnabledSubprocess = Subprocess<{ipc: true}>;
13+
14+
declare const options: {node: true} | {ipcInput: string};
15+
const subprocess = execa('unicorns', options);
16+
expectTypeOf(subprocess.sendMessage).toBe<Subprocess<{ipc: true}>['sendMessage']>();
17+
expectTypeOf(subprocess.getOneMessage).toBe<Subprocess<{ipc: true}>['getOneMessage']>();
18+
const result = await subprocess;
19+
expectTypeOf(result.ipcOutput).toBe<Array<Message<'advanced'>>>();
20+
expectTypeOf(subprocess.getEachMessage).toBe<EnabledSubprocess['getEachMessage']>();
21+
expectTypeOf(subprocess.sendMessage('hello')).toBe<Promise<void>>();
22+
expectTypeOf(subprocess.getOneMessage()).toBe<Promise<Message<'advanced'>>>();
23+
expectTypeOf<Result<typeof options>['ipcOutput']>().toBe<Array<Message<'advanced'>>>();
24+
expectTypeOf<ExecaError<typeof options>['ipcOutput']>().toBe<Array<Message<'advanced'>>>();
25+
expectTypeOf(execa(options)`unicorns`.sendMessage).toBe<EnabledSubprocess['sendMessage']>();
26+
expectTypeOf($(options)`unicorns`.getOneMessage).toBe<EnabledSubprocess['getOneMessage']>();
27+
28+
const piped = execa('unicorns').pipe(subprocess);
29+
expectTypeOf(piped.getOneMessage).toBe<EnabledSubprocess['getOneMessage']>();
30+
const pipedResult = await piped;
31+
expectTypeOf(pipedResult.ipcOutput).toBe<Array<Message<'advanced'>>>();
32+
33+
declare const gracefulOptions: {gracefulCancel: true; cancelSignal: AbortSignal} | {ipcInput: string};
34+
const gracefulSubprocess = execa('unicorns', gracefulOptions);
35+
expectTypeOf(gracefulSubprocess.sendMessage).toBe<EnabledSubprocess['sendMessage']>();
36+
const gracefulResult = await gracefulSubprocess;
37+
expectTypeOf(gracefulResult.ipcOutput).toBe<Array<Message<'advanced'>>>();
38+
39+
declare const falsyInputOptions: {ipcInput: false} | {node: true};
40+
expectTypeOf(execa('unicorns', falsyInputOptions).sendMessage).toBe<EnabledSubprocess['sendMessage']>();
41+
42+
declare const maybeOptions: {node: true} | {encoding: 'buffer'};
43+
const maybeSubprocess = execa('unicorns', maybeOptions);
44+
expectTypeOf(maybeSubprocess.sendMessage).toBe<EnabledSubprocess['sendMessage'] | undefined>();
45+
expectTypeOf(maybeSubprocess.getOneMessage).toBe<EnabledSubprocess['getOneMessage'] | undefined>();
46+
expectTypeOf(maybeSubprocess.getEachMessage).toBe<EnabledSubprocess['getEachMessage'] | undefined>();
47+
const maybeResult = await maybeSubprocess;
48+
expectTypeOf(maybeResult.ipcOutput).toBe<Array<Message<'advanced'>> | []>();
49+
expectError(maybeSubprocess.sendMessage('hello'));
50+
if (maybeSubprocess.sendMessage) {
51+
expectTypeOf(maybeSubprocess.sendMessage('hello')).toBe<Promise<void>>();
52+
expectTypeOf(maybeSubprocess.getOneMessage()).toBe<Promise<Message<'advanced'>>>();
53+
}
54+
55+
declare const jsonOptions: {node: true; serialization: 'json'} | {ipcInput: string; serialization: 'json'};
56+
const jsonSubprocess = execa('unicorns', jsonOptions);
57+
expectTypeOf(jsonSubprocess.getOneMessage()).toBe<Promise<Message<'json'>>>();
58+
const jsonResult = await jsonSubprocess;
59+
expectTypeOf(jsonResult.ipcOutput).toBe<Array<Message<'json'>>>();
60+
expectError(jsonSubprocess.sendMessage(Symbol('message')), 'not assignable');
61+
62+
// Explicit IPC precedence and disabled buffering are unchanged by option unions.
63+
declare const disabledOptions: {node: true; ipc: false} | {encoding: 'buffer'; ipc: false};
64+
const disabledSubprocess = execa('unicorns', disabledOptions);
65+
expectTypeOf(disabledSubprocess.sendMessage).toBe<undefined>();
66+
expectTypeOf(disabledSubprocess.getOneMessage).toBe<undefined>();
67+
const disabledResult = await disabledSubprocess;
68+
expectTypeOf(disabledResult.ipcOutput).toBe<[]>();
69+
expectError(disabledSubprocess.sendMessage('hello'));
70+
71+
declare const explicitOptions: {node: false; ipc: true} | {encoding: 'buffer'; ipc: true};
72+
expectTypeOf(execa('unicorns', explicitOptions).sendMessage).toBe<EnabledSubprocess['sendMessage']>();
73+
expectTypeOf(execaNode('unicorns', options).sendMessage).toBe<EnabledSubprocess['sendMessage']>();
74+
const noBufferSubprocess = execa('unicorns', {...options, buffer: false});
75+
expectTypeOf(noBufferSubprocess.sendMessage).toBe<EnabledSubprocess['sendMessage']>();
76+
const noBufferResult = await noBufferSubprocess;
77+
expectTypeOf(noBufferResult.ipcOutput).toBe<[]>();
78+
79+
declare const absentOptions: {node: false} | {gracefulCancel: false};
80+
expectTypeOf(execa('unicorns', absentOptions).getEachMessage).toBe<undefined>();
81+
expectTypeOf<Result<typeof absentOptions>['ipcOutput']>().toBe<[]>();
82+
83+
declare const optionalInputOptions: {ipcInput?: string} | {node: true};
84+
expectTypeOf(execa('unicorns', optionalInputOptions).sendMessage).toBe<EnabledSubprocess['sendMessage'] | undefined>();
85+
86+
// Each member's own `buffer` and `serialization` options apply to its IPC.
87+
declare const mixedStdioOptions: {node: true} | {encoding: 'buffer'};
88+
const mixedStdioResult = await execa('unicorns', mixedStdioOptions);
89+
expectTypeOf(mixedStdioResult.stdio).toBe<[undefined, string | Uint8Array, string | Uint8Array, undefined] | [undefined, string | Uint8Array, string | Uint8Array]>();
90+
91+
declare const memberBufferOptions: {node: true; buffer: false} | {encoding: 'buffer'};
92+
const memberBufferResult = await execa('unicorns', memberBufferOptions);
93+
expectTypeOf(memberBufferResult.ipcOutput).toBe<[]>();
94+
95+
declare const memberSerializationOptions: {node: true; serialization: 'json'} | {ipc: true};
96+
const memberSerializationSubprocess = execa('unicorns', memberSerializationOptions);
97+
await memberSerializationSubprocess.sendMessage({message: 'hello'});
98+
expectError(memberSerializationSubprocess.sendMessage(new Map()));
99+
expectTypeOf(memberSerializationSubprocess.getOneMessage()).toBe<Promise<Message<'advanced'>>>();
100+
const memberSerializationResult = await memberSerializationSubprocess;
101+
expectTypeOf(memberSerializationResult.ipcOutput).toBe<Array<Message<'json'>> | Array<Message<'advanced'>>>();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {expectTypeOf} from 'nz';
2+
import {
3+
execa,
4+
execaSync,
5+
execaNode,
6+
$,
7+
type Options,
8+
type SyncOptions,
9+
type ResultPromise,
10+
} from '../../index.js';
11+
12+
// Options from a generic type parameter, e.g. in a wrapper function
13+
const runGeneric = <OptionsType extends Options>(options: OptionsType) => {
14+
execa('unicorns', options).stdin?.write('input');
15+
execa(options)`unicorns`.stdin?.write('input');
16+
execaNode('unicorns', options).stdin?.write('input');
17+
$('unicorns', options).stdin?.write('input');
18+
$(options)`unicorns`.stdin?.write('input');
19+
$('unicorns', [], options).stdin?.write('input');
20+
$(options)`unicorns`.kill();
21+
return $('unicorns', options);
22+
};
23+
24+
expectTypeOf(runGeneric({})).toBeAssignableTo<ResultPromise>();
25+
26+
const runGenericSync = <OptionsType extends SyncOptions>(options: OptionsType) => {
27+
execaSync('unicorns', options).command.trim();
28+
$.sync('unicorns', options).command.trim();
29+
};
30+
31+
runGenericSync({});
32+
33+
// Options inferred while the return value has an expected type, e.g. a type annotation
34+
const scriptSubprocess: ResultPromise = $('unicorns');
35+
const scriptOptionsSubprocess: ResultPromise = $('unicorns', {node: true});
36+
const scriptArgumentsSubprocess: ResultPromise = $('unicorns', [], {input: 'input'});
37+
const getScriptSubprocess = (): ResultPromise => $('unicorns', {cwd: '.'});
38+
expectTypeOf([scriptSubprocess, scriptOptionsSubprocess, scriptArgumentsSubprocess, getScriptSubprocess()]).toBeAssignableTo<ResultPromise[]>();

‎types/ipc.d.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ export type HasIpc<OptionsType extends Options> = HasIpcOption<
142142
'gracefulCancel' extends keyof OptionsType ? OptionsType['gracefulCancel'] : undefined
143143
>;
144144

145+
// IPC methods of a subprocess. With an options union, each member enables IPC and sets its `serialization` separately.
146+
export type SubprocessIpcMethods<OptionsType extends Options> = OptionsType extends unknown
147+
? IpcMethods<HasIpc<OptionsType>, OptionsType['serialization']>
148+
: never;
149+
145150
type HasIpcOption<
146151
IpcOption extends Options['ipc'],
147152
NodeOption extends Options['node'],

‎types/methods/script.d.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {SyncResult} from '../return/result.js';
88
import type {ResultPromise} from '../subprocess/subprocess.js';
99
import type {SubprocessStdioStream} from '../subprocess/stdout.js';
1010
import type {SubprocessStdioArray} from '../subprocess/stdio.js';
11-
import type {FdStdioOption, WithInputSources} from '../stdio/option.js';
11+
import type {WithInputSources} from '../stdio/option.js';
1212
import type {TemplateString} from './template.js';
1313
import type {MergeOptions} from './bind.js';
1414

@@ -108,7 +108,8 @@ type ScriptStdin<
108108
StdinOption,
109109
> = StdinOption extends undefined ? ScriptDefaultStdin<OptionsType> : StdinOption;
110110

111-
type ScriptDefaultStdin<OptionsType extends Options> = WithInputSources<OptionsType, 'input' | 'inputFile' | 'stdio', 'inherit', FdStdioOption<'0', Omit<OptionsType, 'stdin'>>>;
111+
// With an input source, the `stdin` option is left `undefined`, so its usual default applies.
112+
type ScriptDefaultStdin<OptionsType extends Options> = WithInputSources<OptionsType, 'input' | 'inputFile' | 'stdio', 'inherit', undefined>;
112113

113114
// We must intersect the overloaded methods with & instead of using a simple object as a workaround for a TypeScript bug
114115
// See https://github.com/microsoft/TypeScript/issues/58765

‎types/pipe.d.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {Options} from './arguments/options.js';
22
import type {Result} from './return/result.js';
33
import type {FromOption, ToOption} from './arguments/fd-options.js';
44
import type {ResultPromise, SubprocessResultMethods} from './subprocess/subprocess.js';
5-
import type {IpcMethods, HasIpc} from './ipc.js';
5+
import type {SubprocessIpcMethods} from './ipc.js';
66
import type {TemplateExpression} from './methods/template.js';
77

88
// `subprocess.pipe()` options
@@ -28,7 +28,7 @@ type PipeOptions = {
2828
// Methods forwarded from the destination subprocess to the return value of `subprocess.pipe()`, so its output can be iterated, converted to a stream, or used for IPC.
2929
type PipeResultMethods<OptionsType extends Options> =
3030
& SubprocessResultMethods<OptionsType>
31-
& IpcMethods<HasIpc<OptionsType>, OptionsType['serialization']>;
31+
& SubprocessIpcMethods<OptionsType>;
3232

3333
// Same as `PipeResultMethods`, but when the destination is another `execa()` call, so its own option types are kept.
3434
// The base `Options` is only used to compute the property names, which do not depend on the specific options.

‎types/return/result-ipc.d.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ export type ResultIpcOutput<
1010
OptionsType extends CommonOptions,
1111
> = IsSync extends true
1212
? []
13-
: ResultIpcAsync<
14-
FdSpecificOption<OptionsType['buffer'], 'ipc'>,
15-
HasIpc<StricterOptions<OptionsType, Options>>,
16-
OptionsType['serialization']
17-
>;
13+
// With an options union, each member enables IPC and sets its `buffer` and `serialization` options separately
14+
: OptionsType extends unknown
15+
? ResultIpcAsync<
16+
FdSpecificOption<OptionsType['buffer'], 'ipc'>,
17+
HasIpc<StricterOptions<OptionsType, Options>>,
18+
OptionsType['serialization']
19+
>
20+
: never;
1821

1922
type ResultIpcAsync<
2023
BufferOption extends boolean | undefined,

‎types/return/result-stdio.d.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ type ResultIpcStdio<
2828
OptionsType extends CommonOptions,
2929
> = IsSync extends true
3030
? []
31-
: HasIpc<StricterOptions<OptionsType, Options>> extends true
32-
? [undefined]
33-
: [];
31+
// With an options union, each member enables IPC separately
32+
: OptionsType extends unknown
33+
? HasIpc<StricterOptions<OptionsType, Options>> extends true
34+
? [undefined]
35+
: []
36+
: never;
3437

3538
export {};

‎types/subprocess/subprocess.d.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
SubprocessAsyncIterable,
1313
IsObjectIterable,
1414
} from '../convert.js';
15-
import type {IpcMethods, HasIpc} from '../ipc.js';
15+
import type {SubprocessIpcMethods} from '../ipc.js';
1616
import type {SubprocessStdioStream} from './stdout.js';
1717
import type {SubprocessStdioArray} from './stdio.js';
1818
import type {SubprocessAll} from './all.js';
@@ -52,7 +52,7 @@ export type SubprocessResultMethods<OptionsType extends Options = Options> = {
5252
};
5353

5454
type ExecaCustomSubprocess<OptionsType extends Options> =
55-
& IpcMethods<HasIpc<OptionsType>, OptionsType['serialization']>
55+
& SubprocessIpcMethods<OptionsType>
5656
& PipableSubprocess
5757
& SubprocessResultMethods<OptionsType>
5858
& {

0 commit comments

Comments
 (0)