From b169327d3f7f02b04b48ae7e4b99657e3b5362ed Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Wed, 8 May 2024 14:37:47 +0200 Subject: [PATCH 1/4] Fetch versions directly from npm registry url --- src/NpmRegistry.res | 25 +++++++++++++++++++++++++ src/RescriptVersions.res | 22 ++-------------------- 2 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 src/NpmRegistry.res diff --git a/src/NpmRegistry.res b/src/NpmRegistry.res new file mode 100644 index 0000000..06332c8 --- /dev/null +++ b/src/NpmRegistry.res @@ -0,0 +1,25 @@ +type response + +@send external toJson: response => promise = "json" +@val external fetch: string => promise = "fetch" + +let getPackageVersions = async (packageName, range) => { + let result = await fetch(`https://registry.npmjs.org/${packageName}`) + + let versions = switch await result->toJson { + | Object(dict) => + switch dict->Dict.get("versions") { + | Some(Object(dict)) => dict->Dict.keysToArray + | _ => [] + } + | _ => [] + } + + let versions = + versions->Array.filterMap(version => + version->CompareVersions.satisfies(range) ? Some(version) : None + ) + + versions->Array.reverse + versions +} diff --git a/src/RescriptVersions.res b/src/RescriptVersions.res index ce7a0a4..1d8fdab 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 => @@ -38,8 +20,8 @@ let promptVersions = async () => { s->P.Spinner.start("Loading available versions...") let (rescriptVersions, rescriptCoreVersions) = await Promise.all2(( - getPackageVersions("rescript", rescriptVersionRange), - getPackageVersions("@rescript/core", rescriptCoreVersionRange), + NpmRegistry.getPackageVersions("rescript", rescriptVersionRange), + NpmRegistry.getPackageVersions("@rescript/core", rescriptCoreVersionRange), )) s->P.Spinner.stop("Versions loaded.") From 500520a1d8f01c469f0068e09235a402e0614fae Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Mon, 13 May 2024 12:28:16 +0200 Subject: [PATCH 2/4] Support alternative npm urls + error handling --- src/NpmRegistry.res | 48 +++++++++++++++++++++++++++------------- src/RescriptVersions.res | 6 +++-- src/bindings/Node.res | 8 +++++++ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/NpmRegistry.res b/src/NpmRegistry.res index 06332c8..853f9b1 100644 --- a/src/NpmRegistry.res +++ b/src/NpmRegistry.res @@ -1,25 +1,43 @@ -type response +type response = { + ok: bool, + json: unit => promise, +} -@send external toJson: response => promise = "json" @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) + let getPackageVersions = async (packageName, range) => { - let result = await fetch(`https://registry.npmjs.org/${packageName}`) + let registry = getNpmRegistry() - let versions = switch await result->toJson { - | Object(dict) => - switch dict->Dict.get("versions") { - | Some(Object(dict)) => dict->Dict.keysToArray + switch await fetch(`${registry}/${packageName}`) { + | response if response.ok => + let versions = switch await response.json() { + | Object(dict) => + switch dict->Dict.get("versions") { + | Some(Object(dict)) => + dict + ->Dict.keysToArray + ->Array.filterMap(version => + version->CompareVersions.satisfies(range) ? Some(version) : None + ) + | _ => [] + } | _ => [] } - | _ => [] - } - let versions = - versions->Array.filterMap(version => - version->CompareVersions.satisfies(range) ? Some(version) : None - ) + versions->Array.reverse + versions - versions->Array.reverse - versions + | _responseNotOk => [] + } } diff --git a/src/RescriptVersions.res b/src/RescriptVersions.res index 1d8fdab..e824cf6 100644 --- a/src/RescriptVersions.res +++ b/src/RescriptVersions.res @@ -19,10 +19,12 @@ let promptVersions = async () => { s->P.Spinner.start("Loading available versions...") - let (rescriptVersions, rescriptCoreVersions) = await Promise.all2(( + let (rescriptVersions, rescriptCoreVersions) = try await Promise.all2(( NpmRegistry.getPackageVersions("rescript", rescriptVersionRange), NpmRegistry.getPackageVersions("@rescript/core", rescriptCoreVersionRange), - )) + )) catch { + | _exn => Error.make("Fetching versions from registry failed.")->Error.raise + } s->P.Spinner.stop("Versions loaded.") diff --git a/src/bindings/Node.res b/src/bindings/Node.res index a0bb3cd..357617f 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 => None + } } module Os = { From b2bc12ae51e27609f7506d97b80aea1ffc505e5d Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Mon, 13 May 2024 13:51:08 +0200 Subject: [PATCH 3/4] Add interface file --- src/NpmRegistry.resi | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/NpmRegistry.resi diff --git a/src/NpmRegistry.resi b/src/NpmRegistry.resi new file mode 100644 index 0000000..03d604f --- /dev/null +++ b/src/NpmRegistry.resi @@ -0,0 +1 @@ +let getPackageVersions: (string, string) => promise> From 213692259d68b98987f7f1cf15cebe15c3ea42c0 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Tue, 14 May 2024 10:29:36 +0200 Subject: [PATCH 4/4] Improve error handling some more --- src/ErrorUtils.res | 5 +++++ src/NpmRegistry.res | 46 ++++++++++++++++++++++++++++------------ src/NpmRegistry.resi | 9 +++++++- src/RescriptVersions.res | 27 +++++++++++++++-------- src/bindings/Node.res | 2 +- 5 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 src/ErrorUtils.res 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 index 853f9b1..b653905 100644 --- a/src/NpmRegistry.res +++ b/src/NpmRegistry.res @@ -1,5 +1,6 @@ type response = { ok: bool, + status: int, json: unit => promise, } @@ -16,28 +17,45 @@ let getNpmRegistry = () => ->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 registry = getNpmRegistry() + let registryUrl = getNpmRegistry() - switch await fetch(`${registry}/${packageName}`) { + switch await fetch(`${registryUrl}/${packageName}`) { | response if response.ok => - let versions = switch await response.json() { + switch await response.json() { | Object(dict) => switch dict->Dict.get("versions") { | Some(Object(dict)) => - dict - ->Dict.keysToArray - ->Array.filterMap(version => - version->CompareVersions.satisfies(range) ? Some(version) : None - ) - | _ => [] + let versions = + dict + ->Dict.keysToArray + ->Array.filterMap(version => + version->CompareVersions.satisfies(range) ? Some(version) : None + ) + versions->Array.reverse + versions->Ok + + | _ => Error(ParseError) } - | _ => [] + | _ => Error(ParseError) } - versions->Array.reverse - versions - - | _responseNotOk => [] + | 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 index 03d604f..2be6e22 100644 --- a/src/NpmRegistry.resi +++ b/src/NpmRegistry.resi @@ -1 +1,8 @@ -let getPackageVersions: (string, string) => promise> +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 e824cf6..6c9dffc 100644 --- a/src/RescriptVersions.res +++ b/src/RescriptVersions.res @@ -14,27 +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) = try await Promise.all2(( + let (rescriptVersionsResult, rescriptCoreVersionsResult) = await Promise.all2(( NpmRegistry.getPackageVersions("rescript", rescriptVersionRange), NpmRegistry.getPackageVersions("@rescript/core", rescriptCoreVersionRange), - )) catch { - | _exn => Error.make("Fetching versions from registry failed.")->Error.raise - } + )) - 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 357617f..14d14e9 100644 --- a/src/bindings/Node.res +++ b/src/bindings/Node.res @@ -70,7 +70,7 @@ module Url = { let make = string => try Some(makeUnsafe(string)) catch { - | _exn => None + | Exn.Error(_exn) => None } }