Schniz / fnm

πŸš€ Fast and simple Node.js version manager, built in Rust
https://fnm.vercel.app
GNU General Public License v3.0
17.52k stars 444 forks source link

Better Support for Automatic Switching #144

Closed CWSpear closed 2 years ago

CWSpear commented 4 years ago

(Sorry, I edited this post quite a bit from the original cuz it was poorly laid out, sorry if this version doesn't match what you might have gotten in an email.)


The nvm docs have scripts that you can add to your .zshrc, etc, similar to the script used with fnm env --use-on-cd, but with better support in searching ancestors for .nvmrc and not switching unless the version is actually from the version.

While fnm is much faster, the latter is not really needed, it'd still be nice to have this sort of capability so that one does not need to see Using latest-dubnium every since time they switch directories when it's the same .nvmrc (which would happen a lot if it had the parent search).

The fnm env --use-on-cd one also doesn't support switching back to default.

nvm has a couple of things to help out with accomplishing this:

nvm version

nvm has a version subcommand that will return the exact (installed) version that matches given input, i.e.:

$ nvm version 6
v6.17.0

$ nvm version lts/dubnium
v10.16.3

This does not do a lookup, but it based on aliases, so if v.10.16.0 is the latest lts/dubnium you have, nvm version lts/dubnium will return that.

nvm_find_nvmrc*

This one isn't as big of a deal, because there are lots of other simple scripts out there to do this thing, but it provides a helper function to find the nearest .nvmrc by first looking in pwd and then checking each ancestor directory.

*I think nvm_find_nvmrc must be a zsh-specific thing because it's not used in the bash version in the nvm docs. It just uses a fairly standard implementation of "find-up":

find-up () {
    path=$(pwd)
    while [[ "$path" != "" && ! -e "$path/$1" ]]; do
        path=${path%/*}
    done
    echo "$path"
}
Schniz commented 4 years ago

(Sorry, I edited this post quite a bit from the original cuz it was poorly laid out, sorry if this version doesn't match what you might have gotten in an email.)

hehe, no worries, it's a holiday here and I forgot to bring a charger home πŸ˜…

While fnm is much faster, the latter is not really needed, it'd still be nice to have this sort of capability so that one does not need to see Using latest-dubnium every since time they switch directories when it's the same .nvmrc (which would happen a lot if it had the parent search).

We can add a --silent-if-unchanged flag so it won't add noise to the terminal. I wonder if there's anything smarter we can do. Maybe this can just be the default for use, and making a debug log for Doesn't need to change anything. I just don't want people to think that fnm doesn't work when it actually does work 😦

What do you think?

nvm has a version subcommand that will return the exact (installed) version that matches given input, i.e.:

We can implement this. Shouldn't be that hard, because it is mostly reusing stuff we already have πŸ˜„One step before actually symlinking πŸ˜„

The only open question regarding this feature, is something that relates to every feature β€” should we show what you'll use, or what you'll install?

If you have 10.0.0 installed, but there is 10.2.0 upstream, what should we show?

nvm_find_nvmrc*

Currently we only look on the current directory. Didn't know that nvm does that. Do you think we should change the current behavior? I don't see the big benefit right now, but I might be wrong!

CWSpear commented 4 years ago

I'm all for more options, so if you wanna make this or that more optional, that's good.

In terms of what I think are reasonable expectations: I think it's pretty important that it would revert to default when it doesn't find an .nvmrc (or .node-version or whatever the other one is). I am a contractor working on multiple projects and have clients that make a big deal about using specific lts releases, but I want to use latest work I'm just messing around with stuff.

This would mean that if I'm anywhere in a project with .nvmrc at its root, it should be on that same version (i.e. the closest ancestor's .nvmrc). I'm not in a place to test, but I'm 99.9% sure if you nvm use in a subdirectory that does not have .nvmrc in it, it will search in parent directories for one.

As for, "should it install/should it compare to the latest upstream version," again, I wouldn't mind options, but I think it's reasonable to just use the latest installed version (i.e. nvm use). Which does offer to install latest if no alias matches (which nvm does not, but I like that fnm does).

This is the current script I use:

fnm-nvmrc() {
  nvmrc_path=$(find-up .nvmrc | tr -d '[:space:]')

  if [ -n "$nvmrc_path" ]; then
    nvm_version=`cat $nvmrc_path/.nvmrc`
    fnm use $nvm_version
  else
    echo "Reverting to fnm default version"
    fnm use default
  fi
}

find-up() {
  path=$(pwd)
  while [[ "$path" != "" && ! -e "$path/$1" ]]; do
    path=${path%/*}
  done
  echo "$path"
}

It'd be nice to have a "compare current version with version in .nvmrc to see if they are a match (i.e. if I'm on v10.6.1 that is aliased to lts/dubnium and that's what's in .nvmrc, it's a match), then it won't call fnm use. Since fnm is so fast, maybe it's fine if it just compares afterwards to see if it changed and (optionally?) not output if it didn't change, like you suggested. Not a bad compromise, as long as fnm use remains super fast!! :)

axxag commented 4 years ago

Just leaving this here in case someone finds it useful.

I modified the --use-on-cd output and the recommendations above to only revert when necessary, so changing dirs is still super fast.

find-up() {
    path=$(pwd)
    while [[ "$path" != "" && ! -e "$path/$1" ]]; do
        path=${path%/*}
    done
    echo "$path"
}

FNM_USING_LOCAL_VERSION=0

autoload -U add-zsh-hook
_fnm_autoload_hook() {
    nvmrc_path=$(find-up .nvmrc | tr -d '[:space:]')

    if [ -n "$nvmrc_path" ]; then
        FNM_USING_LOCAL_VERSION=1
        nvm_version=$(cat $nvmrc_path/.nvmrc)
        fnm use $nvm_version
    elif [ $FNM_USING_LOCAL_VERSION -eq 1 ]; then
        FNM_USING_LOCAL_VERSION=0
        fnm use default
    fi
}

add-zsh-hook chpwd _fnm_autoload_hook &&
    _fnm_autoload_hook
Isos9 commented 3 years ago

A little update,

since fnm don't need to have the version in arg if there is .nvmrc or .node-version, so no need for the find-up function:

FNM_USING_LOCAL_VERSION=0

autoload -U add-zsh-hook
_fnm_autoload_hook() {
    if [[ -f .nvmrc && -r .nvmrc || -f .node-version && -r .node-version ]]; then
        FNM_USING_LOCAL_VERSION=1
        fnm use --install-if-missing
    elif [ $FNM_USING_LOCAL_VERSION -eq 1 ]; then
        FNM_USING_LOCAL_VERSION=0
        fnm use default
    fi
}

add-zsh-hook chpwd _fnm_autoload_hook &&
    _fnm_autoload_hook
miszo commented 3 years ago

We can add a --silent-if-unchanged flag so it won't add noise to the terminal. I wonder if there's anything smarter we can do. Maybe this can just be the default for use, and making a debug log for Doesn't need to change anything. I just don't want people to think that fnm doesn't work when it actually does work 😦

@Schniz I'm all in for --silent-if-unchanged flag πŸ‘

miszo commented 3 years ago

OK, I've just figured out that I can just modify it and use fnm use --log-level=quiet instead of fnm use

gutenye commented 3 years ago

Here's the fish shell version if anyone needs it.

It supports

# ~/.config/fish/conf.d/node.fish

function find_up
  set path (pwd)
  while true
    if test -e "$path/$argv[1]"
      echo "$path/$argv[1]"
      return
    end
    if test -e "$path/$argv[2]"
      echo "$path/$argv[2]"
      return
    end
    if test $path = "/"
      return
    end
    set path (dirname $path)
  end
end

function fnm_use
  set current (string replace "v" "" (fnm current))
  set target (string replace "v" "" $argv[1])
  if test $current != $target
    fnm use $argv[1]
  end
end

function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change'
  status --is-command-substitution; and return
  set found (find_up .nvmrc .node-version)
  if test -n "$found"
    fnm_use (cat $found)
  else
    fnm_use system
  end
end

fnm env | source

# VSCode open integrated terminal does not trigger PWD hook
if test "$TERM_PROGRAM" = "vscode"
  _fnm_autoload_hook
end
Schniz commented 3 years ago

woah, these scripts are crazy πŸ˜ƒ

Maybe it's time to revisit it and add do a recursive (upwards) search for the dotfiles? πŸ˜ƒ This can be configured behind a "feature toggle" and if it will make sense we can make it the default in future release.

Hope I have time soon to make it work πŸ˜ƒ

alexeyten commented 3 years ago

My version of this script for Bash and .nvmrc It doesn't check current version or version in file, it only cares whether the path to current .nvmrc has changed.

__fnm_nvm_path="init"

__fnm_update_version() {
    local path=$PWD

    while [[ "$path" != "" && ! -e "$path/.nvmrc" ]]; do
        path=${path%/*}
    done

    if [[ "$path" != "$__fnm_nvm_path" ]]; then
        if [[ -z "$path" ]]; then
            fnm use --log-level quiet default
        else
            fnm use --log-level quiet $(<"$path/.nvmrc")
        fi
        __fnm_nvm_path="$path"
    fi
}

PROMPT_COMMAND="__fnm_update_version"
vith commented 3 years ago

Thanks @gutenye for the fish shell version. I had to make one small change due to this error:

test: Missing argument at index 2

~/.config/fish/conf.d/node.fish (line 40): 
if test $TERM_PROGRAM = "vscode"
   ^
from sourcing file ~/.config/fish/conf.d/node.fish
    called on line 294 of file /usr/share/fish/config.fish
from sourcing file /usr/share/fish/config.fish
    called during startup

(Type 'help test' for related documentation)

I wrapped the $TERM_PROGRAM argument in double quotes, like so:

if test "$TERM_PROGRAM" = "vscode"

Seems to be working great now. I'm using fish, version 3.1.2.

miszo commented 3 years ago

Hi, I'm using this script as my automatic switch, and I've just found out that it doesn't work with tags (e.g. lts-latest, default).

.nvmrc file

lts-latest
eval "$(fnm env)"

FNM_USING_LOCAL_VERSION=0

autoload -U add-zsh-hook
_fnm_autoload_hook () {
  if [[ -f .nvmrc && -r .nvmrc || -f .node-version && -r .node-version ]]; then
    FNM_USING_LOCAL_VERSION=1
    fnm use --install-if-missing
  elif [ $FNM_USING_LOCAL_VERSION -eq 1 ]; then
    FNM_USING_LOCAL_VERSION=0
    fnm use default --install-if-missing
  fi
}

add-zsh-hook chpwd _fnm_autoload_hook \
    && _fnm_autoload_hook

I've uninstalled version default and lts-latest and when I'm opening the project on my terminal, then I get

error: Requested version lts-latest is not currently installed
miszo commented 3 years ago

@Schniz, do you have any idea how to tackle this? Maybe I'm doing something wrong? Please let me know.

Hi, I'm using this script as my automatic switch, and I've just found out that it doesn't work with tags (e.g. lts-latest, default).

.nvmrc file

lts-latest
eval "$(fnm env)"

FNM_USING_LOCAL_VERSION=0

autoload -U add-zsh-hook
_fnm_autoload_hook () {
  if [[ -f .nvmrc && -r .nvmrc || -f .node-version && -r .node-version ]]; then
    FNM_USING_LOCAL_VERSION=1
    fnm use --install-if-missing
  elif [ $FNM_USING_LOCAL_VERSION -eq 1 ]; then
    FNM_USING_LOCAL_VERSION=0
    fnm use default --install-if-missing
  fi
}

add-zsh-hook chpwd _fnm_autoload_hook \
    && _fnm_autoload_hook

I've uninstalled version default and lts-latest and when I'm opening the project on my terminal, then I get

error: Requested version lts-latest is not currently installed
Schniz commented 3 years ago

looks like a bug. Will take a look when I have some more time soon πŸ˜ƒ

Schniz commented 3 years ago

this is how it can be reproduced:

$ fnm uninstall 'lts/*'
$ echo 'lts/*' > .node-version
$ fnm use --install-if-missing
miszo commented 3 years ago

looks like a bug. Will take a look when I have some more time soon πŸ˜ƒ

Hi @Schniz! Any news on this bug? Should I make a separate issue with it?

Schniz commented 3 years ago

opened πŸ˜ƒ

leohxj commented 8 months ago

here is my custom zsh function:

autoload -U add-zsh-hook
load-nvmrc() {
  DEFAULT_NODE_VERSION=`$FNM_DIR/aliases/default/bin/node -v`
  if [[ (-f .nvmrc && -r .nvmrc) || (-f .node-version && -r .node-version) ]]; then
    fnm use --install-if-missing --silent-if-unchanged
  elif [[ `node -v` != $DEFAULT_NODE_VERSION ]]; then
    echo Reverting to node from "`node -v`" to "$DEFAULT_NODE_VERSION"
    fnm use $DEFAULT_NODE_VERSION
  fi
}
add-zsh-hook chpwd load-nvmrc