Browse Source

Allow recursive version lookups (#607)

remotes/origin/feat/support-install-latest
Gal Schlezinger 3 years ago committed by GitHub
parent
commit
207ef22f32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 223
      docs/commands.md
  2. 7
      src/commands/env.rs
  3. 2
      src/commands/exec.rs
  4. 2
      src/commands/install.rs
  5. 2
      src/commands/uninstall.rs
  6. 70
      src/commands/use.rs
  7. 22
      src/config.rs
  8. 9
      src/default_version.rs
  9. 2
      src/main.rs
  10. 30
      src/shell/bash.rs
  11. 26
      src/shell/fish.rs
  12. 25
      src/shell/powershell.rs
  13. 8
      src/shell/windows_cmd/cd.cmd
  14. 28
      src/shell/zsh.rs
  15. 15
      src/user_version_reader.rs
  16. 41
      src/version_file_strategy.rs
  17. 34
      src/version_files.rs

223
docs/commands.md

@ -28,6 +28,14 @@ OPTIONS:
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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: SUBCOMMANDS:
alias Alias a version to a common name alias Alias a version to a common name
@ -55,24 +63,42 @@ USAGE:
fnm alias [OPTIONS] <to-version> <name> fnm alias [OPTIONS] <to-version> <name>
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
-V, --version Prints version information Prints help information
-V, --version
Prints version information
OPTIONS: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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: ARGS:
<to-version> <to-version>
<name> <name>
``` ```
# `fnm completions` # `fnm completions`
@ -85,14 +111,20 @@ USAGE:
fnm completions [OPTIONS] fnm completions [OPTIONS]
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
-V, --version Prints version information Prints help information
-V, --version
Prints version information
OPTIONS: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
@ -102,6 +134,14 @@ OPTIONS:
--shell <shell> --shell <shell>
The shell syntax to use. Infers when missing [possible values: zsh, bash, fish, powershell, elvish] The shell syntax to use. Infers when missing [possible values: zsh, bash, fish, powershell, elvish]
--version-file-strategy <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` # `fnm current`
@ -114,20 +154,34 @@ USAGE:
fnm current [OPTIONS] fnm current [OPTIONS]
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
-V, --version Prints version information Prints help information
-V, --version
Prints version information
OPTIONS: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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` # `fnm default`
@ -162,6 +216,14 @@ OPTIONS:
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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: ARGS:
<version> <version>
@ -211,6 +273,14 @@ OPTIONS:
--shell <shell> --shell <shell>
The shell syntax to use. Infers when missing [possible values: bash, zsh, fish, powershell] The shell syntax to use. Infers when missing [possible values: bash, zsh, fish, powershell]
--version-file-strategy <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` # `fnm exec`
@ -251,6 +321,14 @@ OPTIONS:
--using <version> --using <version>
Either an explicit version, or a filename with the version written in it Either an explicit version, or a filename with the version written in it
--version-file-strategy <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: ARGS:
<arguments>... <arguments>...
@ -274,24 +352,42 @@ USAGE:
fnm install [FLAGS] [OPTIONS] [version] fnm install [FLAGS] [OPTIONS] [version]
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
--lts Install latest LTS Prints help information
-V, --version Prints version information
--lts
Install latest LTS
-V, --version
Prints version information
OPTIONS: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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: ARGS:
<version> A version string. Can be a partial semver or a LTS version name by the format lts/NAME <version>
A version string. Can be a partial semver or a LTS version name by the format lts/NAME
``` ```
# `fnm list` # `fnm list`
@ -304,20 +400,34 @@ USAGE:
fnm list [OPTIONS] fnm list [OPTIONS]
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
-V, --version Prints version information Prints help information
-V, --version
Prints version information
OPTIONS: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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` # `fnm list-remote`
@ -330,20 +440,34 @@ USAGE:
fnm list-remote [OPTIONS] fnm list-remote [OPTIONS]
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
-V, --version Prints version information Prints help information
-V, --version
Prints version information
OPTIONS: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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` # `fnm unalias`
@ -356,23 +480,39 @@ USAGE:
fnm unalias [OPTIONS] <requested-alias> fnm unalias [OPTIONS] <requested-alias>
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
-V, --version Prints version information Prints help information
-V, --version
Prints version information
OPTIONS: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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: ARGS:
<requested-alias> <requested-alias>
``` ```
# `fnm uninstall` # `fnm uninstall`
@ -408,6 +548,14 @@ OPTIONS:
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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: ARGS:
<version> <version>
@ -425,22 +573,43 @@ USAGE:
fnm use [FLAGS] [OPTIONS] [version] fnm use [FLAGS] [OPTIONS] [version]
FLAGS: FLAGS:
-h, --help Prints help information -h, --help
--install-if-missing Install the version if it isn't installed yet Prints help information
-V, --version Prints version 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: OPTIONS:
--arch <arch> --arch <arch>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH] Override the architecture of the installed Node binary. Defaults to arch of fnm binary [env: FNM_ARCH]
[default: x64] [default: x64]
--fnm-dir <base-dir> The root directory of fnm installations [env: FNM_DIR] --fnm-dir <base-dir>
The root directory of fnm installations [env: FNM_DIR]
--log-level <log-level> --log-level <log-level>
The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all, The log level of fnm commands [env: FNM_LOGLEVEL] [default: info] [possible values: quiet, info, all,
error] error]
--node-dist-mirror <node-dist-mirror> --node-dist-mirror <node-dist-mirror>
https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist] https://nodejs.org/dist/ mirror [env: FNM_NODE_DIST_MIRROR] [default: https://nodejs.org/dist]
--version-file-strategy <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: ARGS:
<version> <version>
``` ```

7
src/commands/env.rs

@ -71,6 +71,13 @@ impl Command for Env {
"{}", "{}",
shell.set_env_var("FNM_MULTISHELL_PATH", multishell_path.to_str().unwrap()) 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!( println!(
"{}", "{}",
shell.set_env_var("FNM_DIR", config.base_dir_with_default().to_str().unwrap()) shell.set_env_var("FNM_DIR", config.base_dir_with_default().to_str().unwrap())

2
src/commands/exec.rs

@ -46,7 +46,7 @@ impl Cmd for Exec {
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
UserVersionReader::Path(current_dir) UserVersionReader::Path(current_dir)
}) })
.into_user_version() .into_user_version(config)
.context(CantInferVersion)?; .context(CantInferVersion)?;
let applicable_version = choose_version_for_user_input(&version, config) let applicable_version = choose_version_for_user_input(&version, config)

2
src/commands/install.rs

@ -50,7 +50,7 @@ impl super::command::Command for Install {
let current_version = self let current_version = self
.version()? .version()?
.or_else(|| get_user_version_for_directory(current_dir)) .or_else(|| get_user_version_for_directory(current_dir, config))
.context(CantInferVersion)?; .context(CantInferVersion)?;
let version = match current_version.clone() { let version = match current_version.clone() {

2
src/commands/uninstall.rs

@ -26,7 +26,7 @@ impl Command for Uninstall {
.version .version
.or_else(|| { .or_else(|| {
let current_dir = std::env::current_dir().unwrap(); 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)?; .context(CantInferVersion)?;

70
src/commands/use.rs

@ -1,14 +1,17 @@
use super::command::Command; use super::command::Command;
use super::install::Install; use super::install::Install;
use crate::current_version::current_version;
use crate::fs; use crate::fs;
use crate::installed_versions; use crate::installed_versions;
use crate::outln; use crate::outln;
use crate::system_version; use crate::system_version;
use crate::user_version::UserVersion; use crate::user_version::UserVersion;
use crate::version::Version; use crate::version::Version;
use crate::version_file_strategy::VersionFileStrategy;
use crate::{config::FnmConfig, user_version_reader::UserVersionReader}; use crate::{config::FnmConfig, user_version_reader::UserVersionReader};
use colored::Colorize; use colored::Colorize;
use snafu::{ensure, OptionExt, ResultExt, Snafu}; use snafu::{ensure, OptionExt, ResultExt, Snafu};
use std::path::Path;
use structopt::StructOpt; use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
@ -17,6 +20,11 @@ pub struct Use {
/// Install the version if it isn't installed yet /// Install the version if it isn't installed yet
#[structopt(long)] #[structopt(long)]
install_if_missing: bool, 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 { impl Command for Use {
@ -34,32 +42,34 @@ impl Command for Use {
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
UserVersionReader::Path(current_dir) 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)?; .context(CantInferVersion)?;
let version_path = if let UserVersion::Full(Version::Bypassed) = requested_version { let (message, version_path) = if let UserVersion::Full(Version::Bypassed) =
outln!( requested_version
config, {
Info, let message = format!(
"Bypassing fnm: using {} node", "Bypassing fnm: using {} node",
system_version::display_name().cyan() system_version::display_name().cyan()
); );
system_version::path() (message, system_version::path())
} else if let Some(alias_name) = requested_version.alias_name() { } else if let Some(alias_name) = requested_version.alias_name() {
let alias_path = config.aliases_dir().join(&alias_name); let alias_path = config.aliases_dir().join(&alias_name);
let system_path = system_version::path(); let system_path = system_version::path();
if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path) if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path)
{ {
outln!( let message = format!(
config,
Info,
"Bypassing fnm: using {} node", "Bypassing fnm: using {} node",
system_version::display_name().cyan() system_version::display_name().cyan()
); );
system_path (message, system_path)
} else if alias_path.exists() { } else if alias_path.exists() {
outln!(config, Info, "Using Node for alias {}", alias_name.cyan()); let message = format!("Using Node for alias {}", alias_name.cyan());
alias_path (message, alias_path)
} else { } else {
install_new_version(requested_version, config, self.install_if_missing)?; install_new_version(requested_version, config, self.install_if_missing)?;
return Ok(()); return Ok(());
@ -67,23 +77,36 @@ impl Command for Use {
} else { } else {
let current_version = requested_version.to_version(&all_versions, config); let current_version = requested_version.to_version(&all_versions, config);
if let Some(version) = current_version { if let Some(version) = current_version {
outln!(config, Info, "Using Node {}", version.to_string().cyan()); let version_path = config
config
.installations_dir() .installations_dir()
.join(version.to_string()) .join(version.to_string())
.join("installation") .join("installation");
let message = format!("Using Node {}", version.to_string().cyan());
(message, version_path)
} else { } else {
install_new_version(requested_version, config, self.install_if_missing)?; install_new_version(requested_version, config, self.install_if_missing)?;
return Ok(()); 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)?; replace_symlink(&version_path, multishell_path).context(SymlinkingCreationIssue)?;
Ok(()) 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( fn install_new_version(
requested_version: UserVersion, requested_version: UserVersion,
config: &FnmConfig, config: &FnmConfig,
@ -106,6 +129,7 @@ fn install_new_version(
Use { Use {
version: Some(UserVersionReader::Direct(requested_version)), version: Some(UserVersionReader::Direct(requested_version)),
install_if_missing: true, install_if_missing: true,
silent_if_unchanged: false,
} }
.apply(config)?; .apply(config)?;
@ -187,10 +211,8 @@ pub enum Error {
VersionListingError { source: installed_versions::Error }, VersionListingError { source: installed_versions::Error },
#[snafu(display("Requested version {} is not currently installed", version))] #[snafu(display("Requested version {} is not currently installed", version))]
CantFindVersion { version: UserVersion }, CantFindVersion { version: UserVersion },
#[snafu(display( #[snafu(display("{}", source))]
"Can't find version in dotfiles. Please provide a version manually to the command." CantInferVersion { source: InferVersionError },
))]
CantInferVersion,
#[snafu(display( #[snafu(display(
"{}\n{}\n{}", "{}\n{}\n{}",
"We can't find the necessary environment variables to replace the Node version.", "We can't find the necessary environment variables to replace the Node version.",
@ -199,3 +221,13 @@ pub enum Error {
))] ))]
FnmEnvWasNotSourced, 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 <VERSION>` to set one,\nor create a .node-version file inside your project to declare a Node.js version."))]
Recursive,
}

