diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..6f2a7b6f3b --- /dev/null +++ b/index.d.ts @@ -0,0 +1,494 @@ +/// +import {ChildProcess} from 'child_process'; +import {Stream, Readable as ReadableStream} from 'stream'; + +export type StdioOption = + | 'pipe' + | 'ipc' + | 'ignore' + | 'inherit' + | Stream + | number + | null + | undefined; + +export interface CommonOptions { + /** + Current working directory of the child process. + + @default process.cwd() + */ + readonly cwd?: string; + + /** + Environment key-value pairs. Extends automatically from `process.env`. Set `extendEnv` to `false` if you don't want this. + + @default process.env + */ + readonly env?: NodeJS.ProcessEnv; + + /** + Set to `false` if you don't want to extend the environment variables when providing the `env` property. + + @default true + */ + readonly extendEnv?: boolean; + + /** + Explicitly set the value of `argv[0]` sent to the child process. This will be set to `command` or `file` if not specified. + */ + readonly argv0?: string; + + /** + Child's [stdio](https://nodejs.org/api/child_process.html#child_process_options_stdio) configuration. + + @default 'pipe' + */ + readonly stdio?: 'pipe' | 'ignore' | 'inherit' | ReadonlyArray; + + /** + Prepare child to run independently of its parent process. Specific behavior [depends on the platform](https://nodejs.org/api/child_process.html#child_process_options_detached). + + @default false + */ + readonly detached?: boolean; + + /** + Sets the user identity of the process. + */ + readonly uid?: number; + + /** + Sets the group identity of the process. + */ + readonly gid?: number; + + /** + If `true`, runs `command` inside of a shell. Uses `/bin/sh` on UNIX and `cmd.exe` on Windows. A different shell can be specified as a string. The shell should understand the `-c` switch on UNIX or `/d /s /c` on Windows. + + @default false + */ + readonly shell?: boolean | string; + + /** + Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from the output. + + @default true + */ + readonly stripFinalNewline?: boolean; + + /** + Prefer locally installed binaries when looking for a binary to execute. + + If you `$ npm install foo`, you can then `execa('foo')`. + + @default true + */ + readonly preferLocal?: boolean; + + /** + Preferred path to find locally installed binaries in (use with `preferLocal`). + + @default process.cwd() + */ + readonly localDir?: string; + + /** + Setting this to `false` resolves the promise with the error instead of rejecting it. + + @default true + */ + readonly reject?: boolean; + + /** + Keep track of the spawned process and `kill` it when the parent process exits. + + @default true + */ + readonly cleanup?: boolean; + + /** + 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. + + @default 'utf8' + */ + readonly encoding?: EncodingType; + + /** + If `timeout` is greater than `0`, the parent will send the signal identified by the `killSignal` property (the default is `SIGTERM`) if the child runs longer than `timeout` milliseconds. + + @default 0 + */ + readonly timeout?: number; + + /** + Buffer the output from the spawned process. When buffering is disabled you must consume the output of the `stdout` and `stderr` streams because the promise will not be resolved/rejected until they have completed. + + @default true + */ + readonly buffer?: boolean; + + /** + Largest amount of data in bytes allowed on `stdout` or `stderr`. Default: 10MB. + + @default 10000000 + */ + readonly maxBuffer?: number; + + /** + Signal value to be used when the spawned process will be killed. + + @default 'SIGTERM' + */ + readonly killSignal?: string | number; + + /** + Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). + + @default 'pipe' + */ + readonly stdin?: StdioOption; + + /** + Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). + + @default 'pipe' + */ + readonly stdout?: StdioOption; + + /** + Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). + + @default 'pipe' + */ + readonly stderr?: StdioOption; + + /** + If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`. + + @default false + */ + readonly windowsVerbatimArguments?: boolean; +} + +export interface Options + extends CommonOptions { + /** + Write some input to the `stdin` of your binary. + */ + readonly input?: string | Buffer | ReadableStream; +} + +export interface SyncOptions + extends CommonOptions { + /** + Write some input to the `stdin` of your binary. + */ + readonly input?: string | Buffer; +} + +export interface ExecaReturnBase { + /** + The numeric exit code of the process that was run. + */ + exitCode: number; + + /** + The textual exit code of the process that was run. + */ + exitCodeName: string; + + /** + The output of the process on stdout. + */ + stdout: StdoutStderrType; + + /** + The output of the process on stderr. + */ + stderr: StdoutStderrType; + + /** + Whether the process failed to run. + */ + failed: boolean; + + /** + The signal that was used to terminate the process. + */ + signal?: string; + + /** + The command that was run. + */ + cmd: string; + + /** + Whether the process timed out. + */ + timedOut: boolean; + + /** + Whether the process was killed. + */ + killed: boolean; +} + +export interface ExecaSyncReturnValue + extends ExecaReturnBase { + /** + The exit code of the process that was run. + */ + code: number; +} + +export interface ExecaReturnValue + extends ExecaSyncReturnValue { + /** + The output of the process with `stdout` and `stderr` interleaved. + */ + all: StdOutErrType; + + /** + Whether the process was canceled. + */ + isCanceled: boolean; +} + +export interface ExecaSyncError + extends Error, + ExecaReturnBase { + /** + The error message. + */ + message: string; + + /** + The exit code (either numeric or textual) of the process that was run. + */ + code: number | string; +} + +export interface ExecaError + extends ExecaSyncError { + /** + The output of the process with `stdout` and `stderr` interleaved. + */ + all: StdOutErrType; + + /** + Whether the process was canceled. + */ + isCanceled: boolean; +} + +export interface ExecaChildPromise { + catch( + onRejected?: + | (( + reason: ExecaError + ) => ResultType | PromiseLike) + | null + ): Promise | ResultType>; + + /** + Cancel the subprocess. + + Causes the promise to reject an error with a `.isCanceled = true` property, provided the process gets canceled. The process will not be canceled if it has already exited. + */ + cancel(): void; +} + +export type ExecaChildProcess = ChildProcess & + ExecaChildPromise & + Promise>; + +declare const execa: { + /** + Execute a file. + + Think of this as a mix of `child_process.execFile` and `child_process.spawn`. + + @param file - The program/script to execute. + @param arguments - Arguments to pass to `file` on execution. + @returns A [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess), which is enhanced to also be a `Promise` for a result `Object` with `stdout` and `stderr` properties. + + @example + ``` + import execa from 'execa'; + + (async () => { + const {stdout} = await execa('echo', ['unicorns']); + console.log(stdout); + //=> 'unicorns' + + // Cancelling a spawned process + const subprocess = execa('node'); + setTimeout(() => { spawned.cancel() }, 1000); + try { + await subprocess; + } catch (error) { + console.log(subprocess.killed); // true + console.log(error.isCanceled); // true + } + })(); + + // Pipe the child process stdout to the current stdout + execa('echo', ['unicorns']).stdout.pipe(process.stdout); + ``` + */ + ( + file: string, + arguments?: ReadonlyArray, + options?: Options + ): ExecaChildProcess; + ( + file: string, + arguments?: ReadonlyArray, + options?: Options + ): ExecaChildProcess; + (file: string, options?: Options): ExecaChildProcess; + (file: string, options?: Options): ExecaChildProcess; + + /** + Same as `execa()`, but returns only `stdout`. + + @param file - The program/script to execute. + @param arguments - Arguments to pass to `file` on execution. + @returns The contents of the executed process' `stdout`. + */ + stdout( + file: string, + arguments?: ReadonlyArray, + options?: Options + ): Promise; + stdout( + file: string, + arguments?: ReadonlyArray, + options?: Options + ): Promise; + stdout(file: string, options?: Options): Promise; + stdout(file: string, options?: Options): Promise; + + /** + Same as `execa()`, but returns only `stderr`. + + @param file - The program/script to execute. + @param arguments - Arguments to pass to `file` on execution. + @returns The contents of the executed process' `stderr`. + */ + stderr( + file: string, + arguments?: ReadonlyArray, + options?: Options + ): Promise; + stderr( + file: string, + arguments?: ReadonlyArray, + options?: Options + ): Promise; + stderr(file: string, options?: Options): Promise; + stderr(file: string, options?: Options): Promise; + + /** + Execute a command through the system shell. + + Prefer `execa()` whenever possible, as it's both faster and safer. + + @param command - The command to execute. + @returns A [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess). + + @example + ``` + import execa from 'execa'; + + (async => { + // Run a shell command + const {stdout} = await execa.shell('echo unicorns'); + //=> 'unicorns' + + // Catching an error + try { + await execa.shell('exit 3'); + } catch (error) { + console.log(error); + //{ + // message: 'Command failed with exit code 3 (ESRCH): exit 3', + // code: 3, + // exitCode: 3, + // exitCodeName: 'ESRCH', + // stdout: '', + // stderr: '', + // all: '', + // failed: true, + // cmd: 'exit 3', + // timedOut: false, + // killed: false + //} + } + })(); + ``` + */ + shell(command: string, options?: Options): ExecaChildProcess; + shell(command: string, options?: Options): ExecaChildProcess; + + /** + Execute a file synchronously. + + This method throws an `Error` if the command fails. + + @param file - The program/script to execute. + @param arguments - Arguments to pass to `file` on execution. + @returns The same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). + */ + sync( + file: string, + arguments?: ReadonlyArray, + options?: SyncOptions + ): ExecaSyncReturnValue; + sync( + file: string, + arguments?: ReadonlyArray, + options?: SyncOptions + ): ExecaSyncReturnValue; + sync(file: string, options?: SyncOptions): ExecaSyncReturnValue; + sync(file: string, options?: SyncOptions): ExecaSyncReturnValue; + + /** + Execute a command synchronously through the system shell. + + This method throws an `Error` if the command fails. + + @param command - The command to execute. + @returns The same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). + + @example + ``` + import execa from 'execa'; + + try { + execa.shellSync('exit 3'); + } catch (error) { + console.log(error); + //{ + // message: 'Command failed with exit code 3 (ESRCH): exit 3', + // code: 3, + // exitCode: 3, + // exitCodeName: 'ESRCH', + // stdout: '', + // stderr: '', + // failed: true, + // cmd: 'exit 3', + // timedOut: false + //} + } + ``` + */ + shellSync(command: string, options?: Options): ExecaSyncReturnValue; + shellSync( + command: string, + options?: Options + ): ExecaSyncReturnValue; +}; + +export default execa; diff --git a/index.js b/index.js index f5a9ea8651..3266a0771c 100644 --- a/index.js +++ b/index.js @@ -225,7 +225,7 @@ function joinCommand(command, args) { return joinedCommand; } -module.exports = (command, args, options) => { +const execa = (command, args, options) => { const parsed = handleArgs(command, args, options); const {encoding, buffer, maxBuffer} = parsed.options; const joinedCommand = joinCommand(command, args); @@ -374,19 +374,22 @@ module.exports = (command, args, options) => { return spawned; }; +module.exports = execa; +module.exports.default = execa; + // TODO: set `stderr: 'ignore'` when that option is implemented module.exports.stdout = async (...args) => { - const {stdout} = await module.exports(...args); + const {stdout} = await execa(...args); return stdout; }; // TODO: set `stdout: 'ignore'` when that option is implemented module.exports.stderr = async (...args) => { - const {stderr} = await module.exports(...args); + const {stderr} = await execa(...args); return stderr; }; -module.exports.shell = (command, options) => handleShell(module.exports, command, options); +module.exports.shell = (command, options) => handleShell(execa, command, options); module.exports.sync = (command, args, options) => { const parsed = handleArgs(command, args, options); @@ -424,4 +427,4 @@ module.exports.sync = (command, args, options) => { }; }; -module.exports.shellSync = (command, options) => handleShell(module.exports.sync, command, options); +module.exports.shellSync = (command, options) => handleShell(execa.sync, command, options); diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000000..3e6eb23e07 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,162 @@ +import {expectType, expectError} from 'tsd'; +import execa, { + ExecaReturnValue, + ExecaChildProcess, + ExecaError, + ExecaSyncReturnValue, + ExecaSyncError +} from '.'; + +try { + const execaPromise = execa('unicorns'); + execaPromise.cancel(); + + const unicornsResult = await execaPromise; + expectType(unicornsResult.cmd); + expectType(unicornsResult.code); + expectType(unicornsResult.failed); + expectType(unicornsResult.killed); + expectType(unicornsResult.signal); + expectType(unicornsResult.stderr); + expectType(unicornsResult.stdout); + expectType(unicornsResult.all); + expectType(unicornsResult.timedOut); + expectType(unicornsResult.isCanceled); +} catch (error) { + const execaError: ExecaError = error; + + expectType(execaError.message); + expectType(execaError.code); + expectType(execaError.all); + expectType(execaError.isCanceled); +} + +try { + const unicornsResult = execa.sync('unicorns'); + expectType(unicornsResult.cmd); + expectType(unicornsResult.code); + expectType(unicornsResult.failed); + expectType(unicornsResult.killed); + expectType(unicornsResult.signal); + expectType(unicornsResult.stderr); + expectType(unicornsResult.stdout); + expectType(unicornsResult.timedOut); + expectError(unicornsResult.all); + expectError(unicornsResult.isCanceled); +} catch (error) { + const execaError: ExecaSyncError = error; + + expectType(execaError.message); + expectType(execaError.code); + expectError(execaError.all); + expectError(execaError.isCanceled); +} + +execa('unicorns', {cwd: '.'}); +execa('unicorns', {env: {PATH: ''}}); +execa('unicorns', {extendEnv: false}); +execa('unicorns', {argv0: ''}); +execa('unicorns', {stdio: 'pipe'}); +execa('unicorns', {stdio: 'ignore'}); +execa('unicorns', {stdio: 'inherit'}); +execa('unicorns', { + stdio: ['pipe', 'ipc', 'ignore', 'inherit', process.stdin, 1, null, undefined] +}); +execa('unicorns', {detached: true}); +execa('unicorns', {uid: 0}); +execa('unicorns', {gid: 0}); +execa('unicorns', {shell: true}); +execa('unicorns', {shell: '/bin/sh'}); +execa('unicorns', {stripFinalNewline: false}); +execa('unicorns', {preferLocal: false}); +execa('unicorns', {localDir: '.'}); +execa('unicorns', {reject: false}); +execa('unicorns', {cleanup: false}); +execa('unicorns', {timeout: 1000}); +execa('unicorns', {buffer: false}); +execa('unicorns', {maxBuffer: 1000}); +execa('unicorns', {killSignal: 'SIGTERM'}); +execa('unicorns', {killSignal: 9}); +execa('unicorns', {stdin: 'pipe'}); +execa('unicorns', {stdin: 'ipc'}); +execa('unicorns', {stdin: 'ignore'}); +execa('unicorns', {stdin: 'inherit'}); +execa('unicorns', {stdin: process.stdin}); +execa('unicorns', {stdin: 1}); +execa('unicorns', {stdin: null}); +execa('unicorns', {stdin: undefined}); +execa('unicorns', {stderr: 'pipe'}); +execa('unicorns', {stderr: 'ipc'}); +execa('unicorns', {stderr: 'ignore'}); +execa('unicorns', {stderr: 'inherit'}); +execa('unicorns', {stderr: process.stderr}); +execa('unicorns', {stderr: 1}); +execa('unicorns', {stderr: null}); +execa('unicorns', {stderr: undefined}); +execa('unicorns', {stdout: 'pipe'}); +execa('unicorns', {stdout: 'ipc'}); +execa('unicorns', {stdout: 'ignore'}); +execa('unicorns', {stdout: 'inherit'}); +execa('unicorns', {stdout: process.stdout}); +execa('unicorns', {stdout: 1}); +execa('unicorns', {stdout: null}); +execa('unicorns', {stdout: undefined}); +execa('unicorns', {windowsVerbatimArguments: true}); + +expectType>(execa('unicorns')); +expectType>(await execa('unicorns')); +expectType>( + await execa('unicorns', {encoding: 'utf8'}) +); +expectType>(await execa('unicorns', {encoding: null})); +expectType>( + await execa('unicorns', ['foo'], {encoding: 'utf8'}) +); +expectType>( + await execa('unicorns', ['foo'], {encoding: null}) +); + +expectType>(execa.stdout('unicorns')); +expectType(await execa.stdout('unicorns')); +expectType(await execa.stdout('unicorns', {encoding: 'utf8'})); +expectType(await execa.stdout('unicorns', {encoding: null})); +expectType(await execa.stdout('unicorns', ['foo'], {encoding: 'utf8'})); +expectType(await execa.stdout('unicorns', ['foo'], {encoding: null})); + +expectType>(execa.stderr('unicorns')); +expectType(await execa.stderr('unicorns')); +expectType(await execa.stderr('unicorns', {encoding: 'utf8'})); +expectType(await execa.stderr('unicorns', {encoding: null})); +expectType(await execa.stderr('unicorns', ['foo'], {encoding: 'utf8'})); +expectType(await execa.stderr('unicorns', ['foo'], {encoding: null})); + +expectType>(execa.shell('unicorns')); +expectType>(await execa.shell('unicorns')); +expectType>( + await execa.shell('unicorns', {encoding: 'utf8'}) +); +expectType>( + await execa.shell('unicorns', {encoding: null}) +); + +expectType>(execa.sync('unicorns')); +expectType>( + execa.sync('unicorns', {encoding: 'utf8'}) +); +expectType>( + execa.sync('unicorns', {encoding: null}) +); +expectType>( + execa.sync('unicorns', ['foo'], {encoding: 'utf8'}) +); +expectType>( + execa.sync('unicorns', ['foo'], {encoding: null}) +); + +expectType>(execa.shellSync('unicorns')); +expectType>( + execa.shellSync('unicorns', {encoding: 'utf8'}) +); +expectType>( + execa.shellSync('unicorns', {encoding: null}) +); diff --git a/package.json b/package.json index d4355ee0dc..b478523f2b 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,11 @@ "node": ">=8" }, "scripts": { - "test": "xo && nyc ava" + "test": "xo && nyc ava && tsd" }, "files": [ "index.js", + "index.d.ts", "lib" ], "keywords": [ @@ -40,13 +41,14 @@ "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", - "merge-stream": "1.0.1", + "merge-stream": "^1.0.1", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-final-newline": "^2.0.0" }, "devDependencies": { + "@types/node": "^11.11.4", "ava": "^1.3.1", "cat-names": "^2.0.0", "coveralls": "^3.0.3", @@ -54,6 +56,7 @@ "is-running": "^2.0.0", "nyc": "^13.3.0", "tempfile": "^2.0.0", + "tsd": "^0.7.0", "xo": "^0.24.0" }, "nyc": {