UNPKG

17.3 kBJavaScriptView Raw
1'use strict';
2cmdShim.ifExists = cmdShimIfExists;
3const util_1 = require("util");
4const path = require("path");
5const isWindows = require("is-windows");
6const CMD_EXTENSION = require("cmd-extension");
7const shebangExpr = /^#!\s*(?:\/usr\/bin\/env(?:\s+-S\s*)?)?\s*([^ \t]+)(.*)$/;
8const DEFAULT_OPTIONS = {
9 // Create PowerShell file by default if the option hasn't been specified
10 createPwshFile: true,
11 createCmdFile: isWindows(),
12 fs: require('graceful-fs')
13};
14/**
15 * Map from extensions of files that this module is frequently used for to their runtime.
16 * @type {Map<string, string>}
17 */
18const extensionToProgramMap = new Map([
19 ['.js', 'node'],
20 ['.cjs', 'node'],
21 ['.mjs', 'node'],
22 ['.cmd', 'cmd'],
23 ['.bat', 'cmd'],
24 ['.ps1', 'pwsh'],
25 ['.sh', 'sh']
26]);
27function ingestOptions(opts) {
28 const opts_ = { ...DEFAULT_OPTIONS, ...opts };
29 const fs = opts_.fs;
30 opts_.fs_ = {
31 chmod: fs.chmod ? (0, util_1.promisify)(fs.chmod) : (async () => { }),
32 mkdir: (0, util_1.promisify)(fs.mkdir),
33 readFile: (0, util_1.promisify)(fs.readFile),
34 stat: (0, util_1.promisify)(fs.stat),
35 unlink: (0, util_1.promisify)(fs.unlink),
36 writeFile: (0, util_1.promisify)(fs.writeFile)
37 };
38 return opts_;
39}
40/**
41 * Try to create shims.
42 *
43 * @param src Path to program (executable or script).
44 * @param to Path to shims.
45 * Don't add an extension if you will create multiple types of shims.
46 * @param opts Options.
47 * @throws If `src` is missing.
48 */
49async function cmdShim(src, to, opts) {
50 const opts_ = ingestOptions(opts);
51 await cmdShim_(src, to, opts_);
52}
53/**
54 * Try to create shims.
55 *
56 * Does nothing if `src` doesn't exist.
57 *
58 * @param src Path to program (executable or script).
59 * @param to Path to shims.
60 * Don't add an extension if you will create multiple types of shims.
61 * @param opts Options.
62 */
63function cmdShimIfExists(src, to, opts) {
64 return cmdShim(src, to, opts).catch(() => { });
65}
66/**
67 * Try to unlink, but ignore errors.
68 * Any problems will surface later.
69 *
70 * @param path File to be removed.
71 */
72function rm(path, opts) {
73 return opts.fs_.unlink(path).catch(() => { });
74}
75/**
76 * Try to create shims **even if `src` is missing**.
77 *
78 * @param src Path to program (executable or script).
79 * @param to Path to shims.
80 * Don't add an extension if you will create multiple types of shims.
81 * @param opts Options.
82 */
83async function cmdShim_(src, to, opts) {
84 const srcRuntimeInfo = await searchScriptRuntime(src, opts);
85 // Always tries to create all types of shims by calling `writeAllShims` as of now.
86 // Append your code here to change the behavior in response to `srcRuntimeInfo`.
87 // Create 3 shims for (Ba)sh in Cygwin / MSYS, no extension) & CMD (.cmd) & PowerShell (.ps1)
88 await writeShimsPreCommon(to, opts);
89 return writeAllShims(src, to, srcRuntimeInfo, opts);
90}
91/**
92 * Do processes before **all** shims are created.
93 * This must be called **only once** for one call of `cmdShim(IfExists)`.
94 *
95 * @param target Path of shims that are going to be created.
96 */
97function writeShimsPreCommon(target, opts) {
98 return opts.fs_.mkdir(path.dirname(target), { recursive: true });
99}
100/**
101 * Write all types (sh & cmd & pwsh) of shims to files.
102 * Extensions (`.cmd` and `.ps1`) are appended to cmd and pwsh shims.
103 *
104 *
105 * @param src Path to program (executable or script).
106 * @param to Path to shims **without extensions**.
107 * Extensions are added for CMD and PowerShell shims.
108 * @param srcRuntimeInfo Return value of `await searchScriptRuntime(src)`.
109 * @param opts Options.
110 */
111function writeAllShims(src, to, srcRuntimeInfo, opts) {
112 const opts_ = ingestOptions(opts);
113 const generatorAndExts = [{ generator: generateShShim, extension: '' }];
114 if (opts_.createCmdFile) {
115 generatorAndExts.push({ generator: generateCmdShim, extension: CMD_EXTENSION });
116 }
117 if (opts_.createPwshFile) {
118 generatorAndExts.push({ generator: generatePwshShim, extension: '.ps1' });
119 }
120 return Promise.all(generatorAndExts.map((generatorAndExt) => writeShim(src, to + generatorAndExt.extension, srcRuntimeInfo, generatorAndExt.generator, opts_)));
121}
122/**
123 * Do processes before writing shim.
124 *
125 * @param target Path to shim that is going to be created.
126 */
127function writeShimPre(target, opts) {
128 return rm(target, opts);
129}
130/**
131 * Do processes after writing the shim.
132 *
133 * @param target Path to just created shim.
134 */
135function writeShimPost(target, opts) {
136 // Only chmoding shims as of now.
137 // Some other processes may be appended.
138 return chmodShim(target, opts);
139}
140/**
141 * Look into runtime (e.g. `node` & `sh` & `pwsh`) and its arguments
142 * of the target program (script or executable).
143 *
144 * @param target Path to the executable or script.
145 * @return Promise of infomation of runtime of `target`.
146 */
147async function searchScriptRuntime(target, opts) {
148 try {
149 const data = await opts.fs_.readFile(target, 'utf8');
150 // First, check if the bin is a #! of some sort.
151 const firstLine = data.trim().split(/\r*\n/)[0];
152 const shebang = firstLine.match(shebangExpr);
153 if (!shebang) {
154 // If not, infer script type from its extension.
155 // If the inference fails, it's something that'll be compiled, or some other
156 // sort of script, and just call it directly.
157 const targetExtension = path.extname(target).toLowerCase();
158 return {
159 // undefined if extension is unknown but it's converted to null.
160 program: extensionToProgramMap.get(targetExtension) || null,
161 additionalArgs: ''
162 };
163 }
164 return {
165 program: shebang[1],
166 additionalArgs: shebang[2]
167 };
168 }
169 catch (err) {
170 if (!isWindows() || err.code !== 'ENOENT')
171 throw err;
172 if (await opts.fs_.stat(`${target}${getExeExtension()}`)) {
173 return {
174 program: null,
175 additionalArgs: '',
176 };
177 }
178 throw err;
179 }
180}
181function getExeExtension() {
182 let cmdExtension;
183 if (process.env.PATHEXT) {
184 cmdExtension = process.env.PATHEXT
185 .split(path.delimiter)
186 .find(ext => ext.toLowerCase() === '.exe');
187 }
188 return cmdExtension || '.exe';
189}
190/**
191 * Write shim to the file system while executing the pre- and post-processes
192 * defined in `WriteShimPre` and `WriteShimPost`.
193 *
194 * @param src Path to the executable or script.
195 * @param to Path to the (sh) shim(s) that is going to be created.
196 * @param srcRuntimeInfo Result of `await searchScriptRuntime(src)`.
197 * @param generateShimScript Generator of shim script.
198 * @param opts Other options.
199 */
200async function writeShim(src, to, srcRuntimeInfo, generateShimScript, opts) {
201 const defaultArgs = opts.preserveSymlinks ? '--preserve-symlinks' : '';
202 // `Array.prototype.filter` removes ''.
203 // ['--foo', '--bar'].join(' ') and [].join(' ') returns '--foo --bar' and '' respectively.
204 const args = [srcRuntimeInfo.additionalArgs, defaultArgs].filter(arg => arg).join(' ');
205 opts = Object.assign({}, opts, {
206 prog: srcRuntimeInfo.program,
207 args: args
208 });
209 await writeShimPre(to, opts);
210 await opts.fs_.writeFile(to, generateShimScript(src, to, opts), 'utf8');
211 return writeShimPost(to, opts);
212}
213/**
214 * Generate the content of a shim for CMD.
215 *
216 * @param src Path to the executable or script.
217 * @param to Path to the shim to be created.
218 * It is highly recommended to end with `.cmd` (or `.bat`).
219 * @param opts Options.
220 * @return The content of shim.
221 */
222function generateCmdShim(src, to, opts) {
223 // `shTarget` is not used to generate the content.
224 const shTarget = path.relative(path.dirname(to), src);
225 let target = shTarget.split('/').join('\\');
226 const quotedPathToTarget = path.isAbsolute(target) ? `"${target}"` : `"%~dp0\\${target}"`;
227 let longProg;
228 let prog = opts.prog;
229 let args = opts.args || '';
230 const nodePath = normalizePathEnvVar(opts.nodePath).win32;
231 const prependToPath = normalizePathEnvVar(opts.prependToPath).win32;
232 if (!prog) {
233 prog = quotedPathToTarget;
234 args = '';
235 target = '';
236 }
237 else if (prog === 'node' && opts.nodeExecPath) {
238 prog = `"${opts.nodeExecPath}"`;
239 target = quotedPathToTarget;
240 }
241 else {
242 longProg = `"%~dp0\\${prog}.exe"`;
243 target = quotedPathToTarget;
244 }
245 let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : '';
246 // @IF EXIST "%~dp0\node.exe" (
247 // "%~dp0\node.exe" "%~dp0\.\node_modules\npm\bin\npm-cli.js" %*
248 // ) ELSE (
249 // SETLOCAL
250 // SET PATHEXT=%PATHEXT:;.JS;=;%
251 // node "%~dp0\.\node_modules\npm\bin\npm-cli.js" %*
252 // )
253 let cmd = '@SETLOCAL\r\n';
254 if (prependToPath) {
255 cmd += `@SET "PATH=${prependToPath}:%PATH%"\r\n`;
256 }
257 if (nodePath) {
258 cmd += `\
259@IF NOT DEFINED NODE_PATH (\r
260 @SET "NODE_PATH=${nodePath}"\r
261) ELSE (\r
262 @SET "NODE_PATH=${nodePath};%NODE_PATH%"\r
263)\r
264`;
265 }
266 if (longProg) {
267 cmd += `\
268@IF EXIST ${longProg} (\r
269 ${longProg} ${args} ${target} ${progArgs}%*\r
270) ELSE (\r
271 @SET PATHEXT=%PATHEXT:;.JS;=;%\r
272 ${prog} ${args} ${target} ${progArgs}%*\r
273)\r
274`;
275 }
276 else {
277 cmd += `@${prog} ${args} ${target} ${progArgs}%*\r\n`;
278 }
279 return cmd;
280}
281/**
282 * Generate the content of a shim for (Ba)sh in, for example, Cygwin and MSYS(2).
283 *
284 * @param src Path to the executable or script.
285 * @param to Path to the shim to be created.
286 * It is highly recommended to end with `.sh` or to contain no extension.
287 * @param opts Options.
288 * @return The content of shim.
289 */
290function generateShShim(src, to, opts) {
291 let shTarget = path.relative(path.dirname(to), src);
292 let shProg = opts.prog && opts.prog.split('\\').join('/');
293 let shLongProg;
294 shTarget = shTarget.split('\\').join('/');
295 const quotedPathToTarget = path.isAbsolute(shTarget) ? `"${shTarget}"` : `"$basedir/${shTarget}"`;
296 let args = opts.args || '';
297 const shNodePath = normalizePathEnvVar(opts.nodePath).posix;
298 if (!shProg) {
299 shProg = quotedPathToTarget;
300 args = '';
301 shTarget = '';
302 }
303 else if (opts.prog === 'node' && opts.nodeExecPath) {
304 shProg = `"${opts.nodeExecPath}"`;
305 shTarget = quotedPathToTarget;
306 }
307 else {
308 shLongProg = `"$basedir/${opts.prog}"`;
309 shTarget = quotedPathToTarget;
310 }
311 let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : '';
312 // #!/bin/sh
313 // basedir=`dirname "$0"`
314 //
315 // case `uname` in
316 // *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
317 // esac
318 //
319 // export NODE_PATH="<nodepath>"
320 //
321 // if [ -x "$basedir/node.exe" ]; then
322 // exec "$basedir/node.exe" "$basedir/node_modules/npm/bin/npm-cli.js" "$@"
323 // else
324 // exec node "$basedir/node_modules/npm/bin/npm-cli.js" "$@"
325 // fi
326 let sh = `\
327#!/bin/sh
328basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')")
329
330case \`uname\` in
331 *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;;
332esac
333
334`;
335 if (opts.prependToPath) {
336 sh += `\
337export PATH="${opts.prependToPath}:$PATH"
338`;
339 }
340 if (shNodePath) {
341 sh += `\
342if [ -z "$NODE_PATH" ]; then
343 export NODE_PATH="${shNodePath}"
344else
345 export NODE_PATH="${shNodePath}:$NODE_PATH"
346fi
347`;
348 }
349 if (shLongProg) {
350 sh += `\
351if [ -x ${shLongProg} ]; then
352 exec ${shLongProg} ${args} ${shTarget} ${progArgs}"$@"
353else
354 exec ${shProg} ${args} ${shTarget} ${progArgs}"$@"
355fi
356`;
357 }
358 else {
359 sh += `\
360${shProg} ${args} ${shTarget} ${progArgs}"$@"
361exit $?
362`;
363 }
364 return sh;
365}
366/**
367 * Generate the content of a shim for PowerShell.
368 *
369 * @param src Path to the executable or script.
370 * @param to Path to the shim to be created.
371 * It is highly recommended to end with `.ps1`.
372 * @param opts Options.
373 * @return The content of shim.
374 */
375function generatePwshShim(src, to, opts) {
376 let shTarget = path.relative(path.dirname(to), src);
377 const shProg = opts.prog && opts.prog.split('\\').join('/');
378 let pwshProg = shProg && `"${shProg}$exe"`;
379 let pwshLongProg;
380 shTarget = shTarget.split('\\').join('/');
381 const quotedPathToTarget = path.isAbsolute(shTarget) ? `"${shTarget}"` : `"$basedir/${shTarget}"`;
382 let args = opts.args || '';
383 let normalizedNodePathEnvVar = normalizePathEnvVar(opts.nodePath);
384 const nodePath = normalizedNodePathEnvVar.win32;
385 const shNodePath = normalizedNodePathEnvVar.posix;
386 let normalizedPrependPathEnvVar = normalizePathEnvVar(opts.prependToPath);
387 const prependPath = normalizedPrependPathEnvVar.win32;
388 const shPrependPath = normalizedPrependPathEnvVar.posix;
389 if (!pwshProg) {
390 pwshProg = quotedPathToTarget;
391 args = '';
392 shTarget = '';
393 }
394 else if (opts.prog === 'node' && opts.nodeExecPath) {
395 pwshProg = `"${opts.nodeExecPath}"`;
396 shTarget = quotedPathToTarget;
397 }
398 else {
399 pwshLongProg = `"$basedir/${opts.prog}$exe"`;
400 shTarget = quotedPathToTarget;
401 }
402 let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : '';
403 // #!/usr/bin/env pwsh
404 // $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
405 //
406 // $ret=0
407 // $exe = ""
408 // if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
409 // # Fix case when both the Windows and Linux builds of Node
410 // # are installed in the same directory
411 // $exe = ".exe"
412 // }
413 // if (Test-Path "$basedir/node") {
414 // # Support pipeline input
415 // if ($MyInvocation.ExpectingInput) {
416 // $input | & "$basedir/node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
417 // } else {
418 // & "$basedir/node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
419 // }
420 // $ret=$LASTEXITCODE
421 // } else {
422 // # Support pipeline input
423 // if ($MyInvocation.ExpectingInput) {
424 // $input | & "node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
425 // } else {
426 // & "node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
427 // }
428 // $ret=$LASTEXITCODE
429 // }
430 // exit $ret
431 let pwsh = `\
432#!/usr/bin/env pwsh
433$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
434
435$exe=""
436${(nodePath || prependPath) ? '$pathsep=":"\n' : ''}\
437${nodePath ? `\
438$env_node_path=$env:NODE_PATH
439$new_node_path="${nodePath}"
440` : ''}\
441${prependPath ? `\
442$env_path=$env:PATH
443$prepend_path="${prependPath}"
444` : ''}\
445if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
446 # Fix case when both the Windows and Linux builds of Node
447 # are installed in the same directory
448 $exe=".exe"
449${(nodePath || prependPath) ? ' $pathsep=";"\n' : ''}\
450}`;
451 if (shNodePath || shPrependPath) {
452 pwsh += `\
453 else {
454${shNodePath ? ` $new_node_path="${shNodePath}"\n` : ''}\
455${shPrependPath ? ` $prepend_path="${shPrependPath}"\n` : ''}\
456}
457`;
458 }
459 if (shNodePath) {
460 pwsh += `\
461if ([string]::IsNullOrEmpty($env_node_path)) {
462 $env:NODE_PATH=$new_node_path
463} else {
464 $env:NODE_PATH="$new_node_path$pathsep$env_node_path"
465}
466`;
467 }
468 if (opts.prependToPath) {
469 pwsh += `
470$env:PATH="$prepend_path$pathsep$env:PATH"
471`;
472 }
473 if (pwshLongProg) {
474 pwsh += `
475$ret=0
476if (Test-Path ${pwshLongProg}) {
477 # Support pipeline input
478 if ($MyInvocation.ExpectingInput) {
479 $input | & ${pwshLongProg} ${args} ${shTarget} ${progArgs}$args
480 } else {
481 & ${pwshLongProg} ${args} ${shTarget} ${progArgs}$args
482 }
483 $ret=$LASTEXITCODE
484} else {
485 # Support pipeline input
486 if ($MyInvocation.ExpectingInput) {
487 $input | & ${pwshProg} ${args} ${shTarget} ${progArgs}$args
488 } else {
489 & ${pwshProg} ${args} ${shTarget} ${progArgs}$args
490 }
491 $ret=$LASTEXITCODE
492}
493${nodePath ? '$env:NODE_PATH=$env_node_path\n' : ''}\
494${prependPath ? '$env:PATH=$env_path\n' : ''}\
495exit $ret
496`;
497 }
498 else {
499 pwsh += `
500# Support pipeline input
501if ($MyInvocation.ExpectingInput) {
502 $input | & ${pwshProg} ${args} ${shTarget} ${progArgs}$args
503} else {
504 & ${pwshProg} ${args} ${shTarget} ${progArgs}$args
505}
506${nodePath ? '$env:NODE_PATH=$env_node_path\n' : ''}\
507${prependPath ? '$env:PATH=$env_path\n' : ''}\
508exit $LASTEXITCODE
509`;
510 }
511 return pwsh;
512}
513/**
514 * Chmod just created shim and make it executable
515 *
516 * @param to Path to shim.
517 */
518function chmodShim(to, opts) {
519 return opts.fs_.chmod(to, 0o755);
520}
521function normalizePathEnvVar(nodePath) {
522 if (!nodePath || !nodePath.length) {
523 return {
524 win32: '',
525 posix: ''
526 };
527 }
528 let split = (typeof nodePath === 'string' ? nodePath.split(path.delimiter) : Array.from(nodePath));
529 let result = {};
530 for (let i = 0; i < split.length; i++) {
531 const win32 = split[i].split('/').join('\\');
532 const posix = isWindows() ? split[i].split('\\').join('/').replace(/^([^:\\/]*):/, (_, $1) => `/mnt/${$1.toLowerCase()}`) : split[i];
533 result.win32 = result.win32 ? `${result.win32};${win32}` : win32;
534 result.posix = result.posix ? `${result.posix}:${posix}` : posix;
535 result[i] = { win32, posix };
536 }
537 return result;
538}
539module.exports = cmdShim;
540//# sourceMappingURL=index.js.map
\No newline at end of file