Skip to content

Instantly share code, notes, and snippets.

@fredrb
Last active July 5, 2018 20:05
Show Gist options
  • Save fredrb/3b3a2bde25233678d0e37a0414b4fb6c to your computer and use it in GitHub Desktop.
Save fredrb/3b3a2bde25233678d0e37a0414b4fb6c to your computer and use it in GitHub Desktop.

DIY Node Version Manager

I have developed with NodeJS professionally and for personal projects for somewhat four years now. JavaScript and the NodeJS ecosystem has never been my favorite stack, but I always managed fine. As the need to manage application releases and parallel projects using Node grew, I started looking for a project such as Ruby's rvm. 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, since 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 that is executing time zsh -ic "exit" . This yield 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 I wanted to profile the startup execution of zsh to discover who was the villain behind this enormous time waste. ZSH has a profiling module called zprof. All you have to do is load the module (zmodload zsh/zprof) and 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 tried to make it work.

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 GitHub issue with people facing the same issue that I was facing, with a helpful comment of 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.

Re-inventing 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 see the final work here [4].

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 single-line the download of the any version:

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 [5].

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 [6].

  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.

Symlink to PATH folder

Given the download went fine and 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. At first, it was just as a proof of concept that it would require a less bloated software to manage node versions in my computer. It doesn't mean I'll throw nvm out of the window and never look at it again, but I might give it a shot using my own script to manage versions for while. I've been using it for the last few weeks and it seems alright so far, but like every personal project, it faces issues when it needs to scale.

I'd also like to point out that nvm is a great project and I learned a lot just by reading their code and understanding the flow of things, its a huge project that is being used for a while and thus provides higher levels of stability. (On the other hand this script works natively with FISH)

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] http://knowyourmeme.com/memes/good-luck-im-behind-7-proxies.

[2] More info on zprof here http://jb-blog.readthedocs.io/en/latest/posts/0032-debugging-zsh-startup-time.html and http://zsh.sourceforge.net/Doc/Release/Zsh-Modules.html#The-zsh_002fzprof-Module

[3] nvm-sh/nvm#1277 (comment) apparently, there is a standard flag to do this (--no-use). It is listed in Readme instructions.

[4] Link to GitHub repository

[5] 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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment