Automatically switching Node.js version upon cd with n

In , I was exploring the possibility to read a Node.js version constraint from the package.json file of every directory upon entering them, and switch Node.js versions accordingly, relying on nvm's default version if a specific one was not required.

The script I hacked together does the job in most cases, but suffers from a few limitations:

  • it only looks for package.json in the (new) current directory, rather than the closest one in the tree
  • it is pretty bad at dealing with file ranges, just parsing the first correct version number it could find in them, which turns out to be their lower end

I therefore dug a bit further and found out that n, nvm's main competitor, can perfectly handle those two aspects natively! Just run n auto anywhere and it will look for the closest version constraint in the tree and interpret it correctly, reading from package.json but also from other files - even .nvmrc!

Running it systematically has one major downside, though: n actually reinstalls Node.js upon every version switch, and even if it uses its local cache for versions it has already downloaded, it turns out to be pretty slow; and unfortunately, there does not seem to be an option to cancel the switch if the current version already matches the expected one. Well, in that case, why not write something of our own to do exactly that?

In order to properly handle ranges, we need a list of all available Node.js versions; fortunately, this is pretty easy to get by running n ls-remote --all, the result of which we will store in a file that then gets passed as a parameter to our script.

const fs = require("fs");
const { exec } = require("child_process");
const semver = require("semver");
const readPkgUp = require("read-pkg-up");

// Build array of available versions from input file
const versions = fs.readFileSync(process.argv.pop(), { encoding: "utf8" }).split("\n");

// Read closest package.json file
readPkgUp().then(file => {
try {
// Determine highest version satisfying its version constraint
const target = semver.maxSatisfying(versions, file.packageJson.engines.node);

// Check current node version and switch if necessary
exec("node -v", (error, stdout) => {
if (semver.clean(stdout) !== target) {
console.log(`Switching to node v${target}`);
exec(`n -p ${target}`);
} catch (e) {} // fail silently

Reusing our zsh bit from last time:

autoload -U add-zsh-hook
n ls-remote --all > path/to/.node-versions # update version list

switch-node-version() {
node path/to/runn path/to/.node-versions

add-zsh-hook chpwd switch-node-version

And there you have it! The script is if you are interested.