Today I'm going to take a stab at trying to understand what nvm is and how it works. nvm stands for node version manager and is made and maintained by ljharb. He has quite the pedigree as this is the first time I actually even checked who made nvm. Open source really is a faceless entity in my eyes. So much software I use without really knowing who's behind it. Anyway! (Maybe I'll think about that idea of facelessness in the future)
nvm is quite brilliant. It's a way of managing node dependencies that I think anyone can sort of come up with. The core idea of nvm is that we can install the node and npm executable into a directory under the user and then munge the PATH to use a specific version of node. Laid out, it's quite simple, we install an executable and then reference that executable to run our programs and install our dependencies. nvm simply makes all of that invisible.
(This entire post is couched in the fact that I could get anything and everything wrong at any or every point.)
I think a good way of understanding this is to manually install two versions of node and npm and see how we can manually manage node versions!
The first step is to create a directory to house our little test project.
> mkdir node_cache
> cd node_cache
Let's install the oldest version of node, v0.1.100.
https://nodejs.org/dist/v0.1.100/
> wget https://nodejs.org/dist/v0.1.100/node-v0.1.100.tar.gz
> tar xvf node-v0.1.100
> cd node-v0.1.100
> ./configure
> make
This should generate a build with our node executable but there should also be a symlink to it. Create a simple test.js file with a console.log and we do a quick test!
> ./node -v
0.1.100
> ./node test.js
Hello, World
Now we have a one of the oldest versions of node installed. (If you have problems building node, then just grab another version of node, I only grabbed the oldest because it seemed poetic.) There doesn't seem to be npm bundled with this version so it probably came later. It's fine but it would be nice to also have multiple versions of npm but the idea will still come across. I hope.
Now let's install the latest, which at the time of this post is 17.3.0.
https://nodejs.org/dist/v17.3.0/
> cd ..
> pwd
/node_cache
> wget https://nodejs.org/dist/v17.3.0/node-v17.3.0-linux-x64.tar.xz
> ls
node-v0.1.100 node-v17.3.0-linux-x64
The latest come pre-built so inside the 17.3.0 directory there is a bin directory with an executable for node and npm. Much easier!
Once again let's create a quick test script to verify the latest version of node works.
> cd node-v17.3.0-linux-x64
> ./bin/node -v
v17.3.0
> ./bin/node test.js
Hello, World!
So now we have two versions of node and one version of npm.
Navigate away from our node_cache folder and let's begin our excersise in futility.
What we want to do is try and use the two version of node that we just installed.
We can do this by using the absolute path to reference each one.
> /home/nivethan/bp/node_cache/node-v0.1.100/node -v
0.1.100
> /home/nivethan/bp/node_cache/node-v17.3.0-linux-x64/bin/node -v
v17.3.0
We can also use this absolute path in our node projects and we can use the absolute path to npm to manage our node projects. So you can see how installing multiple versions of node is actually pretty easy, the referencing of it is a bit of a pain in the ass however.
We would ideally like to be able to just type in node -v.
We can do this by adding the path to the node executable to our path.
> export PATH="/home/nivethan/bp/node_cache/node-v0.1.100/:$PATH"
> node -v
0.1.100
We add the path to our first version of node to the beginning of our path and voila! When we do node, the shell searches the path for the first instance of our command and we find it in our node_cache folder.
Now by playing with this PATH variable we can swap between our two versions.
> export PATH="/home/nivethan/bp/node_cache/node-v17.3.0-linux-x64/bin/:$PATH"
> node -v
v17.3.0
We're obviously polluting our PATH variable but this idea of messing with the PATH is at the core of what nvm does. nvm goes and downloads the version we ask for when we use the install command and it then munges the path when we do a use.
As you can see the logic of it is quite simple! All of this can be done using a shell script and that is exactly what nvm does! nvm is a giant shell script that manages our PATH variable node versions and it makes dealing with node so much more sane.
You or even I could probably write a stripped down nvm where we install node versions to a folder and then do some string manipulation on the path when we want to switch node versions. This will be an excersise left to the reader :).
Before we get off this ride, let's just take a peek at the nvm source code. As a fan of bash scripting it is quite nice and I loved learning that such a useful utility was actually writtin a shell script (somehow I missed the -sh part of nvm-sh).
https://github.com/nvm-sh/nvm
> git clone https://github.com/nvm-sh/nvm.git
> cd nvm
nvm.sh is the main file and the source for everything. We can take a look inside and browse around for everything.
The key pieces that I wanted to look at was the nvm ls command, nvm install command the nvm use command.
nvm ls lists out the current node versions we have and it is under the nvm_ls() function. This is around line 1250 and you can see that the core of this function is a find command. Makes sense though I can imagine it being an ls command in a very simple version of nvm.
VERSIONS="$(command find "${NVM_DIRS_TO_SEARCH1}"/* "${NVM_DIRS_TO_SEARCH2}"/* "${NVM_DIRS_TO_SEARCH3}"/* -name . -o -type d -prune -o -path "${PATTERN}*" \
| command sed -e "
s#${NVM_VERSION_DIR_IOJS}/#versions/${NVM_IOJS_PREFIX}/#;
s#^${NVM_DIR}/##;
\\#^[^v]# d;
\\#^versions\$# d;
s#^versions/##;
s#^v#${NVM_NODE_PREFIX}/v#;
\\#${SEARCH_PATTERN}# !d;
" \
-e 's#^\([^/]\{1,\}\)/\(.*\)$#\2.\1#;' \
| command sort -t. -u -k 1.2,1n -k 2,2n -k 3,3n \
| command sed -e 's#\(.*\)\.\([^\.]\{1,\}\)$#\2-\1#;' \
-e "s#^${NVM_NODE_PREFIX}-##;" \
)"
nvm use is the command we use to switch node versions. This is implemented inside the conditional that handles the use keyword. This is around line 3600. You can see here that it does some string manipulation on the PATH variable.
# Change current version
PATH="$(nvm_change_path "${PATH}" "/bin" "${NVM_VERSION_DIR}")"
nvm install is the command we use to download and untar a node version. This is implemented in the nvm_download() function which you can find around line 120.
curl --fail ${CURL_COMPRESSED_FLAG:-} -q "$@"
There is alot of complexity in the nvm shell script but I'm guessing it all slowly got added in. I'm curious what the oldest versions looked like as I imagine it's quite simple.
Hopefully this explantion makes sense and is actually right!