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 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).
@@ -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
+ }