Nushellでfnmを使いNode.jsのバージョン管理を行ってみる
しばたです。
私は以前から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を使っている有志がいたため私もそれに倣うことにしたのが理由となります。
ただ、本日時点でfnmはNushellに正式対応しておらずPull RequestやIssueは上がっているもののまだ道半ばといったところです。
- Nushell support #463
- 3年前からのIssueで本記事の内容のベースとなります
- Nushell support #801
- 2年前にPull Requestが出ているものの動きが止まってしまっている...
注意事項
fnmのインストール
まずはfnmのインストールを行います。
こちらはNushell上であってもGitHubにある手順がそのまま使えます。
Linuxでのインストール手順
Linux環境の場合はセットアップスクリプトからインストールします。
# 他シェルのインストール手順と同じ
curl -fsSL https://fnm.vercel.app/install | bash
通常であればfnmのバイナリを配置した後に実行シェルに応じた初期設定を行うのですが、Nushellは非対応のため
Could not infer shell type. Please set up manually.
といったメッセージと共に処理が途中で終了します。
~> 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の場合は --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.PATH = ($env.PATH | prepend $"($env.HOME)/.local/share/fnm")
Windowsでのインストール手順
Windows環境の場合はWinGet、Scoop、Chocolateyといったパッケージマネージャからインストールできます。
# 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に追記するコマンド
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パラメーターがあるのでこれを使います。
# JSON形式で環境変数を吐く--jsonパラメーターがあるのでこれを使って環境変数を設定
fnm env --json | from json | load-env
GitHubのコメントを参考にもう少しチェック処理などを追加してやり最終的に以下の様にすればOKです。
# 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
コマンドを実行する」ものとなります。
__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で書くとメインとなる処理は以下となり、
if ([.nvmrc .node-version] | path exists | any { |it| $it }) {
^fnm use --silent-if-unchanged
}
Hook処理に組み込むには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をインストールしてみます。
fnm install --latest
fnm use <インストールされたバージョン>
fnm list
結果は下図の通り最新のNode.js 22.8.0が使える様になりました。
追加で最新のLTSバージョンをインストールして切り替えてみました。
fnm install --lts
fnm use <インストールされたバージョン>
fnm list
結果こちらもいい感じに動作してくれています。
細かい手順は端折りますがLinux環境でも期待した動作となっています。
無事やりたいことを実現できました。
補足1 : fnmコマンドの入力補完
補足としてfnmコマンドの入力補完について触れておきます。
fnmコマンドの入力補完はfnm completions
コマンドを使うことでシェルごとの補完用関数を生成できます。
# Bash用関数を生成する例
fnm completions --shell bash
ただ、残念ながらこの関数は一つの関数で全ての補完パターンを網羅するタイプのものでありNushell向けの代替手段がありませんでした。
このためNushellでfnmコマンドの入力補完を行う場合は自分で実装するしかありません。
最低限使うであろうパラメーターだけまとめたものが以下となります。
クリックして展開
# 最低限の補完処理をまとめたもの
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" [
]
これだけでもそれなりに使えます。
補足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を試しただけで大量のフォルダが残ってしまいました。
1日試しただけで33個のシンボリックリンクが残ってしまった
このため必要に応じて古いシンボリックリンクを削除する処理も追加しておくと良いでしょう。
たとえばWindows環境向けにはenv.nu
に次の様なコマンドを仕込んでおくと7日以前[2]のシンボリックリンクを削除してくれます。
# 必要に応じて古いシンボリックリンク削除処理を追加しておく
# 削除対象とする日数も環境に応じて要調整
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使い自体あまりいない気はしますが、本記事の内容が誰かの役に立てば幸いです。