diff --git a/src/ErrorUtils.res b/src/ErrorUtils.res new file mode 100644 index 0000000..716375e --- /dev/null +++ b/src/ErrorUtils.res @@ -0,0 +1,5 @@ +let getErrorMessage = exn => + switch exn->Exn.message { + | Some(message) => message + | None => exn->String.make + } diff --git a/src/NpmRegistry.res b/src/NpmRegistry.res new file mode 100644 index 0000000..b653905 --- /dev/null +++ b/src/NpmRegistry.res @@ -0,0 +1,61 @@ +type response = { + ok: bool, + status: int, + json: unit => promise, +} + +@val external fetch: string => promise = "fetch" + +@scope(("process", "env")) +external npm_config_registry: option = "NPM_CONFIG_REGISTRY" + +@inline +let defaultRegistryUrl = "https://registry.npmjs.org" + +let getNpmRegistry = () => + npm_config_registry + ->Option.flatMap(registry => registry->Node.Url.make) + ->Option.mapOr(defaultRegistryUrl, url => url->Node.Url.href) + +type fetchError = + | FetchError({message: string}) + | HttpError({status: int}) + | ParseError + +let getFetchErrorMessage = fetchError => { + let message = switch fetchError { + | FetchError({message}) => `Fetch error. Message: ${message}` + | HttpError({status}) => `Http error. Status: ${status->Int.toString}` + | ParseError => "Parse error." + } + + `Fetching versions from registry failed: ${message}` +} + +let getPackageVersions = async (packageName, range) => { + let registryUrl = getNpmRegistry() + + switch await fetch(`${registryUrl}/${packageName}`) { + | response if response.ok => + switch await response.json() { + | Object(dict) => + switch dict->Dict.get("versions") { + | Some(Object(dict)) => + let versions = + dict + ->Dict.keysToArray + ->Array.filterMap(version => + version->CompareVersions.satisfies(range) ? Some(version) : None + ) + versions->Array.reverse + versions->Ok + + | _ => Error(ParseError) + } + | _ => Error(ParseError) + } + + | responseNotOk => Error(HttpError({status: responseNotOk.status})) + | exception Exn.Error(exn) => Error(FetchError({message: exn->ErrorUtils.getErrorMessage})) + } +} diff --git a/src/NpmRegistry.resi b/src/NpmRegistry.resi new file mode 100644 index 0000000..2be6e22 --- /dev/null +++ b/src/NpmRegistry.resi @@ -0,0 +1,8 @@ +type fetchError = + | FetchError({message: string}) + | HttpError({status: int}) + | ParseError + +let getFetchErrorMessage: fetchError => string + +let getPackageVersions: (string, string) => promise, fetchError>> diff --git a/src/RescriptVersions.res b/src/RescriptVersions.res index ce7a0a4..6c9dffc 100644 --- a/src/RescriptVersions.res +++ b/src/RescriptVersions.res @@ -5,24 +5,6 @@ let rescriptCoreVersionRange = ">=1.0.0" type versions = {rescriptVersion: string, rescriptCoreVersion: string} -let getPackageVersions = async (packageName, range) => { - let {stdout} = await Node.Promisified.ChildProcess.exec(`npm view ${packageName} versions --json`) - - let versions = switch JSON.parseExn(stdout) { - | Array(versions) => - versions->Array.filterMap(json => - switch json { - | String(version) if version->CompareVersions.satisfies(range) => Some(version) - | _ => None - } - ) - | _ => [] - } - - versions->Array.reverse - versions -} - let getCompatibleRescriptCoreVersions = (~rescriptVersion, ~rescriptCoreVersions) => if CompareVersions.compareVersions(rescriptVersion, "11.1.0")->Ordering.isLess { rescriptCoreVersions->Array.filter(coreVersion => @@ -32,25 +14,36 @@ let getCompatibleRescriptCoreVersions = (~rescriptVersion, ~rescriptCoreVersions rescriptCoreVersions } +let spinnerMessage = "Loading available versions..." + let promptVersions = async () => { let s = P.spinner() - s->P.Spinner.start("Loading available versions...") + s->P.Spinner.start(spinnerMessage) - let (rescriptVersions, rescriptCoreVersions) = await Promise.all2(( - getPackageVersions("rescript", rescriptVersionRange), - getPackageVersions("@rescript/core", rescriptCoreVersionRange), + let (rescriptVersionsResult, rescriptCoreVersionsResult) = await Promise.all2(( + NpmRegistry.getPackageVersions("rescript", rescriptVersionRange), + NpmRegistry.getPackageVersions("@rescript/core", rescriptCoreVersionRange), )) - s->P.Spinner.stop("Versions loaded.") + switch (rescriptVersionsResult, rescriptCoreVersionsResult) { + | (Ok(_), Ok(_)) => s->P.Spinner.stop("Versions loaded.") + | _ => s->P.Spinner.stop(spinnerMessage) + } - let rescriptVersion = switch rescriptVersions { - | [version] => version - | _ => + let rescriptVersion = switch rescriptVersionsResult { + | Ok([version]) => version + | Ok(rescriptVersions) => await P.select({ message: "ReScript version?", options: rescriptVersions->Array.map(v => {P.value: v}), })->P.resultOrRaise + | Error(error) => error->NpmRegistry.getFetchErrorMessage->Error.make->Error.raise + } + + let rescriptCoreVersions = switch rescriptCoreVersionsResult { + | Ok(versions) => versions + | Error(error) => error->NpmRegistry.getFetchErrorMessage->Error.make->Error.raise } let rescriptCoreVersions = getCompatibleRescriptCoreVersions( diff --git a/src/bindings/Node.res b/src/bindings/Node.res index a0bb3cd..14d14e9 100644 --- a/src/bindings/Node.res +++ b/src/bindings/Node.res @@ -64,6 +64,14 @@ module Url = { type t @module("node:url") external fileURLToPath: t => string = "fileURLToPath" + + @new external makeUnsafe: string => t = "URL" + @get external href: t => string = "href" + + let make = string => + try Some(makeUnsafe(string)) catch { + | Exn.Error(_exn) => None + } } module Os = {