I just migrated from nvm to pnpm runtime for Node.js version management.
Why? I pretty heavily moved to pnpm with all package-manager & workspace chore, I admire the tool, I just put more eggs in this basket :P
You can use it (in the stable version since v11) https://pnpm.io/cli/runtime
But since I have a lot of .nvmrc files that hold the node.js version, which can be used with nvm use, I made a small script that loads the correct node.js version when the terminal loads.
- nvm is per-shell by nature.
pnpm env useis global. The mismatch bites. .nvmrcis not read by pnpm. The hook bridges it.
Below is specific for the tools I use:
- brew (homebrew)
- zsh shell
# dah!
brew install pnpm
# Wire pnpm into your shell
pnpm setup
source ~/.zshrc
# Pick a default global Node.js version
pnpm env use --global 24.17.0Self-contained. Replaces the block pnpm setup created.
# ============================================================
# pnpm + per-directory Node (.nvmrc) — nvm replacement
# ============================================================
export PNPM_HOME="$HOME/Library/pnpm"
# pnpm's global bin dir AND PNPM_HOME (holds the node shim) both on PATH
case ":$PATH:" in *":$PNPM_HOME/bin:"*) ;; *) export PATH="$PNPM_HOME/bin:$PATH" ;; esac
case ":$PATH:" in *":$PNPM_HOME:"*) ;; *) export PATH="$PNPM_HOME:$PATH" ;; esac
autoload -U add-zsh-hook
# Walk up from $PWD to the nearest .nvmrc and print its path.
_pnpm_find_nvmrc() {
local dir="$PWD"
while true; do
[ -f "$dir/.nvmrc" ] && { print -r -- "$dir/.nvmrc"; return; }
[ "$dir" = "/" ] && return
dir="${dir:h}"
done
}
# Ensure pnpm has fetched Node <version> into ~/Library/pnpm/nodejs/<version>.
# Uses a throwaway project so pnpm's runtime downloads it on demand.
_pnpm_fetch_node() {
local v="$1" tmp
[ -x "$PNPM_HOME/nodejs/$v/bin/node" ] && return 0
command -v pnpm >/dev/null || return 1
print -r -- "Fetching Node $v via pnpm ..."
tmp="$(mktemp -d)" || return 1
print -r -- "useNodeVersion: $v" > "$tmp/pnpm-workspace.yaml"
print -r -- "{}" > "$tmp/package.json"
( cd "$tmp" && pnpm node -v ) >/dev/null 2>&1
rm -rf "$tmp"
[ -x "$PNPM_HOME/nodejs/$v/bin/node" ]
}
# Inject the wanted Node's bin dir into THIS shell's PATH (per-shell, like nvm).
# No `pnpm env use --global` — that flips one global Node shared by every terminal.
load-pnpm-runtime() {
export PNPM_HOME="${PNPM_HOME:-$HOME/Library/pnpm}"
path=("${(@)path:#$PNPM_HOME/nodejs/*/bin}") # drop any version dir we injected before
local nvmrc_path want
nvmrc_path="$(_pnpm_find_nvmrc)"
if [ -n "$nvmrc_path" ]; then
want="${$(<"$nvmrc_path")//[[:space:]]/}"; want="${want#v}"
if [ -n "$want" ]; then
if _pnpm_fetch_node "$want"; then
path=("$PNPM_HOME/nodejs/$want/bin" "${path[@]}")
print -r -- "node -> $want (${nvmrc_path/#$HOME/~})"
else
print -r -- "load-pnpm-runtime: Node $want unavailable (pnpm fetch failed)" >&2
fi
fi
fi
export PATH
}
add-zsh-hook chpwd load-pnpm-runtime
load-pnpm-runtime # DO IT AFTER ADDING pnpm to PATH!
# ============================================================