diff --git a/package-lock.json b/package-lock.json
index 03e05c909..1031ebb23 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@rescript/tools": "^0.5.0",
"codemirror": "^5.54.0",
"docson": "^2.1.0",
+ "escodegen": "^2.1.0",
"eslint-config-next": "^13.1.1",
"fuse.js": "^6.4.3",
"gentype": "^3.44.0",
@@ -3364,6 +3365,35 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/eslint": {
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
@@ -3857,12 +3887,9 @@
}
},
"node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.0.tgz",
+ "integrity": "sha512-s6ceX0NFiU/vKPiKvFdR83U1Zffu7upwZsGwpoqfg5rbbq1l50WQ5hCeIvM6E6oD4shUHCYMsiFPns4Jk0YfMQ=="
},
"node_modules/esutils": {
"version": "2.0.3",
@@ -15945,6 +15972,25 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"peer": true
},
+ "escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "requires": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "optional": true
+ }
+ }
+ },
"eslint": {
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
@@ -16290,12 +16336,9 @@
}
},
"estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "requires": {
- "@types/estree": "^1.0.0"
- }
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.0.tgz",
+ "integrity": "sha512-s6ceX0NFiU/vKPiKvFdR83U1Zffu7upwZsGwpoqfg5rbbq1l50WQ5hCeIvM6E6oD4shUHCYMsiFPns4Jk0YfMQ=="
},
"esutils": {
"version": "2.0.3",
diff --git a/package.json b/package.json
index 88bf7f11b..c98c20ed9 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@rescript/tools": "^0.5.0",
"codemirror": "^5.54.0",
"docson": "^2.1.0",
+ "escodegen": "^2.1.0",
"eslint-config-next": "^13.1.1",
"fuse.js": "^6.4.3",
"gentype": "^3.44.0",
diff --git a/src/Playground.res b/src/Playground.res
index 42b5a3547..2eee14e4a 100644
--- a/src/Playground.res
+++ b/src/Playground.res
@@ -22,7 +22,7 @@ open CompilerManagerHook
module Api = RescriptCompilerApi
type layout = Column | Row
-type tab = JavaScript | Problems | Settings
+type tab = RenderOutput | JavaScript | Problems | Settings | Console
let breakingPoint = 1024
module DropdownSelect = {
@@ -1054,6 +1054,20 @@ module Settings = {
}
module ControlPanel = {
+ let codeFromResult = (result: FinalResult.t): string => {
+ open Api
+ switch result {
+ | FinalResult.Comp(comp) =>
+ switch comp {
+ | CompilationResult.Success({js_code}) => js_code
+ | UnexpectedError(_)
+ | Unknown(_, _)
+ | Fail(_) => "/* No JS code generated */"
+ }
+ | Nothing
+ | Conv(_) => "/* No JS code generated */"
+ }
+ }
module Button = {
@react.component
let make = (~children, ~onClick=?) =>
@@ -1168,6 +1182,7 @@ module ControlPanel = {
Next.Router.replace(router, url)
url
}
+
<>
@@ -1193,22 +1208,21 @@ let locMsgToCmError = (~kind: CodeMirror.Error.kind, locMsg: Api.LocMsg.t): Code
}
}
-module OutputPanel = {
- let codeFromResult = (result: FinalResult.t): string => {
- open Api
- switch result {
- | FinalResult.Comp(comp) =>
- switch comp {
- | CompilationResult.Success({js_code}) => js_code
- | UnexpectedError(_)
- | Unknown(_, _)
- | Fail(_) => "/* No JS code generated */"
- }
- | Nothing
- | Conv(_) => "/* No JS code generated */"
- }
+module RenderOutput = {
+ @react.component
+ let make = () => {
+
+
+
}
+}
+module OutputPanel = {
@react.component
let make = (
~compilerDispatch,
@@ -1226,23 +1240,46 @@ module OutputPanel = {
*/
let prevState = React.useRef(None)
+ let (logs, setLogs) = React.useState(_ => None)
+
+ React.useEffect(() => {
+ Webapi.Window.addEventListener("message", e => {
+ let data = e["data"]
+ let type_: string = data["type"]
+
+ if type_ === "log" {
+ let args: array
= data["args"]
+
+ setLogs(
+ _ =>
+ logs
+ ->Belt.Option.getWithDefault([])
+ ->Js.Array2.concat([args])
+ ->Some,
+ )
+ }
+ })
+ None
+ }, [])
+
let cmCode = switch prevState.current {
| Some(prev) =>
switch (prev, compilerState) {
| (_, Ready({result: Nothing})) => None
| (Ready(prevReady), Ready(ready)) =>
switch (prevReady.result, ready.result) {
- | (_, Comp(Success(_))) => codeFromResult(ready.result)->Some
+ | (_, Comp(Success(_))) => ControlPanel.codeFromResult(ready.result)->Some
| _ => None
}
- | (_, Ready({result: Comp(Success(_)) as result})) => codeFromResult(result)->Some
+ | (_, Ready({result: Comp(Success(_)) as result})) =>
+ ControlPanel.codeFromResult(result)->Some
| (Ready({result: Comp(Success(_)) as result}), Compiling(_, _)) =>
- codeFromResult(result)->Some
+ ControlPanel.codeFromResult(result)->Some
| _ => None
}
| None =>
switch compilerState {
- | Ready(ready) => codeFromResult(ready.result)->Some
+ | Ready(ready) => ControlPanel.codeFromResult(ready.result)->Some
| _ => None
}
}
@@ -1276,6 +1313,24 @@ module OutputPanel = {
{HighlightJs.renderHLJS(~code, ~darkmode=true, ~lang="js", ())}
+ let consolePanel = switch logs {
+ | Some(logs) =>
+ let content =
+ logs
+ ->Js.Array2.map(log =>
+
+ {log
+ ->Js.Array2.map(item => {`${item} `->React.string} )
+ ->React.array}
+
+ )
+ ->React.array
+
+ content
+
+ | None => React.null
+ }
+
let output =
resultPane
@@ -1322,7 +1377,13 @@ module OutputPanel = {
prevSelected.current = selected
- let tabs = [(JavaScript, output), (Problems, errorPane), (Settings, settingsPane)]
+ let tabs = [
+ (RenderOutput,
),
+ (Console, consolePanel),
+ (JavaScript, output),
+ (Problems, errorPane),
+ (Settings, settingsPane),
+ ]
let body = Belt.Array.mapWithIndex(tabs, (i, (tab, content)) => {
let className = currentTab == tab ? "block h-inherit" : "hidden"
@@ -1344,7 +1405,8 @@ and the different jsx modes (classic and automatic).
module InitialContent = {
let original = `module Button = {
@react.component
- let make = (~count) => {
+ let make = () => {
+ let (count, setCount) = React.useState(_ => 0)
let times = switch count {
| 1 => "once"
| 2 => "twice"
@@ -1352,60 +1414,42 @@ module InitialContent = {
}
let text = \`Click me $\{times\}\`
-
+
+ }
+}
+
+module App = {
+ @react.component
+ let make = () => {
+
}
}
`
- let since_10_1 = `@@jsxConfig({ version: 4, mode: "automatic" })
+ let since_10_1 = `@@jsxConfig({version: 4, mode: "classic"})
-module CounterMessage = {
+module Button = {
@react.component
- let make = (~count, ~username=?) => {
+ let make = () => {
+ let (count, setCount) = React.useState(_ => 0)
let times = switch count {
| 1 => "once"
| 2 => "twice"
- | n => Belt.Int.toString(n) ++ " times"
+ | n => n->Int.toString ++ " times"
}
+ let msg = \`Click me $\{times\}\`
- let name = switch username {
- | Some("") => "Anonymous"
- | Some(name) => name
- | None => "Anonymous"
- }
-
-
{React.string(\`Hello \$\{name\}, you clicked me \` ++ times)}
+
}
}
module App = {
@react.component
let make = () => {
- let (count, setCount) = React.useState(() => 0)
- let (username, setUsername) = React.useState(() => "Anonymous")
-
-
- {React.string("Username: ")}
- {
- evt->ReactEvent.Form.preventDefault
- let username = (evt->ReactEvent.Form.target)["value"]
- setUsername(_prev => username)
- }}
- />
-
-
-
-
+
}
}
+
`
}
@@ -1700,10 +1744,12 @@ let make = (~versions: array
) => {
"flex-1 items-center p-4 border-t-4 border-transparent " ++ activeClass
}
- let tabs = [JavaScript, Problems, Settings]
+ let tabs = [JavaScript, RenderOutput, Console, Problems, Settings]
let headers = Belt.Array.mapWithIndex(tabs, (i, tab) => {
let title = switch tab {
+ | RenderOutput => "Render Output"
+ | Console => "Console"
| JavaScript => "JavaScript"
| Problems => "Problems"
| Settings => "Settings"
diff --git a/src/bindings/Webapi.res b/src/bindings/Webapi.res
index ff04069a5..7380b7cda 100644
--- a/src/bindings/Webapi.res
+++ b/src/bindings/Webapi.res
@@ -1,4 +1,5 @@
module Document = {
+ @val external document: Dom.element = "document"
@scope("document") @val external createElement: string => Dom.element = "createElement"
@scope("document") @val external createTextNode: string => Dom.element = "createTextNode"
}
@@ -15,6 +16,14 @@ module Element = {
@get external classList: Dom.element => ClassList.t = "classList"
@send external getBoundingClientRect: Dom.element => {..} = "getBoundingClientRect"
+ @send
+ external getElementById: (Dom.element, string) => Js.nullable = "getElementById"
+
+ type contentWindow
+ @get external contentWindow: Dom.element => option = "contentWindow"
+
+ @send external postMessage: (contentWindow, string, string) => unit = "postMessage"
+
module Style = {
@scope("style") @set external width: (Dom.element, string) => unit = "width"
@scope("style") @set external height: (Dom.element, string) => unit = "height"
diff --git a/src/common/RenderOutputManager.res b/src/common/RenderOutputManager.res
new file mode 100644
index 000000000..7f80a627f
--- /dev/null
+++ b/src/common/RenderOutputManager.res
@@ -0,0 +1,94 @@
+module AcornParse = {
+ type t
+ @module("../ffi/acorn-parse.js") external parse: string => t = "parse"
+
+ @module("../ffi/acorn-parse.js") external hasEntryPoint: t => bool = "hasEntryPoint"
+
+ @module("../ffi/acorn-parse.js")
+ external removeImportsAndExports: t => string = "removeImportsAndExports"
+}
+
+module Transpiler = {
+ let run = code =>
+ `(function () {
+ ${code}
+ const root = document.getElementById("root");
+ ReactDOM.render(App.make(), root);
+})();`
+}
+
+module Frame = {
+ let css = `body {
+ background-color: inherit;
+ color: CanvasText;
+ color-scheme: light dark;
+}`
+
+ let srcdoc = `
+
+
+
+ Playground Output
+
+
+
+
+
+
+
+
+
+ `
+
+ let sendOutput = code => {
+ open Webapi
+
+ let frame =
+ Document.document
+ ->Element.getElementById("iframe-eval")
+ ->Js.Nullable.toOption
+
+ switch frame {
+ | Some(element) =>
+ switch element->Element.contentWindow {
+ | Some(win) => win->Element.postMessage(code, "*")
+ | None => ()
+ }
+ | None => ()
+ }
+ }
+}
+
+let renderOutput = code => {
+ let ast = AcornParse.parse(code)
+ let transpiled = AcornParse.removeImportsAndExports(ast)
+ switch AcornParse.hasEntryPoint(ast) {
+ | true =>
+ Transpiler.run(transpiled)->Frame.sendOutput
+ Ok()
+ | false => Error()
+ }
+}
diff --git a/src/ffi/acorn-parse.js b/src/ffi/acorn-parse.js
new file mode 100644
index 000000000..638ac211c
--- /dev/null
+++ b/src/ffi/acorn-parse.js
@@ -0,0 +1,62 @@
+import * as acorn from "acorn";
+import { walk } from "estree-walker";
+import * as escodegen from "escodegen";
+
+export function hasEntryPoint(ast) {
+ let existsApp = false;
+
+ walk(ast, {
+ enter(node) {
+ const isAppVar = node?.type === "VariableDeclaration" &&
+ node?.declarations[0]?.type === "VariableDeclarator" &&
+ node?.declarations[0]?.id.name === "App" &&
+ node?.declarations[0]?.id.type === "Identifier";
+
+ if (isAppVar) {
+ const isObject = node?.declarations[0].init.type === "ObjectExpression"
+ if (isObject) {
+ const hasMake = [...node?.declarations[0].init.properties].some(
+ p => p?.type === "Property" && p?.key?.type === "Identifier" && p?.key?.name === "make"
+ )
+ existsApp = hasMake
+ }
+ }
+ }
+ })
+
+ return existsApp
+}
+
+export function parse(code) {
+ return acorn.parse(code, {
+ ecmaVersion: 9,
+ sourceType: "module"
+ });
+}
+
+export function removeImportsAndExports(ast) {
+ walk(ast, {
+ enter(node) {
+ const isImport =
+ node?.type === "ImportDeclaration" ||
+ (node?.type === "VariableDeclaration" &&
+ node?.declarations[0]?.init?.type === "CallExpression" &&
+ node?.declarations[0]?.init?.callee?.name === "require");
+ const isExport =
+ node?.type === "ExportDefaultDeclaration" ||
+ node?.type === "ExportNamedDeclaration" ||
+ node?.type === "ExportAllDeclaration" ||
+ (node?.type === "ExpressionStatement" &&
+ node?.expression?.type === "AssignmentExpression" &&
+ node?.expression?.operator === "=" &&
+ (node?.expression?.left?.object?.name === "exports" ||
+ (node?.expression?.left?.object?.name === "module" &&
+ node?.expression?.left?.property?.name === "exports")));
+ if (isImport || isExport) {
+ this.remove();
+ }
+ },
+ });
+
+ return escodegen.generate(ast);
+}