cross-spawn-esm 0.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 +104 -0
- package/README.md +168 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +208 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timur Bikmuhametov <https://github.com/Timbic>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
# Attribution
|
|
24
|
+
|
|
25
|
+
This project contains modified code from the following open source projects.
|
|
26
|
+
I am grateful to the authors and contributors of these projects.
|
|
27
|
+
|
|
28
|
+
## cross-spawn
|
|
29
|
+
|
|
30
|
+
Github: <https://github.com/moxystudio/node-cross-spawn>
|
|
31
|
+
|
|
32
|
+
> The MIT License (MIT)
|
|
33
|
+
>
|
|
34
|
+
> Copyright (c) 2018 Made With MOXY Lda <hello@moxy.studio>
|
|
35
|
+
>
|
|
36
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
37
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
38
|
+
> in the Software without restriction, including without limitation the rights
|
|
39
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
40
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
41
|
+
> furnished to do so, subject to the following conditions:
|
|
42
|
+
>
|
|
43
|
+
> The above copyright notice and this permission notice shall be included in
|
|
44
|
+
> all copies or substantial portions of the Software.
|
|
45
|
+
>
|
|
46
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
47
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
48
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
49
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
50
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
51
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
52
|
+
> THE SOFTWARE.
|
|
53
|
+
|
|
54
|
+
## path-key
|
|
55
|
+
|
|
56
|
+
Github: <https://github.com/sindresorhus/path-key>
|
|
57
|
+
|
|
58
|
+
> The MIT License (MIT)
|
|
59
|
+
>
|
|
60
|
+
> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
61
|
+
>
|
|
62
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
63
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
64
|
+
> in the Software without restriction, including without limitation the rights
|
|
65
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
66
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
67
|
+
> furnished to do so, subject to the following conditions:
|
|
68
|
+
>
|
|
69
|
+
> The above copyright notice and this permission notice shall be included in
|
|
70
|
+
> all copies or substantial portions of the Software.
|
|
71
|
+
>
|
|
72
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
73
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
74
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
75
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
76
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
77
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
78
|
+
> THE SOFTWARE.
|
|
79
|
+
|
|
80
|
+
## shebang-command
|
|
81
|
+
|
|
82
|
+
Github: <https://github.com/kevva/shebang-command>
|
|
83
|
+
|
|
84
|
+
> The MIT License (MIT)
|
|
85
|
+
>
|
|
86
|
+
> Copyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> (github.com/kevva)
|
|
87
|
+
>
|
|
88
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
89
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
90
|
+
> in the Software without restriction, including without limitation the rights
|
|
91
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
92
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
93
|
+
> furnished to do so, subject to the following conditions:
|
|
94
|
+
>
|
|
95
|
+
> The above copyright notice and this permission notice shall be included in
|
|
96
|
+
> all copies or substantial portions of the Software.
|
|
97
|
+
>
|
|
98
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
99
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
100
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
101
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
102
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
103
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
104
|
+
> THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# cross-spawn-esm
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://www.npmjs.com/package/cross-spawn-esm"><img src="https://img.shields.io/npm/v/cross-spawn-esm.svg?logo=nodedotjs" alt="npm package"></a>
|
|
5
|
+
<a href="https://github.com/Timbic/cross-spawn-esm"><img src="https://img.shields.io/badge/Github-gray.svg?logo=github" alt="github repo"></a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
> **Note:** This package is still in development and not recommended for production use yet. However, testing it in your projects is greatly
|
|
9
|
+
> appreciated. If you find any errors, please open an issue - PRs are also welcomed!
|
|
10
|
+
|
|
11
|
+
This is a "fork" of [cross-spawn](https://www.npmjs.com/package/cross-spawn?activeTab=readme) ( a cross-platform solution to node's spawn
|
|
12
|
+
and spawnSync ) which ports its codebase to modern ESM and TypeScript.
|
|
13
|
+
|
|
14
|
+
This package isn't a 100% drop-in replacement for **cross-spawn** ( the API differs slightly ) but it tries to behave the same as the
|
|
15
|
+
original package. Please refer to the [Migration guide](#migration-guide) for further explanation.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
With NPM:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install cross-spawn-esm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
With Yarn:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn add cross-spawn-esm
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
With PNPM:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add cross-spawn-esm
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
With Bun:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bun add cross-spawn-esm
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With Deno:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
deno add cross-spawn-esm
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Use it exactly the same way as node's `spawn` and `spawnSync` ( a drop-in replacement for them ) with the same arguments and options. There
|
|
52
|
+
is **no default export**; always use named imports.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { spawn, spawnSync } from "cross-spawn-esm";
|
|
56
|
+
|
|
57
|
+
// Spawn NPM asynchronously
|
|
58
|
+
const child = spawn("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
|
|
59
|
+
|
|
60
|
+
// Spawn NPM synchronously
|
|
61
|
+
const result = spawnSync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Benefits
|
|
65
|
+
|
|
66
|
+
- Overall smaller bundle size
|
|
67
|
+
- Tree-shaking friendly
|
|
68
|
+
- Fewer dependencies
|
|
69
|
+
- No need for an additional types package (`@types/cross-spawn`)
|
|
70
|
+
- Modern codebase and active maintenance
|
|
71
|
+
- Better documentation
|
|
72
|
+
|
|
73
|
+
## Migration guide
|
|
74
|
+
|
|
75
|
+
Porting from `cross-spawn` to `cross-spawn-esm` is mostly a matter of changing how you import the package and how you call its sync API.
|
|
76
|
+
|
|
77
|
+
### 1. Install
|
|
78
|
+
|
|
79
|
+
Uninstall `cross-spawn` (and `@types/cross-spawn` if you had it) and install `cross-spawn-esm`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm remove cross-spawn @types/cross-spawn
|
|
83
|
+
npm install cross-spawn-esm
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Update imports & sync calls
|
|
87
|
+
|
|
88
|
+
**Before**:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import spawn from "cross-spawn";
|
|
92
|
+
|
|
93
|
+
const child = spawn("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
|
|
94
|
+
const result = spawn.sync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**After**:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { spawn, spawnSync } from "cross-spawn-esm";
|
|
101
|
+
|
|
102
|
+
const child = spawn("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
|
|
103
|
+
const result = spawnSync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Internal utilities
|
|
107
|
+
|
|
108
|
+
If you were relying on the hidden internals:
|
|
109
|
+
|
|
110
|
+
**Before**: `spawn._parse(...)`, `spawn._enoent.verifyENOENT(...)`
|
|
111
|
+
|
|
112
|
+
**After**:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { _parse, _enoent } from "cross-spawn-esm";
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
##### Changes:
|
|
119
|
+
|
|
120
|
+
- `_enoent.verifyENOENT(status, parsed, syscall)` now takes an explicit `syscall` argument (`"spawn"` or `"spawnSync"`), where the original
|
|
121
|
+
shipped two separate functions (`verifyENOENT` / `verifyENOENTSync`).
|
|
122
|
+
- `_enoent.notFoundError(...)` now returns a valid **NodeJS.ErrnoException**.
|
|
123
|
+
- `_parse` now contains both `parse` and `parseNonShell` functions:
|
|
124
|
+
|
|
125
|
+
> **Before**: const parsed = _parse(...)
|
|
126
|
+
>
|
|
127
|
+
> **After**: const parsed = _parse.parse(...)
|
|
128
|
+
|
|
129
|
+
- New `_utils` object exposes all the lower-level helpers (`shebangCommand`, `readShebang`, `detectShebang`, `enterCwd`, `resolveCommand`,
|
|
130
|
+
`resolveCommandAttempt`, `escapeLineBreaks`, `escapeMetaChars`, `escapeCommand`, `escapeArgument`, `pathKey`) If you were relying on the
|
|
131
|
+
original **cross-spawn** dependencies ( **path-key** and **shebang-command** ), their improved versions can be found in `_utils`.
|
|
132
|
+
|
|
133
|
+
### 4. TypeScript
|
|
134
|
+
|
|
135
|
+
`cross-spawn-esm` ships its own type definitions, so the third-party `@types/cross-spawn` package is no longer needed.
|
|
136
|
+
|
|
137
|
+
### 5. Behavior notes
|
|
138
|
+
|
|
139
|
+
Beyond the API surface, a few implementation details intentionally differ:
|
|
140
|
+
|
|
141
|
+
- **`original.args` is an independent snapshot**: `parse` clones the args and gives `original.args` its own copy, so shebang rewiring and
|
|
142
|
+
`cmd.exe` escaping never mutate it.
|
|
143
|
+
- **Broader shebang support**: Shebang detection on Windows reads the shebang of the resolved script and rewires the command to its
|
|
144
|
+
interpreter. `#!/usr/bin/env <program>` is resolved from `PATH` and spawned directly. Any other shebang (`#!/bin/sh`, `#!/bin/bash -e`) is
|
|
145
|
+
reduced to the interpreter's basename (plus its single argument, if any) and falls back to the `cmd.exe` wrapper, which works when that
|
|
146
|
+
interpreter is on `PATH`.
|
|
147
|
+
|
|
148
|
+
The following behaviors are intentionally kept identical to `cross-spawn`:
|
|
149
|
+
|
|
150
|
+
- When `options.shell` is used, parsing, escaping, and shebang enhancements are disabled — matching both the original and Node.js behavior.
|
|
151
|
+
- Windows-only ENOENT detection: when the process exits with code `1` and the command could not be resolved, an `error` event (async) or
|
|
152
|
+
`result.error` (sync) is produced.
|
|
153
|
+
|
|
154
|
+
### API comparison
|
|
155
|
+
|
|
156
|
+
| Feature | cross-spawn | cross-spawn-esm |
|
|
157
|
+
| ---------------- | ------------------------ | ------------------------ |
|
|
158
|
+
| Module format | CommonJS | ESM (`"type": "module"`) |
|
|
159
|
+
| Async spawn | `spawn` (default export) | `spawn` (named export) |
|
|
160
|
+
| Sync spawn | `spawn.sync` | `spawnSync` |
|
|
161
|
+
| Parse internals | `spawn._parse` | `_parse` |
|
|
162
|
+
| ENOENT internals | `spawn._enoent` | `_enoent` |
|
|
163
|
+
| Other internals | not exposed | `_utils` |
|
|
164
|
+
| TypeScript types | `@types/cross-spawn` | built-in |
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
Released under the [MIT License](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { ChildProcess, SpawnOptions, SpawnSyncOptions, SpawnSyncReturns } from "node:child_process";
|
|
2
|
+
import { NonSharedBuffer } from "node:buffer";
|
|
3
|
+
//#region src/utils/resolve-command.d.ts
|
|
4
|
+
interface ParsedCommand {
|
|
5
|
+
command: string;
|
|
6
|
+
args: Array<string>;
|
|
7
|
+
options: SpawnOptions & {
|
|
8
|
+
_forceShell?: boolean;
|
|
9
|
+
};
|
|
10
|
+
file?: string | null;
|
|
11
|
+
original: {
|
|
12
|
+
command: string;
|
|
13
|
+
args: ReadonlyArray<string>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Change the process working directory to `cwd`.
|
|
18
|
+
*/
|
|
19
|
+
declare function enterCwd(cwd?: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve `parsed.command` to an absolute file path by searching the
|
|
22
|
+
* environment's `PATH`, using the custom `cwd` when one is set.
|
|
23
|
+
*/
|
|
24
|
+
declare function resolveCommandAttempt(parsed: ParsedCommand, withoutPathExt?: boolean): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve `parsed.command` to an absolute file path by searching the
|
|
27
|
+
* environment's `PATH`, returning `null` when it cannot be found.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* resolveCommand({ command: "node", args: [], options: {}, original: {...} });
|
|
31
|
+
* //=> "C:\\Program Files\\nodejs\\node.exe"
|
|
32
|
+
*
|
|
33
|
+
* resolveCommand({ command: "no-such-tool", ... });
|
|
34
|
+
* //=> null
|
|
35
|
+
*/
|
|
36
|
+
declare function resolveCommand(parsed: ParsedCommand): string | null;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/enoent.d.ts
|
|
39
|
+
/**
|
|
40
|
+
* Builds an ENOENT error for a command that could not be found. Creates
|
|
41
|
+
* an error whose shape matches what Node's own `spawn`/`spawnSync` would produce
|
|
42
|
+
* when it fails to locate the command file. This makes the error interchangeable
|
|
43
|
+
* with a real ENOENT from Node.
|
|
44
|
+
*/
|
|
45
|
+
declare function notFoundError(original: ParsedCommand["original"], syscall: "spawn" | "spawnSync"): NodeJS.ErrnoException;
|
|
46
|
+
/**
|
|
47
|
+
* Detects the Windows-only fake ENOENT that `cmd.exe` produces. On POSIX or
|
|
48
|
+
* for any other exit code the error is not applicable, so `null` is returned.
|
|
49
|
+
*/
|
|
50
|
+
declare function verifyENOENT(status: number | null, parsed: ParsedCommand, syscall: "spawn" | "spawnSync"): NodeJS.ErrnoException | null;
|
|
51
|
+
/**
|
|
52
|
+
* Hooks into a `ChildProcess` `exit` event so that a missing command is
|
|
53
|
+
* surfaced as an `error` event instead of silently exiting with code `1`.
|
|
54
|
+
*
|
|
55
|
+
* On POSIX, a missing command already produces a real ENOENT from libuv and
|
|
56
|
+
* this hook is not needed. On Windows, the command is typically wrapped by
|
|
57
|
+
* `cmd.exe`, which reports an unrecognized command as exit status `1`. This
|
|
58
|
+
* monkey-patches `cp.emit` to intercept that case and emit an `error` event
|
|
59
|
+
* with the proper ENOENT, so Node-style error handling works unchanged.
|
|
60
|
+
*/
|
|
61
|
+
declare function hookChildProcess(cp: ChildProcess, parsed: ParsedCommand): void;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/utils/shebang.d.ts
|
|
64
|
+
/**
|
|
65
|
+
* Parses a shebang line into the interpreter command to run the script.
|
|
66
|
+
* The interpreter path is reduced to its last segment (like `basename`, e.g. `/usr/bin/env` -> `env`).
|
|
67
|
+
* Returns the bare binary, or `binary <argument>` when an argument is present.
|
|
68
|
+
* The special case `env` returns only its argument (e.g. `/usr/bin/env node` -> `node`), because `env` would otherwise run with no program.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* shebangCommand("#!/usr/bin/env node");
|
|
72
|
+
* //=> "node"
|
|
73
|
+
*
|
|
74
|
+
* shebangCommand("#!/bin/bash -e");
|
|
75
|
+
* //=> "bash -e"
|
|
76
|
+
*
|
|
77
|
+
* shebangCommand("not a shebang");
|
|
78
|
+
* //=> null
|
|
79
|
+
*/
|
|
80
|
+
declare function shebangCommand(string: string): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Reads the shebang from a script file's contents.
|
|
83
|
+
* Returns `null` if the file cannot be opened/read or if `shebangCommand` finds no shebang
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* readShebang("/usr/local/bin/script");
|
|
87
|
+
* //=> "node" (when the file starts with "#!/usr/bin/env node")
|
|
88
|
+
* //=> null (when the file has no shebang or cannot be read)
|
|
89
|
+
*/
|
|
90
|
+
declare function readShebang(file: string): string | null;
|
|
91
|
+
/**
|
|
92
|
+
* Detects the shebang of the resolved `parsed.command` file and rewire
|
|
93
|
+
* `parsed` to run the script through its interpreter. When the file has a shebang,
|
|
94
|
+
* the script path is prepended to `parsed.args` and `parsed.command` becomes
|
|
95
|
+
* the interpreter; the interpreter path is then resolved and returned.
|
|
96
|
+
* Otherwise the resolved script path is returned, or `null` when the command could not be found.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* detectShebang({ command: "my-script", args: [], file: undefined, ... });
|
|
100
|
+
* // Rewired: command = "node", args = ["C:\\dir\\my-script"]
|
|
101
|
+
* //=> "C:\\Program Files\\nodejs\\node.exe"
|
|
102
|
+
*/
|
|
103
|
+
declare function detectShebang(parsed: ParsedCommand): string | null;
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/utils/escapes.d.ts
|
|
106
|
+
/**
|
|
107
|
+
* Remove line breaks (`\r` and `\n`) from the given string. Without this, an argument containing a newline could terminate the enclosing
|
|
108
|
+
* command early and inject an arbitrary second command into the shell.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Without escaping, cmd.exe would run two commands:
|
|
112
|
+
* // node -e "console.log('ok')
|
|
113
|
+
* // calc.exe"
|
|
114
|
+
* escapeLineBreaks("console.log('ok')\ncalc.exe");
|
|
115
|
+
* //=> "console.log('ok')calc.exe"
|
|
116
|
+
*/
|
|
117
|
+
declare function escapeLineBreaks(arg: string): string;
|
|
118
|
+
/**
|
|
119
|
+
* Escape Windows `cmd.exe` shell meta characters (``/([()\][%!^"`<>&|;, *?])/g``) by prefixing each with `^`. Without this, a meta character in a command or argument would be interpreted
|
|
120
|
+
* by the shell, breaking the command or enabling arbitrary command execution.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* // Without escaping, cmd.exe would treat the spaces and `&` as separators:
|
|
124
|
+
* escapeMetaChars('notepad "a & b.txt"');
|
|
125
|
+
* //=> 'notepad^ ^"a^ ^&^ b.txt^"'
|
|
126
|
+
*/
|
|
127
|
+
declare function escapeMetaChars(arg: string): string;
|
|
128
|
+
/**
|
|
129
|
+
* Escapes a command name for safe use in a Windows `cmd.exe` invocation.
|
|
130
|
+
*/
|
|
131
|
+
declare function escapeCommand(arg: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* Escape a single argument into a fully self-contained token for a Windows
|
|
134
|
+
* `cmd.exe` command line.
|
|
135
|
+
*/
|
|
136
|
+
declare function escapeArgument(arg: unknown, doubleEscapeMetaChars?: boolean): string;
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/utils/path-key.d.ts
|
|
139
|
+
interface PathKeyOptions {
|
|
140
|
+
/**
|
|
141
|
+
* Use a custom environment variables object.
|
|
142
|
+
* @default process.env
|
|
143
|
+
*
|
|
144
|
+
* @see https://nodejs.org/api/process.html#process_process_env
|
|
145
|
+
*/
|
|
146
|
+
env?: Record<string, string | undefined>;
|
|
147
|
+
/**
|
|
148
|
+
* Get the PATH key for a specific platform.
|
|
149
|
+
* @default process.platform
|
|
150
|
+
*
|
|
151
|
+
* @see https://nodejs.org/api/process.html#process_process_platform
|
|
152
|
+
*/
|
|
153
|
+
platform?: NodeJS.Platform;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the [PATH] environment variable key cross-platform.
|
|
157
|
+
* @see https://en.wikipedia.org/wiki/PATH_(variable)
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const key = pathKey();
|
|
161
|
+
* //=> 'PATH'
|
|
162
|
+
*
|
|
163
|
+
* const PATH = process.env[key];
|
|
164
|
+
* //=> '/usr/local/bin:/usr/bin:/bin'
|
|
165
|
+
*/
|
|
166
|
+
declare function pathKey({ env, platform }: PathKeyOptions): string;
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/parse.d.ts
|
|
169
|
+
/**
|
|
170
|
+
* Prepares a parsed command for spawning without a shell.
|
|
171
|
+
* On POSIX the command is returned as-is, since the native `spawn` handles it.
|
|
172
|
+
* On Windows the command is resolved first: a shebang script is rewired to run
|
|
173
|
+
* through its interpreter. Unless the resolved command is a plain `.exe`,
|
|
174
|
+
* the command is then wrapped for `cmd.exe`: the command and arguments are
|
|
175
|
+
* escaped and reassembled into a `cmd /d /s /c "..."` invocation with
|
|
176
|
+
* `windowsVerbatimArguments` set. The result is a `parsed` whose `command`/`args` are directly spawnable.
|
|
177
|
+
*/
|
|
178
|
+
declare function parseNonShell(parsed: ParsedCommand): ParsedCommand;
|
|
179
|
+
/**
|
|
180
|
+
* Turns a `spawn`/`spawnSync` call signature into a `ParsedCommand`.
|
|
181
|
+
* Accepts the same overload as Node: when the second argument is an options
|
|
182
|
+
* object instead of an args array, it is treated as `options`. The args and
|
|
183
|
+
* options are cloned so the caller's objects are never mutated, and parsing is
|
|
184
|
+
* delegated to `parseNonShell` unless `options.shell` is set (in which case
|
|
185
|
+
* the command is passed through untouched, like Node).
|
|
186
|
+
*/
|
|
187
|
+
declare function parse(command: string, args: ReadonlyArray<string> | SpawnOptions, options: SpawnOptions): ParsedCommand;
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/index.d.ts
|
|
190
|
+
/**
|
|
191
|
+
* A cross-platform drop-in replacement for `child_process.spawn()`.
|
|
192
|
+
*
|
|
193
|
+
* On POSIX, arguments are forwarded to the native `spawn` unchanged. On
|
|
194
|
+
* Windows the command and its arguments are rewritten so that the correct
|
|
195
|
+
* interpreter is invoked for shebang scripts, special characters and
|
|
196
|
+
* whitespace in arguments are safely escaped for `cmd.exe`, and a missing
|
|
197
|
+
* command (which `cmd.exe` signals with exit status `1` instead of a real
|
|
198
|
+
* `ENOENT`) is surfaced as an `error` event, matching what Node produces on POSIX.
|
|
199
|
+
*/
|
|
200
|
+
export declare function spawn(command: string, options?: SpawnOptions): ChildProcess;
|
|
201
|
+
export declare function spawn(command: string, args?: ReadonlyArray<string>, options?: SpawnOptions): ChildProcess;
|
|
202
|
+
/**
|
|
203
|
+
* A cross-platform drop-in replacement for `child_process.spawnSync()`.
|
|
204
|
+
*
|
|
205
|
+
* On POSIX, arguments are forwarded to the native `spawnSync` unchanged. On
|
|
206
|
+
* Windows the same shebang resolution, argument escaping, and ENOENT
|
|
207
|
+
* detection applied by {@link spawn} are performed synchronously, so the
|
|
208
|
+
* returned `SpawnSyncResult` carries a proper `error` when the command does not exist.
|
|
209
|
+
*/
|
|
210
|
+
export declare function spawnSync(command: string, options?: SpawnSyncOptions): SpawnSyncReturns<string | NonSharedBuffer>;
|
|
211
|
+
export declare function spawnSync(command: string, args?: ReadonlyArray<string>, options?: SpawnSyncOptions): SpawnSyncReturns<string | NonSharedBuffer>;
|
|
212
|
+
export declare const _parse: {
|
|
213
|
+
parseNonShell: typeof parseNonShell;
|
|
214
|
+
parse: typeof parse;
|
|
215
|
+
};
|
|
216
|
+
export declare const _enoent: {
|
|
217
|
+
notFoundError: typeof notFoundError;
|
|
218
|
+
verifyENOENT: typeof verifyENOENT;
|
|
219
|
+
hookChildProcess: typeof hookChildProcess;
|
|
220
|
+
};
|
|
221
|
+
export declare const _utils: {
|
|
222
|
+
shebangCommand: typeof shebangCommand;
|
|
223
|
+
readShebang: typeof readShebang;
|
|
224
|
+
detectShebang: typeof detectShebang;
|
|
225
|
+
enterCwd: typeof enterCwd;
|
|
226
|
+
resolveCommand: typeof resolveCommand;
|
|
227
|
+
resolveCommandAttempt: typeof resolveCommandAttempt;
|
|
228
|
+
escapeLineBreaks: typeof escapeLineBreaks;
|
|
229
|
+
escapeMetaChars: typeof escapeMetaChars;
|
|
230
|
+
escapeArgument: typeof escapeArgument;
|
|
231
|
+
escapeCommand: typeof escapeCommand;
|
|
232
|
+
pathKey: typeof pathKey;
|
|
233
|
+
};
|
|
234
|
+
//#endregion
|
|
235
|
+
export type { ParsedCommand, PathKeyOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import cp from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import which from "which";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
//#region src/utils/constants.ts
|
|
6
|
+
const _cwd = process.cwd();
|
|
7
|
+
const _env = process.env;
|
|
8
|
+
const _platform = process.platform;
|
|
9
|
+
const isWin = _platform === "win32";
|
|
10
|
+
const shebangRegExp = /^#!(.*)/;
|
|
11
|
+
const lineBreaksRegExp = /[\r\n]+/g;
|
|
12
|
+
const metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g;
|
|
13
|
+
const executableRegExp = /\.(?:exe)$/i;
|
|
14
|
+
const cmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/utils/path-key.ts
|
|
17
|
+
function pathKey({ env = _env, platform = _platform }) {
|
|
18
|
+
return platform === "win32" ? Object.keys(env).reverse().find((key) => key.toUpperCase() === "PATH") ?? "Path" : "PATH";
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/utils/resolve-command.ts
|
|
22
|
+
function enterCwd(cwd) {
|
|
23
|
+
if (cwd == null) return false;
|
|
24
|
+
try {
|
|
25
|
+
process.chdir(cwd);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function resolveCommandAttempt(parsed, withoutPathExt) {
|
|
32
|
+
const env = parsed.options.env ?? _env;
|
|
33
|
+
const cwd = parsed.options.cwd?.toString();
|
|
34
|
+
const switchCwd = process.chdir;
|
|
35
|
+
const switched = cwd != null && process.chdir !== void 0 && !switchCwd?.disabled && enterCwd(cwd);
|
|
36
|
+
try {
|
|
37
|
+
const resolved = which.sync(parsed.command, {
|
|
38
|
+
path: env[pathKey({ env })],
|
|
39
|
+
pathExt: withoutPathExt ? path.delimiter : void 0
|
|
40
|
+
});
|
|
41
|
+
return path.resolve(cwd ?? "", resolved);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
} finally {
|
|
45
|
+
if (switched) process.chdir(_cwd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolveCommand(parsed) {
|
|
49
|
+
return resolveCommandAttempt(parsed) ?? resolveCommandAttempt(parsed, true);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/enoent.ts
|
|
53
|
+
function notFoundError(original, syscall) {
|
|
54
|
+
return Object.assign(/* @__PURE__ */ new Error(`${syscall} ${original.command} ENOENT`), {
|
|
55
|
+
code: "ENOENT",
|
|
56
|
+
errno: -2,
|
|
57
|
+
syscall: `${syscall} ${original.command}`,
|
|
58
|
+
path: original.command,
|
|
59
|
+
spawnargs: original.args
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function verifyENOENT(status, parsed, syscall) {
|
|
63
|
+
if (isWin && status === 1 && !parsed.file) return notFoundError(parsed.original, syscall);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function hookChildProcess(cp, parsed) {
|
|
67
|
+
if (!isWin) return;
|
|
68
|
+
const originalEmit = cp.emit;
|
|
69
|
+
cp.emit = function(name, ...args) {
|
|
70
|
+
if (name === "exit") {
|
|
71
|
+
const status = args[0];
|
|
72
|
+
const err = verifyENOENT(status, parsed, "spawn");
|
|
73
|
+
if (err) return originalEmit.call(cp, "error", err);
|
|
74
|
+
}
|
|
75
|
+
return originalEmit.apply(cp, [name, ...args]);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/utils/shebang.ts
|
|
80
|
+
function shebangCommand(string) {
|
|
81
|
+
const match = string.match(shebangRegExp);
|
|
82
|
+
if (!match) return null;
|
|
83
|
+
const [path, argument] = match[0].replace(/^#!\s*/, "").split(/\s+/);
|
|
84
|
+
const binary = path.split("/").pop() ?? "";
|
|
85
|
+
return binary === "env" ? argument ?? binary : argument ? `${binary} ${argument}` : binary;
|
|
86
|
+
}
|
|
87
|
+
function readShebang(file) {
|
|
88
|
+
const buffer = Buffer.alloc(150);
|
|
89
|
+
try {
|
|
90
|
+
const fd = fs.openSync(file, "r");
|
|
91
|
+
fs.readSync(fd, buffer, 0, 150, 0);
|
|
92
|
+
fs.closeSync(fd);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return shebangCommand(buffer.toString());
|
|
97
|
+
}
|
|
98
|
+
function detectShebang(parsed) {
|
|
99
|
+
parsed.file = resolveCommand(parsed);
|
|
100
|
+
if (parsed.file) {
|
|
101
|
+
const shebang = readShebang(parsed.file);
|
|
102
|
+
if (shebang) {
|
|
103
|
+
parsed.args.unshift(parsed.file);
|
|
104
|
+
parsed.command = shebang;
|
|
105
|
+
return resolveCommand(parsed);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return parsed.file;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/utils/escapes.ts
|
|
112
|
+
function escapeLineBreaks(arg) {
|
|
113
|
+
return arg.replace(lineBreaksRegExp, "");
|
|
114
|
+
}
|
|
115
|
+
function escapeMetaChars(arg) {
|
|
116
|
+
return arg.replace(metaCharsRegExp, "^$1");
|
|
117
|
+
}
|
|
118
|
+
function escapeCommand(arg) {
|
|
119
|
+
return escapeMetaChars(escapeLineBreaks(arg));
|
|
120
|
+
}
|
|
121
|
+
function escapeArgument(arg, doubleEscapeMetaChars) {
|
|
122
|
+
let argStr = `${arg}`;
|
|
123
|
+
argStr = escapeLineBreaks(argStr);
|
|
124
|
+
argStr = argStr.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"");
|
|
125
|
+
argStr = argStr.replace(/(?=(\\+?)?)\1$/, "$1$1");
|
|
126
|
+
argStr = escapeMetaChars(`"${argStr}"`);
|
|
127
|
+
if (doubleEscapeMetaChars) argStr = escapeMetaChars(argStr);
|
|
128
|
+
return argStr;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/parse.ts
|
|
132
|
+
function parseNonShell(parsed) {
|
|
133
|
+
if (!isWin) return parsed;
|
|
134
|
+
const commandFile = detectShebang(parsed) ?? "";
|
|
135
|
+
const needsShell = !executableRegExp.test(commandFile);
|
|
136
|
+
if (parsed.options._forceShell ?? needsShell) {
|
|
137
|
+
const needsDoubleEscapeMetaChars = cmdShimRegExp.test(commandFile);
|
|
138
|
+
parsed.command = path.normalize(parsed.command);
|
|
139
|
+
parsed.command = escapeCommand(parsed.command);
|
|
140
|
+
parsed.args = parsed.args.map((arg) => escapeArgument(arg, needsDoubleEscapeMetaChars));
|
|
141
|
+
parsed.args = [
|
|
142
|
+
"/d",
|
|
143
|
+
"/s",
|
|
144
|
+
"/c",
|
|
145
|
+
`"${[parsed.command].concat(parsed.args).join(" ")}"`
|
|
146
|
+
];
|
|
147
|
+
parsed.command = _env.comspec ?? "cmd.exe";
|
|
148
|
+
parsed.options.windowsVerbatimArguments = true;
|
|
149
|
+
}
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
function parse(command, args, options) {
|
|
153
|
+
if (!Array.isArray(args)) {
|
|
154
|
+
options = args;
|
|
155
|
+
args = [];
|
|
156
|
+
}
|
|
157
|
+
const argsCopy = args.slice(0);
|
|
158
|
+
options = Object.assign({}, options);
|
|
159
|
+
const parsed = {
|
|
160
|
+
command,
|
|
161
|
+
args: argsCopy,
|
|
162
|
+
options,
|
|
163
|
+
file: void 0,
|
|
164
|
+
original: {
|
|
165
|
+
command,
|
|
166
|
+
args: argsCopy.slice(0)
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return options.shell ? parsed : parseNonShell(parsed);
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/index.ts
|
|
173
|
+
function spawn(command, args = [], options = {}) {
|
|
174
|
+
const parsed = parse(command, args, options);
|
|
175
|
+
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
176
|
+
hookChildProcess(spawned, parsed);
|
|
177
|
+
return spawned;
|
|
178
|
+
}
|
|
179
|
+
function spawnSync(command, args = [], options = {}) {
|
|
180
|
+
const parsed = parse(command, args, options);
|
|
181
|
+
const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
|
|
182
|
+
result.error = result.error ?? verifyENOENT(result.status, parsed, "spawnSync") ?? void 0;
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
const _parse = {
|
|
186
|
+
parseNonShell,
|
|
187
|
+
parse
|
|
188
|
+
};
|
|
189
|
+
const _enoent = {
|
|
190
|
+
notFoundError,
|
|
191
|
+
verifyENOENT,
|
|
192
|
+
hookChildProcess
|
|
193
|
+
};
|
|
194
|
+
const _utils = {
|
|
195
|
+
shebangCommand,
|
|
196
|
+
readShebang,
|
|
197
|
+
detectShebang,
|
|
198
|
+
enterCwd,
|
|
199
|
+
resolveCommand,
|
|
200
|
+
resolveCommandAttempt,
|
|
201
|
+
escapeLineBreaks,
|
|
202
|
+
escapeMetaChars,
|
|
203
|
+
escapeArgument,
|
|
204
|
+
escapeCommand,
|
|
205
|
+
pathKey
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
208
|
+
export { _enoent, _parse, _utils, spawn, spawnSync };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cross-spawn-esm",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "ESM version of cross-spawn package",
|
|
6
|
+
"author": "Timur Bikmuhametov <tim.bic22@gmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"cross-spawn",
|
|
10
|
+
"spawn",
|
|
11
|
+
"spawnSync",
|
|
12
|
+
"windows",
|
|
13
|
+
"cross-platform",
|
|
14
|
+
"path-ext",
|
|
15
|
+
"shebang",
|
|
16
|
+
"cmd",
|
|
17
|
+
"execute"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Timbic/cross-spawn-esm.git"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "tsdown --watch",
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"fmt": "prettier --cache --write .",
|
|
39
|
+
"fmt:chk": "prettier --check .",
|
|
40
|
+
"lint": "eslint --cache",
|
|
41
|
+
"lint:fix": "eslint --cache --fix",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"release": "bumpp",
|
|
44
|
+
"prepare": "husky"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"which": "7.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@timbic/eslint-config": "1.2.2",
|
|
51
|
+
"@timbic/prettier-config": "1.2.3",
|
|
52
|
+
"@types/node": "26.6.1",
|
|
53
|
+
"@types/which": "3.0.4",
|
|
54
|
+
"bumpp": "12.3.0",
|
|
55
|
+
"eslint": "10.10.0",
|
|
56
|
+
"husky": "9.1.7",
|
|
57
|
+
"lint-staged": "17.5.1",
|
|
58
|
+
"prettier": "3.9.6",
|
|
59
|
+
"tsdown": "0.23.0",
|
|
60
|
+
"typescript": "6.0.3"
|
|
61
|
+
},
|
|
62
|
+
"lint-staged": {
|
|
63
|
+
"*/**/*.{js,ts,md,json}": [
|
|
64
|
+
"pnpm fmt",
|
|
65
|
+
"pnpm lint:fix"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"packageManager": "pnpm@12.4.2"
|
|
69
|
+
}
|