UNPKG

@zkochan/cmd-shim

Version:

Used in pnpm for command line application support

540 lines (537 loc) 17.3 kB
'use strict'; cmdShim.ifExists = cmdShimIfExists; const util_1 = require("util"); const path = require("path"); const isWindows = require("is-windows"); const CMD_EXTENSION = require("cmd-extension"); const shebangExpr = /^#!\s*(?:\/usr\/bin\/env(?:\s+-S\s*)?)?\s*([^ \t]+)(.*)$/; const DEFAULT_OPTIONS = { // Create PowerShell file by default if the option hasn't been specified createPwshFile: true, createCmdFile: isWindows(), fs: require('graceful-fs') }; /** * Map from extensions of files that this module is frequently used for to their runtime. * @type {Map<string, string>} */ const extensionToProgramMap = new Map([ ['.js', 'node'], ['.cjs', 'node'], ['.mjs', 'node'], ['.cmd', 'cmd'], ['.bat', 'cmd'], ['.ps1', 'pwsh'], ['.sh', 'sh'] ]); function ingestOptions(opts) { const opts_ = { ...DEFAULT_OPTIONS, ...opts }; const fs = opts_.fs; opts_.fs_ = { chmod: fs.chmod ? (0, util_1.promisify)(fs.chmod) : (async () => { }), mkdir: (0, util_1.promisify)(fs.mkdir), readFile: (0, util_1.promisify)(fs.readFile), stat: (0, util_1.promisify)(fs.stat), unlink: (0, util_1.promisify)(fs.unlink), writeFile: (0, util_1.promisify)(fs.writeFile) }; return opts_; } /** * Try to create shims. * * @param src Path to program (executable or script). * @param to Path to shims. * Don't add an extension if you will create multiple types of shims. * @param opts Options. * @throws If `src` is missing. */ async function cmdShim(src, to, opts) { const opts_ = ingestOptions(opts); await cmdShim_(src, to, opts_); } /** * Try to create shims. * * Does nothing if `src` doesn't exist. * * @param src Path to program (executable or script). * @param to Path to shims. * Don't add an extension if you will create multiple types of shims. * @param opts Options. */ function cmdShimIfExists(src, to, opts) { return cmdShim(src, to, opts).catch(() => { }); } /** * Try to unlink, but ignore errors. * Any problems will surface later. * * @param path File to be removed. */ function rm(path, opts) { return opts.fs_.unlink(path).catch(() => { }); } /** * Try to create shims **even if `src` is missing**. * * @param src Path to program (executable or script). * @param to Path to shims. * Don't add an extension if you will create multiple types of shims. * @param opts Options. */ async function cmdShim_(src, to, opts) { const srcRuntimeInfo = await searchScriptRuntime(src, opts); // Always tries to create all types of shims by calling `writeAllShims` as of now. // Append your code here to change the behavior in response to `srcRuntimeInfo`. // Create 3 shims for (Ba)sh in Cygwin / MSYS, no extension) & CMD (.cmd) & PowerShell (.ps1) await writeShimsPreCommon(to, opts); return writeAllShims(src, to, srcRuntimeInfo, opts); } /** * Do processes before **all** shims are created. * This must be called **only once** for one call of `cmdShim(IfExists)`. * * @param target Path of shims that are going to be created. */ function writeShimsPreCommon(target, opts) { return opts.fs_.mkdir(path.dirname(target), { recursive: true }); } /** * Write all types (sh & cmd & pwsh) of shims to files. * Extensions (`.cmd` and `.ps1`) are appended to cmd and pwsh shims. * * * @param src Path to program (executable or script). * @param to Path to shims **without extensions**. * Extensions are added for CMD and PowerShell shims. * @param srcRuntimeInfo Return value of `await searchScriptRuntime(src)`. * @param opts Options. */ function writeAllShims(src, to, srcRuntimeInfo, opts) { const opts_ = ingestOptions(opts); const generatorAndExts = [{ generator: generateShShim, extension: '' }]; if (opts_.createCmdFile) { generatorAndExts.push({ generator: generateCmdShim, extension: CMD_EXTENSION }); } if (opts_.createPwshFile) { generatorAndExts.push({ generator: generatePwshShim, extension: '.ps1' }); } return Promise.all(generatorAndExts.map((generatorAndExt) => writeShim(src, to + generatorAndExt.extension, srcRuntimeInfo, generatorAndExt.generator, opts_))); } /** * Do processes before writing shim. * * @param target Path to shim that is going to be created. */ function writeShimPre(target, opts) { return rm(target, opts); } /** * Do processes after writing the shim. * * @param target Path to just created shim. */ function writeShimPost(target, opts) { // Only chmoding shims as of now. // Some other processes may be appended. return chmodShim(target, opts); } /** * Look into runtime (e.g. `node` & `sh` & `pwsh`) and its arguments * of the target program (script or executable). * * @param target Path to the executable or script. * @return Promise of infomation of runtime of `target`. */ async function searchScriptRuntime(target, opts) { try { const data = await opts.fs_.readFile(target, 'utf8'); // First, check if the bin is a #! of some sort. const firstLine = data.trim().split(/\r*\n/)[0]; const shebang = firstLine.match(shebangExpr); if (!shebang) { // If not, infer script type from its extension. // If the inference fails, it's something that'll be compiled, or some other // sort of script, and just call it directly. const targetExtension = path.extname(target).toLowerCase(); return { // undefined if extension is unknown but it's converted to null. program: extensionToProgramMap.get(targetExtension) || null, additionalArgs: '' }; } return { program: shebang[1], additionalArgs: shebang[2] }; } catch (err) { if (!isWindows() || err.code !== 'ENOENT') throw err; if (await opts.fs_.stat(`${target}${getExeExtension()}`)) { return { program: null, additionalArgs: '', }; } throw err; } } function getExeExtension() { let cmdExtension; if (process.env.PATHEXT) { cmdExtension = process.env.PATHEXT .split(path.delimiter) .find(ext => ext.toLowerCase() === '.exe'); } return cmdExtension || '.exe'; } /** * Write shim to the file system while executing the pre- and post-processes * defined in `WriteShimPre` and `WriteShimPost`. * * @param src Path to the executable or script. * @param to Path to the (sh) shim(s) that is going to be created. * @param srcRuntimeInfo Result of `await searchScriptRuntime(src)`. * @param generateShimScript Generator of shim script. * @param opts Other options. */ async function writeShim(src, to, srcRuntimeInfo, generateShimScript, opts) { const defaultArgs = opts.preserveSymlinks ? '--preserve-symlinks' : ''; // `Array.prototype.filter` removes ''. // ['--foo', '--bar'].join(' ') and [].join(' ') returns '--foo --bar' and '' respectively. const args = [srcRuntimeInfo.additionalArgs, defaultArgs].filter(arg => arg).join(' '); opts = Object.assign({}, opts, { prog: srcRuntimeInfo.program, args: args }); await writeShimPre(to, opts); await opts.fs_.writeFile(to, generateShimScript(src, to, opts), 'utf8'); return writeShimPost(to, opts); } /** * Generate the content of a shim for CMD. * * @param src Path to the executable or script. * @param to Path to the shim to be created. * It is highly recommended to end with `.cmd` (or `.bat`). * @param opts Options. * @return The content of shim. */ function generateCmdShim(src, to, opts) { // `shTarget` is not used to generate the content. const shTarget = path.relative(path.dirname(to), src); let target = shTarget.split('/').join('\\'); const quotedPathToTarget = path.isAbsolute(target) ? `"${target}"` : `"%~dp0\\${target}"`; let longProg; let prog = opts.prog; let args = opts.args || ''; const nodePath = normalizePathEnvVar(opts.nodePath).win32; const prependToPath = normalizePathEnvVar(opts.prependToPath).win32; if (!prog) { prog = quotedPathToTarget; args = ''; target = ''; } else if (prog === 'node' && opts.nodeExecPath) { prog = `"${opts.nodeExecPath}"`; target = quotedPathToTarget; } else { longProg = `"%~dp0\\${prog}.exe"`; target = quotedPathToTarget; } let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : ''; // @IF EXIST "%~dp0\node.exe" ( // "%~dp0\node.exe" "%~dp0\.\node_modules\npm\bin\npm-cli.js" %* // ) ELSE ( // SETLOCAL // SET PATHEXT=%PATHEXT:;.JS;=;% // node "%~dp0\.\node_modules\npm\bin\npm-cli.js" %* // ) let cmd = '@SETLOCAL\r\n'; if (prependToPath) { cmd += `@SET "PATH=${prependToPath}:%PATH%"\r\n`; } if (nodePath) { cmd += `\ @IF NOT DEFINED NODE_PATH (\r @SET "NODE_PATH=${nodePath}"\r ) ELSE (\r @SET "NODE_PATH=${nodePath};%NODE_PATH%"\r )\r `; } if (longProg) { cmd += `\ @IF EXIST ${longProg} (\r ${longProg} ${args} ${target} ${progArgs}%*\r ) ELSE (\r @SET PATHEXT=%PATHEXT:;.JS;=;%\r ${prog} ${args} ${target} ${progArgs}%*\r )\r `; } else { cmd += `@${prog} ${args} ${target} ${progArgs}%*\r\n`; } return cmd; } /** * Generate the content of a shim for (Ba)sh in, for example, Cygwin and MSYS(2). * * @param src Path to the executable or script. * @param to Path to the shim to be created. * It is highly recommended to end with `.sh` or to contain no extension. * @param opts Options. * @return The content of shim. */ function generateShShim(src, to, opts) { let shTarget = path.relative(path.dirname(to), src); let shProg = opts.prog && opts.prog.split('\\').join('/'); let shLongProg; shTarget = shTarget.split('\\').join('/'); const quotedPathToTarget = path.isAbsolute(shTarget) ? `"${shTarget}"` : `"$basedir/${shTarget}"`; let args = opts.args || ''; const shNodePath = normalizePathEnvVar(opts.nodePath).posix; if (!shProg) { shProg = quotedPathToTarget; args = ''; shTarget = ''; } else if (opts.prog === 'node' && opts.nodeExecPath) { shProg = `"${opts.nodeExecPath}"`; shTarget = quotedPathToTarget; } else { shLongProg = `"$basedir/${opts.prog}"`; shTarget = quotedPathToTarget; } let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : ''; // #!/bin/sh // basedir=`dirname "$0"` // // case `uname` in // *CYGWIN*) basedir=`cygpath -w "$basedir"`;; // esac // // export NODE_PATH="<nodepath>" // // if [ -x "$basedir/node.exe" ]; then // exec "$basedir/node.exe" "$basedir/node_modules/npm/bin/npm-cli.js" "$@" // else // exec node "$basedir/node_modules/npm/bin/npm-cli.js" "$@" // fi let sh = `\ #!/bin/sh basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") case \`uname\` in *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; esac `; if (opts.prependToPath) { sh += `\ export PATH="${opts.prependToPath}:$PATH" `; } if (shNodePath) { sh += `\ if [ -z "$NODE_PATH" ]; then export NODE_PATH="${shNodePath}" else export NODE_PATH="${shNodePath}:$NODE_PATH" fi `; } if (shLongProg) { sh += `\ if [ -x ${shLongProg} ]; then exec ${shLongProg} ${args} ${shTarget} ${progArgs}"$@" else exec ${shProg} ${args} ${shTarget} ${progArgs}"$@" fi `; } else { sh += `\ ${shProg} ${args} ${shTarget} ${progArgs}"$@" exit $? `; } return sh; } /** * Generate the content of a shim for PowerShell. * * @param src Path to the executable or script. * @param to Path to the shim to be created. * It is highly recommended to end with `.ps1`. * @param opts Options. * @return The content of shim. */ function generatePwshShim(src, to, opts) { let shTarget = path.relative(path.dirname(to), src); const shProg = opts.prog && opts.prog.split('\\').join('/'); let pwshProg = shProg && `"${shProg}$exe"`; let pwshLongProg; shTarget = shTarget.split('\\').join('/'); const quotedPathToTarget = path.isAbsolute(shTarget) ? `"${shTarget}"` : `"$basedir/${shTarget}"`; let args = opts.args || ''; let normalizedNodePathEnvVar = normalizePathEnvVar(opts.nodePath); const nodePath = normalizedNodePathEnvVar.win32; const shNodePath = normalizedNodePathEnvVar.posix; let normalizedPrependPathEnvVar = normalizePathEnvVar(opts.prependToPath); const prependPath = normalizedPrependPathEnvVar.win32; const shPrependPath = normalizedPrependPathEnvVar.posix; if (!pwshProg) { pwshProg = quotedPathToTarget; args = ''; shTarget = ''; } else if (opts.prog === 'node' && opts.nodeExecPath) { pwshProg = `"${opts.nodeExecPath}"`; shTarget = quotedPathToTarget; } else { pwshLongProg = `"$basedir/${opts.prog}$exe"`; shTarget = quotedPathToTarget; } let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : ''; // #!/usr/bin/env pwsh // $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent // // $ret=0 // $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" // } // if (Test-Path "$basedir/node") { // # Support pipeline input // if ($MyInvocation.ExpectingInput) { // $input | & "$basedir/node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args // } else { // & "$basedir/node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args // } // $ret=$LASTEXITCODE // } else { // # Support pipeline input // if ($MyInvocation.ExpectingInput) { // $input | & "node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args // } else { // & "node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args // } // $ret=$LASTEXITCODE // } // exit $ret let pwsh = `\ #!/usr/bin/env pwsh $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent $exe="" ${(nodePath || prependPath) ? '$pathsep=":"\n' : ''}\ ${nodePath ? `\ $env_node_path=$env:NODE_PATH $new_node_path="${nodePath}" ` : ''}\ ${prependPath ? `\ $env_path=$env:PATH $prepend_path="${prependPath}" ` : ''}\ 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" ${(nodePath || prependPath) ? ' $pathsep=";"\n' : ''}\ }`; if (shNodePath || shPrependPath) { pwsh += `\ else { ${shNodePath ? ` $new_node_path="${shNodePath}"\n` : ''}\ ${shPrependPath ? ` $prepend_path="${shPrependPath}"\n` : ''}\ } `; } if (shNodePath) { pwsh += `\ if ([string]::IsNullOrEmpty($env_node_path)) { $env:NODE_PATH=$new_node_path } else { $env:NODE_PATH="$new_node_path$pathsep$env_node_path" } `; } if (opts.prependToPath) { pwsh += ` $env:PATH="$prepend_path$pathsep$env:PATH" `; } if (pwshLongProg) { pwsh += ` $ret=0 if (Test-Path ${pwshLongProg}) { # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${pwshLongProg} ${args} ${shTarget} ${progArgs}$args } else { & ${pwshLongProg} ${args} ${shTarget} ${progArgs}$args } $ret=$LASTEXITCODE } else { # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${pwshProg} ${args} ${shTarget} ${progArgs}$args } else { & ${pwshProg} ${args} ${shTarget} ${progArgs}$args } $ret=$LASTEXITCODE } ${nodePath ? '$env:NODE_PATH=$env_node_path\n' : ''}\ ${prependPath ? '$env:PATH=$env_path\n' : ''}\ exit $ret `; } else { pwsh += ` # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${pwshProg} ${args} ${shTarget} ${progArgs}$args } else { & ${pwshProg} ${args} ${shTarget} ${progArgs}$args } ${nodePath ? '$env:NODE_PATH=$env_node_path\n' : ''}\ ${prependPath ? '$env:PATH=$env_path\n' : ''}\ exit $LASTEXITCODE `; } return pwsh; } /** * Chmod just created shim and make it executable * * @param to Path to shim. */ function chmodShim(to, opts) { return opts.fs_.chmod(to, 0o755); } function normalizePathEnvVar(nodePath) { if (!nodePath || !nodePath.length) { return { win32: '', posix: '' }; } let split = (typeof nodePath === 'string' ? nodePath.split(path.delimiter) : Array.from(nodePath)); let result = {}; for (let i = 0; i < split.length; i++) { const win32 = split[i].split('/').join('\\'); const posix = isWindows() ? split[i].split('\\').join('/').replace(/^([^:\\/]*):/, (_, $1) => `/mnt/${$1.toLowerCase()}`) : split[i]; result.win32 = result.win32 ? `${result.win32};${win32}` : win32; result.posix = result.posix ? `${result.posix}:${posix}` : posix; result[i] = { win32, posix }; } return result; } module.exports = cmdShim; //# sourceMappingURL=index.js.map