cross-spawn-esm 1.0.0 → 1.1.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
@@ -5,9 +5,6 @@
5
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
6
  </p>
7
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
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
12
9
  and spawnSync ) which ports its codebase to modern ESM and TypeScript.
13
10
 
@@ -65,7 +62,7 @@ const result = spawnSync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit
65
62
 
66
63
  - Overall smaller bundle size
67
64
  - Tree-shaking friendly
68
- - Fewer dependencies
65
+ - Zero dependencies
69
66
  - No need for an additional types package (`@types/cross-spawn`)
70
67
  - Modern codebase and active maintenance
71
68
  - Better documentation
@@ -126,7 +123,7 @@ import { _parse, _enoent } from "cross-spawn-esm";
126
123
  >
127
124
  > **After**: const parsed = _parse.parse(...)
128
125
 
129
- - New `_utils` object exposes all the lower-level helpers (`shebangCommand`, `readShebang`, `detectShebang`, `enterCwd`, `resolveCommand`,
126
+ - New `_utils` object exposes all the lower-level helpers (`shebangCommand`, `readShebang`, `detectShebang`, `resolveCommand`,
130
127
  `resolveCommandAttempt`, `escapeLineBreaks`, `escapeMetaChars`, `escapeCommand`, `escapeArgument`, `pathKey`) If you were relying on the
131
128
  original **cross-spawn** dependencies ( **path-key** and **shebang-command** ), their improved versions can be found in `_utils`.
132
129
 
package/dist/index.d.ts CHANGED
@@ -13,10 +13,6 @@ interface ParsedCommand {
13
13
  args: ReadonlyArray<string>;
14
14
  };
15
15
  }
16
- /**
17
- * Change the process working directory to `cwd`.
18
- */
19
- declare function enterCwd(cwd?: string): boolean;
20
16
  /**
21
17
  * Resolve `parsed.command` to an absolute file path by searching the
22
18
  * environment's `PATH`, using the custom `cwd` when one is set.
@@ -222,7 +218,6 @@ export declare const _utils: {
222
218
  shebangCommand: typeof shebangCommand;
223
219
  readShebang: typeof readShebang;
224
220
  detectShebang: typeof detectShebang;
225
- enterCwd: typeof enterCwd;
226
221
  resolveCommand: typeof resolveCommand;
227
222
  resolveCommandAttempt: typeof resolveCommandAttempt;
228
223
  escapeLineBreaks: typeof escapeLineBreaks;
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import cp from "node:child_process";
2
2
  import path from "node:path";
3
- import which from "which";
4
3
  import fs from "node:fs";
5
4
  //#region src/utils/constants.ts
6
5
  const _cwd = process.cwd();
@@ -18,35 +17,72 @@ function pathKey({ env = _env, platform = _platform }) {
18
17
  return platform === "win32" ? Object.keys(env).reverse().find((key) => key.toUpperCase() === "PATH") ?? "Path" : "PATH";
19
18
  }
20
19
  //#endregion
21
- //#region src/utils/resolve-command.ts
22
- function enterCwd(cwd) {
23
- if (cwd == null) return false;
20
+ //#region src/utils/isexe.ts
21
+ const execOther = 1;
22
+ const execGroup = 8;
23
+ const execOwner = 64;
24
+ const execOwnerOrGroup = 72;
25
+ function isInPathExt(file, pathExt) {
26
+ const extensions = pathExt.split(path.delimiter);
27
+ if (extensions.includes("")) return true;
28
+ return extensions.some((ext) => {
29
+ const e = ext.toLowerCase();
30
+ return e !== "" && file.slice(-e.length).toLowerCase() === e;
31
+ });
32
+ }
33
+ function isExecutableMode(stat) {
34
+ const myUid = process.getuid?.();
35
+ const myGroups = process.getgroups?.() ?? [];
36
+ const myGid = process.getgid?.() ?? myGroups[0];
37
+ if (myUid === void 0 || myGid === void 0) throw new Error("cannot get uid or gid");
38
+ const groups = /* @__PURE__ */ new Set([myGid, ...myGroups]);
39
+ const { mode, uid, gid } = stat;
40
+ return !!(mode & execOther || mode & execGroup && groups.has(gid) || mode & execOwner && uid === myUid || mode & execOwnerOrGroup && myUid === 0);
41
+ }
42
+ function isexeSync(file, { pathExt = process.env.PATHEXT ?? "" } = {}) {
24
43
  try {
25
- process.chdir(cwd);
26
- return true;
44
+ const stat = fs.statSync(file);
45
+ if (!stat.isFile()) return false;
46
+ return isWin ? isInPathExt(file, pathExt) : isExecutableMode(stat);
27
47
  } catch {
28
48
  return false;
29
49
  }
30
50
  }
