nano-spawn-compat 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/license +9 -0
- package/package.json +72 -0
- package/readme.md +269 -0
- package/source/context.js +14 -0
- package/source/index.d.ts +250 -0
- package/source/index.js +28 -0
- package/source/iterable.js +88 -0
- package/source/options.js +37 -0
- package/source/pipe.js +43 -0
- package/source/result.js +74 -0
- package/source/spawn.js +59 -0
- package/source/windows.js +71 -0
package/license
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nano-spawn-compat",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Tiny process execution for humans — a better child_process",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": "leonsilicon/nano-spawn-compat",
|
|
7
|
+
"funding": "https://github.com/sindresorhus/nano-spawn?sponsor=1",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Sindre Sorhus",
|
|
10
|
+
"url": "https://sindresorhus.com"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
"types": "./source/index.d.ts",
|
|
15
|
+
"default": "./source/index.js"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20.17"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "xo && c8 ava && npm run type",
|
|
23
|
+
"type": "tsd -t ./source/index.d.ts -f ./source/index.test-d.ts"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"source/**/*.js",
|
|
27
|
+
"source/**/*.d.ts"
|
|
28
|
+
],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"spawn",
|
|
31
|
+
"exec",
|
|
32
|
+
"child",
|
|
33
|
+
"process",
|
|
34
|
+
"subprocess",
|
|
35
|
+
"execute",
|
|
36
|
+
"fork",
|
|
37
|
+
"execfile",
|
|
38
|
+
"file",
|
|
39
|
+
"shell",
|
|
40
|
+
"bin",
|
|
41
|
+
"binary",
|
|
42
|
+
"binaries",
|
|
43
|
+
"npm",
|
|
44
|
+
"path",
|
|
45
|
+
"local",
|
|
46
|
+
"zx",
|
|
47
|
+
"execa"
|
|
48
|
+
],
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.5.4",
|
|
51
|
+
"ava": "^6.1.3",
|
|
52
|
+
"c8": "^10.1.2",
|
|
53
|
+
"get-node": "^15.0.1",
|
|
54
|
+
"log-process-errors": "^12.0.1",
|
|
55
|
+
"path-key": "^4.0.0",
|
|
56
|
+
"tempy": "^3.1.0",
|
|
57
|
+
"tsd": "^0.32.0",
|
|
58
|
+
"typescript": "^5.8.3",
|
|
59
|
+
"xo": "^0.60.0",
|
|
60
|
+
"yoctocolors": "^2.1.1"
|
|
61
|
+
},
|
|
62
|
+
"ava": {
|
|
63
|
+
"concurrency": 1,
|
|
64
|
+
"timeout": "240s",
|
|
65
|
+
"require": [
|
|
66
|
+
"./test/helpers/setup.js"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"strip-ansi": "^7.2.0"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
> This repo is a fork of [nano-spawn](https://github.com/sindresorhus/nano-spawn) by [@sindresorhus](https://github.com/sindresorhus) without depending on `node:readline` and `node:util` in order to support the [QuickJS-based LLRT runtime](https://github.com/awslabs/llrt).
|
|
2
|
+
>
|
|
3
|
+
> Why? Because this package is otherwise perfect for the LLRT runtime since it only relies on `child_process.spawn` which is currently the only [implemented export in `llrt_child_process`](https://github.com/awslabs/llrt/blob/36bd4a837d3fd85b5e3246906f23b1873c067503/modules/llrt_child_process/src/lib.rs#L580).
|
|
4
|
+
|
|
5
|
+
<h1 align="center" title="nano-spawn">
|
|
6
|
+
<img src="media/logo.jpg" alt="nano-spawn logo">
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
<!-- [](https://npmjs.com/nano-spawn) -->
|
|
11
|
+
<!--  -->
|
|
12
|
+
|
|
13
|
+
> Tiny process execution for humans — a better [`child_process`](https://nodejs.org/api/child_process.html)
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
No dependencies. Small package size:  [](https://packagephobia.com/result?p=nano-spawn)
|
|
18
|
+
|
|
19
|
+
Despite the small size, this is packed with some essential features:
|
|
20
|
+
- [Promise-based](#spawnfile-arguments-options-default-export) interface.
|
|
21
|
+
- [Iterate](#subprocesssymbolasynciterator) over the output lines.
|
|
22
|
+
- [Pipe](#subprocesspipefile-arguments-options) multiple subprocesses and retrieve [intermediate results](#resultpipedfrom).
|
|
23
|
+
- Execute [locally installed binaries](#optionspreferlocal) without `npx`.
|
|
24
|
+
- Improved [Windows support](#windows-support).
|
|
25
|
+
- Proper handling of [subprocess failures](#subprocesserror) and better error messages.
|
|
26
|
+
- Get [interleaved output](#resultoutput) from stdout and stderr similar to what is printed on the terminal.
|
|
27
|
+
- Strip [unnecessary newlines](#resultstdout).
|
|
28
|
+
- Pass strings as [`stdin` input](#optionsstdin-optionsstdout-optionsstderr) to the subprocess.
|
|
29
|
+
- Preserve the current [Node.js version and flags](#spawnfile-arguments-options-default-export).
|
|
30
|
+
- Simpler syntax to set [environment variables](#optionsenv) or [`stdin`/`stdout`/`stderr`](#optionsstdin-optionsstdout-optionsstderr).
|
|
31
|
+
- Compute the command [duration](#resultdurationms).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install nano-spawn
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Run commands
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import spawn from 'nano-spawn';
|
|
45
|
+
|
|
46
|
+
const result = await spawn('echo', ['🦄']);
|
|
47
|
+
|
|
48
|
+
console.log(result.output);
|
|
49
|
+
//=> '🦄'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Iterate over output lines
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
for await (const line of spawn('ls', ['--oneline'])) {
|
|
56
|
+
console.log(line);
|
|
57
|
+
}
|
|
58
|
+
//=> index.d.ts
|
|
59
|
+
//=> index.js
|
|
60
|
+
//=> …
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Pipe commands
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const result = await spawn('npm', ['run', 'build'])
|
|
67
|
+
.pipe('sort')
|
|
68
|
+
.pipe('head', ['-n', '2']);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
### spawn(file, arguments?, options?) <sup>default export</sup>
|
|
74
|
+
|
|
75
|
+
`file`: `string`\
|
|
76
|
+
`arguments`: `string[]`\
|
|
77
|
+
`options`: [`Options`](#options)\
|
|
78
|
+
_Returns_: [`Subprocess`](#subprocess)
|
|
79
|
+
|
|
80
|
+
Executes a command using `file ...arguments`.
|
|
81
|
+
|
|
82
|
+
This has the same syntax as [`child_process.spawn()`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options).
|
|
83
|
+
|
|
84
|
+
If `file` is `'node'`, the current Node.js version and [flags](https://nodejs.org/api/cli.html#options) are inherited.
|
|
85
|
+
|
|
86
|
+
#### Options
|
|
87
|
+
|
|
88
|
+
##### options.stdio, options.shell, options.timeout, options.signal, options.cwd, options.killSignal, options.serialization, options.detached, options.uid, options.gid, options.windowsVerbatimArguments, options.windowsHide, options.argv0
|
|
89
|
+
|
|
90
|
+
All [`child_process.spawn()` options](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) can be passed to [`spawn()`](#spawnfile-arguments-options-default-export).
|
|
91
|
+
|
|
92
|
+
##### options.env
|
|
93
|
+
|
|
94
|
+
_Type_: `object`\
|
|
95
|
+
_Default_: `{}`
|
|
96
|
+
|
|
97
|
+
Override specific [environment variables](https://en.wikipedia.org/wiki/Environment_variable). Other environment variables are inherited from the current process ([`process.env`](https://nodejs.org/api/process.html#processenv)).
|
|
98
|
+
|
|
99
|
+
##### options.preferLocal
|
|
100
|
+
|
|
101
|
+
_Type_: `boolean`\
|
|
102
|
+
_Default_: `false`
|
|
103
|
+
|
|
104
|
+
Allows executing binaries installed locally with `npm` (or `yarn`, etc.).
|
|
105
|
+
|
|
106
|
+
##### options.stdin, options.stdout, options.stderr
|
|
107
|
+
|
|
108
|
+
_Type_: `string | number | Stream | {string: string}`
|
|
109
|
+
|
|
110
|
+
Subprocess's standard [input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin))/[output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout))/[error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)).
|
|
111
|
+
|
|
112
|
+
[All values supported](https://nodejs.org/api/child_process.html#optionsstdio) by `node:child_process` are available. The most common ones are:
|
|
113
|
+
- `'pipe'` (default value): returns the output using [`result.stdout`](#resultstdout), [`result.stderr`](#resultstderr) and [`result.output`](#resultoutput).
|
|
114
|
+
- `'inherit'`: uses the current process's [input](https://nodejs.org/api/process.html#processstdin)/[output](https://nodejs.org/api/process.html#processstdout). This is useful when running in a terminal.
|
|
115
|
+
- `'ignore'`: discards the input/output.
|
|
116
|
+
- [`Stream`](https://nodejs.org/api/stream.html#stream): redirects the input/output from/to a stream. For example, [`fs.createReadStream()`](https://nodejs.org/api/fs.html#fscreatereadstreampath-options)/[`fs.createWriteStream()`](https://nodejs.org/api/fs.html#fscreatewritestreampath-options) can be used, once the stream's [`open`](https://nodejs.org/api/fs.html#event-open) event has been emitted.
|
|
117
|
+
- `{string: '...'}`: passes a string as input to `stdin`.
|
|
118
|
+
|
|
119
|
+
#### Subprocess
|
|
120
|
+
|
|
121
|
+
Subprocess started by [`spawn()`](#spawnfile-arguments-options-default-export).
|
|
122
|
+
|
|
123
|
+
##### await subprocess
|
|
124
|
+
|
|
125
|
+
_Returns_: [`Result`](#result)\
|
|
126
|
+
_Throws_: [`SubprocessError`](#subprocesserror)
|
|
127
|
+
|
|
128
|
+
A subprocess is a promise that is either resolved with a successful [`result` object](#result) or rejected with a [`subprocessError`](#error).
|
|
129
|
+
|
|
130
|
+
##### subprocess.stdout
|
|
131
|
+
|
|
132
|
+
_Returns_: `AsyncIterable<string>`\
|
|
133
|
+
_Throws_: [`SubprocessError`](#subprocesserror)
|
|
134
|
+
|
|
135
|
+
Iterates over each [`stdout`](#resultstdout) line, as soon as it is available.
|
|
136
|
+
|
|
137
|
+
The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess [fails](#subprocesserror). This means you do not need to call [`await subprocess`](#await-subprocess).
|
|
138
|
+
|
|
139
|
+
##### subprocess.stderr
|
|
140
|
+
|
|
141
|
+
_Returns_: `AsyncIterable<string>`\
|
|
142
|
+
_Throws_: [`SubprocessError`](#subprocesserror)
|
|
143
|
+
|
|
144
|
+
Same as [`subprocess.stdout`](#subprocessstdout) but for [`stderr`](#resultstderr) instead.
|
|
145
|
+
|
|
146
|
+
##### subprocess[Symbol.asyncIterator]\()
|
|
147
|
+
|
|
148
|
+
_Returns_: `AsyncIterable<string>`\
|
|
149
|
+
_Throws_: [`SubprocessError`](#subprocesserror)
|
|
150
|
+
|
|
151
|
+
Same as [`subprocess.stdout`](#subprocessstdout) but for both [`stdout` and `stderr`](#resultoutput).
|
|
152
|
+
|
|
153
|
+
##### subprocess.pipe(file, arguments?, options?)
|
|
154
|
+
|
|
155
|
+
`file`: `string`\
|
|
156
|
+
`arguments`: `string[]`\
|
|
157
|
+
`options`: [`Options`](#options)\
|
|
158
|
+
_Returns_: [`Subprocess`](#subprocess)
|
|
159
|
+
|
|
160
|
+
Similar to the `|` symbol in shells. [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the subprocess's[`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) to a second subprocess's [`stdin`](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)).
|
|
161
|
+
|
|
162
|
+
This resolves with that second subprocess's [result](#result). If either subprocess is rejected, this is rejected with that subprocess's [error](#subprocesserror) instead.
|
|
163
|
+
|
|
164
|
+
This follows the same syntax as [`spawn(file, arguments?, options?)`](#spawnfile-arguments-options-default-export). It can be done multiple times in a row.
|
|
165
|
+
|
|
166
|
+
##### await subprocess.nodeChildProcess
|
|
167
|
+
|
|
168
|
+
_Type_: `ChildProcess`
|
|
169
|
+
|
|
170
|
+
Underlying [Node.js child process](https://nodejs.org/api/child_process.html#class-childprocess).
|
|
171
|
+
|
|
172
|
+
Among other things, this can be used to terminate the subprocess using [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) or exchange IPC message using [`.send()`](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).
|
|
173
|
+
|
|
174
|
+
#### Result
|
|
175
|
+
|
|
176
|
+
When the subprocess succeeds, its [promise](#await-subprocess) is resolved with an object with the following properties.
|
|
177
|
+
|
|
178
|
+
##### result.stdout
|
|
179
|
+
|
|
180
|
+
_Type_: `string`
|
|
181
|
+
|
|
182
|
+
The output of the subprocess on [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)).
|
|
183
|
+
|
|
184
|
+
If the output ends with a [newline](https://en.wikipedia.org/wiki/Newline), that newline is automatically stripped.
|
|
185
|
+
|
|
186
|
+
This is an empty string if either:
|
|
187
|
+
- The [`stdout`](#optionsstdin-optionsstdout-optionsstderr) option is set to another value than `'pipe'` (its default value).
|
|
188
|
+
- The output is being iterated using [`subprocess.stdout`](#subprocessstdout) or [`subprocess[Symbol.asyncIterator]`](#subprocesssymbolasynciterator).
|
|
189
|
+
|
|
190
|
+
##### result.stderr
|
|
191
|
+
|
|
192
|
+
_Type_: `string`
|
|
193
|
+
|
|
194
|
+
Like [`result.stdout`](#resultstdout) but for the [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)) instead.
|
|
195
|
+
|
|
196
|
+
##### result.output
|
|
197
|
+
|
|
198
|
+
_Type_: `string`
|
|
199
|
+
|
|
200
|
+
Like [`result.stdout`](#resultstdout) but for both the [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) and [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)), interleaved.
|
|
201
|
+
|
|
202
|
+
##### result.command
|
|
203
|
+
|
|
204
|
+
_Type_: `string`
|
|
205
|
+
|
|
206
|
+
The file and arguments that were run.
|
|
207
|
+
|
|
208
|
+
It is intended for logging or debugging. Since the escaping is fairly basic, it should not be executed directly.
|
|
209
|
+
|
|
210
|
+
##### result.durationMs
|
|
211
|
+
|
|
212
|
+
_Type_: `number`
|
|
213
|
+
|
|
214
|
+
Duration of the subprocess, in milliseconds.
|
|
215
|
+
|
|
216
|
+
##### result.pipedFrom
|
|
217
|
+
|
|
218
|
+
_Type_: `Result | SubprocessError | undefined`
|
|
219
|
+
|
|
220
|
+
If [`subprocess.pipe()`](#subprocesspipefile-arguments-options) was used, the [result](#result) or [error](#subprocesserror) of the other subprocess that was piped into this subprocess.
|
|
221
|
+
|
|
222
|
+
#### SubprocessError
|
|
223
|
+
|
|
224
|
+
_Type_: `Error`
|
|
225
|
+
|
|
226
|
+
When the subprocess fails, its [promise](#await-subprocess) is rejected with this error.
|
|
227
|
+
|
|
228
|
+
Subprocesses fail either when their [exit code](#subprocesserrorexitcode) is not `0` or when terminated by a [signal](#subprocesserrorsignalname). Other failure reasons include misspelling the command name or using the [`timeout`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) option.
|
|
229
|
+
|
|
230
|
+
Subprocess errors have the same shape as [successful results](#result), with the following additional properties.
|
|
231
|
+
|
|
232
|
+
This error class is exported, so you can use `if (error instanceof SubprocessError) { ... }`.
|
|
233
|
+
|
|
234
|
+
##### subprocessError.exitCode
|
|
235
|
+
|
|
236
|
+
_Type_: `number | undefined`
|
|
237
|
+
|
|
238
|
+
The numeric [exit code](https://en.wikipedia.org/wiki/Exit_status) of the subprocess that was run.
|
|
239
|
+
|
|
240
|
+
This is `undefined` when the subprocess could not be started, or when it was terminated by a [signal](#subprocesserrorsignalname).
|
|
241
|
+
|
|
242
|
+
##### subprocessError.signalName
|
|
243
|
+
|
|
244
|
+
_Type_: `string | undefined`
|
|
245
|
+
|
|
246
|
+
The name of the [signal](https://en.wikipedia.org/wiki/Signal_(IPC)) (like [`SIGTERM`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM)) that terminated the subprocess, sent by either:
|
|
247
|
+
- The current process.
|
|
248
|
+
- Another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events).
|
|
249
|
+
|
|
250
|
+
If a signal terminated the subprocess, this property is defined and included in the [error message](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message). Otherwise it is `undefined`.
|
|
251
|
+
|
|
252
|
+
## Windows support
|
|
253
|
+
|
|
254
|
+
This package fixes several cross-platform issues with [`node:child_process`](https://nodejs.org/api/child_process.html). It brings full Windows support for:
|
|
255
|
+
- Node modules binaries (without requiring the [`shell`](https://nodejs.org/api/child_process.html#default-windows-shell) option). This includes running `npm ...` or `yarn ...`.
|
|
256
|
+
- `.cmd`, `.bat`, and other shell files.
|
|
257
|
+
- The [`PATHEXT`](https://wiki.tcl-lang.org/page/PATHEXT) environment variable.
|
|
258
|
+
- Windows-specific [newlines](https://en.wikipedia.org/wiki/Newline#Representation).
|
|
259
|
+
|
|
260
|
+
## Alternatives
|
|
261
|
+
|
|
262
|
+
`nano-spawn`'s main goal is to be small, yet useful. Nonetheless, depending on your use case, there are other ways to run subprocesses in Node.js.
|
|
263
|
+
|
|
264
|
+
## Maintainers
|
|
265
|
+
|
|
266
|
+
- [Sindre Sorhus](https://github.com/sindresorhus)
|
|
267
|
+
- [@ehmicky](https://github.com/ehmicky)
|
|
268
|
+
- [Leon Si](https://github.com/leonsilicon)
|
|
269
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import stripAnsi from 'strip-ansi';
|
|
3
|
+
|
|
4
|
+
export const getContext = raw => ({
|
|
5
|
+
start: process.hrtime.bigint(),
|
|
6
|
+
command: raw.map(part => getCommandPart(stripAnsi(part))).join(' '),
|
|
7
|
+
state: {
|
|
8
|
+
stdout: '', stderr: '', output: '', isIterating: {}, nonIterable: [false, false],
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const getCommandPart = part => /[^\w./-]/.test(part)
|
|
13
|
+
? `'${part.replaceAll('\'', '\'\\\'\'')}'`
|
|
14
|
+
: part;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type {ChildProcess, SpawnOptions} from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
type StdioOption = Readonly<Exclude<SpawnOptions['stdio'], undefined>[number]>;
|
|
4
|
+
type StdinOption = StdioOption | {readonly string?: string};
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
Options passed to `nano-spawn`.
|
|
8
|
+
|
|
9
|
+
All [`child_process.spawn()` options](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) can be passed.
|
|
10
|
+
*/
|
|
11
|
+
export type Options = Omit<SpawnOptions, 'env' | 'stdio'> & Readonly<Partial<{
|
|
12
|
+
/**
|
|
13
|
+
Subprocess's standard [input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)).
|
|
14
|
+
|
|
15
|
+
[All values supported](https://nodejs.org/api/child_process.html#optionsstdio) by `node:child_process` are available. The most common ones are:
|
|
16
|
+
- `'pipe'` (default value): use `nodeChildProcess.stdin` stream.
|
|
17
|
+
- `'inherit'`: uses the current process's [input](https://nodejs.org/api/process.html#processstdin). This is useful when running in a terminal.
|
|
18
|
+
- `'ignore'`: discards the input/output.
|
|
19
|
+
- [`Stream`](https://nodejs.org/api/stream.html#stream): redirects the input from/to a stream. For example, [`fs.createReadStream()`](https://nodejs.org/api/fs.html#fscreatereadstreampath-options) can be used, once the stream's [`open`](https://nodejs.org/api/fs.html#event-open) event has been emitted.
|
|
20
|
+
- `{string: '...'}`: passes a string as input.
|
|
21
|
+
|
|
22
|
+
@default 'pipe'
|
|
23
|
+
*/
|
|
24
|
+
stdin: StdinOption;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
Subprocess's standard [output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)).
|
|
28
|
+
|
|
29
|
+
[All values supported](https://nodejs.org/api/child_process.html#optionsstdio) by `node:child_process` are available. The most common ones are:
|
|
30
|
+
- `'pipe'` (default value): returns the output using `result.stdout` and `result.output`.
|
|
31
|
+
- `'inherit'`: uses the current process's [output](https://nodejs.org/api/process.html#processstdout). This is useful when running in a terminal.
|
|
32
|
+
- `'ignore'`: discards the input/output.
|
|
33
|
+
- [`Stream`](https://nodejs.org/api/stream.html#stream): redirects the output from/to a stream. For example, [`fs.createWriteStream()`](https://nodejs.org/api/fs.html#fscreatewritestreampath-options) can be used, once the stream's [`open`](https://nodejs.org/api/fs.html#event-open) event has been emitted.
|
|
34
|
+
|
|
35
|
+
@default 'pipe'
|
|
36
|
+
*/
|
|
37
|
+
stdout: StdioOption;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
Subprocess's standard [error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)).
|
|
41
|
+
|
|
42
|
+
[All values supported](https://nodejs.org/api/child_process.html#optionsstdio) by `node:child_process` are available. The most common ones are:
|
|
43
|
+
- `'pipe'` (default value): returns the output using `result.stderr` and `result.output`.
|
|
44
|
+
- `'inherit'`: uses the current process's [output](https://nodejs.org/api/process.html#processstdout). This is useful when running in a terminal.
|
|
45
|
+
- `'ignore'`: discards the input/output.
|
|
46
|
+
- [`Stream`](https://nodejs.org/api/stream.html#stream): redirects the output from/to a stream. For example, [`fs.createWriteStream()`](https://nodejs.org/api/fs.html#fscreatewritestreampath-options) can be used, once the stream's [`open`](https://nodejs.org/api/fs.html#event-open) event has been emitted.
|
|
47
|
+
|
|
48
|
+
@default 'pipe'
|
|
49
|
+
*/
|
|
50
|
+
stderr: StdioOption;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
Subprocess's standard [input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin))/[output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout))/[error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)).
|
|
54
|
+
|
|
55
|
+
[All values supported](https://nodejs.org/api/child_process.html#optionsstdio) by `node:child_process` are available. The most common ones are:
|
|
56
|
+
- `'pipe'` (default value): returns the output using `result.stdout`, `result.stderr` and `result.output`.
|
|
57
|
+
- `'inherit'`: uses the current process's [input](https://nodejs.org/api/process.html#processstdin)/[output](https://nodejs.org/api/process.html#processstdout). This is useful when running in a terminal.
|
|
58
|
+
- `'ignore'`: discards the input/output.
|
|
59
|
+
- [`Stream`](https://nodejs.org/api/stream.html#stream): redirects the input/output from/to a stream. For example, [`fs.createReadStream()`](https://nodejs.org/api/fs.html#fscreatereadstreampath-options)/[`fs.createWriteStream()`](https://nodejs.org/api/fs.html#fscreatewritestreampath-options) can be used, once the stream's [`open`](https://nodejs.org/api/fs.html#event-open) event has been emitted.
|
|
60
|
+
- `{string: '...'}`: passes a string as input to `stdin`.
|
|
61
|
+
|
|
62
|
+
@default ['pipe', 'pipe', 'pipe']
|
|
63
|
+
*/
|
|
64
|
+
stdio: SpawnOptions['stdio'] | readonly [StdinOption, ...readonly StdioOption[]];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
Allows executing binaries installed locally with `npm` (or `yarn`, etc.).
|
|
68
|
+
|
|
69
|
+
@default false
|
|
70
|
+
*/
|
|
71
|
+
preferLocal: boolean;
|
|
72
|
+
|
|
73
|
+
// Fixes issues with Remix and Next.js
|
|
74
|
+
// See https://github.com/sindresorhus/execa/pull/1141
|
|
75
|
+
/**
|
|
76
|
+
Override specific [environment variables](https://en.wikipedia.org/wiki/Environment_variable). Other environment variables are inherited from the current process ([`process.env`](https://nodejs.org/api/process.html#processenv)).
|
|
77
|
+
|
|
78
|
+
@default {}
|
|
79
|
+
*/
|
|
80
|
+
env: Readonly<Partial<Record<string, string>>>;
|
|
81
|
+
}>>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
When the subprocess succeeds, its promise is resolved with this object.
|
|
85
|
+
*/
|
|
86
|
+
export type Result = {
|
|
87
|
+
/**
|
|
88
|
+
The output of the subprocess on [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)).
|
|
89
|
+
|
|
90
|
+
If the output ends with a [newline](https://en.wikipedia.org/wiki/Newline), that newline is automatically stripped.
|
|
91
|
+
|
|
92
|
+
This is an empty string if either:
|
|
93
|
+
- The `stdout` option is set to another value than `'pipe'` (its default value).
|
|
94
|
+
- The output is being iterated using `subprocess.stdout` or `subprocess[Symbol.asyncIterator]`.
|
|
95
|
+
*/
|
|
96
|
+
stdout: string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
The output of the subprocess on [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)).
|
|
100
|
+
|
|
101
|
+
If the output ends with a [newline](https://en.wikipedia.org/wiki/Newline), that newline is automatically stripped.
|
|
102
|
+
|
|
103
|
+
This is an empty string if either:
|
|
104
|
+
- The `stderr` option is set to another value than `'pipe'` (its default value).
|
|
105
|
+
- The output is being iterated using `subprocess.stderr` or `subprocess[Symbol.asyncIterator]`.
|
|
106
|
+
*/
|
|
107
|
+
stderr: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
Like `result.stdout` but for both the [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) and [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)), interleaved.
|
|
111
|
+
*/
|
|
112
|
+
output: string;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
The file and arguments that were run.
|
|
116
|
+
|
|
117
|
+
It is intended for logging or debugging. Since the escaping is fairly basic, it should not be executed directly.
|
|
118
|
+
*/
|
|
119
|
+
command: string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
Duration of the subprocess, in milliseconds.
|
|
123
|
+
*/
|
|
124
|
+
durationMs: number;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
If `subprocess.pipe()` was used, the result or error of the other subprocess that was piped into this subprocess.
|
|
128
|
+
*/
|
|
129
|
+
pipedFrom?: Result | SubprocessError;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
When the subprocess fails, its promise is rejected with this error.
|
|
134
|
+
|
|
135
|
+
Subprocesses fail either when their exit code is not `0` or when terminated by a signal. Other failure reasons include misspelling the command name or using the [`timeout`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) option.
|
|
136
|
+
*/
|
|
137
|
+
export class SubprocessError extends Error implements Result {
|
|
138
|
+
stdout: Result['stdout'];
|
|
139
|
+
stderr: Result['stderr'];
|
|
140
|
+
output: Result['output'];
|
|
141
|
+
command: Result['command'];
|
|
142
|
+
durationMs: Result['durationMs'];
|
|
143
|
+
pipedFrom?: Exclude<Result['pipedFrom'], undefined>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
The numeric [exit code](https://en.wikipedia.org/wiki/Exit_status) of the subprocess that was run.
|
|
147
|
+
|
|
148
|
+
This is `undefined` when the subprocess could not be started, or when it was terminated by a signal.
|
|
149
|
+
*/
|
|
150
|
+
exitCode?: number;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
The name of the [signal](https://en.wikipedia.org/wiki/Signal_(IPC)) (like [`SIGTERM`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM)) that terminated the subprocess, sent by either:
|
|
154
|
+
- The current process.
|
|
155
|
+
- Another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events).
|
|
156
|
+
|
|
157
|
+
If a signal terminated the subprocess, this property is defined and included in the [error message](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message). Otherwise it is `undefined`.
|
|
158
|
+
*/
|
|
159
|
+
signalName?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
Subprocess started by `spawn()`.
|
|
164
|
+
|
|
165
|
+
A subprocess is a promise that is either resolved with a successful `result` object or rejected with a `subprocessError`.
|
|
166
|
+
|
|
167
|
+
It is also an iterable, iterating over each `stdout`/`stderr` line, as soon as it is available. The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess fails. This means you do not need to call `await subprocess`.
|
|
168
|
+
*/
|
|
169
|
+
export type Subprocess = Promise<Result> & AsyncIterable<string> & {
|
|
170
|
+
/**
|
|
171
|
+
Underlying [Node.js child process](https://nodejs.org/api/child_process.html#class-childprocess).
|
|
172
|
+
|
|
173
|
+
Among other things, this can be used to terminate the subprocess using [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) or exchange IPC message using [`.send()`](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).
|
|
174
|
+
*/
|
|
175
|
+
nodeChildProcess: Promise<ChildProcess>;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
Iterates over each `stdout` line, as soon as it is available.
|
|
179
|
+
|
|
180
|
+
The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess fails. This means you do not need to call `await subprocess`.
|
|
181
|
+
*/
|
|
182
|
+
stdout: AsyncIterable<string>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
Iterates over each `stderr` line, as soon as it is available.
|
|
186
|
+
|
|
187
|
+
The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess fails. This means you do not need to call `await subprocess`.
|
|
188
|
+
*/
|
|
189
|
+
stderr: AsyncIterable<string>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
Similar to the `|` symbol in shells. [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the subprocess's[`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) to a second subprocess's [`stdin`](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)).
|
|
193
|
+
|
|
194
|
+
This resolves with that second subprocess's result. If either subprocess is rejected, this is rejected with that subprocess's error instead.
|
|
195
|
+
|
|
196
|
+
This follows the same syntax as `spawn(file, arguments?, options?)`. It can be done multiple times in a row.
|
|
197
|
+
|
|
198
|
+
@param file - The program/script to execute
|
|
199
|
+
@param arguments - Arguments to pass to `file` on execution.
|
|
200
|
+
@param options
|
|
201
|
+
@returns `Subprocess`
|
|
202
|
+
|
|
203
|
+
@example
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
const result = await spawn('npm', ['run', 'build'])
|
|
207
|
+
.pipe('sort')
|
|
208
|
+
.pipe('head', ['-n', '2']);
|
|
209
|
+
```
|
|
210
|
+
*/
|
|
211
|
+
pipe(file: string, arguments?: readonly string[], options?: Options): Subprocess;
|
|
212
|
+
pipe(file: string, options?: Options): Subprocess;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
Executes a command using `file ...arguments`.
|
|
217
|
+
|
|
218
|
+
This has the same syntax as [`child_process.spawn()`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options).
|
|
219
|
+
|
|
220
|
+
If `file` is `'node'`, the current Node.js version and [flags](https://nodejs.org/api/cli.html#options) are inherited.
|
|
221
|
+
|
|
222
|
+
@param file - The program/script to execute
|
|
223
|
+
@param arguments - Arguments to pass to `file` on execution.
|
|
224
|
+
@param options
|
|
225
|
+
@returns `Subprocess`
|
|
226
|
+
|
|
227
|
+
@example <caption>Run commands</caption>
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
import spawn from 'nano-spawn';
|
|
231
|
+
|
|
232
|
+
const result = await spawn('echo', ['🦄']);
|
|
233
|
+
|
|
234
|
+
console.log(result.output);
|
|
235
|
+
//=> '🦄'
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
@example <caption>Iterate over output lines</caption>
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
for await (const line of spawn('ls', ['--oneline'])) {
|
|
242
|
+
console.log(line);
|
|
243
|
+
}
|
|
244
|
+
//=> index.d.ts
|
|
245
|
+
//=> index.js
|
|
246
|
+
//=> …
|
|
247
|
+
```
|
|
248
|
+
*/
|
|
249
|
+
export default function spawn(file: string, arguments?: readonly string[], options?: Options): Subprocess;
|
|
250
|
+
export default function spawn(file: string, options?: Options): Subprocess;
|
package/source/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {getContext} from './context.js';
|
|
2
|
+
import {getOptions} from './options.js';
|
|
3
|
+
import {spawnSubprocess} from './spawn.js';
|
|
4
|
+
import {getResult} from './result.js';
|
|
5
|
+
import {handlePipe} from './pipe.js';
|
|
6
|
+
import {lineIterator, combineAsyncIterators} from './iterable.js';
|
|
7
|
+
|
|
8
|
+
export {SubprocessError} from './result.js';
|
|
9
|
+
|
|
10
|
+
export default function spawn(file, second, third, previous) {
|
|
11
|
+
const [commandArguments = [], options = {}] = Array.isArray(second) ? [second, third] : [[], second];
|
|
12
|
+
const context = getContext([file, ...commandArguments]);
|
|
13
|
+
const spawnOptions = getOptions(options);
|
|
14
|
+
const nodeChildProcess = spawnSubprocess(file, commandArguments, spawnOptions, context);
|
|
15
|
+
let subprocess = getResult(nodeChildProcess, spawnOptions, context);
|
|
16
|
+
Object.assign(subprocess, {nodeChildProcess});
|
|
17
|
+
subprocess = previous ? handlePipe([previous, subprocess]) : subprocess;
|
|
18
|
+
|
|
19
|
+
const stdout = lineIterator(subprocess, context, 'stdout', 0);
|
|
20
|
+
const stderr = lineIterator(subprocess, context, 'stderr', 1);
|
|
21
|
+
return Object.assign(subprocess, {
|
|
22
|
+
nodeChildProcess,
|
|
23
|
+
stdout,
|
|
24
|
+
stderr,
|
|
25
|
+
[Symbol.asyncIterator]: () => combineAsyncIterators(context, stdout, stderr),
|
|
26
|
+
pipe: (file, second, third) => spawn(file, second, third, subprocess),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const lineIterator = async function * (subprocess, {state}, streamName, index) {
|
|
2
|
+
// Prevent buffering when iterating.
|
|
3
|
+
// This would defeat one of the main goals of iterating: low memory consumption.
|
|
4
|
+
if (state.isIterating[streamName] === false) {
|
|
5
|
+
throw new Error(`The subprocess must be iterated right away, for example:
|
|
6
|
+
for await (const line of spawn(...)) { ... }`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
state.isIterating[streamName] = true;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const {[streamName]: stream} = await subprocess.nodeChildProcess;
|
|
13
|
+
if (!stream) {
|
|
14
|
+
state.nonIterable[index] = true;
|
|
15
|
+
const message = state.nonIterable.every(Boolean)
|
|
16
|
+
? 'either the option `stdout` or `stderr`'
|
|
17
|
+
: `the option \`${streamName}\``;
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
`The subprocess cannot be iterated unless ${message} is 'pipe'.`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
handleErrors(subprocess);
|
|
24
|
+
|
|
25
|
+
// Reverted to before https://github.com/sindresorhus/nano-spawn/commit/51e8d8f481d1fc8210d19005efb1d70339683721 to avoid depending on "node:readline"
|
|
26
|
+
let buffer = '';
|
|
27
|
+
for await (const chunk of stream.iterator({destroyOnReturn: false})) {
|
|
28
|
+
const lines = `${buffer}${chunk}`.split(/\r?\n/);
|
|
29
|
+
buffer = lines.pop(); // Keep last line in buffer as it may not be complete
|
|
30
|
+
yield * lines;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (buffer) {
|
|
34
|
+
yield buffer; // Yield any remaining data as the last line
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
await subprocess;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// When the `subprocess` promise is rejected, we await it in the `finally`
|
|
42
|
+
// block. However, this might not happen right away, so an `unhandledRejection`
|
|
43
|
+
// error is emitted first, crashing the process. This prevents it.
|
|
44
|
+
// This is safe since we are guaranteed to propagate the `subprocess` error
|
|
45
|
+
// with the `finally` block.
|
|
46
|
+
// See https://github.com/sindresorhus/nano-spawn/issues/104
|
|
47
|
+
const handleErrors = async subprocess => {
|
|
48
|
+
try {
|
|
49
|
+
await subprocess;
|
|
50
|
+
} catch {}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Merge two async iterators into one
|
|
54
|
+
export const combineAsyncIterators = async function * ({state}, ...iterators) {
|
|
55
|
+
try {
|
|
56
|
+
let promises = [];
|
|
57
|
+
while (iterators.length > 0) {
|
|
58
|
+
promises = iterators.map((iterator, index) => promises[index] ?? getNext(iterator, index, state));
|
|
59
|
+
// eslint-disable-next-line no-await-in-loop
|
|
60
|
+
const [{value, done}, index] = await Promise.race(promises
|
|
61
|
+
.map((promise, index) => Promise.all([promise, index])));
|
|
62
|
+
|
|
63
|
+
const [iterator] = iterators.splice(index, 1);
|
|
64
|
+
promises.splice(index, 1);
|
|
65
|
+
|
|
66
|
+
if (!done) {
|
|
67
|
+
iterators.push(iterator);
|
|
68
|
+
yield value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
await Promise.all(iterators.map(iterator => iterator.return()));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getNext = async (iterator, index, {nonIterable}) => {
|
|
77
|
+
try {
|
|
78
|
+
return await iterator.next();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return shouldIgnoreError(nonIterable, index)
|
|
81
|
+
? iterator.return()
|
|
82
|
+
: iterator.throw(error);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const shouldIgnoreError = (nonIterable, index) => nonIterable.every(Boolean)
|
|
87
|
+
? index !== nonIterable.length - 1
|
|
88
|
+
: nonIterable[index];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
export const getOptions = ({
|
|
6
|
+
stdin,
|
|
7
|
+
stdout,
|
|
8
|
+
stderr,
|
|
9
|
+
stdio = [stdin, stdout, stderr],
|
|
10
|
+
env: envOption,
|
|
11
|
+
preferLocal,
|
|
12
|
+
cwd: cwdOption = '.',
|
|
13
|
+
...options
|
|
14
|
+
}) => {
|
|
15
|
+
const cwd = cwdOption instanceof URL ? fileURLToPath(cwdOption) : path.resolve(cwdOption);
|
|
16
|
+
const env = envOption ? {...process.env, ...envOption} : undefined;
|
|
17
|
+
const input = stdio[0]?.string;
|
|
18
|
+
return {
|
|
19
|
+
...options,
|
|
20
|
+
input,
|
|
21
|
+
stdio: input === undefined ? stdio : ['pipe', ...stdio.slice(1)],
|
|
22
|
+
env: preferLocal ? addLocalPath(env ?? process.env, cwd) : env,
|
|
23
|
+
cwd,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const addLocalPath = ({Path = '', PATH = Path, ...env}, cwd) => {
|
|
28
|
+
const pathParts = PATH.split(path.delimiter);
|
|
29
|
+
const localPaths = getLocalPaths([], path.resolve(cwd))
|
|
30
|
+
.map(localPath => path.join(localPath, 'node_modules/.bin'))
|
|
31
|
+
.filter(localPath => !pathParts.includes(localPath));
|
|
32
|
+
return {...env, PATH: [...localPaths, PATH].filter(Boolean).join(path.delimiter)};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getLocalPaths = (localPaths, localPath) => localPaths.at(-1) === localPath
|
|
36
|
+
? localPaths
|
|
37
|
+
: getLocalPaths([...localPaths, localPath], path.resolve(localPath, '..'));
|
package/source/pipe.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {pipeline} from 'node:stream/promises';
|
|
2
|
+
|
|
3
|
+
export const handlePipe = async subprocesses => {
|
|
4
|
+
// Ensure both subprocesses have exited before resolving, and that we handle errors from both
|
|
5
|
+
const [[from, to]] = await Promise.all([Promise.allSettled(subprocesses), pipeStreams(subprocesses)]);
|
|
6
|
+
|
|
7
|
+
// If both subprocesses fail, throw destination error to use a predictable order and avoid race conditions
|
|
8
|
+
if (to.reason) {
|
|
9
|
+
to.reason.pipedFrom = from.reason ?? from.value;
|
|
10
|
+
throw to.reason;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (from.reason) {
|
|
14
|
+
throw from.reason;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {...to.value, pipedFrom: from.value};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const pipeStreams = async subprocesses => {
|
|
21
|
+
try {
|
|
22
|
+
const [{stdout}, {stdin}] = await Promise.all(subprocesses.map(({nodeChildProcess}) => nodeChildProcess));
|
|
23
|
+
if (stdin === null) {
|
|
24
|
+
throw new Error('The "stdin" option must be set on the first "spawn()" call in the pipeline.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (stdout === null) {
|
|
28
|
+
throw new Error('The "stdout" option must be set on the last "spawn()" call in the pipeline.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Do not `await` nor handle stream errors since this is already done by each subprocess
|
|
32
|
+
// eslint-disable-next-line promise/prefer-await-to-then
|
|
33
|
+
pipeline(stdout, stdin).catch(() => {});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
await Promise.allSettled(subprocesses.map(({nodeChildProcess}) => closeStdin(nodeChildProcess)));
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const closeStdin = async nodeChildProcess => {
|
|
41
|
+
const {stdin} = await nodeChildProcess;
|
|
42
|
+
stdin.end();
|
|
43
|
+
};
|
package/source/result.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {once, on} from 'node:events';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
|
|
4
|
+
export const getResult = async (nodeChildProcess, {input}, context) => {
|
|
5
|
+
const instance = await nodeChildProcess;
|
|
6
|
+
if (input !== undefined) {
|
|
7
|
+
instance.stdin.end(input);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const onClose = once(instance, 'close');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await Promise.race([
|
|
14
|
+
onClose,
|
|
15
|
+
...instance.stdio.filter(Boolean).map(stream => onStreamError(stream)),
|
|
16
|
+
]);
|
|
17
|
+
checkFailure(context, getErrorOutput(instance));
|
|
18
|
+
return getOutputs(context);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
await Promise.allSettled([onClose]);
|
|
21
|
+
throw getResultError(error, instance, context);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const onStreamError = async stream => {
|
|
26
|
+
for await (const [error] of on(stream, 'error')) {
|
|
27
|
+
// Ignore errors that are due to closing errors when the subprocesses exit normally, or due to piping
|
|
28
|
+
if (!['ERR_STREAM_PREMATURE_CLOSE', 'EPIPE'].includes(error?.code)) {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const checkFailure = ({command}, {exitCode, signalName}) => {
|
|
35
|
+
if (signalName !== undefined) {
|
|
36
|
+
throw new SubprocessError(`Command was terminated with ${signalName}: ${command}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (exitCode !== undefined) {
|
|
40
|
+
throw new SubprocessError(`Command failed with exit code ${exitCode}: ${command}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const getResultError = (error, instance, context) => Object.assign(
|
|
45
|
+
getErrorInstance(error, context),
|
|
46
|
+
getErrorOutput(instance),
|
|
47
|
+
getOutputs(context),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const getErrorInstance = (error, {command}) => error instanceof SubprocessError
|
|
51
|
+
? error
|
|
52
|
+
: new SubprocessError(`Command failed: ${command}`, {cause: error});
|
|
53
|
+
|
|
54
|
+
export class SubprocessError extends Error {
|
|
55
|
+
name = 'SubprocessError';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const getErrorOutput = ({exitCode, signalCode}) => ({
|
|
59
|
+
// `exitCode` can be a negative number (`errno`) when the `error` event is emitted on the `instance`
|
|
60
|
+
...(exitCode < 1 ? {} : {exitCode}),
|
|
61
|
+
...(signalCode === null ? {} : {signalName: signalCode}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const getOutputs = ({state: {stdout, stderr, output}, command, start}) => ({
|
|
65
|
+
stdout: getOutput(stdout),
|
|
66
|
+
stderr: getOutput(stderr),
|
|
67
|
+
output: getOutput(output),
|
|
68
|
+
command,
|
|
69
|
+
durationMs: Number(process.hrtime.bigint() - start) / 1e6,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const getOutput = output => output.at(-1) === '\n'
|
|
73
|
+
? output.slice(0, output.at(-2) === '\r' ? -2 : -1)
|
|
74
|
+
: output;
|
package/source/spawn.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
import {once} from 'node:events';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import {applyForceShell} from './windows.js';
|
|
5
|
+
import {getResultError} from './result.js';
|
|
6
|
+
|
|
7
|
+
export const spawnSubprocess = async (file, commandArguments, options, context) => {
|
|
8
|
+
try {
|
|
9
|
+
// When running `node`, keep the current Node version and CLI flags.
|
|
10
|
+
// Not applied with file paths to `.../node` since those indicate a clear intent to use a specific Node version.
|
|
11
|
+
// This also provides a way to opting out, e.g. using `process.execPath` instead of `node` to discard current CLI flags.
|
|
12
|
+
// Does not work with shebangs, but those don't work cross-platform anyway.
|
|
13
|
+
if (['node', 'node.exe'].includes(file.toLowerCase())) {
|
|
14
|
+
file = process.execPath;
|
|
15
|
+
commandArguments = [...process.execArgv.filter(flag => !flag.startsWith('--inspect')), ...commandArguments];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[file, commandArguments, options] = await applyForceShell(file, commandArguments, options);
|
|
19
|
+
[file, commandArguments, options] = concatenateShell(file, commandArguments, options);
|
|
20
|
+
const instance = spawn(file, commandArguments, options);
|
|
21
|
+
bufferOutput(instance.stdout, context, 'stdout');
|
|
22
|
+
bufferOutput(instance.stderr, context, 'stderr');
|
|
23
|
+
|
|
24
|
+
// The `error` event is caught by `once(instance, 'spawn')` and `once(instance, 'close')`.
|
|
25
|
+
// But it creates an uncaught exception if it happens exactly one tick after 'spawn'.
|
|
26
|
+
// This prevents that.
|
|
27
|
+
instance.once('error', () => {});
|
|
28
|
+
|
|
29
|
+
await once(instance, 'spawn');
|
|
30
|
+
return instance;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw getResultError(error, {}, context);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// When the `shell` option is set, any command argument is concatenated as a single string by Node.js:
|
|
37
|
+
// https://github.com/nodejs/node/blob/e38ce27f3ca0a65f68a31cedd984cddb927d4002/lib/child_process.js#L614-L624
|
|
38
|
+
// However, since Node 24, it also prints a deprecation warning.
|
|
39
|
+
// To avoid this warning, we perform that same operation before calling `node:child_process`.
|
|
40
|
+
// Shells only understand strings, which is why Node.js performs that concatenation.
|
|
41
|
+
// However, we rely on users splitting command arguments as an array.
|
|
42
|
+
// For example, this allows us to easily detect whether the binary file is `node` or `node.exe`.
|
|
43
|
+
// So we do want users to pass array of arguments even with `shell: true`, but we also want to avoid any warning.
|
|
44
|
+
const concatenateShell = (file, commandArguments, options) => options.shell && commandArguments.length > 0
|
|
45
|
+
? [[file, ...commandArguments].join(' '), [], options]
|
|
46
|
+
: [file, commandArguments, options];
|
|
47
|
+
|
|
48
|
+
const bufferOutput = (stream, {state}, streamName) => {
|
|
49
|
+
if (stream) {
|
|
50
|
+
stream.setEncoding('utf8');
|
|
51
|
+
if (!state.isIterating[streamName]) {
|
|
52
|
+
state.isIterating[streamName] = false;
|
|
53
|
+
stream.on('data', chunk => {
|
|
54
|
+
state[streamName] += chunk;
|
|
55
|
+
state.output += chunk;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
// When setting `shell: true` under-the-hood, we must manually escape the file and arguments.
|
|
6
|
+
// This ensures arguments are properly split, and prevents command injection.
|
|
7
|
+
export const applyForceShell = async (file, commandArguments, options) => await shouldForceShell(file, options)
|
|
8
|
+
? [escapeFile(file), commandArguments.map(argument => escapeArgument(argument)), {...options, shell: true}]
|
|
9
|
+
: [file, commandArguments, options];
|
|
10
|
+
|
|
11
|
+
// On Windows, running most executable files (except *.exe and *.com) requires using a shell.
|
|
12
|
+
// This includes *.cmd and *.bat, which itself includes Node modules binaries.
|
|
13
|
+
// We detect this situation and automatically:
|
|
14
|
+
// - Set the `shell: true` option
|
|
15
|
+
// - Escape shell-specific characters
|
|
16
|
+
const shouldForceShell = async (file, {shell, cwd, env = process.env}) => process.platform === 'win32'
|
|
17
|
+
&& !shell
|
|
18
|
+
&& !(await isExe(file, cwd, env));
|
|
19
|
+
|
|
20
|
+
// Detect whether the executable file is a *.exe or *.com file.
|
|
21
|
+
// Windows allows omitting file extensions (present in the `PATHEXT` environment variable).
|
|
22
|
+
// Therefore we must use the `PATH` environment variable and make `access` calls to check this.
|
|
23
|
+
// Environment variables are case-insensitive on Windows, so we check both `PATH` and `Path`.
|
|
24
|
+
const isExe = (file, cwd, {Path = '', PATH = Path}) =>
|
|
25
|
+
// If the *.exe or *.com file extension was not omitted.
|
|
26
|
+
// Windows common file systems are case-insensitive.
|
|
27
|
+
exeExtensions.some(extension => file.toLowerCase().endsWith(extension))
|
|
28
|
+
|| mIsExe(file, cwd, PATH);
|
|
29
|
+
|
|
30
|
+
// Memoize the `mIsExe` and `fs.access`, for performance
|
|
31
|
+
const EXE_MEMO = {};
|
|
32
|
+
// eslint-disable-next-line no-return-assign
|
|
33
|
+
const memoize = function_ => (...arguments_) =>
|
|
34
|
+
// Use returned assignment to keep code small
|
|
35
|
+
EXE_MEMO[arguments_.join('\0')] ??= function_(...arguments_);
|
|
36
|
+
|
|
37
|
+
const access = memoize(fs.access);
|
|
38
|
+
const mIsExe = memoize(async (file, cwd, PATH) => {
|
|
39
|
+
const parts = PATH
|
|
40
|
+
// `PATH` is ;-separated on Windows
|
|
41
|
+
.split(path.delimiter)
|
|
42
|
+
// `PATH` allows leading/trailing ; on Windows
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
// `PATH` parts can be double quoted on Windows
|
|
45
|
+
.map(part => part.replace(/^"(.*)"$/, '$1'));
|
|
46
|
+
|
|
47
|
+
// For performance, parallelize and stop iteration as soon as an *.exe or *.com file is found
|
|
48
|
+
try {
|
|
49
|
+
await Promise.any(
|
|
50
|
+
[cwd, ...parts].flatMap(part => exeExtensions
|
|
51
|
+
.map(extension => access(`${path.resolve(part, file)}${extension}`)),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Other file extensions require using a shell
|
|
62
|
+
const exeExtensions = ['.exe', '.com'];
|
|
63
|
+
|
|
64
|
+
// `cmd.exe` escaping for arguments.
|
|
65
|
+
// Taken from https://github.com/moxystudio/node-cross-spawn
|
|
66
|
+
const escapeArgument = argument => escapeFile(escapeFile(`"${argument
|
|
67
|
+
.replaceAll(/(\\*)"/g, '$1$1\\"')
|
|
68
|
+
.replace(/(\\*)$/, '$1$1')}"`));
|
|
69
|
+
|
|
70
|
+
// `cmd.exe` escaping for file and arguments.
|
|
71
|
+
const escapeFile = file => file.replaceAll(/([()\][%!^"`<>&|;, *?])/g, '^$1');
|