Nushellでfnmを使いNode.jsのバージョン管理を行ってみる

Nushellでfnmを使いNode.jsのバージョン管理を行ってみる

Clock Icon2024.09.10

しばたです。

私は以前からNode.jsのバージョン管理にnvmを使っていたのですが、常用するシェルをNushellに変えたため使えなくなってしまいました。
これまではNode.jsを使う時だけシェルをBashに切り替えていたのですが、段々と面倒さが増してきたので今回バージョンマネージャーをfnmに変更することにしました。

本記事ではNushellでfnmを使える様にするまでの手順を共有しようと思います。

Why fnm?

nvmはGitHub上で

Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions

と謳っている通りPOSIX準拠のBashスクリプトの集合体であるためNushellでの利用は絶望的です。

このため他のバージョンマネージャーを使う必要があり、GitHub上でのディスカッションを見たところfnmを使っている有志がいたため私もそれに倣うことにしたのが理由となります。

https://github.com/nushell/nushell/discussions/4220

ただ、本日時点でfnmはNushellに正式対応しておらずPull RequestやIssueは上がっているもののまだ道半ばといったところです。

  • Nushell support #463
    • 3年前からのIssueで本記事の内容のベースとなります
  • Nushell support #801
    • 2年前にPull Requestが出ているものの動きが止まってしまっている...

注意事項

fnmのインストール

まずはfnmのインストールを行います。

こちらはNushell上であってもGitHubにある手順がそのまま使えます。

https://github.com/Schniz/fnm?tab=readme-ov-file#installation

Linuxでのインストール手順

Linux環境の場合はセットアップスクリプトからインストールします。

Nushell on Linux
# 他シェルのインストール手順と同じ
curl -fsSL https://fnm.vercel.app/install | bash

通常であればfnmのバイナリを配置した後に実行シェルに応じた初期設定を行うのですが、Nushellは非対応のため

Could not infer shell type. Please set up manually.

といったメッセージと共に処理が途中で終了します。

Nushell on Linuxでの実行例
~> curl -fsSL https://fnm.vercel.app/install | bash
Checking dependencies for the installation script...
Checking availability of curl... OK!
Checking availability of unzip... OK!
Downloading https://github.com/Schniz/fnm/releases/latest/download/fnm-linux.zip...
######################################################################## 100.0%
Could not infer shell type. Please set up manually.

これでもバイナリは配置されているので問題ありません。
ちなみに、明示してバイナリの配置だけ行いたい場合は以下の様に--skip-shellパラメーターを指定してやることもできます。

Nushell
# Nushellの場合は --skip-shell パラメーターを指定する形の方がベター
curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell

バイナリのインストール後はfnmの実行バイナリのあるディレクトリに対しPATHを通しておきます。
セットアップスクリプトの中身を読むと、

  • $HOME/.fnmディレクトリがある場合は$HOME/.fnm配下にバイナリを配置
  • $XDG_DATA_HOME環境変数が指定されている場合は$XDG_DATA_HOME/fnm配下にバイナリを配置
  • それ以外の場合は$HOME/.local/share/fnm配下にバイナリを配置

という優先順位になっていたので環境に応じた設定を行ってください。

私の開発環境の場合は$HOME/.local/share/fnmが選ばれていたのでenv.nuファイルに以下の内容を追記することでPATHを通しました。

env.nu
# env.nu に以下の記述を追記
$env.PATH = ($env.PATH | prepend $"($env.HOME)/.local/share/fnm")

Windowsでのインストール手順

Windows環境の場合はWinGet、Scoop、Chocolateyといったパッケージマネージャからインストールできます。

Nushell on Windows
# WinGetを使っている場合
winget install Schniz.fnm

# Scoopを使っている場合
scoop install fnm

# Chocolateyを使っている場合
choco install fnm

これらのツールからインストールした場合は自動でPATHが通っているので追加作業は不要です。

具体的な手順までは紹介しませんがGitHubのリリースページにあるZipファイルを直接ダウンロードして配置しても構いません。
この場合は自分でPATHを通す様にしてください。

Nushellでfnmを使うための初期設定

fnmのインストール後は初期設定を行います。
本日時点で最新のfnm 1.37.1を対象としています。

初期設定はシェルに応じた内容となり、例えばBashであれば

.bashrc
# .bashrcに追記するコマンド
eval "$(fnm env --use-on-cd --shell bash)"

といった感じでfnm envコマンドの実行結果をevalさせる手順を.bashrcに追記する形になっています。

この際に--shellパラメーターで利用するシェルを指定するのですが、残念ながらNushellは非対応なのでそのまま利用することは出来ず同等の処理を自分で書いてやる必要があります。