51
+ //#endregion
52
+ //#region src/utils/which.ts
53
+ const pathSeparatorRegExp = new RegExp(`[${path.posix.sep}${path.sep === path.posix.sep ? "" : path.sep}]`.replace(/(\\)/g, "\\$1"));
54
+ function whichSync(command, { cwd = _cwd, path: optPath = _env.PATH, pathExt: optPathExt = _env.PATHEXT } = {}) {
55
+ const delimiter = path.delimiter;
56
+ const dirs = command.match(pathSeparatorRegExp) ? [""] : [...isWin ? [cwd] : [], ...(optPath ?? "").split(delimiter)];
57
+ const pathExtExe = isWin ? optPathExt ?? [
58
+ ".EXE",
59
+ ".CMD",
60
+ ".BAT",
61
+ ".COM"
62
+ ].join(delimiter) : void 0;
63
+ const extensions = pathExtExe?.split(delimiter).flatMap((ext) => [ext, ext.toLowerCase()]) ?? [""];
64
+ if (isWin && command.includes(".") && extensions[0] !== "") extensions.unshift("");
65
+ for (const dir of dirs) {
66
+ const base = path.resolve(cwd, dir === "" ? command : path.join(dir.replace(/^"(.*)"$/, "$1"), command));
67
+ for (const ext of extensions) {
68
+ const candidate = base + ext;
69
+ if (isexeSync(candidate, { pathExt: pathExtExe })) return candidate;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ //#endregion
75
+ //#region src/utils/resolve-command.ts
31
76
  function resolveCommandAttempt(parsed, withoutPathExt) {
32
77
  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
- }
78
+ return whichSync(parsed.command, {
79
+ cwd: parsed.options.cwd?.toString(),
80
+ path: env[pathKey({ env })],
81
+ pathExt: withoutPathExt ? path.delimiter : void 0
82
+ });
47
83
  }
48
84
  function resolveCommand(parsed) {
49
- return resolveCommandAttempt(parsed) ?? resolveCommandAttempt(parsed, true);
85
+ return resolveCommandAttempt(parsed) ?? (isWin ? resolveCommandAttempt(parsed, true) : null);
50
86
  }
51
87
  //#endregion
52
88
  //#region src/enoent.ts
@@ -195,7 +231,6 @@ const _utils = {
195
231
  shebangCommand,
196
232
  readShebang,
197
233
  detectShebang,
198
- enterCwd,
199
234
  resolveCommand,
200
235
  resolveCommandAttempt,
201
236
  escapeLineBreaks,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cross-spawn-esm",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "description": "ESM version of cross-spawn package",
6
6
  "author": "Timur Bikmuhametov <tim.bic22@gmail.com>",
7
7
  "license": "MIT",
@@ -32,14 +32,10 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
- "dependencies": {
36
- "which": "7.0.0"
37
- },
38
35
  "devDependencies": {
39
36
  "@timbic/eslint-config": "1.2.2",
40
37
  "@timbic/prettier-config": "1.2.3",
41
38
  "@types/node": "26.6.1",
42
- "@types/which": "3.0.4",
43
39
  "bumpp": "12.3.0",
44
40
  "eslint": "10.10.0",
45
41
  "husky": "9.1.7",