Skip to the final result: GitHub Repository

JavaScript and the NodeJS ecosystem have never been my favorite technologies in a stack, but I have managed fine in the last four years of professional and personal projects. With the growing need to manage application releases and parallel projects using Node, the lack of version manager such as Ruby’s rvm was indisputable. I was alone back then, and was managing different Node versions myself. Luckily the community had already provided a project called nvm, which had a very similar interface to rvm. Me and nvm got well together and became good friends, as I was with my former buddy rvm in the past. Things went pretty well for a good couple of years until I had to travel to other technologies and we stopped talking. Fast forward to a few months ago, I was setting up a new workspace, and decided to reach out to nvm again, due to the fact I was about to start another project using node. Unfortunately, the experience was not as joyful.

I like to make my own .zshrc configuration, and after a few weeks in the project, I noticed the startup of interactive shells was pretty slow. Specially in my work computer (which is connected to an internal network behind seven proxies I should mention [1]). First, I wanted to know how long was the startup taking, the simplest way to do this, is to run the command time zsh -ic "exit". The result yielded almost 2 seconds. That’s outrageous! Having a very low attention span and lack of focus, this was a productivity killer. Most of the times halfway through the first second I was already reaching out for my phone while the terminal window was loading. Not satisfied with the current situation, I wanted to profile the startup execution of zsh to discover who was the villain behind this enormous waste of time. zsh has a profiling module called zprof. All you have to do is to load the module (i.e zmodload zsh/zprof) and to call a built-in function zprof at the end of your file [2]. To my surprise, the responsible lines were sitting at the bottom of my .zshrc and were only needed by nvm:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 

Exporting the path is surely not a problem, but the invocation of nvm.sh was kinda of a dead giveaway that it was a monstrous time consuming script. I was right. The single execution of \. "$NVM_DIR/nvm.sh" was taking more than half of my execution time.

Trying to fix NVM

It was not a single heartbreaking moment that made my long relationship with nvm be shattered into pieces. I was willing to put an effort to squash those 2 seconds of procrastination trigger into a more sane milliseconds-based shell startup.

The first attempt of reconciliation was accepting that perhaps nvm was designed for bash only (given some hints in README), and since they offered a zsh plug-in I figured that the execution would be faster if I used the plug-in instead. It was not.

Then I landed on a discussion in a GitHub issue with people having the same bottleneck in shell startup that I was having. The solution shifted to a workaround on how to delay the execution of that script until you really needed a NodeJS command [3].

It works. But it didn’t feel right. I was not ok with the fact of putting this sorcery in my .zshrc just to postpone the execution of a script that was going to be executed anyway. It should be simpler. As a matter of fact, why am I not having only a few symlinks to my NodeJS installation other than having to load zsh built-in functions to select which Node version I wanted? I could solve this by only adding $NODE_INSTALLATIONS/bin and $NODE_MODULES_BINARIES to path and symlink the current NodeJS binaries folder and binaries from node modules for current version into the folders in path. This works, doesn’t it? I wanted to put it to test.

Reinventing the wheel

I can’t help myself by creating homemade versions of tools I use. They’re usually not fit for many users and don’t scale very well, but it’s a good learning exercise to understand the design decisions behind software we use on daily basis. So I took the quest of implementing the simplest version of nvm in my controlled environment and see if my idea would play out well. You can check the final work here.

Node tarballs

All the tar.gz files for node releases can be found in their HTTP file server. Not only listed by version but also release branches (latest, argon carbon and boron). The tarball has everything you need to run node applications, as it comes both with node and npm binaries. A simple bash script using wget can be used to solve this part, as a matter of fact, you can download any version of Node with a single-line command:

wget -O /tmp/node-$version.tar.xz "https://nodejs.org/dist/v$version/node-v$version-$OS-$ARCH.tar.xz"

Unpacking and moving into an installation folder is simply running tar -xvf $file -C $install_folder

Fetching the latest version

