diff --git a/executable/Install.re b/executable/Install.re index c217e59..a60d425 100644 --- a/executable/Install.re +++ b/executable/Install.re @@ -42,8 +42,11 @@ let main = (~version as versionName) => { , ); - let%lwt filepath = + let%lwt (versionName, filepath) = Versions.getFileToDownload(~version=versionName, ~os, ~arch); + + let%lwt _ = Versions.throwIfInstalled(versionName); + let tarDestination = Filename.concat( Directories.downloads, diff --git a/executable/Use.re b/executable/Use.re index a24d211..31f8712 100644 --- a/executable/Use.re +++ b/executable/Use.re @@ -21,6 +21,12 @@ let switchVersion = (~version, ~quiet) => { | Alias(alias) => Versions.Aliases.toDirectory(alias) |> Lwt.return }; + let versionName = + switch (parsedVersion) { + | Local(v) => v + | Alias(v) => v + }; + let destination = Filename.concat(versionPath, "installation"); let source = Directories.currentVersion; @@ -45,7 +51,10 @@ let switchVersion = (~version, ~quiet) => { }; log( - "Using " version , + + "Using " + versionName + , ); Lwt.return(); diff --git a/feature_tests/partial_semver/run.sh b/feature_tests/partial_semver/run.sh new file mode 100644 index 0000000..5b4b7ad --- /dev/null +++ b/feature_tests/partial_semver/run.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +eval `fnm env --multi` + +fnm install 6 # no new versions would be issued for this unsupported version +fnm install 8.11.3 + +fnm use 6 +if [ "$(node -v)" != "v6.16.0" ]; then + echo "Node version mismatch: $(node -v). Expected: v6.16.0" +fi + +fnm use 8 +if [ "$(node -v)" != "v8.11.3" ]; then + echo "Node version mismatch: $(node -v). Expected: v8.11.3" +fi diff --git a/library/Versions.re b/library/Versions.re index 8f8a825..699f21d 100644 --- a/library/Versions.re +++ b/library/Versions.re @@ -2,6 +2,28 @@ module VersionSet = Set.Make(String); let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return()); +let flip = (fn, a, b) => fn(b, a); + +let skip = (~amount, str) => + Str.last_chars(str, String.length(str) - amount); + +let parseSemver = version => version |> skip(~amount=1) |> Semver.of_string; + +let compare = (v1, v2) => + switch (parseSemver(v1), parseSemver(v2)) { + | (Some(v1), Some(v2)) => Semver.compare(v1, v2) + | (None, _) + | (_, None) => - Core.String.compare(v1, v2) + }; + +let isVersionFitsPrefix = (prefix, version) => { + let length = String.length(prefix); + String.length(version) >= length + + 1 + && Str.first_chars(version, length + 1) == prefix + ++ "."; +}; + module Local = { type t = { name: string, @@ -10,6 +32,18 @@ module Local = { }; let toDirectory = name => Filename.concat(Directories.nodeVersions, name); + + let getLatestInstalledNameByPrefix = prefix => { + open Lwt; + let%lwt versions = + Fs.readdir(Directories.nodeVersions) + >|= List.filter(isVersionFitsPrefix(prefix)) + >|= List.sort(flip(compare)); + switch (versions) { + | [version, ...xs] => Lwt.return_some(version) + | [] => Lwt.return_none + }; + }; }; exception Version_not_found(string); @@ -79,18 +113,6 @@ module Remote = { installed: bool, }; - let skip = (~amount, str) => - Str.last_chars(str, String.length(str) - amount); - - let parseSemver = version => version |> skip(~amount=1) |> Semver.of_string; - - let compare = (v1, v2) => - switch (parseSemver(v1), parseSemver(v2)) { - | (Some(v1), Some(v2)) => Semver.compare(v1, v2) - | (None, _) - | (_, None) => - Core.String.compare(v1, v2) - }; - let getInstalledVersionSet = () => Lwt.( catch(() => Fs.readdir(Directories.nodeVersions), _ => return([])) @@ -142,39 +164,6 @@ let endsWith = (~suffix, str) => { exception No_Download_For_System(System.NodeOS.t, System.NodeArch.t); -let getFileToDownload = (~version as versionName, ~os, ~arch) => { - let versionName = - switch (Str.first_chars(versionName, 1) |> Int32.of_string) { - | _ => "v" ++ versionName - | exception _ => versionName - }; - - let url = - Printf.sprintf("%s%s/", Config.FNM_NODE_DIST_MIRROR.get(), versionName); - - let%lwt html = - try%lwt (Http.makeRequest(url) |> Lwt.map(Http.body)) { - | Http.Not_found(_) => Lwt.fail(Version_not_found(versionName)) - }; - let filenames = - html - |> Remote.getRelativeLinksFromHTML - |> List.filter( - endsWith( - ~suffix= - System.NodeOS.toString(os) - ++ "-" - ++ System.NodeArch.toString(arch) - ++ Remote.downloadFileSuffix, - ), - ); - - switch (filenames |> List.hd) { - | x => Lwt.return(url ++ x) - | exception _ => Lwt.fail(No_Download_For_System(os, arch)) - }; -}; - let getCurrentVersion = () => switch (Fs.realpath(Directories.currentVersion)) { | installationPath => @@ -187,8 +176,7 @@ let getCurrentVersion = () => let getInstalledVersions = () => { let%lwt versions = - Fs.readdir(Directories.nodeVersions) - |> Lwt.map(List.sort(Remote.compare)) + Fs.readdir(Directories.nodeVersions) |> Lwt.map(List.sort(compare)) and aliases = Aliases.byVersion(); versions @@ -216,7 +204,7 @@ let getRemoteVersions = () => { Str.last_chars(x, 1) == "/" && Str.first_chars(x, 1) != "." ) |> Core.List.map(~f=x => Str.first_chars(x, String.length(x) - 1)) - |> List.sort(Remote.compare) + |> List.sort(compare) |> List.map(name => Remote.{ name, @@ -228,6 +216,70 @@ let getRemoteVersions = () => { |> Lwt.return; }; +let getRemoteLatestVersionByPrefix = prefix => { + open Remote; + open Lwt; + + let%lwt remoteVersions = getRemoteVersions(); + let compatibleVersions = + remoteVersions + |> List.map(x => x.name) + |> List.filter(isVersionFitsPrefix(prefix)) + |> List.sort(flip(compare)); + + switch (compatibleVersions) { + | [version, ...vs] => Lwt.return_some(version) + | [] => Lwt.return_none + }; +}; + +let getExactFileToDownload = (~version as versionName, ~os, ~arch) => { + let versionName = + switch (Str.first_chars(versionName, 1) |> Int32.of_string) { + | _ => "v" ++ versionName + | exception _ => versionName + }; + + let url = + Printf.sprintf("%s%s/", Config.FNM_NODE_DIST_MIRROR.get(), versionName); + + let%lwt html = + try%lwt (Http.makeRequest(url) |> Lwt.map(Http.body)) { + | Http.Not_found(_) => Lwt.fail(Version_not_found(versionName)) + }; + + let filenames = + html + |> Remote.getRelativeLinksFromHTML + |> List.filter( + endsWith( + ~suffix= + System.NodeOS.toString(os) + ++ "-" + ++ System.NodeArch.toString(arch) + ++ Remote.downloadFileSuffix, + ), + ); + + switch (filenames |> List.hd) { + | filename => + let nodeVersion = List.nth(String.split_on_char('-', filename), 1); + Lwt.return((nodeVersion, url ++ filename)); + | exception _ => Lwt.fail(No_Download_For_System(os, arch)) + }; +}; + +let getFileToDownload = (~version, ~os, ~arch) => { + try%lwt (getExactFileToDownload(~version, ~os, ~arch)) { + | Version_not_found(_) as e => + switch%lwt (getRemoteLatestVersionByPrefix(version)) { + | None => Lwt.fail(e) + | Some(exactVersion) => + getExactFileToDownload(~version=exactVersion, ~os, ~arch) + } + }; +}; + type t = | Alias(string) | Local(string); @@ -238,12 +290,15 @@ let parse = version => { let versionPath = Local.toDirectory(formattedVersion); let%lwt aliasExists = Lwt_unix.file_exists(aliasPath) - and versionExists = Lwt_unix.file_exists(versionPath); - - switch (versionExists, aliasExists) { - | (true, _) => Some(Local(formattedVersion)) |> Lwt.return - | (_, true) => Some(Alias(version)) |> Lwt.return - | (false, false) => Lwt.return_none + and versionExists = Lwt_unix.file_exists(versionPath) + and versionByPrefixPath = + Local.getLatestInstalledNameByPrefix(formattedVersion); + + switch (versionExists, aliasExists, versionByPrefixPath) { + | (true, _, _) => Some(Local(formattedVersion)) |> Lwt.return + | (_, true, _) => Some(Alias(version)) |> Lwt.return + | (_, false, Some(version)) => Some(Local(version)) |> Lwt.return + | (false, false, None) => Lwt.return_none }; };