Skip to content

Commit

Permalink
load stdlib runtime in playground (#964)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsnobip authored Jan 30, 2025
1 parent fc636af commit a71ae31
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 27 deletions.
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

0 comments on commit a71ae31

Please sign in to comment.