Skip to content

Commit

Permalink
Automatic version switching in powershell
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin committed Nov 7, 2016
1 parent 2d31526 commit 93f3cde
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 44 deletions.
11 changes: 11 additions & 0 deletions doc/AUTO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# AUTO Command - Node Version Switcher
```
nvs auto
nvs auto on
nvs auto off
```
When invoked with no parameters, `nvs auto` searches for the nearest `.node-version` file in the current directory or parent directories. If found, the version specified in the file is then added (if necessary) and used. If no `.node-version` file is found, then the default (linked) version, if any, is used.

The `nvs auto on` command enables automatic switching as needed whenever the current shell's working directory changes; `nvs auto off` disables automatic switching in the current shell. (This feature is not supported in Windows Command Prompt.)

A `.node-version` file must contain a single line with a valid NVS version string. A version string consists of a semantic version number, optionally preceded by a remote name, optionally followed by a processor architecture or bitness ("x86", "x64", "32", "64"), separated by slashes. Version labels ("lts", "latest") are not supported for use with the USE command. Examples: "4.6.0", "6.3.1/x86", "node/6.7.0/x64"
6 changes: 3 additions & 3 deletions lib/addRemove.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,15 @@ function getArchiveFileNameAndUri(version, remoteUri) {
function remove(version) {
let result = [];

// Unlink this version if it is linked.
result = result.concat(nvsLink.unlink(version));

let currentVersion = nvsUse.getCurrentVersion();
if (currentVersion && nvsVersion.equal(currentVersion, version)) {
// The specified version is currently in use. Remove it from the PATH.
result = result.concat(nvsUse.use(null));
}

// Unlink this version if it is linked.
result = result.concat(nvsLink.unlink(version));

// Remove all contents of the version directory,
// along with parent directories if they are empty.
let versionDir = nvsUse.getVersionDir(version);
Expand Down
89 changes: 89 additions & 0 deletions lib/auto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* global settings */
let fs = require('fs'); // Non-const enables test mocking
const os = require('os');
const path = require('path');
const Error = require('./error');

const nvsVersion = require('./version');
let nvsUse = require('./use'); // Non-const enables test mocking
let nvsAddRemove = require('./addRemove'); // Non-const enables test mocking

/**
* Searches for the nearest `.node-version` file in the current directory or parent directories.
* If found, the version specified in the file is then added (if necessary) and used. If no
* `.node-version` file is found, then the default (linked) version, if any, is used.
*/
function autoSwitchAsync(cwd) {
let version = null;
let dir = cwd || process.cwd();
while (dir) {
let versionFile = path.join(dir, '.node-version');
try {
let versionString = fs.readFileSync(versionFile, 'utf8').trim();
try {
version = nvsVersion.parse(versionString);
} catch (e) {
throw new Error('Failed to parse version in file: ' + versionFile, e);
}
} catch (e) {
Error.throwIfNot(Error.ENOENT, e, 'Failed to read file: ' + versionFile);
}

let parentDir = path.dirname(dir);
dir = (parentDir !== dir ? parentDir : null);
}


if (version) {
let binPath = nvsUse.getVersionBinary(version);
if (!binPath) {
let versionString =
version.remoteName + '/' + version.semanticVersion + '/' + version.arch;
console.log('Adding: ' + versionString);

return nvsAddRemove.addAsync(version).then(() => {
return nvsUse.use(version);
});
}
}

return Promise.resolve(nvsUse.use(version));
}

/**
* Enables or disables automatic version switching based on the presence of a
* .node-version file in the current shell directory or a parent directory.
* (This functionality requires support from the bootstrap shell script.)
*
* @param {any} enable
*/
function enableAutoSwitch(enable) {
if (/\.cmd/i.test(process.env['NVS_POSTSCRIPT'])) {
throw new Error('Automatic switching is not supported from a Windows Command Prompt.' +
os.EOL + 'Use PowerShell instead.');
}

let psScriptFile = path.join(path.resolve(__dirname, '..'), 'nvs.ps1');
let shScriptFile = path.join(path.resolve(__dirname, '..'), 'nvs.sh');

require('./postScript').generate(null, {
'.PS1': [
// Patch the function that is invoked every time PowerShell shows a prompt.
// Export the function from the script using a dynamic module; this
// does NOT require the script to be sourced.
'if (-not $env:NVS_ORIGINAL_PROMPT) { $env:NVS_ORIGINAL_PROMPT = prompt }',
'New-Module -Script {',
enable
? 'function prompt { . "' + psScriptFile + '" "prompt" }'
: 'function prompt { Invoke-Expression $env:NVS_ORIGINAL_PROMPT }',
'Export-ModuleMember -Function prompt',
'} > $null',
],
'.SH': null // TODO: define cd(), pushd(), popd() functions
});
}

module.exports = {
autoSwitchAsync,
enableAutoSwitch,
};
3 changes: 3 additions & 0 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ function help(topic) {
'nvs use [version] ' + (canUpdateEnv
? 'Use a node version in the current shell'
: '(Not available, source nvs.sh instead)'),
'nvs auto [on/off] ' + (canUpdateEnv
? 'Automatically switch based on cwd'
: '(Not available, source nvs.sh instead)'),
'nvs run <ver> <js> [args...] Run a script using a node version',
'nvs exec <ver> <exe> [args...] Run an executable using a node version',
'nvs which [version] Show the path to a node version binary',
Expand Down
7 changes: 7 additions & 0 deletions lib/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ function unlink(version) {

let result = [];
let linkPath = nvsUse.getLinkPath();
let currentVersion = nvsUse.getCurrentVersion();

nvsInstall = nvsInstall || require('./install');
if (nvsInstall.isInSystemDirectory()) {
Expand All @@ -129,6 +130,12 @@ function unlink(version) {
Error.throwIfNot(Error.ENOENT, e, 'Failed to remove symbolic link: ' + linkPath);
}

if (currentVersion && !nvsUse.getVersionBinary(currentVersion)) {
currentVersion = null;
}

result = result.concat(nvsUse.use(currentVersion));

return result;
}

Expand Down
24 changes: 24 additions & 0 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,30 @@ function doCommand(args) {
}
return require('./link').unlink(version);

case 'auto':
if (help) return help('auto');
if (!canUpdateEnv) {
throw new Error(
'The \'auto\' command is not available when ' +
'invoking this script as an' + os.EOL +
'executable. To enable PATH updates, source ' +
'nvs.sh from your shell instead.');
}
if (!args[1]) {
return require('./auto').autoSwitchAsync();
} else switch (args[1].toLowerCase()) {
case 'at':
return require('./auto').autoSwitchAsync(args[2]);
case 'on':
case 'enable':
return require('./auto').enableAutoSwitch(true);
case 'off':
case 'disable':
return require('./auto').enableAutoSwitch(false);
default:
return require('./help')('auto');
}

case 'use':
if (help) return help('use');
if (!canUpdateEnv) {
Expand Down
70 changes: 37 additions & 33 deletions lib/postScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const canUpdateEnv = !process.env['NVS_EXECUTE'];
* The NVS shim shell scripts take care of invoking the generated script and
* deleting it afterward.
*/
function generate(exportVars) {
function generate(exportVars, additionalLines) {
if (!canUpdateEnv) {
// Don't throw an error; this allows commands like uninstall to
// work even when they can't update PATH.
Expand All @@ -18,40 +18,44 @@ function generate(exportVars) {
}

let envVars = Object.keys(exportVars || {});
if (envVars.length > 0) {
let postScriptLines = [];
let postScriptFile = process.env['NVS_POSTSCRIPT'];
if (!postScriptFile) {
throw new Error('NVS_POSTSCRIPT environment variable not set.');
}
let postScriptLines = [];
let postScriptFile = process.env['NVS_POSTSCRIPT'];
if (!postScriptFile) {
throw new Error('NVS_POSTSCRIPT environment variable not set.');
}

let postScriptExtension = path.extname(postScriptFile).toUpperCase();
if (postScriptExtension === '.CMD') {
envVars.forEach(envVar => {
if (exportVars[envVar] !== null) {
postScriptLines.push('SET ' + envVar + '=' + exportVars[envVar]);
} else {
postScriptLines.push('SET ' + envVar + '=');
}
});
} else if (postScriptExtension === '.PS1') {
envVars.forEach(envVar => {
if (exportVars[envVar] !== null) {
postScriptLines.push('$env:' + envVar + '="' + exportVars[envVar] + '"');
} else {
postScriptLines.push('Remove-Item env:' + envVar + ' -ErrorAction SilentlyContinue');
}
});
} else if (postScriptExtension === '.SH') {
envVars.forEach(envVar => {
if (exportVars[envVar] !== null) {
postScriptLines.push('export ' + envVar + '="' + exportVars[envVar] + '"');
} else {
postScriptLines.push('unset ' + envVar);
}
});
}

let postScriptExtension = path.extname(postScriptFile).toUpperCase();
if (postScriptExtension === '.CMD') {
envVars.forEach(envVar => {
if (exportVars[envVar] !== null) {
postScriptLines.push('SET ' + envVar + '=' + exportVars[envVar]);
} else {
postScriptLines.push('SET ' + envVar + '=');
}
});
} else if (postScriptExtension === '.PS1') {
envVars.forEach(envVar => {
if (exportVars[envVar] !== null) {
postScriptLines.push('$env:' + envVar + '="' + exportVars[envVar] + '"');
} else {
postScriptLines.push('Remove-Item env:' + envVar + ' -ErrorAction SilentlyContinue');
}
});
} else if (postScriptExtension === '.SH') {
envVars.forEach(envVar => {
if (exportVars[envVar] !== null) {
postScriptLines.push('export ' + envVar + '="' + exportVars[envVar] + '"');
} else {
postScriptLines.push('unset ' + envVar);
}
});
}
if (additionalLines && additionalLines[postScriptExtension]) {
postScriptLines = postScriptLines.concat(additionalLines[postScriptExtension]);
}

if (postScriptLines.length > 0) {
fs.writeFileSync(postScriptFile, postScriptLines.join(os.EOL) + os.EOL);
}
}
Expand Down
15 changes: 13 additions & 2 deletions lib/use.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function use(version, skipUpdateShellEnv) {
let versionString = pathEntry.substr(settings.home.length);
if (versionString === linkName) {
nvsLink = nvsLink || require('./link');
previousVersion = nvsLink.getLinkedVersion();
previousVersion = nvsLink.getLinkedVersion() || {};
} else {
if (path.sep === '\\') {
versionString = versionString.replace(/\\/g, '/');
Expand All @@ -136,9 +136,20 @@ function use(version, skipUpdateShellEnv) {
}
}

let versionDir = null;
if (version) {
// Insert the requested version at the front of the PATH.
let versionDir = getVersionDir(version);
versionDir = getVersionDir(version);
} else {
// Insert the default version (if any) at the front of the path.
nvsLink = nvsLink || require('./link');
let defaultVersion = nvsLink.getLinkedVersion();
if (defaultVersion) {
versionDir = path.join(settings.home, 'default');
}
}

if (versionDir) {
if (!isWindows) {
versionDir = path.join(versionDir, 'bin');
} else if (versionDir.endsWith(path.sep)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/version.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function parse(versionString, requireFull) {
}

if (label && label !== 'latest' && label !== 'lts' && !label.startsWith('lts-')) {
throw new Error('Invalid version label: ' + label);
throw new Error('Invalid version label or alias: ' + label);
}

if (!arch) {
Expand Down
39 changes: 34 additions & 5 deletions nvs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Bootstraps node.exe if necessary, then forwards arguments to the main nvs.js script.

$scriptDir = Split-Path $MyInvocation.MyCommand.Path
$mainScript = Join-Path $scriptDir "lib\main.js"

# The NVS_HOME path may be overridden in the environment.
if (-not $env:NVS_HOME) {
Expand Down Expand Up @@ -56,10 +57,39 @@ if (-not (Test-Path $bootstrapNodePath)) {
}
}

# Forward the args to the main JavaScript file.
$mainScript = Join-Path $scriptDir "lib\main.js"
. "$bootstrapNodePath" "$mainScript" @args
$exitCode = $LastExitCode
# Check if this script was invoked as a PS prompt function that enables auto-switching.
if ($args -eq "prompt") {
Invoke-Expression $env:NVS_ORIGINAL_PROMPT

# Find the nearest .node-version file in current or parent directories
for ($parentDir = $pwd.Path; $parentDir; $parentDir = Split-Path $parentDir) {
if (Test-Path (Join-Path $parentDir ".node-version") -PathType Leaf) { break }
}

# If it's still the same as the last auto-switched directory, then there's nothing to do.
if ([string]$parentDir -eq [string]$env:NVS_AUTO_DIRECTORY) {
exit 0
}
$env:NVS_AUTO_DIRECTORY = $parentDir

# Output needs to be redirected to Write-Host, because stdout is ignored by prompt.
# Process a byte at a time so that output like progress bars is real-time.
$startInfo = New-Object System.Diagnostics.ProcessStartInfo $bootstrapNodePath
$startInfo.Arguments = ($mainScript, "auto", "at", $pwd.Path)
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardOutput = $true
$proc = [System.Diagnostics.Process]::Start($startInfo)
while (($b = $proc.StandardOutput.Read()) -ne -1) {
Write-Host -NoNewline ([char]$b)
}
$proc.WaitForExit
$exitCode = $proc.ExitCode
}
else {
# Forward the args to the main JavaScript file.
. "$bootstrapNodePath" "$mainScript" @args
$exitCode = $LastExitCode
}

# Call the post-invocation script if it is present, then delete it.
# This allows the invocation to potentially modify the caller's environment (e.g. PATH).
Expand All @@ -69,5 +99,4 @@ if (Test-Path $env:NVS_POSTSCRIPT) {
}

$env:NVS_POSTSCRIPT = $null

exit $exitCode
Loading

0 comments on commit 93f3cde

Please sign in to comment.