unwrap-npm-cmd 2.1.7 → 2.1.8

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
@@ -1,5 +1,16 @@
1
1
  # unwrap-npm-cmd
2
2
 
3
+ [![License][license-image]][license-url]
4
+ [![build][build-image]][build-url]
5
+
6
+ [![Downloads][downloads-image]][downloads-url]
7
+
8
+ [![npm badge][npm-badge-png]][package-url]
9
+
10
+ - [API reference](https://fynjs.pages.dev/api/modules/unwrap-npm-cmd) - TypeDoc
11
+ - [Markdown reference](docs/reference.md) - the full API and runtime behavior in one file. Also ships in the package.
12
+ - [GitHub](https://github.com/jchip/fynjs/tree/main/packages/unwrap-npm-cmd)
13
+
3
14
  Unwrap npm's node.js bin CMD batch for js files on Windows.
4
15
 
5
16
  [Sample](./test/fixtures/sample.js):
@@ -55,3 +66,12 @@ unwrapNpmCmd(cmd, options);
55
66
  # License
56
67
 
57
68
  Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
69
+
70
+ [license-image]: https://img.shields.io/npm/l/unwrap-npm-cmd.svg
71
+ [license-url]: LICENSE
72
+ [build-image]: https://github.com/jchip/fynjs/actions/workflows/ci.yml/badge.svg
73
+ [build-url]: https://github.com/jchip/fynjs/actions/workflows/ci.yml
74
+ [downloads-image]: https://img.shields.io/npm/dm/unwrap-npm-cmd.svg
75
+ [downloads-url]: https://npm-stat.com/charts.html?package=unwrap-npm-cmd
76
+ [npm-badge-png]: https://nodei.co/npm/unwrap-npm-cmd.png?downloads=true&stars=true
77
+ [package-url]: https://npmjs.com/package/unwrap-npm-cmd
@@ -36,6 +36,9 @@ export function resolveNpmCmd(exe, options) {
36
36
  }
37
37
  // update JS script from batch file
38
38
  const a = nodeCmd.split(" ").filter((x) => x)[1];
39
+ if (!a) {
40
+ return quote(resolvedExe);
41
+ }
39
42
  const b = a.replace(`%~dp0`, resolvedDir).replace(`%dp0%`, resolvedDir);
40
43
  let jsFile;
41
44
  if (nodeJsVer < 18) {
package/dist/utils.js CHANGED
@@ -3,5 +3,8 @@ export const unquote = (x) => x.trim().replace(/^['"]+|['"]+$/g, "");
3
3
  export const quote = (x) => (!x.startsWith(`"`) ? `"${x}"` : x);
4
4
  export const relative = (x, cwd) => {
5
5
  const r = Path.relative(cwd || process.cwd(), unquote(x));
6
+ // across drives on Windows, Path.relative returns an absolute path
7
+ if (Path.isAbsolute(r))
8
+ return r;
6
9
  return r.startsWith(".") ? r : `.${Path.sep}${r}`;
7
10
  };
@@ -0,0 +1,169 @@
1
+ # unwrap-npm-cmd reference
2
+
3
+ `unwrap-npm-cmd` rewrites a command string so that, on Windows, an npm-generated `.cmd` batch shim is replaced by `node.exe` plus the real JS file the shim runs. That lets you spawn the command without a shell and without the batch wrapper. On non-Windows platforms `unwrapNpmCmd` returns its input unchanged. The package has one runtime dependency, `which`. The source is ESM (`"type": "module"`) and a CJS shim is provided.
4
+
5
+ ## Imports
6
+
7
+ ```js
8
+ import unwrapNpmCmd, { unwrapNpmCmd, resolveNpmCmd, quote, unquote, relative } from "unwrap-npm-cmd";
9
+ import type { UnwrapOptions, ResolveResult, ResolveOptions } from "unwrap-npm-cmd";
10
+ ```
11
+
12
+ ```js
13
+ const unwrapNpmCmd = require("unwrap-npm-cmd");
14
+ const { resolveNpmCmd, quote, unquote, relative } = require("unwrap-npm-cmd");
15
+ ```
16
+
17
+ There are no subpaths (only `unwrap-npm-cmd/package.json`).
18
+
19
+ Runtime exports: `default` (same function as `unwrapNpmCmd`), `unwrapNpmCmd`, `resolveNpmCmd`, `quote`, `unquote`, `relative`.
20
+
21
+ Type-only exports: `UnwrapOptions`, `ResolveResult`, `ResolveOptions`.
22
+
23
+ CJS: `index.cjs` does `require("./dist/index.js")` and exports `Object.assign(m.default, m)`. So `require("unwrap-npm-cmd")` is the `unwrapNpmCmd` function itself, with `unwrapNpmCmd`, `default`, `resolveNpmCmd`, `quote`, `unquote` and `relative` copied onto it as properties. This mutates the ESM default function object. It relies on `require()` of an ES module, so it needs a Node version that supports that (the `engines` field is `^22.22.2 || ^24.15.0 || >=26.0.0`).
24
+
25
+ ## `unwrapNpmCmd(cmd, options?)`
26
+
27
+ ```ts
28
+ interface UnwrapOptions {
29
+ path?: string;
30
+ relative?: boolean;
31
+ cwd?: string;
32
+ jsOnly?: boolean;
33
+ }
34
+
35
+ function unwrapNpmCmd(
36
+ cmd: string,
37
+ options: UnwrapOptions = { path: process.env.PATH }
38
+ ): string
39
+
40
+ export default unwrapNpmCmd;
41
+ ```
42
+
43
+ Returns a command string. Never throws for a command that cannot be resolved; it returns `cmd` unchanged.
44
+
45
+ Algorithm:
46
+
47
+ 1. If `process.platform !== "win32"`, return `cmd` unchanged. `options` is ignored.
48
+ 2. Split `cmd` on the single space character `" "`. The first piece is the executable name. The rest are kept as is.
49
+ 3. Resolve the executable name with `resolveNpmCmd(name, options)`. If it throws (for example `which` finds nothing), return `cmd` unchanged.
50
+ 4. If `resolveNpmCmd` returned a string (the found path, quoted), replace the first piece with that string.
51
+ 5. If it returned `{ jsFile }`:
52
+ - With `options.relative` truthy, `jsFile = relative(jsFile, options.cwd)`.
53
+ - With `options.jsOnly` truthy, the replacement is `quote(jsFile)`.
54
+ - Otherwise the replacement is `quote(process.execPath) + " " + quote(jsFile)`.
55
+ 6. Join the replacement and the remaining pieces with single spaces and return.
56
+
57
+ The returned string is `cmd` itself when the replacement equals the first piece. Remaining arguments are never parsed, quoted or changed.
58
+
59
+ ### Options
60
+
61
+ | option | type | default | behavior |
62
+ | --- | --- | --- | --- |
63
+ | `path` | `string` | `process.env.PATH`, only when `options` is omitted entirely | Search path passed to `which` in place of `PATH`. If you pass an options object without `path`, `which` falls back to its own default (`process.env.PATH`). Also used as the cache key (see below). |
64
+ | `relative` | `boolean` | falsy | Convert the JS file path to a path relative to `cwd` using `relative()`. Applies only when a batch shim was unwrapped. |
65
+ | `cwd` | `string` | `process.cwd()` | Base directory for `relative`. Has no effect without `relative`. |
66
+ | `jsOnly` | `boolean` | falsy | Return only the quoted JS file path, without `node.exe`. Applies only when a batch shim was unwrapped. |
67
+
68
+ The whole options object is also passed to `which.sync`, so any `which` option present on it (such as `pathExt`) is honored. Only the four fields above are declared in `UnwrapOptions`.
69
+
70
+ ### Which command strings are unwrapped
71
+
72
+ Only the first space-separated token is examined, so `"npm test"` looks up `npm` and keeps `test`. Outcomes for that token on Windows:
73
+
74
+ | the token resolves to | result |
75
+ | --- | --- |
76
+ | a `.cmd` file (extension compared case-insensitively) with a recognized node launch line | node.exe plus JS file, or the JS file alone with `jsOnly` |
77
+ | a `.cmd` file with no recognized launch line | the full path of the `.cmd`, quoted |
78
+ | any file without a `.cmd` extension (such as `.exe`) | the full path as found by `which`, quoted. `relative` and `jsOnly` are ignored for this case |
79
+ | nothing found, or any other error while resolving | `cmd` unchanged |
80
+
81
+ Because only a plain space splits the string, a command with a quoted executable path containing spaces, or with tab separators, is not tokenized properly. The first token is then looked up literally and normally fails, so `cmd` comes back unchanged.
82
+
83
+ ### Caching
84
+
85
+ Results are cached in a module-level object keyed by `options.path || ""` and then by the executable name. A cache hit skips `which` and the batch file read. Details:
86
+
87
+ - The cache stores the result of `resolveNpmCmd` (string or `{ jsFile }`), before `relative` and `jsOnly` are applied. Different `relative`, `cwd` and `jsOnly` values therefore share an entry.
88
+ - Failures are cached too, as the bare name. A command that was not found keeps coming back unchanged for that `path` even if it is installed later.
89
+ - There is no invalidation and no exported way to clear the cache.
90
+ - `options.path` is the key, not `process.env.PATH`. A call with `options` omitted uses the `PATH` value at that call as the key.
91
+
92
+ ### Examples
93
+
94
+ ```js
95
+ unwrapNpmCmd("npm test");
96
+ // "C:\...\node.exe" "C:\...\node_modules\npm\bin\npm-cli.js" test
97
+ unwrapNpmCmd("npx mocha", { relative: true });
98
+ // "C:\...\node.exe" ".\...\npx-cli.js" mocha
99
+ unwrapNpmCmd("mocha test", { jsOnly: true });
100
+ // "C:\...\node_modules\mocha\bin\mocha" test
101
+ unwrapNpmCmd(`find "name" package.json`);
102
+ // "C:\WINDOWS\system32\find.EXE" "name" package.json
103
+ ```
104
+
105
+ ## `resolveNpmCmd(exe, options?)`
106
+
107
+ ```ts
108
+ interface ResolveResult {
109
+ jsFile: string;
110
+ }
111
+
112
+ interface ResolveOptions {
113
+ path?: string;
114
+ }
115
+
116
+ function resolveNpmCmd(exe: string, options?: ResolveOptions): string | ResolveResult
117
+ ```
118
+
119
+ The lower-level resolver used by `unwrapNpmCmd`. It does no platform check and no caching, and it does not catch errors.
120
+
121
+ Steps:
122
+
123
+ 1. `which.sync(exe, options)` finds the executable. `options` is passed straight through, so `path` and other `which` options apply. If `which` finds nothing it throws, and `resolveNpmCmd` lets that propagate.
124
+ 2. If the found path does not end in `.cmd` (case-insensitive), return `quote(foundPath)`. This is a string, not an object.
125
+ 3. Otherwise read the file and split it on `"\n"`, trimming each line (so `\r` is removed).
126
+ 4. Pick the line that holds the node launch, by the lower-cased base name of the `.cmd` file:
127
+ - `npm.cmd`: the first line starting with `SET "NPM_CLI_JS=`. The text `NPM_CLI_JS=` is removed from that line.
128
+ - `npx.cmd`: the first line starting with `SET "NPX_CLI_JS=`. The text `NPX_CLI_JS=` is removed.
129
+ - any other `.cmd`: the first line starting with `"%~dp0\node.exe"`, else the first line starting with `"%_prog%"`.
130
+ 5. If no line matched, return `quote(foundPath)` (the `.cmd` path itself).
131
+ 6. Take the second space-separated token of the chosen line, with empty tokens ignored. For npm/npx that is the quoted JS path left after removing `SET`. For other shims it is the token after `"%~dp0\node.exe"` or `"%_prog%"`, such as `"%dp0%\node_modules\mocha\bin\mocha"`.
132
+ 7. In that token, replace the first `%~dp0` and the first `%dp0%` with the directory of the `.cmd` file. Each replacement changes only the first occurrence.
133
+ 8. Normalize with `Path.normalize`, strip surrounding quotes with `unquote`, then wrap with `quote`, and return `{ jsFile }`. `jsFile` is therefore quoted and normalized. (A code branch for Node older than 18 skips the `unquote` and `quote` steps, but `engines` excludes those versions.)
134
+
135
+ Failure behavior: throws if `which` finds nothing, if reading the `.cmd` file fails, or if the chosen line has no second token (the `.replace` call runs on `undefined` and throws a `TypeError`). A missing launch line is not a failure; it returns the quoted `.cmd` path.
136
+
137
+ Limits: the line match is a prefix test on the trimmed line. The second token is found by splitting on spaces, so a JS path that contains a space inside the batch file is cut at the space. Only the first launch line is used. On non-Windows platforms the function still runs. It finds executables with `which` and returns the quoted path, since non-Windows commands rarely have a `.cmd` extension.
138
+
139
+ ## `quote(x)`
140
+
141
+ ```ts
142
+ const quote: (x: string) => string
143
+ ```
144
+
145
+ Returns `x` unchanged if it already starts with `"`. Otherwise returns `"` + `x` + `"`. Only the start is checked, so `"abc` is returned as is. No escaping is done. An empty string becomes `""`.
146
+
147
+ ## `unquote(x)`
148
+
149
+ ```ts
150
+ const unquote: (x: string) => string
151
+ ```
152
+
153
+ Trims whitespace, then removes every run of `'` and `"` characters from the start and the end. Single and double quotes are both removed, and mixed runs such as `'"x"'` are fully removed. Inner quotes are kept.
154
+
155
+ ## `relative(x, cwd?)`
156
+
157
+ ```ts
158
+ const relative: (x: string, cwd?: string) => string
159
+ ```
160
+
161
+ Returns the path of `unquote(x)` relative to `cwd` (default `process.cwd()`), using `path.relative` of the running platform. The result is not quoted. If the relative path does not begin with `.`, the prefix `.` + `path.sep` is added (`.\` on Windows, `./` elsewhere). The check is only on the first character, so a name such as `.bin\x` is left without an extra prefix. When `path.relative` returns an absolute path (different drive on Windows), the prefix is still added, giving a path like `.\D:\x`.
162
+
163
+ ## `UnwrapOptions`, `ResolveResult`, `ResolveOptions`
164
+
165
+ Type-only exports. Their definitions are shown under `unwrapNpmCmd` and `resolveNpmCmd`. `ResolveOptions` declares only `path`.
166
+
167
+ ## Notes on the README
168
+
169
+ The README option table matches the code. The README does not mention that the cache ignores `relative`, `cwd` and `jsOnly`, that failures are cached, or the exported `resolveNpmCmd`, `quote`, `unquote` and `relative` helpers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unwrap-npm-cmd",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
4
4
  "description": "Unwrap npm's node.js bin CMD batch for js files on Windows",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "index.cjs",
29
+ "docs/reference.md",
29
30
  "dist"
30
31
  ],
31
32
  "keywords": [
@@ -38,7 +39,7 @@
38
39
  ],
39
40
  "author": "Joel Chen",
40
41
  "license": "Apache-2.0",
41
- "homepage": "https://github.com/jchip/fynjs/tree/main/packages/unwrap-npm-cmd",
42
+ "homepage": "https://fynjs.pages.dev/api/modules/unwrap-npm-cmd",
42
43
  "bugs": {
43
44
  "url": "https://github.com/jchip/fynjs/issues"
44
45
  },
@@ -55,7 +56,7 @@
55
56
  "@types/which": "^3.0.4",
56
57
  "@vitest/coverage-v8": "^5.0.0",
57
58
  "prettier": "^3.5.3",
58
- "publish-util": "^3.1.7",
59
+ "publish-util": "^3.1.8",
59
60
  "typescript": "^7.0.2",
60
61
  "vite": "^8.2.2",
61
62
  "vitest": "^5.0.0"