@zkochan/cmd-shim
Version:
Used in pnpm for command line application support
540 lines (537 loc) • 17.3 kB
JavaScript
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
;
cmdShim.