abstract repl interaction code

pull/1979/head
David Pershouse 6 years ago
parent 2c537e7cb4
commit 5eafdd446f

@ -1,6 +1,7 @@
<script>
import { onMount, createEventDispatcher } from 'svelte';
import getLocationFromStack from '../../_utils/getLocationFromStack.js';
import ReplProxy from '../../_utils/replProxy.js';
import { decode } from 'sourcemap-codec';
const dispatch = createEventDispatcher();
@ -14,34 +15,18 @@
export let error;
export function setProp(prop, value) {
if (!refs.child) return;
refs.child.contentWindow.postMessage({
action: 'set_prop',
args: {
prop: prop,
value: value
}
},'*');
}
if (!replProxy) return;
replProxy.setProp(prop, value);
}
let component;
let hasComponent = false;
const refs = {};
const importCache = {};
let pendingImports = 0;
let pending = false;
function fetchImport(id, curl) {
return new Promise((fulfil, reject) => {
curl([`https://bundle.run/${id}`]).then(module => {
importCache[id] = module;
fulfil(module);
}, err => {
console.error(err.stack);
reject(new Error(`Error loading ${id} from bundle.run`));
});
});
}
let replProxy = null;
const namespaceSpecifier = /\*\s+as\s+(\w+)/;
const namedSpecifiers = /\{(.+)\}/;
@ -82,115 +67,24 @@
let createComponent;
let init;
onMount(() => {
replProxy = new ReplProxy(refs.child);
refs.child.addEventListener('load', () => {
const iframe = refs.child;
let evalId = 1;
let fetchId = 1;
let fetchHandler = null;
let pendingResults = new Map();
const evalInIframe = function(scriptToEval) {
let id = evalId++;
let promise = new Promise((resolve,reject) => {
iframe.contentWindow.postMessage({
action: "eval",
args: {
evalId: id,
script: scriptToEval
}
}, '*')
pendingResults.set(id, { resolve: resolve, reject: reject });
});
return promise;
}
const handleReplMessage = (ev) => {
console.log("got message in parent", ev.data);
let action = ev.data.action;
if (action == "evalError") {
let { message, stack, evalId } = ev.data.args;
let e = new Error(message);
e.stack = e.stack;
let resultHandler = pendingResults.get(evalId);
if (resultHandler) {
pendingResults.delete(evalId);
resultHandler.reject(e);
} else {
console.err("error evaluating script in iframe", e);
}
}
if (action == "evalOk") {
let { evalId } = ev.data.args;
let resultHandler = pendingResults.get(evalId);
if (resultHandler) {
pendingResults.delete(evalId);
resultHandler.resolve();
}
}
if (action == "prop_update") {
let { prop, value } = ev.data.args;
dispatch('binding', { prop, value });
values_store.update(values => Object.assign({}, values, {
[prop]: value
}));
}
replProxy.onPropUpdate = (prop, value) => {
dispatch('binding', { prop, value });
values_store.update(values => Object.assign({}, values, {
[prop]: value
}));
};
if (action == "fetch_complete") {
console.log(fetchHandler, ev.data.args);
if (fetchHandler && fetchHandler.fetchId == ev.data.args.fetchId) {
fetchHandler.resolve()
fetchHandler = null;
}
}
if (action == "fetch_error") {
if (fetchHandler && fetchHandler.fetchId == ev.data.args.fetchId) {
fetchHandler.reject(new Error(ev.data.args.message));
fetchHandler = null;
}
}
if (action == "fetch_progress") {
if (fetchHandler && fetchHandler.fetchId == ev.data.args.fetchId) {
pendingImports = ev.data.args.remaining;
}
}
replProxy.onFetchProgress = (progress) => {
pendingImports = progress
}
window.addEventListener("message", handleReplMessage, false);
replProxy.handleLinks();
// iframe.contentWindow.eval;
/* TODO
// intercept links, so that we can use #hashes inside the iframe
body.addEventListener('click', event => {
if (event.which !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
// ensure target is a link
let el = event.target;
while (el && el.nodeName !== 'A') el = el.parentNode;
if (!el || el.nodeName !== 'A') return;
if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
event.preventDefault();
if (el.href.startsWith(top.location.origin)) {
const url = new URL(el.href);
if (url.hash[0] === '#') {
iframe.contentWindow.location.hash = url.hash;
return;
}
}
window.open(el.href, '_blank');
});
*/
let promise = null;
let updating = false;
@ -199,26 +93,26 @@
const init = () => {
if (sourceError) return;
const imports = [];
const missingImports = bundle.imports.filter(x => !importCache[x]);
const removeStyles = () => {
evalInIframe(`
replProxy.eval(`
const styles = document.querySelectorAll('style.svelte');
let i = styles.length;
while (i--) styles[i].parentNode.removeChild(styles[i]);
`)
};
const destroyComponent = () => {
replProxy.eval(`if (window.component)
window.component.\$destroy();
window.component = null`);
};
const ready = () => {
error = null;
if (toDestroy) {
removeStyles();
toDestroy.$destroy();
destroyComponent();
toDestroy = null;
}
@ -229,11 +123,10 @@
pending = false;
createComponent();
}
};
}
const createHtml = () => {
evalInIframe(`${ssr.code}
replProxy.eval(`${ssr.code}
var rendered = SvelteComponent.render(${JSON.stringify($values_store)});
if (rendered.css.code) {
@ -244,23 +137,23 @@
}
document.body.innerHTML = rendered.html;
`).catch( e => {
const loc = getLocationFromStack(e.stack, ssr.map);
if (loc) {
e.filename = loc.source;
e.loc = { line: loc.line, column: loc.column };
}
`)
.catch( e => {
const loc = getLocationFromStack(e.stack, ssr.map);
if (loc) {
e.filename = loc.source;
e.loc = { line: loc.line, column: loc.column };
}
error = e;
});
};
error = e;
});
};
const createComponent = () => {
// remove leftover styles from SSR renderer
if (ssr) removeStyles();
evalInIframe(`${dom.code}
replProxy.eval(`${dom.code}
document.body.innerHTML = '';
window.location.hash = '';
window._svelteTransitionManager = null;
@ -268,11 +161,11 @@
window.component = new SvelteComponent({
target: document.body,
props: ${JSON.stringify($values_store)}
});`)
.catch(e=> {
});
`)
.catch(e => {
// TODO show in UI
component = null;
hasComponent = false;
const loc = getLocationFromStack(e.stack, dom.map);
if (loc) {
@ -284,28 +177,20 @@
});
};
new Promise((resolve, reject)=> {
fetchHandler = {
fetchId: fetchId++,
resolve: resolve,
reject: reject
}
iframe.contentWindow.postMessage({
action: "fetch_imports",
args: {
bundle: bundle,
fetchId: fetchHandler.fetchId
}
}, '*');
})
.then(() => {
ready();
})
.catch(e => {
error = e;
});
// Download the imports (sets them on iframe window when complete)
{
let cancelled = false;
promise = replProxy.fetchImports(bundle);
promise.cancel = () => { cancelled = true };
promise.then(() => {
if (cancelled) return;
ready()
}).catch(e => {
if (cancelled) return;
error = e;
});
}
run = () => {
pending = false;
@ -316,21 +201,16 @@
bundle_handler = bundle => {
if (!bundle) return; // TODO can this ever happen?
if (fetchHandler) fetchHandler = null;
if (promise) promise.cancel();
toDestroy = component;
component = null;
toDestroy = hasComponent;
hasComponent = false;
init();
};
props_handler = props => {
iframe.contentWindow.postMessage({
action:"bind_props",
args: {
props: [...props]
}
},'*')
replProxy.bindProps(props)
};
});
});
@ -399,7 +279,7 @@
</style>
<div class="iframe-container">
<iframe title="Result" bind:this={refs.child} sandbox="allow-scripts" class="{error || pending || pendingImports ? 'greyed-out' : ''}" srcdoc='
<iframe title="Result" bind:this={refs.child} sandbox="allow-scripts allow-popups" class="{error || pending || pendingImports ? 'greyed-out' : ''}" srcdoc='
<!doctype html>
<html>
<head>

@ -0,0 +1,87 @@
export default class ReplProxy {
constructor(iframe) {
this.iframe = iframe;
this.cmdId = 1;
this.pendingCmds = new Map();
this.onPropUpdate = null;
this.onFetchProgress = null;
window.addEventListener("message", ev => this.handleReplMessage(ev), false);
}
iframeCommand(command, args) {
return new Promise( (resolve, reject) => {
this.cmdId = this.cmdId + 1;
this.iframe.contentWindow.postMessage({
action: command,
cmdId: this.cmdId,
args: args
}, '*')
this.pendingCmds.set(this.cmdId, { resolve: resolve, reject: reject });
});
}
handleCommandMessage(cmdData) {
let action = cmdData.action;
let id = cmdData.cmdId;
let handler = this.pendingCmds.get(id);
if (handler) {
this.pendingCmds.delete(id);
if (action == "cmdError") {
let { message, stack } = cmdData;
let e = new Error(message);
e.stack = stack;
console.log("cmd fail");
handler.reject(e)
}
if (action == "cmdOk") {
console.log("cmd okay");
handler.resolve(cmdData.args)
}
} else {
console.error("command not found", id);
}
}
handleReplMessage(ev) {
let action = ev.data.action;
if ( action == "cmdError" || action == "cmdOk" ) {
this.handleCommandMessage(ev.data);
}
if (action == "prop_update") {
let { prop, value } = ev.data.args;
if (this.onPropUpdate)
this.onPropUpdate(prop, value)
}
if (action == "fetch_progress") {
if (this.onFetchProgress)
this.onFetchProgress(ev.data.args.remaining)
}
}
eval(script) {
return this.iframeCommand("eval", { script: script });
}
setProp(prop, value) {
return this.iframeCommand("set_prop", {prop, value})
}
bindProps(props) {
return this.iframeCommand("bind_props", { props: props })
}
handleLinks() {
return this.iframeCommand("catch_clicks", {});
}
fetchImports(bundle) {
return this.iframeCommand("fetch_imports", { bundle })
}
}

@ -1,10 +1,7 @@
(function (){
const importCache = {};
function fetchImport(id) {
return new Promise((fulfil, reject) => {
curl([`https://bundle.run/${id}`]).then(module => {
@ -31,36 +28,30 @@ function fetchImports(bundle, progressFunc) {
return promise
} else {
return P.resolve();
return Promise.resolve();
}
}
function handleMessage(ev) {
if (ev.data.action == "eval") {
let { script, evalId } = ev.data.args;
let { action, cmdId } = ev.data;
const sendMessage = (payload) => parent.postMessage( { ...payload }, ev.origin);
const sendReply = (payload) => sendMessage({ ...payload, cmdId })
const sendOk = () => sendReply({ action: "cmdOk" });
const sendError = (message, stack) => sendReply({ action: "cmdError", message, stack })
parent.postMessage({ action: "test" }, ev.origin);
if (action == "eval") {
let { script } = ev.data.args;
try {
eval(script);
parent.postMessage({
action: "evalOk",
args: {
evalId: evalId
}
}, ev.origin);
sendOk();
} catch (e) {
parent.postMessage({
action: "evalError",
args: {
evalId: evalId,
stack: e.stack,
message: e.message
}
}, ev.origin);
sendError(e.message, e.stack)
}
}
if (ev.data.action == "bind_props") {
if (action == "bind_props") {
let { props } = ev.data.args
if (!window.component) {
@ -74,35 +65,52 @@ function handleMessage(ev) {
// e.g. `component.$watch(prop, handler)`?
// (answer: probably)
window.component.$$.bound[prop] = value => {
parent.postMessage({
action: "propUpdate",
args: {
prop: prop,
value: value
}
}, ev.origin);
sendMessage({ action:"prop_update", args: { prop, value } })
};
});
}
if (ev.data.action == "set_prop") {
if (action == "set_prop") {
if (!window.component) {
return;
}
let { prop, value } = ev.data.args;
component[prop] = value;
}
if (action == "catch_clicks") {
let topOrigin = ev.origin;
document.body.addEventListener('click', event => {
if (event.which !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
// ensure target is a link
let el = event.target;
while (el && el.nodeName !== 'A') el = el.parentNode;
if (!el || el.nodeName !== 'A') return;
if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
event.preventDefault();
if (el.href.startsWith(topOrigin)) {
const url = new URL(el.href);
if (url.hash[0] === '#') {
window.location.hash = url.hash;
return;
}
}
window.open(el.href, '_blank');
});
}
if (ev.data.action == "fetch_imports") {
let { bundle, fetchId } = ev.data.args;
if (action == "fetch_imports") {
let { bundle } = ev.data.args;
fetchImports(bundle, (remaining) => {
parent.postMessage({
action: "fetch_progress",
args: {
fetchId: fetchId,
remaining: remaining
}
}, ev.origin);
sendMessage({action: "fetch_progress", args: { remaining }});
})
.then(() => {
bundle.imports.forEach(x=> {
@ -110,22 +118,10 @@ function handleMessage(ev) {
const name = bundle.importMap.get(x);
window[name] = module;
});
parent.postMessage({
action: "fetch_complete",
args: {
fetchId: fetchId
}
}, ev.origin);
sendOk();
})
.catch(e => {
parent.postMessage({
action: "fetch_error",
args: {
fetchId: fetchId,
message: e.message
}
}, ev.origin);
sendError(e.message, e.stack);
})
}
}
@ -134,3 +130,4 @@ window.addEventListener("message", handleMessage, false)
console.log("repl-runner initialized");
})();

Loading…
Cancel
Save