Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit be78d05

Browse files
tsnobipdiogomqbmaspeddro
authoredMay 23, 2024
Output logs in playground (#865)
* wip: running custom code * feat: running code 🥳 * feat: bundling stdlib * fix: fixing ci * feat: better ux * chore: removing useless deps * chore: removing useless file * chore: better method name * fix: adjusting iframe height * fix: safelly checking for import and exports * refactor: better naming * chore: adjusting code style * refactor: moving logic to RenderOutputManager * remove .mjs files * add log * update * update * WIP output console log * complete console and react output * polish log output * explicitly add acorn as a dependency --------- Co-authored-by: Diogo Mafra <diogomqbm13@gmail.com> Co-authored-by: Diogo Mafra <diogo@mafra.sh> Co-authored-by: Pedro Castro <aspeddro@gmail.com>
1 parent a0cf858 commit be78d05

File tree

10 files changed

+2569
-169
lines changed

10 files changed

+2569
-169
lines changed
 

‎package-lock.json

Lines changed: 2210 additions & 140 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
"@rescript/core": "^1.3.0",
2424
"@rescript/react": "^0.12.0-alpha.3",
2525
"@rescript/tools": "^0.5.0",
26+
"acorn": "^8.11.3",
2627
"codemirror": "^5.54.0",
2728
"docson": "^2.1.0",
29+
"escodegen": "^2.1.0",
2830
"eslint-config-next": "^13.1.1",
2931
"fuse.js": "^6.4.3",
3032
"gentype": "^3.44.0",
@@ -71,4 +73,4 @@
7173
"simple-functional-loader": "^1.2.1",
7274
"tailwindcss": "^3.3.3"
7375
}
74-
}
76+
}

‎src/ConsolePanel.res

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
type logLevel = [
2+
| #log
3+
| #warn
4+
| #error
5+
]
6+
7+
@react.component
8+
let make = (~compilerState, ~runOutput) => {
9+
let (logs, setLogs) = React.useState(_ => [])
10+
11+
React.useEffect(() => {
12+
let cb = e => {
13+
let data = e["data"]
14+
switch data["type"] {
15+
| #...logLevel as logLevel =>
16+
let args: array<string> = data["args"]
17+
setLogs(previous => previous->Belt.Array.concat([(logLevel, args)]))
18+
| _ => ()
19+
}
20+
}
21+
Webapi.Window.addEventListener("message", cb)
22+
Some(() => Webapi.Window.removeEventListener("message", cb))
23+
}, [])
24+
25+
React.useEffect(() => {
26+
if runOutput {
27+
switch compilerState {
28+
| CompilerManagerHook.Ready({result: Comp(Success({js_code}))}) =>
29+
setLogs(_ => [])
30+
let ast = AcornParse.parse(js_code)
31+
let transpiled = AcornParse.removeImportsAndExports(ast)
32+
EvalIFrame.sendOutput(transpiled)
33+
| _ => ()
34+
}
35+
}
36+
None
37+
}, (compilerState, runOutput))
38+
39+
<div>
40+
{switch logs {
41+
| [] => React.null
42+
| logs =>
43+
let content =
44+
logs
45+
->Belt.Array.mapWithIndex((i, (logLevel, log)) => {
46+
let log = Js.Array2.joinWith(log, " ")
47+
<pre
48+
key={RescriptCore.Int.toString(i)}
49+
className={switch logLevel {
50+
| #log => ""
51+
| #warn => "text-orange"
52+
| #error => "text-fire"
53+
}}>
54+
{React.string(log)}
55+
</pre>
56+
})
57+
->React.array
58+
59+
<div className="whitespace-pre-wrap p-4 block"> content </div>
60+
}}
61+
<EvalIFrame />
62+
</div>
63+
}

‎src/Playground.res

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ open CompilerManagerHook
2222
module Api = RescriptCompilerApi
2323

2424
type layout = Column | Row
25-
type tab = JavaScript | Problems | Settings
25+
type tab = JavaScript | Problems | Settings | Console
2626
let breakingPoint = 1024
2727

