Skip to content

load stdlib runtime in playground #964

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 1 commit into from
Jan 30, 2025
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
},
"scripts": {
"dev": "next",
"res:watch": "rescript build -w",
"build": "rescript && npm run update-index && next build",
"test": "node scripts/test-examples.mjs && node scripts/test-hrefs.mjs",
"reanalyze": "reanalyze -all-cmt .",
Expand Down
12 changes: 9 additions & 3 deletions src/RenderPanel.res
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ let make = (~compilerState: CompilerManagerHook.state, ~clearLogs, ~runOutput) =
React.useEffect(() => {
if runOutput {
switch compilerState {
| CompilerManagerHook.Ready({result: Comp(Success({js_code}))}) =>
| CompilerManagerHook.Ready({selected, result: Comp(Success({js_code}))}) =>
clearLogs()
open Babel

let ast = Parser.parse(js_code, {sourceType: "module"})
let {entryPointExists, code} = PlaygroundValidator.validate(ast)
let {entryPointExists, code, imports} = PlaygroundValidator.validate(ast)
let imports = imports->Dict.mapValues(path => {
let filename = path->String.sliceToEnd(~start=9) // the part after "./stdlib/"
CompilerManagerHook.CdnMeta.getStdlibRuntimeUrl(selected.id, filename)
})

entryPointExists ? code->wrapReactApp->EvalIFrame.sendOutput : EvalIFrame.sendOutput(code)
entryPointExists
? code->wrapReactApp->EvalIFrame.sendOutput(imports)
: EvalIFrame.sendOutput(code, imports)
setValidReact(_ => entryPointExists)
| _ => ()
}
Expand Down
84 changes: 66 additions & 18 deletions src/bindings/Babel.res
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,48 @@ module Ast = {
@tag("type")
type expression = ObjectExpression({properties: array<objectProperties>})

type variableDeclarator = {
@as("type") type_: string,
id: lval,
init?: Null.t<expression>,
module VariableDeclarator = {
@tag("type")
type t = VariableDeclarator({id: lval, init?: Null.t<expression>})
}
module Specifier = {
@tag("type")
type t =
| ImportSpecifier({local: lval})
| ImportDefaultSpecifier({local: lval})
| ImportNamespaceSpecifier({local: lval})
}

module StringLiteral = {
@tag("type")
type t = StringLiteral({value: string})
}

module VariableDeclaration = {
@tag("type")
type t = VariableDeclaration({kind: string, declarations: array<VariableDeclarator.t>})
}

module ImportDeclaration = {
@tag("type")
type t = ImportDeclaration({specifiers: array<Specifier.t>, source: StringLiteral.t})
}

module Identifier = {
@tag("type")
type t = Identifier({mutable name: string})
}

@tag("type")
type node = VariableDeclaration({kind: string, declarations: array<variableDeclarator>})
type nodePath = {node: node}
type node =
| ...StringLiteral.t
| ...Specifier.t
| ...VariableDeclarator.t
| ...VariableDeclaration.t
| ...ImportDeclaration.t
| ...Identifier.t

type nodePath<'nodeType> = {node: 'nodeType}
}

module Parser = {
Expand All @@ -30,7 +64,7 @@ module Traverse = {
}

module Generator = {
@send external remove: Ast.nodePath => unit = "remove"
@send external remove: Ast.nodePath<'nodeType> => unit = "remove"

type t = {code: string}
@module("@babel/generator") external generator: Ast.t => t = "default"
Expand All @@ -40,26 +74,42 @@ module PlaygroundValidator = {
type validator = {
entryPointExists: bool,
code: string,
imports: Dict.t<string>,
}

let validate = ast => {
let entryPoint = ref(false)
let imports = Dict.make()

let remove = nodePath => Generator.remove(nodePath)
Traverse.traverse(
ast,
{
"ImportDeclaration": remove,
"ImportDeclaration": (
{
node: ImportDeclaration({specifiers, source: StringLiteral({value: source})}),
} as nodePath: Ast.nodePath<Ast.ImportDeclaration.t>,
) => {
if source->String.startsWith("./stdlib") {
switch specifiers {
| [ImportNamespaceSpecifier({local: Identifier({name})})] =>
imports->Dict.set(name, source)
| _ => ()
}
}
remove(nodePath)
},
"ExportNamedDeclaration": remove,
"VariableDeclaration": (nodePath: Ast.nodePath) => {
switch nodePath.node {
| VariableDeclaration({declarations}) if Array.length(declarations) > 0 =>
"VariableDeclaration": (
{node: VariableDeclaration({declarations})}: Ast.nodePath<Ast.VariableDeclaration.t>,
) => {
if Array.length(declarations) > 0 {
let firstDeclaration = Array.getUnsafe(declarations, 0)

switch (firstDeclaration.id, firstDeclaration.init) {
| (Identifier({name}), Some(init)) if name === "App" =>
switch init->Null.toOption {
| Some(ObjectExpression({properties})) =>
switch firstDeclaration {
| VariableDeclarator({id: Identifier({name}), init}) if name === "App" =>
switch init {
| Value(ObjectExpression({properties})) =>
let foundEntryPoint = properties->Array.find(property => {
switch property {
| ObjectProperty({
Expand All @@ -74,12 +124,10 @@ module PlaygroundValidator = {
}
| _ => ()
}
| _ => ()
}
},
},
)

{entryPointExists: entryPoint.contents, code: Generator.generator(ast).code}
{entryPointExists: entryPoint.contents, imports, code: Generator.generator(ast).code}
}
}
3 changes: 3 additions & 0 deletions src/bindings/Webapi.res
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ module Element = {
@send
external postMessage: (contentWindow, string, ~targetOrigin: string=?) => unit = "postMessage"

@send
external postMessageAny: (contentWindow, 'a, ~targetOrigin: 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"
Expand Down
3 changes: 3 additions & 0 deletions src/common/CompilerManagerHook.res
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ module CdnMeta = {

let getLibraryCmijUrl = (version, libraryName: string): string =>
`https://cdn.rescript-lang.org/${Semver.toString(version)}/${libraryName}/cmij.js`

let getStdlibRuntimeUrl = (version, filename) =>
`https://cdn.rescript-lang.org/${Semver.toString(version)}/compiler-builtins/stdlib/${filename}`
}

module FinalResult = {
Expand Down
4 changes: 4 additions & 0 deletions src/common/CompilerManagerHook.resi
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type ready = {
result: FinalResult.t,
}

module CdnMeta: {
let getStdlibRuntimeUrl: (Semver.t, string) => string
}

type state =
| Init
| SetupFailed(string)
Expand Down
19 changes: 13 additions & 6 deletions src/common/EvalIFrame.res
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ let srcDoc = `
<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"
Expand All @@ -36,11 +35,14 @@ let srcDoc = `
window.JsxRuntime = JsxRuntime;
</script>
<script>
window.addEventListener("message", (event) => {
window.addEventListener("message", async (event) => {
try {
// https://rollupjs.org/troubleshooting/#avoiding-eval
const eval2 = eval;
eval2(event.data);
const imports = {};
for (const [key, path] of Object.entries(event.data.imports)) {
imports[key] = await import(path);
}
(Function(...Object.keys(imports), event.data.code))(...Object.values(imports));
} catch (err) {
console.error(err);
}
Expand All @@ -67,15 +69,20 @@ let srcDoc = `
</html>
`

let sendOutput = code => {
type message = {
code: string,
imports: Dict.t<string>,
}

let sendOutput = (code, imports) => {
open Webapi

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

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