こなさんみんばんわ。
今日は 9 月 31 日なので明日から 10 月、いよいよ秋ですね(現実を見ろ)

そんな話は置いといて、Node.js のバージョン管理ツール Fast Node Manager(以下 fnm)は公式のスクリプトを使ってインストール & シェルのセットアップを済ませると本当に基本的な設定しかやってくれないので、インストール後にそのまま使い始めてあれっ? てなる方は割と多いんじゃないかと思います。

なので通常はまずシェルの設定ファイルを開いて、スクリプトによって追加された fnm 関連の行を README.md に書いてある内容に修正したりする訳ですが、実はここに書いてあるコードもちょっと不完全でして、我々が本当に期待しているような動作をしてもらうには、もう少し追記をする必要があるのです。

さて、その追加すべき設定とは? 答えは…本編でどうぞ🤣

念のために: fnm について簡単に

fnm は数ある Node.js のバージョン管理ツールのひとつで、同種のツールの中では比較的新しめの方かと思います。Rust で書かれていてスピードが速い・作業するプロジェクトに応じて自動で使用バージョンを切り替えてくれる(何ならインストールまでやってくれる)といったところが、よくメリットとして挙げられる特徴ですかね。

僕は今年に入ってから(ようやく)今まで使っていた nodebrew をやめて fnm に切り替えたのですが、nodebrew だと違うプロジェクトの作業に入るたびにいちいち手動でバージョンを切り替えないといけなかったのが自動でやってくれるようになり、それだけでも導入した価値はあったかなという感じです。

導入: 自動インストールスクリプトを使ったインストール方法

さて、そんな fnm には自動インストールスクリプトが公開されており、macOS または Linux 環境であれば、ターミナルから次のようなコマンドを入力することでインストールできます1。Windows 環境の場合は Winget や Chocolatery などといったものでインストールできるようですが、僕 Windows 使ってないのでよく知らないです😭 他の参考になる記事を当たってください…

curl -fsSL https://fnm.vercel.app/install | bash

で、この自動インストールスクリプトがやってくれるのは、

  1. fnm 本体バイナリのダウンロードと配置
  2. シェルの設定ファイルに 1. へパスを通す処理を書き込む
  3. 同様に、必要な環境変数の設定を行う初期化コードを書き込む

といったようなことです。2. と 3. については自分が使っている zsh を例にあげますと、おおむね次のような内容が .zshrc に追記されます(注: コメントはすべて僕による補足です)。

# fmn 本体がインストールされた場所が入る。内容は環境依存
FNM_PATH="/opt/homebrew/opt/fnm/bin"
if [ -d "$FNM_PATH" ]; then
  # fmn コマンドにパスを通す処理。なお macOS の場合 fmn
  # 本体は Homebrew でインストールされるので、この行は入らない
  export PATH="$FNM_PATH:$PATH"
  # 動作に必要な環境変数を設定する処理。全環境共通
  eval "`fnm env`"
fi

問題点: スクリプトは基本的な設定しかやってくれない

ですがこの自動インストールスクリプト、設定に関してはまじで基本的な設定しかやってくれないんですね。もちろん Node.js のインストールや使用バージョンの切り替えについてはこれだけでも動作するんですが、噂に聞いてた? ようなバージョンの自動切り替えなどの便利機能は有効にしてくれないんですよ。

なので、インストールしたあと試しに .node-version とかがあるディレクトリに移動してみても一切何も起こらなくて、「あれ、思ってたんと違う…」みたいに最初はなりがちですよね。えっなりませんか? オレはなったぞ😭

そこで: より便利に使うなら eval … の部分をこう変更しよう!

という訳で、このあたりを解消してより便利に fmn を使えるようにするために、シェルの設定ファイルに追加された eval "`fnm env`" の部分を次のように変更する(または手動インストールした人は自分で追記する)といいです:

eval "$(fnv env --use-on-cd --version-file-strategy recursive --shell zsh)"
シェルが zsh の場合の設定ファイル (.zshrc) の変更例。他のシェルの場合もコードは変わるが、同様に fnm env にこれらのオプションを渡すように変更する

各オプションについては公式の設定ドキュメントにも記載があるのですが、いちおう順に簡単な解説を入れておきます。

--use-on-cd

これは公式の README.md にも記載がありますし、すでに世の中にある色々な fnm の導入記事でもほぼ必ず触れられていることなので設定してる方は多いと思いますが、--use-on-cd を有効にすることでいわゆる自動バージョン切り替えが動作するようになります。

もう少し具体的にいうと、プロジェクトの作業ディレクトリに cd コマンドで移動した際に .node-versions.nvmrc といったファイルがあり2、そこに指定されている Node.js バージョンがインストール済みであれば使用するランタイムがそのバージョンに切り替えられますし、未インストールであれば確認メッセージが表示されるので y と答えることでインストールされ、使えるようになります。

jforg@local ~ % cd sample-project
Can't find an installed Node version matching v22.20.0.
Do you want to install it? answer [y/N]: y
Installing Node v22.20.0 (arm64)
00:00:02 █████████████████████████████████ 24.38 MiB/24.38 MiB (11.42 MiB/s, 0s)
Using Node v22.20.0
自動インストールの流れを示すシェルの出力サンプル。sample-project 内には 22.20.0 と記載された .node-version ファイルがあるが、インストールはされていない状態。メッセージに対して y enter を入力すると v22.20.0 のインストールが開始され、完了後は自動で切り替わる

--version-file-strategy recursive(ここ重要)

