diff --git a/next.config.js b/next.config.js index 4e2e77fa0..936bce648 100644 --- a/next.config.js +++ b/next.config.js @@ -1,8 +1,9 @@ const { ProvidePlugin } = require('webpack'); -const { ESBuildMinifyPlugin } = require('esbuild-loader'); const bsconfig = require("./bsconfig.json"); -const path = require("path"); +const SSRPlugin = require("next/dist/build/webpack/plugins/nextjs-ssr-import") + .default; +const { dirname, relative, resolve, join } = require("path"); const remarkSlug = require("remark-slug"); const fs = require("fs"); @@ -17,17 +18,42 @@ const withMdx = require("./plugins/next-mdx")({ }); -// esbuild-loader specific features -// See: https://github.com/privatenumber/esbuild-loader-examples/blob/master/examples/next/next.config.js -function useEsbuildMinify(config, options) { - const terserIndex = config.optimization.minimizer.findIndex(minimizer => (minimizer.constructor.name === 'TerserPlugin')); - if (terserIndex > -1) { - config.optimization.minimizer.splice( - terserIndex, - 1, - new ESBuildMinifyPlugin(options), - ); - } +// Unfortunately there isn't an easy way to override the replacement function body, so we +// have to just replace the whole plugin `apply` body. +function patchSsrPlugin(plugin) { + plugin.apply = function apply(compiler) { + compiler.hooks.compilation.tap("NextJsSSRImport", compilation => { + compilation.mainTemplate.hooks.requireEnsure.tap( + "NextJsSSRImport", + (code, chunk) => { + // This is the block that fixes https://github.com/vercel/next.js/issues/22581 + if (!chunk.name) { + return; + } + + // Update to load chunks from our custom chunks directory + const outputPath = resolve("/"); + const pagePath = join("/", dirname(chunk.name)); + const relativePathToBaseDir = relative(pagePath, outputPath); + // Make sure even in windows, the path looks like in unix + // Node.js require system will convert it accordingly + const relativePathToBaseDirNormalized = relativePathToBaseDir.replace( + /\\/g, + "/" + ); + return code + .replace( + 'require("./"', + `require("${relativePathToBaseDirNormalized}/"` + ) + .replace( + "readFile(join(__dirname", + `readFile(join(__dirname, "${relativePathToBaseDirNormalized}"` + ); + } + ); + }); + }; } const isWebpack5 = true; @@ -48,14 +74,12 @@ const config = { path: false, }; } - useEsbuildMinify(config); + // We need this additional rule to make sure that mjs files are // correctly detected within our src/ folder config.module.rules.push({ test: /\.m?js$/, - // v-- currently using an experimental setting with esbuild-loader - //use: options.defaultLoaders.babel, - use: [{loader: 'esbuild-loader', options: { loader: 'jsx'}}], + use: options.defaultLoaders.babel, exclude: /node_modules/, type: "javascript/auto", resolve: { @@ -64,6 +88,14 @@ const config = { }); config.plugins.push(new ProvidePlugin({ React: "react" })); } + + const ssrPlugin = config.plugins.find( + plugin => plugin instanceof SSRPlugin + ); + + if (ssrPlugin) { + patchSsrPlugin(ssrPlugin); + } return config; }, async redirects() { diff --git a/src/Playground.mjs b/src/Playground.mjs index d551b644e..62a748c43 100644 --- a/src/Playground.mjs +++ b/src/Playground.mjs @@ -1,5 +1,6 @@ // Generated by ReScript, PLEASE EDIT WITH CARE +import * as Eval from "./common/Eval.mjs"; import * as Icon from "./components/Icon.mjs"; import * as Meta from "./components/Meta.mjs"; import * as Next from "./bindings/Next.mjs"; @@ -1422,6 +1423,15 @@ function Playground$Settings(Props) { })))); } +function Playground$Logs(Props) { + var logs = Props.logs; + return React.createElement("ul", undefined, logs.map(function (log, i) { + return React.createElement("li", { + key: i.toString() + }, JSON.stringify(log)); + })); +} + function Playground$ControlPanel$Button(Props) { var children = Props.children; var onClick = Props.onClick; @@ -1499,6 +1509,7 @@ function Playground$ControlPanel(Props) { var state = Props.state; var dispatch = Props.dispatch; var editorCode = Props.editorCode; + var dispatchEval = Props.dispatchEval; var router = Next.Router.useRouter(undefined); var children; var exit = 0; @@ -1529,6 +1540,28 @@ function Playground$ControlPanel(Props) { [Symbol.for("name")]: "Format" }); }; + var onRunClick = function (evt) { + evt.preventDefault(); + var getSuccessCompilationResult = function (result) { + if (result.TAG === /* Success */1) { + return result._0; + } + + }; + var x = ready.result; + if (typeof x === "number") { + console.log("nothing"); + return ; + } + if (x.TAG === /* Conv */0) { + console.log("conv"); + return ; + } + Belt_Option.map(getSuccessCompilationResult(x._0), (function (r) { + return Curry._1(dispatchEval, r.js_code); + })); + + }; var createShareLink = function (param) { var lang = ready.targetLang; var params = lang >= 2 ? [] : [[ @@ -1551,6 +1584,11 @@ function Playground$ControlPanel(Props) { }, React.createElement(Playground$ControlPanel$Button, { children: "Format", onClick: onFormatClick + })), React.createElement("div", { + className: "mr-2" + }, React.createElement(Playground$ControlPanel$Button, { + children: "Run", + onClick: onRunClick })), React.createElement(Playground$ControlPanel$ShareButton, { createShareLink: createShareLink, actionIndicatorKey: actionIndicatorKey @@ -1591,6 +1629,7 @@ function Playground$OutputPanel(Props) { var compilerDispatch = Props.compilerDispatch; var compilerState = Props.compilerState; var editorCode = Props.editorCode; + var evalState = Props.evalState; var prevState = React.useRef(undefined); var prev = prevState.current; var cmCode; @@ -1768,6 +1807,21 @@ function Playground$OutputPanel(Props) { selected = 0; } } + var logs; + if (typeof evalState === "number") { + logs = []; + } else { + switch (evalState.TAG | 0) { + case /* Evaluating */0 : + logs = []; + break; + case /* Evaluated */1 : + case /* Error */2 : + logs = evalState.logs; + break; + + } + } prevSelected.current = selected; var tabs = [ { @@ -1782,6 +1836,12 @@ function Playground$OutputPanel(Props) { } }, errorPane) }, + { + title: "Logs", + content: React.createElement(Playground$Logs, { + logs: logs + }) + }, { title: "Settings", content: React.createElement("div", { @@ -1807,12 +1867,13 @@ var initialResContent = "module Button = {\n @react.component\n let make = (~c function Playground$default(Props) { var router = Next.Router.useRouter(undefined); - var match = Js_dict.get(router.query, "ext"); - var initialLang = match === "re" ? /* Reason */0 : /* Res */2; - var match$1 = Js_dict.get(router.query, "code"); + var match = Eval.useEval(undefined); + var match$1 = Js_dict.get(router.query, "ext"); + var initialLang = match$1 === "re" ? /* Reason */0 : /* Res */2; + var match$2 = Js_dict.get(router.query, "code"); var initialContent; - if (match$1 !== undefined) { - initialContent = LzString.decompressFromEncodedURIComponent(match$1); + if (match$2 !== undefined) { + initialContent = LzString.decompressFromEncodedURIComponent(match$2); } else { switch (initialLang) { case /* Reason */0 : @@ -1825,11 +1886,11 @@ function Playground$default(Props) { } } - var match$2 = React.useState(function () { + var match$3 = React.useState(function () { return 0; }); - var setActionCount = match$2[1]; - var actionCount = match$2[0]; + var setActionCount = match$3[1]; + var actionCount = match$3[0]; var onAction = function (param) { return Curry._1(setActionCount, (function (prev) { if (prev > 1000000) { @@ -1839,32 +1900,32 @@ function Playground$default(Props) { } })); }; - var match$3 = CompilerManagerHook.useCompilerManager(initialLang, onAction, undefined); - var compilerDispatch = match$3[1]; - var compilerState = match$3[0]; + var match$4 = CompilerManagerHook.useCompilerManager(initialLang, onAction, undefined); + var compilerDispatch = match$4[1]; + var compilerState = match$4[0]; var overlayState = React.useState(function () { return false; }); var windowWidth = CodeMirror.useWindowWidth(undefined); - var match$4 = React.useState(function () { + var match$5 = React.useState(function () { }); - var setFocusedRowCol = match$4[1]; + var setFocusedRowCol = match$5[1]; var editorCode = React.useRef(initialContent); if (typeof compilerState !== "number" && compilerState.TAG === /* Ready */2) { var ready = compilerState._0; - var match$5 = ready.result; - if (typeof match$5 === "number") { + var match$6 = ready.result; + if (typeof match$6 === "number") { Curry._1(compilerDispatch, { TAG: 3, _0: ready.targetLang, _1: editorCode.current, [Symbol.for("name")]: "CompileCode" }); - } else if (match$5.TAG === /* Conv */0) { - var match$6 = match$5._0; - if (match$6.TAG === /* Success */0) { - editorCode.current = match$6._0.code; + } else if (match$6.TAG === /* Conv */0) { + var match$7 = match$6._0; + if (match$7.TAG === /* Success */0) { + editorCode.current = match$7._0.code; } } @@ -1897,8 +1958,8 @@ function Playground$default(Props) { if (typeof result === "number") { cmErrors = []; } else if (result.TAG === /* Conv */0) { - var match$7 = result._0; - cmErrors = match$7.TAG === /* Fail */1 ? match$7.details.map(function (param) { + var match$8 = result._0; + cmErrors = match$8.TAG === /* Fail */1 ? match$8.details.map(function (param) { return locMsgToCmError("Error", param); }) : []; } else { @@ -1940,14 +2001,14 @@ function Playground$default(Props) { if (typeof compilerState === "number") { cmHoverHints = []; } else if (compilerState.TAG === /* Ready */2) { - var match$8 = compilerState._0.result; - if (typeof match$8 === "number") { + var match$9 = compilerState._0.result; + if (typeof match$9 === "number") { cmHoverHints = []; - } else if (match$8.TAG === /* Conv */0) { + } else if (match$9.TAG === /* Conv */0) { cmHoverHints = []; } else { - var match$9 = match$8._0; - cmHoverHints = match$9.TAG === /* Success */1 ? match$9._0.type_hints.map(function (hint) { + var match$10 = match$9._0; + cmHoverHints = match$10.TAG === /* Success */1 ? match$10._0.type_hints.map(function (hint) { var start = hint._0.start; var end = hint._0.end; return { @@ -2015,7 +2076,8 @@ function Playground$default(Props) { actionIndicatorKey: String(actionCount), state: compilerState, dispatch: compilerDispatch, - editorCode: editorCode + editorCode: editorCode, + dispatchEval: match[1] }), React.createElement(CodeMirror.make, { errors: cmErrors, hoverHints: cmHoverHints, @@ -2056,7 +2118,8 @@ function Playground$default(Props) { }, React.createElement(Playground$OutputPanel, { compilerDispatch: compilerDispatch, compilerState: compilerState, - editorCode: editorCode + editorCode: editorCode, + evalState: match[0] }), React.createElement("div", { className: "absolute bottom-0 w-full" }, React.createElement(Playground$Statusbar, { diff --git a/src/Playground.res b/src/Playground.res index b5b6f5d9a..4d2325030 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1101,6 +1101,18 @@ module Settings = { } } +module Logs = { + @react.component + let make = (~logs) => + +} + module ControlPanel = { module Button = { @react.component @@ -1182,6 +1194,7 @@ module ControlPanel = { ~state: CompilerManagerHook.state, ~dispatch: CompilerManagerHook.action => unit, ~editorCode: React.ref, + ~dispatchEval: string => unit, ) => { let router = Next.Router.useRouter() let children = switch state { @@ -1194,6 +1207,23 @@ module ControlPanel = { dispatch(Format(editorCode.current)) } + let onRunClick = evt => { + ReactEvent.Mouse.preventDefault(evt) + + let getSuccessCompilationResult = result => + switch result { + | RescriptCompilerApi.CompilationResult.Success(r) => Some(r) + | _ => None + } + + switch ready.result { + | FinalResult.Nothing => Js.log("nothing") + | FinalResult.Comp(x) => + getSuccessCompilationResult(x)->Belt.Option.map(r => dispatchEval(r.js_code))->ignore + | FinalResult.Conv(_) => Js.log("conv") + } + } + let createShareLink = () => { let params = switch ready.targetLang { | Res => [] @@ -1216,6 +1246,7 @@ module ControlPanel = {
+
| _ => React.null @@ -1258,6 +1289,7 @@ module OutputPanel = { ~compilerDispatch, ~compilerState: CompilerManagerHook.state, ~editorCode: React.ref, + ~evalState: Eval.state, ) => { /* We need the prevState to understand different @@ -1366,6 +1398,13 @@ module OutputPanel = { | _ => 0 } + let logs = switch evalState { + | Eval.Error({logs}) + | Eval.Evaluated({logs}) => logs + | Eval.Evaluating(_) + | Idle => [] + } + prevSelected.current = selected let tabs = [ @@ -1374,6 +1413,10 @@ module OutputPanel = { title: "Problems", content:
errorPane
, }, + { + title: "Logs", + content: , + }, { title: "Settings", content:
settingsPane
, @@ -1417,6 +1460,7 @@ let initialReContent = j`Js.log("Hello Reason 3.6!");` @react.component let default = () => { let router = Next.Router.useRouter() + let (evalState, dispatchEval) = Eval.useEval() let initialLang = switch Js.Dict.get(router.query, "ext") { | Some("re") => Api.Lang.Reason @@ -1563,6 +1607,7 @@ let default = () => { state=compilerState dispatch=compilerDispatch editorCode + dispatchEval /> {
1024 ? "56rem" : "100%", ())}> - +
unit = "toggle" } @@ -15,7 +15,6 @@ module Element = { @get external classList: Dom.element => ClassList.t = "classList" } - type animationFrameId @scope("window") @val @@ -23,3 +22,11 @@ external requestAnimationFrame: (unit => unit) => animationFrameId = "requestAni @scope("window") @val external cancelAnimationFrame: animationFrameId => unit = "cancelAnimationFrame" + +module Url = { + type t + @new external make: string => t = "URL" + @new external makeWithBase: (string, string) => t = "URL" + + @send external toString: t => string = "toString" +} diff --git a/src/bindings/Worker.resi b/src/bindings/Worker.resi index 12b1858d2..0d3e7060f 100644 --- a/src/bindings/Worker.resi +++ b/src/bindings/Worker.resi @@ -13,16 +13,16 @@ module Make: (Config: Config) => include Config module App: { - let postMessage: (worker, fromApp) => unit - let onMessage: (worker, {"data": fromWorker} => unit) => unit + let postMessage: (worker, Config.fromApp) => unit + let onMessage: (worker, {"data": Config.fromWorker} => unit) => unit let onError: (worker, 'a => unit) => unit let terminate: worker => unit } module Worker: { type self - let postMessage: fromWorker => unit - let onMessage: (self, {"data": fromApp} => unit) => unit + let postMessage: Config.fromWorker => unit + let onMessage: (self, {"data": Config.fromApp} => unit) => unit let self: self let importScripts: string => unit } diff --git a/src/common/Eval.mjs b/src/common/Eval.mjs new file mode 100644 index 000000000..19e6bd77f --- /dev/null +++ b/src/common/Eval.mjs @@ -0,0 +1,140 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Curry from "rescript/lib/es6/curry.js"; +import * as React from "react"; +import * as $$Worker from "../bindings/Worker.mjs"; +import * as Belt_Option from "rescript/lib/es6/belt_Option.js"; +import * as Caml_option from "rescript/lib/es6/caml_option.js"; + +function make(param) { + return (new Worker(new URL("./EvalWorker.mjs", import.meta.url))); +} + +var Config = { + make: make +}; + +var EvalWorker = $$Worker.Make(Config); + +function reducer(state, action) { + if (typeof state === "number") { + if (action.TAG === /* Evaluate */0) { + return { + TAG: 0, + code: action._0, + logs: [], + [Symbol.for("name")]: "Evaluating" + }; + } else { + return state; + } + } + switch (state.TAG | 0) { + case /* Evaluating */0 : + var logs = state.logs; + var code = state.code; + switch (action.TAG | 0) { + case /* Evaluate */0 : + return state; + case /* Success */1 : + if (action.forCode === code) { + return { + TAG: 1, + logs: logs, + [Symbol.for("name")]: "Evaluated" + }; + } else { + return state; + } + case /* Exception */2 : + if (action.forCode !== code) { + return state; + } + var exn = action.exn; + var message = exn.message; + return { + TAG: 2, + logs: logs.concat([message !== undefined ? message : ""]), + exn: exn, + [Symbol.for("name")]: "Error" + }; + case /* Log */3 : + if (action.forCode === code) { + return { + TAG: 0, + code: code, + logs: logs.concat(action.logArgs), + [Symbol.for("name")]: "Evaluating" + }; + } else { + return state; + } + + } + case /* Evaluated */1 : + if (action.TAG === /* Evaluate */0) { + return { + TAG: 0, + code: action._0, + logs: [], + [Symbol.for("name")]: "Evaluating" + }; + } else { + return state; + } + case /* Error */2 : + if (action.TAG === /* Evaluate */0) { + return { + TAG: 0, + code: action._0, + logs: [], + [Symbol.for("name")]: "Evaluating" + }; + } else { + return state; + } + + } +} + +function useEval(param) { + var match = React.useReducer(reducer, /* Idle */0); + var dispatch = match[1]; + var workerRef = React.useRef(undefined); + React.useEffect((function () { + var worker = Curry._1(EvalWorker.make, undefined); + workerRef.current = Caml_option.some(worker); + Curry._2(EvalWorker.App.onMessage, worker, (function (message) { + return Curry._1(dispatch, message.data); + })); + return (function (param) { + Belt_Option.map(workerRef.current, (function (worker) { + return Curry._1(EvalWorker.App.terminate, worker); + })); + + }); + }), []); + return [ + match[0], + (function (code) { + var evaluateAction = { + TAG: 0, + _0: code, + [Symbol.for("name")]: "Evaluate" + }; + Curry._1(dispatch, evaluateAction); + return Belt_Option.forEach(workerRef.current, (function (worker) { + return Curry._2(EvalWorker.App.postMessage, worker, evaluateAction); + })); + }) + ]; +} + +export { + Config , + EvalWorker , + reducer , + useEval , + +} +/* EvalWorker Not a pure module */ diff --git a/src/common/Eval.res b/src/common/Eval.res new file mode 100644 index 000000000..80315f0f5 --- /dev/null +++ b/src/common/Eval.res @@ -0,0 +1,71 @@ +@val external importMetaUrl: string = "import.meta.url" + +type state = + | Idle + | Evaluating({code: string, logs: array}) + | Evaluated({logs: array}) + | Error({logs: array, exn: Js.Exn.t}) +type action = + | Evaluate(string) + | Success({forCode: string}) + | Exception({forCode: string, exn: Js.Exn.t}) + | Log({forCode: string, logArgs: array}) + +module Config = { + type fromWorker = action + type fromApp = action + let make = () => %raw(`new Worker(new URL("./EvalWorker.mjs", import.meta.url))`) +} + +module EvalWorker = Worker.Make(Config) + +let reducer = (state, action) => + switch (state, action) { + | (Idle, Evaluate(code)) => Evaluating({code: code, logs: []}) + | (Evaluating({code, logs}), Success({forCode})) if forCode === code => Evaluated({logs: logs}) + | (Evaluating({code, logs}), Exception({forCode, exn})) if forCode === code => + Error({ + logs: logs->Js.Array2.concat([ + switch exn->Js.Exn.message { + | Some(message) => message->Js.Json.string + | None => ""->Js.Json.string + }, + ]), + exn: exn, + }) + | (Evaluating({code, logs}), Log({forCode, logArgs})) if forCode === code => + Evaluating({ + code: code, + logs: logs->Js.Array2.concat(logArgs), + }) + | (Error(_), Evaluate(code)) => Evaluating({code: code, logs: []}) + | (Evaluated(_), Evaluate(code)) => Evaluating({code: code, logs: []}) + | _ => state + } + +let useEval = () => { + let (state, dispatch) = React.useReducer(reducer, Idle) + let workerRef = React.useRef(None) + + React.useEffect1(() => { + let worker = EvalWorker.make() + workerRef.current = Some(worker) + + worker->EvalWorker.App.onMessage(message => dispatch(message["data"])) + + Some( + () => workerRef.current->Belt.Option.map(worker => worker->EvalWorker.App.terminate)->ignore, + ) + }, []) + + ( + state, + code => { + let evaluateAction = Evaluate(code) + evaluateAction->dispatch + workerRef.current->Belt.Option.forEach(worker => + worker->EvalWorker.App.postMessage(evaluateAction) + ) + }, + ) +} diff --git a/src/common/EvalWorker.mjs b/src/common/EvalWorker.mjs new file mode 100644 index 000000000..69a64c1a0 --- /dev/null +++ b/src/common/EvalWorker.mjs @@ -0,0 +1,80 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Eval from "./Eval.mjs"; +import * as Curry from "rescript/lib/es6/curry.js"; + +var evaluateCode = (function (code, handlers) { + const rawConsole = console; + try { + // TODO: For some reason this isn't capturing logs... + const replace = { + log: function (...args) { handlers.onConsoleLog(args) }, + warn: function (...args) { handlers.onConsoleWarn(args) }, + error: function (...args) { handlers.onConsoleError(args) } + }; + self.console = Object.assign({}, rawConsole, replace); + eval(code); + handlers.onDone() + } catch (exn) { + handlers.onException(exn); + } + self.console = rawConsole + }); + +var dispatch = Eval.EvalWorker.$$Worker.postMessage; + +Curry._2(Eval.EvalWorker.$$Worker.onMessage, Eval.EvalWorker.$$Worker.self, (function (msg) { + var code = msg.data; + if (code.TAG !== /* Evaluate */0) { + return ; + } + var code$1 = code._0; + return evaluateCode(code$1, { + onConsoleLog: (function (logArgs) { + return Curry._1(dispatch, { + TAG: 3, + forCode: code$1, + logArgs: logArgs, + [Symbol.for("name")]: "Log" + }); + }), + onConsoleWarn: (function (logArgs) { + return Curry._1(dispatch, { + TAG: 3, + forCode: code$1, + logArgs: logArgs, + [Symbol.for("name")]: "Log" + }); + }), + onConsoleError: (function (logArgs) { + return Curry._1(dispatch, { + TAG: 3, + forCode: code$1, + logArgs: logArgs, + [Symbol.for("name")]: "Log" + }); + }), + onException: (function (exn) { + return Curry._1(dispatch, { + TAG: 2, + forCode: code$1, + exn: exn, + [Symbol.for("name")]: "Exception" + }); + }), + onDone: (function (param) { + return Curry._1(dispatch, { + TAG: 1, + forCode: code$1, + [Symbol.for("name")]: "Success" + }); + }) + }); + })); + +export { + evaluateCode , + dispatch , + +} +/* Not a pure module */ diff --git a/src/common/EvalWorker.res b/src/common/EvalWorker.res new file mode 100644 index 000000000..84adb6ce0 --- /dev/null +++ b/src/common/EvalWorker.res @@ -0,0 +1,46 @@ +type evaluationHandlers = { + onConsoleLog: array => unit, + onConsoleWarn: array => unit, + onConsoleError: array => unit, + onException: Js.Exn.t => unit, + onDone: unit => unit, +} + +let evaluateCode: (string, evaluationHandlers) => unit = %raw(` + function (code, handlers) { + const rawConsole = console; + try { + // TODO: For some reason this isn't capturing logs... + const replace = { + log: function (...args) { handlers.onConsoleLog(args) }, + warn: function (...args) { handlers.onConsoleWarn(args) }, + error: function (...args) { handlers.onConsoleError(args) } + }; + self.console = Object.assign({}, rawConsole, replace); + eval(code); + handlers.onDone() + } catch (exn) { + handlers.onException(exn); + } + self.console = rawConsole + } +`) + +let dispatch = Eval.EvalWorker.Worker.postMessage + +Eval.EvalWorker.Worker.self->Eval.EvalWorker.Worker.onMessage(msg => + switch msg["data"] { + | Eval.Evaluate(code) => + evaluateCode( + code, + { + onConsoleLog: logArgs => dispatch(Eval.Log({forCode: code, logArgs: logArgs})), + onConsoleWarn: logArgs => dispatch(Eval.Log({forCode: code, logArgs: logArgs})), + onConsoleError: logArgs => dispatch(Eval.Log({forCode: code, logArgs: logArgs})), + onException: exn => dispatch(Eval.Exception({forCode: code, exn: exn})), + onDone: () => dispatch(Eval.Success({forCode: code})), + }, + ) + | _ => () + } +)