POC for postmessage Repl

pull/1979/head
David Pershouse 6 years ago
parent 9b7d0152bc
commit 2c537e7cb4

@ -14,9 +14,14 @@
export let error; export let error;
export function setProp(prop, value) { export function setProp(prop, value) {
if (component) { if (!refs.child) return;
component[prop] = value; refs.child.contentWindow.postMessage({
action: 'set_prop',
args: {
prop: prop,
value: value
} }
},'*');
} }
let component; let component;
@ -81,9 +86,84 @@
onMount(() => { onMount(() => {
refs.child.addEventListener('load', () => { refs.child.addEventListener('load', () => {
const iframe = refs.child; const iframe = refs.child;
const body = iframe.contentDocument.body;
const evalInIframe = iframe.contentWindow.eval;
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
}));
}
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;
}
}
}
window.addEventListener("message", handleReplMessage, false);
// iframe.contentWindow.eval;
/* TODO
// intercept links, so that we can use #hashes inside the iframe // intercept links, so that we can use #hashes inside the iframe
body.addEventListener('click', event => { body.addEventListener('click', event => {
if (event.which !== 1) return; if (event.which !== 1) return;
@ -109,6 +189,7 @@
window.open(el.href, '_blank'); window.open(el.href, '_blank');
}); });
*/
let promise = null; let promise = null;
let updating = false; let updating = false;
@ -122,10 +203,13 @@
const missingImports = bundle.imports.filter(x => !importCache[x]); const missingImports = bundle.imports.filter(x => !importCache[x]);
const removeStyles = () => { const removeStyles = () => {
const styles = iframe.contentDocument.querySelectorAll('style.svelte'); evalInIframe(`
const styles = document.querySelectorAll('style.svelte');
let i = styles.length; let i = styles.length;
while (i--) styles[i].parentNode.removeChild(styles[i]); while (i--) styles[i].parentNode.removeChild(styles[i]);
`)
}; };
const ready = () => { const ready = () => {
@ -138,13 +222,6 @@
toDestroy = null; toDestroy = null;
} }
bundle.imports.forEach(x => {
const module = importCache[x];
const name = bundle.importMap.get(x);
iframe.contentWindow[name] = module;
});
if (ssr) { // this only gets generated if component uses lifecycle hooks if (ssr) { // this only gets generated if component uses lifecycle hooks
pending = true; pending = true;
createHtml(); createHtml();
@ -155,7 +232,7 @@
}; };
const createHtml = () => { const createHtml = () => {
try {
evalInIframe(`${ssr.code} evalInIframe(`${ssr.code}
var rendered = SvelteComponent.render(${JSON.stringify($values_store)}); var rendered = SvelteComponent.render(${JSON.stringify($values_store)});
@ -167,8 +244,8 @@
} }
document.body.innerHTML = rendered.html; document.body.innerHTML = rendered.html;
`) `).catch( e => {
} catch (e) {
const loc = getLocationFromStack(e.stack, ssr.map); const loc = getLocationFromStack(e.stack, ssr.map);
if (loc) { if (loc) {
e.filename = loc.source; e.filename = loc.source;
@ -176,33 +253,24 @@
} }
error = e; error = e;
} });
}; };
const createComponent = () => { const createComponent = () => {
// remove leftover styles from SSR renderer // remove leftover styles from SSR renderer
if (ssr) removeStyles(); if (ssr) removeStyles();
try {
evalInIframe(`${dom.code} evalInIframe(`${dom.code}
document.body.innerHTML = ''; document.body.innerHTML = '';
window.location.hash = ''; window.location.hash = '';
window._svelteTransitionManager = null; window._svelteTransitionManager = null;
var component = new SvelteComponent({ window.component = new SvelteComponent({
target: document.body, target: document.body,
props: ${JSON.stringify($values_store)} props: ${JSON.stringify($values_store)}
});`); });`)
.catch(e=> {
component = window.app = window.component = iframe.contentWindow.component;
// component.on('state', ({ current }) => {
// if (updating) return;
// updating = true;
// this.fire('data', { current });
// updating = false;
// });
} catch (e) {
// TODO show in UI // TODO show in UI
component = null; component = null;
@ -213,34 +281,30 @@
} }
error = e; error = e;
} });
}; };
pendingImports = missingImports.length;
if (missingImports.length) {
let cancelled = false;
promise = Promise.all(
missingImports.map(id => fetchImport(id, iframe.contentWindow.curl).then(module => {
pendingImports -= 1;
return module;
}))
);
promise.cancel = () => cancelled = true;
promise new Promise((resolve, reject)=> {
fetchHandler = {
fetchId: fetchId++,
resolve: resolve,
reject: reject
}
iframe.contentWindow.postMessage({
action: "fetch_imports",
args: {
bundle: bundle,
fetchId: fetchHandler.fetchId
}
}, '*');
})
.then(() => { .then(() => {
if (cancelled) return;
ready(); ready();
}) })
.catch(e => { .catch(e => {
if (cancelled) return;
error = e; error = e;
}); });
} else {
ready();
}
run = () => { run = () => {
pending = false; pending = false;
@ -252,7 +316,7 @@
bundle_handler = bundle => { bundle_handler = bundle => {
if (!bundle) return; // TODO can this ever happen? if (!bundle) return; // TODO can this ever happen?
if (promise) promise.cancel(); if (fetchHandler) fetchHandler = null;
toDestroy = component; toDestroy = component;
component = null; component = null;
@ -261,23 +325,12 @@
}; };
props_handler = props => { props_handler = props => {
if (!component) { iframe.contentWindow.postMessage({
// TODO can this happen? action:"bind_props",
console.error(`no component to bind to`); args: {
return; props: [...props]
} }
},'*')
props.forEach(prop => {
// TODO should there be a public API for binding?
// e.g. `component.$watch(prop, handler)`?
// (answer: probably)
component.$$.bound[prop] = value => {
dispatch('binding', { prop, value });
values_store.update(values => Object.assign({}, values, {
[prop]: value
}));
};
});
}; };
}); });
}); });
@ -346,7 +399,7 @@
</style> </style>
<div class="iframe-container"> <div class="iframe-container">
<iframe title="Result" bind:this={refs.child} class="{error || pending || pendingImports ? 'greyed-out' : ''}" srcdoc=' <iframe title="Result" bind:this={refs.child} sandbox="allow-scripts" class="{error || pending || pendingImports ? 'greyed-out' : ''}" srcdoc='
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
@ -355,6 +408,7 @@
<body> <body>
<script src="/curl.js"></script> <script src="/curl.js"></script>
<script>curl.config(&#123; dontAddFileExt: /./ });</script> <script>curl.config(&#123; dontAddFileExt: /./ });</script>
<script src="/repl-runner.js"></script>
</body> </body>
</html> </html>
'></iframe> '></iframe>

@ -0,0 +1,136 @@
const importCache = {};
function fetchImport(id) {
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`));
});
});
}
function fetchImports(bundle, progressFunc) {
const missingImports = bundle.imports.filter(x => !importCache[x]);
let pendingImports = missingImports.length;
if (missingImports.length) {
let promise = Promise.all(
missingImports.map(id => fetchImport(id).then(() => {
pendingImports -= 1;
if (progressFunc) progressFunc(pendingImports);
}))
);
return promise
} else {
return P.resolve();
}
}
function handleMessage(ev) {
if (ev.data.action == "eval") {
let { script, evalId } = ev.data.args;
try {
eval(script);
parent.postMessage({
action: "evalOk",
args: {
evalId: evalId
}
}, ev.origin);
} catch (e) {
parent.postMessage({
action: "evalError",
args: {
evalId: evalId,
stack: e.stack,
message: e.message
}
}, ev.origin);
}
}
if (ev.data.action == "bind_props") {
let { props } = ev.data.args
if (!window.component) {
// TODO can this happen?
console.error(`no component to bind to`);
return;
}
props.forEach(prop => {
// TODO should there be a public API for binding?
// e.g. `component.$watch(prop, handler)`?
// (answer: probably)
window.component.$$.bound[prop] = value => {
parent.postMessage({
action: "propUpdate",
args: {
prop: prop,
value: value
}
}, ev.origin);
};
});
}
if (ev.data.action == "set_prop") {
if (!window.component) {
return;
}
let { prop, value } = ev.data.args;
component[prop] = value;
}
if (ev.data.action == "fetch_imports") {
let { bundle, fetchId } = ev.data.args;
fetchImports(bundle, (remaining) => {
parent.postMessage({
action: "fetch_progress",
args: {
fetchId: fetchId,
remaining: remaining
}
}, ev.origin);
})
.then(() => {
bundle.imports.forEach(x=> {
const module = importCache[x];
const name = bundle.importMap.get(x);
window[name] = module;
});
parent.postMessage({
action: "fetch_complete",
args: {
fetchId: fetchId
}
}, ev.origin);
})
.catch(e => {
parent.postMessage({
action: "fetch_error",
args: {
fetchId: fetchId,
message: e.message
}
}, ev.origin);
})
}
}
window.addEventListener("message", handleMessage, false)
console.log("repl-runner initialized");
Loading…
Cancel
Save