效果
本文通过 hook 原生的 cd 命令,实现了切换目录的同时自动切换 Node.js 版本号,让依赖不同 Node.js 版本的项目能共存。
打开终端或进入目录之前自动搜索当前项目设置的 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
}
- 优先从当前目录的
.nvmrc获取版本号,通过正则表达式匹配,并且只取第一个 (head -n1)。 nvm use "$node_version" || use_from_pkg_json:用||的用意是如果版本号切换报错,则兜底去package.json获取版本号。- 如果不存在
.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 2(A 表示 After)即从匹配处往下多获取两行
匹配示例如下:
"engines": {
"node": ">=22.0.0"
},
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 命令,故也会自动切换。