The tricky part, in my opinion, is getting the actual version you’d like to download. As much as we have folders for each branch, the filename is a concatenation of both the version and your computer spec. Therefore the first step is discovering the actual version we’re targeting.

We could expect the user to input the version every time a new download is requested, but that’s not optimal. Ideally, we want two things:

  • If no version is specified always get latest
  • Allow for named releases (carbon, argon and boron)

To do this, a little bash black magic was introduced and another get request must be executed [4].

wget -qO /dev/stdout $DIST_URL | grep "node-v" | sed 's/<a href="node-v//' | head -1 | sed -n 's/\([0-9]*\.[0-9]*\.[0-9]\).*/\1/p'

Command Breakdown (optional)

1) wget accepts a -q parameter, which stands for quiet mode. This avoids printing loading bars and other things in the standard output that doesn’t interest us. The other parameter -O /dev/stdout redirects the downloaded content into stdout [5].

2) Having the result in stdout we can grep for the lines that output the pattern “node-v”.

3) sed is used to replace the occurrences of <a href="node-v to an empty string, leaving <major>.<minor> as the first things in each line.

4) Only the first line is relevant, so we filter it out with head -1

5) Then we use a sed regex to extract only the version number. Thus leaving us a single output line with version number for the folder specified in the reuqest. e.g. 10.6.0.

Given that the download went fine, and now we have the latest version of NodeJS in an installation folder (e.g. $HOME/.config/node_versions/<version>/bin), we could add that folder to path. However, whenever we change versions, the $PATH variable would have to change along with it. This is, again, not optimal.

So my approach to this was keeping a symlink folder such as $HOME/.config/node_versions/current/bin/ which would point to my current installation. If I was to, for example, select version 4.9.1, creating a symlink from $HOME/.config/node_versions/4.9.1/bin to $HOME/.config/node_versions/current/bin is sufficient.

Not only we’d get node and npm binaries, any module installed globally that has executable files would also find its way to this folder. With that, we cover all the functionality we need to create a node version manager.

Command-Line application

Piecing all the above into a command-line application was interesting. I tried to keep a similar API from its counterparts nvm and rvm.

usage: nv command [options]

command:

  list [options]:
    prints all installed versions
    options:
      --remote|-r: print available release versions

  get <version>:
    downloads and install version number
    (e.g. nv get 10.4.0)
    version can be either version number or release names (latest|carbon|boron|argon)
    (e.g. nv get latest)

  use <version>:
    selects version as current node version
    (e.g. nv use 10.4.0)

Outcomes

It was an interesting experience to develop this from end-to-end. This project started as a proof of concept that it would require a less bloated software to manage node versions in my computer. The end result, however, seemed like an application I could really use on a daily basis. It doesn’t mean I’ll throw nvm out of the window and never look at it again, but I was willing to give it a shot using my own script to manage versions for while.

After using it for a little more than two weeks, I was not disappointed at what I have put together. Requiring less intrusive changes in my environment is the way to go when it comes to such a version manager. Changing node versions relies on the change of single symlink and exposing them to $PATH variable is a single bash line with the root node folder.

I’d also like to point out that nvm is great and I learned a lot just by reading their code and understanding the flow of things, it’s a huge project that has been around for quite some time, thus provides much more stability.

Here is the repository, if you’d like to give it a try, here’s a install script.

wget -qO- https://bitbucket.org/fredericorb/nv/raw/525d6a81a3c8f2ac9cb36b97d632e482ff4b83ff/install.sh | bash

Thanks for reading.

References

[1] Good Luck, I’m behind 7 proxies reference

[2] More info on zprof here and here

[3] Apparently, there is a standard flag to do this (--no-use). It is listed in Readme instructions. Link to the github issue discussion

[4] At this point I did try to cache something or keep something like a “versions list file”, but maintaining this file would also require eventual requests. And thus, seeing that the flow of the application would be somewhat cumbersome, I decided to simply request for the latest version of each named branch (latest as default) before download.

[5] -O /dev/stdout can be simplified to -O- which ends up being wget -qO- ...