From 205232cd6d968750c8c4daff58bbb9395ae981d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Silvani?= Date: Wed, 3 Apr 2024 19:49:36 -0300 Subject: [PATCH] Sandbox web languages using iframes (#268) Fixes #239 Load web libraries in separate pages and embed them using iframes. This way, these libraries do not pollute the global Window from the session page and there are no collisions between the globals each library define. Incidentally, this also fixes #193: Because the web language libraries are only loaded inside the iframe and thus in a separate JS context, when unloading the Iframe DOM element, sound and visuals stop. Strudel also adds some extensions to the Codemirror editor and has an animation frame to update decorators. For some reason, these updates need to run in the same context as the editor, and can't be modified from within the Iframe page. --- packages/web/src/components/editor.tsx | 31 ++--- .../web/src/components/web-target-iframe.tsx | 47 +++++++ .../web/src/hooks/use-animation-frame.tsx | 21 +++ packages/web/src/hooks/use-eval-handler.tsx | 21 +++ packages/web/src/hooks/use-hydra.tsx | 40 ------ packages/web/src/hooks/use-mercury.tsx | 25 ---- .../use-strudel-codemirror-extensions.ts | 31 +++++ packages/web/src/hooks/use-strudel.tsx | 34 ----- packages/web/src/lib/hydra-wrapper.ts | 1 - packages/web/src/lib/strudel-wrapper.ts | 124 ++++++++---------- packages/web/src/lib/utils.ts | 51 ++++++- packages/web/src/main.tsx | 9 +- packages/web/src/routes/frames/hydra.tsx | 54 ++++++++ .../web/src/routes/frames/mercury-web.tsx | 36 +++++ packages/web/src/routes/frames/strudel.tsx | 37 ++++++ packages/web/src/routes/session.tsx | 112 ++++++++-------- 16 files changed, 419 insertions(+), 255 deletions(-) create mode 100644 packages/web/src/components/web-target-iframe.tsx create mode 100644 packages/web/src/hooks/use-animation-frame.tsx create mode 100644 packages/web/src/hooks/use-eval-handler.tsx delete mode 100644 packages/web/src/hooks/use-hydra.tsx delete mode 100644 packages/web/src/hooks/use-mercury.tsx create mode 100644 packages/web/src/hooks/use-strudel-codemirror-extensions.ts delete mode 100644 packages/web/src/hooks/use-strudel.tsx create mode 100644 packages/web/src/routes/frames/hydra.tsx create mode 100644 packages/web/src/routes/frames/mercury-web.tsx create mode 100644 packages/web/src/routes/frames/strudel.tsx diff --git a/packages/web/src/components/editor.tsx b/packages/web/src/components/editor.tsx index 7e5f82a5..8b67ed46 100644 --- a/packages/web/src/components/editor.tsx +++ b/packages/web/src/components/editor.tsx @@ -7,11 +7,12 @@ import { } from "@/settings.json"; import { javascript } from "@codemirror/lang-javascript"; import { python } from "@codemirror/lang-python"; -import { EditorState, Prec, Compartment, Extension } from "@codemirror/state"; +import { Compartment, EditorState, Extension, Prec } from "@codemirror/state"; import { EditorView, keymap, lineNumbers } from "@codemirror/view"; import { evalKeymap, flashField, remoteEvalFlash } from "@flok-editor/cm-eval"; import { tidal } from "@flok-editor/lang-tidal"; import type { Document } from "@flok-editor/session"; +import { highlightExtension } from "@strudel/codemirror"; import CodeMirror, { ReactCodeMirrorProps, ReactCodeMirrorRef, @@ -19,7 +20,6 @@ import CodeMirror, { import React, { useEffect, useState } from "react"; import { yCollab } from "y-codemirror.next"; import { UndoManager } from "yjs"; -import { highlightExtension } from '@strudel/codemirror'; const defaultLanguage = "javascript"; const langByTarget = langByTargetUntyped as { [lang: string]: string }; @@ -121,28 +121,25 @@ const flokSetup = ( ]; }; -export interface EditorProps extends ReactCodeMirrorProps { - document?: Document; -} - // Code example from: // https://codemirror.net/examples/config/#dynamic-configuration // Allows toggling of extensions based on string shortkey -// +// const toggleWith = (key: string, extension: Extension) => { - let comp = new Compartment; + let comp = new Compartment(); function toggle(view: EditorView) { let on = comp.get(view.state) == extension; view.dispatch({ - effects: comp.reconfigure(on ? [] : extension) - }) + effects: comp.reconfigure(on ? [] : extension), + }); return true; } - return [ - comp.of([]), - keymap.of([{key, run: toggle}]) - ] + return [comp.of([]), keymap.of([{ key, run: toggle }])]; +}; + +export interface EditorProps extends ReactCodeMirrorProps { + document?: Document; } export const Editor = React.forwardRef( @@ -175,10 +172,8 @@ export const Editor = React.forwardRef( languageExtension(), highlightExtension, readOnly ? EditorState.readOnly.of(true) : [], - // toggle linenumbers on/off - toggleWith('shift-ctrl-l', lineNumbers()), - // toggle linewrapping on/off - toggleWith('shift-ctrl-w', EditorView.lineWrapping) + toggleWith("shift-ctrl-l", lineNumbers()), // toggle linenumbers on/off + toggleWith("shift-ctrl-w", EditorView.lineWrapping), // toggle linewrapping on/off ]; // If it's read-only, put a div in front of the editor so that the user diff --git a/packages/web/src/components/web-target-iframe.tsx b/packages/web/src/components/web-target-iframe.tsx new file mode 100644 index 00000000..2341c25b --- /dev/null +++ b/packages/web/src/components/web-target-iframe.tsx @@ -0,0 +1,47 @@ +import { useQuery } from "@/hooks/use-query"; +import { EvalMessage, Session } from "@flok-editor/session"; +import { useEffect, useRef } from "react"; + +export interface WebTargetIframeProps { + target: string; + session: Session | null; +} + +export const WebTargetIframe = ({ target, session }: WebTargetIframeProps) => { + const ref = useRef(null); + + const query = useQuery(); + const noWebEval = query.get("noWebEval")?.split(",") || []; + + // Check if we should load the target + if (noWebEval.includes(target) || noWebEval.includes("*")) { + return null; + } + + // Handle evaluation messages from session + useEffect(() => { + if (!session || !ref.current) return; + + const handler = (msg: EvalMessage) => { + const payload = { + type: "eval", + body: msg, + }; + ref.current?.contentWindow?.postMessage(payload, "*"); + }; + + session.on(`eval:${target}`, handler); + + return () => { + session.off(`eval:${target}`, handler); + }; + }, [session, ref]); + + return ( +