Skip to content

Commit

Permalink
chore(playground): migrate runner iframe to web component
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoMcA committed Dec 10, 2024
1 parent 4e9c442 commit 3741838
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 74 deletions.
3 changes: 2 additions & 1 deletion client/src/playground/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ main.play {
}
}

iframe {
play-runner {
border: 1px solid var(--border-primary);
height: 100%;
width: 100%;
}
Expand Down
89 changes: 17 additions & 72 deletions client/src/playground/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,14 @@ import prettierPluginHTML from "prettier/plugins/html";
import { Button } from "../ui/atoms/button";
import Editor, { EditorHandle } from "./editor";
import { SidePlacement } from "../ui/organisms/placement";
import {
compressAndBase64Encode,
decompressFromBase64,
EditorContent,
SESSION_KEY,
} from "./utils";
import { decompressFromBase64, EditorContent, SESSION_KEY } from "./utils";

import "./index.scss";
import { PLAYGROUND_BASE_HOST } from "../env";
import { FlagForm, ShareForm } from "./forms";
import { Console, VConsole } from "./console";
import { useGleanClick } from "../telemetry/glean-context";
import { PLAYGROUND } from "../telemetry/constants";
import { ReactPlayRunner } from "./runner";

const HTML_DEFAULT = "";
const CSS_DEFAULT = "";
Expand Down Expand Up @@ -79,14 +74,12 @@ export default function Playground() {
let [shareUrl, setShareUrl] = useState<URL | null>(null);
let [vConsole, setVConsole] = useState<VConsole[]>([]);
let [state, setState] = useState(State.initial);
const [code, setCode] = useState<EditorContent>();
let [codeSrc, setCodeSrc] = useState<string | undefined>();
let [iframeSrc, setIframeSrc] = useState("about:blank");
const [isEmpty, setIsEmpty] = useState<boolean>(true);
const subdomain = useRef<string>(crypto.randomUUID());
const [initialContent, setInitialContent] = useState<EditorContent | null>(
null
);
const [flipFlop, setFlipFlop] = useState(0);
let { data: initialCode } = useSWRImmutable<EditorContent>(
!stateParam && !shared && gistId
? `/api/v1/play/${encodeURIComponent(gistId)}`
Expand Down Expand Up @@ -118,34 +111,11 @@ export default function Playground() {
const htmlRef = useRef<EditorHandle | null>(null);
const cssRef = useRef<EditorHandle | null>(null);
const jsRef = useRef<EditorHandle | null>(null);
const iframe = useRef<HTMLIFrameElement | null>(null);
const diaRef = useRef<HTMLDialogElement | null>(null);

const updateWithCode = useCallback(
async (code: EditorContent) => {
const { state } = await compressAndBase64Encode(JSON.stringify(code));

// We're using a random subdomain for origin isolation.
const url = new URL(
window.location.hostname.endsWith("localhost")
? window.location.origin
: `${window.location.protocol}//${
PLAYGROUND_BASE_HOST.startsWith("localhost")
? ""
: `${subdomain.current}.`
}${PLAYGROUND_BASE_HOST}`
);
setVConsole([]);
url.searchParams.set("state", state);
// ensure iframe reloads even if code doesn't change
url.searchParams.set("f", flipFlop.toString());
url.pathname = `${codeSrc || code.src || ""}/runner.html`;
setIframeSrc(url.href);
// using an updater function causes the second "run" to not reload properly:
setFlipFlop((flipFlop + 1) % 2);
},
[codeSrc, setVConsole, setIframeSrc, flipFlop, setFlipFlop]
);
useEffect(() => {
setVConsole([]);
}, [code, setVConsole]);

useEffect(() => {
if (initialCode) {
Expand All @@ -167,26 +137,9 @@ export default function Playground() {
return code;
}, [initialContent?.src, initialCode?.src]);

let messageListener = useCallback(({ data: { typ, prop, message } }) => {
if (typ === "console") {
if (
(prop === "log" || prop === "error" || prop === "warn") &&
typeof message === "string"
) {
setVConsole((vConsole) => [...vConsole, { prop, message }]);
} else {
const warning = "[Playground] Unsupported console message";
setVConsole((vConsole) => [
...vConsole,
{
prop: "warn",
message: `${warning} (see browser console)`,
},
]);
console.warn(warning, { prop, message });
}
}
}, []);
const onConsole = ({ detail }: CustomEvent<VConsole>) => {
setVConsole((vConsole) => [...vConsole, detail]);
};

const setEditorContent = ({ html, css, js, src }: EditorContent) => {
htmlRef.current?.setContent(html);
Expand All @@ -205,7 +158,7 @@ export default function Playground() {
setEditorContent(initialCode);
if (!gistId) {
// don't auto run shared code
updateWithCode(initialCode);
setCode(initialCode);
}
} else if (stateParam) {
try {
Expand All @@ -225,14 +178,7 @@ export default function Playground() {
setState(State.ready);
}
})();
}, [initialCode, state, gistId, stateParam, updateWithCode]);

useEffect(() => {
window.addEventListener("message", messageListener);
return () => {
window.removeEventListener("message", messageListener);
};
}, [messageListener]);
}, [initialCode, state, gistId, stateParam, setCode]);

const clear = async () => {
setSearchParams([], { replace: true });
Expand Down Expand Up @@ -284,7 +230,7 @@ export default function Playground() {
iterations: 1,
};
document.getElementById("run")?.firstElementChild?.animate(loading, timing);
updateWithCode({ html, css, js, src });
setCode({ html, css, js, src });
};

const format = async () => {
Expand Down Expand Up @@ -414,12 +360,11 @@ export default function Playground() {
Seeing something inappropriate?
</button>
)}
<iframe
title="runner"
ref={iframe}
src={iframeSrc}
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
<ReactPlayRunner
code={code}
srcPrefix={codeSrc}
onConsole={onConsole}
/>
<Console vConsole={vConsole} />
<SidePlacement extraClasses={["horizontal"]} />
</section>
Expand Down
121 changes: 121 additions & 0 deletions client/src/playground/runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { html, LitElement } from "lit";
import { compressAndBase64Encode } from "./utils.ts";
import { PLAYGROUND_BASE_HOST } from "../env.ts";
import { createComponent } from "@lit/react";
import { Task } from "@lit/task";
import React from "react";

import styles from "./runner.scss?css" with { type: "css" };

/** @import { EditorContent } from "./utils.ts" */
/** @import { VConsole } from "./console.tsx" */
/** @import { EventName } from "@lit/react" */

export class PlayRunner extends LitElement {
static properties = {
code: { type: Object },
srcPrefix: { type: String, attribute: "src-prefix" },
_src: { state: true },
};

static styles = styles;

constructor() {
super();
/** @type {EditorContent | undefined} */
this.code = undefined;
/** @type {string | undefined} */
this.srcPrefix = undefined;
this._src = "about:blank";

this._subdomain = crypto.randomUUID();
this._flipFlop = 0;
}

/** @param {MessageEvent} e */
_onMessage({ data: { typ, prop, message } }) {
if (typ === "console") {
if (
(prop === "log" || prop === "error" || prop === "warn") &&
typeof message === "string"
) {
/** @type {VConsole} */
const detail = { prop, message };
this.dispatchEvent(
new CustomEvent("console", { bubbles: true, composed: true, detail })
);
} else {
const warning = "[Playground] Unsupported console message";
/** @type {VConsole} */
const detail = {
prop: "warn",
message: `${warning} (see browser console)`,
};
this.dispatchEvent(
new CustomEvent("console", { bubbles: true, composed: true, detail })
);
console.warn(warning, { prop, message });
}
}
}

_updateSrc = new Task(this, {
args: () => /** @type {const} */ ([this.code, this.srcPrefix]),
task: async ([code, srcPrefix], { signal }) => {
if (code) {
const { state } = await compressAndBase64Encode(JSON.stringify(code));
signal.throwIfAborted();
// We're using a random subdomain for origin isolation.
const url = new URL(
window.location.hostname.endsWith("localhost")
? window.location.origin
: `${window.location.protocol}//${
PLAYGROUND_BASE_HOST.startsWith("localhost")
? ""
: `${this._subdomain}.`
}${PLAYGROUND_BASE_HOST}`
);
url.searchParams.set("state", state);
// ensure iframe reloads even if code doesn't change
url.searchParams.set("f", this._flipFlop.toString());
url.pathname = `${srcPrefix || code.src || ""}/runner.html`;
this._src = url.href;
this._flipFlop = (this._flipFlop + 1) % 2;
} else {
this._src = "about:blank";
}
},
});

connectedCallback() {
super.connectedCallback();
this._onMessage = this._onMessage.bind(this);
window.addEventListener("message", this._onMessage);
}

render() {
return html`
<iframe
title="runner"
src=${this._src}
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
`;
}

disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("message", this._onMessage);
}
}

customElements.define("play-runner", PlayRunner);

export const ReactPlayRunner = createComponent({
tagName: "play-runner",
elementClass: PlayRunner,
react: React,
events: {
onConsole: /** @type {EventName<CustomEvent<VConsole>>} */ ("console"),
},
});
5 changes: 5 additions & 0 deletions client/src/playground/runner.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
iframe {
border: none;
height: 100%;
width: 100%;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"@fast-csv/parse": "^5.0.2",
"@inquirer/prompts": "^7.2.0",
"@lit/react": "^1.0.6",
"@lit/task": "^1.0.1",
"@mdn/bcd-utils-api": "^0.0.7",
"@mdn/browser-compat-data": "^5.6.22",
"@mdn/rari": "^0.1.0",
Expand Down
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2183,13 +2183,20 @@
resolved "https://registry.yarnpkg.com/@lit/react/-/react-1.0.6.tgz#9518ba471157becd1a3e6fb7ddc16bcef16be64e"
integrity sha512-QIss8MPh6qUoFJmuaF4dSHts3qCsA36S3HcOLiNPShxhgYPr4XJRnCBKPipk85sR9xr6TQrOcDMfexwbNdJHYA==

"@lit/reactive-element@^2.0.4":
"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b"
integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==
dependencies:
"@lit-labs/ssr-dom-shim" "^1.2.0"

"@lit/task@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@lit/task/-/task-1.0.1.tgz#7462aeaa973766822567f5ca90fe157404e8eb81"
integrity sha512-fVLDtmwCau8NywnFIXaJxsCZjzaIxnVq+cFRKYC1Y4tA4/0rMTvF6DLZZ2JE51BwzOluaKtgJX8x1QDsQtAaIw==
dependencies:
"@lit/reactive-element" "^1.0.0 || ^2.0.0"

"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
Expand Down

0 comments on commit 3741838

Please sign in to comment.