Making Nvm And Volta Co-exist With Zsh

I recently learned about Volta, which is just a better tool than nvm. It is noticably faster on my 4 year old MacBook. It auto-downloads node versions when needed.

How I previously set up nvm

I have been using nvm and have been rather happy with it. I set up a hook to automatically use the right version if a folder contains a .nvmrc file:

export NVM_DIR="$HOME/.nvm"
export PATH="$PATH:$HOME/.local/bin"

[ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"

autoload -U add-zsh-hook
load-nvmrc() {
  local nvmrc_path=`pwd`/.nvmrc
  
  if [ -e "$nvmrc_path" ] ; then
    local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")

    if [[ "$nvmrc_node_version" == "N/A" ]]; then
      nvm install >/dev/null
    elif [[ "$nvmrc_node_version" != `nvm version` ]]; then
      nvm use >/dev/null
    fi
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc

This actually does exactly the same as Volta, just with a little zsh magic sprinkled on top.

With this, I actually don't really need Volta at all.

But, I do work with other people. Some of them like nvm. Some of them like Volta. So I need both in my life.

Setup

If we pretended that I could just stop using nvm from one day to the next, it could be as simple as removing the bit above from my .zshrc file and running the following:

brew install volta
volta setup

Co-existing

It does get a little tricky when you have some projects using nvm and others using volta.

Now I need to extend my nvm hook to initialise nvm when appropriate, and I also need a hook to uninitialize nvm and initialise volta when that is appropriate.

So, we extend the script above a little:

export NVM_DIR="$HOME/.nvm"
export PATH="$PATH:$HOME/.local/bin"

autoload -U bashcompinit
bashcompinit

autoload -U add-zsh-hook
load-nvmrc() {
  local nvmrc_path=`pwd`/.nvmrc
  
  if [ -e "$nvmrc_path" ] ; then
    if [[ "$__npm_initialized" != "1" ]] ; then
      # explicitly do not try to co-exist with volta
      clear-volta
      
      __npm_initialized=1
      [ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"
    fi

    local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")

    if [[ "$nvmrc_node_version" == "N/A" ]]; then
      nvm install >/dev/null
    elif [[ "$nvmrc_node_version" != `nvm version` ]]; then
      nvm use >/dev/null
    fi
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc

Next, we need a similar hook for volta:

init-volta() {
  if [[ "$VOLTA_HOME" == "" ]] ; then
    export __npm_initialized=0
    export VOLTA_HOME="$HOME/.volta"
    export PATH=`echo $PATH | perl -ane 's|:.*?\.nvm/.*?:|:|;s|^.*?\.nvm/.*?:||;print'`
    export PATH="$VOLTA_HOME/bin:$PATH"
  fi
}

clear-volta() {
  export VOLTA_HOME=
  export PATH=`echo $PATH | perl -ane 's|:.*?volta/bin:|:|;s|^.*?volta/bin:||;print'`
}

autoload -U add-zsh-hook
load-volta() {
  local package_path=`pwd`/package.json
  
  if [ -e "$package_path" ] ; then
    if [[ `jq -cM '.volta' $package_path` != "null" ]] ; then
      if [[ "$VOLTA_HOME" == "" ]] ; then
        init-volta
      fi
    fi
  fi
}
add-zsh-hook chpwd load-volta
load-volta

Conclusion

Some of the magic of not having to do any chpwd hooks at all are clearly negated by this, but such is the world of working on multiple heterogenous projects.