2828
module DropdownSelect = {
@@ -1054,6 +1054,20 @@ module Settings = {
10541054
}
10551055

10561056
module ControlPanel = {
1057+
let codeFromResult = (result: FinalResult.t): string => {
1058+
open Api
1059+
switch result {
1060+
| FinalResult.Comp(comp) =>
1061+
switch comp {
1062+
| CompilationResult.Success({js_code}) => js_code
1063+
| UnexpectedError(_)
1064+
| Unknown(_, _)
1065+
| Fail(_) => "/* No JS code generated */"
1066+
}
1067+
| Nothing
1068+
| Conv(_) => "/* No JS code generated */"
1069+
}
1070+
}
10571071
module Button = {
10581072
@react.component
10591073
let make = (~children, ~onClick=?) =>
@@ -1134,8 +1148,11 @@ module ControlPanel = {
11341148
~state: CompilerManagerHook.state,
11351149
~dispatch: CompilerManagerHook.action => unit,
11361150
~editorCode: React.ref<string>,
1151+
~runOutput,
1152+
~toggleRunOutput,
11371153
) => {
11381154
let router = Next.Router.useRouter()
1155+
11391156
let children = switch state {
11401157
| Init => React.string("Initializing...")
11411158
| SwitchingCompiler(_ready, _version) => React.string("Switching Compiler...")
@@ -1168,12 +1185,14 @@ module ControlPanel = {
11681185
Next.Router.replace(router, url)
11691186
url
11701187
}
1171-
<>
1172-
<div className="mr-2">
1173-
<Button onClick=onFormatClick> {React.string("Format")} </Button>
1174-
</div>
1188+
1189+
<div className="flex flex-row gap-x-2">
1190+
<ToggleButton checked=runOutput onChange={_ => toggleRunOutput()}>
1191+
{React.string("Auto-run")}
1192+
</ToggleButton>
1193+
<Button onClick=onFormatClick> {React.string("Format")} </Button>
11751194
<ShareButton actionIndicatorKey createShareLink />
1176-
</>
1195+
</div>
11771196
| _ => React.null
11781197
}
11791198

@@ -1193,28 +1212,75 @@ let locMsgToCmError = (~kind: CodeMirror.Error.kind, locMsg: Api.LocMsg.t): Code
11931212
}
11941213
}
11951214

1196-
module OutputPanel = {
1197-
let codeFromResult = (result: FinalResult.t): string => {
1198-
open Api
1199-
switch result {
1200-
| FinalResult.Comp(comp) =>
1201-
switch comp {
1202-
| CompilationResult.Success({js_code}) => js_code
1203-
| UnexpectedError(_)
1204-
| Unknown(_, _)
1205-
| Fail(_) => "/* No JS code generated */"
1206-
}
1207-
| Nothing
1208-
| Conv(_) => "/* No JS code generated */"
1209-
}
1210-
}
1215+
// module RenderOutput = {
1216+
// @react.component
1217+
// let make = (~compilerState: CompilerManagerHook.state) => {
1218+
// React.useEffect(() => {
1219+
// let code = switch compilerState {
1220+
// | Ready(ready) =>
1221+
// switch ready.result {
1222+
// | Comp(Success(_)) => ControlPanel.codeFromResult(ready.result)->Some
1223+
// | _ => None
1224+
// }
1225+
// | _ => None
1226+
// }
1227+
1228+
// let _valid = switch code {
1229+
// | Some(code) =>
1230+
// switch RenderOutputManager.renderOutput(code) {
1231+
// | Ok(_) => true
1232+
// | Error(_) => false
1233+
// }
1234+
// | None => false
1235+
// }
1236+
// None
1237+
// }, [compilerState])
1238+
1239+
// <div className={""}>
1240+
// <iframe
1241+
// width="100%"
1242+
// id="iframe-eval"
1243+
// className="relative w-full text-gray-20"
1244+
// srcDoc=RenderOutputManager.Frame.srcdoc
1245+
// />
1246+
// </div>
1247+
1248+
// // switch code {
1249+
// // | Some(code) =>
1250+
// // switch RenderOutputManager.renderOutput(code) {
1251+
// // | Ok() =>
1252+
// // <iframe
1253+
// // width="100%"
1254+
// // id="iframe-eval"
1255+
// // className="relative w-full text-gray-20"
1256+
// // srcDoc=RenderOutputManager.Frame.srcdoc
1257+
// // />
1258+
// // | Error() =>
1259+
// // let code = `module App = {
1260+
// // @react.component
1261+
// // let make = () => {
1262+
// // <ModuleName />
1263+
// // }
1264+
// // }`
1265+
// // <div className={"whitespace-pre-wrap p-4 block"}>
1266+
// // <p className={"mb-2"}> {React.string("To render element create a module App")} </p>
1267+
// // <pre> {HighlightJs.renderHLJS(~code, ~darkmode=true, ~lang="rescript", ())} </pre>
1268+
// // </div>
1269+
// // }
1270+
1271+
// // | _ => React.null
1272+
// // }
1273+
// }
1274+
// }
12111275

1276+
module OutputPanel = {
12121277
@react.component
12131278
let make = (
12141279
~compilerDispatch,
12151280
~compilerState: CompilerManagerHook.state,
12161281
~editorCode: React.ref<string>,
12171282
~currentTab: tab,
1283+
~runOutput,
12181284
) => {
12191285
/*
12201286
We need the prevState to understand different
@@ -1232,17 +1298,18 @@ module OutputPanel = {
12321298
| (_, Ready({result: Nothing})) => None
12331299
| (Ready(prevReady), Ready(ready)) =>
12341300
switch (prevReady.result, ready.result) {
1235-
| (_, Comp(Success(_))) => codeFromResult(ready.result)->Some
1301+
| (_, Comp(Success(_))) => ControlPanel.codeFromResult(ready.result)->Some
12361302
| _ => None
12371303
}
1238-
| (_, Ready({result: Comp(Success(_)) as result})) => codeFromResult(result)->Some
1304+
| (_, Ready({result: Comp(Success(_)) as result})) =>
1305+
ControlPanel.codeFromResult(result)->Some
12391306
| (Ready({result: Comp(Success(_)) as result}), Compiling(_, _)) =>
1240-
codeFromResult(result)->Some
1307+
ControlPanel.codeFromResult(result)->Some
12411308
| _ => None
12421309
}
12431310
| None =>
12441311
switch compilerState {
1245-
| Ready(ready) => codeFromResult(ready.result)->Some
1312+
| Ready(ready) => ControlPanel.codeFromResult(ready.result)->Some
12461313
| _ => None
12471314
}
12481315
}
@@ -1322,7 +1389,12 @@ module OutputPanel = {
13221389

13231390
prevSelected.current = selected
13241391

1325-
let tabs = [(JavaScript, output), (Problems, errorPane), (Settings, settingsPane)]
1392+
let tabs = [
1393+
(Console, <ConsolePanel compilerState runOutput />),
1394+
(JavaScript, output),
1395+
(Problems, errorPane),
1396+
(Settings, settingsPane),
1397+
]
13261398

13271399
let body = Belt.Array.mapWithIndex(tabs, (i, (tab, content)) => {
13281400
let className = currentTab == tab ? "block h-inherit" : "hidden"
@@ -1700,10 +1772,12 @@ let make = (~versions: array<string>) => {
17001772
"flex-1 items-center p-4 border-t-4 border-transparent " ++ activeClass
17011773
}
17021774

1703-
let tabs = [JavaScript, Problems, Settings]
1775+
let tabs = [JavaScript, Console, Problems, Settings]
17041776

17051777
let headers = Belt.Array.mapWithIndex(tabs, (i, tab) => {
17061778
let title = switch tab {
1779+
// | RenderOutput => "Render Output"
1780+
| Console => "Console"
17071781
| JavaScript => "JavaScript"
17081782
| Problems => "Problems"
17091783
| Settings => "Settings"
@@ -1722,12 +1796,17 @@ let make = (~versions: array<string>) => {
17221796
</button>
17231797
})
17241798

1799+
let (runOutput, setRunOutput) = React.useState(() => false)
1800+
let toggleRunOutput = () => setRunOutput(prev => !prev)
1801+
17251802
<main className={"flex flex-col bg-gray-100 overflow-hidden"}>
17261803
<ControlPanel
17271804
actionIndicatorKey={Belt.Int.toString(actionCount)}
17281805
state=compilerState
17291806
dispatch=compilerDispatch
17301807
editorCode
1808+
runOutput
1809+
toggleRunOutput
17311810
/>
17321811
<div
17331812
className={`flex ${layout == Column ? "flex-col" : "flex-row"}`}
@@ -1782,7 +1861,7 @@ let make = (~versions: array<string>) => {
17821861
{React.array(headers)}
17831862
</div>
17841863
<div ref={ReactDOM.Ref.domRef(subPanelRef)} className="overflow-auto">
1785-
<OutputPanel currentTab compilerDispatch compilerState editorCode />
1864+
<OutputPanel currentTab compilerDispatch compilerState editorCode runOutput />
17861865
</div>
17871866
</div>
17881867
</div>

‎src/bindings/AcornParse.res

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type t
2+
@module("../ffi/acorn-parse.js") external parse: string => t = "parse"
3+
4+
@module("../ffi/acorn-parse.js") external hasEntryPoint: t => bool = "hasEntryPoint"
5+
6+
@module("../ffi/acorn-parse.js")
7+
external removeImportsAndExports: t => string = "removeImportsAndExports"

‎src/bindings/Webapi.res

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module Document = {
2+
@val external document: Dom.element = "document"
23
@scope("document") @val external createElement: string => Dom.element = "createElement"
34
@scope("document") @val external createTextNode: string => Dom.element = "createTextNode"
45
}
@@ -17,6 +18,15 @@ module Element = {
1718
@send external getBoundingClientRect: Dom.element => {..} = "getBoundingClientRect"
1819
@send external addEventListener: (Dom.element, string, unit => unit) => unit = "addEventListener"
1920

21+
@send
22+
external getElementById: (Dom.element, string) => Js.nullable<Dom.element> = "getElementById"
23+
24+
type contentWindow
25+
@get external contentWindow: Dom.element => option<contentWindow> = "contentWindow"
26+
27+
@send
28+
external postMessage: (contentWindow, string, ~targetOrigin: string=?) => unit = "postMessage"
29+
2030
module Style = {
2131
@scope("style") @set external width: (Dom.element, string) => unit = "width"
2232
@scope("style") @set external height: (Dom.element, string) => unit = "height"

‎src/common/EvalIFrame.res

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
let css = `body {
2+
background-color: inherit;
3+
color: CanvasText;
4+
color-scheme: light dark;
5+
}`
6+
7+
let srcDoc = `
8+
<html>
9+
<head>
10+
<meta charset="UTF-8" />
11+
<title>Playground Output</title>
12+
<style>${css}</style>
13+
</head>
14+
<body>
15+
<div id="root"></div>
16+
<script
17+
src="https://unpkg.com/react@17/umd/react.production.min.js"
18+
crossorigin
19+
></script>
20+
<script
21+
src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"
22+
crossorigin
23+
></script>
24+
<script>
25+
window.addEventListener("message", (event) => {
26+
try {
27+
eval(event.data);
28+
} catch (err) {
29+
console.error(err);
30+
}
31+
});
32+
const sendLog = (logLevel) => (...args) => {
33+
let finalArgs = args.map(arg => {
34+
if (typeof arg === 'object') {
35+
return JSON.stringify(arg);
36+
}
37+
return arg;
38+
});
39+
parent.window.postMessage({ type: logLevel, args: finalArgs }, '*')
40+
};
41+
console.log = sendLog('log');
42+
console.warn = sendLog('warn');
43+
console.error = sendLog('error');
44+
</script>
45+
</body>
46+
</html>
47+
`
48+
49+
let sendOutput = code => {
50+
open Webapi
51+
52+
let frame =
53+
Document.document
54+
->Element.getElementById("iframe-eval")
55+
->Js.Nullable.toOption
56+
57+
switch frame {
58+
| Some(element) =>
59+
switch element->Element.contentWindow {
60+
| Some(win) => win->Element.postMessage(code, ~targetOrigin="*")
61+
| None => ()
62+
}
63+
| None => ()
64+
}
65+
}
66+
67+
@react.component
68+
let make = () => {
69+
<iframe width="100%" id="iframe-eval" className="relative w-full text-gray-20" srcDoc />
70+
}

‎src/common/RenderOutputManager.res

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Transpiler = {
2+
let run = code =>
3+
`(function () {
4+
${code}
5+
const root = document.getElementById("root");
6+
ReactDOM.render(App.make(), root);
7+
})();`
8+
}
9+
10+
let renderOutput = code => {
11+
let ast = AcornParse.parse(code)
12+
let transpiled = AcornParse.removeImportsAndExports(ast)
13+
switch AcornParse.hasEntryPoint(ast) {
14+
| true =>
15+
Transpiler.run(transpiled)->EvalIFrame.sendOutput
16+
Ok()
17+
| false =>
18+
EvalIFrame.sendOutput(transpiled)
19+
Error()
20+
}
21+
}

‎src/components/ToggleButton.res

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@react.component
2+
let make = (~checked, ~onChange, ~children) => {
3+
<label className="inline-flex items-center cursor-pointer">
4+
<input type_="checkbox" value="" checked onChange className="sr-only peer" />
5+
<div
6+
className={`relative w-8 h-4 bg-gray-200
7+
rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full
8+
rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white
9+
after:content-[''] after:absolute after:top-[2px] after:start-[4px]
10+
after:bg-white after:border-gray-300 after:border after:rounded-full
11+
after:h-3 after:w-3 after:transition-all dark:border-gray-600
12+
peer-checked:bg-sky`}
13+
/>
14+
<span className={"ms-2 text-sm text-gray-900 dark:text-gray-300"}> {children} </span>
15+
</label>
16+
}

‎src/ffi/acorn-parse.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as acorn from "acorn";
2+
import { walk } from "estree-walker";
3+
import * as escodegen from "escodegen";
4+
5+
export function hasEntryPoint(ast) {
6+
let existsApp = false;
7+
8+
walk(ast, {
9+
enter(node) {
10+
const isAppVar = node?.type === "VariableDeclaration" &&
11+
node?.declarations[0]?.type === "VariableDeclarator" &&
12+
node?.declarations[0]?.id.name === "App" &&
13+
node?.declarations[0]?.id.type === "Identifier";
14+
15+
if (isAppVar) {
16+
const isObject = node?.declarations[0].init.type === "ObjectExpression"
17+
if (isObject) {
18+
const hasMake = [...node?.declarations[0].init.properties].some(
19+
p => p?.type === "Property" && p?.key?.type === "Identifier" && p?.key?.name === "make"
20+
)
21+
existsApp = hasMake
22+
}
23+
}
24+
}
25+
})
26+
27+
return existsApp
28+
}
29+
30+
export function parse(code) {
31+
return acorn.parse(code, {
32+
ecmaVersion: 9,
33+
sourceType: "module"
34+
});
35+
}
36+
37+
export function removeImportsAndExports(ast) {
38+
walk(ast, {
39+
enter(node) {
40+
const isImport =
41+
node?.type === "ImportDeclaration" ||
42+
(node?.type === "VariableDeclaration" &&
43+
node?.declarations[0]?.init?.type === "CallExpression" &&
44+
node?.declarations[0]?.init?.callee?.name === "require");
45+
const isExport =
46+
node?.type === "ExportDefaultDeclaration" ||
47+
node?.type === "ExportNamedDeclaration" ||
48+
node?.type === "ExportAllDeclaration" ||
49+
(node?.type === "ExpressionStatement" &&
50+
node?.expression?.type === "AssignmentExpression" &&
51+
node?.expression?.operator === "=" &&
52+
(node?.expression?.left?.object?.name === "exports" ||
53+
(node?.expression?.left?.object?.name === "module" &&
54+
node?.expression?.left?.property?.name === "exports")));
55+
if (isImport || isExport) {
56+
this.remove();
57+
}
58+
},
59+
});
60+
61+
return escodegen.generate(ast);
62+
}

0 commit comments

Comments
 (0)
Please sign in to comment.