fnm envコマンドで生成される内容自体は

  • fnmで使う各種環境変数を自動生成
  • FNM_MULTISHELL_PATH環境変数で指定されるパスをPATH環境変数に追加
    • Windows環境だけ少しパスが異なる
  • --use-on-cdパラメーターを指定した際はディレクトリ移動時にHookする処理を追加で自動生成

であり、そこまで難しいものではないので順に解説していきます。

設定1 : 環境変数の自動生成 (とPATHの追加)

環境変数の自動生成については以下のコマンドで代替可能です。
fnm envコマンドにはJSON形式で環境変数を吐く--jsonパラメーターがあるのでこれを使います。

Nushell
# JSON形式で環境変数を吐く--jsonパラメーターがあるのでこれを使って環境変数を設定
fnm env --json | from json | load-env

GitHubのコメントを参考にもう少しチェック処理などを追加してやり最終的に以下の様にすればOKです。

env.nu に追記する処理
# GitHubのコメントをベースに Nushell 0.97.1 で動作する様に少し改変
if not (which ^fnm | is-empty) {
    # 環境変数の設定
    ^fnm env --json | from json | load-env
    # PATHの追加
    let node_path = match $nu.os-info.name {
      "windows" => $"($env.FNM_MULTISHELL_PATH)",
      _ => $"($env.FNM_MULTISHELL_PATH)/bin",
    }
    $env.PATH = ($env.PATH | prepend [ $node_path ])
}

この処理をenv.nuに追記してやります。

設定2 : ディレクトリ移動時にHookする処理を追加

ディレクトリ移動時にHookする処理はBashだと以下の通りとなり、単純に「移動先ディレクトリに.node-version.nvmrcがある場合はfnm use --silent-if-unchangedコマンドを実行する」ものとなります。

Bash向けに自動生成される処理
__fnm_use_if_file_found() {
    if [[ -f .node-version || -f .nvmrc ]]; then
        fnm use --silent-if-unchanged
    fi
}

__fnmcd() {
    \cd "$@" || return $?
    __fnm_use_if_file_found
}

alias cd=__fnmcd
__fnm_use_if_file_found

この処理をNushellで書くとメインとなる処理は以下となり、

Nushell
if ([.nvmrc .node-version] | path exists | any { |it| $it }) {
    ^fnm use --silent-if-unchanged
}

Hook処理に組み込むにはconfig.nuに以下のコードを追記してやります。

config.nu に追記する処理
# config.nu に追記
$env.config = ($env.config | merge {
    hooks: {
        env_change: {
            PWD: [{|before, after|
                if ([.nvmrc .node-version] | path exists | any { |it| $it }) {
                    ^fnm use --silent-if-unchanged
                }
            }]
        }
    }
})

既存の$env.configにHook処理をマージする形にしていますが、直接最初の$env.configの定義を修正する形でも大丈夫です。

補足 : その他パラメーター

fnm envコマンドには他にもいくつかパラメーターがありますが、それらの指定は環境に応じて行ってください。

動作確認

ここまでの設定をした環境で軽く動作確認をしてみます。
Windows 11上のNushell 0.97.1が検証環境となり、Scoopからfnm 1.37.1をインストール済みです。

この環境で一通りのコマンドを実行し最新のNode.jsをインストールしてみます。

Nushell
fnm install --latest
fnm use <インストールされたバージョン>
fnm list

結果は下図の通り最新のNode.js 22.8.0が使える様になりました。

how-to-use-fnm-on-nushell-202409-01

追加で最新のLTSバージョンをインストールして切り替えてみました。

Nushell
fnm install --lts
fnm use <インストールされたバージョン>
fnm list

結果こちらもいい感じに動作してくれています。

how-to-use-fnm-on-nushell-202409-02

細かい手順は端折りますがLinux環境でも期待した動作となっています。

how-to-use-fnm-on-nushell-202409-03

無事やりたいことを実現できました。

補足1 : fnmコマンドの入力補完

補足としてfnmコマンドの入力補完について触れておきます。

fnmコマンドの入力補完はfnm completionsコマンドを使うことでシェルごとの補完用関数を生成できます。

# Bash用関数を生成する例
fnm completions --shell bash

ただ、残念ながらこの関数は一つの関数で全ての補完パターンを網羅するタイプのものでありNushell向けの代替手段がありませんでした。

このためNushellでfnmコマンドの入力補完を行う場合は自分で実装するしかありません。
最低限使うであろうパラメーターだけまとめたものが以下となります。

クリックして展開
fnm-completions.nu
# 最低限の補完処理をまとめたもの

def fnm_sort_order [] {["desc", "asc"]}
def fnm_progress_type [] {["auto", "never", "always"]}
def fnm_shell_type [] {["bash", "zsh", "fish", "power-shell"]}

