Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ $ nvs which myalias/32
[An alias may also refer to a local directory](doc/ALIAS.md#aliasing-directories), enabling NVS to switch to a local private build of node.

## Automatic switching per directory
In either Bash or PowerShell, NVS can automatically switch the node version in the current shell as you change directories. This function is disabled by default; to enable it run `nvs auto on`. Afterward, whenever you `cd` or `pushd` under a directory containing a `.node-version` or an [`.nvmrc`](https://github.com/nvm-sh/nvm#nvmrc) file then NVS will automatically switch the node version accordingly, downloading a new version if necessary. When you `cd` out to a directory with no `.node-version` or `.nvmrc` file anywhere above it, then the default (linked) version is restored, if any.
In either Bash or PowerShell, NVS can automatically switch the node version in the current shell as you change directories. This function is disabled by default; to enable it run `nvs auto on`. Afterward, whenever you `cd` or `pushd` under a directory containing a `package.json`, `.node-version`, or an [`.nvmrc`](https://github.com/nvm-sh/nvm#nvmrc) file then NVS will automatically switch the node version accordingly, downloading a new version if necessary. When you `cd` out to a directory with no `package.json`, `.node-version`, or `.nvmrc` file anywhere above it, then the default (linked) version is restored, if any. If a `package.json` is found we'll prefer the [officially recommended](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#devengines) `devEngines.runtime.version` over all other options.
```
~$ nvs link 6.9.1
~/.nvs/default -> ~/.nvs/node/6.9.1/x64
Expand Down
10 changes: 6 additions & 4 deletions doc/AUTO.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# AUTO Command - Node Version Switcher

nvs auto
nvs auto on
nvs auto off
```
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 downloaded (if necessary) and used. If no `.node-version` file is found, then the default (linked) version, if any, is used.
When invoked with no parameters, `nvs auto` searches for the nearest `package.json` file, if found, it will use the Node version supplied by [devEngines](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#devengines) or the [volta](https://github.com/volta-cli/volta) object if either are available. If not, it will use the nearest `.node-version` or `.nvmrc` file in the current directory or parent directories. If a Node version is found in any of these locations, it will be downloaded (if necessary) and used. If no Node version 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.)

Expand Down
240 changes: 225 additions & 15 deletions lib/auto.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,242 @@ let nvsLink = null; // Lazy load
function findAutoVersionAsync(cwd) {
let version = null;
let dir = cwd || process.cwd();
while (dir) {
let versionFile = path.join(dir, '.node-version');
let versionString;
try {
versionString = fs.readFileSync(versionFile, 'utf8').trim();
} catch (e) {
Error.throwIfNot(Error.ENOENT, e, 'Failed to read file: ' + versionFile);
/**
* Reads and parses the package.json to find devEngines or Volta object.
*
* @param {string} directory Current working path
* @param {string} versionString The Node version if already found
* @return {string} Updated versionString
*/
function findDevEnginesOrVolta(directory, versionString) {
/**
* Reads in the package.json and parse it to the JSON manifest.
*
* @param {string} directory Current working path to look for package.json in
* @return {object} The parsed manifest
*/
function getManifest(directory) {
let manifest;
if (directory) {
let manifestFile = path.join(directory, 'package.json');
try {
if (fs.existsSync(manifestFile)) {
manifest = fs.readFileSync(manifestFile, 'utf8');
manifest = JSON.parse(manifest);
}
} catch (error) {
Error.throwIfNot(Error.ENOENT, error, 'Failed to read file: ' + manifestFile);
}
}
return manifest;
}

// If we don't have a version string, try checking for an .nvmrc file
if (!versionString && !settings.disableNvmrc) {
versionFile = path.join(dir, '.nvmrc');
if (directory && !versionString) {
const manifest = getManifest(directory);
if (manifest) {
if (
manifest.devEngines &&
manifest.devEngines.runtime
) {
// Future proofing: Spec mentions runtime shorthand may be added later
if (typeof(manifest.devEngines.runtime) === 'string') {
versionString = manifest.devEngines.runtime;
} else if (
manifest.devEngines.runtime.name &&
manifest.devEngines.runtime.name === 'node' &&
manifest.devEngines.runtime.version
) {
versionString = manifest.devEngines.runtime.version;
}
}
if (
!versionString &&
manifest.volta &&
manifest.volta.node
) {
versionString = manifest.volta.node;
}
}
}
return versionString;
}
/**
* Get the Node version from files that only store it, like
* .nvmrc or .node-version.
*
* @param {string} directory Current working path
* @param {string} versionString The Node version if already found
* @param {string} fileName The file to open ('.nvmrc', '.node-version')
* @return {string} Updated versionString
*/
function findVersionFile(directory, versionString, fileName) {
if (directory && !versionString) {
let versionFile = path.join(directory, fileName);
try {
versionString = fs.readFileSync(versionFile, 'utf8').trim();
} catch (e) {
Error.throwIfNot(Error.ENOENT, e, 'Failed to read file: ' + versionFile);
if (fs.existsSync(versionFile)) {
versionString = fs.readFileSync(versionFile, 'utf8').trim();
}
} catch (error) {
Error.throwIfNot(Error.ENOENT, error, 'Failed to read file: ' + versionFile);
}
}
return versionString;
}
/**
* Get the Node version from the .node-version file.
*
* @param {string} directory Current working path
* @param {string} versionString The Node version if already found
* @return {string} Updated versionString
*/
function findDotNodeVersion(directory, versionString) {
return findVersionFile(directory, versionString, '.node-version');
}
/**
* Get the Node version from the .nvmrc file.
*
* @param {string} directory Current working path
* @param {string} versionString The Node version if already found
* @return {string} Updated versionString
*/
function findDotNvmrc(directory, versionString) {
if (!settings.disableNvmrc) {
return findVersionFile(directory, versionString, '.nvmrc');
}
return versionString;
}
/**
* Gets the Node version from the Mise or Proto TOML file.
*
* @param {string} directory Current working path
* @param {string} versionString The Node version if already found
* @param {'mise'|'proto'} toolName Which Tool is the TOML file associated with
* @return {string} Updated versionString
*/
function getNodeVersionFromTomlFile(directory, versionString, toolName) {
if (!directory || versionString) {
return versionString;
}
/**
* Converts CRLF and CR line endings to LF.
*
* @param {string} contents Any text
* @return {string} The same text with normalized LF line-endings
*/
function normalizeLineEndings(contents) {
// convert all line endings to LF
return contents
// CRLF => LF
.split('\r\n').join('\n')
// CR => LF
.split('\r').join('\n');
}
/**
* Manually extracts the Node version number defined in the TOML file.
* @param {string} contents The contents of the TOML file
* @return {string} The Node version number, '24.0.0'
*/
function findNodeVersionInToml(contents) {
// Break up TOML file contents to lines
const lines = contents.split('\n');
// Find the first line with 'node = "24.0.0"'
let nodeLine = lines.filter((line) => {
return (
line.includes('node=') ||
line.includes('node =')
);
})[0] || '';
// ' "24.0.0"'
let nodeVersion = nodeLine.split('=')[1] || '';
// ' "24.0.0"' => '24.0.0'
nodeVersion = nodeVersion
.split('"').join('')
.split('\'').join('')
.trim();
// '24.0.0' || ''
return nodeVersion;
}
/**
* Tries to read in the file contents for a TOML file and normalize it.
*
* @param {string} directory Current working path
* @param {string} fileName The name of the file to read in the directory
* @return {string} The normalized file contents
*/
function readTheTomlContents(directory, fileName) {
let contents = '';
let versionFile = path.join(directory, fileName);
try {
if (fs.existsSync(versionFile)) {
contents = fs.readFileSync(versionFile, 'utf8');
contents = normalizeLineEndings(contents);
contents = contents.toLowerCase();
contents = contents.trim();
}
} catch (error) {
Error.throwIfNot(Error.ENOENT, error, 'Failed to read file: ' + versionFile);
}
return contents;
}
/**
* Reads in the .prototools file contents and extracts the Node version from it.
*
* @param {string} directory Current working path
* @param {string} versionString The Node version if already found
* @return {string} Updated versionString
*/
function findDotPrototools(directory, versionString) {
let contents = readTheTomlContents(directory, '.prototools');
versionString = findNodeVersionInToml(contents) || versionString;
return versionString;
}
/**
* Reads in the mise.toml file contents and extracts the Node version from it.
*
* @param {string} directory Current working path
* @param {string} versionString The Node version if already found
* @return {string} Updated versionString
*/
function findMiseDotToml(directory, versionString) {
let contents = readTheTomlContents(directory, 'mise.toml');
// Remove everything before the tools section
contents = contents.split('[tools]')[1] || '';
// Remove everything after the tools section
contents = contents.split('[')[0] || '';
versionString = findNodeVersionInToml(contents);
return versionString;
}

const toolNameMap = {
proto: findDotPrototools,
mise: findMiseDotToml
};
if (toolNameMap[toolName]) {
return toolNameMap[toolName](directory, versionString);
}
return versionString;
}
while (dir) {
/**
* Attempt to find a Node version in various locations,
* once found, skip all subsequent checks. DevEngines
* is the official standardized location and should be
* prefererred over all other options.
*/
let versionString;
versionString = findDevEnginesOrVolta(dir, versionString);
versionString = findDotNodeVersion(dir, versionString);
versionString = findDotNvmrc(dir, versionString);
versionString = getNodeVersionFromTomlFile(dir, versionString, 'proto');
versionString = getNodeVersionFromTomlFile(dir, versionString, 'mise');

if (versionString) {
try {
version = NodeVersion.parse(versionString);
version.arch = version.arch || version.defaultArch;
break;
} catch (e) {
throw new Error('Failed to parse version in file: ' + versionFile, e);
} catch (error) {
throw new Error('Failed to parse version', error);
}
}

Expand Down
4 changes: 4 additions & 0 deletions test/mocks/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ const mockFs = {
try { cb(null, this.accessSync(path, mode)); } catch (e) { cb(e); }
},

existsSync(path) {
return true;
},

statSync(path) {
path = this.fixSep(path);
if (this.trace) console.log('statSync(' + path + ')');
Expand Down