-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresolve.ts
More file actions
451 lines (437 loc) · 17.6 KB
/
resolve.ts
File metadata and controls
451 lines (437 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
/**
* @fileoverview Resolve a binary path to the underlying script file.
*
* Wrapper-script unwrapping: a binary like `npm` is rarely an executable
* — on Windows it's `npm.cmd` / `npm.ps1` / extensionless `npm` shell
* script. Each wrapper variant has a fixed format generated by either
* the npm CLI build (npm/npx specific) or by `cmd-shim` for arbitrary
* package binaries. We pattern-match each format and extract the
* relative path to the underlying `.js` so callers can spawn Node with
* that path directly (cheaper than re-launching a shell wrapper).
*
* Volta managed installs: Volta layers binaries through
* `~/.volta/tools/{image,user}/...` with version-pinned subdirs. We
* detect the `.volta/` segment in the path and walk into the image
* directory to find the real CLI script.
*
* Caching: per-Volta-key cache + a final `realpathSync` to dedupe
* symlinked installations. Both caches live in `_internal.ts` so
* `which.ts` can flush them when its own cache eviction fires.
*
* Cycles with `which.ts`: `resolveRealBinSync` calls `whichRealSync`
* to handle relative input paths. ESM tolerates the cycle since both
* sides expose only functions (no eager top-level evaluation that
* needs the other module's bindings).
*/
import { WIN32 } from '../constants/platform'
import { readJsonSync } from '../fs/read-json'
import { normalizePath } from '../paths/normalize'
import { StringPrototypeStartsWith } from '../primordials/string'
import { getFs, getPath, voltaBinCache } from './_internal'
import { whichRealSync } from './which'
/*@__NO_SIDE_EFFECTS__*/
/**
* Resolve a binary path to the real underlying script file.
* Handles Windows .cmd wrappers and Unix shell scripts, resolving them to the actual .js files they execute.
*
* @example
* ```typescript
* const realPath = resolveRealBinSync('/usr/local/bin/npm')
* // e.g. '/usr/local/lib/node_modules/npm/bin/npm-cli.js'
* ```
*/
export function resolveRealBinSync(binPath: string): string {
const fs = getFs()
const path = getPath()
// If it's not an absolute path, try to find it in PATH first
if (!path.isAbsolute(binPath)) {
try {
const resolved = whichRealSync(binPath)
if (resolved) {
binPath = resolved as string
}
} catch {}
}
// Normalize the path once for consistent pattern matching.
binPath = normalizePath(binPath)
// Handle empty string that normalized to '.' (current directory)
if (binPath === '.') {
return binPath
}
const ext = path.extname(binPath)
const extLowered = ext.toLowerCase()
const basename = path.basename(binPath, ext)
const voltaIndex =
basename === 'node' ? -1 : (/(?<=\/)\.volta\//i.exec(binPath)?.index ?? -1)
if (voltaIndex !== -1) {
const voltaPath = binPath.slice(0, voltaIndex)
// Check Volta cache first - keyed by volta path + binary name.
const voltaCacheKey = `${voltaPath}:${basename}`
const cachedVolta = voltaBinCache.get(voltaCacheKey)
// Cache hit fires on second resolveRealBinSync call for the same
// Volta key; tests exercise distinct keys. The npm-CLI cascade
// (voltaImage/npm/<ver> → voltaImage/node/<ver>/lib/node_modules/npm)
// and the .cmd extension fallback are tested on Windows runners.
/* c8 ignore start */
if (cachedVolta) {
if (fs.existsSync(cachedVolta)) {
return cachedVolta
}
voltaBinCache.delete(voltaCacheKey)
}
/* c8 ignore stop */
const voltaToolsPath = path.join(voltaPath, 'tools')
const voltaImagePath = path.join(voltaToolsPath, 'image')
const voltaUserPath = path.join(voltaToolsPath, 'user')
const voltaPlatform = readJsonSync(
path.join(voltaUserPath, 'platform.json'),
{ throws: false },
) as { node?: { runtime?: string; npm?: string } } | null
const voltaNodeVersion = voltaPlatform?.node?.runtime
const voltaNpmVersion = voltaPlatform?.node?.npm
let voltaBinPath = ''
/* c8 ignore start */
if (
basename === 'npm' ||
basename === 'npx' // # socket-hook: allow npx
) {
if (voltaNpmVersion) {
const relCliPath = `bin/${basename}-cli.js`
voltaBinPath = path.join(
voltaImagePath,
`npm/${voltaNpmVersion}/${relCliPath}`,
)
if (voltaNodeVersion && !fs.existsSync(voltaBinPath)) {
voltaBinPath = path.join(
voltaImagePath,
`node/${voltaNodeVersion}/lib/node_modules/npm/${relCliPath}`,
)
if (!fs.existsSync(voltaBinPath)) {
voltaBinPath = ''
}
}
}
} else {
const voltaUserBinPath = path.join(voltaUserPath, 'bin')
const binInfo = readJsonSync(
path.join(voltaUserBinPath, `${basename}.json`),
{ throws: false },
) as { package?: string } | null
const binPackage = binInfo?.package
if (binPackage) {
voltaBinPath = path.join(
voltaImagePath,
`packages/${binPackage}/bin/${basename}`,
)
if (!fs.existsSync(voltaBinPath)) {
voltaBinPath = `${voltaBinPath}.cmd`
if (!fs.existsSync(voltaBinPath)) {
voltaBinPath = ''
}
}
}
}
/* c8 ignore stop */
if (voltaBinPath) {
let resolvedVoltaPath = voltaBinPath
try {
resolvedVoltaPath = normalizePath(fs.realpathSync.native(voltaBinPath))
} catch {}
// Cache the resolved Volta path.
voltaBinCache.set(voltaCacheKey, resolvedVoltaPath)
return resolvedVoltaPath
}
}
/* c8 ignore start - Windows-only wrapper-script resolution; tested
on Windows runners. The whole `if (WIN32)` block parses npm/npx/ // socket-hook: allow npx
pnpm/yarn .cmd/.bat/.ps1 shims to extract the underlying CLI JS
path. Unreachable on macOS/Linux. */
if (WIN32) {
const hasKnownExt =
extLowered === '' ||
extLowered === '.cmd' ||
extLowered === '.exe' ||
extLowered === '.ps1'
const isNpmOrNpx = basename === 'npm' || basename === 'npx' // # socket-hook: allow npx
const isPnpmOrYarn = basename === 'pnpm' || basename === 'yarn'
if (hasKnownExt && isNpmOrNpx) {
// The quick route assumes a bin path like: C:\Program Files\nodejs\npm.cmd
const quickPath = path.join(
path.dirname(binPath),
`node_modules/npm/bin/${basename}-cli.js`,
)
if (fs.existsSync(quickPath)) {
try {
return fs.realpathSync.native(quickPath)
} catch {}
return quickPath
}
}
let relPath = ''
if (
hasKnownExt &&
// Only parse shell scripts and batch files, not actual executables.
// .exe files are already executables and don't need path resolution from wrapper scripts.
extLowered !== '.exe' &&
// Check if file exists before attempting to read it to avoid ENOENT errors.
fs.existsSync(binPath)
) {
const source = fs.readFileSync(binPath, 'utf8')
if (isNpmOrNpx) {
if (extLowered === '.cmd') {
// "npm.cmd" and "npx.cmd" defined by
// https://github.com/npm/cli/blob/v11.4.2/bin/npm.cmd
// https://github.com/npm/cli/blob/v11.4.2/bin/npx.cmd
relPath =
basename === 'npm'
? /(?<="NPM_CLI_JS=%~dp0\\).*(?=")/.exec(source)?.[0] || ''
: /(?<="NPX_CLI_JS=%~dp0\\).*(?=")/.exec(source)?.[0] || ''
} else if (extLowered === '') {
// Extensionless "npm" and "npx" defined by
// https://github.com/npm/cli/blob/v11.4.2/bin/npm
// https://github.com/npm/cli/blob/v11.4.2/bin/npx
relPath =
basename === 'npm'
? /(?<=NPM_CLI_JS="\$CLI_BASEDIR\/).*(?=")/.exec(source)?.[0] ||
''
: /(?<=NPX_CLI_JS="\$CLI_BASEDIR\/).*(?=")/.exec(source)?.[0] ||
''
} else if (extLowered === '.ps1') {
// "npm.ps1" and "npx.ps1" defined by
// https://github.com/npm/cli/blob/v11.4.2/bin/npm.ps1
// https://github.com/npm/cli/blob/v11.4.2/bin/npx.ps1
relPath =
basename === 'npm'
? /(?<=\$NPM_CLI_JS="\$PSScriptRoot\/).*(?=")/.exec(
source,
)?.[0] || ''
: /(?<=\$NPX_CLI_JS="\$PSScriptRoot\/).*(?=")/.exec(
source,
)?.[0] || ''
}
} else if (isPnpmOrYarn) {
if (extLowered === '.cmd') {
// pnpm.cmd and yarn.cmd can have different formats depending on installation method
// Common formats include:
// 1. Setup-pnpm action format: node "%~dp0\..\pnpm\bin\pnpm.cjs" %*
// 2. npm install -g pnpm format: similar to cmd-shim
// 3. Standalone installer format: various patterns
// Try setup-pnpm/setup-yarn action format first
relPath =
/(?<=node\s+")%~dp0\\([^"]+)(?="\s+%\*)/.exec(source)?.[1] || ''
// Try alternative format: "%~dp0\node.exe" "%~dp0\..\package\bin\binary.js" %*
if (!relPath) {
relPath =
/(?<="%~dp0\\[^"]*node[^"]*"\s+")%~dp0\\([^"]+)(?="\s+%\*)/.exec(
source,
)?.[1] || ''
}
// Try cmd-shim format as fallback
if (!relPath) {
relPath = /(?<="%dp0%\\).*(?=" %\*\r\n)/.exec(source)?.[0] || ''
}
} else if (extLowered === '') {
// Extensionless pnpm/yarn - try common shebang formats
// Handle pnpm installed via standalone installer or global install
// Format: exec "$basedir/node" "$basedir/.tools/pnpm/VERSION/..." "$@"
// Note: may have multiple spaces between arguments
relPath =
/(?<="\$basedir\/)\.tools\/pnpm\/[^"]+(?="\s+"\$@")/.exec(
source,
)?.[0] || ''
if (!relPath) {
// Also try: exec node "$basedir/.tools/pnpm/VERSION/..." "$@"
relPath =
/(?<=exec\s+node\s+"\$basedir\/)\.tools\/pnpm\/[^"]+(?="\s+"\$@")/.exec(
source,
)?.[0] || ''
}
if (!relPath) {
// Try standard cmd-shim format: exec node "$basedir/../package/bin/binary.js" "$@"
relPath = /(?<="\$basedir\/).*(?=" "\$@"\n)/.exec(source)?.[0] || ''
}
} else if (extLowered === '.ps1') {
// PowerShell format
relPath = /(?<="\$basedir\/).*(?=" $args\n)/.exec(source)?.[0] || ''
}
} else if (extLowered === '.cmd') {
// "bin.CMD" generated by
// https://github.com/npm/cmd-shim/blob/v7.0.0/lib/index.js#L98:
//
// @ECHO off
// GOTO start
// :find_dp0
// SET dp0=%~dp0
// EXIT /b
// :start
// SETLOCAL
// CALL :find_dp0
//
// IF EXIST "%dp0%\node.exe" (
// SET "_prog=%dp0%\node.exe"
// ) ELSE (
// SET "_prog=node"
// SET PATHEXT=%PATHEXT:;.JS;=;%
// )
//
// endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\<PACKAGE_NAME>\path\to\bin.js" %*
relPath = /(?<="%dp0%\\).*(?=" %\*\r\n)/.exec(source)?.[0] || ''
} else if (extLowered === '') {
// Extensionless "bin" generated by
// https://github.com/npm/cmd-shim/blob/v7.0.0/lib/index.js#L138:
//
// #!/bin/sh
// basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
//
// case `uname` in
// *CYGWIN*|*MINGW*|*MSYS*)
// if command -v cygpath > /dev/null 2>&1; then
// basedir=`cygpath -w "$basedir"`
// fi
// ;;
// esac
//
// if [ -x "$basedir/node" ]; then
// exec "$basedir/node" "$basedir/../<PACKAGE_NAME>/path/to/bin.js" "$@"
// else
// exec node "$basedir/../<PACKAGE_NAME>/path/to/bin.js" "$@"
// fi
relPath = /(?<="$basedir\/).*(?=" "\$@"\n)/.exec(source)?.[0] || ''
} else if (extLowered === '.ps1') {
// "bin.PS1" generated by
// https://github.com/npm/cmd-shim/blob/v7.0.0/lib/index.js#L192:
//
// #!/usr/bin/env pwsh
// $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
//
// $exe=""
// if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
// # Fix case when both the Windows and Linux builds of Node
// # are installed in the same directory
// $exe=".exe"
// }
// $ret=0
// if (Test-Path "$basedir/node$exe") {
// # Support pipeline input
// if ($MyInvocation.ExpectingInput) {
// $input | & "$basedir/node$exe" "$basedir/../<PACKAGE_NAME>/path/to/bin.js" $args
// } else {
// & "$basedir/node$exe" "$basedir/../<PACKAGE_NAME>/path/to/bin.js" $args
// }
// $ret=$LASTEXITCODE
// } else {
// # Support pipeline input
// if ($MyInvocation.ExpectingInput) {
// $input | & "node$exe" "$basedir/../<PACKAGE_NAME>/path/to/bin.js" $args
// } else {
// & "node$exe" "$basedir/../<PACKAGE_NAME>/path/to/bin.js" $args
// }
// $ret=$LASTEXITCODE
// }
// exit $ret
relPath = /(?<="\$basedir\/).*(?=" $args\n)/.exec(source)?.[0] || ''
}
if (relPath) {
binPath = normalizePath(path.resolve(path.dirname(binPath), relPath))
}
}
} else {
// Handle Unix shell scripts (non-Windows platforms)
let hasNoExt = extLowered === ''
const isPnpmOrYarn = basename === 'pnpm' || basename === 'yarn'
const isNpmOrNpx = basename === 'npm' || basename === 'npx' // # socket-hook: allow npx
// Handle special case where pnpm path in CI has extra segments.
// In setup-pnpm GitHub Action, the path might be malformed like:
// /home/user/setup-pnpm/node_modules/.bin/pnpm/bin/pnpm.cjs
// This happens when the shell script contains a relative path that
// when resolved, creates an invalid nested structure.
if (isPnpmOrYarn && binPath.includes('/.bin/pnpm/bin/')) {
// Extract the correct pnpm bin path.
const binIndex = binPath.indexOf('/.bin/pnpm')
if (binIndex !== -1) {
// Get the base path up to /.bin/pnpm.
const baseBinPath = binPath.slice(0, binIndex + '/.bin/pnpm'.length)
// Need stat (not existsSync) — the discriminator is
// `isFile()` because the malformed pnpm path could resolve
// to a directory. The lint rule recommends existsSync, but
// that loses the file-vs-directory signal.
try {
// oxlint-disable-next-line socket/prefer-exists-sync
const stats = fs.statSync(baseBinPath)
// Only use this path if it's a file (the shell script).
if (stats.isFile()) {
binPath = normalizePath(baseBinPath)
// Recompute hasNoExt since we changed the path.
hasNoExt = !path.extname(binPath)
}
} catch {
// If stat fails, continue with the original path.
}
}
}
if (
hasNoExt &&
(isPnpmOrYarn || isNpmOrNpx) &&
// For extensionless files (Unix shell scripts), verify existence before reading.
// This prevents ENOENT errors when the bin path doesn't exist.
fs.existsSync(binPath)
) {
const source = fs.readFileSync(binPath, 'utf8')
let relPath = ''
if (isPnpmOrYarn) {
// Handle pnpm/yarn Unix shell scripts.
// Format: exec "$basedir/node" "$basedir/.tools/pnpm/VERSION/..." "$@"
// or: exec node "$basedir/.tools/pnpm/VERSION/..." "$@"
relPath =
/(?<="\$basedir\/)\.tools\/[^"]+(?="\s+"\$@")/.exec(source)?.[0] || ''
if (!relPath) {
// Try standard cmd-shim format: exec node "$basedir/../package/bin/binary.js" "$@"
// Example: exec node "$basedir/../pnpm/bin/pnpm.cjs" "$@"
// ^^^^^^^^^^^^^^^^^^^^^ captures this part
// This regex needs to be more careful to not match "$@" at the end.
relPath =
/(?<="\$basedir\/)[^"]+(?="\s+"\$@")/.exec(source)?.[0] || ''
}
// Special case for setup-pnpm GitHub Action which may use a different format.
// The setup-pnpm action creates a shell script that references ../pnpm/bin/pnpm.cjs
if (!relPath) {
// Try to match: exec node "$basedir/../pnpm/bin/pnpm.cjs" "$@"
const match = /exec\s+node\s+"?\$basedir\/([^"]+)"?\s+"\$@"/.exec(
source,
)
if (match) {
relPath = match[1] || ''
}
}
// Check if the extracted path looks wrong (e.g., pnpm/bin/pnpm.cjs without ../).
// This happens with setup-pnpm action when it creates a malformed shell script.
if (
relPath &&
basename === 'pnpm' &&
StringPrototypeStartsWith(relPath, 'pnpm/')
) {
// The path should be ../pnpm/... not pnpm/...
// Prepend ../ to fix the relative path.
relPath = `../${relPath}`
}
} else if (isNpmOrNpx) {
// Handle npm/npx Unix shell scripts
relPath =
basename === 'npm'
? /(?<=NPM_CLI_JS="\$CLI_BASEDIR\/).*(?=")/.exec(source)?.[0] || ''
: /(?<=NPX_CLI_JS="\$CLI_BASEDIR\/).*(?=")/.exec(source)?.[0] || ''
}
if (relPath) {
// Resolve the relative path to handle .. segments properly.
binPath = normalizePath(path.resolve(path.dirname(binPath), relPath))
}
}
}
/* c8 ignore stop */
try {
const realPath = fs.realpathSync.native(binPath)
return normalizePath(realPath)
} catch {}
// Return normalized path even if realpath fails.
return normalizePath(binPath)
}