今回の記事のポイントはここです。これはバージョンの指定なしに fnm usefnm install をした場合に親階層を遡って .node-versions などを探し、見つかればそこに書かれたバージョンを指定したものとみなして実行してくれる機能ですが、実はもうひとつ重要な機能がありましてこれを recursive にしてないと作業ディレクトリを抜けてひとつ上の階層に移動した時などに、使用する Node.js のバージョンを普段使いしている(デフォルトの)ものに戻してくれないんですね。

例えば普段使いのバージョンが v22.20.0 で、my-project ディレクトリ内の .node-version では v20.19.5 が指定されているとします。設定ファイルで fnm env--version-file-strategy recursive オプションを渡してない場合は:

jforg@local ~ % node -v    # デフォルトのバージョンを確認
v22.20.0
jforg@local ~ % cd my-project
Using Node v20.19.5    # ← バージョンが切り替わった旨が出力される
jforg@local my-project % node -v    # 念のために確認
v20.19.5
jforg@local my-project % cd ../    # ひとつ上に移動…しても `using node …` が出ない
jforg@local ~ % node -v
v20.19.5    # ← バージョン変わってない…(´・_・`)

という状態ですが、--version-file-strategy recursive を渡すと:

jforg@local ~ % node -v
v22.20.0
jforg@local ~ % cd my-project
Using Node v20.19.5
jforg@local my-project % node -v
v20.19.5    # (ここまでは上と一緒)
jforg@local my-project % cd ../    # ひとつ上に移動すると
Using Node v22.20.0    # ← 今度はバージョンが切り替わった旨が出力された
jforg@local ~ % node-v    # 念のために確認
v22.20.0    # ← 戻りました

というふうになります。便利〜😆

先に挙げた公式の設定ドキュメントでもこのオプションは highly recommended だと言及されてるくらい重要な設定なんですが、なぜか既存の日本語の fnm 記事ではほぼ紹介されてないんですよね。なので、不思議に思いながらも「そういうもんなんかな…」とそのまま使い続けている人が多いかもしれません。ちゃんと戻るんですよ! というておきます。涙

--shell zsh

ここは説明しなくても分かると思いますが、環境変数を設定するコマンドはシェルによって違うので、ここで指定されたシェルに合わせて fmn env を出力してね、というオプションですね。もちろん zsh の部分は自分が使ってるシェル (bash, zsh, fish, powershell) を入れてください。

とはいえ、公式のスクリプトが省略するくらいですし、あと普通に考えてこれは省略しても自動判別で適したものを出力してくれるんだとは思います😅 だからまぁ念のために指定しておく、程度のものじゃないですかね。知らんけど。

補足: Issue と Pull request は上がっている

「ていうかそんな全力で推奨みたいな設定なら README でもこれがオススメやで! って言及しといてくれよ…」とかちょっと思ったりする訳ですが、これはすでに Issue や Pull request が上がっております。

なので、そのうちマージされて README に反映されるかもしれません。Issue の方が一年以上前のものなのに未だどうにもなってないのが気掛かりではありますが、何か流れも速そうだし追い切れてないんですかね。まぁ本体のバグじゃないから「なるはやで!」ともならないでしょうし、気長に待ちましょう。

あとは「オススメならスクリプトが最初からこれを有効にすればいいのに…」と思うかもしれませんが、この辺はやっぱりシェルの挙動に手を入れることになりますんで、基本的にはユーザーの判断(自己責任)で有効にしてね…ということなのではないかな、と。それはそれで良心的な判断だと思います。

そんな訳で

今日は Node.js のバージョン管理に fnm を使うなら、シェルの設定ファイルで fnm env--use-on-cd にプラスして --version-file-strategy recursive をオプションで渡すと、作業ディレクトリを抜けた時に Node.js のバージョンが自分のデフォルトに戻るからいいぞ! という話をしてみました。

この記事を書くにあたって少し調べてたら、この「ディレクトリを抜けてもバージョンが変わらない」ということに言及してる記事はいくつかありましたが、どなたも少し疑問に思いつつも、そのまま使ってるような印象を受けました。本文中にも書きましたが、本当にこの話は日本語だと書いてるところがほぼなくて、それだとやっぱり知る機会もないからそうなるのかもしれないですね。なので、この記事がそういった方の手助けになればいいかなと思って書かせていただいた次第です。

まぁでもこの話、実は僕もつい最近気が付いたばっかりでして😅 最初に fnm をテストした時にはご多分に漏れず --use-on-cd しか指定してなくて、ディレクトリ抜けたらバージョン戻ってくれた方がいいし、というだけの理由で Volta をしばらく使ってました…

でも自分的には fnm の方が仕様的にも色々と都合がよかった3ので、やっぱり切り替えるかーともう一度試してみたら「あれバージョン戻るやん!」となりまして、じゃあ前回と何が違うねんと思ったらこのオプションだった…という流れです。

結論: 最初にオプションは色々試しておきましょう。涙

  1. 必ずこのスクリプトを使わないといけない訳ではなく、Homebrew などを使って手動でインストールもできます(設定は自力でやる)。というか macOS の場合、本体の DL はスクリプトが裏で brew install fmn してるだけだったりします… 

  2. これに加えて package.jsonengines フィールドの内容も加味してくれるようです(設定が必要ですが、どうもデフォルトになってる感じです)が、このフィールドは semver 指定ができるので、あまりカッチリしたバージョンを指定する訳じゃないんですよね。なので開発環境用の指定というよりは、とりあえず動くバージョンを入れるという用途かと思います。 

  3. 色々ありますが、一番の理由はこのサイトは Netlify を使ってるので package.json の独自フィールドを使う Volta より .node-version ファイルでバージョン管理できる方がいいのです。