自动切换 Node.js 版本号 📍 Pin Node.js Version Per Project

307 阅读3分钟

nodejs_cover_photo_smaller_size.png

效果

本文通过 hook 原生的 cd 命令,实现了切换目录的同时自动切换 Node.js 版本号,让依赖不同 Node.js 版本的项目能共存。

image.png

打开终端或进入目录之前自动搜索当前项目设置的 Node.js 版本

支持 .nvmrc 和 package.json engines 字段

🤒 问题

当遇到多个项目每个项目依赖的 Node.js 版本不相同时,我们需要手动用 nvm use 切换版本号,太麻烦还容易忘记。只要重复做的事情就要问问能否自动化

我们想要的效果是:cd 到某个项目,自动切换 Node.js 版本。

nvm 自动切换 macOS 支持但不支持 Windows,且定义在 package.json 中的版本号也无法切换。 Volta 支持 pin,推荐大家使用。 fnm 支持 fnm env --use-on-cd,但如果 Volta 和 fnm 比还是选择前者,fnm 采用 cd 后切换复用同一个 node.js 实例,导致不使用 cd 而通过 VSCode 切换窗口会出现错误的 Node.js 版本。而 Volta pin 不存在该问题,因为 Volta 的版本切换是在运行命令时动态解析的(如运行 node 或 npm 时),而不是在 cd 时切换。

我已经用了 nvm 不想折腾 volta 索性自己给 nvm 添加一个自动切换功能!

🏗️ 实现

一般版本号会存在两个地方:.nvmrc 或 package.json 的 engines 字段。

.nvmrc:

22

package.json:

{
  "name": "foo",
  ...
  "engines": {
    "node": ">=22.0.0"
  },
  ...
}

思路:我们可以重写 cd 命令,在 Linux 下可以使用 alias。

📁 覆写 cd 命令

.zshrc:

cd() {
    builtin cd "$@" && change_node_version_per_project
}
  • builtin cd 表示使用内置命令
  • "$@"cd 所有参数传给内置 cd 命令,相当于 js 的 ...args,如此我们并没有破坏 cd 的功能,而是对其进行了增强。

上述代码翻译成 JS 就是:

const originalCd = window.cd

function cd(...args) {
  originalCd(...args)
  change_node_version_per_project()
}

🎯 实现 change_node_version_per_project

change_node_version_per_project() {
    local nvmrc_path="./.nvmrc" # Best Practices:局部变量使用 `local` 是好习惯避免意外定义全局变量

    if [[ -f "$nvmrc_path" ]]; then
        local node_version=$(grep -oP '\d+(\.\d+)?(\.\d+)?' .nvmrc | head -n1)
        
        echo '---------------------------------------'
        echo "node_version in .nvmrc is $node_version"
        echo '---------------------------------------'

        nvm use "$node_version" || use_from_pkg_json
    else
        # echo "No .nvmrc file found in the current directory."
        use_from_pkg_json
    fi
}
  1. 优先从当前目录的 .nvmrc 获取版本号,通过正则表达式匹配,并且只取第一个 (head -n1)。
  2. nvm use "$node_version" || use_from_pkg_json:用 || 的用意是如果版本号切换报错,则兜底去 package.json 获取版本号。
  3. 如果不存在 .nvmrc 则去 package.json 获取版本号 use_from_pkg_json

迭代 1:性能优化

如果当前版本已经匹配则无需切换。

- nvm use "$node_version" || use_from_pkg_json

+ cur_ver=$(node -v)
+ if [[ $cur_ver =~ ^v$node_version\. ]]; then
+  echo 'Current node version "'$cur_ver'" matches "'$node_version'", no need to change.'
+ else
+  nvm use "$node_version" || use_from_pkg_json
+ fi

📦 实现 use_from_pkg_json

use_from_pkg_json() {
    if [[ -f "./package.json" ]]; then
      local node_version=$(grep -A 2 engines package.json | grep -oP '[0-9]+' | head -n1);

      echo '---------------------------------------------'
      echo "engines.node in package.json is $node_version";
      echo '---------------------------------------------'

      # local pkg_json_path="package.json"
      # local node_version=$(jq -r '.engines.node' "$pkg_json_path" | cut -d' ' -f1)

      if [ -z "$node_version" ]; then
          echo "No node version specified in package.json."
      else
          nvm use "$node_version"
      fi
  fi
}

grep -A 2 engines package.json:从 package.json 中匹配 engines:使用 grep -A 2A 表示 After)即从匹配处往下多获取两行

匹配示例如下:

  "engines": {
    "node": ">=22.0.0"
  },
  1. if [ -z "$node_version" ]: 如果匹配结果为空则打印未找到,否则使用 nvm use xxx 切换版本号。

至此我们的功能完整实现了。

💐 效果

如果某个项目没有配置 nvmrc 但是指定了 engines:cd 目录

---------------------------------------------
engines.node in package.json is 16
---------------------------------------------
Now using node v16.20.2 (64-bit)

如果有 nvmrc:cd 目录

---------------------------------------
node_version in .nvmrc is 22
---------------------------------------
Now using node v22.7.0 (64-bit)

VSCode 打开某个项目的 terminal 也会使用 cd 命令,故也会自动切换。