cross-spawn-esm 1.1.1 → 1.2.0

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/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
  This is a "fork" of [cross-spawn](https://www.npmjs.com/package/cross-spawn?activeTab=readme) ( a cross-platform solution to node's spawn
9
9
  and spawnSync ) which ports its codebase to modern ESM and TypeScript.
10
10
 
11
- 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
12
- original package. Please refer to the [Migration guide](#migration-guide) for further explanation.
11
+ This package is a drop-in replacement for **cross-spawn** which tries to behave the same as the original package. Please refer to the
12
+ [Migration guide](#migration-guide) for further explanation.
13
13
 
14
14
  ## Installation
15
15
 
@@ -60,7 +60,7 @@ const result = spawnSync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit
60
60
 
61
61
  ## Benefits
62
62
 
63
- - Overall smaller bundle size
63
+ - Overall smaller bundle size (**~50% smaller**)
64
64
  - Tree-shaking friendly
65
65
  - Zero dependencies
66
66
  - No need for an additional types package (`@types/cross-spawn`)
@@ -144,7 +144,7 @@ Beyond the API surface, a few implementation details intentionally differ:
144
144
 
145
145
  The following behaviors are intentionally kept identical to `cross-spawn`:
146
146
 
147
- - When `options.shell` is used, parsing, escaping, and shebang enhancements are disabled — matching both the original and Node.js behavior.
147
+ - When `options.shell` is used, parsing, escaping, and shebang enhancements are disabled - matching both the original and Node.js behavior.
148
148
  - Windows-only ENOENT detection: when the process exits with code `1` and the command could not be resolved, an `error` event (async) or
149
149
  `result.error` (sync) is produced.
150
150
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,38 @@
1
1
  import { ChildProcess, SpawnOptions, SpawnSyncOptions, SpawnSyncReturns } from "node:child_process";
2
2
  import { NonSharedBuffer } from "node:buffer";
3
+ //#region src/utils/escapes.d.ts
4
+ /**
5
+ * Remove line breaks (`\r` and `\n`) from the given string. Without this, an argument containing a newline could terminate the enclosing
6
+ * command early and inject an arbitrary second command into the shell.
7
+ *
8
+ * @example
9
+ * // Without escaping, cmd.exe would run two commands:
10
+ * // node -e "console.log('ok')
11
+ * // calc.exe"
12
+ * escapeLineBreaks("console.log('ok')\ncalc.exe");
13
+ * //=> "console.log('ok')calc.exe"
14
+ */
15
+ declare function escapeLineBreaks(arg: string): string;
16
+ /**
17
+ * 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
18
+ * by the shell, breaking the command or enabling arbitrary command execution.
19
+ *
20
+ * @example
21
+ * // Without escaping, cmd.exe would treat the spaces and `&` as separators:
22
+ * escapeMetaChars('notepad "a & b.txt"');
23
+ * //=> 'notepad^ ^"a^ ^&^ b.txt^"'
24
+ */
25
+ declare function escapeMetaChars(arg: string): string;
26
+ /**
27
+ * Escapes a command name for safe use in a Windows `cmd.exe` invocation.
28
+ */
29
+ declare function escapeCommand(arg: string): string;
30
+ /**
31
+ * Escape a single argument into a fully self-contained token for a Windows
32
+ * `cmd.exe` command line.
33
+ */
34
+ declare function escapeArgument(arg: unknown, doubleEscapeMetaChars?: boolean): string;
35
+ //#endregion
3
36
  //#region src/utils/resolve-command.d.ts
4
37
  interface ParsedCommand {
5
38
  command: string;
@@ -32,18 +65,25 @@ declare function resolveCommandAttempt(parsed: ParsedCommand, withoutPathExt?: b
32
65
  declare function resolveCommand(parsed: ParsedCommand): string | null;
33
66
  //#endregion
34
67
  //#region src/enoent.d.ts
68
+ interface SpawnError extends NodeJS.ErrnoException {
69
+ code: string;
70
+ errno: number;
71
+ path: string;
72
+ syscall: string;
73
+ spawnargs: ReadonlyArray<string>;
74
+ }
35
75
  /**
36
76
  * Builds an ENOENT error for a command that could not be found. Creates
37
77
  * an error whose shape matches what Node's own `spawn`/`spawnSync` would produce
38
78
  * when it fails to locate the command file. This makes the error interchangeable
39
79
  * with a real ENOENT from Node.
40
80
  */
41
- declare function notFoundError(original: ParsedCommand["original"], syscall: "spawn" | "spawnSync"): NodeJS.ErrnoException;
81
+ declare function notFoundError(original: ParsedCommand["original"], syscall: "spawn" | "spawnSync"): SpawnError;
42
82
  /**
43
83
  * Detects the Windows-only fake ENOENT that `cmd.exe` produces. On POSIX or
44
84
  * for any other exit code the error is not applicable, so `null` is returned.
45
85
  */
46
- declare function verifyENOENT(status: number | null, parsed: ParsedCommand, syscall: "spawn" | "spawnSync"): NodeJS.ErrnoException | null;
86
+ declare function verifyENOENT(status: number | null, parsed: ParsedCommand, syscall: "spawn" | "spawnSync"): SpawnError | null;
47
87
  /**
48
88
  * Hooks into a `ChildProcess` `exit` event so that a missing command is
49
89
  * surfaced as an `error` event instead of silently exiting with code `1`.
@@ -98,39 +138,6 @@ declare function readShebang(file: string): string | null;
98
138
  */
99
139
  declare function detectShebang(parsed: ParsedCommand): string | null;
100
140
  //#endregion
101
- //#region src/utils/escapes.d.ts
102
- /**
103
- * Remove line breaks (`\r` and `\n`) from the given string. Without this, an argument containing a newline could terminate the enclosing
104
- * command early and inject an arbitrary second command into the shell.
105
- *
106
- * @example
107
- * // Without escaping, cmd.exe would run two commands:
108
- * // node -e "console.log('ok')
109
- * // calc.exe"
110
- * escapeLineBreaks("console.log('ok')\ncalc.exe");
111
- * //=> "console.log('ok')calc.exe"
112
- */
113
- declare function escapeLineBreaks(arg: string): string;
114
- /**
115
- * 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
116
- * by the shell, breaking the command or enabling arbitrary command execution.
117
- *
118
- * @example
119
- * // Without escaping, cmd.exe would treat the spaces and `&` as separators:
120
- * escapeMetaChars('notepad "a & b.txt"');
121
- * //=> 'notepad^ ^"a^ ^&^ b.txt^"'
122
- */
123
- declare function escapeMetaChars(arg: string): string;
124
- /**
125
- * Escapes a command name for safe use in a Windows `cmd.exe` invocation.
126
- */
127
- declare function escapeCommand(arg: string): string;
128
- /**
129
- * Escape a single argument into a fully self-contained token for a Windows
130
- * `cmd.exe` command line.
131
- */
132
- declare function escapeArgument(arg: unknown, doubleEscapeMetaChars?: boolean): string;
133
- //#endregion
134
141
  //#region src/utils/path-key.d.ts
135
142
  interface PathKeyOptions {
136
143
  /**
@@ -139,14 +146,14 @@ interface PathKeyOptions {
139
146
  *
140
147
  * @see https://nodejs.org/api/process.html#process_process_env
141
148
  */
142
- env?: Record<string, string | undefined>;
149
+ readonly env?: NodeJS.ProcessEnv;
143
150
  /**
144
151
  * Get the PATH key for a specific platform.
145
152
  * @default process.platform
146
153
  *
147
154
  * @see https://nodejs.org/api/process.html#process_process_platform
148
155
  */
149
- platform?: NodeJS.Platform;
156
+ readonly platform?: NodeJS.Platform;
150
157
  }
151
158
  /**
152
159
  * Get the [PATH] environment variable key cross-platform.
@@ -159,7 +166,7 @@ interface PathKeyOptions {
159
166
  * const PATH = process.env[key];
160
167
  * //=> '/usr/local/bin:/usr/bin:/bin'
161
168
  */
162
- declare function pathKey({ env, platform }: PathKeyOptions): string;
169
+ declare function pathKey({ env, platform }?: PathKeyOptions): string;
163
170
  //#endregion
164
171
  //#region src/parse.d.ts
165
172
  /**
@@ -227,4 +234,4 @@ export declare const _utils: {
227
234
  pathKey: typeof pathKey;
228
235
  };
229
236
  //#endregion
230
- export type { ParsedCommand, PathKeyOptions };
237
+ export type { ParsedCommand, PathKeyOptions, SpawnError };
package/dist/index.js CHANGED
@@ -12,9 +12,51 @@ const metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g;
12
12
  const executableRegExp = /\.(?:exe)$/i;
13
13
  const cmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
14
14
  //#endregion
15
- //#region src/utils/path-key.ts
16
- function pathKey({ env = _env, platform = _platform }) {
17
- return platform === "win32" ? Object.keys(env).reverse().find((key) => key.toUpperCase() === "PATH") ?? "Path" : "PATH";
15
+ //#region src/utils/escapes.ts
16
+ function escapeLineBreaks(arg) {
17
+ return arg.replace(lineBreaksRegExp, "");
18
+ }
19
+ function escapeMetaChars(arg) {
20
+ return arg.replace(metaCharsRegExp, "^$1");
21
+ }
22
+ function escapeCommand(arg) {
23
+ return escapeMetaChars(escapeLineBreaks(arg));
24
+ }
25
+ function escapeArgument(arg, doubleEscapeMetaChars) {
26
+ let argStr = `${arg}`;
27
+ argStr = escapeLineBreaks(argStr);
28
+ argStr = argStr.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"");
29
+ argStr = argStr.replace(/(?=(\\+?)?)\1$/, "$1$1");
30
+ argStr = escapeMetaChars(`"${argStr}"`);
31
+ if (doubleEscapeMetaChars) argStr = escapeMetaChars(argStr);
32
+ return argStr;
33
+ }
34
+ //#endregion
35
+ //#region src/enoent.ts
36
+ function notFoundError(original, syscall) {
37
+ return Object.assign(/* @__PURE__ */ new Error(`${syscall} ${original.command} ENOENT`), {
38
+ code: "ENOENT",
39
+ errno: -2,
40
+ syscall: `${syscall} ${original.command}`,
41
+ path: original.command,
42
+ spawnargs: original.args
43
+ });
44
+ }
45
+ function verifyENOENT(status, parsed, syscall) {
46
+ if (isWin && status === 1 && !parsed.file) return notFoundError(parsed.original, syscall);
47
+ return null;
48
+ }
49
+ function hookChildProcess(cp, parsed) {
50
+ if (!isWin) return;
51
+ const originalEmit = cp.emit;
52
+ cp.emit = function(name, ...args) {
53
+ if (name === "exit") {
54
+ const status = args[0];
55
+ const err = verifyENOENT(status, parsed, "spawn");
56
+ if (err) return originalEmit.call(cp, "error", err);
57
+ }
58
+ return originalEmit.apply(cp, [name, ...args]);
59
+ };
18
60
  }
19
61
  //#endregion
20
62
  //#region src/utils/isexe.ts
@@ -39,7 +81,7 @@ function isExecutableMode(stat) {
39
81
  const { mode, uid, gid } = stat;
40
82
  return !!(mode & execOther || mode & execGroup && groups.has(gid) || mode & execOwner && uid === myUid || mode & execOwnerOrGroup && myUid === 0);
41
83
  }
42
- function isexeSync(file, { pathExt = process.env.PATHEXT ?? "" } = {}) {
84
+ function isexeSync(file, { pathExt = _env.PATHEXT ?? "" } = {}) {
43
85
  try {
44
86
  const stat = fs.statSync(file);
45
87
  if (!stat.isFile()) return false;
@@ -54,7 +96,7 @@ const pathSeparatorRegExp = new RegExp(`[${path.posix.sep}${path.sep === path.po
54
96
  function whichSync(command, { cwd = _cwd, path: optPath = _env.PATH, pathExt: optPathExt = _env.PATHEXT } = {}) {
55
97
  const delimiter = path.delimiter;
56
98
  const dirs = command.match(pathSeparatorRegExp) ? [""] : [...isWin ? [cwd] : [], ...(optPath ?? "").split(delimiter)];
57
- const pathExtExe = isWin ? optPathExt ?? [
99
+ const pathExtExe = isWin ? optPathExt || [
58
100
  ".EXE",
59
101
  ".CMD",
60
102
  ".BAT",
@@ -72,6 +114,11 @@ function whichSync(command, { cwd = _cwd, path: optPath = _env.PATH, pathExt: op
72
114
  return null;
73
115
  }
74
116
  //#endregion
117
+ //#region src/utils/path-key.ts
118
+ function pathKey({ env = _env, platform = _platform } = {}) {
119
+ return platform === "win32" ? Object.keys(env).reverse().find((key) => key.toUpperCase() === "PATH") ?? "Path" : "PATH";
120
+ }
121
+ //#endregion
75
122
  //#region src/utils/resolve-command.ts
76
123
  function resolveCommandAttempt(parsed, withoutPathExt) {
77
124
  const env = parsed.options.env ?? _env;
@@ -85,33 +132,6 @@ function resolveCommand(parsed) {
85
132
  return resolveCommandAttempt(parsed) ?? (isWin ? resolveCommandAttempt(parsed, true) : null);
86
133
  }
87
134
  //#endregion
88
- //#region src/enoent.ts
89
- function notFoundError(original, syscall) {
90
- return Object.assign(/* @__PURE__ */ new Error(`${syscall} ${original.command} ENOENT`), {
91
- code: "ENOENT",
92
- errno: -2,
93
- syscall: `${syscall} ${original.command}`,
94
- path: original.command,
95
- spawnargs: original.args
96
- });
97
- }
98
- function verifyENOENT(status, parsed, syscall) {
99
- if (isWin && status === 1 && !parsed.file) return notFoundError(parsed.original, syscall);
100
- return null;
101
- }
102
- function hookChildProcess(cp, parsed) {
103
- if (!isWin) return;
104
- const originalEmit = cp.emit;
105
- cp.emit = function(name, ...args) {
106
- if (name === "exit") {
107
- const status = args[0];
108
- const err = verifyENOENT(status, parsed, "spawn");
109
- if (err) return originalEmit.call(cp, "error", err);
110
- }
111
- return originalEmit.apply(cp, [name, ...args]);
112
- };
113
- }
114
- //#endregion
115
135
  //#region src/utils/shebang.ts
116
136
  function shebangCommand(string) {
117
137
  const match = string.match(shebangRegExp);
@@ -144,26 +164,6 @@ function detectShebang(parsed) {
144
164
  return parsed.file;
145
165
  }
146
166
  //#endregion
147
- //#region src/utils/escapes.ts
148
- function escapeLineBreaks(arg) {
149
- return arg.replace(lineBreaksRegExp, "");
150
- }
151
- function escapeMetaChars(arg) {
152
- return arg.replace(metaCharsRegExp, "^$1");
153
- }
154
- function escapeCommand(arg) {
155
- return escapeMetaChars(escapeLineBreaks(arg));
156
- }
157
- function escapeArgument(arg, doubleEscapeMetaChars) {
158
- let argStr = `${arg}`;
159
- argStr = escapeLineBreaks(argStr);
160
- argStr = argStr.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"");
161
- argStr = argStr.replace(/(?=(\\+?)?)\1$/, "$1$1");
162
- argStr = escapeMetaChars(`"${argStr}"`);
163
- if (doubleEscapeMetaChars) argStr = escapeMetaChars(argStr);
164
- return argStr;
165
- }
166
- //#endregion
167
167
  //#region src/parse.ts
168
168
  function parseNonShell(parsed) {
169
169
  if (!isWin) return parsed;
@@ -186,11 +186,11 @@ function parseNonShell(parsed) {
186
186
  return parsed;
187
187
  }
188
188
  function parse(command, args, options) {
189
- if (!Array.isArray(args)) {
189
+ if (args && !Array.isArray(args)) {
190
190
  options = args;
191
191
  args = [];
192
192
  }
193
- const argsCopy = args.slice(0);
193
+ const argsCopy = args ? args.slice(0) : [];
194
194
  options = Object.assign({}, options);
195
195
  const parsed = {
196
196
  command,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cross-spawn-esm",
3
3
  "type": "module",
4
- "version": "1.1.1",
4
+ "version": "1.2.0",
5
5
  "description": "ESM version of cross-spawn package",
6
6
  "author": "Timur Bikmuhametov <tim.bic22@gmail.com>",
7
7
  "license": "MIT",
@@ -35,14 +35,18 @@
35
35
  "devDependencies": {
36
36
  "@timbic/eslint-config": "1.2.2",
37
37
  "@timbic/prettier-config": "1.2.3",
38
- "@types/node": "26.6.1",
38
+ "@types/node": "26.6.4",
39
+ "@vitest/coverage-v8": "5.0.3",
39
40
  "bumpp": "12.3.0",
40
- "eslint": "10.10.0",
41
+ "eslint": "10.12.0",
41
42
  "husky": "9.1.7",
42
- "lint-staged": "17.5.1",
43
- "prettier": "3.9.6",
43
+ "lint-staged": "17.6.0",
44
+ "mkdirp": "3.0.1",
45
+ "prettier": "3.9.9",
46
+ "rimraf": "6.1.3",
44
47
  "tsdown": "0.23.0",
45
- "typescript": "6.0.3"
48
+ "typescript": "6.0.3",
49
+ "vitest": "5.0.3"
46
50
  },
47
51
  "lint-staged": {
48
52
  "*/**/*.{js,ts,md,json}": [
@@ -58,6 +62,9 @@
58
62
  "lint": "eslint --cache",
59
63
  "lint:fix": "eslint --cache --fix",
60
64
  "typecheck": "tsc --noEmit",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest",
67
+ "test:coverage": "vitest run --coverage",
61
68
  "release": "bumpp"
62
69
  }
63
70
  }