mirror of https://github.com/sveltejs/svelte
Merge pull request #1979 from halfnelson/feature/repl-refactor
Sandbox iframe contentspull/1983/head
commit
1097858ea2
@ -0,0 +1,26 @@
|
||||
const sh = require('shelljs');
|
||||
const fs = require('fs')
|
||||
|
||||
sh.cd(__dirname+'/../')
|
||||
|
||||
// fetch svelte app
|
||||
sh.rm('-rf','scripts/svelte-app')
|
||||
sh.exec('npx degit sveltejs/template scripts/svelte-app')
|
||||
|
||||
// update repl-viewer.css based on template
|
||||
sh.cp('scripts/svelte-app/public/global.css', 'static/repl-viewer.css')
|
||||
|
||||
// remove src (will be recreated client-side) and node_modules
|
||||
sh.rm('-rf', 'scripts/svelte-app/src')
|
||||
sh.rm('-rf', 'scripts/svelte-app/node_modules')
|
||||
|
||||
// build svelte-app.json
|
||||
const appPath = 'scripts/svelte-app'
|
||||
let files = []
|
||||
|
||||
for (const path of sh.find(appPath).filter(p => fs.lstatSync(p).isFile()) ) {
|
||||
files.push({ path: path.slice(appPath.length + 1), data: fs.readFileSync(path).toString() });
|
||||
}
|
||||
|
||||
fs.writeFileSync('static/svelte-app.json', JSON.stringify(files));
|
||||
|
@ -1,15 +0,0 @@
|
||||
cd `dirname $0`/..
|
||||
|
||||
# fetch svelte-app
|
||||
rm -rf scripts/svelte-app
|
||||
node_modules/.bin/degit sveltejs/template scripts/svelte-app
|
||||
|
||||
# update repl-viewer.css based on template
|
||||
cp scripts/svelte-app/public/global.css static/repl-viewer.css
|
||||
|
||||
# remove src (will be recreated client-side) and node_modules
|
||||
rm -rf scripts/svelte-app/src
|
||||
rm -rf scripts/svelte-app/node_modules
|
||||
|
||||
# build svelte-app.json
|
||||
node scripts/build-svelte-app-json.js `find scripts/svelte-app -type f`
|
@ -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 })
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
|
||||
(function (){
|
||||
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 Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessage(ev) {
|
||||
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);
|
||||
sendOk();
|
||||
} catch (e) {
|
||||
sendError(e.message, e.stack)
|
||||
}
|
||||
}
|
||||
|
||||
if (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 => {
|
||||
sendMessage({ action:"prop_update", args: { prop, value } })
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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 (action == "fetch_imports") {
|
||||
let { bundle } = ev.data.args;
|
||||
fetchImports(bundle, (remaining) => {
|
||||
sendMessage({action: "fetch_progress", args: { remaining }});
|
||||
})
|
||||
.then(() => {
|
||||
bundle.imports.forEach(x=> {
|
||||
const module = importCache[x];
|
||||
const name = bundle.importMap.get(x);
|
||||
window[name] = module;
|
||||
});
|
||||
sendOk();
|
||||
})
|
||||
.catch(e => {
|
||||
sendError(e.message, e.stack);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage, false)
|
||||
|
||||
console.log("repl-runner initialized");
|
||||
|
||||
})();
|
Loading…
Reference in new issue