diff --git a/docs/commands.md b/docs/commands.md index 9850e25..63b27f8 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -28,6 +28,14 @@ OPTIONS: --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] SUBCOMMANDS: alias Alias a version to a common name @@ -55,24 +63,42 @@ USAGE: fnm alias [OPTIONS] FLAGS: - -h, --help Prints help information - -V, --version Prints version information + -h, --help + Prints help information + + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ARGS: + + + + ``` # `fnm completions` @@ -85,14 +111,20 @@ USAGE: fnm completions [OPTIONS] FLAGS: - -h, --help Prints help information - -V, --version Prints version information + -h, --help + Prints help information + + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] @@ -102,6 +134,14 @@ OPTIONS: --shell The shell syntax to use. Infers when missing [possible values: zsh, bash, fish, powershell, elvish] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ``` # `fnm current` @@ -114,20 +154,34 @@ USAGE: fnm current [OPTIONS] FLAGS: - -h, --help Prints help information - -V, --version Prints version information + -h, --help + Prints help information + + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ``` # `fnm default` @@ -162,6 +216,14 @@ OPTIONS: --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ARGS: @@ -211,6 +273,14 @@ OPTIONS: --shell The shell syntax to use. Infers when missing [possible values: bash, zsh, fish, powershell] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ``` # `fnm exec` @@ -251,6 +321,14 @@ OPTIONS: --using Either an explicit version, or a filename with the version written in it + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ARGS: ... @@ -274,24 +352,42 @@ USAGE: fnm install [FLAGS] [OPTIONS] [version] FLAGS: - -h, --help Prints help information - --lts Install latest LTS - -V, --version Prints version information + -h, --help + Prints help information + + --lts + Install latest LTS + + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ARGS: - A version string. Can be a partial semver or a LTS version name by the format lts/NAME + + A version string. Can be a partial semver or a LTS version name by the format lts/NAME + ``` # `fnm list` @@ -304,20 +400,34 @@ USAGE: fnm list [OPTIONS] FLAGS: - -h, --help Prints help information - -V, --version Prints version information + -h, --help + Prints help information + + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ``` # `fnm list-remote` @@ -330,20 +440,34 @@ USAGE: fnm list-remote [OPTIONS] FLAGS: - -h, --help Prints help information - -V, --version Prints version information + -h, --help + Prints help information + + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ``` # `fnm unalias` @@ -356,23 +480,39 @@ USAGE: fnm unalias [OPTIONS] FLAGS: - -h, --help Prints help information - -V, --version Prints version information + -h, --help + Prints help information + + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ARGS: + + ``` # `fnm uninstall` @@ -408,6 +548,14 @@ OPTIONS: --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ARGS: @@ -425,22 +573,43 @@ USAGE: fnm use [FLAGS] [OPTIONS] [version] FLAGS: - -h, --help Prints help information - --install-if-missing Install the version if it isn't installed yet - -V, --version Prints version information + -h, --help + Prints help information + + --install-if-missing + Install the version if it isn't installed yet + + --silent-if-unchanged + Don't output a message identifying the version being used if it will not change due to execution of this + command + -V, --version + Prints version information + OPTIONS: --arch Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] [default: x64] - --fnm-dir The root directory of fnm installations [env: FNM_DIR] + --fnm-dir + The root directory of fnm installations [env: FNM_DIR] + --log-level The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, error] --node-dist-mirror https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] + --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. + + * `local`: Use the local version of Node defined within the current directory + + * `recursive`: Use the version of Node defined within the current directory and all parent directories [env: + FNM_VERSION_FILE_STRATEGY] [default: local] [possible values: local, recursive] ARGS: + + ``` diff --git a/src/commands/env.rs b/src/commands/env.rs index c19198b..423243a 100644 --- a/src/commands/env.rs +++ b/src/commands/env.rs @@ -71,6 +71,13 @@ impl Command for Env { "{}", shell.set_env_var("FNM_MULTISHELL_PATH", multishell_path.to_str().unwrap()) ); + println!( + "{}", + shell.set_env_var( + "FNM_VERSION_FILE_STRATEGY", + config.version_file_strategy().as_str() + ) + ); println!( "{}", shell.set_env_var("FNM_DIR", config.base_dir_with_default().to_str().unwrap()) diff --git a/src/commands/exec.rs b/src/commands/exec.rs index f2e8215..9f9cf59 100644 --- a/src/commands/exec.rs +++ b/src/commands/exec.rs @@ -46,7 +46,7 @@ impl Cmd for Exec { let current_dir = std::env::current_dir().unwrap(); UserVersionReader::Path(current_dir) }) - .into_user_version() + .into_user_version(config) .context(CantInferVersion)?; let applicable_version = choose_version_for_user_input(&version, config) diff --git a/src/commands/install.rs b/src/commands/install.rs index a3dd6a9..183ab20 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -50,7 +50,7 @@ impl super::command::Command for Install { let current_version = self .version()? - .or_else(|| get_user_version_for_directory(current_dir)) + .or_else(|| get_user_version_for_directory(current_dir, config)) .context(CantInferVersion)?; let version = match current_version.clone() { diff --git a/src/commands/uninstall.rs b/src/commands/uninstall.rs index be448c5..2152a1b 100644 --- a/src/commands/uninstall.rs +++ b/src/commands/uninstall.rs @@ -26,7 +26,7 @@ impl Command for Uninstall { .version .or_else(|| { let current_dir = std::env::current_dir().unwrap(); - get_user_version_for_directory(current_dir) + get_user_version_for_directory(current_dir, config) }) .context(CantInferVersion)?; diff --git a/src/commands/use.rs b/src/commands/use.rs index 3c44b17..6f53421 100644 --- a/src/commands/use.rs +++ b/src/commands/use.rs @@ -1,14 +1,17 @@ use super::command::Command; use super::install::Install; +use crate::current_version::current_version; use crate::fs; use crate::installed_versions; use crate::outln; use crate::system_version; use crate::user_version::UserVersion; use crate::version::Version; +use crate::version_file_strategy::VersionFileStrategy; use crate::{config::FnmConfig, user_version_reader::UserVersionReader}; use colored::Colorize; use snafu::{ensure, OptionExt, ResultExt, Snafu}; +use std::path::Path; use structopt::StructOpt; #[derive(StructOpt, Debug)] @@ -17,6 +20,11 @@ pub struct Use { /// Install the version if it isn't installed yet #[structopt(long)] install_if_missing: bool, + + /// Don't output a message identifying the version being used + /// if it will not change due to execution of this command + #[structopt(long)] + silent_if_unchanged: bool, } impl Command for Use { @@ -34,32 +42,34 @@ impl Command for Use { let current_dir = std::env::current_dir().unwrap(); UserVersionReader::Path(current_dir) }) - .into_user_version() + .into_user_version(config) + .ok_or_else(|| match config.version_file_strategy() { + VersionFileStrategy::Local => InferVersionError::Local, + VersionFileStrategy::Recursive => InferVersionError::Recursive, + }) .context(CantInferVersion)?; - let version_path = if let UserVersion::Full(Version::Bypassed) = requested_version { - outln!( - config, - Info, + let (message, version_path) = if let UserVersion::Full(Version::Bypassed) = + requested_version + { + let message = format!( "Bypassing fnm: using {} node", system_version::display_name().cyan() ); - system_version::path() + (message, system_version::path()) } else if let Some(alias_name) = requested_version.alias_name() { let alias_path = config.aliases_dir().join(&alias_name); let system_path = system_version::path(); if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path) { - outln!( - config, - Info, + let message = format!( "Bypassing fnm: using {} node", system_version::display_name().cyan() ); - system_path + (message, system_path) } else if alias_path.exists() { - outln!(config, Info, "Using Node for alias {}", alias_name.cyan()); - alias_path + let message = format!("Using Node for alias {}", alias_name.cyan()); + (message, alias_path) } else { install_new_version(requested_version, config, self.install_if_missing)?; return Ok(()); @@ -67,23 +77,36 @@ impl Command for Use { } else { let current_version = requested_version.to_version(&all_versions, config); if let Some(version) = current_version { - outln!(config, Info, "Using Node {}", version.to_string().cyan()); - config + let version_path = config .installations_dir() .join(version.to_string()) - .join("installation") + .join("installation"); + let message = format!("Using Node {}", version.to_string().cyan()); + (message, version_path) } else { install_new_version(requested_version, config, self.install_if_missing)?; return Ok(()); } }; + if !self.silent_if_unchanged || will_version_change(&version_path, config) { + outln!(config, Info, "{}", message); + } + replace_symlink(&version_path, multishell_path).context(SymlinkingCreationIssue)?; Ok(()) } } +fn will_version_change(resolved_path: &Path, config: &FnmConfig) -> bool { + let current_version_path = current_version(config) + .unwrap_or(None) + .map(|v| v.installation_path(config)); + + current_version_path.as_deref() != Some(resolved_path) +} + fn install_new_version( requested_version: UserVersion, config: &FnmConfig, @@ -106,6 +129,7 @@ fn install_new_version( Use { version: Some(UserVersionReader::Direct(requested_version)), install_if_missing: true, + silent_if_unchanged: false, } .apply(config)?; @@ -187,10 +211,8 @@ pub enum Error { VersionListingError { source: installed_versions::Error }, #[snafu(display("Requested version {} is not currently installed", version))] CantFindVersion { version: UserVersion }, - #[snafu(display( - "Can't find version in dotfiles. Please provide a version manually to the command." - ))] - CantInferVersion, + #[snafu(display("{}", source))] + CantInferVersion { source: InferVersionError }, #[snafu(display( "{}\n{}\n{}", "We can't find the necessary environment variables to replace the Node version.", @@ -199,3 +221,13 @@ pub enum Error { ))] FnmEnvWasNotSourced, } + +#[derive(Debug, Snafu)] +pub enum InferVersionError { + #[snafu(display( + "Can't find version in dotfiles. Please provide a version manually to the command." + ))] + Local, + #[snafu(display("Could not find any version to use. Maybe you don't have a default version set?\nTry running `fnm default ` to set one,\nor create a .node-version file inside your project to declare a Node.js version."))] + Recursive, +} diff --git a/src/config.rs b/src/config.rs index ec69b4c..8fbdd82 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use crate::arch::Arch; use crate::log_level::LogLevel; use crate::outln; use crate::path_ext::PathExt; +use crate::version_file_strategy::VersionFileStrategy; use colored::Colorize; use dirs::{data_dir, home_dir}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -63,6 +64,22 @@ pub struct FnmConfig { hide_env_values = true )] pub arch: Arch, + + /// 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. + /// + /// * `local`: Use the local version of Node defined within the current directory + /// + /// * `recursive`: Use the version of Node defined within the current directory and all parent directories + #[structopt( + long, + env = "FNM_VERSION_FILE_STRATEGY", + possible_values = VersionFileStrategy::possible_values(), + default_value = "local", + global = true, + hide_env_values = true, + )] + version_file_strategy: VersionFileStrategy, } impl Default for FnmConfig { @@ -73,11 +90,16 @@ impl Default for FnmConfig { multishell_path: None, log_level: LogLevel::Info, arch: Arch::default(), + version_file_strategy: VersionFileStrategy::default(), } } } impl FnmConfig { + pub fn version_file_strategy(&self) -> &VersionFileStrategy { + &self.version_file_strategy + } + pub fn multishell_path(&self) -> Option<&std::path::Path> { match &self.multishell_path { None => None, diff --git a/src/default_version.rs b/src/default_version.rs new file mode 100644 index 0000000..dc42eb9 --- /dev/null +++ b/src/default_version.rs @@ -0,0 +1,9 @@ +use crate::config::FnmConfig; +use crate::version::Version; +use std::str::FromStr; + +pub fn find_default_version(config: &FnmConfig) -> Option { + let version_path = config.default_version_dir().canonicalize().ok()?; + let file_name = version_path.parent()?.file_name()?; + Version::from_str(file_name.to_str()?).ok()?.into() +} diff --git a/src/main.rs b/src/main.rs index 8efefb7..587011e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,10 +28,12 @@ mod system_version; mod user_version; mod user_version_reader; mod version; +mod version_file_strategy; mod version_files; #[macro_use] mod log_level; +mod default_version; fn main() { env_logger::init(); diff --git a/src/shell/bash.rs b/src/shell/bash.rs index 9589e5a..81ae55e 100644 --- a/src/shell/bash.rs +++ b/src/shell/bash.rs @@ -1,5 +1,7 @@ +use crate::version_file_strategy::VersionFileStrategy; + use super::shell::Shell; -use indoc::indoc; +use indoc::{formatdoc, indoc}; use std::path::Path; #[derive(Debug)] @@ -18,24 +20,32 @@ impl Shell for Bash { format!("export {}={:?}", name, value) } - fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { - indoc!( - r#" - __fnm_use_if_file_found() { + fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String { + let autoload_hook = match config.version_file_strategy() { + VersionFileStrategy::Local => indoc!( + r#" if [[ -f .node-version || -f .nvmrc ]]; then - fnm use + fnm use --silent-if-unchanged fi - } + "# + ), + VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#, + }; + formatdoc!( + r#" + __fnm_use_if_file_found() {{ + {autoload_hook} + }} - __fnmcd() { + __fnmcd() {{ \cd "$@" || return $? __fnm_use_if_file_found - } + }} alias cd=__fnmcd __fnm_use_if_file_found - "# + "#, + autoload_hook = autoload_hook ) - .into() } } diff --git a/src/shell/fish.rs b/src/shell/fish.rs index 0d08af2..6eef262 100644 --- a/src/shell/fish.rs +++ b/src/shell/fish.rs @@ -1,5 +1,7 @@ +use crate::version_file_strategy::VersionFileStrategy; + use super::shell::Shell; -use indoc::indoc; +use indoc::{formatdoc, indoc}; use std::path::Path; #[derive(Debug)] @@ -18,19 +20,27 @@ impl Shell for Fish { format!("set -gx {name} {value:?};", name = name, value = value) } - fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { - indoc!( + fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String { + let autoload_hook = match config.version_file_strategy() { + VersionFileStrategy::Local => indoc!( + r#" + if test -f .node-version -o -f .nvmrc + fnm use --silent-if-unchanged + end + "# + ), + VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#, + }; + formatdoc!( r#" function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change' status --is-command-substitution; and return - if test -f .node-version -o -f .nvmrc - fnm use - end + {autoload_hook} end _fnm_autoload_hook - "# + "#, + autoload_hook = autoload_hook ) - .into() } } diff --git a/src/shell/powershell.rs b/src/shell/powershell.rs index 9603e6f..663fd3e 100644 --- a/src/shell/powershell.rs +++ b/src/shell/powershell.rs @@ -1,5 +1,7 @@ +use crate::version_file_strategy::VersionFileStrategy; + use super::Shell; -use indoc::indoc; +use indoc::{formatdoc, indoc}; use std::path::Path; #[derive(Debug)] @@ -18,15 +20,26 @@ impl Shell for PowerShell { format!(r#"$env:{} = "{}""#, name, value) } - fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { - indoc!(r#" - function Set-FnmOnLoad { If ((Test-Path .nvmrc) -Or (Test-Path .node-version)) { & fnm use } } - function Set-LocationWithFnm { param($path); Set-Location $path; Set-FnmOnLoad } - Set-Alias cd_with_fnm Set-LocationWithFnm -Force - Remove-Item alias:\cd - New-Alias cd Set-LocationWithFnm - Set-FnmOnLoad - "#).into() + fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String { + let autoload_hook = match config.version_file_strategy() { + VersionFileStrategy::Local => indoc!( + r#" + If ((Test-Path .nvmrc) -Or (Test-Path .node-version)) { & fnm use --silent-if-unchanged } + "# + ), + VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#, + }; + formatdoc!( + r#" + function Set-FnmOnLoad {{ {autoload_hook} }} + function Set-LocationWithFnm {{ param($path); Set-Location $path; Set-FnmOnLoad }} + Set-Alias cd_with_fnm Set-LocationWithFnm -Force + Remove-Item alias:\cd + New-Alias cd Set-LocationWithFnm + Set-FnmOnLoad + "#, + autoload_hook = autoload_hook + ) } fn to_structopt_shell(&self) -> clap::Shell { clap::Shell::PowerShell diff --git a/src/shell/windows_cmd/cd.cmd b/src/shell/windows_cmd/cd.cmd index a49c3a7..3e78176 100644 --- a/src/shell/windows_cmd/cd.cmd +++ b/src/shell/windows_cmd/cd.cmd @@ -1,10 +1,14 @@ @echo off cd %1 -if exist .nvmrc ( - fnm use +if "%FNM_VERSION_FILE_STRATEGY%" == "recursive" ( + fnm use --silent-if-unchanged ) else ( + if exist .nvmrc ( + fnm use --silent-if-unchanged + ) else ( if exist .node-version ( - fnm use + fnm use --silent-if-unchanged ) + ) ) -@echo on \ No newline at end of file +@echo on diff --git a/src/shell/zsh.rs b/src/shell/zsh.rs index 9848048..59a87f1 100644 --- a/src/shell/zsh.rs +++ b/src/shell/zsh.rs @@ -1,5 +1,7 @@ +use crate::version_file_strategy::VersionFileStrategy; + use super::shell::Shell; -use indoc::indoc; +use indoc::{formatdoc, indoc}; use std::path::Path; #[derive(Debug)] @@ -22,20 +24,28 @@ impl Shell for Zsh { Some("rehash".to_string()) } - fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { - indoc!( - r#" - autoload -U add-zsh-hook - _fnm_autoload_hook () { + fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String { + let autoload_hook = match config.version_file_strategy() { + VersionFileStrategy::Local => indoc!( + r#" if [[ -f .node-version || -f .nvmrc ]]; then - fnm use + fnm use --silent-if-unchanged fi - } + "# + ), + VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#, + }; + formatdoc!( + r#" + autoload -U add-zsh-hook + _fnm_autoload_hook () {{ + {autoload_hook} + }} add-zsh-hook chpwd _fnm_autoload_hook \ && _fnm_autoload_hook - "# + "#, + autoload_hook = autoload_hook ) - .into() } } diff --git a/src/user_version_reader.rs b/src/user_version_reader.rs index 78ab0ee..14190a6 100644 --- a/src/user_version_reader.rs +++ b/src/user_version_reader.rs @@ -1,3 +1,4 @@ +use crate::config::FnmConfig; use crate::user_version::UserVersion; use crate::version_files::{get_user_version_for_directory, get_user_version_for_file}; use std::path::PathBuf; @@ -10,11 +11,11 @@ pub enum UserVersionReader { } impl UserVersionReader { - pub fn into_user_version(self) -> Option { + pub fn into_user_version(self, config: &FnmConfig) -> Option { match self { Self::Direct(uv) => Some(uv), Self::Path(pathbuf) if pathbuf.is_file() => get_user_version_for_file(&pathbuf), - Self::Path(pathbuf) => get_user_version_for_directory(&pathbuf), + Self::Path(pathbuf) => get_user_version_for_directory(&pathbuf, config), } } } @@ -47,7 +48,8 @@ mod tests { write!(file, "14").unwrap(); let pathbuf = file.path().to_path_buf(); - let user_version = UserVersionReader::Path(pathbuf).into_user_version(); + let user_version = + UserVersionReader::Path(pathbuf).into_user_version(&FnmConfig::default()); assert_eq!(user_version, Some(UserVersion::OnlyMajor(14))); } @@ -58,14 +60,15 @@ mod tests { std::fs::write(node_version_path, "14").unwrap(); let pathbuf = directory.path().to_path_buf(); - let user_version = UserVersionReader::Path(pathbuf).into_user_version(); + let user_version = + UserVersionReader::Path(pathbuf).into_user_version(&FnmConfig::default()); assert_eq!(user_version, Some(UserVersion::OnlyMajor(14))); } #[test] fn test_direct_to_version() { - let user_version = - UserVersionReader::Direct(UserVersion::OnlyMajor(14)).into_user_version(); + let user_version = UserVersionReader::Direct(UserVersion::OnlyMajor(14)) + .into_user_version(&FnmConfig::default()); assert_eq!(user_version, Some(UserVersion::OnlyMajor(14))); } diff --git a/src/version_file_strategy.rs b/src/version_file_strategy.rs new file mode 100644 index 0000000..89005be --- /dev/null +++ b/src/version_file_strategy.rs @@ -0,0 +1,41 @@ +use std::str::FromStr; + +#[derive(Debug)] +pub enum VersionFileStrategy { + Local, + Recursive, +} + +impl VersionFileStrategy { + pub fn possible_values() -> &'static [&'static str] { + &["local", "recursive"] + } + + pub fn as_str(&self) -> &'static str { + match self { + VersionFileStrategy::Local => "local", + VersionFileStrategy::Recursive => "recursive", + } + } +} + +impl Default for VersionFileStrategy { + fn default() -> Self { + VersionFileStrategy::Local + } +} + +impl FromStr for VersionFileStrategy { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "local" => Ok(VersionFileStrategy::Local), + "recursive" => Ok(VersionFileStrategy::Recursive), + _ => Err(format!( + "Invalid strategy: {}. Expected one of: local, recursive", + s + )), + } + } +} diff --git a/src/version_files.rs b/src/version_files.rs index 4d0bbea..f002a68 100644 --- a/src/version_files.rs +++ b/src/version_files.rs @@ -1,4 +1,7 @@ +use crate::config::FnmConfig; +use crate::default_version; use crate::user_version::UserVersion; +use crate::version_file_strategy::VersionFileStrategy; use encoding_rs_io::DecodeReaderBytes; use log::info; use std::io::Read; @@ -7,7 +10,36 @@ use std::str::FromStr; const PATH_PARTS: [&str; 2] = [".nvmrc", ".node-version"]; -pub fn get_user_version_for_directory(path: impl AsRef) -> Option { +pub fn get_user_version_for_directory( + path: impl AsRef, + config: &FnmConfig, +) -> Option { + match config.version_file_strategy() { + VersionFileStrategy::Local => get_user_version_for_single_directory(path), + VersionFileStrategy::Recursive => { + get_user_version_for_directory_recursive(path).or_else(|| { + info!("Did not find anything recursively. Falling back to default alias."); + default_version::find_default_version(config).map(UserVersion::Full) + }) + } + } +} + +fn get_user_version_for_directory_recursive(path: impl AsRef) -> Option { + let mut current_path = Some(path.as_ref()); + + while let Some(child_path) = current_path { + if let Some(version) = get_user_version_for_single_directory(child_path) { + return Some(version); + } + + current_path = child_path.parent(); + } + + None +} + +pub fn get_user_version_for_single_directory(path: impl AsRef) -> Option { let path = path.as_ref(); for path_part in &PATH_PARTS {