diff --git a/pages/api/revalidate.js b/pages/api/revalidate.js new file mode 100644 index 000000000..a2618b5d4 --- /dev/null +++ b/pages/api/revalidate.js @@ -0,0 +1 @@ +export { handler as default } from "src/others/Revalidate.mjs"; diff --git a/pages/try.js b/pages/try.js index 93d83541a..fe297fd6c 100644 --- a/pages/try.js +++ b/pages/try.js @@ -1,12 +1,19 @@ import dynamic from "next/dynamic"; -const Try = dynamic(() => import("src/Try.mjs"), { +export { getStaticProps } from "src/Try.mjs"; +import Try from "src/Try.mjs"; + +const Playground = dynamic(() => import("src/Playground.mjs"), { ssr: false, - //loading: () =>
Loading...
+ loading: () => Loading... }); -function Comp() { - return ; +function Comp(props) { + return ( + + + + ); } export default Comp; diff --git a/src/Playground.res b/src/Playground.res index 227021602..ae5e937e9 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -915,30 +915,88 @@ module Settings = {
{React.string("ReScript Version")}
{ ReactEvent.Form.preventDefault(evt) - let id = (evt->ReactEvent.Form.target)["value"] - onCompilerSelect(id) + let id: string = (evt->ReactEvent.Form.target)["value"] + switch id->CompilerManagerHook.Semver.parse { + | Some(v) => onCompilerSelect(v) + | None => () + } }}> - {switch readyState.experimentalVersions { - | [] => React.null - | experimentalVersions => + { + let (experimentalVersions, stableVersions) = + readyState.versions->Js.Array2.reduce((acc, item) => { + let (lhs, rhs) = acc + if item.preRelease->Belt.Option.isSome { + Js.Array2.push(lhs, item) + } else { + Js.Array2.push(rhs, item) + }->ignore + acc + }, ([], [])) + <> - - {Belt.Array.map(experimentalVersions, version => - - )->React.array} - + {switch experimentalVersions { + | [] => React.null + | experimentalVersions => + let versionByOrder = experimentalVersions->Js.Array2.sortInPlaceWith((a, b) => { + let cmp = ({ + CompilerManagerHook.Semver.major: major, + minor, + patch, + preRelease, + }) => { + let preRelease = switch preRelease { + | Some(preRelease) => + switch preRelease { + | Dev(id) => 0 + id + | Alpha(id) => 10 + id + | Beta(id) => 20 + id + | Rc(id) => 30 + id + } + | None => 0 + } + let number = + [major, minor, patch] + ->Js.Array2.map(v => v->Belt.Int.toString) + ->Js.Array2.joinWith("") + ->Belt.Int.fromString + ->Belt.Option.getWithDefault(0) + + number + preRelease + } + cmp(b) - cmp(a) + }) + <> + + {versionByOrder + ->Belt.Array.map(version => { + let version = CompilerManagerHook.Semver.toString(version) + + }) + ->React.array} + + + }} + {switch stableVersions { + | [] => React.null + | stableVersions => + Belt.Array.map(stableVersions, version => { + let version = CompilerManagerHook.Semver.toString(version) + + })->React.array + }} - }} - {Belt.Array.map(readyState.versions, version => - - )->React.array} + }
@@ -1350,29 +1408,29 @@ module App = { let initialReContent = j`Js.log("Hello Reason 3.6!");` -/** -Takes a `versionStr` starting with a "v" and ending in major.minor.patch (e.g. -"v10.1.0") returns major, minor, patch as an integer tuple if it's actually in -a x.y.z format, otherwise will return `None`. -*/ -let parseVersion = (versionStr: string): option<(int, int, int)> => { - switch versionStr->Js.String2.replace("v", "")->Js.String2.split(".") { - | [major, minor, patch] => - switch (major->Belt.Int.fromString, minor->Belt.Int.fromString, patch->Belt.Int.fromString) { - | (Some(major), Some(minor), Some(patch)) => Some((major, minor, patch)) - | _ => None - } - | _ => None - } -} - -@react.component -let make = () => { +let default = (~props: Try.props) => { let router = Next.Router.useRouter() + let versions = + props.versions + ->Belt.Array.keepMap(v => v->CompilerManagerHook.Semver.parse) + ->Js.Array2.sortInPlaceWith((a, b) => { + let cmp = ({CompilerManagerHook.Semver.major: major, minor, patch, _}) => { + [major, minor, patch] + ->Js.Array2.map(v => v->Belt.Int.toString) + ->Js.Array2.joinWith("") + ->Belt.Int.fromString + ->Belt.Option.getWithDefault(0) + } + cmp(b) - cmp(a) + }) + + let lastStableVersion = + versions->Js.Array2.find(version => version.preRelease->Belt.Option.isNone) + let initialVersion = switch Js.Dict.get(router.query, "version") { - | Some(version) => Some(version) - | None => CompilerManagerHook.CdnMeta.versions->Belt.Array.get(0) + | Some(version) => version->CompilerManagerHook.Semver.parse + | None => lastStableVersion } let initialLang = switch Js.Dict.get(router.query, "ext") { @@ -1386,15 +1444,11 @@ let make = () => { | (None, Res) | (None, _) => switch initialVersion { - | Some(initialVersion) => - switch parseVersion(initialVersion) { - | Some((major, minor, _)) => - if major >= 10 && minor >= 1 { - InitialContent.since_10_1 - } else { - InitialContent.original - } - | None => InitialContent.original + | Some({CompilerManagerHook.Semver.major: major, minor, _}) => + if major >= 10 && minor >= 1 { + InitialContent.since_10_1 + } else { + InitialContent.original } | None => InitialContent.original } @@ -1408,6 +1462,7 @@ let make = () => { ~initialVersion?, ~initialLang, ~onAction, + ~versions, (), ) diff --git a/src/Playground.resi b/src/Playground.resi index 1ca44ce26..72031edb8 100644 --- a/src/Playground.resi +++ b/src/Playground.resi @@ -1,2 +1 @@ -@react.component -let make: unit => React.element +let default: (~props: Try.props) => React.element diff --git a/src/Try.res b/src/Try.res index 8152a69d2..704acd270 100644 --- a/src/Try.res +++ b/src/Try.res @@ -1,7 +1,8 @@ -@react.component -let default = () => { +let default = (props: {"children": React.element}) => { let overlayState = React.useState(() => false) + let playground = props["children"] + <> @@ -10,8 +11,39 @@ let default = () => {
- + playground
} + +type props = {versions: array} + +let getStaticProps: Next.GetStaticProps.t = async _ => { + let versions = { + let response = await Webapi.Fetch.fetch("https://cdn.rescript-lang.org/") + let text = await Webapi.Fetch.Response.text(response) + text + ->Js.String2.split("\n") + ->Belt.Array.keepMap(line => { + switch line->Js.String2.startsWith(" + // Adapted from https://semver.org/ + let semverRe = %re( + "/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/" + ) + switch Js.Re.exec_(semverRe, line) { + | Some(result) => + switch Js.Re.captures(result)->Belt.Array.get(0) { + | Some(str) => Js.Nullable.toOption(str) + | None => None + } + | None => None + } + | false => None + } + }) + } + + {"props": {versions: versions}} +} diff --git a/src/Try.resi b/src/Try.resi new file mode 100644 index 000000000..2611af9cf --- /dev/null +++ b/src/Try.resi @@ -0,0 +1,3 @@ +let default: {"children": React.element} => React.element +type props = {versions: array} +let getStaticProps: Next.GetStaticProps.t diff --git a/src/bindings/Webapi.res b/src/bindings/Webapi.res index 92fa03094..5e92050dc 100644 --- a/src/bindings/Webapi.res +++ b/src/bindings/Webapi.res @@ -36,3 +36,12 @@ module Window = { @scope("window") @val external innerWidth: int = "innerWidth" @scope("window") @val external innerHeight: int = "innerHeight" } + +module Fetch = { + module Response = { + type t + @send external text: t => promise = "text" + } + + @val external fetch: string => promise = "fetch" +} diff --git a/src/common/CompilerManagerHook.res b/src/common/CompilerManagerHook.res index 5044e8130..4f753c5b3 100644 --- a/src/common/CompilerManagerHook.res +++ b/src/common/CompilerManagerHook.res @@ -34,28 +34,85 @@ module LoadScript = { } } +module Semver = { + type preRelease = Alpha(int) | Beta(int) | Dev(int) | Rc(int) + + type t = {major: int, minor: int, patch: int, preRelease: option} + + /** + Takes a `version` string starting with a "v" and ending in major.minor.patch or + major.minor.patch-prerelease.identifier (e.g. "v10.1.0" or "v10.1.0-alpha.2") + */ + let parse = (versionStr: string) => { + let parsePreRelease = str => { + switch str->Js.String2.split("-") { + | [_, identifier] => + switch identifier->Js.String2.split(".") { + | [name, number] => + switch Belt.Int.fromString(number) { + | None => None + | Some(buildIdentifier) => + switch name { + | "dev" => buildIdentifier->Dev->Some + | "beta" => buildIdentifier->Beta->Some + | "alpha" => buildIdentifier->Alpha->Some + | "rc" => buildIdentifier->Rc->Some + | _ => None + } + } + | _ => None + } + | _ => None + } + } + + // Some version contain a suffix. Example: v11.0.0-alpha.5, v11.0.0-beta.1 + let isPrerelease = versionStr->Js.String2.search(%re("/-/")) != -1 + + // Get the first part i.e vX.Y.Z + let versionNumber = + versionStr->Js.String2.split("-")->Belt.Array.get(0)->Belt.Option.getWithDefault(versionStr) + + switch versionNumber->Js.String2.replace("v", "")->Js.String2.split(".") { + | [major, minor, patch] => + switch (major->Belt.Int.fromString, minor->Belt.Int.fromString, patch->Belt.Int.fromString) { + | (Some(major), Some(minor), Some(patch)) => + let preReleaseIdentifier = if isPrerelease { + parsePreRelease(versionStr) + } else { + None + } + Some({major, minor, patch, preRelease: preReleaseIdentifier}) + | _ => None + } + | _ => None + } + } + + let toString = ({major, minor, patch, preRelease}) => { + let mainVersion = `v${major->Belt.Int.toString}.${minor->Belt.Int.toString}.${patch->Belt.Int.toString}` + + switch preRelease { + | None => mainVersion + | Some(identifier) => + let identifier = switch identifier { + | Dev(number) => `dev.${number->Belt.Int.toString}` + | Alpha(number) => `alpha.${number->Belt.Int.toString}` + | Beta(number) => `beta.${number->Belt.Int.toString}` + | Rc(number) => `rc.${number->Belt.Int.toString}` + } + + `${mainVersion}-${identifier}` + } + } +} + module CdnMeta = { - // Make sure versions exist on https://cdn.rescript-lang.org - // [0] = latest - let versions = [ - "v10.1.2", - "v10.0.1", - "v10.0.0", - "v9.1.2", - "v9.0.2", - "v9.0.1", - "v9.0.0", - "v8.4.2", - "v8.3.0-dev.2", - ] - - let experimentalVersions = ["v11.0.0-rc.3", "v11.0.0-beta.4", "v11.0.0-beta.1", "v11.0.0-alpha.5"] - - let getCompilerUrl = (version: string): string => - j`https://cdn.rescript-lang.org/$version/compiler.js` - - let getLibraryCmijUrl = (version: string, libraryName: string): string => - j`https://cdn.rescript-lang.org/$version/$libraryName/cmij.js` + let getCompilerUrl = (version): string => + `https://cdn.rescript-lang.org/${Semver.toString(version)}/compiler.js` + + let getLibraryCmijUrl = (version, libraryName: string): string => + `https://cdn.rescript-lang.org/${Semver.toString(version)}/${libraryName}/cmij.js` } module FinalResult = { @@ -69,30 +126,23 @@ module FinalResult = { // This will a given list of libraries to a specific target version of the compiler. // E.g. starting from v9, @rescript/react instead of reason-react is used. // If the version can't be parsed, an empty array will be returned. -let getLibrariesForVersion = (~version: string): array => { - switch Js.String2.split(version, ".")->Belt.List.fromArray { - | list{major, ..._rest} => - let version = - Js.String2.replace(major, "v", "")->Belt.Int.fromString->Belt.Option.getWithDefault(0) - - let libraries = if version >= 9 { - ["@rescript/react"] - } else if version < 9 { - ["reason-react"] - } else { - [] - } - - // Since version 11, we ship the compiler-builtins as a separate file, and - // we also added @rescript/core as a pre-vendored package - if version >= 11 { - libraries->Js.Array2.push("@rescript/core")->ignore - libraries->Js.Array2.push("compiler-builtins")->ignore - } +let getLibrariesForVersion = (~version: Semver.t): array => { + let libraries = if version.major >= 9 { + ["@rescript/react"] + } else if version.major < 9 { + ["reason-react"] + } else { + [] + } - libraries - | _ => [] + // Since version 11, we ship the compiler-builtins as a separate file, and + // we also added @rescript/core as a pre-vendored package + if version.major >= 11 { + libraries->Js.Array2.push("@rescript/core")->ignore + libraries->Js.Array2.push("compiler-builtins")->ignore } + + libraries } /* @@ -108,7 +158,7 @@ let getLibrariesForVersion = (~version: string): array => { We coupled the compiler / library loading to prevent ppl to try loading compiler / cmij files separately and cause all kinds of race conditions. */ -let attachCompilerAndLibraries = async (~version: string, ~libraries: array, ()): result< +let attachCompilerAndLibraries = async (~version, ~libraries: array, ()): result< unit, array, > => { @@ -149,7 +199,7 @@ type error = | CompilerLoadingError(string) type selected = { - id: string, // The id used for loading the compiler bundle (ideally should be the same as compilerVersion) + id: Semver.t, // The id used for loading the compiler bundle (ideally should be the same as compilerVersion) apiVersion: Version.t, // The playground API version in use compilerVersion: string, ocamlVersion: string, @@ -159,8 +209,7 @@ type selected = { } type ready = { - versions: array, - experimentalVersions: array, + versions: array, selected: selected, targetLang: Lang.t, errors: array, // For major errors like bundle loading @@ -170,12 +219,12 @@ type ready = { type state = | Init | SetupFailed(string) - | SwitchingCompiler(ready, string) // (ready, targetId, libraries) + | SwitchingCompiler(ready, Semver.t) // (ready, targetId, libraries) | Ready(ready) | Compiling(ready, (Lang.t, string)) type action = - | SwitchToCompiler(string) // id + | SwitchToCompiler(Semver.t) // id | SwitchLanguage({lang: Lang.t, code: string}) | Format(string) | CompileCode(Lang.t, string) @@ -193,9 +242,10 @@ type action = // component to give feedback to the user that an action happened (useful in // cases where the output didn't visually change) let useCompilerManager = ( - ~initialVersion: option=?, + ~initialVersion: option=?, ~initialLang: Lang.t=Res, ~onAction: option unit>=?, + ~versions: array, (), ) => { let (state, setState) = React.useState(_ => Init) @@ -334,74 +384,58 @@ let useCompilerManager = ( let updateState = async () => { switch state { | Init => - switch CdnMeta.versions { + switch versions { | [] => dispatchError(SetupError("No compiler versions found")) | versions => - let latest = versions[0] - - // If the provided initialVersion is not available, fall back - // to "latest" - let initVersion = switch initialVersion { + switch initialVersion { | Some(version) => - let allVersions = Belt.Array.concat(CdnMeta.versions, CdnMeta.experimentalVersions) - if ( - allVersions->Js.Array2.some(v => { - version == v - }) - ) { - version - } else { - latest - } - | None => latest - } + // Latest version is already running on @rescript/react + let libraries = getLibrariesForVersion(~version) + + switch await attachCompilerAndLibraries(~version, ~libraries, ()) { + | Ok() => + let instance = Compiler.make() + let apiVersion = apiVersion->Version.fromString + + // Note: The compiler bundle currently defaults to + // commonjs when initiating the compiler, but our playground + // should default to ES6. So we override the config + // and use the `setConfig` function to sync up the + // internal compiler state with our playground state. + let config = { + ...instance->Compiler.getConfig, + module_system: "es6", + } + instance->Compiler.setConfig(config) + + let selected = { + id: version, + apiVersion, + compilerVersion: instance->Compiler.version, + ocamlVersion: instance->Compiler.ocamlVersion, + config, + libraries, + instance, + } - // Latest version is already running on @rescript/react - let libraries = getLibrariesForVersion(~version=initVersion) - - switch await attachCompilerAndLibraries(~version=initVersion, ~libraries, ()) { - | Ok() => - let instance = Compiler.make() - let apiVersion = apiVersion->Version.fromString - - // Note: The compiler bundle currently defaults to - // commonjs when initiating the compiler, but our playground - // should default to ES6. So we override the config - // and use the `setConfig` function to sync up the - // internal compiler state with our playground state. - let config = { - ...instance->Compiler.getConfig, - module_system: "es6", + let targetLang = + Version.availableLanguages(apiVersion) + ->Js.Array2.find(l => l === initialLang) + ->Belt.Option.getWithDefault(Version.defaultTargetLang) + + setState(_ => Ready({ + selected, + targetLang, + versions, + errors: [], + result: FinalResult.Nothing, + })) + | Error(errs) => + let msg = Js.Array2.joinWith(errs, "; ") + + dispatchError(CompilerLoadingError(msg)) } - instance->Compiler.setConfig(config) - - let selected = { - id: initVersion, - apiVersion, - compilerVersion: instance->Compiler.version, - ocamlVersion: instance->Compiler.ocamlVersion, - config, - libraries, - instance, - } - - let targetLang = - Version.availableLanguages(apiVersion) - ->Js.Array2.find(l => l === initialLang) - ->Belt.Option.getWithDefault(Version.defaultTargetLang) - - setState(_ => Ready({ - selected, - targetLang, - versions, - experimentalVersions: CdnMeta.experimentalVersions, - errors: [], - result: FinalResult.Nothing, - })) - | Error(errs) => - let msg = Js.Array2.joinWith(errs, "; ") - - dispatchError(CompilerLoadingError(msg)) + | None => dispatchError(CompilerLoadingError("Cant not found the initial version")) } } | SwitchingCompiler(ready, version) => @@ -435,7 +469,6 @@ let useCompilerManager = ( selected, targetLang: Version.defaultTargetLang, versions: ready.versions, - experimentalVersions: ready.experimentalVersions, errors: [], result: FinalResult.Nothing, })) diff --git a/src/common/CompilerManagerHook.resi b/src/common/CompilerManagerHook.resi index ca82af5f1..54b534b4c 100644 --- a/src/common/CompilerManagerHook.resi +++ b/src/common/CompilerManagerHook.resi @@ -8,13 +8,24 @@ module FinalResult: { | Nothing } -module CdnMeta: { - /** All available versions on the CDN */ - let versions: array +module Semver: { + type preRelease = + | Alpha(int) + | Beta(int) + | Dev(int) + | Rc(int) + type t = { + major: int, + minor: int, + patch: int, + preRelease: option, + } + let parse: string => option + let toString: t => string } type selected = { - id: string, // The id used for loading the compiler bundle (ideally should be the same as compilerVersion) + id: Semver.t, // The id used for loading the compiler bundle (ideally should be the same as compilerVersion) apiVersion: Version.t, // The playground API version in use compilerVersion: string, ocamlVersion: string, @@ -24,8 +35,7 @@ type selected = { } type ready = { - versions: array, - experimentalVersions: array, + versions: array, selected: selected, targetLang: Lang.t, errors: array, // For major errors like bundle loading @@ -35,20 +45,21 @@ type ready = { type state = | Init | SetupFailed(string) - | SwitchingCompiler(ready, string) // (ready, targetId, libraries) + | SwitchingCompiler(ready, Semver.t) // (ready, targetId, libraries) | Ready(ready) | Compiling(ready, (Lang.t, string)) type action = - | SwitchToCompiler(string) // id + | SwitchToCompiler(Semver.t) // id | SwitchLanguage({lang: Lang.t, code: string}) | Format(string) | CompileCode(Lang.t, string) | UpdateConfig(Config.t) let useCompilerManager: ( - ~initialVersion: string=?, + ~initialVersion: Semver.t=?, ~initialLang: Lang.t=?, ~onAction: action => unit=?, + ~versions: array, unit, ) => (state, action => unit) diff --git a/src/others/Revalidate.res b/src/others/Revalidate.res new file mode 100644 index 000000000..f4d6dff4f --- /dev/null +++ b/src/others/Revalidate.res @@ -0,0 +1,37 @@ +module Req = { + type req = {query: Js.Dict.t} +} + +module Res = { + type res + + @send external revalidate: (res, string) => promise = "revalidate" + @send external json: (res, {..}) => res = "json" + + module Status = { + type t + @send external make: (res, int) => t = "status" + @send external send: (t, string) => res = "send" + @send external json: (t, {..}) => res = "json" + } +} + +@val external process: 'a = "process" + +let handler = async (req: Req.req, res: Res.res) => { + switch req.query->Js.Dict.get("secret") { + | Some(secret) => + if secret !== process["env"]["NEXT_REVALIDATE_SECRET_TOKEN"] { + res->Res.Status.make(401)->Res.Status.json({"message": "Invalid secret"}) + } else { + try { + let () = await res->Res.revalidate("/try") + res->Res.json({"revalidated": true}) + } catch { + | Js.Exn.Error(_) => res->Res.Status.make(500)->Res.Status.send("Error revalidating") + } + } + | None => + res->Res.Status.make(500)->Res.Status.send("Error revalidating, param `secret` not found") + } +} diff --git a/src/others/Revalidate.resi b/src/others/Revalidate.resi new file mode 100644 index 000000000..694cd1958 --- /dev/null +++ b/src/others/Revalidate.resi @@ -0,0 +1,7 @@ +module Req: { + type req +} +module Res: { + type res +} +let handler: (Req.req, Res.res) => promise