From 5899a399214abb461fd5ee5f9356ae1cbc027a42 Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Thu, 10 Jan 2019 17:07:42 +1000 Subject: [PATCH 1/5] convert update_template.sh to js for windows support --- site/package.json | 3 ++- site/scripts/update_template.js | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 site/scripts/update_template.js diff --git a/site/package.json b/site/package.json index 2e633b774c..b9390f3c39 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "sapper dev", "sapper": "sapper build --legacy", - "update": "sh scripts/update_template.sh && node scripts/get-contributors.js", + "update": "node scripts/update_template.js && node scripts/get-contributors.js", "start": "node __sapper__/build", "cy:run": "cypress run", "cy:open": "cypress open", @@ -29,6 +29,7 @@ "passport-github": "^1.1.0", "prismjs": "^1.15.0", "session-file-store": "^1.2.0", + "shelljs": "^0.8.3", "sirv": "^0.2.0", "yootils": "0.0.14" }, diff --git a/site/scripts/update_template.js b/site/scripts/update_template.js new file mode 100644 index 0000000000..23e2333bab --- /dev/null +++ b/site/scripts/update_template.js @@ -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)); + From 8e26da8dda0e10976a85bfa0fb4368f861d966c1 Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Thu, 10 Jan 2019 17:07:58 +1000 Subject: [PATCH 2/5] remove redundant bash based update script --- site/scripts/update_template.sh | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100755 site/scripts/update_template.sh diff --git a/site/scripts/update_template.sh b/site/scripts/update_template.sh deleted file mode 100755 index 59ab3cb0cd..0000000000 --- a/site/scripts/update_template.sh +++ /dev/null @@ -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` From 959bb6869ab5a099784619e43e3b1e34f6320ec3 Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Thu, 10 Jan 2019 17:39:10 +1000 Subject: [PATCH 3/5] handle markdown on windows machines --- site/src/utils/_process_markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/utils/_process_markdown.js b/site/src/utils/_process_markdown.js index a79d759cbb..f760018a3b 100644 --- a/site/src/utils/_process_markdown.js +++ b/site/src/utils/_process_markdown.js @@ -1,5 +1,5 @@ export default function process_markdown(markdown) { - const match = /---\n([\s\S]+?)\n---/.exec(markdown); + const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown); const frontMatter = match[1]; const content = markdown.slice(match[0].length); From 8a70f3ac47b44710386409acdf45036324168d8b Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Fri, 11 Jan 2019 13:19:00 +1000 Subject: [PATCH 4/5] POC for postmessage Repl --- .../repl/_components/Output/Viewer.html | 260 +++++++++++------- site/static/repl-runner.js | 136 +++++++++ 2 files changed, 293 insertions(+), 103 deletions(-) create mode 100644 site/static/repl-runner.js diff --git a/site/src/routes/repl/_components/Output/Viewer.html b/site/src/routes/repl/_components/Output/Viewer.html index ef377b8b3c..9cfe5a5942 100644 --- a/site/src/routes/repl/_components/Output/Viewer.html +++ b/site/src/routes/repl/_components/Output/Viewer.html @@ -14,9 +14,14 @@ export let error; export function setProp(prop, value) { - if (component) { - component[prop] = value; - } + if (!refs.child) return; + refs.child.contentWindow.postMessage({ + action: 'set_prop', + args: { + prop: prop, + value: value + } + },'*'); } let component; @@ -81,9 +86,84 @@ onMount(() => { refs.child.addEventListener('load', () => { 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 body.addEventListener('click', event => { if (event.which !== 1) return; @@ -109,6 +189,7 @@ window.open(el.href, '_blank'); }); + */ let promise = null; let updating = false; @@ -121,11 +202,14 @@ const imports = []; const missingImports = bundle.imports.filter(x => !importCache[x]); + const removeStyles = () => { - const styles = iframe.contentDocument.querySelectorAll('style.svelte'); - let i = styles.length; - while (i--) styles[i].parentNode.removeChild(styles[i]); + evalInIframe(` + const styles = document.querySelectorAll('style.svelte'); + let i = styles.length; + while (i--) styles[i].parentNode.removeChild(styles[i]); + `) }; const ready = () => { @@ -137,14 +221,7 @@ toDestroy.$destroy(); 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 pending = true; createHtml(); @@ -155,54 +232,45 @@ }; const createHtml = () => { - try { - evalInIframe(`${ssr.code} - var rendered = SvelteComponent.render(${JSON.stringify($values_store)}); - - if (rendered.css.code) { - var style = document.createElement('style'); - style.className = 'svelte'; - style.textContent = rendered.css.code; - document.head.appendChild(style); - } - - 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 }; + + evalInIframe(`${ssr.code} + var rendered = SvelteComponent.render(${JSON.stringify($values_store)}); + + if (rendered.css.code) { + var style = document.createElement('style'); + style.className = 'svelte'; + style.textContent = rendered.css.code; + document.head.appendChild(style); } - error = e; - } - }; + 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 }; + } + error = e; + }); + }; + const createComponent = () => { // remove leftover styles from SSR renderer if (ssr) removeStyles(); - try { - evalInIframe(`${dom.code} - document.body.innerHTML = ''; - window.location.hash = ''; - window._svelteTransitionManager = null; - - var component = new SvelteComponent({ - target: document.body, - props: ${JSON.stringify($values_store)} - });`); - - 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) { + evalInIframe(`${dom.code} + document.body.innerHTML = ''; + window.location.hash = ''; + window._svelteTransitionManager = null; + + window.component = new SvelteComponent({ + target: document.body, + props: ${JSON.stringify($values_store)} + });`) + .catch(e=> { + // TODO show in UI component = null; @@ -213,35 +281,31 @@ } 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 - .then(() => { - if (cancelled) return; - ready(); - }) - .catch(e => { - if (cancelled) return; - error = e; - }); - } else { + + + 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; + }); + run = () => { pending = false; @@ -252,7 +316,7 @@ bundle_handler = bundle => { if (!bundle) return; // TODO can this ever happen? - if (promise) promise.cancel(); + if (fetchHandler) fetchHandler = null; toDestroy = component; component = null; @@ -261,23 +325,12 @@ }; props_handler = props => { - if (!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) - component.$$.bound[prop] = value => { - dispatch('binding', { prop, value }); - values_store.update(values => Object.assign({}, values, { - [prop]: value - })); - }; - }); + iframe.contentWindow.postMessage({ + action:"bind_props", + args: { + props: [...props] + } + },'*') }; }); }); @@ -346,7 +399,7 @@
- diff --git a/site/static/repl-runner.js b/site/static/repl-runner.js new file mode 100644 index 0000000000..d346ad1fc5 --- /dev/null +++ b/site/static/repl-runner.js @@ -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"); + From 7f3c674defdd6f74815e02211e206796753ccfed Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Sat, 12 Jan 2019 01:46:33 +1000 Subject: [PATCH 5/5] abstract repl interaction code --- .../repl/_components/Output/Viewer.html | 244 +++++------------- site/src/routes/repl/_utils/replProxy.js | 87 +++++++ site/static/repl-runner.js | 107 ++++---- 3 files changed, 201 insertions(+), 237 deletions(-) create mode 100644 site/src/routes/repl/_utils/replProxy.js diff --git a/site/src/routes/repl/_components/Output/Viewer.html b/site/src/routes/repl/_components/Output/Viewer.html index 9cfe5a5942..e62e0d695b 100644 --- a/site/src/routes/repl/_components/Output/Viewer.html +++ b/site/src/routes/repl/_components/Output/Viewer.html @@ -1,6 +1,7 @@