# A fast and simple Node.js manager
extern "fnm" [
    --help(-h)    # Print help (see a summary with '-h')
    --version(-V) # Print version
]
# List all remote Node.js versions [aliases: ls-remote]
extern "fnm list-remote" [
    --filter: string              # Filter versions by a user-defined version or a semver range
    --lts                         # Show only LTS versions (optionally filter by LTS codename)
    --sort: string@fnm_sort_order # Version sorting order
    --latest                      # Only show the latest matching version
] 
# List all remote Node.js versions
extern "fnm ls-remote" [
    --filter: string              # Filter versions by a user-defined version or a semver range
    --lts                         # Show only LTS versions (optionally filter by LTS codename)
    --sort: string@fnm_sort_order # Version sorting order
    --latest                      # Only show the latest matching version
]
# List all locally installed Node.js versions [aliases: ls]
extern "fnm list" [
]
# List all locally installed Node.js versions
extern "fnm ls" [
]
# Install a new Node.js version
extern "fnm install" [
    --progress: string@fnm_progress_type # Show an interactive progress bar for the download status
    --lts                                # Install latest LTS
    --latest                             # Install latest version
]
# Change Node.js version
extern "fnm use" [
]
# Print and set up required environment variables for fnm
extern "fnm env" [
    --shell: string@fnm_shell_type # The shell syntax to use. Infers when missing
    --version-file-strategy        # A strategy for how to resolve the Node version. Used whenever `fnm use` or `fnm install` is called without a version, or when `--use-on-cd` is configured on evaluation
    --json                         # Print JSON instead of shell commands
    --use-on-cd                    # Print the script to change Node versions every directory change
    --corepack-enabled             # Enable corepack support for each new installation. This will make fnm call `corepack enable` on every Node.js installation. For more information about corepack see <https://nodejs.org/api/corepack.html>
]
# Print shell completions to stdout
extern "fnm completions" [
    --shell: string@fnm_shell_type # The shell syntax to use. Infers when missing
]
# Alias a version to a common name
extern "fnm alias" [
]
# Remove an alias definition
extern "fnm unalias" [
]
# Set a version as the default version
extern "fnm default" [
]
# Print the current Node.js version
extern "fnm current" [
]
# Run a command within fnm context
extern "fnm exec" [
]
# Uninstall a Node.js version
extern "fnm uninstall" [
]
# Print this message or the help of the given subcommand(s)
extern "fnm help" [
]

これだけでもそれなりに使えます。

how-to-use-fnm-on-nushell-202409-04

補足2 : FNM_MULTISHELL_PATHのディレクトリが増えすぎる問題

これはNushellだけでなくfnm全体の問題となるのですが、fnmでは複数のシェルでの同時利用をサポートするためシェル起動の都度[1]FNM_MULTISHELL_PATH環境変数向けに個別の一時ディレクトリを作成します。
この一時ディレクトリはシンボリックリンクであり利用するNode.jsのバイナリがあるディレクトリを実体としています。

この挙動自体は真っ当なのですが、問題となるのは古くなり使わなくなった一時ディレクトリを消す方法が環境依存である点です。

たとえば私の開発環境だとWSL上のUbuntuでは/mnt/wslg/runtime-dir/fnm_multishells/配下に一時ディレクトリが作成されており、このディレクトリはWSL起動の都度削除されます。
対してWindows環境では%LOCALAPPDATA%\fnm_multishells配下に一時ディレクトリが作成され自動で削除はされません。

先日から1日fnmを試しただけで大量のフォルダが残ってしまいました。

how-to-use-fnm-on-nushell-202409-05
1日試しただけで33個のシンボリックリンクが残ってしまった

このため必要に応じて古いシンボリックリンクを削除する処理も追加しておくと良いでしょう。
たとえばWindows環境向けにはenv.nuに次の様なコマンドを仕込んでおくと7日以前[2]のシンボリックリンクを削除してくれます。

env.nu に任意で追記
# 必要に応じて古いシンボリックリンク削除処理を追加しておく
# 削除対象とする日数も環境に応じて要調整
if $nu.os-info.name == "windows" {
    ls $"($env.LOCALAPPDATA)\\fnm_multishells\\" | where type == symlink and modified < ((date now) - 7day) | each { |d| rm $d.name } | null
}

最後に

以上となります。

nvmからfnmに切り替えたおかげでWindows上でも気軽にNode.jsのバージョン管理ができる様になったのは思わぬ副産物でした。
Nushell使い自体あまりいない気はしますが、本記事の内容が誰かの役に立てば幸いです。

脚注
  1. 正確にはfnm envコマンド実行の都度 ↩︎

  2. 流石に一週間シェルを立ち上げっぱなしという事は無いだろうという判断です ↩︎

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.