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)); + 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` diff --git a/site/src/routes/repl/_components/Output/Viewer.html b/site/src/routes/repl/_components/Output/Viewer.html index ef377b8b3c..e62e0d695b 100644 --- a/site/src/routes/repl/_components/Output/Viewer.html +++ b/site/src/routes/repl/_components/Output/Viewer.html @@ -1,6 +1,7 @@ + '> diff --git a/site/src/routes/repl/_utils/replProxy.js b/site/src/routes/repl/_utils/replProxy.js new file mode 100644 index 0000000000..922d6e3f18 --- /dev/null +++ b/site/src/routes/repl/_utils/replProxy.js @@ -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 }) + } + +} \ No newline at end of file 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); diff --git a/site/static/repl-runner.js b/site/static/repl-runner.js new file mode 100644 index 0000000000..d1026839b5 --- /dev/null +++ b/site/static/repl-runner.js @@ -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"); + +})();