Skip to content

React output in playground #875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 8 additions & 21 deletions src/ConsolePanel.res
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ type logLevel = [
]

@react.component
let make = (~compilerState, ~runOutput) => {
let (logs, setLogs) = React.useState(_ => [])

let make = (~logs, ~setLogs) => {
React.useEffect(() => {
let cb = e => {
let data = e["data"]
Expand All @@ -22,23 +20,13 @@ let make = (~compilerState, ~runOutput) => {
Some(() => Webapi.Window.removeEventListener("message", cb))
}, [])

React.useEffect(() => {
if runOutput {
switch compilerState {
| CompilerManagerHook.Ready({result: Comp(Success({js_code}))}) =>
setLogs(_ => [])
let ast = AcornParse.parse(js_code)
let transpiled = AcornParse.removeImportsAndExports(ast)
EvalIFrame.sendOutput(transpiled)
| _ => ()
}
}
None
}, (compilerState, runOutput))

<div>
<div className="px-2 py-6 relative flex flex-col flex-1 overflow-y-hidden">
<h2 className="font-bold text-gray-5/50 absolute right-2 top-2"> {React.string("Console")} </h2>
{switch logs {
| [] => React.null
| [] =>
React.string(
"Add some 'Console.log' to your code and enable 'Auto-run' to see your logs here.",
)
| logs =>
let content =
logs
Expand All @@ -56,8 +44,7 @@ let make = (~compilerState, ~runOutput) => {
})
->React.array

<div className="whitespace-pre-wrap p-4 block"> content </div>
<div className="whitespace-pre-wrap p-4 overflow-auto"> content </div>
}}
<EvalIFrame />
</div>
}
8 changes: 8 additions & 0 deletions src/OutputPanel.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@react.component
let make = (~runOutput, ~compilerState, ~logs, ~setLogs) => {
<div className="h-full flex flex-col overflow-y-hidden">
<RenderPanel runOutput compilerState clearLogs={() => setLogs(_ => [])} />
<hr className="border-gray-60" />
<ConsolePanel logs setLogs />
</div>
}
72 changes: 6 additions & 66 deletions src/Playground.res
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ open CompilerManagerHook
module Api = RescriptCompilerApi

type layout = Column | Row
type tab = JavaScript | Problems | Settings | Console
type tab = JavaScript | Output | Problems | Settings
let breakingPoint = 1024

module DropdownSelect = {
Expand Down Expand Up @@ -1204,67 +1204,6 @@ let locMsgToCmError = (~kind: CodeMirror.Error.kind, locMsg: Api.LocMsg.t): Code
}
}

// module RenderOutput = {
// @react.component
// let make = (~compilerState: CompilerManagerHook.state) => {
// React.useEffect(() => {
// let code = switch compilerState {
// | Ready(ready) =>
// switch ready.result {
// | Comp(Success(_)) => ControlPanel.codeFromResult(ready.result)->Some
// | _ => None
// }
// | _ => None
// }

// let _valid = switch code {
// | Some(code) =>
// switch RenderOutputManager.renderOutput(code) {
// | Ok(_) => true
// | Error(_) => false
// }
// | None => false
// }
// None
// }, [compilerState])

// <div className={""}>
// <iframe
// width="100%"
// id="iframe-eval"
// className="relative w-full text-gray-20"
// srcDoc=RenderOutputManager.Frame.srcdoc
// />
// </div>

// // switch code {
// // | Some(code) =>
// // switch RenderOutputManager.renderOutput(code) {
// // | Ok() =>
// // <iframe
// // width="100%"
// // id="iframe-eval"
// // className="relative w-full text-gray-20"
// // srcDoc=RenderOutputManager.Frame.srcdoc
// // />
// // | Error() =>
// // let code = `module App = {
// // @react.component
// // let make = () => {
// // <ModuleName />
// // }
// // }`
// // <div className={"whitespace-pre-wrap p-4 block"}>
// // <p className={"mb-2"}> {React.string("To render element create a module App")} </p>
// // <pre> {HighlightJs.renderHLJS(~code, ~darkmode=true, ~lang="rescript", ())} </pre>
// // </div>
// // }

// // | _ => React.null
// // }
// }
// }

module OutputPanel = {
@react.component
let make = (
Expand Down Expand Up @@ -1381,8 +1320,10 @@ module OutputPanel = {

prevSelected.current = selected

let (logs, setLogs) = React.useState(_ => [])

let tabs = [
(Console, <ConsolePanel compilerState runOutput />),
(Output, <OutputPanel runOutput compilerState logs setLogs />),
(JavaScript, output),
(Problems, errorPane),
(Settings, settingsPane),
Expand Down Expand Up @@ -1763,12 +1704,11 @@ let make = (~versions: array<string>) => {
"flex-1 items-center p-4 border-t-4 border-transparent " ++ activeClass
}

let tabs = [JavaScript, Console, Problems, Settings]
let tabs = [JavaScript, Output, Problems, Settings]

let headers = Array.mapWithIndex(tabs, (tab, i) => {
let title = switch tab {
// | RenderOutput => "Render Output"
| Console => "Console"
| Output => "Output"
| JavaScript => "JavaScript"
| Problems => "Problems"
| Settings => "Settings"
Expand Down
39 changes: 39 additions & 0 deletions src/RenderPanel.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
let wrapReactApp = code =>
`(function () {
${code}
window.reactRoot.render(React.createElement(App.make, {}));
})();`

@react.component
let make = (~compilerState: CompilerManagerHook.state, ~clearLogs, ~runOutput) => {
let (validReact, setValidReact) = React.useState(() => false)
React.useEffect(() => {
if runOutput {
switch compilerState {
| CompilerManagerHook.Ready({result: Comp(Success({js_code}))}) =>
clearLogs()
let ast = AcornParse.parse(js_code)
let transpiled = AcornParse.removeImportsAndExports(ast)
let isValidReact = AcornParse.hasEntryPoint(ast)
isValidReact
? transpiled->wrapReactApp->EvalIFrame.sendOutput
: EvalIFrame.sendOutput(transpiled)
setValidReact(_ => isValidReact)
| _ => ()
}
}
None
}, (compilerState, runOutput))

<div className={`px-2 relative ${validReact ? "flex-1 py-2 overflow-y-auto" : "h-auto py-6"}`}>
<h2 className="font-bold text-gray-5/50 absolute right-2 top-2"> {React.string("React")} </h2>
{validReact
? React.null
: React.string(
"Create a React component called 'App' if you want to render it here, then enable 'Auto-run'.",
)}
<div className={validReact ? "h-full" : "h-0"}>
<EvalIFrame />
</div>
</div>
}
50 changes: 38 additions & 12 deletions src/common/EvalIFrame.res
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ let css = `body {
color-scheme: light dark;
}`

let reactVersion = "18.2.0"

let srcDoc = `
<html>
<head>
Expand All @@ -13,22 +15,49 @@ let srcDoc = `
</head>
<body>
<div id="root"></div>
<script type="importmap">
{
"imports": {
"@jsxImportSource": "https://esm.sh/react@${reactVersion}",
"react-dom/client": "https://esm.sh/react-dom@${reactVersion}/client",
"react": "https://esm.sh/react@${reactVersion}",
"react/jsx-runtime": "https://esm.sh/react@${reactVersion}/jsx-runtime"
}
}
</script>
<script type="module">
import * as ReactDOM from 'react-dom/client';
import * as React from 'react';
import * as JsxRuntime from 'react/jsx-runtime';
const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
window.reactRoot = root;
window.React = React;
window.JsxRuntime = JsxRuntime;
</script>
<script>
window.addEventListener("message", (event) => {
try {
eval(event.data);
// https://rollupjs.org/troubleshooting/#avoiding-eval
const eval2 = eval;
eval2(event.data);
} catch (err) {
console.error(err);
}
});
const sendLog = (logLevel) => (...args) => {
let finalArgs = args.map(arg => {
if (typeof arg === 'object') {
return JSON.stringify(arg);
if (arg === undefined) {
return 'undefined';
}
else if (typeof arg === 'object') {
return JSON.stringify(arg, Object.getOwnPropertyNames(arg));
} else if (typeof arg === 'function') {
return '[function]';
}
return arg;
});
parent.window.postMessage({ type: logLevel, args: finalArgs }, '*')
parent.window.postMessage({ type: logLevel, args: finalArgs }, '*');
};
console.log = sendLog('log');
console.warn = sendLog('warn');
Expand All @@ -41,22 +70,19 @@ let srcDoc = `
let sendOutput = code => {
open Webapi

let frame =
Document.document
->Element.getElementById("iframe-eval")
->Nullable.toOption
let frame = Document.document->Element.getElementById("iframe-eval")

switch frame {
| Some(element) =>
| Value(element) =>
switch element->Element.contentWindow {
| Some(win) => win->Element.postMessage(code, ~targetOrigin="*")
| None => ()
| None => RescriptCore.Console.error("contentWindow not found")
}
| None => ()
| Null | Undefined => RescriptCore.Console.error("iframe not found")
}
}

@react.component
let make = () => {
<iframe width="100%" id="iframe-eval" className="relative w-full text-gray-20" srcDoc />
<iframe width="100%" id="iframe-eval" className="relative h-full w-full text-gray-20" srcDoc />
}
21 changes: 0 additions & 21 deletions src/common/RenderOutputManager.res

This file was deleted.