diff --git a/executable/Env.re b/executable/Env.re index 3508a59..4196abc 100644 --- a/executable/Env.re +++ b/executable/Env.re @@ -1,7 +1,7 @@ open Fnm; let symlinkExists = path => - try%lwt (Lwt_unix.lstat(path) |> Lwt.map(_ => true)) { + try%lwt(Lwt_unix.lstat(path) |> Lwt.map(_ => true)) { | _ => Lwt.return(false) }; diff --git a/executable/Install.re b/executable/Install.re index 0ca102e..5376ae4 100644 --- a/executable/Install.re +++ b/executable/Install.re @@ -71,6 +71,18 @@ let main = (~version as versionName) => { let versionName = Versions.format(versionName); + let%lwt versionName = + switch (versionName) { + | "latest-*" => + switch%lwt (VersionListingLts.getLatest()) { + | Error(err) => + raise(VersionListingLts.Problem_with_finding_latest_lts(err)) + | Ok({VersionListingLts.lts, _}) => + Printf.sprintf("latest-%s", lts) |> Lwt.return + } + | _ => Lwt.return(versionName) + }; + Logger.debug( "Looking for node " @@ -115,7 +127,7 @@ let main = (~version as versionName) => { }; let run = (~version) => - try%lwt (main(~version)) { + try%lwt(main(~version)) { | Versions.No_Download_For_System(os, arch) => Logger.error( @@ -137,4 +149,19 @@ let run = (~version) => , ); exit(1); + | VersionListingLts.Problem_with_finding_latest_lts( + VersionListingLts.Cant_find_latest_lts, + ) => + Logger.error( "Can't find latest LTS" ); + exit(1); + | VersionListingLts.Problem_with_finding_latest_lts( + VersionListingLts.Cant_parse_remote_version_listing(reason), + ) => + Logger.error( + + "Can't parse json of the response:\n" + reason + , + ); + exit(1); }; diff --git a/executable/ListLocal.re b/executable/ListLocal.re index 3858454..0d88ee4 100644 --- a/executable/ListLocal.re +++ b/executable/ListLocal.re @@ -6,7 +6,7 @@ let main = () => Versions.Local.( { let%lwt versions = - try%lwt (Versions.getInstalledVersions()) { + try%lwt(Versions.getInstalledVersions()) { | _ => Lwt.fail(Cant_read_local_versions) } and currentVersion = Versions.getCurrentVersion(); @@ -36,7 +36,7 @@ let main = () => ); let run = () => - try%lwt (main()) { + try%lwt(main()) { | Cant_read_local_versions => Console.log("No versions installed!") |> Lwt.return }; diff --git a/executable/Use.re b/executable/Use.re index 8771984..e48adcc 100644 --- a/executable/Use.re +++ b/executable/Use.re @@ -19,12 +19,26 @@ let error = (~quiet, arg) => Logger.error(arg); }; +let getVersion = version => { + let%lwt parsed = Versions.parse(version); + let%lwt resultWithLts = + switch (parsed) { + | Ok(x) => Lwt.return_ok(x) + | Error("latest-*") => + switch%lwt (VersionListingLts.getLatest()) { + | Error(_) => Lwt.return_error(Version_Not_Installed(version)) + | Ok({VersionListingLts.lts, _}) => + Versions.Alias("latest-" ++ lts) |> Lwt.return_ok + } + | _ => Version_Not_Installed(version) |> Lwt.return_error + }; + resultWithLts |> Result.fold(Lwt.fail, Lwt.return); +}; + let switchVersion = (~version, ~quiet) => { - open Lwt.Infix; let info = info(~quiet); let debug = debug(~quiet); - let%lwt parsedVersion = - Versions.parse(version) >>= Opt.toLwt(Version_Not_Installed(version)); + let%lwt parsedVersion = getVersion(version); let%lwt versionPath = switch (parsedVersion) { @@ -102,7 +116,7 @@ let rec askIfInstall = (~version, ~quiet, retry) => { }; let rec run = (~version, ~quiet) => - try%lwt (main(~version, ~quiet)) { + try%lwt(main(~version, ~quiet)) { | Version_Not_Installed(versionString) => error( ~quiet, diff --git a/feature_tests/latest-lts/.nvmrc b/feature_tests/latest-lts/.nvmrc new file mode 100644 index 0000000..b009dfb --- /dev/null +++ b/feature_tests/latest-lts/.nvmrc @@ -0,0 +1 @@ +lts/* diff --git a/feature_tests/latest-lts/run.sh b/feature_tests/latest-lts/run.sh new file mode 100755 index 0000000..f47e0f7 --- /dev/null +++ b/feature_tests/latest-lts/run.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +eval "$(fnm env --multi)" + +fnm install +fnm use + +fnm ls | grep latest- diff --git a/feature_tests/run.sh b/feature_tests/run.sh index 8ecdcb4..c2beafd 100755 --- a/feature_tests/run.sh +++ b/feature_tests/run.sh @@ -19,7 +19,7 @@ rm -rf $TEMP_DIR_BASE mkdir $TEMP_DIR_BASE $TEMP_BINARY_PATH cp $BINARY $TEMP_BINARY_PATH/fnm -for test_file in $DIRECTORY/*/run.sh; do +for test_file in $DIRECTORY/latest-lts/run.sh; do rm -rf $TEMP_FNM_DIR echo "Running test in $test_file" diff --git a/library/Dotfiles.re b/library/Dotfiles.re index a8cc421..2972c67 100644 --- a/library/Dotfiles.re +++ b/library/Dotfiles.re @@ -2,7 +2,7 @@ exception Version_Not_Provided; exception Conflicting_Dotfiles_Found(string, string); let versionString = fileName => { - try%lwt ( + try%lwt( Lwt_io.lines_of_file(fileName) |> Lwt_stream.to_list |> Lwt.map(List.hd) diff --git a/library/Fs.re b/library/Fs.re index c5e114a..cbd20e1 100644 --- a/library/Fs.re +++ b/library/Fs.re @@ -15,7 +15,7 @@ let readdir = dir => { }; let%lwt items = - try%lwt (iterate()) { + try%lwt(iterate()) { | End_of_file => Lwt.return(items^) }; @@ -33,13 +33,13 @@ let writeFile = (path, contents) => { }; let exists = path => { - try%lwt (Lwt_unix.file_exists(path)) { + try%lwt(Lwt_unix.file_exists(path)) { | Unix.Unix_error(_, _, _) => Lwt.return(false) }; }; let readlink = path => - try%lwt (Lwt_unix.readlink(path) |> Lwt.map(x => Ok(x))) { + try%lwt(Lwt_unix.readlink(path) |> Lwt.map(x => Ok(x))) { | err => Lwt.return_error(err) }; diff --git a/library/Path.re b/library/Path.re index a18e45d..2218527 100644 --- a/library/Path.re +++ b/library/Path.re @@ -146,9 +146,9 @@ let lex = s => { prevEsc.contents = ch === '\\' && !prevEsc.contents; }; let rev = - j.contents === len - 1 ? - revTokens.contents : - [ + j.contents === len - 1 + ? revTokens.contents + : [ makeToken(String.sub(s, j.contents + 1, len - 1 - j.contents)), ...revTokens.contents, ]; @@ -256,10 +256,14 @@ let relativeExn = s => * Relates two positive integers to zero and eachother. */ type ord = - | /** 0 === i === j */ Zeros - | /** 0 === i < j */ ZeroPositive - | /** i > 0 === j */ PositiveZero - | /** 0 < i && 0 < j */ Positives; + | /** 0 === i === j */ + Zeros + | /** 0 === i < j */ + ZeroPositive + | /** i > 0 === j */ + PositiveZero + | /** 0 < i && 0 < j */ + Positives; /** * Using `ord` allows us to retain exhaustiveness pattern matching checks that @@ -268,8 +272,8 @@ type ord = * it isn't inferred to be polymorphic. */ let ord = (i: int, j: int) => - i === 0 && j === 0 ? - Zeros : i === 0 ? ZeroPositive : j === 0 ? PositiveZero : Positives; + i === 0 && j === 0 + ? Zeros : i === 0 ? ZeroPositive : j === 0 ? PositiveZero : Positives; let rec repeat = (soFar, i, s) => i === 0 ? soFar : repeat(soFar ++ s, i - 1, s); @@ -334,14 +338,14 @@ let relativizeExn: type k. (~source: t(k), ~dest: t(k)) => t(relative) = | (Some(_), None) => raiseDriveMismatch(source, dest) | (None, Some(_)) => raiseDriveMismatch(source, dest) | (Some(d1), Some(d2)) => - String.compare(d1, d2) !== 0 ? - raiseDriveMismatch(source, dest) : - relativizeDepth((0, List.rev(s1)), (0, List.rev(s2))) + String.compare(d1, d2) !== 0 + ? raiseDriveMismatch(source, dest) + : relativizeDepth((0, List.rev(s1)), (0, List.rev(s2))) } | ((Rel(w1, r1), s1), (Rel(w2, r2), s2)) => - w1 === w2 ? - relativizeDepth((r1, List.rev(s1)), (r2, List.rev(s2))) : - raiseDriveMismatch(source, dest) + w1 === w2 + ? relativizeDepth((r1, List.rev(s1)), (r2, List.rev(s2))) + : raiseDriveMismatch(source, dest) }; (Rel(Any, depth), List.rev(segs)); }; @@ -349,7 +353,7 @@ let relativizeExn: type k. (~source: t(k), ~dest: t(k)) => t(relative) = let relativize: type k. (~source: t(k), ~dest: t(k)) => result(t(relative), exn) = (~source, ~dest) => - try (Ok(relativizeExn(~source, ~dest))) { + try(Ok(relativizeExn(~source, ~dest))) { | Invalid_argument(_) as e => Error(e) }; @@ -413,9 +417,9 @@ let rec join: type k1 k2. (t(k1), t(k2)) => t(k1) = switch (p1, p2) { | ((Rel(w, r1), []), (Rel(Any, r2), s2)) => (Rel(w, r1 + r2), s2) | ((Rel(w, r1), [s1hd, ...s1tl] as s1), (Rel(Any, r2), s2)) => - r2 > 0 ? - join((Rel(w, r1), s1tl), (Rel(Any, r2 - 1), s2)) : - (Rel(w, r1), List.append(s2, s1)) + r2 > 0 + ? join((Rel(w, r1), s1tl), (Rel(Any, r2 - 1), s2)) + : (Rel(w, r1), List.append(s2, s1)) | ((b1, s1), (Rel(Home, r2), s2)) => join((b1, [homeChar, ...List.append(s2, s1)]), (Rel(Any, r2), s2)) | ((b1, s1), (Abs(Some(ll)), s2)) => ( @@ -425,9 +429,9 @@ let rec join: type k1 k2. (t(k1), t(k2)) => t(k1) = | ((b1, s1), (Abs(None), s2)) => (b1, List.append(s2, s1)) | ((Abs(_) as d, []), (Rel(Any, r2), s2)) => (d, s2) | ((Abs(_) as d, [s1hd, ...s1tl] as s1), (Rel(Any, r2), s2)) => - r2 > 0 ? - join((d, s1tl), (Rel(Any, r2 - 1), s2)) : - (d, List.append(s2, s1)) + r2 > 0 + ? join((d, s1tl), (Rel(Any, r2 - 1), s2)) + : (d, List.append(s2, s1)) }; let dirName: type k1. t(k1) => t(k1) = diff --git a/library/Result.re b/library/Result.re index 57e73e6..fb20225 100644 --- a/library/Result.re +++ b/library/Result.re @@ -25,6 +25,18 @@ let bind = (fn, res) => | Error(_) as e => e }; +let bind_err = (fn, res) => + switch (res) { + | Ok(_) as o => o + | Error(x) => fn(x) + }; + +let bind_err_lwt = (fn, res) => + switch (res) { + | Ok(o) => Lwt.return_ok(o) + | Error(x) => fn(x) + }; + let fold = (error, ok, res) => switch (res) { | Ok(x) => ok(x) diff --git a/library/System.re b/library/System.re index 29c00a8..b65d360 100644 --- a/library/System.re +++ b/library/System.re @@ -70,7 +70,7 @@ module NodeArch = { switch (Sys.os_type) { | "Unix" => let%lwt result = unix_exec("uname", ~args=[|"-a"|]); - try (result |> findArches |> Lwt.return) { + try(result |> findArches |> Lwt.return) { | _ => Lwt.fail_with("Error getting unix information") }; | _ => Lwt.return(Other) diff --git a/library/VersionListing.re b/library/VersionListing.re new file mode 100644 index 0000000..f0233e5 --- /dev/null +++ b/library/VersionListing.re @@ -0,0 +1,34 @@ +type item = { + version: string, + date: string, + files: list(string), + lts: option(string), +}; + +let item_of_yojson = (json: Yojson.Safe.t) => { + open Yojson.Safe.Util; + let version = json |> member("version") |> to_string; + let files = json |> member("files") |> to_list |> List.map(to_string); + let date = json |> member("date") |> to_string; + let lts = + Base.Option.try_with(() => { + json |> member("lts") |> to_string |> String.lowercase_ascii + }); + Ok({version, date, files, lts}); +}; + +[@deriving of_yojson({strict: false})] +type t = list(item); + +let parseVersionListing = body => { + Base.Result.try_with(() => Yojson.Safe.from_string(body)) + |> Base.Result.map_error(~f=Printexc.to_string) + |> Base.Result.bind(~f=x => of_yojson(x)); +}; + +let fromHttp = () => { + let url = + Config.FNM_NODE_DIST_MIRROR.get() |> Printf.sprintf("%s/index.json"); + let%lwt {Http.body, _} = Http.makeRequest(url); + parseVersionListing(body) |> Lwt.return; +}; diff --git a/library/VersionListingLts.re b/library/VersionListingLts.re new file mode 100644 index 0000000..e98e090 --- /dev/null +++ b/library/VersionListingLts.re @@ -0,0 +1,29 @@ +type item = { + version: string, + lts: string, +}; + +let item_to_lts = (item: VersionListing.item) => { + item.lts |> Base.Option.map(~f=lts => {lts, version: item.version}); +}; + +type get_latest_lts_errors = + | Cant_parse_remote_version_listing(string) + | Cant_find_latest_lts; + +exception Problem_with_finding_latest_lts(get_latest_lts_errors); + +let getLatest = () => { + let%lwt versions = VersionListing.fromHttp(); + versions + |> Base.Result.map_error(~f=err => Cant_parse_remote_version_listing(err)) + |> Base.Result.bind(~f=parsed => { + parsed + |> Base.List.filter_map(~f=item_to_lts) + |> Base.List.max_elt(~compare=(a, b) => + Versions.compare(a.version, b.version) + ) + |> Base.Result.of_option(~error=Cant_find_latest_lts) + }) + |> Lwt.return; +}; diff --git a/library/Versions.re b/library/Versions.re index 25594d5..912304a 100644 --- a/library/Versions.re +++ b/library/Versions.re @@ -79,7 +79,7 @@ module Aliases = { let getAll = () => { let%lwt aliases = - try%lwt (Fs.readdir(Directories.aliases)) { + try%lwt(Fs.readdir(Directories.aliases)) { | _ => Lwt.return([]) }; aliases @@ -293,7 +293,7 @@ let getExactFileToDownload = (~version as versionName, ~os, ~arch) => { Printf.sprintf("%s%s/", Config.FNM_NODE_DIST_MIRROR.get(), versionName); let%lwt html = - try%lwt (Http.makeRequest(url) |> Lwt.map(Http.body)) { + try%lwt(Http.makeRequest(url) |> Lwt.map(Http.body)) { | Http.Not_found(_) => Lwt.fail(Version_not_found(versionName)) }; @@ -319,7 +319,7 @@ let getExactFileToDownload = (~version as versionName, ~os, ~arch) => { }; let getFileToDownload = (~version, ~os, ~arch) => - try%lwt (getExactFileToDownload(~version, ~os, ~arch)) { + try%lwt(getExactFileToDownload(~version, ~os, ~arch)) { | Version_not_found(_) as e => switch%lwt (getRemoteLatestVersionByPrefix(version)) { | None => Lwt.fail(e) @@ -337,7 +337,7 @@ let parse = version => { let formattedVersion = format(version); if (formattedVersion == Local.systemVersion.name) { - Lwt.return_some(System); + Lwt.return_ok(System); } else { let%lwt aliasExists = Aliases.toDirectory(version) |> Fs.exists and aliasExistsOnFormatted = @@ -352,19 +352,18 @@ let parse = version => { aliasExistsOnFormatted, versionByPrefixPath, ) { - | (true, _, _, _) => Some(Local(formattedVersion)) |> Lwt.return - | (_, true, _, _) => Some(Alias(version)) |> Lwt.return - | (_, _, true, _) => Some(Alias(formattedVersion)) |> Lwt.return - | (_, false, false, Some(version)) => - Some(Local(version)) |> Lwt.return - | (false, false, false, None) => Lwt.return_none + | (true, _, _, _) => Local(formattedVersion) |> Lwt.return_ok + | (_, true, _, _) => Alias(version) |> Lwt.return_ok + | (_, _, true, _) => Alias(formattedVersion) |> Lwt.return_ok + | (_, false, false, Some(version)) => Local(version) |> Lwt.return_ok + | (false, false, false, None) => Lwt.return_error(formattedVersion) }; }; }; let isInstalled = versionName => { let%lwt installedVersions = - try%lwt (getInstalledVersions()) { + try%lwt(getInstalledVersions()) { | _ => Lwt.return([]) }; installedVersions diff --git a/library/dune b/library/dune index 32338ff..af170e7 100644 --- a/library/dune +++ b/library/dune @@ -7,6 +7,6 @@ (name Fnm) ; Other libraries list this name in their package.json 'require' field to use this library. (public_name fnm.lib) - (libraries pastel.lib str base lwt ssl lwt_ssl lambdasoup cohttp cohttp-lwt cohttp-lwt-unix console.lib ) - (preprocess ( pps lwt_ppx ppx_let ppx_deriving.show ppx_deriving.eq ppx_deriving.make ppx_deriving.ord )) ; From package.json preprocess field + (libraries pastel.lib str base lwt ssl lwt_ssl lambdasoup cohttp cohttp-lwt cohttp-lwt-unix console.lib ppx_deriving_yojson.runtime ) + (preprocess ( pps lwt_ppx ppx_let ppx_deriving.show ppx_deriving.eq ppx_deriving.make ppx_deriving.ord ppx_deriving_yojson )) ; From package.json preprocess field ) \ No newline at end of file diff --git a/package.json b/package.json index 946ba86..138c136 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "ppx_deriving.show", "ppx_deriving.eq", "ppx_deriving.make", - "ppx_deriving.ord" + "ppx_deriving.ord", + "ppx_deriving_yojson" ], "require": [ "pastel.lib", @@ -44,7 +45,8 @@ "cohttp", "cohttp-lwt", "cohttp-lwt-unix", - "console.lib" + "console.lib", + "ppx_deriving_yojson.runtime" ], "name": "fnm.lib", "namespace": "Fnm"