node-version-use
Version:
Cross-platform solution for using multiple versions of node. Useful for compatibility testing
202 lines (201 loc) • 9.09 kB
JavaScript
var _process_env_OSTYPE;
import spawn from 'cross-spawn-cb';
import fs from 'fs';
import resolveVersions from 'node-resolve-versions';
import { spawnOptions as createSpawnOptions } from 'node-version-utils';
import path from 'path';
import Queue from 'queue-cb';
import resolveBin from 'resolve-bin-sync';
import spawnStreaming from 'spawn-streaming';
import { createSession, formatArguments } from 'spawn-term';
import { objectAssign, stringEndsWith } from './compat.js';
import { storagePath } from './constants.js';
import loadNodeVersionInstall from './lib/loadNodeVersionInstall.js';
import { getPathWithoutNvuBin, resolveSystemBinary } from './lib/resolveSystemBinary.js';
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test((_process_env_OSTYPE = process.env.OSTYPE) !== null && _process_env_OSTYPE !== void 0 ? _process_env_OSTYPE : '');
const NODE = isWindows ? 'node.exe' : 'node';
// Parse npm-generated .cmd wrapper to extract the JS script path
function parseNpmCmdWrapper(cmdPath) {
try {
const content = fs.readFileSync(cmdPath, 'utf8');
// Match: "%_prog%" "%dp0%\node_modules\...\cli.js" %*
// or: "%_prog%" "%dp0%\path\to\script.js" %*
const match = content.match(/"%_prog%"\s+"?%dp0%\\([^"]+)"?\s+%\*/);
if (match) {
const relativePath = match[1];
const cmdDir = path.dirname(cmdPath);
return path.join(cmdDir, relativePath);
}
} catch (_e) {
// ignore
}
return null;
}
// On Windows, resolve npm bin commands to their JS entry points to bypass .cmd wrappers
// This fixes issues with nvm-windows where .cmd wrappers use symlinked node.exe directly
function resolveCommand(command, args) {
if (!isWindows) return {
command,
args
};
// Case 1: Command is a .cmd file path
if (stringEndsWith(command.toLowerCase(), '.cmd')) {
const scriptPath = parseNpmCmdWrapper(command);
if (scriptPath) {
return {
command: NODE,
args: [
scriptPath
].concat(args)
};
}
}
// Case 2: Try to resolve the command as an npm package bin from node_modules
try {
const binPath = resolveBin(command);
return {
command: NODE,
args: [
binPath
].concat(args)
};
} catch (_e) {
// Not an npm package bin, use original command
}
return {
command,
args
};
}
export default function worker(versionExpression, command, args, options, callback) {
// Handle "system" as a special version that uses system binaries directly
if (versionExpression === 'system') {
runWithSystemBinaries(command, args, options, callback);
return;
}
// Load node-version-install lazily
loadNodeVersionInstall((loadErr, installVersion)=>{
if (loadErr) return callback(loadErr);
resolveVersions(versionExpression, options, (err, result)=>{
const versions = result;
if (err) return callback(err);
if (!versions || !versions.length) {
callback(new Error(`No versions found from expression: ${versionExpression}`));
return;
}
const installOptions = objectAssign({
storagePath: storagePath
}, options);
const streamingOptions = options;
const results = [];
const queue = new Queue(1);
// Create session once for all processes (only if multiple versions)
const interactive = options.interactive !== false;
const session = versions.length >= 2 && process.stdout.isTTY && typeof createSession === 'function' && !streamingOptions.streaming ? createSession({
header: `${command} ${args.join(' ')}`,
showStatusBar: true,
interactive
}) : null;
versions.forEach((version, index)=>queue.defer((cb)=>installVersion === null || installVersion === void 0 ? void 0 : installVersion(version, installOptions, (err, installs)=>{
const install = installs && installs.length === 1 ? installs[0] : null;
if (err || !install) {
const error = err || new Error(`Unexpected version results for version ${version}. Install ${JSON.stringify(installs)}`);
results.push({
install,
command,
version,
error,
result: undefined
});
return cb();
}
const spawnOptions = createSpawnOptions(install.installPath, options);
const prefix = install.version;
function next(err, res) {
if (!session && !options.silent) console.log('==============');
if (err && err.message.indexOf('ExperimentalWarning') >= 0) {
res = err;
err = undefined;
}
results.push({
install,
command,
version,
error: err !== null && err !== void 0 ? err : undefined,
result: res
});
cb();
}
// On Windows, resolve npm bin commands to bypass .cmd wrappers
const resolved = resolveCommand(command, args);
// Show command when running single version (no terminal session, unless silent)
if (!session && !options.silent) console.log(`${index > 0 ? '\n' : ''}${version}`);
if (!session && !options.silent) console.log('--------------');
if (!session && !options.silent) console.log(`$ ${formatArguments([
resolved.command
].concat(resolved.args)).join(' ')}`);
if ((versions === null || versions === void 0 ? void 0 : versions.length) < 2) spawn(resolved.command, resolved.args, spawnOptions, next);
else if (session) session.spawn(resolved.command, resolved.args, spawnOptions, {
group: prefix,
expanded: streamingOptions.expanded
}, next);
else spawnStreaming(resolved.command, resolved.args, spawnOptions, {
prefix: process.stdout.isTTY ? prefix : undefined
}, next);
})));
queue.await((err)=>{
if (session) {
session.waitAndClose(()=>{
err ? callback(err) : callback(undefined, results);
});
} else {
err ? callback(err) : callback(undefined, results);
}
});
});
});
}
/**
* Run command using system binaries (bypassing nvu version management)
* This handles the "system" version specifier
*/ function runWithSystemBinaries(command, args, options, callback) {
// Find the system binary for the command
const systemBinary = resolveSystemBinary(command);
if (!systemBinary) {
callback(new Error(`System ${command} not found in PATH`));
return;
}
// Create spawn options with PATH excluding ~/.nvu/bin
// This ensures any child processes also use system binaries
const cleanPath = getPathWithoutNvuBin();
const spawnOptions = objectAssign({}, options);
spawnOptions.env = objectAssign({}, process.env);
spawnOptions.env.PATH = cleanPath;
spawnOptions.stdio = options.stdio || 'inherit';
// On Windows, resolve npm bin commands to bypass .cmd wrappers
const resolved = resolveCommand(command, args);
// For system, use the resolved system binary path
const finalCommand = resolved.command === command ? systemBinary : resolved.command;
const finalArgs = resolved.command === command ? args : resolved.args;
if (!options.silent) {
console.log(`$ ${formatArguments([
finalCommand
].concat(finalArgs)).join(' ')}`);
}
spawn(finalCommand, finalArgs, spawnOptions, (err, res)=>{
if (err && err.message && err.message.indexOf('ExperimentalWarning') >= 0) {
res = err;
err = undefined;
}
const result = {
install: null,
command,
version: 'system',
error: err,
result: res
};
callback(err, [
result
]);
});
}