Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
- uses: actions/checkout@v3

- uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}

- uses: actions-rs/toolchain@v1
with:
Expand Down Expand Up @@ -63,6 +65,8 @@ jobs:
- uses: actions/checkout@v3

- uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}

- uses: actions-rs/toolchain@v1
with:
Expand All @@ -84,6 +88,8 @@ jobs:
- uses: actions/checkout@v3

- uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}

- uses: actions-rs/toolchain@v1
with:
Expand Down
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ anyhow = "1.0.58"
clap = { version = "3.2.8", features = ["derive", "env", "cargo"] }
dialoguer = "0.10.1"
dirs = "4.0.0"
itertools = "0.10.3"
node-semver = "2.0.0"
reqwest = { version = "0.11.11", features = ["blocking"] }
serde = { version = "1.0.138", features = ["derive"] }
Expand Down
93 changes: 61 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,61 @@
# nvm(-rust)

Cross platform nvm that doesn't suck™

## Feature Comparison

| | **nvm-rust** | [nvm-windows](https://github.com/coreybutler/nvm-windows) | [nvm](https://github.com/nvm-sh/nvm) |
| ---: | :---: | :---: | :---: |
| Platforms | [Rust Platforms](https://doc.rust-lang.org/nightly/rustc/platform-support.html#tier-1) | Windows | POSIX |
| [Range matching](#range-matching) | ✅ | ❌ | ✅ |
| [.nvmrc](#nvmrc) | 🔧 | ❌ | ✅ |
| [Default global packages](#default-global-packages) | 🔧 | ❌ | ✅ |
| Node <4 | ✅* | ✅ | ✅ |
| Disabling nvm temporarily | ❌ | ✅ | ✅ |
| Caching | ❌ | ❌ | ✅ |
| Aliases | ❌ | ❌ | ✅ |

\*not supported, might work?

### Range Matching

Allowing you to not have to write out the full versions when running a command.

For example:

- `nvm install 12` will install the latest version matching `12`, instead of `12.0.0`.
- `nvm install "12 <12.18"` will install the latest `12.17.x` version, instead of just giving you an error.
- `nvm use 12` switch use the newest installed `12.x.x` version instead of `12.0.0` (and most likely giving you an error, who has that version installed?).

### .nvmrc

### Default global packages
# nvm(-rust)

Cross platform nvm that doesn't suck™

## Feature Comparison

| | **nvm-rust** | [nvm-windows](https://github.com/coreybutler/nvm-windows) | [nvm](https://github.com/nvm-sh/nvm) |
|-----------------------------------------------------------------------:|:---------------:|:---------------------------------------------------------:|:------------------------------------:|
| Platforms | Win, Mac, Linux | Windows | POSIX |
| [Range matching](#range-matching) | ✅ | ❌ | ✅ |
| [Version files](#version-files-packagejsonengines-nvmrc-tool-versions) | ✅ | ❌ | ✅ |
| [Default global packages](#default-global-packages) | 🔧 | ❌ | ✅ |
| Node <4 | ✅* | ✅ | ✅ |
| Disabling nvm temporarily | ❌ | ✅ | ✅ |
| Caching | ❌ | ❌ | ✅ |
| Aliases | ❌ | ❌ | ✅ |



**not supported, might work?

### Range Matching

Allowing you to not have to write out the full versions when running a command.

For example:

- `nvm install 12` will install the latest version matching `12`, instead of `12.0.0`.
- `nvm install "12 <12.18"` will install the latest `12.17.x` version, instead of just giving you an error.
- `nvm use 12` switch use the newest installed `12.x.x` version instead of `12.0.0` (and most likely giving you an error, who has that version installed?).

### Version files (`package.json#engines`, `.nvmrc`, `.tool-versions`)

If a version is not specified for the `use` and `install` commands nvm-rust will look for and parse any files containing Node version specifications amd use that!

nvm-rust handles files containing ranges, unlike [nvm](https://github.com/nvm-sh/nvm).

e.g.

```
// package.json
{
...
"engines": {
"node": "^14.17"
}
...
}

# Installs 14.19.3 as of the time of writing
$ nvm install
```

The program will use the following file priority:

1. `package.json#engines`
2. `.nvmrc`
3. `.node-version`
4. [`.tool-versions` from `asdf`](https://asdf-vm.com/guide/getting-started.html#local)

### Default global packages
94 changes: 94 additions & 0 deletions src/files/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::{fs, path::PathBuf};

use itertools::Itertools;
use node_semver::Range;

pub mod package_json;

const PACKAGE_JSON_FILE_NAME: &str = "package.json";
const NVMRC_FILE_NAME: &str = ".nvmrc";
const NODE_VERSION_FILE_NAME: &str = ".node-version";
const ASDF_FILE_NAME: &str = ".tool-versions";

pub enum VersionFile {
Nvmrc(Range),
PackageJson(Range),
Asdf(Range),
}

impl VersionFile {
pub fn range(self) -> Range {
match self {
VersionFile::Nvmrc(range) => range,
VersionFile::PackageJson(range) => range,
VersionFile::Asdf(range) => range,
}
}
}

pub fn get_version_file() -> Option<VersionFile> {
if PathBuf::from(PACKAGE_JSON_FILE_NAME).exists() {
let parse_result =
package_json::PackageJson::try_from(PathBuf::from(PACKAGE_JSON_FILE_NAME));

if let Ok(parse_result) = parse_result {
return parse_result
.engines
.and_then(|engines| engines.node)
.map(VersionFile::PackageJson);
} else {
println!(
"Failed to parse package.json: {}",
parse_result.unwrap_err()
);
}
}

if let Some(existing_file) = [NVMRC_FILE_NAME, NODE_VERSION_FILE_NAME]
.iter()
.find_or_first(|&path| PathBuf::from(path).exists())
{
let contents = fs::read_to_string(existing_file);

if let Ok(contents) = contents {
let parse_result = Range::parse(&contents);

if let Ok(parse_result) = parse_result {
return Some(VersionFile::Nvmrc(parse_result));
} else {
println!(
"Failed to parse {}: '{}'",
existing_file,
parse_result.unwrap_err().input(),
);
}
}
}

if PathBuf::from(ASDF_FILE_NAME).exists() {
let contents = fs::read_to_string(ASDF_FILE_NAME);

if let Ok(contents) = contents {
let version_string = contents
.lines()
.find(|line| line.starts_with("nodejs"))
.and_then(|line| line.split(' ').nth(1));

if let Some(version_string) = version_string {
let parse_result = Range::parse(&version_string);

if let Ok(parse_result) = parse_result {
return Some(VersionFile::Asdf(parse_result));
} else {
println!(
"Failed to parse {}: '{}'",
ASDF_FILE_NAME,
parse_result.unwrap_err().input(),
);
}
}
}
}

None
}
31 changes: 31 additions & 0 deletions src/files/package_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::{fs, path::PathBuf};

use node_semver::Range;
use serde::Deserialize;

#[derive(Clone, Deserialize, Debug, Eq, PartialEq)]
pub struct PackageJson {
#[serde()]
pub name: Option<String>,
#[serde()]
pub version: Option<String>,
#[serde()]
pub engines: Option<PackageJsonEngines>,
}

#[derive(Clone, Deserialize, Debug, Eq, PartialEq)]
pub struct PackageJsonEngines {
#[serde()]
pub node: Option<Range>,
}

impl TryFrom<PathBuf> for PackageJson {
type Error = anyhow::Error;

fn try_from(path: PathBuf) -> Result<Self, anyhow::Error> {
let contents = fs::read_to_string(path)?;
let package_json: PackageJson = serde_json::from_str(&contents)?;

Ok(package_json)
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::subcommand::{
mod archives;
mod node_version;
mod subcommand;
mod files;

#[derive(Parser, Clone, Debug)]
enum Subcommands {
Expand Down
20 changes: 15 additions & 5 deletions src/subcommand/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clap::{AppSettings, Parser};
use node_semver::Range;

use crate::{
archives, node_version,
archives, files, node_version,
node_version::{InstalledNodeVersion, NodeVersion, OnlineNodeVersion},
subcommand::{switch::SwitchCommand, Action},
Config,
Expand All @@ -21,20 +21,30 @@ setting = AppSettings::ColoredHelp
pub struct InstallCommand {
/// A semver range. The latest version matching this range will be installed
#[clap(validator = node_version::is_version_range)]
pub version: Range,
pub version: Option<Range>,
/// Switch to the new version after installing it
#[clap(long, short, default_value("false"))]
pub switch: bool,
}

impl Action<InstallCommand> for InstallCommand {
fn run(config: &Config, options: &InstallCommand) -> Result<()> {
let version_filter = options
.version
.clone()
.xor(files::get_version_file().map(|version_file| version_file.range()));

if version_filter.is_none() {
anyhow::bail!("You did not pass a version and we did not find any version files (package.json#engines, .nvmrc) in the current directory.");
}
let version_filter = version_filter.unwrap();

let online_versions = OnlineNodeVersion::fetch_all()?;
let filtered_versions = node_version::filter_version_req(online_versions, &options.version);
let filtered_versions = node_version::filter_version_req(online_versions, &version_filter);

let version_to_install = filtered_versions.first().context(format!(
"Did not find a version matching `{}`!",
options.version
&version_filter
))?;

if !config.force && InstalledNodeVersion::is_installed(config, version_to_install.version())
Expand Down Expand Up @@ -64,7 +74,7 @@ impl Action<InstallCommand> for InstallCommand {
SwitchCommand::run(
&config.with_force(),
&SwitchCommand {
version: Range::parse(version_to_install.to_string())?,
version: Some(Range::parse(version_to_install.to_string())?),
},
)?;
}
Expand Down
29 changes: 20 additions & 9 deletions src/subcommand/parse_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use clap::{AppSettings, Parser};
use node_semver::Range;

use crate::{node_version::is_version_range, subcommand::Action, Config};
use crate::{files, node_version::is_version_range, subcommand::Action, Config};

#[derive(Parser, Clone, Debug)]
#[clap(
Expand All @@ -14,28 +14,39 @@ setting = AppSettings::Hidden
pub struct ParseVersionCommand {
/// The semver range to echo the parsed result of
#[clap(validator = is_version_range)]
pub version: String,
pub version: Option<String>,
}

impl Action<ParseVersionCommand> for ParseVersionCommand {
fn run(_: &Config, options: &ParseVersionCommand) -> Result<()> {
match Range::parse(&options.version) {
let version = options.version.clone();

if version.is_none() {
if let Some(version_from_files) = files::get_version_file() {
println!("{}", version_from_files.range());

return Ok(());
}
}

if version.is_none() {
anyhow::bail!("Did not get a version");
}
let version = version.unwrap();

match Range::parse(&version) {
Ok(result) => {
println!(
"{:^pad$}\n{:^pad$}\n{}",
options.version,
version,
"⬇",
result,
pad = result.to_string().len()
);
Ok(())
},
Err(err) => {
println!(
"Failed to parse `{}`: `{}`",
options.version,
err
);
println!("Failed to parse `{}`", err.input());
Ok(())
},
}
Expand Down
Loading