node-version-use
Version:
Cross-platform solution for using multiple versions of node. Useful for compatibility testing
416 lines • 16.7 kB
JavaScript
"use strict";
var _process_env_OSTYPE;
var envPathKey = require('env-path-key');
var fs = require('fs');
var safeRmSync = require('fs-remove-compat').safeRmSync;
var getFile = require('get-file-compat');
var mkdirp = require('mkdirp-classic');
var os = require('os');
var path = require('path');
var Queue = require('queue-cb');
var moduleRoot = require('module-root-sync');
var cpuArch = require('cpu-arch');
var root = moduleRoot(__dirname);
// Configuration
var GITHUB_REPO = 'kmalakoff/node-version-use';
var BINARY_VERSION = require(path.join(root, 'package.json')).binaryVersion;
var isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test((_process_env_OSTYPE = process.env.OSTYPE) !== null && _process_env_OSTYPE !== void 0 ? _process_env_OSTYPE : '');
var hasHomedir = typeof os.homedir === 'function';
function homedir() {
if (hasHomedir) return os.homedir();
var home = require('homedir-polyfill');
return home();
}
// Allow NVU_HOME override for testing
var storagePath = process.env.NVU_HOME || path.join(homedir(), '.nvu');
var hasTmpdir = typeof os.tmpdir === 'function';
function tmpdir() {
if (hasTmpdir) return os.tmpdir();
var osShim = require('os-shim');
return osShim.tmpdir();
}
function removeIfExistsSync(filePath) {
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
} catch (_e) {
// ignore cleanup errors
}
}
}
/**
* Move a file out of the way (works even if running on Windows)
* First tries to unlink; if that fails (Windows locked), rename to .old-timestamp
*/ function moveOutOfWay(filePath) {
if (!fs.existsSync(filePath)) return;
// First try to unlink (works on Unix, fails on Windows if running)
try {
fs.unlinkSync(filePath);
return;
} catch (_e) {
// Unlink failed (likely Windows locked file), try rename
}
// Rename to .old-timestamp as fallback
var timestamp = Date.now();
var oldPath = "".concat(filePath, ".old-").concat(timestamp);
try {
fs.renameSync(filePath, oldPath);
} catch (_e2) {
// Both unlink and rename failed - will fail on atomic rename instead
}
}
/**
* Clean up old .old-* files from previous installs
*/ function cleanupOldFiles(dir) {
try {
var entries = fs.readdirSync(dir);
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
try {
for(var _iterator = entries[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
var entry = _step.value;
if (entry.includes('.old-')) {
try {
fs.unlinkSync(path.join(dir, entry));
} catch (_e) {
// ignore - file may still be in use
}
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally{
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally{
if (_didIteratorError) {
throw _iteratorError;
}
}
}
} catch (_e) {
// ignore if dir doesn't exist
}
}
/**
* Get the platform-specific archive base name (without extension)
*/ function getArchiveBaseName() {
var platform = process.platform;
var arch = cpuArch();
var platformMap = {
darwin: 'darwin',
linux: 'linux',
win32: 'win32'
};
var archMap = {
x64: 'x64',
arm64: 'arm64',
amd64: 'x64'
};
var platformName = platformMap[platform];
var archName = archMap[arch];
if (!platformName || !archName) return null;
return "nvu-binary-".concat(platformName, "-").concat(archName);
}
/**
* Copy file
*/ function copyFileSync(src, dest) {
var content = fs.readFileSync(src);
fs.writeFileSync(dest, content);
}
/**
* Sync all shims by copying the nvu binary to all other files in the bin directory
* All shims (node, npm, npx, corepack, eslint, etc.) are copies of the same binary
*/ module.exports.syncAllShims = function syncAllShims(binDir) {
var _process_env_OSTYPE;
var isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test((_process_env_OSTYPE = process.env.OSTYPE) !== null && _process_env_OSTYPE !== void 0 ? _process_env_OSTYPE : '');
var ext = isWindows ? '.exe' : '';
// Source: nvu binary
var nvuSource = path.join(binDir, "nvu".concat(ext));
if (!fs.existsSync(nvuSource)) return;
try {
var entries = fs.readdirSync(binDir);
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
try {
for(var _iterator = entries[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
var name = _step.value;
// Skip nvu itself and nvu.json
if (name === "nvu".concat(ext) || name === 'nvu.json') continue;
// On Windows, only process .exe files
if (isWindows && !name.endsWith('.exe')) continue;
var shimPath = path.join(binDir, name);
var stat = fs.statSync(shimPath);
if (!stat.isFile()) continue;
// Move existing file out of the way (Windows compatibility)
moveOutOfWay(shimPath);
// Copy nvu binary to shim
copyFileSync(nvuSource, shimPath);
// Make executable on Unix
if (!isWindows) {
fs.chmodSync(shimPath, 493);
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally{
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally{
if (_didIteratorError) {
throw _iteratorError;
}
}
}
} catch (_e) {
// Ignore errors - shim sync is best effort
}
};
/**
* Atomic rename with fallback to copy+delete for cross-device moves
*/ function atomicRename(src, dest, callback) {
fs.rename(src, dest, function(err) {
if (!err) return callback(null);
// Cross-device link error - fall back to copy + delete
if (err.code === 'EXDEV') {
try {
copyFileSync(src, dest);
fs.unlinkSync(src);
callback(null);
} catch (copyErr) {
callback(copyErr);
}
return;
}
callback(err);
});
}
/**
* Extract archive to a directory (callback-based)
*/ function extractArchive(archivePath, dest, callback) {
var Iterator = isWindows ? require('zip-iterator') : require('tar-iterator');
var stream = isWindows ? fs.createReadStream(archivePath) : fs.createReadStream(archivePath).pipe(require('zlib').createGunzip());
var iterator = new Iterator(stream);
// one by one
var links = [];
iterator.forEach(function(entry, callback) {
if (entry.type === 'link') {
links.unshift(entry);
callback();
} else if (entry.type === 'symlink') {
links.push(entry);
callback();
} else entry.create(dest, callback);
}, {
callbacks: true,
concurrency: 1
}, function(_err) {
// create links after directories and files
var queue = new Queue();
for(var index = 0; index < links.length; index++){
var entry = links[index];
queue.defer(entry.create.bind(entry, dest));
}
queue.await(function(err) {
iterator.destroy();
iterator = null;
callback(err);
});
});
}
/**
* Install binaries using atomic rename pattern
* 1. Extract to temp directory
* 2. Copy binary to temp files in destination directory
* 3. Atomic rename temp files to final names
*/ function extractAndInstall(archivePath, destDir, binaryName, callback) {
var binaries = [
'nvu',
'node',
'npm',
'npx',
'corepack'
];
var ext = isWindows ? '.exe' : '';
// Create temp extraction directory
var tempExtractDir = path.join(tmpdir(), "nvu-extract-".concat(Date.now()));
mkdirp.sync(tempExtractDir);
extractArchive(archivePath, tempExtractDir, function(err) {
if (err) {
safeRmSync(tempExtractDir);
return callback(err);
}
var extractedPath = path.join(tempExtractDir, binaryName);
if (!fs.existsSync(extractedPath)) {
safeRmSync(tempExtractDir);
callback(new Error("Extracted binary not found: ".concat(binaryName, ". ").concat(archivePath, " ").concat(tempExtractDir)));
return;
}
// Binary names to install
var timestamp = Date.now();
var installError = null;
// Step 1: Copy extracted binary to temp files in destination directory
// This ensures the temp files are on the same filesystem for atomic rename
for(var i = 0; i < binaries.length; i++){
var name = binaries[i];
var tempDest = path.join(destDir, "".concat(name, ".tmp-").concat(timestamp).concat(ext));
try {
// Copy to temp file in destination directory
copyFileSync(extractedPath, tempDest);
// Set permissions on Unix
if (!isWindows) fs.chmodSync(tempDest, 493);
} catch (err) {
installError = err;
break;
}
}
if (installError) {
// Clean up any temp files we created
for(var j = 0; j < binaries.length; j++){
var tempPath = path.join(destDir, "".concat(binaries[j], ".tmp-").concat(timestamp).concat(ext));
removeIfExistsSync(tempPath);
}
safeRmSync(tempExtractDir);
callback(installError);
return;
}
// Step 2: Atomic rename temp files to final names
var renameError = null;
function doRename(index) {
if (index >= binaries.length) {
// All renames complete
safeRmSync(tempExtractDir);
callback(renameError);
return;
}
var name = binaries[index];
var tempDest = path.join(destDir, "".concat(name, ".tmp-").concat(timestamp).concat(ext));
var finalDest = path.join(destDir, "".concat(name).concat(ext));
// Move existing file out of the way (works even if running on Windows)
moveOutOfWay(finalDest);
atomicRename(tempDest, finalDest, function(err) {
if (err && !renameError) {
renameError = err;
}
doRename(index + 1);
});
}
doRename(0);
});
}
/**
* Print setup instructions
*/ module.exports.printInstructions = function printInstructions() {
var _nvuBinPath = path.join(storagePath, 'bin');
console.log('nvu binaries installed in ~/.nvu/bin/');
var pathKey = envPathKey(); // PATH or Path or similar
var envPath = process.env[pathKey] || '';
if (envPath.indexOf('.nvu/bin') >= 0) return; // path exists
// provide instructions for path setup
console.log('');
console.log('============================================================');
console.log(' Global node setup');
console.log('============================================================');
console.log('');
if (isWindows) {
console.log(' # Edit your PowerShell profile');
console.log(' # Open with: notepad $PROFILE');
console.log(' # Add this line:');
console.log(' $env:PATH = "$HOME\\.nvu\\bin;$env:APPDATA\\npm;$env:PATH"');
console.log('');
console.log(' # This adds:');
console.log(' # ~/.nvu/bin - node/npm version switching shims');
console.log(' # %APPDATA%/npm - globally installed npm packages (like nvu)');
} else {
console.log(' # For bash (~/.bashrc):');
console.log(' echo \'export PATH="$HOME/.nvu/bin:$PATH"\' >> ~/.bashrc');
console.log('');
console.log(' # For zsh (~/.zshrc):');
console.log(' echo \'export PATH="$HOME/.nvu/bin:$PATH"\' >> ~/.zshrc');
console.log('');
console.log(' # For fish (~/.config/fish/config.fish):');
console.log(" echo 'set -gx PATH $HOME/.nvu/bin $PATH' >> ~/.config/fish/config.fish");
}
console.log('');
console.log('Then restart your terminal or source your shell profile.');
console.log('');
console.log("Without this, 'nvu 18 npm test' still works - you just won't have");
console.log("transparent 'node' command override.");
console.log('============================================================');
};
/**
* Main installation function
*/ module.exports.installBinaries = function installBinaries(options, callback) {
var archiveBaseName = getArchiveBaseName();
if (!archiveBaseName) {
callback(new Error('Unsupported platform/architecture for binary.'));
return;
}
var extractedBinaryName = "".concat(archiveBaseName).concat(isWindows ? '.exe' : '');
var binDir = path.join(storagePath, 'bin');
var nvuJsonPath = path.join(binDir, 'nvu.json');
// check if we need to upgrade
if (!options.force) {
try {
// already installed - read nvu.json
var nvuJson = JSON.parse(fs.readFileSync(nvuJsonPath, 'utf8'));
if (nvuJson.binaryVersion === BINARY_VERSION) {
callback(null, false);
return;
}
} catch (_err) {}
}
// Create directories
mkdirp.sync(storagePath);
mkdirp.sync(binDir);
mkdirp.sync(path.join(storagePath, 'cache'));
// Clean up old .old-* files from previous installs
cleanupOldFiles(binDir);
var downloadUrl = "https://github.com/".concat(GITHUB_REPO, "/releases/download/binary-v").concat(BINARY_VERSION, "/").concat(archiveBaseName).concat(isWindows ? '.zip' : '.tar.gz');
var cachePath = path.join(storagePath, 'cache', "".concat(archiveBaseName).concat(isWindows ? '.zip' : '.tar.gz'));
// Check cache first
if (fs.existsSync(cachePath)) {
console.log('Using cached binary...');
// Use cached file
extractAndInstall(cachePath, binDir, extractedBinaryName, function(err) {
if (err) return callback(err);
// save binary version for upgrade checks
fs.writeFileSync(nvuJsonPath, JSON.stringify({
binaryVersion: BINARY_VERSION
}, null, 2), 'utf8');
console.log('Binary installed successfully!');
callback(null, true);
});
return;
}
// Download to temp file
console.log("Downloading binary for ".concat(archiveBaseName, "..."));
var tempPath = path.join(tmpdir(), "nvu-binary-".concat(Date.now()).concat(isWindows ? '.zip' : '.tar.gz'));
getFile(downloadUrl, tempPath, function(err) {
if (err) {
removeIfExistsSync(tempPath);
return callback(new Error("No prebuilt binary available for ".concat(archiveBaseName, ". Download: ").concat(downloadUrl, ". Error: ").concat(err.message)));
}
// Copy to cache for future use
try {
copyFileSync(tempPath, cachePath);
} catch (_e) {
// Cache write failed, continue anyway
}
extractAndInstall(tempPath, binDir, extractedBinaryName, function(err) {
removeIfExistsSync(tempPath);
if (err) return callback(err);
// save binary version for upgrade checks
fs.writeFileSync(nvuJsonPath, JSON.stringify({
binaryVersion: BINARY_VERSION
}, null, 2), 'utf8');
console.log('Binary installed successfully!');
callback(null, true);
});
});
};
/* CJS INTEROP */ if (exports.__esModule && exports.default) { try { Object.defineProperty(exports.default, '__esModule', { value: true }); for (var key in exports) { exports.default[key] = exports[key]; } } catch (_) {}; module.exports = exports.default; }