Skip to content

Commit 5edc6cc

Browse files
authored
Add Version File Parsing (#126)
1 parent 2a32f0a commit 5edc6cc

File tree

10 files changed

+244
-51
lines changed

10 files changed

+244
-51
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
- uses: actions/checkout@v3
3131

3232
- uses: arduino/setup-task@v1
33+
with:
34+
repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
3335

3436
- uses: actions-rs/toolchain@v1
3537
with:
@@ -63,6 +65,8 @@ jobs:
6365
- uses: actions/checkout@v3
6466

6567
- uses: arduino/setup-task@v1
68+
with:
69+
repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
6670

6771
- uses: actions-rs/toolchain@v1
6872
with:
@@ -84,6 +88,8 @@ jobs:
8488
- uses: actions/checkout@v3
8589

8690
- uses: arduino/setup-task@v1
91+
with:
92+
repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
8793

8894
- uses: actions-rs/toolchain@v1
8995
with:

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ anyhow = "1.0.58"
1111
clap = { version = "3.2.8", features = ["derive", "env", "cargo"] }
1212
dialoguer = "0.10.1"
1313
dirs = "4.0.0"
14+
itertools = "0.10.3"
1415
node-semver = "2.0.0"
1516
reqwest = { version = "0.11.11", features = ["blocking"] }
1617
serde = { version = "1.0.138", features = ["derive"] }

README.md

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,61 @@
1-
# nvm(-rust)
2-
3-
Cross platform nvm that doesn't suck™
4-
5-
## Feature Comparison
6-
7-
| | **nvm-rust** | [nvm-windows](https://github.com/coreybutler/nvm-windows) | [nvm](https://github.com/nvm-sh/nvm) |
8-
| ---: | :---: | :---: | :---: |
9-
| Platforms | [Rust Platforms](https://doc.rust-lang.org/nightly/rustc/platform-support.html#tier-1) | Windows | POSIX |
10-
| [Range matching](#range-matching) ||||
11-
| [.nvmrc](#nvmrc) | 🔧 |||
12-
| [Default global packages](#default-global-packages) | 🔧 |||
13-
| Node <4 |* |||
14-
| Disabling nvm temporarily ||||
15-
| Caching ||||
16-
| Aliases ||||
17-
18-
\*not supported, might work?
19-
20-
### Range Matching
21-
22-
Allowing you to not have to write out the full versions when running a command.
23-
24-
For example:
25-
26-
- `nvm install 12` will install the latest version matching `12`, instead of `12.0.0`.
27-
- `nvm install "12 <12.18"` will install the latest `12.17.x` version, instead of just giving you an error.
28-
- `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?).
29-
30-
### .nvmrc
31-
32-
### Default global packages
1+
# nvm(-rust)
2+
3+
Cross platform nvm that doesn't suck™
4+
5+
## Feature Comparison
6+
7+
| | **nvm-rust** | [nvm-windows](https://github.com/coreybutler/nvm-windows) | [nvm](https://github.com/nvm-sh/nvm) |
8+
|-----------------------------------------------------------------------:|:---------------:|:---------------------------------------------------------:|:------------------------------------:|
9+
| Platforms | Win, Mac, Linux | Windows | POSIX |
10+
| [Range matching](#range-matching) ||||
11+
| [Version files](#version-files-packagejsonengines-nvmrc-tool-versions) ||||
12+
| [Default global packages](#default-global-packages) | 🔧 |||
13+
| Node <4 |* |||
14+
| Disabling nvm temporarily ||||
15+
| Caching ||||
16+
| Aliases ||||
17+
18+
19+
20+
**not supported, might work?
21+
22+
### Range Matching
23+
24+
Allowing you to not have to write out the full versions when running a command.
25+
26+
For example:
27+
28+
- `nvm install 12` will install the latest version matching `12`, instead of `12.0.0`.
29+
- `nvm install "12 <12.18"` will install the latest `12.17.x` version, instead of just giving you an error.
30+
- `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?).
31+
32+
### Version files (`package.json#engines`, `.nvmrc`, `.tool-versions`)
33+
34+
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!
35+
36+
nvm-rust handles files containing ranges, unlike [nvm](https://github.com/nvm-sh/nvm).
37+
38+
e.g.
39+
40+
```
41+
// package.json
42+
{
43+
...
44+
"engines": {
45+
"node": "^14.17"
46+
}
47+
...
48+
}
49+
50+
# Installs 14.19.3 as of the time of writing
51+
$ nvm install
52+
```
53+
54+
The program will use the following file priority:
55+
56+
1. `package.json#engines`
57+
2. `.nvmrc`
58+
3. `.node-version`
59+
4. [`.tool-versions` from `asdf`](https://asdf-vm.com/guide/getting-started.html#local)
60+
61+
### Default global packages

src/files/mod.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use std::{fs, path::PathBuf};
2+
3+
use itertools::Itertools;
4+
use node_semver::Range;
5+
6+
pub mod package_json;
7+
8+
const PACKAGE_JSON_FILE_NAME: &str = "package.json";
9+
const NVMRC_FILE_NAME: &str = ".nvmrc";
10+
const NODE_VERSION_FILE_NAME: &str = ".node-version";
11+
const ASDF_FILE_NAME: &str = ".tool-versions";
12+
13+
pub enum VersionFile {
14+
Nvmrc(Range),
15+
PackageJson(Range),
16+
Asdf(Range),
17+
}
18+
19+
impl VersionFile {
20+
pub fn range(self) -> Range {
21+
match self {
22+
VersionFile::Nvmrc(range) => range,
23+
VersionFile::PackageJson(range) => range,
24+
VersionFile::Asdf(range) => range,
25+
}
26+
}
27+
}
28+
29+
pub fn get_version_file() -> Option<VersionFile> {
30+
if PathBuf::from(PACKAGE_JSON_FILE_NAME).exists() {
31+
let parse_result =
32+
package_json::PackageJson::try_from(PathBuf::from(PACKAGE_JSON_FILE_NAME));
33+
34+
if let Ok(parse_result) = parse_result {
35+
return parse_result
36+
.engines
37+
.and_then(|engines| engines.node)
38+
.map(VersionFile::PackageJson);
39+
} else {
40+
println!(
41+
"Failed to parse package.json: {}",
42+
parse_result.unwrap_err()
43+
);
44+
}
45+
}
46+
47+
if let Some(existing_file) = [NVMRC_FILE_NAME, NODE_VERSION_FILE_NAME]
48+
.iter()
49+
.find_or_first(|&path| PathBuf::from(path).exists())
50+
{
51+
let contents = fs::read_to_string(existing_file);
52+
53+
if let Ok(contents) = contents {
54+
let parse_result = Range::parse(&contents);
55+
56+
if let Ok(parse_result) = parse_result {
57+
return Some(VersionFile::Nvmrc(parse_result));
58+
} else {
59+
println!(
60+
"Failed to parse {}: '{}'",
61+
existing_file,
62+
parse_result.unwrap_err().input(),
63+
);
64+
}
65+
}
66+
}
67+
68+
if PathBuf::from(ASDF_FILE_NAME).exists() {
69+
let contents = fs::read_to_string(ASDF_FILE_NAME);
70+
71+
if let Ok(contents) = contents {
72+
let version_string = contents
73+
.lines()
74+
.find(|line| line.starts_with("nodejs"))
75+
.and_then(|line| line.split(' ').nth(1));
76+
77+
if let Some(version_string) = version_string {
78+
let parse_result = Range::parse(&version_string);
79+
80+
if let Ok(parse_result) = parse_result {
81+
return Some(VersionFile::Asdf(parse_result));
82+
} else {
83+
println!(
84+
"Failed to parse {}: '{}'",
85+
ASDF_FILE_NAME,
86+
parse_result.unwrap_err().input(),
87+
);
88+
}
89+
}
90+
}
91+
}
92+
93+
None
94+
}

src/files/package_json.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use std::{fs, path::PathBuf};
2+
3+
use node_semver::Range;
4+
use serde::Deserialize;
5+
6+
#[derive(Clone, Deserialize, Debug, Eq, PartialEq)]
7+
pub struct PackageJson {
8+
#[serde()]
9+
pub name: Option<String>,
10+
#[serde()]
11+
pub version: Option<String>,
12+
#[serde()]
13+
pub engines: Option<PackageJsonEngines>,
14+
}
15+
16+
#[derive(Clone, Deserialize, Debug, Eq, PartialEq)]
17+
pub struct PackageJsonEngines {
18+
#[serde()]
19+
pub node: Option<Range>,
20+
}
21+
22+
impl TryFrom<PathBuf> for PackageJson {
23+
type Error = anyhow::Error;
24+
25+
fn try_from(path: PathBuf) -> Result<Self, anyhow::Error> {
26+
let contents = fs::read_to_string(path)?;
27+
let package_json: PackageJson = serde_json::from_str(&contents)?;
28+
29+
Ok(package_json)
30+
}
31+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::subcommand::{
1414
mod archives;
1515
mod node_version;
1616
mod subcommand;
17+
mod files;
1718

1819
#[derive(Parser, Clone, Debug)]
1920
enum Subcommands {

src/subcommand/install.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use clap::{AppSettings, Parser};
55
use node_semver::Range;
66

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

3030
impl Action<InstallCommand> for InstallCommand {
3131
fn run(config: &Config, options: &InstallCommand) -> Result<()> {
32+
let version_filter = options
33+
.version
34+
.clone()
35+
.xor(files::get_version_file().map(|version_file| version_file.range()));
36+
37+
if version_filter.is_none() {
38+
anyhow::bail!("You did not pass a version and we did not find any version files (package.json#engines, .nvmrc) in the current directory.");
39+
}
40+
let version_filter = version_filter.unwrap();
41+
3242
let online_versions = OnlineNodeVersion::fetch_all()?;
33-
let filtered_versions = node_version::filter_version_req(online_versions, &options.version);
43+
let filtered_versions = node_version::filter_version_req(online_versions, &version_filter);
3444

3545
let version_to_install = filtered_versions.first().context(format!(
3646
"Did not find a version matching `{}`!",
37-
options.version
47+
&version_filter
3848
))?;
3949

4050
if !config.force && InstalledNodeVersion::is_installed(config, version_to_install.version())
@@ -64,7 +74,7 @@ impl Action<InstallCommand> for InstallCommand {
6474
SwitchCommand::run(
6575
&config.with_force(),
6676
&SwitchCommand {
67-
version: Range::parse(version_to_install.to_string())?,
77+
version: Some(Range::parse(version_to_install.to_string())?),
6878
},
6979
)?;
7080
}

src/subcommand/parse_version.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::Result;
22
use clap::{AppSettings, Parser};
33
use node_semver::Range;
44

5-
use crate::{node_version::is_version_range, subcommand::Action, Config};
5+
use crate::{files, node_version::is_version_range, subcommand::Action, Config};
66

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

2020
impl Action<ParseVersionCommand> for ParseVersionCommand {
2121
fn run(_: &Config, options: &ParseVersionCommand) -> Result<()> {
22-
match Range::parse(&options.version) {
22+
let version = options.version.clone();
23+
24+
if version.is_none() {
25+
if let Some(version_from_files) = files::get_version_file() {
26+
println!("{}", version_from_files.range());
27+
28+
return Ok(());
29+
}
30+
}
31+
32+
if version.is_none() {
33+
anyhow::bail!("Did not get a version");
34+
}
35+
let version = version.unwrap();
36+
37+
match Range::parse(&version) {
2338
Ok(result) => {
2439
println!(
2540
"{:^pad$}\n{:^pad$}\n{}",
26-
options.version,
41+
version,
2742
"⬇",
2843
result,
2944
pad = result.to_string().len()
3045
);
3146
Ok(())
3247
},
3348
Err(err) => {
34-
println!(
35-
"Failed to parse `{}`: `{}`",
36-
options.version,
37-
err
38-
);
49+
println!("Failed to parse `{}`", err.input());
3950
Ok(())
4051
},
4152
}

0 commit comments

Comments
 (0)