22
src/config.rs

@ -2,6 +2,7 @@ use crate::arch::Arch;
use crate::log_level::LogLevel; use crate::log_level::LogLevel;
use crate::outln; use crate::outln;
use crate::path_ext::PathExt; use crate::path_ext::PathExt;
use crate::version_file_strategy::VersionFileStrategy;
use colored::Colorize; use colored::Colorize;
use dirs::{data_dir, home_dir}; use dirs::{data_dir, home_dir};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@ -63,6 +64,22 @@ pub struct FnmConfig {
hide_env_values = true hide_env_values = true
)] )]
pub arch: Arch, 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 { impl Default for FnmConfig {
@ -73,11 +90,16 @@ impl Default for FnmConfig {
multishell_path: None, multishell_path: None,
log_level: LogLevel::Info, log_level: LogLevel::Info,
arch: Arch::default(), arch: Arch::default(),
version_file_strategy: VersionFileStrategy::default(),
} }
} }
} }
impl FnmConfig { impl FnmConfig {
pub fn version_file_strategy(&self) -> &VersionFileStrategy {
&self.version_file_strategy
}
pub fn multishell_path(&self) -> Option<&std::path::Path> { pub fn multishell_path(&self) -> Option<&std::path::Path> {
match &self.multishell_path { match &self.multishell_path {
None => None, None => None,

9
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<Version> {
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()
}

2
src/main.rs

@ -28,10 +28,12 @@ mod system_version;
mod user_version; mod user_version;
mod user_version_reader; mod user_version_reader;
mod version; mod version;
mod version_file_strategy;
mod version_files; mod version_files;
#[macro_use] #[macro_use]
mod log_level; mod log_level;
mod default_version;
fn main() { fn main() {
env_logger::init(); env_logger::init();

30
src/shell/bash.rs

@ -1,5 +1,7 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::shell::Shell; use super::shell::Shell;
use indoc::indoc; use indoc::{formatdoc, indoc};
use std::path::Path; use std::path::Path;
#[derive(Debug)] #[derive(Debug)]
@ -18,24 +20,32 @@ impl Shell for Bash {
format!("export {}={:?}", name, value) format!("export {}={:?}", name, value)
} }
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String {
indoc!( let autoload_hook = match config.version_file_strategy() {
VersionFileStrategy::Local => indoc!(
r#" r#"
__fnm_use_if_file_found() {
if [[ -f .node-version || -f .nvmrc ]]; then if [[ -f .node-version || -f .nvmrc ]]; then
fnm use fnm use --silent-if-unchanged
fi fi
} "#
),
VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#,
};
formatdoc!(
r#"
__fnm_use_if_file_found() {{
{autoload_hook}
}}
__fnmcd() { __fnmcd() {{
\cd "$@" || return $? \cd "$@" || return $?
__fnm_use_if_file_found __fnm_use_if_file_found
} }}
alias cd=__fnmcd alias cd=__fnmcd
__fnm_use_if_file_found __fnm_use_if_file_found
"# "#,
autoload_hook = autoload_hook
) )
.into()
} }
} }

26
src/shell/fish.rs

@ -1,5 +1,7 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::shell::Shell; use super::shell::Shell;
use indoc::indoc; use indoc::{formatdoc, indoc};
use std::path::Path; use std::path::Path;
#[derive(Debug)] #[derive(Debug)]
@ -18,19 +20,27 @@ impl Shell for Fish {
format!("set -gx {name} {value:?};", name = name, value = value) format!("set -gx {name} {value:?};", name = name, value = value)
} }
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String {
indoc!( let autoload_hook = match config.version_file_strategy() {
VersionFileStrategy::Local => indoc!(
r#" 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 if test -f .node-version -o -f .nvmrc
fnm use fnm use --silent-if-unchanged
end 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
{autoload_hook}
end end
_fnm_autoload_hook _fnm_autoload_hook
"# "#,
autoload_hook = autoload_hook
) )
.into()
} }
} }

25
src/shell/powershell.rs

@ -1,5 +1,7 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::Shell; use super::Shell;
use indoc::indoc; use indoc::{formatdoc, indoc};
use std::path::Path; use std::path::Path;
#[derive(Debug)] #[derive(Debug)]
@ -18,15 +20,26 @@ impl Shell for PowerShell {
format!(r#"$env:{} = "{}""#, name, value) format!(r#"$env:{} = "{}""#, name, value)
} }
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String {
indoc!(r#" let autoload_hook = match config.version_file_strategy() {
function Set-FnmOnLoad { If ((Test-Path .nvmrc) -Or (Test-Path .node-version)) { & fnm use } } VersionFileStrategy::Local => indoc!(
function Set-LocationWithFnm { param($path); Set-Location $path; Set-FnmOnLoad } 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 Set-Alias cd_with_fnm Set-LocationWithFnm -Force
Remove-Item alias:\cd Remove-Item alias:\cd
New-Alias cd Set-LocationWithFnm New-Alias cd Set-LocationWithFnm
Set-FnmOnLoad Set-FnmOnLoad
"#).into() "#,
autoload_hook = autoload_hook
)
} }
fn to_structopt_shell(&self) -> clap::Shell { fn to_structopt_shell(&self) -> clap::Shell {
clap::Shell::PowerShell clap::Shell::PowerShell

8
src/shell/windows_cmd/cd.cmd

@ -1,10 +1,14 @@
@echo off @echo off
cd %1 cd %1
if "%FNM_VERSION_FILE_STRATEGY%" == "recursive" (
fnm use --silent-if-unchanged
) else (
if exist .nvmrc ( if exist .nvmrc (
fnm use fnm use --silent-if-unchanged
) else ( ) else (
if exist .node-version ( if exist .node-version (
fnm use fnm use --silent-if-unchanged
)
) )
) )
@echo on @echo on

28
src/shell/zsh.rs

@ -1,5 +1,7 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::shell::Shell; use super::shell::Shell;
use indoc::indoc; use indoc::{formatdoc, indoc};
use std::path::Path; use std::path::Path;
#[derive(Debug)] #[derive(Debug)]
@ -22,20 +24,28 @@ impl Shell for Zsh {
Some("rehash".to_string()) Some("rehash".to_string())
} }
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String {
indoc!( let autoload_hook = match config.version_file_strategy() {
VersionFileStrategy::Local => indoc!(
r#" r#"
autoload -U add-zsh-hook
_fnm_autoload_hook () {
if [[ -f .node-version || -f .nvmrc ]]; then if [[ -f .node-version || -f .nvmrc ]]; then
fnm use fnm use --silent-if-unchanged
fi 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 \ add-zsh-hook chpwd _fnm_autoload_hook \
&& _fnm_autoload_hook && _fnm_autoload_hook
"# "#,
autoload_hook = autoload_hook
) )
.into()
} }
} }

15
src/user_version_reader.rs

@ -1,3 +1,4 @@
use crate::config::FnmConfig;
use crate::user_version::UserVersion; use crate::user_version::UserVersion;
use crate::version_files::{get_user_version_for_directory, get_user_version_for_file}; use crate::version_files::{get_user_version_for_directory, get_user_version_for_file};
use std::path::PathBuf; use std::path::PathBuf;
@ -10,11 +11,11 @@ pub enum UserVersionReader {
} }
impl UserVersionReader { impl UserVersionReader {
pub fn into_user_version(self) -> Option<UserVersion> { pub fn into_user_version(self, config: &FnmConfig) -> Option<UserVersion> {
match self { match self {
Self::Direct(uv) => Some(uv), Self::Direct(uv) => Some(uv),
Self::Path(pathbuf) if pathbuf.is_file() => get_user_version_for_file(&pathbuf), 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(); write!(file, "14").unwrap();
let pathbuf = file.path().to_path_buf(); 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))); assert_eq!(user_version, Some(UserVersion::OnlyMajor(14)));
} }
@ -58,14 +60,15 @@ mod tests {
std::fs::write(node_version_path, "14").unwrap(); std::fs::write(node_version_path, "14").unwrap();
let pathbuf = directory.path().to_path_buf(); 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))); assert_eq!(user_version, Some(UserVersion::OnlyMajor(14)));
} }
#[test] #[test]
fn test_direct_to_version() { fn test_direct_to_version() {
let user_version = let user_version = UserVersionReader::Direct(UserVersion::OnlyMajor(14))
UserVersionReader::Direct(UserVersion::OnlyMajor(14)).into_user_version(); .into_user_version(&FnmConfig::default());
assert_eq!(user_version, Some(UserVersion::OnlyMajor(14))); assert_eq!(user_version, Some(UserVersion::OnlyMajor(14)));
} }

