From 770788027308cb8a5d2002445c65e25024e1a4c6 Mon Sep 17 00:00:00 2001 From: Minh Nguyen <2852660+NMinhNguyen@users.noreply.github.com> Date: Wed, 17 Nov 2021 10:48:00 +0000 Subject: [PATCH] Require Node.js 12.20 and move to ESM (#478) --- .github/workflows/main.yml | 1 - index.d.ts | 940 +++++++++--------- index.js | 69 +- index.test-d.ts | 90 +- lib/command.js | 19 +- lib/error.js | 9 +- lib/kill.js | 33 +- lib/promise.js | 44 +- lib/stdio.js | 7 +- lib/stream.js | 25 +- package.json | 26 +- readme.md | 139 ++- test/command.js | 64 +- test/error.js | 92 +- ...mmand with space => command with space.js} | 3 +- test/fixtures/{delay => delay.js} | 2 +- test/fixtures/detach | 8 - test/fixtures/detach.js | 7 + test/fixtures/{echo-fail => echo-fail.js} | 3 +- test/fixtures/{echo => echo.js} | 3 +- test/fixtures/{environment => environment.js} | 3 +- test/fixtures/{exit => exit.js} | 3 +- test/fixtures/{fail => fail.js} | 2 +- test/fixtures/{forever => forever.js} | 2 - test/fixtures/{max-buffer => max-buffer.js} | 3 +- test/fixtures/no-killable | 12 - test/fixtures/no-killable.js | 12 + .../{non-executable => non-executable.js} | 1 - test/fixtures/{noop-132 => noop-132.js} | 2 +- test/fixtures/{noop-err => noop-err.js} | 3 +- test/fixtures/{noop-throw => noop-throw.js} | 3 +- test/fixtures/{noop => noop.js} | 3 +- test/fixtures/{send => send.js} | 5 +- test/fixtures/{stdin => stdin.js} | 3 +- test/fixtures/sub-process | 8 - .../{sub-process-exit => sub-process-exit.js} | 6 +- test/fixtures/sub-process.js | 8 + test/helpers/override-promise.js | 11 + test/kill.js | 70 +- test/node.js | 44 +- test/override-promise.js | 24 +- test/promise.js | 19 +- test/stdio.js | 10 +- test/stream.js | 83 +- test/test.js | 78 +- 45 files changed, 992 insertions(+), 1010 deletions(-) rename test/fixtures/{command with space => command with space.js} (64%) rename test/fixtures/{delay => delay.js} (65%) delete mode 100755 test/fixtures/detach create mode 100755 test/fixtures/detach.js rename test/fixtures/{echo-fail => echo-fail.js} (69%) rename test/fixtures/{echo => echo.js} (64%) rename test/fixtures/{environment => environment.js} (68%) rename test/fixtures/{exit => exit.js} (61%) rename test/fixtures/{fail => fail.js} (51%) rename test/fixtures/{forever => forever.js} (75%) rename test/fixtures/{max-buffer => max-buffer.js} (81%) delete mode 100755 test/fixtures/no-killable create mode 100755 test/fixtures/no-killable.js rename test/fixtures/{non-executable => non-executable.js} (58%) rename test/fixtures/{noop-132 => noop-132.js} (78%) rename test/fixtures/{noop-err => noop-err.js} (58%) rename test/fixtures/{noop-throw => noop-throw.js} (65%) rename test/fixtures/{noop => noop.js} (57%) rename test/fixtures/{send => send.js} (74%) rename test/fixtures/{stdin => stdin.js} (60%) delete mode 100755 test/fixtures/sub-process rename test/fixtures/{sub-process-exit => sub-process-exit.js} (61%) create mode 100755 test/fixtures/sub-process.js create mode 100644 test/helpers/override-promise.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f00996f767..e9ba5d9ef7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,6 @@ jobs: - 16 - 14 - 12 - - 10 os: - ubuntu-latest - macos-latest diff --git a/index.d.ts b/index.d.ts index 417d535575..fdf9de1c39 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,564 +1,552 @@ -/// -import {ChildProcess} from 'child_process'; -import {Stream, Readable as ReadableStream} from 'stream'; +import {Buffer} from 'node:buffer'; +import {ChildProcess} from 'node:child_process'; +import {Stream, Readable as ReadableStream} from 'node:stream'; + +export type StdioOption = + | 'pipe' + | 'ipc' + | 'ignore' + | 'inherit' + | Stream + | number + | undefined; + +export interface CommonOptions { + /** + 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) + - the parent process is terminated abruptly, for example, with `SIGKILL` as opposed to `SIGTERM` or a normal exit -declare namespace execa { - type StdioOption = - | 'pipe' - | 'ipc' - | 'ignore' - | 'inherit' - | Stream - | number - | undefined; + @default true + */ + readonly cleanup?: boolean; - interface CommonOptions { - /** - 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) - - the parent process is terminated abruptly, for example, with `SIGKILL` as opposed to `SIGTERM` or a normal exit + /** + Prefer locally installed binaries when looking for a binary to execute. - @default true - */ - readonly cleanup?: boolean; + If you `$ npm install foo`, you can then `execa('foo')`. - /** - Prefer locally installed binaries when looking for a binary to execute. + @default false + */ + readonly preferLocal?: boolean; - If you `$ npm install foo`, you can then `execa('foo')`. + /** + Preferred path to find locally installed binaries in (use with `preferLocal`). - @default false - */ - readonly preferLocal?: boolean; + @default process.cwd() + */ + readonly localDir?: string; - /** - Preferred path to find locally installed binaries in (use with `preferLocal`). + /** + Path to the Node.js executable to use in child processes. - @default process.cwd() - */ - readonly localDir?: string; + This can be either an absolute path or a path relative to the `cwd` option. - /** - Path to the Node.js executable to use in child processes. + Requires `preferLocal` to be `true`. - This can be either an absolute path or a path relative to the `cwd` option. + For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process. - Requires `preferLocal` to be `true`. + @default process.execPath + */ + readonly execPath?: string; - For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process. + /** + Buffer the output from the spawned process. When set to `false`, you must read the output of `stdout` and `stderr` (or `all` if the `all` option is `true`). Otherwise the returned promise will not be resolved/rejected. - @default process.execPath - */ - readonly execPath?: string; + If the spawned process fails, `error.stdout`, `error.stderr`, and `error.all` will contain the buffered data. - /** - Buffer the output from the spawned process. When set to `false`, you must read the output of `stdout` and `stderr` (or `all` if the `all` option is `true`). Otherwise the returned promise will not be resolved/rejected. + @default true + */ + readonly buffer?: boolean; - If the spawned process fails, `error.stdout`, `error.stderr`, and `error.all` will contain the buffered data. + /** + Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). - @default true - */ - readonly buffer?: boolean; + @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). + /** + 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; + @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). + /** + 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; + @default 'pipe' + */ + readonly stderr?: StdioOption; - /** - Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). + /** + Setting this to `false` resolves the promise with the error instead of rejecting it. - @default 'pipe' - */ - readonly stderr?: StdioOption; + @default true + */ + readonly reject?: boolean; - /** - Setting this to `false` resolves the promise with the error instead of rejecting it. + /** + Add an `.all` property on the promise and the resolved value. The property contains the output of the process with `stdout` and `stderr` interleaved. - @default true - */ - readonly reject?: boolean; + @default false + */ + readonly all?: boolean; - /** - Add an `.all` property on the promise and the resolved value. The property contains the output of the process with `stdout` and `stderr` interleaved. + /** + Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from the output. - @default false - */ - readonly all?: boolean; + @default true + */ + readonly stripFinalNewline?: boolean; - /** - Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from the output. + /** + Set to `false` if you don't want to extend the environment variables when providing the `env` property. - @default true - */ - readonly stripFinalNewline?: boolean; + @default true + */ + readonly extendEnv?: boolean; - /** - Set to `false` if you don't want to extend the environment variables when providing the `env` property. + /** + Current working directory of the child process. - @default true - */ - readonly extendEnv?: boolean; + @default process.cwd() + */ + readonly cwd?: string; - /** - Current working directory of the child process. + /** + Environment key-value pairs. Extends automatically from `process.env`. Set `extendEnv` to `false` if you don't want this. - @default process.cwd() - */ - readonly cwd?: string; + @default process.env + */ + readonly env?: NodeJS.ProcessEnv; - /** - Environment key-value pairs. Extends automatically from `process.env`. Set `extendEnv` to `false` if you don't want this. + /** + 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; - @default process.env - */ - readonly env?: NodeJS.ProcessEnv; + /** + Child's [stdio](https://nodejs.org/api/child_process.html#child_process_options_stdio) configuration. - /** - 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; + @default 'pipe' + */ + readonly stdio?: 'pipe' | 'ignore' | 'inherit' | readonly StdioOption[]; - /** - Child's [stdio](https://nodejs.org/api/child_process.html#child_process_options_stdio) configuration. + /** + Specify the kind of serialization used for sending messages between processes when using the `stdio: 'ipc'` option or `execaNode()`: + - `json`: Uses `JSON.stringify()` and `JSON.parse()`. + - `advanced`: Uses [`v8.serialize()`](https://nodejs.org/api/v8.html#v8_v8_serialize_value) - @default 'pipe' - */ - readonly stdio?: 'pipe' | 'ignore' | 'inherit' | readonly StdioOption[]; + Requires Node.js `13.2.0` or later. - /** - Specify the kind of serialization used for sending messages between processes when using the `stdio: 'ipc'` option or `execa.node()`: - - `json`: Uses `JSON.stringify()` and `JSON.parse()`. - - `advanced`: Uses [`v8.serialize()`](https://nodejs.org/api/v8.html#v8_v8_serialize_value) + [More info.](https://nodejs.org/api/child_process.html#child_process_advanced_serialization) - Requires Node.js `13.2.0` or later. + @default 'json' + */ + readonly serialization?: 'json' | 'advanced'; - [More info.](https://nodejs.org/api/child_process.html#child_process_advanced_serialization) + /** + 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 'json' - */ - readonly serialization?: 'json' | 'advanced'; + @default false + */ + readonly detached?: boolean; - /** - 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). + /** + Sets the user identity of the process. + */ + readonly uid?: number; - @default false - */ - readonly detached?: boolean; + /** + Sets the group identity of the process. + */ + readonly gid?: number; - /** - Sets the user identity of the process. - */ - readonly uid?: 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. - /** - Sets the group identity of the process. - */ - readonly gid?: number; + We recommend against using this option since it is: + - not cross-platform, encouraging shell-specific syntax. + - slower, because of the additional shell interpretation. + - unsafe, potentially allowing command injection. - /** - 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; - We recommend against using this option since it is: - - not cross-platform, encouraging shell-specific syntax. - - slower, because of the additional shell interpretation. - - unsafe, potentially allowing command injection. + /** + 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 false - */ - readonly shell?: boolean | string; + @default 'utf8' + */ + readonly encoding?: EncodingType; - /** - 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. + /** + 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; + + /** + Largest amount of data in bytes allowed on `stdout` or `stderr`. Default: 100 MB. - @default 'utf8' - */ - readonly encoding?: EncodingType; + @default 100_000_000 + */ + readonly maxBuffer?: number; - /** - 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. + /** + Signal value to be used when the spawned process will be killed. - @default 0 - */ - readonly timeout?: number; + @default 'SIGTERM' + */ + readonly killSignal?: string | number; - /** - Largest amount of data in bytes allowed on `stdout` or `stderr`. Default: 100 MB. + /** + 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 100_000_000 - */ - readonly maxBuffer?: number; + @default false + */ + readonly windowsVerbatimArguments?: boolean; - /** - Signal value to be used when the spawned process will be killed. + /** + On Windows, do not create a new console window. Please note this also prevents `CTRL-C` [from working](https://github.com/nodejs/node/issues/29837) on Windows. - @default 'SIGTERM' - */ - readonly killSignal?: string | number; + @default true + */ + readonly windowsHide?: boolean; +} - /** - 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`. +export interface Options extends CommonOptions { + /** + Write some input to the `stdin` of your binary. + */ + readonly input?: string | Buffer | ReadableStream; +} - @default false - */ - readonly windowsVerbatimArguments?: boolean; +export interface SyncOptions extends CommonOptions { + /** + Write some input to the `stdin` of your binary. + */ + readonly input?: string | Buffer; +} - /** - On Windows, do not create a new console window. Please note this also prevents `CTRL-C` [from working](https://github.com/nodejs/node/issues/29837) on Windows. +export interface NodeOptions extends Options { + /** + The Node.js executable to use. - @default true - */ - readonly windowsHide?: boolean; - } + @default process.execPath + */ + readonly nodePath?: string; - interface Options extends CommonOptions { - /** - Write some input to the `stdin` of your binary. - */ - readonly input?: string | Buffer | ReadableStream; - } + /** + List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable. - interface SyncOptions extends CommonOptions { - /** - Write some input to the `stdin` of your binary. - */ - readonly input?: string | Buffer; - } - - interface NodeOptions extends Options { - /** - The Node.js executable to use. - - @default process.execPath - */ - readonly nodePath?: string; - - /** - List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable. - - @default process.execArgv - */ - readonly nodeOptions?: string[]; - } - - interface ExecaReturnBase { - /** - The file and arguments that were run, for logging purposes. - - This is not escaped and should not be executed directly as a process, including using `execa()` or `execa.command()`. - */ - command: string; - - /** - Same as `command` but escaped. - - This is meant to be copy and pasted into a shell, for debugging purposes. - Since the escaping is fairly basic, this should not be executed directly as a process, including using `execa()` or `execa.command()`. - */ - escapedCommand: string; - - /** - The numeric exit code of the process that was run. - */ - exitCode: number; - - /** - 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; - - /** - Whether the process timed out. - */ - timedOut: boolean; - - /** - Whether the process was killed. - */ - killed: boolean; - - /** - The name of the signal that was used to terminate the process. For example, `SIGFPE`. - - If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. - */ - signal?: string; - - /** - A human-friendly description of the signal that was used to terminate the process. For example, `Floating point arithmetic error`. - - If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen. - */ - signalDescription?: string; - } - - interface ExecaSyncReturnValue - extends ExecaReturnBase { - } - - /** - Result of a child process execution. On success this is a plain object. On failure this is also an `Error` instance. - - The child process fails when: - - its exit code is not `0` - - it was killed with a signal - - timing out - - being canceled - - there's not enough memory or there are already too many child processes - */ - interface ExecaReturnValue - extends ExecaSyncReturnValue { - /** - The output of the process with `stdout` and `stderr` interleaved. - - This is `undefined` if either: - - the `all` option is `false` (default value) - - `execa.sync()` was used - */ - all?: StdoutErrorType; - - /** - Whether the process was canceled. - */ - isCanceled: boolean; - } - - interface ExecaSyncError - extends Error, - ExecaReturnBase { - /** - Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored. - - The child process stderr then stdout are appended to the end, separated with newlines and not interleaved. - */ - message: string; - - /** - This is the same as the `message` property except it does not include the child process stdout/stderr. - */ - shortMessage: string; - - /** - Original error message. This is the same as the `message` property except it includes neither the child process stdout/stderr nor some additional information added by Execa. - - This is `undefined` unless the child process exited due to an `error` event or a timeout. - */ - originalMessage?: string; - } - - interface ExecaError - extends ExecaSyncError { - /** - The output of the process with `stdout` and `stderr` interleaved. - - This is `undefined` if either: - - the `all` option is `false` (default value) - - `execa.sync()` was used - */ - all?: StdoutErrorType; - - /** - Whether the process was canceled. - */ - isCanceled: boolean; - } - - interface KillOptions { - /** - Milliseconds to wait for the child process to terminate before sending `SIGKILL`. - - Can be disabled with `false`. - - @default 5000 - */ - forceKillAfterTimeout?: number | false; - } - - interface ExecaChildPromise { - /** - Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr). - - This is `undefined` if either: - - the `all` option is `false` (the default value) - - both `stdout` and `stderr` options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio) - */ - all?: ReadableStream; - - catch( - onRejected?: (reason: ExecaError) => ResultType | PromiseLike - ): Promise | ResultType>; - - /** - Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal), except if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. - */ - kill(signal?: string, options?: KillOptions): void; - - /** - Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This is preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. - */ - cancel(): void; - } - - type ExecaChildProcess = ChildProcess & - ExecaChildPromise & - Promise>; + @default process.execArgv + */ + readonly nodeOptions?: string[]; } -declare const execa: { +export interface ExecaReturnBase { /** - Execute a file. + The file and arguments that were run, for logging purposes. - Think of this as a mix of `child_process.execFile` and `child_process.spawn`. + This is not escaped and should not be executed directly as a process, including using `execa()` or `execaCommand()`. + */ + command: string; - @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. + /** + Same as `command` but escaped. - @example - ``` - import execa = require('execa'); + This is meant to be copy and pasted into a shell, for debugging purposes. + Since the escaping is fairly basic, this should not be executed directly as a process, including using `execa()` or `execaCommand()`. + */ + escapedCommand: string; + + /** + The numeric exit code of the process that was run. + */ + exitCode: number; + + /** + 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; + + /** + Whether the process timed out. + */ + timedOut: boolean; + + /** + Whether the process was killed. + */ + killed: boolean; + + /** + The name of the signal that was used to terminate the process. For example, `SIGFPE`. + + If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. + */ + signal?: string; + + /** + A human-friendly description of the signal that was used to terminate the process. For example, `Floating point arithmetic error`. + + If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen. + */ + signalDescription?: string; +} + +export interface ExecaSyncReturnValue + extends ExecaReturnBase { +} + +/** +Result of a child process execution. On success this is a plain object. On failure this is also an `Error` instance. + +The child process fails when: +- its exit code is not `0` +- it was killed with a signal +- timing out +- being canceled +- there's not enough memory or there are already too many child processes +*/ +export interface ExecaReturnValue + extends ExecaSyncReturnValue { + /** + The output of the process with `stdout` and `stderr` interleaved. + + This is `undefined` if either: + - the `all` option is `false` (default value) + - `execaSync()` was used + */ + all?: StdoutErrorType; - (async () => { - const {stdout} = await execa('echo', ['unicorns']); - console.log(stdout); - //=> 'unicorns' + /** + Whether the process was canceled. + */ + isCanceled: boolean; +} - // Cancelling a spawned process +export interface ExecaSyncError + extends Error, + ExecaReturnBase { + /** + Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored. - const subprocess = execa('node'); + The child process stderr then stdout are appended to the end, separated with newlines and not interleaved. + */ + message: string; - setTimeout(() => { - subprocess.cancel() - }, 1000); + /** + This is the same as the `message` property except it does not include the child process stdout/stderr. + */ + shortMessage: string; - try { - await subprocess; - } catch (error) { - console.log(subprocess.killed); // true - console.log(error.isCanceled); // true - } - })(); + /** + Original error message. This is the same as the `message` property except it includes neither the child process stdout/stderr nor some additional information added by Execa. - // Pipe the child process stdout to the current stdout - execa('echo', ['unicorns']).stdout.pipe(process.stdout); - ``` + This is `undefined` unless the child process exited due to an `error` event or a timeout. */ - ( - file: string, - arguments?: readonly string[], - options?: execa.Options - ): execa.ExecaChildProcess; - ( - file: string, - arguments?: readonly string[], - options?: execa.Options - ): execa.ExecaChildProcess; - (file: string, options?: execa.Options): execa.ExecaChildProcess; - (file: string, options?: execa.Options): execa.ExecaChildProcess< - Buffer - >; + originalMessage?: string; +} +export interface ExecaError + extends ExecaSyncError { /** - Execute a file synchronously. + The output of the process with `stdout` and `stderr` interleaved. - This method throws an `Error` if the command fails. + This is `undefined` if either: + - the `all` option is `false` (default value) + - `execaSync()` was used + */ + all?: StdoutErrorType; - @param file - The program/script to execute. - @param arguments - Arguments to pass to `file` on execution. - @returns A result `Object` with `stdout` and `stderr` properties. + /** + Whether the process was canceled. */ - sync( - file: string, - arguments?: readonly string[], - options?: execa.SyncOptions - ): execa.ExecaSyncReturnValue; - sync( - file: string, - arguments?: readonly string[], - options?: execa.SyncOptions - ): execa.ExecaSyncReturnValue; - sync(file: string, options?: execa.SyncOptions): execa.ExecaSyncReturnValue; - sync( - file: string, - options?: execa.SyncOptions - ): execa.ExecaSyncReturnValue; + isCanceled: boolean; +} +export interface KillOptions { /** - Same as `execa()` except both file and arguments are specified in a single `command` string. For example, `execa('echo', ['unicorns'])` is the same as `execa.command('echo unicorns')`. + Milliseconds to wait for the child process to terminate before sending `SIGKILL`. + + Can be disabled with `false`. - If the file or an argument contains spaces, they must be escaped with backslashes. This matters especially if `command` is not a constant but a variable, for example with `__dirname` or `process.cwd()`. Except for spaces, no escaping/quoting is needed. + @default 5000 + */ + forceKillAfterTimeout?: number | false; +} + +export interface ExecaChildPromise { + /** + Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr). - The `shell` option must be used if the `command` uses shell-specific features (for example, `&&` or `||`), as opposed to being a simple `file` followed by its `arguments`. + This is `undefined` if either: + - the `all` option is `false` (the default value) + - both `stdout` and `stderr` options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio) + */ + all?: ReadableStream; - @param command - The program/script to execute and its arguments. - @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. + catch( + onRejected?: (reason: ExecaError) => ResultType | PromiseLike + ): Promise | ResultType>; - @example - ``` - import execa = require('execa'); + /** + Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal), except if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. + */ + kill(signal?: string, options?: KillOptions): void; - (async () => { - const {stdout} = await execa.command('echo unicorns'); - console.log(stdout); - //=> 'unicorns' - })(); - ``` + /** + Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This is preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. */ - command(command: string, options?: execa.Options): execa.ExecaChildProcess; - command(command: string, options?: execa.Options): execa.ExecaChildProcess; - - /** - Same as `execa.command()` but synchronous. - - @param command - The program/script to execute and its arguments. - @returns A result `Object` with `stdout` and `stderr` properties. - */ - commandSync(command: string, options?: execa.SyncOptions): execa.ExecaSyncReturnValue; - commandSync(command: string, options?: execa.SyncOptions): execa.ExecaSyncReturnValue; - - /** - Execute a Node.js script as a child process. - - Same as `execa('node', [scriptPath, ...arguments], options)` except (like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options)): - - the current Node version and options are used. This can be overridden using the `nodePath` and `nodeArguments` options. - - the `shell` option cannot be used - - an extra channel [`ipc`](https://nodejs.org/api/child_process.html#child_process_options_stdio) is passed to [`stdio`](#stdio) - - @param scriptPath - Node.js script to execute. - @param arguments - Arguments to pass to `scriptPath` 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. - */ - node( - scriptPath: string, - arguments?: readonly string[], - options?: execa.NodeOptions - ): execa.ExecaChildProcess; - node( - scriptPath: string, - arguments?: readonly string[], - options?: execa.Options - ): execa.ExecaChildProcess; - node(scriptPath: string, options?: execa.Options): execa.ExecaChildProcess; - node(scriptPath: string, options?: execa.Options): execa.ExecaChildProcess; -}; - -export = execa; + cancel(): void; +} + +export type ExecaChildProcess = ChildProcess & +ExecaChildPromise & +Promise>; + +/** +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'; + +const {stdout} = await execa('echo', ['unicorns']); +console.log(stdout); +//=> 'unicorns' + +// Cancelling a spawned process + +const subprocess = execa('node'); + +setTimeout(() => { + subprocess.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); +``` +*/ +export function execa( + file: string, + arguments?: readonly string[], + options?: Options +): ExecaChildProcess; +export function execa( + file: string, + arguments?: readonly string[], + options?: Options +): ExecaChildProcess; +export function execa(file: string, options?: Options): ExecaChildProcess; +export function execa(file: 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 A result `Object` with `stdout` and `stderr` properties. +*/ +export function execaSync( + file: string, + arguments?: readonly string[], + options?: SyncOptions +): ExecaSyncReturnValue; +export function execaSync( + file: string, + arguments?: readonly string[], + options?: SyncOptions +): ExecaSyncReturnValue; +export function execaSync(file: string, options?: SyncOptions): ExecaSyncReturnValue; +export function execaSync( + file: string, + options?: SyncOptions +): ExecaSyncReturnValue; + +/** +Same as `execa()` except both file and arguments are specified in a single `command` string. For example, `execa('echo', ['unicorns'])` is the same as `execaCommand('echo unicorns')`. + +If the file or an argument contains spaces, they must be escaped with backslashes. This matters especially if `command` is not a constant but a variable, for example with `__dirname` or `process.cwd()`. Except for spaces, no escaping/quoting is needed. + +The `shell` option must be used if the `command` uses shell-specific features (for example, `&&` or `||`), as opposed to being a simple `file` followed by its `arguments`. + +@param command - The program/script to execute and its arguments. +@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 {execaCommand} from 'execa'; + +const {stdout} = await execaCommand('echo unicorns'); +console.log(stdout); +//=> 'unicorns' +``` +*/ +export function execaCommand(command: string, options?: Options): ExecaChildProcess; +export function execaCommand(command: string, options?: Options): ExecaChildProcess; + +/** +Same as `execaCommand()` but synchronous. + +@param command - The program/script to execute and its arguments. +@returns A result `Object` with `stdout` and `stderr` properties. +*/ +export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; +export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; + +/** +Execute a Node.js script as a child process. + +Same as `execa('node', [scriptPath, ...arguments], options)` except (like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options)): + - the current Node version and options are used. This can be overridden using the `nodePath` and `nodeArguments` options. + - the `shell` option cannot be used + - an extra channel [`ipc`](https://nodejs.org/api/child_process.html#child_process_options_stdio) is passed to [`stdio`](#stdio) + +@param scriptPath - Node.js script to execute. +@param arguments - Arguments to pass to `scriptPath` 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. +*/ +export function execaNode( + scriptPath: string, + arguments?: readonly string[], + options?: NodeOptions +): ExecaChildProcess; +export function execaNode( + scriptPath: string, + arguments?: readonly string[], + options?: Options +): ExecaChildProcess; +export function execaNode(scriptPath: string, options?: Options): ExecaChildProcess; +export function execaNode(scriptPath: string, options?: Options): ExecaChildProcess; diff --git a/index.js b/index.js index 6fc9f12954..ff33fce3aa 100644 --- a/index.js +++ b/index.js @@ -1,16 +1,17 @@ -'use strict'; -const path = require('path'); -const childProcess = require('child_process'); -const crossSpawn = require('cross-spawn'); -const stripFinalNewline = require('strip-final-newline'); -const npmRunPath = require('npm-run-path'); -const onetime = require('onetime'); -const makeError = require('./lib/error'); -const normalizeStdio = require('./lib/stdio'); -const {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} = require('./lib/kill'); -const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = require('./lib/stream'); -const {mergePromise, getSpawnedPromise} = require('./lib/promise'); -const {joinCommand, parseCommand, getEscapedCommand} = require('./lib/command'); +import {Buffer} from 'node:buffer'; +import path from 'node:path'; +import childProcess from 'node:child_process'; +import process from 'node:process'; +import crossSpawn from 'cross-spawn'; +import stripFinalNewline from 'strip-final-newline'; +import {npmRunPathEnv} from 'npm-run-path'; +import onetime from 'onetime'; +import {makeError} from './lib/error.js'; +import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; +import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; +import {handleInput, getSpawnedResult, makeAllStream, validateInputSync} from './lib/stream.js'; +import {mergePromise, getSpawnedPromise} from './lib/promise.js'; +import {joinCommand, parseCommand, getEscapedCommand} from './lib/command.js'; const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; @@ -18,7 +19,7 @@ const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => const env = extendEnv ? {...process.env, ...envOption} : envOption; if (preferLocal) { - return npmRunPath.env({env, cwd: localDir, execPath}); + return npmRunPathEnv({env, cwd: localDir, execPath}); } return env; @@ -43,7 +44,7 @@ const handleArguments = (file, args, options = {}) => { cleanup: true, all: false, windowsHide: true, - ...options + ...options, }; options.env = getEnv(options); @@ -60,7 +61,7 @@ const handleArguments = (file, args, options = {}) => { const handleOutput = (options, value, error) => { if (typeof value !== 'string' && !Buffer.isBuffer(value)) { - // When `execa.sync()` errors, we normalize it to '' to mimic `execa()` + // When `execaSync()` errors, we normalize it to '' to mimic `execa()` return error === undefined ? undefined : ''; } @@ -71,7 +72,7 @@ const handleOutput = (options, value, error) => { return value; }; -const execa = (file, args, options) => { +export const execa = (file, args, options) => { const parsed = handleArguments(file, args, options); const command = joinCommand(file, args); const escapedCommand = getEscapedCommand(file, args); @@ -94,7 +95,7 @@ const execa = (file, args, options) => { parsed, timedOut: false, isCanceled: false, - killed: false + killed: false, })); return mergePromise(dummySpawned, errorPromise); } @@ -127,7 +128,7 @@ const execa = (file, args, options) => { parsed, timedOut, isCanceled: context.isCanceled, - killed: spawned.killed + killed: spawned.killed, }); if (!parsed.options.reject) { @@ -147,7 +148,7 @@ const execa = (file, args, options) => { failed: false, timedOut: false, isCanceled: false, - killed: false + killed: false, }; }; @@ -160,9 +161,7 @@ const execa = (file, args, options) => { return mergePromise(spawned, handlePromiseOnce); }; -module.exports = execa; - -module.exports.sync = (file, args, options) => { +export const execaSync = (file, args, options) => { const parsed = handleArguments(file, args, options); const command = joinCommand(file, args); const escapedCommand = getEscapedCommand(file, args); @@ -183,7 +182,7 @@ module.exports.sync = (file, args, options) => { parsed, timedOut: false, isCanceled: false, - killed: false + killed: false, }); } @@ -202,7 +201,7 @@ module.exports.sync = (file, args, options) => { parsed, timedOut: result.error && result.error.code === 'ETIMEDOUT', isCanceled: false, - killed: result.signal !== null + killed: result.signal !== null, }); if (!parsed.options.reject) { @@ -221,32 +220,32 @@ module.exports.sync = (file, args, options) => { failed: false, timedOut: false, isCanceled: false, - killed: false + killed: false, }; }; -module.exports.command = (command, options) => { +export const execaCommand = (command, options) => { const [file, ...args] = parseCommand(command); return execa(file, args, options); }; -module.exports.commandSync = (command, options) => { +export const execaCommandSync = (command, options) => { const [file, ...args] = parseCommand(command); - return execa.sync(file, args, options); + return execaSync(file, args, options); }; -module.exports.node = (scriptPath, args, options = {}) => { +export const execaNode = (scriptPath, args, options = {}) => { if (args && !Array.isArray(args) && typeof args === 'object') { options = args; args = []; } - const stdio = normalizeStdio.node(options); + const stdio = normalizeStdioNode(options); const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); const { nodePath = process.execPath, - nodeOptions = defaultExecArgv + nodeOptions = defaultExecArgv, } = options; return execa( @@ -254,7 +253,7 @@ module.exports.node = (scriptPath, args, options = {}) => { [ ...nodeOptions, scriptPath, - ...(Array.isArray(args) ? args : []) + ...(Array.isArray(args) ? args : []), ], { ...options, @@ -262,7 +261,7 @@ module.exports.node = (scriptPath, args, options = {}) => { stdout: undefined, stderr: undefined, stdio, - shell: false - } + shell: false, + }, ); }; diff --git a/index.test-d.ts b/index.test-d.ts index b5da697b24..9b1d0924bf 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,13 +1,22 @@ +import {Buffer} from 'node:buffer'; +// For some reason a default import of `process` causes +// `process.stdin`, `process.stderr`, and `process.stdout` +// to get treated as `any` by `@typescript-eslint/no-unsafe-assignment`. +import * as process from 'node:process'; +import {Readable as ReadableStream} from 'node:stream'; import {expectType, expectError} from 'tsd'; -import {Readable as ReadableStream} from 'stream'; -import execa = require('.'); import { + execa, + execaSync, + execaCommand, + execaCommandSync, + execaNode, ExecaReturnValue, ExecaChildProcess, ExecaError, ExecaSyncReturnValue, - ExecaSyncError -} from '.'; + ExecaSyncError, +} from './index.js'; try { const execaPromise = execa('unicorns'); @@ -46,7 +55,7 @@ try { } try { - const unicornsResult = execa.sync('unicorns'); + const unicornsResult = execaSync('unicorns'); expectType(unicornsResult.command); expectType(unicornsResult.escapedCommand); expectType(unicornsResult.exitCode); @@ -77,6 +86,7 @@ try { expectType(execaError.originalMessage); } +/* eslint-disable @typescript-eslint/no-floating-promises */ execa('unicorns', {cleanup: false}); execa('unicorns', {preferLocal: false}); execa('unicorns', {localDir: '.'}); @@ -111,13 +121,14 @@ execa('unicorns', {reject: false}); execa('unicorns', {stripFinalNewline: false}); execa('unicorns', {extendEnv: false}); execa('unicorns', {cwd: '.'}); +// eslint-disable-next-line @typescript-eslint/naming-convention execa('unicorns', {env: {PATH: ''}}); 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, undefined] + stdio: ['pipe', 'ipc', 'ignore', 'inherit', process.stdin, 1, undefined], }); execa('unicorns', {serialization: 'advanced'}); execa('unicorns', {detached: true}); @@ -131,6 +142,7 @@ execa('unicorns', {killSignal: 'SIGTERM'}); execa('unicorns', {killSignal: 9}); execa('unicorns', {windowsVerbatimArguments: true}); execa('unicorns', {windowsHide: false}); +/* eslint-enable @typescript-eslint/no-floating-promises */ execa('unicorns').kill(); execa('unicorns').kill('SIGKILL'); execa('unicorns').kill(undefined); @@ -139,55 +151,55 @@ execa('unicorns').kill('SIGKILL', {forceKillAfterTimeout: false}); execa('unicorns').kill('SIGKILL', {forceKillAfterTimeout: 42}); execa('unicorns').kill('SIGKILL', {forceKillAfterTimeout: undefined}); -expectType>(execa('unicorns')); -expectType>(await execa('unicorns')); -expectType>( - await execa('unicorns', {encoding: 'utf8'}) +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: 'utf8'}), ); expectType>( - await execa('unicorns', ['foo'], {encoding: null}) + await execa('unicorns', ['foo'], {encoding: null}), ); -expectType>(execa.sync('unicorns')); -expectType>( - execa.sync('unicorns', {encoding: 'utf8'}) +expectType(execaSync('unicorns')); +expectType( + execaSync('unicorns', {encoding: 'utf8'}), ); expectType>( - execa.sync('unicorns', {encoding: null}) + execaSync('unicorns', {encoding: null}), ); -expectType>( - execa.sync('unicorns', ['foo'], {encoding: 'utf8'}) +expectType( + execaSync('unicorns', ['foo'], {encoding: 'utf8'}), ); expectType>( - execa.sync('unicorns', ['foo'], {encoding: null}) + execaSync('unicorns', ['foo'], {encoding: null}), ); -expectType>(execa.command('unicorns')); -expectType>(await execa.command('unicorns')); -expectType>(await execa.command('unicorns', {encoding: 'utf8'})); -expectType>(await execa.command('unicorns', {encoding: null})); -expectType>(await execa.command('unicorns foo', {encoding: 'utf8'})); -expectType>(await execa.command('unicorns foo', {encoding: null})); +expectType(execaCommand('unicorns')); +expectType(await execaCommand('unicorns')); +expectType(await execaCommand('unicorns', {encoding: 'utf8'})); +expectType>(await execaCommand('unicorns', {encoding: null})); +expectType(await execaCommand('unicorns foo', {encoding: 'utf8'})); +expectType>(await execaCommand('unicorns foo', {encoding: null})); -expectType>(execa.commandSync('unicorns')); -expectType>(execa.commandSync('unicorns', {encoding: 'utf8'})); -expectType>(execa.commandSync('unicorns', {encoding: null})); -expectType>(execa.commandSync('unicorns foo', {encoding: 'utf8'})); -expectType>(execa.commandSync('unicorns foo', {encoding: null})); +expectType(execaCommandSync('unicorns')); +expectType(execaCommandSync('unicorns', {encoding: 'utf8'})); +expectType>(execaCommandSync('unicorns', {encoding: null})); +expectType(execaCommandSync('unicorns foo', {encoding: 'utf8'})); +expectType>(execaCommandSync('unicorns foo', {encoding: null})); -expectType>(execa.node('unicorns')); -expectType>(await execa.node('unicorns')); -expectType>( - await execa.node('unicorns', {encoding: 'utf8'}) +expectType(execaNode('unicorns')); +expectType(await execaNode('unicorns')); +expectType( + await execaNode('unicorns', {encoding: 'utf8'}), ); -expectType>(await execa.node('unicorns', {encoding: null})); -expectType>( - await execa.node('unicorns', ['foo'], {encoding: 'utf8'}) +expectType>(await execaNode('unicorns', {encoding: null})); +expectType( + await execaNode('unicorns', ['foo'], {encoding: 'utf8'}), ); expectType>( - await execa.node('unicorns', ['foo'], {encoding: null}) + await execaNode('unicorns', ['foo'], {encoding: null}), ); diff --git a/lib/command.js b/lib/command.js index 859b006a06..be64255216 100644 --- a/lib/command.js +++ b/lib/command.js @@ -1,4 +1,3 @@ -'use strict'; const normalizeArgs = (file, args = []) => { if (!Array.isArray(args)) { return [file]; @@ -18,18 +17,14 @@ const escapeArg = arg => { return `"${arg.replace(DOUBLE_QUOTES_REGEXP, '\\"')}"`; }; -const joinCommand = (file, args) => { - return normalizeArgs(file, args).join(' '); -}; +export const joinCommand = (file, args) => normalizeArgs(file, args).join(' '); -const getEscapedCommand = (file, args) => { - return normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' '); -}; +export const getEscapedCommand = (file, args) => normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' '); const SPACES_REGEXP = / +/g; -// Handle `execa.command()` -const parseCommand = command => { +// Handle `execaCommand()` +export const parseCommand = command => { const tokens = []; for (const token of command.trim().split(SPACES_REGEXP)) { // Allow spaces to be escaped by a backslash if not meant as a delimiter @@ -44,9 +39,3 @@ const parseCommand = command => { return tokens; }; - -module.exports = { - joinCommand, - getEscapedCommand, - parseCommand -}; diff --git a/lib/error.js b/lib/error.js index 42144674dc..b12c144428 100644 --- a/lib/error.js +++ b/lib/error.js @@ -1,5 +1,4 @@ -'use strict'; -const {signalsByName} = require('human-signals'); +import {signalsByName} from 'human-signals'; const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { if (timedOut) { @@ -25,7 +24,7 @@ const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription return 'failed'; }; -const makeError = ({ +export const makeError = ({ stdout, stderr, all, @@ -37,7 +36,7 @@ const makeError = ({ timedOut, isCanceled, killed, - parsed: {options: {timeout}} + parsed: {options: {timeout}}, }) => { // `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`. // We normalize them to `undefined` @@ -84,5 +83,3 @@ const makeError = ({ return error; }; - -module.exports = makeError; diff --git a/lib/kill.js b/lib/kill.js index 287a14238e..efc6b9ba41 100644 --- a/lib/kill.js +++ b/lib/kill.js @@ -1,11 +1,10 @@ -'use strict'; -const os = require('os'); -const onExit = require('signal-exit'); +import os from 'node:os'; +import onExit from 'signal-exit'; const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; // Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior -const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { +export const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { const killResult = kill(signal); setKillTimeout(kill, signal, options, killResult); return killResult; @@ -30,14 +29,10 @@ const setKillTimeout = (kill, signal, options, killResult) => { } }; -const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => { - return isSigterm(signal) && forceKillAfterTimeout !== false && killResult; -}; +const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult; -const isSigterm = signal => { - return signal === os.constants.signals.SIGTERM || - (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); -}; +const isSigterm = signal => signal === os.constants.signals.SIGTERM + || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { if (forceKillAfterTimeout === true) { @@ -52,7 +47,7 @@ const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { }; // `childProcess.cancel()` -const spawnedCancel = (spawned, context) => { +export const spawnedCancel = (spawned, context) => { const killResult = spawned.kill(); if (killResult) { @@ -66,7 +61,7 @@ const timeoutKill = (spawned, signal, reject) => { }; // `timeout` option handling -const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { +export const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { if (timeout === 0 || timeout === undefined) { return spawnedPromise; } @@ -85,14 +80,14 @@ const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise return Promise.race([timeoutPromise, safeSpawnedPromise]); }; -const validateTimeout = ({timeout}) => { +export const validateTimeout = ({timeout}) => { if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) { throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); } }; // `cleanup` option handling -const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { +export const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { if (!cleanup || detached) { return timedPromise; } @@ -105,11 +100,3 @@ const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { removeExitHandler(); }); }; - -module.exports = { - spawnedKill, - spawnedCancel, - setupTimeout, - validateTimeout, - setExitHandler -}; diff --git a/lib/promise.js b/lib/promise.js index bd9d52333d..c655c0e4a5 100644 --- a/lib/promise.js +++ b/lib/promise.js @@ -1,18 +1,16 @@ -'use strict'; - const nativePromisePrototype = (async () => {})().constructor.prototype; const descriptors = ['then', 'catch', 'finally'].map(property => [ property, - Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property) + Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property), ]); // The return value is a mixin of `childProcess` and `Promise` -const mergePromise = (spawned, promise) => { +export const mergePromise = (spawned, promise) => { for (const [property, descriptor] of descriptors) { // Starting the main `promise` is deferred to avoid consuming streams - const value = typeof promise === 'function' ? - (...args) => Reflect.apply(descriptor.value, promise(), args) : - descriptor.value.bind(promise); + const value = typeof promise === 'function' + ? (...args) => Reflect.apply(descriptor.value, promise(), args) + : descriptor.value.bind(promise); Reflect.defineProperty(spawned, property, {...descriptor, value}); } @@ -21,26 +19,18 @@ const mergePromise = (spawned, promise) => { }; // Use promises instead of `child_process` events -const getSpawnedPromise = spawned => { - return new Promise((resolve, reject) => { - spawned.on('exit', (exitCode, signal) => { - resolve({exitCode, signal}); - }); - - spawned.on('error', error => { - reject(error); - }); - - if (spawned.stdin) { - spawned.stdin.on('error', error => { - reject(error); - }); - } +export const getSpawnedPromise = spawned => new Promise((resolve, reject) => { + spawned.on('exit', (exitCode, signal) => { + resolve({exitCode, signal}); }); -}; -module.exports = { - mergePromise, - getSpawnedPromise -}; + spawned.on('error', error => { + reject(error); + }); + if (spawned.stdin) { + spawned.stdin.on('error', error => { + reject(error); + }); + } +}); diff --git a/lib/stdio.js b/lib/stdio.js index 45129ed7ef..e8c1132dc1 100644 --- a/lib/stdio.js +++ b/lib/stdio.js @@ -1,9 +1,8 @@ -'use strict'; const aliases = ['stdin', 'stdout', 'stderr']; const hasAlias = options => aliases.some(alias => options[alias] !== undefined); -const normalizeStdio = options => { +export const normalizeStdio = options => { if (!options) { return; } @@ -30,10 +29,8 @@ const normalizeStdio = options => { return Array.from({length}, (value, index) => stdio[index]); }; -module.exports = normalizeStdio; - // `ipc` is pushed unless it is already present -module.exports.node = options => { +export const normalizeStdioNode = options => { const stdio = normalizeStdio(options); if (stdio === 'ipc') { diff --git a/lib/stream.js b/lib/stream.js index d445dd4710..b140bf14f9 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -1,10 +1,9 @@ -'use strict'; -const isStream = require('is-stream'); -const getStream = require('get-stream'); -const mergeStream = require('merge-stream'); +import {isStream} from 'is-stream'; +import getStream from 'get-stream'; +import mergeStream from 'merge-stream'; // `input` option -const handleInput = (spawned, input) => { +export const handleInput = (spawned, input) => { // Checking for stdin is workaround for https://github.com/nodejs/node/issues/26852 // @todo remove `|| spawned.stdin === undefined` once we drop support for Node.js <=12.2.0 if (input === undefined || spawned.stdin === undefined) { @@ -19,7 +18,7 @@ const handleInput = (spawned, input) => { }; // `all` interleaves `stdout` and `stderr` -const makeAllStream = (spawned, {all}) => { +export const makeAllStream = (spawned, {all}) => { if (!all || (!spawned.stdout && !spawned.stderr)) { return; } @@ -65,7 +64,7 @@ const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => { }; // Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) -const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { +export const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { const stdoutPromise = getStreamPromise(stdout, {encoding, buffer, maxBuffer}); const stderrPromise = getStreamPromise(stderr, {encoding, buffer, maxBuffer}); const allPromise = getStreamPromise(all, {encoding, buffer, maxBuffer: maxBuffer * 2}); @@ -77,21 +76,13 @@ const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuf {error, signal: error.signal, timedOut: error.timedOut}, getBufferedData(stdout, stdoutPromise), getBufferedData(stderr, stderrPromise), - getBufferedData(all, allPromise) + getBufferedData(all, allPromise), ]); } }; -const validateInputSync = ({input}) => { +export const validateInputSync = ({input}) => { if (isStream(input)) { throw new TypeError('The `input` option cannot be a stream in sync mode'); } }; - -module.exports = { - handleInput, - makeAllStream, - getSpawnedResult, - validateInputSync -}; - diff --git a/package.json b/package.json index 22556f28d8..3f3a87bb0c 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ "url": "https://sindresorhus.com" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, + "type": "module", + "exports": "./index.js", "scripts": { - "test": "xo && nyc ava && tsd" + "test": "xo && c8 ava && tsd" }, "files": [ "index.js", @@ -41,26 +43,26 @@ "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", + "npm-run-path": "^5.0.1", + "onetime": "^6.0.0", "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "strip-final-newline": "^3.0.0" }, "devDependencies": { - "@types/node": "^14.14.10", - "ava": "^2.4.0", + "@types/node": "^16.11.7", + "ava": "^3.15.0", + "c8": "^7.10.0", "get-node": "^11.0.1", "is-running": "^2.1.0", - "nyc": "^15.1.0", "p-event": "^4.2.0", "tempfile": "^3.0.0", "tsd": "^0.13.1", - "xo": "^0.35.0" + "xo": "^0.46.4" }, - "nyc": { + "c8": { "reporter": [ "text", "lcov" diff --git a/readme.md b/readme.md index 843edbc7d1..33f031a365 100644 --- a/readme.md +++ b/readme.md @@ -29,19 +29,17 @@ $ npm install execa ## Usage ```js -const execa = require('execa'); +import {execa} from 'execa'; -(async () => { - const {stdout} = await execa('echo', ['unicorns']); - console.log(stdout); - //=> 'unicorns' -})(); +const {stdout} = await execa('echo', ['unicorns']); +console.log(stdout); +//=> 'unicorns' ``` ### Pipe the child process stdout to the parent ```js -const execa = require('execa'); +import {execa} from 'execa'; execa('echo', ['unicorns']).stdout.pipe(process.stdout); ``` @@ -49,66 +47,63 @@ execa('echo', ['unicorns']).stdout.pipe(process.stdout); ### Handling Errors ```js -const execa = require('execa'); - -(async () => { - // Catching an error - try { - await execa('unknown', ['command']); - } catch (error) { - console.log(error); - /* - { - message: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', - errno: -2, - code: 'ENOENT', - syscall: 'spawn unknown', - path: 'unknown', - spawnargs: ['command'], - originalMessage: 'spawn unknown ENOENT', - shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', - command: 'unknown command', - escapedCommand: 'unknown command', - stdout: '', - stderr: '', - all: '', - failed: true, - timedOut: false, - isCanceled: false, - killed: false - } - */ - } +import {execa} from 'execa'; -})(); +// Catching an error +try { + await execa('unknown', ['command']); +} catch (error) { + console.log(error); + /* + { + message: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', + errno: -2, + code: 'ENOENT', + syscall: 'spawn unknown', + path: 'unknown', + spawnargs: ['command'], + originalMessage: 'spawn unknown ENOENT', + shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', + command: 'unknown command', + escapedCommand: 'unknown command', + stdout: '', + stderr: '', + all: '', + failed: true, + timedOut: false, + isCanceled: false, + killed: false + } + */ +} ``` ### Cancelling a spawned process ```js -const execa = require('execa'); +import {execa} from 'execa'; -(async () => { - const subprocess = execa('node'); +const subprocess = execa('node'); - setTimeout(() => { - subprocess.cancel(); - }, 1000); +setTimeout(() => { + subprocess.cancel(); +}, 1000); - try { - await subprocess; - } catch (error) { - console.log(subprocess.killed); // true - console.log(error.isCanceled); // true - } -})() +try { + await subprocess; +} catch (error) { + console.log(subprocess.killed); // true + console.log(error.isCanceled); // true +} ``` ### Catching an error with the sync method ```js +import {execaSync} from 'execa'; + try { - execa.sync('unknown', ['command']); + execaSync('unknown', ['command']); } catch (error) { console.log(error); /* @@ -190,27 +185,27 @@ This is `undefined` if either: - the [`all` option](#all-2) is `false` (the default value) - both [`stdout`](#stdout-1) and [`stderr`](#stderr-1) options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio) -### execa.sync(file, arguments?, options?) +### execaSync(file, arguments?, options?) Execute a file synchronously. Returns or throws a [`childProcessResult`](#childProcessResult). -### execa.command(command, options?) +### execaCommand(command, options?) -Same as [`execa()`](#execafile-arguments-options) except both file and arguments are specified in a single `command` string. For example, `execa('echo', ['unicorns'])` is the same as `execa.command('echo unicorns')`. +Same as [`execa()`](#execafile-arguments-options) except both file and arguments are specified in a single `command` string. For example, `execa('echo', ['unicorns'])` is the same as `execaCommand('echo unicorns')`. If the file or an argument contains spaces, they must be escaped with backslashes. This matters especially if `command` is not a constant but a variable, for example with `__dirname` or `process.cwd()`. Except for spaces, no escaping/quoting is needed. The [`shell` option](#shell) must be used if the `command` uses shell-specific features (for example, `&&` or `||`), as opposed to being a simple `file` followed by its `arguments`. -### execa.commandSync(command, options?) +### execaCommandSync(command, options?) -Same as [`execa.command()`](#execacommand-command-options) but synchronous. +Same as [`execaCommand()`](#execacommand-command-options) but synchronous. Returns or throws a [`childProcessResult`](#childProcessResult). -### execa.node(scriptPath, arguments?, options?) +### execaNode(scriptPath, arguments?, options?) Execute a Node.js script as a child process. @@ -238,7 +233,7 @@ Type: `string` The file and arguments that were run, for logging purposes. -This is not escaped and should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execa.command()`](#execacommandcommand-options). +This is not escaped and should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execaCommand()`](#execacommandcommand-options). #### escapedCommand @@ -247,7 +242,7 @@ Type: `string` Same as [`command`](#command) but escaped. This is meant to be copy and pasted into a shell, for debugging purposes. -Since the escaping is fairly basic, this should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execa.command()`](#execacommandcommand-options). +Since the escaping is fairly basic, this should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execaCommand()`](#execacommandcommand-options). #### exitCode @@ -275,7 +270,7 @@ The output of the process with `stdout` and `stderr` interleaved. This is `undefined` if either: - the [`all` option](#all-2) is `false` (the default value) - - `execa.sync()` was used + - `execaSync()` was used #### failed @@ -481,7 +476,7 @@ Child's [stdio](https://nodejs.org/api/child_process.html#child_process_options_ Type: `string`\ Default: `'json'` -Specify the kind of serialization used for sending messages between processes when using the [`stdio: 'ipc'`](#stdio) option or [`execa.node()`](#execanodescriptpath-arguments-options): +Specify the kind of serialization used for sending messages between processes when using the [`stdio: 'ipc'`](#stdio) option or [`execaNode()`](#execanodescriptpath-arguments-options): - `json`: Uses `JSON.stringify()` and `JSON.parse()`. - `advanced`: Uses [`v8.serialize()`](https://nodejs.org/api/v8.html#v8_v8_serialize_value) @@ -582,16 +577,14 @@ List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Gracefully handle failures by using automatic retries and exponential backoff with the [`p-retry`](https://github.com/sindresorhus/p-retry) package: ```js -const pRetry = require('p-retry'); +import pRetry from 'p-retry'; const run = async () => { const results = await execa('curl', ['-sSL', 'https://sindresorhus.com/unicorn']); return results; }; -(async () => { - console.log(await pRetry(run, {retries: 5})); -})(); +console.log(await pRetry(run, {retries: 5})); ``` ### Save and pipe output from a child process @@ -599,21 +592,19 @@ const run = async () => { Let's say you want to show the output of a child process in real-time while also saving it to a variable. ```js -const execa = require('execa'); +import {execa} from 'execa'; const subprocess = execa('echo', ['foo']); subprocess.stdout.pipe(process.stdout); -(async () => { - const {stdout} = await subprocess; - console.log('child output:', stdout); -})(); +const {stdout} = await subprocess; +console.log('child output:', stdout); ``` ### Redirect output to a file ```js -const execa = require('execa'); +import {execa} from 'execa'; const subprocess = execa('echo', ['foo']) subprocess.stdout.pipe(fs.createWriteStream('stdout.txt')) @@ -622,7 +613,7 @@ subprocess.stdout.pipe(fs.createWriteStream('stdout.txt')) ### Redirect input from a file ```js -const execa = require('execa'); +import {execa} from 'execa'; const subprocess = execa('cat') fs.createReadStream('stdin.txt').pipe(subprocess.stdin) @@ -631,7 +622,7 @@ fs.createReadStream('stdin.txt').pipe(subprocess.stdin) ### Execute the current package's binary ```js -const {getBinPathSync} = require('get-bin-path'); +import {getBinPathSync} from 'get-bin-path'; const binPath = getBinPathSync(); const subprocess = execa(binPath); diff --git a/test/command.js b/test/command.js index 6c45715388..0db7ab9c49 100644 --- a/test/command.js +++ b/test/command.js @@ -1,15 +1,17 @@ -import path from 'path'; +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; import test from 'ava'; -import execa from '..'; +import {execa, execaSync, execaCommand, execaCommandSync} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; const command = async (t, expected, ...args) => { - const {command: failCommand} = await t.throwsAsync(execa('fail', args)); - t.is(failCommand, `fail${expected}`); + const {command: failCommand} = await t.throwsAsync(execa('fail.js', args)); + t.is(failCommand, `fail.js${expected}`); - const {command} = await execa('noop', args); - t.is(command, `noop${expected}`); + const {command} = await execa('noop.js', args); + t.is(command, `noop.js${expected}`); }; command.title = (message, expected) => `command is: ${JSON.stringify(expected)}`; @@ -19,19 +21,19 @@ test(command, ' baz quz', 'baz', 'quz'); test(command, ''); const testEscapedCommand = async (t, expected, args) => { - const {escapedCommand: failEscapedCommand} = await t.throwsAsync(execa('fail', args)); - t.is(failEscapedCommand, `fail ${expected}`); + const {escapedCommand: failEscapedCommand} = await t.throwsAsync(execa('fail.js', args)); + t.is(failEscapedCommand, `fail.js ${expected}`); const {escapedCommand: failEscapedCommandSync} = t.throws(() => { - execa.sync('fail', args); + execaSync('fail.js', args); }); - t.is(failEscapedCommandSync, `fail ${expected}`); + t.is(failEscapedCommandSync, `fail.js ${expected}`); - const {escapedCommand} = await execa('noop', args); - t.is(escapedCommand, `noop ${expected}`); + const {escapedCommand} = await execa('noop.js', args); + t.is(escapedCommand, `noop.js ${expected}`); - const {escapedCommand: escapedCommandSync} = execa.sync('noop', args); - t.is(escapedCommandSync, `noop ${expected}`); + const {escapedCommand: escapedCommandSync} = execaSync('noop.js', args); + t.is(escapedCommandSync, `noop.js ${expected}`); }; testEscapedCommand.title = (message, expected) => `escapedCommand is: ${JSON.stringify(expected)}`; @@ -42,46 +44,46 @@ test(testEscapedCommand, '"\\"foo\\""', ['"foo"']); test(testEscapedCommand, '"*"', ['*']); test('allow commands with spaces and no array arguments', async t => { - const {stdout} = await execa('command with space'); + const {stdout} = await execa('command with space.js'); t.is(stdout, ''); }); test('allow commands with spaces and array arguments', async t => { - const {stdout} = await execa('command with space', ['foo', 'bar']); + const {stdout} = await execa('command with space.js', ['foo', 'bar']); t.is(stdout, 'foo\nbar'); }); -test('execa.command()', async t => { - const {stdout} = await execa.command('node test/fixtures/echo foo bar'); +test('execaCommand()', async t => { + const {stdout} = await execaCommand('node test/fixtures/echo.js foo bar'); t.is(stdout, 'foo\nbar'); }); -test('execa.command() ignores consecutive spaces', async t => { - const {stdout} = await execa.command('node test/fixtures/echo foo bar'); +test('execaCommand() ignores consecutive spaces', async t => { + const {stdout} = await execaCommand('node test/fixtures/echo.js foo bar'); t.is(stdout, 'foo\nbar'); }); -test('execa.command() allows escaping spaces in commands', async t => { - const {stdout} = await execa.command('command\\ with\\ space foo bar'); +test('execaCommand() allows escaping spaces in commands', async t => { + const {stdout} = await execaCommand('command\\ with\\ space.js foo bar'); t.is(stdout, 'foo\nbar'); }); -test('execa.command() allows escaping spaces in arguments', async t => { - const {stdout} = await execa.command('node test/fixtures/echo foo\\ bar'); +test('execaCommand() allows escaping spaces in arguments', async t => { + const {stdout} = await execaCommand('node test/fixtures/echo.js foo\\ bar'); t.is(stdout, 'foo bar'); }); -test('execa.command() escapes other whitespaces', async t => { - const {stdout} = await execa.command('node test/fixtures/echo foo\tbar'); +test('execaCommand() escapes other whitespaces', async t => { + const {stdout} = await execaCommand('node test/fixtures/echo.js foo\tbar'); t.is(stdout, 'foo\tbar'); }); -test('execa.command() trims', async t => { - const {stdout} = await execa.command(' node test/fixtures/echo foo bar '); +test('execaCommand() trims', async t => { + const {stdout} = await execaCommand(' node test/fixtures/echo.js foo bar '); t.is(stdout, 'foo\nbar'); }); -test('execa.command.sync()', t => { - const {stdout} = execa.commandSync('node test/fixtures/echo foo bar'); +test('execaCommandSync()', t => { + const {stdout} = execaCommandSync('node test/fixtures/echo.js foo bar'); t.is(stdout, 'foo\nbar'); }); diff --git a/test/error.js b/test/error.js index c375bc0763..a9c7746f86 100644 --- a/test/error.js +++ b/test/error.js @@ -1,24 +1,29 @@ -import path from 'path'; -import childProcess from 'child_process'; +import path from 'node:path'; +import process from 'node:process'; +import childProcess from 'node:child_process'; +import {fileURLToPath} from 'node:url'; +import {promisify} from 'node:util'; import test from 'ava'; -import execa from '..'; +import {execa, execaSync} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +const pExec = promisify(childProcess.exec); + +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; const TIMEOUT_REGEXP = /timed out after/; const getExitRegExp = exitMessage => new RegExp(`failed with exit code ${exitMessage}`); test('stdout/stderr/all available on errors', async t => { - const {stdout, stderr, all} = await t.throwsAsync(execa('exit', ['2'], {all: true}), {message: getExitRegExp('2')}); + const {stdout, stderr, all} = await t.throwsAsync(execa('exit.js', ['2'], {all: true}), {message: getExitRegExp('2')}); t.is(typeof stdout, 'string'); t.is(typeof stderr, 'string'); t.is(typeof all, 'string'); }); -const WRONG_COMMAND = process.platform === 'win32' ? - '\'wrong\' is not recognized as an internal or external command,\r\noperable program or batch file.' : - ''; +const WRONG_COMMAND = process.platform === 'win32' + ? '\'wrong\' is not recognized as an internal or external command,\r\noperable program or batch file.' + : ''; test('stdout/stderr/all on process errors', async t => { const {stdout, stderr, all} = await t.throwsAsync(execa('wrong command', {all: true})); @@ -29,7 +34,7 @@ test('stdout/stderr/all on process errors', async t => { test('stdout/stderr/all on process errors, in sync mode', t => { const {stdout, stderr, all} = t.throws(() => { - execa.sync('wrong command'); + execaSync('wrong command'); }); t.is(stdout, ''); t.is(stderr, WRONG_COMMAND); @@ -37,12 +42,12 @@ test('stdout/stderr/all on process errors, in sync mode', t => { }); test('exitCode is 0 on success', async t => { - const {exitCode} = await execa('noop', ['foo']); + const {exitCode} = await execa('noop.js', ['foo']); t.is(exitCode, 0); }); const testExitCode = async (t, number) => { - const {exitCode} = await t.throwsAsync(execa('exit', [`${number}`]), {message: getExitRegExp(number)}); + const {exitCode} = await t.throwsAsync(execa('exit.js', [`${number}`]), {message: getExitRegExp(number)}); t.is(exitCode, number); }; @@ -51,44 +56,47 @@ test('exitCode is 3', testExitCode, 3); test('exitCode is 4', testExitCode, 4); test('error.message contains the command', async t => { - await t.throwsAsync(execa('exit', ['2', 'foo', 'bar']), {message: /exit 2 foo bar/}); + await t.throwsAsync(execa('exit.js', ['2', 'foo', 'bar']), {message: /exit.js 2 foo bar/}); }); test('error.message contains stdout/stderr if available', async t => { - const {message} = await t.throwsAsync(execa('echo-fail')); + const {message} = await t.throwsAsync(execa('echo-fail.js')); t.true(message.includes('stderr')); t.true(message.includes('stdout')); }); test('error.message does not contain stdout/stderr if not available', async t => { - const {message} = await t.throwsAsync(execa('echo-fail', {stdio: 'ignore'})); + const {message} = await t.throwsAsync(execa('echo-fail.js', {stdio: 'ignore'})); t.false(message.includes('stderr')); t.false(message.includes('stdout')); }); test('error.shortMessage does not contain stdout/stderr', async t => { - const {shortMessage} = await t.throwsAsync(execa('echo-fail')); + const {shortMessage} = await t.throwsAsync(execa('echo-fail.js')); t.false(shortMessage.includes('stderr')); t.false(shortMessage.includes('stdout')); }); test('Original error.message is kept', async t => { - const {originalMessage} = await t.throwsAsync(execa('noop', {cwd: 1})); - t.true(originalMessage.startsWith('The "options.cwd" property must be of type string. Received type number')); + const {originalMessage} = await t.throwsAsync(execa('noop.js', {cwd: 1})); + // On Node >=14.18.0, the error message is + // `The "options.cwd" property must be of type string or an instance of Buffer or URL. Received type number (1)` + t.true(originalMessage.startsWith('The "options.cwd" property must be of type string')); + t.true(originalMessage.includes('. Received type number')); }); test('failed is false on success', async t => { - const {failed} = await execa('noop', ['foo']); + const {failed} = await execa('noop.js', ['foo']); t.false(failed); }); test('failed is true on failure', async t => { - const {failed} = await t.throwsAsync(execa('exit', ['2'])); + const {failed} = await t.throwsAsync(execa('exit.js', ['2'])); t.true(failed); }); test('error.killed is true if process was killed directly', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.kill(); @@ -97,7 +105,7 @@ test('error.killed is true if process was killed directly', async t => { }); test('error.killed is false if process was killed indirectly', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); process.kill(subprocess.pid, 'SIGINT'); @@ -108,12 +116,12 @@ test('error.killed is false if process was killed indirectly', async t => { }); test('result.killed is false if not killed', async t => { - const {killed} = await execa('noop'); + const {killed} = await execa('noop.js'); t.false(killed); }); test('result.killed is false if not killed, in sync mode', t => { - const {killed} = execa.sync('noop'); + const {killed} = execaSync('noop.js'); t.false(killed); }); @@ -124,26 +132,26 @@ test('result.killed is false on process error', async t => { test('result.killed is false on process error, in sync mode', t => { const {killed} = t.throws(() => { - execa.sync('wrong command'); + execaSync('wrong command'); }); t.false(killed); }); if (process.platform === 'darwin') { - test.cb('sanity check: child_process.exec also has killed.false if killed indirectly', t => { - const {pid} = childProcess.exec('noop', error => { - t.truthy(error); - t.false(error.killed); - t.end(); - }); - - process.kill(pid, 'SIGINT'); + test('sanity check: child_process.exec also has killed.false if killed indirectly', async t => { + const promise = pExec('noop.js'); + + process.kill(promise.child.pid, 'SIGINT'); + + const error = await t.throwsAsync(promise); + t.truthy(error); + t.false(error.killed); }); } if (process.platform !== 'win32') { test('error.signal is SIGINT', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); process.kill(subprocess.pid, 'SIGINT'); @@ -152,7 +160,7 @@ if (process.platform !== 'win32') { }); test('error.signalDescription is defined', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); process.kill(subprocess.pid, 'SIGINT'); @@ -161,7 +169,7 @@ if (process.platform !== 'win32') { }); test('error.signal is SIGTERM', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); process.kill(subprocess.pid, 'SIGTERM'); @@ -170,12 +178,12 @@ if (process.platform !== 'win32') { }); test('custom error.signal', async t => { - const {signal} = await t.throwsAsync(execa('noop', {killSignal: 'SIGHUP', timeout: 1, message: TIMEOUT_REGEXP})); + const {signal} = await t.throwsAsync(execa('noop.js', {killSignal: 'SIGHUP', timeout: 1, message: TIMEOUT_REGEXP})); t.is(signal, 'SIGHUP'); }); test('exitCode is undefined on signal termination', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); process.kill(subprocess.pid); @@ -185,26 +193,26 @@ if (process.platform !== 'win32') { } test('result.signal is undefined for successful execution', async t => { - const {signal} = await execa('noop'); + const {signal} = await execa('noop.js'); t.is(signal, undefined); }); test('result.signal is undefined if process failed, but was not killed', async t => { - const {signal} = await t.throwsAsync(execa('exit', [2]), {message: getExitRegExp('2')}); + const {signal} = await t.throwsAsync(execa('exit.js', [2]), {message: getExitRegExp('2')}); t.is(signal, undefined); }); test('result.signalDescription is undefined for successful execution', async t => { - const {signalDescription} = await execa('noop'); + const {signalDescription} = await execa('noop.js'); t.is(signalDescription, undefined); }); test('error.code is undefined on success', async t => { - const {code} = await execa('noop'); + const {code} = await execa('noop.js'); t.is(code, undefined); }); test('error.code is defined on failure if applicable', async t => { - const {code} = await t.throwsAsync(execa('noop', {cwd: 1})); + const {code} = await t.throwsAsync(execa('noop.js', {cwd: 1})); t.is(code, 'ERR_INVALID_ARG_TYPE'); }); diff --git a/test/fixtures/command with space b/test/fixtures/command with space.js similarity index 64% rename from test/fixtures/command with space rename to test/fixtures/command with space.js index e9baf7ef67..828107d6f1 100755 --- a/test/fixtures/command with space +++ b/test/fixtures/command with space.js @@ -1,3 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + console.log(process.argv.slice(2).join('\n')); diff --git a/test/fixtures/delay b/test/fixtures/delay.js similarity index 65% rename from test/fixtures/delay rename to test/fixtures/delay.js index ed89fd8ddb..795190fc1e 100755 --- a/test/fixtures/delay +++ b/test/fixtures/delay.js @@ -1,4 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; setTimeout(() => {}, Number(process.argv[2])); diff --git a/test/fixtures/detach b/test/fixtures/detach deleted file mode 100755 index 80f8980097..0000000000 --- a/test/fixtures/detach +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -const execa = require('../..'); - -const subprocess = execa('node', ['./test/fixtures/forever'], {detached: true}); -console.log(subprocess.pid); -process.exit(); diff --git a/test/fixtures/detach.js b/test/fixtures/detach.js new file mode 100755 index 0000000000..b21d056a3a --- /dev/null +++ b/test/fixtures/detach.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import process from 'node:process'; +import {execa} from '../../index.js'; + +const subprocess = execa('node', ['./test/fixtures/forever.js'], {detached: true}); +console.log(subprocess.pid); +process.exit(0); diff --git a/test/fixtures/echo-fail b/test/fixtures/echo-fail.js similarity index 69% rename from test/fixtures/echo-fail rename to test/fixtures/echo-fail.js index 233f1d0c07..7ac2f13676 100755 --- a/test/fixtures/echo-fail +++ b/test/fixtures/echo-fail.js @@ -1,5 +1,6 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + console.log('stdout'); console.error('stderr'); process.exit(1); diff --git a/test/fixtures/echo b/test/fixtures/echo.js similarity index 64% rename from test/fixtures/echo rename to test/fixtures/echo.js index e9baf7ef67..828107d6f1 100755 --- a/test/fixtures/echo +++ b/test/fixtures/echo.js @@ -1,3 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + console.log(process.argv.slice(2).join('\n')); diff --git a/test/fixtures/environment b/test/fixtures/environment.js similarity index 68% rename from test/fixtures/environment rename to test/fixtures/environment.js index 280faea9cb..0a4698ae59 100755 --- a/test/fixtures/environment +++ b/test/fixtures/environment.js @@ -1,4 +1,5 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + console.log(process.env.FOO); console.log(process.env.BAR); diff --git a/test/fixtures/exit b/test/fixtures/exit.js similarity index 61% rename from test/fixtures/exit rename to test/fixtures/exit.js index 0650d1108b..76f1f91666 100755 --- a/test/fixtures/exit +++ b/test/fixtures/exit.js @@ -1,3 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + process.exit(Number(process.argv[2])); diff --git a/test/fixtures/fail b/test/fixtures/fail.js similarity index 51% rename from test/fixtures/fail rename to test/fixtures/fail.js index 02e87041b5..881d915444 100755 --- a/test/fixtures/fail +++ b/test/fixtures/fail.js @@ -1,4 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; process.exit(2); diff --git a/test/fixtures/forever b/test/fixtures/forever.js similarity index 75% rename from test/fixtures/forever rename to test/fixtures/forever.js index b64113fa86..406c861da1 100755 --- a/test/fixtures/forever +++ b/test/fixtures/forever.js @@ -1,4 +1,2 @@ #!/usr/bin/env node -'use strict'; - setTimeout(() => {}, 1e8); diff --git a/test/fixtures/max-buffer b/test/fixtures/max-buffer.js similarity index 81% rename from test/fixtures/max-buffer rename to test/fixtures/max-buffer.js index d1cfea941c..4d28851820 100755 --- a/test/fixtures/max-buffer +++ b/test/fixtures/max-buffer.js @@ -1,5 +1,6 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + const output = process.argv[2] || 'stdout'; const bytes = Number(process.argv[3] || 1e7); diff --git a/test/fixtures/no-killable b/test/fixtures/no-killable deleted file mode 100755 index 3ce9ba121f..0000000000 --- a/test/fixtures/no-killable +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -process.on('SIGTERM', () => { - console.log('Received SIGTERM, but we ignore it'); -}); - -process.send(''); - -setInterval(() => { - // Run forever -}, 20000); diff --git a/test/fixtures/no-killable.js b/test/fixtures/no-killable.js new file mode 100755 index 0000000000..b27edf71d3 --- /dev/null +++ b/test/fixtures/no-killable.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import process from 'node:process'; + +process.on('SIGTERM', () => { + console.log('Received SIGTERM, but we ignore it'); +}); + +process.send(''); + +setInterval(() => { + // Run forever +}, 20_000); diff --git a/test/fixtures/non-executable b/test/fixtures/non-executable.js similarity index 58% rename from test/fixtures/non-executable rename to test/fixtures/non-executable.js index 0e12b08794..908ba8417a 100644 --- a/test/fixtures/non-executable +++ b/test/fixtures/non-executable.js @@ -1,2 +1 @@ #!/usr/bin/env node -'use strict'; diff --git a/test/fixtures/noop-132 b/test/fixtures/noop-132.js similarity index 78% rename from test/fixtures/noop-132 rename to test/fixtures/noop-132.js index 277e817db6..77dfb77d66 100755 --- a/test/fixtures/noop-132 +++ b/test/fixtures/noop-132.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; process.stdout.write('1'); process.stderr.write('3'); diff --git a/test/fixtures/noop-err b/test/fixtures/noop-err.js similarity index 58% rename from test/fixtures/noop-err rename to test/fixtures/noop-err.js index 8f6266d78f..505fb97fc2 100755 --- a/test/fixtures/noop-err +++ b/test/fixtures/noop-err.js @@ -1,3 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + console.error(process.argv[2]); diff --git a/test/fixtures/noop-throw b/test/fixtures/noop-throw.js similarity index 65% rename from test/fixtures/noop-throw rename to test/fixtures/noop-throw.js index 521e70abf8..2a42f98f33 100755 --- a/test/fixtures/noop-throw +++ b/test/fixtures/noop-throw.js @@ -1,4 +1,5 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + console.error(process.argv[2]); process.exit(2); diff --git a/test/fixtures/noop b/test/fixtures/noop.js similarity index 57% rename from test/fixtures/noop rename to test/fixtures/noop.js index 258e2bd30a..d55e447c8b 100755 --- a/test/fixtures/noop +++ b/test/fixtures/noop.js @@ -1,3 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + console.log(process.argv[2]); diff --git a/test/fixtures/send b/test/fixtures/send.js similarity index 74% rename from test/fixtures/send rename to test/fixtures/send.js index c215df6f74..ff52052af8 100755 --- a/test/fixtures/send +++ b/test/fixtures/send.js @@ -1,5 +1,6 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + process.on('message', message => { if (message === 'ping') { process.send('pong'); @@ -7,3 +8,5 @@ process.on('message', message => { throw new Error('Receive wrong message'); } }); + +process.send(''); diff --git a/test/fixtures/stdin b/test/fixtures/stdin.js similarity index 60% rename from test/fixtures/stdin rename to test/fixtures/stdin.js index ddb0796f0c..4a10065ba7 100755 --- a/test/fixtures/stdin +++ b/test/fixtures/stdin.js @@ -1,3 +1,4 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; + process.stdin.pipe(process.stdout); diff --git a/test/fixtures/sub-process b/test/fixtures/sub-process deleted file mode 100755 index 6920e79348..0000000000 --- a/test/fixtures/sub-process +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env node -'use strict'; -const execa = require('../..'); - -const cleanup = process.argv[2] === 'true'; -const detached = process.argv[3] === 'true'; -const subprocess = execa('node', ['./test/fixtures/forever'], {cleanup, detached}); -process.send(subprocess.pid); diff --git a/test/fixtures/sub-process-exit b/test/fixtures/sub-process-exit.js similarity index 61% rename from test/fixtures/sub-process-exit rename to test/fixtures/sub-process-exit.js index ea05f0e2d4..ae21147ef1 100755 --- a/test/fixtures/sub-process-exit +++ b/test/fixtures/sub-process-exit.js @@ -1,13 +1,13 @@ #!/usr/bin/env node -'use strict'; -const execa = require('../..'); +import process from 'node:process'; +import {execa} from '../../index.js'; const cleanup = process.argv[2] === 'true'; const detached = process.argv[3] === 'true'; const runChild = async () => { try { - await execa('node', ['./test/fixtures/noop'], {cleanup, detached}); + await execa('node', ['./test/fixtures/noop.js'], {cleanup, detached}); } catch (error) { console.error(error); process.exit(1); diff --git a/test/fixtures/sub-process.js b/test/fixtures/sub-process.js new file mode 100755 index 0000000000..a1b806db05 --- /dev/null +++ b/test/fixtures/sub-process.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import process from 'node:process'; +import {execa} from '../../index.js'; + +const cleanup = process.argv[2] === 'true'; +const detached = process.argv[3] === 'true'; +const subprocess = execa('node', ['./test/fixtures/forever.js'], {cleanup, detached}); +process.send(subprocess.pid); diff --git a/test/helpers/override-promise.js b/test/helpers/override-promise.js new file mode 100644 index 0000000000..f69bb8ae5d --- /dev/null +++ b/test/helpers/override-promise.js @@ -0,0 +1,11 @@ +// Can't use `test.before`, because `ava` needs `Promise`. +const nativePromise = Promise; +global.Promise = class BrokenPromise { + then() { + throw new Error('error'); + } +}; + +export function restorePromise() { + global.Promise = nativePromise; +} diff --git a/test/kill.js b/test/kill.js index 08bf8336ce..7cf7c09eff 100644 --- a/test/kill.js +++ b/test/kill.js @@ -1,15 +1,17 @@ -import path from 'path'; +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; import test from 'ava'; import pEvent from 'p-event'; import isRunning from 'is-running'; -import execa from '..'; +import {execa, execaSync} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; const TIMEOUT_REGEXP = /timed out after/; test('kill("SIGKILL") should terminate cleanly', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + const subprocess = execa('node', ['./test/fixtures/no-killable.js'], {stdio: ['ipc']}); await pEvent(subprocess, 'message'); subprocess.kill('SIGKILL'); @@ -22,7 +24,7 @@ test('kill("SIGKILL") should terminate cleanly', async t => { // Therefore, this feature and those tests do not make sense on Windows. if (process.platform !== 'win32') { test('`forceKillAfterTimeout: false` should not kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + const subprocess = execa('node', ['./test/fixtures/no-killable.js'], {stdio: ['ipc']}); await pEvent(subprocess, 'message'); subprocess.kill('SIGTERM', {forceKillAfterTimeout: false}); @@ -32,7 +34,7 @@ if (process.platform !== 'win32') { }); test('`forceKillAfterTimeout: number` should kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + const subprocess = execa('node', ['./test/fixtures/no-killable.js'], {stdio: ['ipc']}); await pEvent(subprocess, 'message'); subprocess.kill('SIGTERM', {forceKillAfterTimeout: 50}); @@ -42,7 +44,7 @@ if (process.platform !== 'win32') { }); test('`forceKillAfterTimeout: true` should kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + const subprocess = execa('node', ['./test/fixtures/no-killable.js'], {stdio: ['ipc']}); await pEvent(subprocess, 'message'); subprocess.kill('SIGTERM', {forceKillAfterTimeout: true}); @@ -52,7 +54,7 @@ if (process.platform !== 'win32') { }); test('kill() with no arguments should kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + const subprocess = execa('node', ['./test/fixtures/no-killable.js'], {stdio: ['ipc']}); await pEvent(subprocess, 'message'); subprocess.kill(); @@ -63,38 +65,38 @@ if (process.platform !== 'win32') { test('`forceKillAfterTimeout` should not be NaN', t => { t.throws(() => { - execa('noop').kill('SIGTERM', {forceKillAfterTimeout: Number.NaN}); + execa('noop.js').kill('SIGTERM', {forceKillAfterTimeout: Number.NaN}); }, {instanceOf: TypeError, message: /non-negative integer/}); }); test('`forceKillAfterTimeout` should not be negative', t => { t.throws(() => { - execa('noop').kill('SIGTERM', {forceKillAfterTimeout: -1}); + execa('noop.js').kill('SIGTERM', {forceKillAfterTimeout: -1}); }, {instanceOf: TypeError, message: /non-negative integer/}); }); } test('execa() returns a promise with kill()', t => { - const {kill} = execa('noop', ['foo']); + const {kill} = execa('noop.js', ['foo']); t.is(typeof kill, 'function'); }); test('timeout kills the process if it times out', async t => { - const {killed, timedOut} = await t.throwsAsync(execa('noop', {timeout: 1}), TIMEOUT_REGEXP); + const {killed, timedOut} = await t.throwsAsync(execa('noop.js', {timeout: 1}), {message: TIMEOUT_REGEXP}); t.false(killed); t.true(timedOut); }); test('timeout kills the process if it times out, in sync mode', async t => { const {killed, timedOut} = await t.throws(() => { - execa.sync('noop', {timeout: 1, message: TIMEOUT_REGEXP}); + execaSync('noop.js', {timeout: 1, message: TIMEOUT_REGEXP}); }); t.false(killed); t.true(timedOut); }); test('timeout does not kill the process if it does not time out', async t => { - const {timedOut} = await execa('delay', ['500'], {timeout: 1e8}); + const {timedOut} = await execa('delay.js', ['500'], {timeout: 1e8}); t.false(timedOut); }); @@ -102,34 +104,34 @@ const INVALID_TIMEOUT_REGEXP = /`timeout` option to be a non-negative integer/; test('timeout must not be negative', async t => { await t.throws(() => { - execa('noop', {timeout: -1}); - }, INVALID_TIMEOUT_REGEXP); + execa('noop.js', {timeout: -1}); + }, {message: INVALID_TIMEOUT_REGEXP}); }); test('timeout must be an integer', async t => { await t.throws(() => { - execa('noop', {timeout: false}); - }, INVALID_TIMEOUT_REGEXP); + execa('noop.js', {timeout: false}); + }, {message: INVALID_TIMEOUT_REGEXP}); }); test('timedOut is false if timeout is undefined', async t => { - const {timedOut} = await execa('noop'); + const {timedOut} = await execa('noop.js'); t.false(timedOut); }); test('timedOut is false if timeout is 0', async t => { - const {timedOut} = await execa('noop', {timeout: 0}); + const {timedOut} = await execa('noop.js', {timeout: 0}); t.false(timedOut); }); test('timedOut is false if timeout is undefined and exit code is 0 in sync mode', t => { - const {timedOut} = execa.sync('noop'); + const {timedOut} = execaSync('noop.js'); t.false(timedOut); }); // When child process exits before parent process const spawnAndExit = async (t, cleanup, detached) => { - await t.notThrowsAsync(execa('sub-process-exit', [cleanup, detached])); + await t.notThrowsAsync(execa('sub-process-exit.js', [cleanup, detached])); }; test('spawnAndExit', spawnAndExit, false, false); @@ -139,7 +141,7 @@ test('spawnAndExit cleanup detached', spawnAndExit, true, true); // When parent process exits before child process const spawnAndKill = async (t, [signal, cleanup, detached, isKilled]) => { - const subprocess = execa('sub-process', [cleanup, detached], {stdio: ['ignore', 'ignore', 'ignore', 'ipc']}); + const subprocess = execa('sub-process.js', [cleanup, detached], {stdio: ['ignore', 'ignore', 'ignore', 'ipc']}); const pid = await pEvent(subprocess, 'message'); t.true(Number.isInteger(pid)); @@ -178,7 +180,7 @@ test('removes exit handler on exit', async t => { // @todo this relies on `signal-exit` internals const emitter = process.__signal_exit_emitter__; - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); const listener = emitter.listeners('exit').pop(); await new Promise((resolve, reject) => { @@ -197,49 +199,49 @@ test('cancel method kills the subprocess', t => { }); test('result.isCanceled is false when spawned.cancel() isn\'t called (success)', async t => { - const {isCanceled} = await execa('noop'); + const {isCanceled} = await execa('noop.js'); t.false(isCanceled); }); test('result.isCanceled is false when spawned.cancel() isn\'t called (failure)', async t => { - const {isCanceled} = await t.throwsAsync(execa('fail')); + const {isCanceled} = await t.throwsAsync(execa('fail.js')); t.false(isCanceled); }); test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (success)', t => { - const {isCanceled} = execa.sync('noop'); + const {isCanceled} = execaSync('noop.js'); t.false(isCanceled); }); test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (failure)', t => { const {isCanceled} = t.throws(() => { - execa.sync('fail'); + execaSync('fail.js'); }); t.false(isCanceled); }); test('calling cancel method throws an error with message "Command was canceled"', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.cancel(); await t.throwsAsync(subprocess, {message: /Command was canceled/}); }); test('error.isCanceled is true when cancel method is used', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.cancel(); const {isCanceled} = await t.throwsAsync(subprocess); t.true(isCanceled); }); test('error.isCanceled is false when kill method is used', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.kill(); const {isCanceled} = await t.throwsAsync(subprocess); t.false(isCanceled); }); test('calling cancel method twice should show the same behaviour as calling it once', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.cancel(); subprocess.cancel(); const {isCanceled} = await t.throwsAsync(subprocess); @@ -248,14 +250,14 @@ test('calling cancel method twice should show the same behaviour as calling it o }); test('calling cancel method on a successfully completed process does not make result.isCanceled true', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); const {isCanceled} = await subprocess; subprocess.cancel(); t.false(isCanceled); }); test('calling cancel method on a process which has been killed does not make error.isCanceled true', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.kill(); const {isCanceled} = await t.throwsAsync(subprocess); t.false(isCanceled); diff --git a/test/node.js b/test/node.js index 51ca5576e3..acebef121d 100644 --- a/test/node.js +++ b/test/node.js @@ -1,16 +1,18 @@ -import path from 'path'; +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; import test from 'ava'; import pEvent from 'p-event'; -import execa from '..'; +import {execaNode} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; async function inspectMacro(t, input) { const originalArgv = process.execArgv; process.execArgv = [input, '-e']; try { - const subprocess = execa.node('console.log("foo")', { - reject: false + const subprocess = execaNode('console.log("foo")', { + reject: false, }); const {stdout, stderr} = await subprocess; @@ -23,32 +25,32 @@ async function inspectMacro(t, input) { } test('node()', async t => { - const {exitCode} = await execa.node('test/fixtures/noop'); + const {exitCode} = await execaNode('test/fixtures/noop.js'); t.is(exitCode, 0); }); test('node pipe stdout', async t => { - const {stdout} = await execa.node('test/fixtures/noop', ['foo'], { - stdout: 'pipe' + const {stdout} = await execaNode('test/fixtures/noop.js', ['foo'], { + stdout: 'pipe', }); t.is(stdout, 'foo'); }); test('node correctly use nodePath', async t => { - const {stdout} = await execa.node(process.platform === 'win32' ? 'hello.cmd' : 'hello.sh', { + const {stdout} = await execaNode(process.platform === 'win32' ? 'hello.cmd' : 'hello.sh', { stdout: 'pipe', nodePath: process.platform === 'win32' ? 'cmd.exe' : 'bash', - nodeOptions: process.platform === 'win32' ? ['/c'] : [] + nodeOptions: process.platform === 'win32' ? ['/c'] : [], }); t.is(stdout, 'Hello World'); }); test('node pass on nodeOptions', async t => { - const {stdout} = await execa.node('console.log("foo")', { + const {stdout} = await execaNode('console.log("foo")', { stdout: 'pipe', - nodeOptions: ['-e'] + nodeOptions: ['-e'], }); t.is(stdout, 'foo'); @@ -57,42 +59,44 @@ test('node pass on nodeOptions', async t => { test.serial( 'node removes --inspect from nodeOptions when defined by parent process', inspectMacro, - '--inspect' + '--inspect', ); test.serial( 'node removes --inspect=9222 from nodeOptions when defined by parent process', inspectMacro, - '--inspect=9222' + '--inspect=9222', ); test.serial( 'node removes --inspect-brk from nodeOptions when defined by parent process', inspectMacro, - '--inspect-brk' + '--inspect-brk', ); test.serial( 'node removes --inspect-brk=9222 from nodeOptions when defined by parent process', inspectMacro, - '--inspect-brk=9222' + '--inspect-brk=9222', ); test.serial( 'node should not remove --inspect when passed through nodeOptions', async t => { - const {stdout, stderr} = await execa.node('console.log("foo")', { + const {stdout, stderr} = await execaNode('console.log("foo")', { reject: false, - nodeOptions: ['--inspect', '-e'] + nodeOptions: ['--inspect', '-e'], }); t.is(stdout, 'foo'); t.true(stderr.includes('Debugger listening')); - } + }, ); test('node\'s forked script has a communication channel', async t => { - const subprocess = execa.node('test/fixtures/send'); + const subprocess = execaNode('test/fixtures/send.js'); + await pEvent(subprocess, 'message'); + subprocess.send('ping'); const message = await pEvent(subprocess, 'message'); diff --git a/test/override-promise.js b/test/override-promise.js index 30c2465c55..4c200e37f4 100644 --- a/test/override-promise.js +++ b/test/override-promise.js @@ -1,20 +1,18 @@ -import path from 'path'; +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; import test from 'ava'; +// The helper module overrides Promise on import so has to be imported before `execa`. +// Can't use top-level await (TLA) + `import(…)` since Node.js 12 doesn't support TLA. +import {restorePromise} from './helpers/override-promise.js'; +// eslint-disable-next-line import/order +import {execa} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +restorePromise(); -// Can't use `test.before`, because `ava` needs `Promise`. -// Can't use `import(…)` either, because `execa` is not an ES Module. -const nativePromise = Promise; -global.Promise = class BrokenPromise { - then() { - throw new Error('error'); - } -}; -const execa = require('..'); -global.Promise = nativePromise; +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; test('should work with third-party Promise', async t => { - const {stdout} = await execa('noop', ['foo']); + const {stdout} = await execa('noop.js', ['foo']); t.is(stdout, 'foo'); }); diff --git a/test/promise.js b/test/promise.js index db9e7a4e4c..9f52892e7c 100644 --- a/test/promise.js +++ b/test/promise.js @@ -1,12 +1,13 @@ -import path from 'path'; +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; import test from 'ava'; -import execa from '..'; +import {execa} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; test('promise methods are not enumerable', t => { - const descriptors = Object.getOwnPropertyDescriptors(execa('noop')); - // eslint-disable-next-line promise/prefer-await-to-then + const descriptors = Object.getOwnPropertyDescriptors(execa('noop.js')); t.false(descriptors.then.enumerable); t.false(descriptors.catch.enumerable); t.false(descriptors.finally.enumerable); @@ -14,7 +15,7 @@ test('promise methods are not enumerable', t => { test('finally function is executed on success', async t => { let isCalled = false; - const {stdout} = await execa('noop', ['foo']).finally(() => { + const {stdout} = await execa('noop.js', ['foo']).finally(() => { isCalled = true; }); t.is(isCalled, true); @@ -23,7 +24,7 @@ test('finally function is executed on success', async t => { test('finally function is executed on failure', async t => { let isError = false; - const {stdout, stderr} = await t.throwsAsync(execa('exit', ['2']).finally(() => { + const {stdout, stderr} = await t.throwsAsync(execa('exit.js', ['2']).finally(() => { isError = true; })); t.is(isError, true); @@ -32,14 +33,14 @@ test('finally function is executed on failure', async t => { }); test('throw in finally function bubbles up on success', async t => { - const {message} = await t.throwsAsync(execa('noop', ['foo']).finally(() => { + const {message} = await t.throwsAsync(execa('noop.js', ['foo']).finally(() => { throw new Error('called'); })); t.is(message, 'called'); }); test('throw in finally bubbles up on error', async t => { - const {message} = await t.throwsAsync(execa('exit', ['2']).finally(() => { + const {message} = await t.throwsAsync(execa('exit.js', ['2']).finally(() => { throw new Error('called'); })); t.is(message, 'called'); diff --git a/test/stdio.js b/test/stdio.js index 3f849e78b0..9d9e5547ee 100644 --- a/test/stdio.js +++ b/test/stdio.js @@ -1,12 +1,12 @@ -import {inspect} from 'util'; +import {inspect} from 'node:util'; import test from 'ava'; -import normalizeStdio from '../lib/stdio'; +import {normalizeStdio, normalizeStdioNode} from '../lib/stdio.js'; const macro = (t, input, expected, func) => { if (expected instanceof Error) { t.throws(() => { normalizeStdio(input); - }, expected.message); + }, {message: expected.message}); return; } @@ -47,8 +47,8 @@ test(stdioMacro, {stdin: 'inherit', stdio: ['pipe']}, new Error('It\'s not possi test(stdioMacro, {stdin: 'inherit', stdio: [undefined, 'pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`')); test(stdioMacro, {stdin: 0, stdio: 'pipe'}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`')); -const forkMacro = (...args) => macro(...args, normalizeStdio.node); -forkMacro.title = macroTitle('execa.fork()'); +const forkMacro = (...args) => macro(...args, normalizeStdioNode); +forkMacro.title = macroTitle('execaNode()'); test(forkMacro, undefined, [undefined, undefined, undefined, 'ipc']); test(forkMacro, {stdio: 'ignore'}, ['ignore', 'ignore', 'ignore', 'ipc']); diff --git a/test/stream.js b/test/stream.js index 08cc53958c..7788ae99dc 100644 --- a/test/stream.js +++ b/test/stream.js @@ -1,62 +1,65 @@ -import path from 'path'; -import fs from 'fs'; -import Stream from 'stream'; +import {Buffer} from 'node:buffer'; +import path from 'node:path'; +import process from 'node:process'; +import fs from 'node:fs'; +import {fileURLToPath} from 'node:url'; +import Stream from 'node:stream'; import test from 'ava'; import getStream from 'get-stream'; import tempfile from 'tempfile'; -import execa from '..'; +import {execa, execaSync} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; test('buffer', async t => { - const {stdout} = await execa('noop', ['foo'], {encoding: null}); + const {stdout} = await execa('noop.js', ['foo'], {encoding: null}); t.true(Buffer.isBuffer(stdout)); t.is(stdout.toString(), 'foo'); }); test('pass `stdout` to a file descriptor', async t => { const file = tempfile('.txt'); - await execa('test/fixtures/noop', ['foo bar'], {stdout: fs.openSync(file, 'w')}); + await execa('test/fixtures/noop.js', ['foo bar'], {stdout: fs.openSync(file, 'w')}); t.is(fs.readFileSync(file, 'utf8'), 'foo bar\n'); }); test('pass `stderr` to a file descriptor', async t => { const file = tempfile('.txt'); - await execa('test/fixtures/noop-err', ['foo bar'], {stderr: fs.openSync(file, 'w')}); + await execa('test/fixtures/noop-err.js', ['foo bar'], {stderr: fs.openSync(file, 'w')}); t.is(fs.readFileSync(file, 'utf8'), 'foo bar\n'); }); test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => { - const {all} = await execa('noop-132', {all: true}); + const {all} = await execa('noop-132.js', {all: true}); t.is(all, '132'); }); test('result.all is undefined unless opts.all is true', async t => { - const {all} = await execa('noop'); + const {all} = await execa('noop.js'); t.is(all, undefined); }); test('stdout/stderr/all are undefined if ignored', async t => { - const {stdout, stderr, all} = await execa('noop', {stdio: 'ignore', all: true}); + const {stdout, stderr, all} = await execa('noop.js', {stdio: 'ignore', all: true}); t.is(stdout, undefined); t.is(stderr, undefined); t.is(all, undefined); }); test('stdout/stderr/all are undefined if ignored in sync mode', t => { - const {stdout, stderr, all} = execa.sync('noop', {stdio: 'ignore', all: true}); + const {stdout, stderr, all} = execaSync('noop.js', {stdio: 'ignore', all: true}); t.is(stdout, undefined); t.is(stderr, undefined); t.is(all, undefined); }); test('input option can be a String', async t => { - const {stdout} = await execa('stdin', {input: 'foobar'}); + const {stdout} = await execa('stdin.js', {input: 'foobar'}); t.is(stdout, 'foobar'); }); test('input option can be a Buffer', async t => { - const {stdout} = await execa('stdin', {input: 'testing12'}); + const {stdout} = await execa('stdin.js', {input: 'testing12'}); t.is(stdout, 'testing12'); }); @@ -64,30 +67,30 @@ test('input can be a Stream', async t => { const stream = new Stream.PassThrough(); stream.write('howdy'); stream.end(); - const {stdout} = await execa('stdin', {input: stream}); + const {stdout} = await execa('stdin.js', {input: stream}); t.is(stdout, 'howdy'); }); test('you can write to child.stdin', async t => { - const subprocess = execa('stdin'); + const subprocess = execa('stdin.js'); subprocess.stdin.end('unicorns'); t.is((await subprocess).stdout, 'unicorns'); }); test('input option can be a String - sync', t => { - const {stdout} = execa.sync('stdin', {input: 'foobar'}); + const {stdout} = execaSync('stdin.js', {input: 'foobar'}); t.is(stdout, 'foobar'); }); test('input option can be a Buffer - sync', t => { - const {stdout} = execa.sync('stdin', {input: Buffer.from('testing12', 'utf8')}); + const {stdout} = execaSync('stdin.js', {input: Buffer.from('testing12', 'utf8')}); t.is(stdout, 'testing12'); }); test('opts.stdout:ignore - stdout will not collect data', async t => { - const {stdout} = await execa('stdin', { + const {stdout} = await execa('stdin.js', { input: 'hello', - stdio: [undefined, 'ignore', undefined] + stdio: [undefined, 'ignore', undefined], }); t.is(stdout, undefined); }); @@ -95,31 +98,31 @@ test('opts.stdout:ignore - stdout will not collect data', async t => { test('helpful error trying to provide an input stream in sync mode', t => { t.throws( () => { - execa.sync('stdin', {input: new Stream.PassThrough()}); + execaSync('stdin.js', {input: new Stream.PassThrough()}); }, - /The `input` option cannot be a stream in sync mode/ + {message: /The `input` option cannot be a stream in sync mode/}, ); }); test('maxBuffer affects stdout', async t => { - await t.notThrowsAsync(execa('max-buffer', ['stdout', '10'], {maxBuffer: 10})); - const {stdout, all} = await t.throwsAsync(execa('max-buffer', ['stdout', '11'], {maxBuffer: 10, all: true}), /max-buffer stdout/); + await t.notThrowsAsync(execa('max-buffer.js', ['stdout', '10'], {maxBuffer: 10})); + const {stdout, all} = await t.throwsAsync(execa('max-buffer.js', ['stdout', '11'], {maxBuffer: 10, all: true}), {message: /max-buffer.js stdout/}); t.is(stdout, '.'.repeat(10)); t.is(all, '.'.repeat(10)); }); test('maxBuffer affects stderr', async t => { - await t.notThrowsAsync(execa('max-buffer', ['stderr', '10'], {maxBuffer: 10})); - const {stderr, all} = await t.throwsAsync(execa('max-buffer', ['stderr', '11'], {maxBuffer: 10, all: true}), /max-buffer stderr/); + await t.notThrowsAsync(execa('max-buffer.js', ['stderr', '10'], {maxBuffer: 10})); + const {stderr, all} = await t.throwsAsync(execa('max-buffer.js', ['stderr', '11'], {maxBuffer: 10, all: true}), {message: /max-buffer.js stderr/}); t.is(stderr, '.'.repeat(10)); t.is(all, '.'.repeat(10)); }); test('do not buffer stdout when `buffer` set to `false`', async t => { - const promise = execa('max-buffer', ['stdout', '10'], {buffer: false}); + const promise = execa('max-buffer.js', ['stdout', '10'], {buffer: false}); const [result, stdout] = await Promise.all([ promise, - getStream(promise.stdout) + getStream(promise.stdout), ]); t.is(result.stdout, undefined); @@ -127,10 +130,10 @@ test('do not buffer stdout when `buffer` set to `false`', async t => { }); test('do not buffer stderr when `buffer` set to `false`', async t => { - const promise = execa('max-buffer', ['stderr', '10'], {buffer: false}); + const promise = execa('max-buffer.js', ['stderr', '10'], {buffer: false}); const [result, stderr] = await Promise.all([ promise, - getStream(promise.stderr) + getStream(promise.stderr), ]); t.is(result.stderr, undefined); @@ -138,44 +141,44 @@ test('do not buffer stderr when `buffer` set to `false`', async t => { }); test('do not buffer when streaming', async t => { - const {stdout} = execa('max-buffer', ['stdout', '21'], {maxBuffer: 10}); + const {stdout} = execa('max-buffer.js', ['stdout', '21'], {maxBuffer: 10}); const result = await getStream(stdout); t.is(result, '....................\n'); }); test('buffer: false > promise resolves', async t => { - await t.notThrowsAsync(execa('noop', {buffer: false})); + await t.notThrowsAsync(execa('noop.js', {buffer: false})); }); test('buffer: false > promise resolves when output is big but is not pipable', async t => { - await t.notThrowsAsync(execa('max-buffer', {buffer: false, stdout: 'ignore'})); + await t.notThrowsAsync(execa('max-buffer.js', {buffer: false, stdout: 'ignore'})); }); test('buffer: false > promise resolves when output is big and is read', async t => { - const subprocess = execa('max-buffer', {buffer: false}); + const subprocess = execa('max-buffer.js', {buffer: false}); subprocess.stdout.resume(); subprocess.stderr.resume(); await t.notThrowsAsync(subprocess); }); test('buffer: false > promise resolves when output is big and "all" is used and is read', async t => { - const subprocess = execa('max-buffer', {buffer: false, all: true}); + const subprocess = execa('max-buffer.js', {buffer: false, all: true}); subprocess.all.resume(); await t.notThrowsAsync(subprocess); }); test('buffer: false > promise rejects when process returns non-zero', async t => { - const subprocess = execa('fail', {buffer: false}); + const subprocess = execa('fail.js', {buffer: false}); const {exitCode} = await t.throwsAsync(subprocess); t.is(exitCode, 2); }); test('can use all: true with stdout: ignore', async t => { - await t.notThrowsAsync(execa('max-buffer', {buffer: false, stdout: 'ignore', all: true})); + await t.notThrowsAsync(execa('max-buffer.js', {buffer: false, stdout: 'ignore', all: true})); }); test('can use all: true with stderr: ignore', async t => { - await t.notThrowsAsync(execa('max-buffer', ['stderr'], {buffer: false, stderr: 'ignore', all: true})); + await t.notThrowsAsync(execa('max-buffer.js', ['stderr'], {buffer: false, stderr: 'ignore', all: true})); }); const BUFFER_TIMEOUT = 1e3; @@ -183,12 +186,12 @@ const BUFFER_TIMEOUT = 1e3; // On Unix (not Windows), a process won't exit if stdout has not been read. if (process.platform !== 'win32') { test.serial('buffer: false > promise does not resolve when output is big and is not read', async t => { - const {timedOut} = await t.throwsAsync(execa('max-buffer', {buffer: false, timeout: BUFFER_TIMEOUT})); + const {timedOut} = await t.throwsAsync(execa('max-buffer.js', {buffer: false, timeout: BUFFER_TIMEOUT})); t.true(timedOut); }); test.serial('buffer: false > promise does not resolve when output is big and "all" is used but not read', async t => { - const subprocess = execa('max-buffer', {buffer: false, all: true, timeout: BUFFER_TIMEOUT}); + const subprocess = execa('max-buffer.js', {buffer: false, all: true, timeout: BUFFER_TIMEOUT}); subprocess.stdout.resume(); subprocess.stderr.resume(); const {timedOut} = await t.throwsAsync(subprocess); diff --git a/test/test.js b/test/test.js index ba59d7c37d..23449f52b8 100644 --- a/test/test.js +++ b/test/test.js @@ -1,16 +1,18 @@ -import path from 'path'; +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; import test from 'ava'; import isRunning from 'is-running'; import getNode from 'get-node'; -import execa from '..'; +import {execa, execaSync} from '../index.js'; -process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; +process.env.PATH = fileURLToPath(new URL('./fixtures', import.meta.url)) + path.delimiter + process.env.PATH; process.env.FOO = 'foo'; const ENOENT_REGEXP = process.platform === 'win32' ? /failed with exit code 1/ : /spawn.* ENOENT/; test('execa()', async t => { - const {stdout} = await execa('noop', ['foo']); + const {stdout} = await execa('noop.js', ['foo']); t.is(stdout, 'foo'); }); @@ -26,50 +28,50 @@ if (process.platform === 'win32') { }); } -test('execa.sync()', t => { - const {stdout} = execa.sync('noop', ['foo']); +test('execaSync()', t => { + const {stdout} = execaSync('noop.js', ['foo']); t.is(stdout, 'foo'); }); -test('execa.sync() throws error if written to stderr', t => { +test('execaSync() throws error if written to stderr', t => { t.throws(() => { - execa.sync('foo'); - }, ENOENT_REGEXP); + execaSync('foo'); + }, {message: ENOENT_REGEXP}); }); test('skip throwing when using reject option', async t => { - const {exitCode} = await execa('fail', {reject: false}); + const {exitCode} = await execa('fail.js', {reject: false}); t.is(exitCode, 2); }); test('skip throwing when using reject option in sync mode', t => { - const {exitCode} = execa.sync('fail', {reject: false}); + const {exitCode} = execaSync('fail.js', {reject: false}); t.is(exitCode, 2); }); test('stripFinalNewline: true', async t => { - const {stdout} = await execa('noop', ['foo']); + const {stdout} = await execa('noop.js', ['foo']); t.is(stdout, 'foo'); }); test('stripFinalNewline: false', async t => { - const {stdout} = await execa('noop', ['foo'], {stripFinalNewline: false}); + const {stdout} = await execa('noop.js', ['foo'], {stripFinalNewline: false}); t.is(stdout, 'foo\n'); }); test('stripFinalNewline on failure', async t => { - const {stderr} = await t.throwsAsync(execa('noop-throw', ['foo'], {stripFinalNewline: true})); + const {stderr} = await t.throwsAsync(execa('noop-throw.js', ['foo'], {stripFinalNewline: true})); t.is(stderr, 'foo'); }); test('stripFinalNewline in sync mode', t => { - const {stdout} = execa.sync('noop', ['foo'], {stripFinalNewline: true}); + const {stdout} = execaSync('noop.js', ['foo'], {stripFinalNewline: true}); t.is(stdout, 'foo'); }); test('stripFinalNewline in sync mode on failure', t => { const {stderr} = t.throws(() => { - execa.sync('noop-throw', ['foo'], {stripFinalNewline: true}); + execaSync('noop-throw.js', ['foo'], {stripFinalNewline: true}); }); t.is(stderr, 'foo'); }); @@ -79,11 +81,11 @@ test('preferLocal: true', async t => { }); test('preferLocal: false', async t => { - await t.throwsAsync(execa('ava', ['--version'], {preferLocal: false, env: {Path: '', PATH: ''}}), ENOENT_REGEXP); + await t.throwsAsync(execa('ava', ['--version'], {preferLocal: false, env: {Path: '', PATH: ''}}), {message: ENOENT_REGEXP}); }); test('preferLocal: undefined', async t => { - await t.throwsAsync(execa('ava', ['--version'], {env: {Path: '', PATH: ''}}), ENOENT_REGEXP); + await t.throwsAsync(execa('ava', ['--version'], {env: {Path: '', PATH: ''}}), {message: ENOENT_REGEXP}); }); test('localDir option', async t => { @@ -100,30 +102,30 @@ test('execPath option', async t => { }); test('stdin errors are handled', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.stdin.emit('error', new Error('test')); - await t.throwsAsync(subprocess, /test/); + await t.throwsAsync(subprocess, {message: /test/}); }); test('child process errors are handled', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.emit('error', new Error('test')); - await t.throwsAsync(subprocess, /test/); + await t.throwsAsync(subprocess, {message: /test/}); }); test('child process errors rejects promise right away', async t => { - const subprocess = execa('noop'); + const subprocess = execa('noop.js'); subprocess.emit('error', new Error('test')); - await t.throwsAsync(subprocess, /test/); + await t.throwsAsync(subprocess, {message: /test/}); }); test('execa() returns a promise with pid', t => { - const {pid} = execa('noop', ['foo']); + const {pid} = execa('noop.js', ['foo']); t.is(typeof pid, 'number'); }); test('child_process.spawn() propagated errors have correct shape', t => { - const subprocess = execa('noop', {uid: -1}); + const subprocess = execa('noop.js', {uid: -1}); t.notThrows(() => { subprocess.catch(() => {}); subprocess.unref(); @@ -132,39 +134,39 @@ test('child_process.spawn() propagated errors have correct shape', t => { }); test('child_process.spawn() errors are propagated', async t => { - const {failed} = await t.throwsAsync(execa('noop', {uid: -1})); + const {failed} = await t.throwsAsync(execa('noop.js', {uid: -1})); t.true(failed); }); test('child_process.spawnSync() errors are propagated with a correct shape', t => { const {failed} = t.throws(() => { - execa.sync('noop', {timeout: -1}); + execaSync('noop.js', {timeout: -1}); }); t.true(failed); }); test('do not try to consume streams twice', async t => { - const subprocess = execa('noop', ['foo']); + const subprocess = execa('noop.js', ['foo']); t.is((await subprocess).stdout, 'foo'); t.is((await subprocess).stdout, 'foo'); }); test('use relative path with \'..\' chars', async t => { - const pathViaParentDir = path.join('..', path.basename(path.dirname(__dirname)), 'test', 'fixtures', 'noop'); + const pathViaParentDir = path.join('..', path.basename(fileURLToPath(new URL('..', import.meta.url))), 'test', 'fixtures', 'noop.js'); const {stdout} = await execa(pathViaParentDir, ['foo']); t.is(stdout, 'foo'); }); if (process.platform !== 'win32') { test('execa() rejects if running non-executable', async t => { - const subprocess = execa('non-executable'); + const subprocess = execa('non-executable.js'); await t.throwsAsync(subprocess); }); test('execa() rejects with correct error and doesn\'t throw if running non-executable with input', async t => { // On Node <12.6.0, `EACCESS` is emitted on `childProcess`. // On Node >=12.6.0, `EPIPE` is emitted on `childProcess.stdin`. - await t.throwsAsync(execa('non-executable', {input: 'Hey!'}), /EACCES|EPIPE/); + await t.throwsAsync(execa('non-executable.js', {input: 'Hey!'}), {message: /EACCES|EPIPE/}); }); } @@ -182,28 +184,28 @@ if (process.platform !== 'win32') { } test('use environment variables by default', async t => { - const {stdout} = await execa('environment'); + const {stdout} = await execa('environment.js'); t.deepEqual(stdout.split('\n'), ['foo', 'undefined']); }); test('extend environment variables by default', async t => { - const {stdout} = await execa('environment', [], {env: {BAR: 'bar'}}); + const {stdout} = await execa('environment.js', [], {env: {BAR: 'bar'}}); t.deepEqual(stdout.split('\n'), ['foo', 'bar']); }); test('do not extend environment with `extendEnv: false`', async t => { - const {stdout} = await execa('environment', [], {env: {BAR: 'bar', PATH: process.env.PATH}, extendEnv: false}); + const {stdout} = await execa('environment.js', [], {env: {BAR: 'bar', PATH: process.env.PATH}, extendEnv: false}); t.deepEqual(stdout.split('\n'), ['undefined', 'bar']); }); test('can use `options.shell: true`', async t => { - const {stdout} = await execa('node test/fixtures/noop foo', {shell: true}); + const {stdout} = await execa('node test/fixtures/noop.js foo', {shell: true}); t.is(stdout, 'foo'); }); test('can use `options.shell: string`', async t => { const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash'; - const {stdout} = await execa('node test/fixtures/noop foo', {shell}); + const {stdout} = await execa('node test/fixtures/noop.js foo', {shell}); t.is(stdout, 'foo'); }); @@ -216,7 +218,7 @@ test('use extend environment with `extendEnv: true` and `shell: true`', async t }); test('detach child process', async t => { - const {stdout} = await execa('detach'); + const {stdout} = await execa('detach.js'); const pid = Number(stdout); t.true(Number.isInteger(pid)); t.true(isRunning(pid));