node-version-use
Version:
Cross-platform solution for using multiple versions of node. Useful for compatibility testing
381 lines (380 loc) • 14.7 kB
JavaScript
var _process_env_OSTYPE;
const envPathKey = require('env-path-key');
const fs = require('fs');
const { safeRmSync } = require('fs-remove-compat');
const getFile = require('get-file-compat');
const mkdirp = require('mkdirp-classic');
const os = require('os');
const path = require('path');
const Queue = require('queue-cb');
const moduleRoot = require('module-root-sync');
const cpuArch = require('cpu-arch');
const root = moduleRoot(__dirname);
// Configuration
const GITHUB_REPO = 'kmalakoff/node-version-use';
const BINARY_VERSION = require(path.join(root, 'package.json')).binaryVersion;
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test((_process_env_OSTYPE = process.env.OSTYPE) !== null && _process_env_OSTYPE !== void 0 ? _process_env_OSTYPE : '');
const hasHomedir = typeof os.homedir === 'function';
function homedir() {
if (hasHomedir) return os.homedir();
const home = require('homedir-polyfill');
return home();
}
// Allow NVU_HOME override for testing
const storagePath = process.env.NVU_HOME || path.join(homedir(), '.nvu');
const hasTmpdir = typeof os.tmpdir === 'function';
function tmpdir() {
if (hasTmpdir) return os.tmpdir();
const 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
const timestamp = Date.now();
const oldPath = `${filePath}.old-${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 {
const entries = fs.readdirSync(dir);
for (const entry of entries){
if (entry.includes('.old-')) {
try {
fs.unlinkSync(path.join(dir, entry));
} catch (_e) {
// ignore - file may still be in use
}
}
}
} catch (_e) {
// ignore if dir doesn't exist
}
}
/**
* Get the platform-specific archive base name (without extension)
*/ function getArchiveBaseName() {
const { platform } = process;
const arch = cpuArch();
const platformMap = {
darwin: 'darwin',
linux: 'linux',
win32: 'win32'
};
const archMap = {
x64: 'x64',
arm64: 'arm64',
amd64: 'x64'
};
const platformName = platformMap[platform];
const archName = archMap[arch];
if (!platformName || !archName) return null;
return `nvu-binary-${platformName}-${archName}`;
}
/**
* Copy file
*/ function copyFileSync(src, dest) {
const 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;
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test((_process_env_OSTYPE = process.env.OSTYPE) !== null && _process_env_OSTYPE !== void 0 ? _process_env_OSTYPE : '');
const ext = isWindows ? '.exe' : '';
// Source: nvu binary
const nvuSource = path.join(binDir, `nvu${ext}`);
if (!fs.existsSync(nvuSource)) return;
try {
const entries = fs.readdirSync(binDir);
for (const name of entries){
// Skip nvu itself and nvu.json
if (name === `nvu${ext}` || name === 'nvu.json') continue;
// On Windows, only process .exe files
if (isWindows && !name.endsWith('.exe')) continue;
const shimPath = path.join(binDir, name);
const 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, 0o755);
}
}
} 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, (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) {
const Iterator = isWindows ? require('zip-iterator') : require('tar-iterator');
const stream = isWindows ? fs.createReadStream(archivePath) : fs.createReadStream(archivePath).pipe(require('zlib').createGunzip());
let iterator = new Iterator(stream);
// one by one
const links = [];
iterator.forEach((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
}, (_err)=>{
// create links after directories and files
const queue = new Queue();
for(let index = 0; index < links.length; index++){
const entry = links[index];
queue.defer(entry.create.bind(entry, dest));
}
queue.await((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) {
const binaries = [
'nvu',
'node',
'npm',
'npx',
'corepack'
];
const ext = isWindows ? '.exe' : '';
// Create temp extraction directory
const tempExtractDir = path.join(tmpdir(), `nvu-extract-${Date.now()}`);
mkdirp.sync(tempExtractDir);
extractArchive(archivePath, tempExtractDir, (err)=>{
if (err) {
safeRmSync(tempExtractDir);
return callback(err);
}
const extractedPath = path.join(tempExtractDir, binaryName);
if (!fs.existsSync(extractedPath)) {
safeRmSync(tempExtractDir);
callback(new Error(`Extracted binary not found: ${binaryName}. ${archivePath} ${tempExtractDir}`));
return;
}
// Binary names to install
const timestamp = Date.now();
let 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(let i = 0; i < binaries.length; i++){
const name = binaries[i];
const tempDest = path.join(destDir, `${name}.tmp-${timestamp}${ext}`);
try {
// Copy to temp file in destination directory
copyFileSync(extractedPath, tempDest);
// Set permissions on Unix
if (!isWindows) fs.chmodSync(tempDest, 0o755);
} catch (err) {
installError = err;
break;
}
}
if (installError) {
// Clean up any temp files we created
for(let j = 0; j < binaries.length; j++){
const tempPath = path.join(destDir, `${binaries[j]}.tmp-${timestamp}${ext}`);
removeIfExistsSync(tempPath);
}
safeRmSync(tempExtractDir);
callback(installError);
return;
}
// Step 2: Atomic rename temp files to final names
let renameError = null;
function doRename(index) {
if (index >= binaries.length) {
// All renames complete
safeRmSync(tempExtractDir);
callback(renameError);
return;
}
const name = binaries[index];
const tempDest = path.join(destDir, `${name}.tmp-${timestamp}${ext}`);
const finalDest = path.join(destDir, `${name}${ext}`);
// Move existing file out of the way (works even if running on Windows)
moveOutOfWay(finalDest);
atomicRename(tempDest, finalDest, (err)=>{
if (err && !renameError) {
renameError = err;
}
doRename(index + 1);
});
}
doRename(0);
});
}
/**
* Print setup instructions
*/ module.exports.printInstructions = function printInstructions() {
const _nvuBinPath = path.join(storagePath, 'bin');
console.log('nvu binaries installed in ~/.nvu/bin/');
const pathKey = envPathKey(); // PATH or Path or similar
const 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) {
const archiveBaseName = getArchiveBaseName();
if (!archiveBaseName) {
callback(new Error('Unsupported platform/architecture for binary.'));
return;
}
const extractedBinaryName = `${archiveBaseName}${isWindows ? '.exe' : ''}`;
const binDir = path.join(storagePath, 'bin');
const nvuJsonPath = path.join(binDir, 'nvu.json');
// check if we need to upgrade
if (!options.force) {
try {
// already installed - read nvu.json
const 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);
const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/binary-v${BINARY_VERSION}/${archiveBaseName}${isWindows ? '.zip' : '.tar.gz'}`;
const cachePath = path.join(storagePath, 'cache', `${archiveBaseName}${isWindows ? '.zip' : '.tar.gz'}`);
// Check cache first
if (fs.existsSync(cachePath)) {
console.log('Using cached binary...');
// Use cached file
extractAndInstall(cachePath, binDir, extractedBinaryName, (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 ${archiveBaseName}...`);
const tempPath = path.join(tmpdir(), `nvu-binary-${Date.now()}${isWindows ? '.zip' : '.tar.gz'}`);
getFile(downloadUrl, tempPath, (err)=>{
if (err) {
removeIfExistsSync(tempPath);
return callback(new Error(`No prebuilt binary available for ${archiveBaseName}. Download: ${downloadUrl}. Error: ${err.message}`));
}
// Copy to cache for future use
try {
copyFileSync(tempPath, cachePath);
} catch (_e) {
// Cache write failed, continue anyway
}
extractAndInstall(tempPath, binDir, extractedBinaryName, (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);
});
});
};