41
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<Self, Self::Err> {
match s {
"local" => Ok(VersionFileStrategy::Local),
"recursive" => Ok(VersionFileStrategy::Recursive),
_ => Err(format!(
"Invalid strategy: {}. Expected one of: local, recursive",
s
)),
}
}
}

34
src/version_files.rs

@ -1,4 +1,7 @@
use crate::config::FnmConfig;
use crate::default_version;
use crate::user_version::UserVersion; use crate::user_version::UserVersion;
use crate::version_file_strategy::VersionFileStrategy;
use encoding_rs_io::DecodeReaderBytes; use encoding_rs_io::DecodeReaderBytes;
use log::info; use log::info;
use std::io::Read; use std::io::Read;
@ -7,7 +10,36 @@ use std::str::FromStr;
const PATH_PARTS: [&str; 2] = [".nvmrc", ".node-version"]; const PATH_PARTS: [&str; 2] = [".nvmrc", ".node-version"];
pub fn get_user_version_for_directory(path: impl AsRef<Path>) -> Option<UserVersion> { pub fn get_user_version_for_directory(
path: impl AsRef<Path>,
config: &FnmConfig,
) -> Option<UserVersion> {
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<Path>) -> Option<UserVersion> {
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<Path>) -> Option<UserVersion> {
let path = path.as_ref(); let path = path.as_ref();
for path_part in &PATH_PARTS { for path_part in &PATH_PARTS {

Loading…
Cancel
Save