Skip to content
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

chore(playground): migrate runner iframe to web component #12283

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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
Loading