diff --git a/site/src/components/Repl/Bundler.js b/site/src/components/Repl/Bundler.js new file mode 100644 index 0000000000..ddde546e7f --- /dev/null +++ b/site/src/components/Repl/Bundler.js @@ -0,0 +1,44 @@ +const workers = new Map(); + +let uid = 1; + +export default class Bundler { + constructor(version) { + if (!workers.has(version)) { + const worker = new Worker('/workers/bundler.js'); + worker.postMessage({ type: 'init', version }); + workers.set(version, worker); + } + + this.worker = workers.get(version); + + this.handlers = new Map(); + + this.worker.addEventListener('message', event => { + const handler = this.handlers.get(event.data.id); + + if (handler) { // if no handler, was meant for a different REPL + handler(event.data); + this.handlers.delete(event.data.id); + } + }); + } + + bundle(components) { + return new Promise(fulfil => { + const id = uid++; + + this.handlers.set(id, fulfil); + + this.worker.postMessage({ + id, + type: 'bundle', + components + }); + }); + } + + destroy() { + this.worker.terminate(); + } +} \ No newline at end of file diff --git a/site/src/components/Repl/Output/Compiler.js b/site/src/components/Repl/Output/Compiler.js index b6d13e53bd..7ffb8b6bbc 100644 --- a/site/src/components/Repl/Output/Compiler.js +++ b/site/src/components/Repl/Output/Compiler.js @@ -1,21 +1,32 @@ +const workers = new Map(); + +let uid = 1; + export default class Compiler { constructor(version) { - this.worker = new Worker('/workers/compiler.js'); - this.worker.postMessage({ type: 'init', version }); + if (!workers.has(version)) { + const worker = new Worker('/workers/compiler.js'); + worker.postMessage({ type: 'init', version }); + workers.set(version, worker); + } + + this.worker = workers.get(version); - this.uid = 1; this.handlers = new Map(); - this.worker.onmessage = event => { + this.worker.addEventListener('message', event => { const handler = this.handlers.get(event.data.id); - handler(event.data.result); - this.handlers.delete(event.data.id); - }; + + if (handler) { // if no handler, was meant for a different REPL + handler(event.data.result); + this.handlers.delete(event.data.id); + } + }); } compile(component, options) { return new Promise(fulfil => { - const id = this.uid++; + const id = uid++; this.handlers.set(id, fulfil); diff --git a/site/src/components/Repl/Output/ReplProxy.js b/site/src/components/Repl/Output/ReplProxy.js index 5988c20f19..7ae140a8a7 100644 --- a/site/src/components/Repl/Output/ReplProxy.js +++ b/site/src/components/Repl/Output/ReplProxy.js @@ -1,12 +1,13 @@ +let uid = 1; + export default class ReplProxy { constructor(iframe, handlers) { this.iframe = iframe; this.handlers = handlers; - this.cmdId = 1; - this.pendingCmds = new Map(); + this.pending_cmds = new Map(); - this.handle_event = e => this.handleReplMessage(e); + this.handle_event = e => this.handle_repl_message(e); window.addEventListener('message', this.handle_event, false); } @@ -14,63 +15,61 @@ export default class ReplProxy { window.removeEventListener('message', this.handle_event); } - iframeCommand(command, args) { + iframe_command(action, args) { return new Promise((resolve, reject) => { - this.cmdId += 1; - this.pendingCmds.set(this.cmdId, { resolve, reject }); - - this.iframe.contentWindow.postMessage({ - action: command, - cmdId: this.cmdId, - args - }, '*'); + const cmd_id = uid++; + + this.pending_cmds.set(cmd_id, { resolve, reject }); + + this.iframe.contentWindow.postMessage({ action, cmd_id, args }, '*'); }); } - handleCommandMessage(cmdData) { - let action = cmdData.action; - let id = cmdData.cmdId; - let handler = this.pendingCmds.get(id); + handle_command_message(cmd_data) { + let action = cmd_data.action; + let id = cmd_data.cmd_id; + let handler = this.pending_cmds.get(id); if (handler) { - this.pendingCmds.delete(id); - if (action === 'cmdError') { - let { message, stack } = cmdData; + this.pending_cmds.delete(id); + if (action === 'cmd_error') { + let { message, stack } = cmd_data; let e = new Error(message); e.stack = stack; - console.log('repl cmd fail'); handler.reject(e) } - if (action === 'cmdOk') { - handler.resolve(cmdData.args) + if (action === 'cmd_ok') { + handler.resolve(cmd_data.args) } } else { - console.error('command not found', id, cmdData, [...this.pendingCmds.keys()]); + console.error('command not found', id, cmd_data, [...this.pending_cmds.keys()]); } } - handleReplMessage(event) { + handle_repl_message(event) { + if (event.source !== this.iframe.contentWindow) return; + const { action, args } = event.data; - if (action === 'cmdError' || action === 'cmdOk') { - this.handleCommandMessage(event.data); + if (action === 'cmd_error' || action === 'cmd_ok') { + this.handle_command_message(event.data); } if (action === 'fetch_progress') { - this.handlers.onFetchProgress(args.remaining) + this.handlers.on_fetch_progress(args.remaining) } } eval(script) { - return this.iframeCommand('eval', { script }); + return this.iframe_command('eval', { script }); } - handleLinks() { - return this.iframeCommand('catch_clicks', {}); + handle_links() { + return this.iframe_command('catch_clicks', {}); } - fetchImports(imports, import_map) { - return this.iframeCommand('fetch_imports', { imports, import_map }) + fetch_imports(imports, import_map) { + return this.iframe_command('fetch_imports', { imports, import_map }) } } \ No newline at end of file diff --git a/site/src/components/Repl/Output/Viewer.svelte b/site/src/components/Repl/Output/Viewer.svelte index 55e2ca9edc..66221d6acb 100644 --- a/site/src/components/Repl/Output/Viewer.svelte +++ b/site/src/components/Repl/Output/Viewer.svelte @@ -26,13 +26,13 @@ onMount(() => { proxy = new ReplProxy(iframe, { - onFetchProgress: progress => { + on_fetch_progress: progress => { pending_imports = progress; } }); iframe.addEventListener('load', () => { - proxy.handleLinks(); + proxy.handle_links(); ready = true; }); @@ -49,7 +49,7 @@ const token = current_token = {}; try { - await proxy.fetchImports($bundle.imports, $bundle.import_map); + await proxy.fetch_imports($bundle.imports, $bundle.import_map); if (token !== current_token) return; await proxy.eval(` diff --git a/site/src/components/Repl/Output/index.svelte b/site/src/components/Repl/Output/index.svelte index 59d67b5fcf..6c312a0b76 100644 --- a/site/src/components/Repl/Output/index.svelte +++ b/site/src/components/Repl/Output/index.svelte @@ -9,9 +9,9 @@ const { register_output } = getContext('REPL'); export let version; - export let sourceErrorLoc; - export let runtimeError; - export let embedded; + export let sourceErrorLoc = null; + export let runtimeError = null; + export let embedded = false; let foo; // TODO workaround for https://github.com/sveltejs/svelte/issues/2122 @@ -39,12 +39,7 @@ } }); - let compiler; - - onMount(() => { - compiler = new Compiler(version); - return () => compiler.destroy(); - }); + const compiler = process.browser && new Compiler(version); // refs let viewer; diff --git a/site/src/components/Repl/ReplWidget.svelte b/site/src/components/Repl/ReplWidget.svelte index 25beed2988..7211256884 100644 --- a/site/src/components/Repl/ReplWidget.svelte +++ b/site/src/components/Repl/ReplWidget.svelte @@ -4,8 +4,8 @@ import Repl from '../../components/Repl/index.svelte'; export let version = 'beta'; - export let gist; - export let example; + export let gist = null; + export let example = null; let repl; let name = 'loading...'; diff --git a/site/src/components/Repl/index.svelte b/site/src/components/Repl/index.svelte index aa9e423932..5b8f798b32 100644 --- a/site/src/components/Repl/index.svelte +++ b/site/src/components/Repl/index.svelte @@ -7,6 +7,7 @@ import ModuleEditor from './Input/ModuleEditor.svelte'; import Output from './Output/index.svelte'; import InputOutputToggle from './InputOutputToggle.svelte'; + import Bundler from './Bundler.js'; export let version = 'beta'; // TODO change this to latest when the time comes export let embedded = false; @@ -69,8 +70,11 @@ let module_editor; let output; - function rebundle() { - workers.bundler.postMessage({ type: 'bundle', components: $components }); + let current_token; + async function rebundle() { + const token = current_token = {}; + const result = await bundler.bundle($components); + if (result && token === current_token) bundle.set(result); } setContext('REPL', { @@ -145,30 +149,7 @@ let width = typeof window !== 'undefined' ? window.innerWidth : 300; let show_output = false; - onMount(async () => { - workers = { - bundler: new Worker('/workers/bundler.js') - }; - - workers.bundler.postMessage({ type: 'init', version }); - workers.bundler.onmessage = event => { - bundle.set(event.data); - }; - - return () => { - workers.bundler.terminate(); - }; - }); - - $: if ($bundle && $bundle.error && $selected) { - sourceErrorLoc = $bundle.error.filename === `${$selected.name}.${$selected.type}` - ? $bundle.error.start - : null; - } - - $: if (workers && $components) { - workers.bundler.postMessage({ type: 'bundle', components: $components }); - } + const bundler = process.browser && new Bundler(version); $: if (output && $selected) { output.update($selected, $compile_options); diff --git a/site/src/routes/_layout.svelte b/site/src/routes/_layout.svelte index 4cf0bac475..a72065a7c2 100644 --- a/site/src/routes/_layout.svelte +++ b/site/src/routes/_layout.svelte @@ -4,7 +4,6 @@ import Nav from '../components/TopNav.svelte'; export let segment; - export let path; diff --git a/site/static/repl-runner.js b/site/static/repl-runner.js index 4e0194e726..513e1d91e9 100644 --- a/site/static/repl-runner.js +++ b/site/static/repl-runner.js @@ -1,149 +1,105 @@ -(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() { + const import_cache = {}; + + function fetch_import(id) { + return new Promise((fulfil, reject) => { + curl([`https://bundle.run/${id}`]).then(module => { + import_cache[id] = module; + fulfil(module); + }, err => { + console.error(err.stack); + reject(new Error(`Error loading ${id} from bundle.run`)); + }); }); - }); -} - -function fetchImports(imports, progressFunc) { - const missingImports = 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 }) - - if (action == "eval") { - let { script } = ev.data.args; - try { - eval(script); - sendOk(); - } catch (e) { - sendError(e.message, e.stack); + function fetch_imports(imports, progress_func) { + const missing_imports = imports.filter(x => !import_cache[x]); + let pending_imports = missing_imports.length; + + if (missing_imports.length) { + let promise = Promise.all( + missing_imports.map(id => fetch_import(id).then(() => { + pending_imports -= 1; + if (progress_func) progress_func(pending_imports); + })) + ); + + return promise; + } else { + return Promise.resolve(); } } - if (action == "bind_props") { - let { props } = ev.data.args - - if (!window.component) { - // TODO can this happen? - console.warn('no component to bind to'); - sendOk(); - return; - } - - try { - 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 } }) - }; - }); - sendOk(); - } catch (e) { - - sendError(e.message, e.stack); - } - } - - if (action == "set_prop") { - try { - if (!window.component) { - return; + function handle_message(ev) { + let { action, cmd_id } = ev.data; + const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin); + const send_reply = (payload) => send_message({ ...payload, cmd_id }); + const send_ok = () => send_reply({ action: 'cmd_ok' }); + const send_error = (message, stack) => send_reply({ action: 'cmd_error', message, stack }); + + if (action === 'eval') { + try { + const { script } = ev.data.args; + eval(script); + send_ok(); + } catch (e) { + send_error(e.message, e.stack); } - let { prop, value } = ev.data.args; - component[prop] = value; - sendOk(); - } catch (e) { - sendError(e.message, e.stack); } - } - - if (action == "catch_clicks") { - try { - 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; + if (action === 'catch_clicks') { + try { + const top_origin = 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(top_origin)) { + const url = new URL(el.href); + if (url.hash[0] === '#') { + window.location.hash = url.hash; + return; + } } - } - window.open(el.href, '_blank'); - }); - sendOk(); - } catch(e) { - sendError(e.message, e.stack); + window.open(el.href, '_blank'); + }); + send_ok(); + } catch(e) { + send_error(e.message, e.stack); + } } - } - - if (action == "fetch_imports") { - let { imports, import_map } = ev.data.args; - fetchImports(imports, (remaining) => { - sendMessage({action: "fetch_progress", args: { remaining }}); - }) - .then(() => { - imports.forEach(x=> { - const module = importCache[x]; - const name = import_map.get(x); - window[name] = module; + if (action === 'fetch_imports') { + const { imports, import_map } = ev.data.args; + fetch_imports(imports, (remaining) => { + send_message({action: 'fetch_progress', args: { remaining }}); + }) + .then(() => { + imports.forEach(x=> { + const module = import_cache[x]; + const name = import_map.get(x); + window[name] = module; + }); + send_ok(); + }) + .catch(e => { + send_error(e.message, e.stack); }); - sendOk(); - }) - .catch(e => { - sendError(e.message, e.stack); - }) + } } -} - -window.addEventListener("message", handleMessage, false) - -console.log("repl-runner initialized"); + window.addEventListener('message', handle_message, false); })(); diff --git a/site/static/workers/bundler.js b/site/static/workers/bundler.js index 06a2364f93..48aed10ede 100644 --- a/site/static/workers/bundler.js +++ b/site/static/workers/bundler.js @@ -24,7 +24,7 @@ self.addEventListener('message', async event => { if (event.data.components.length === 0) return; await ready; - const result = await bundle(event.data.components); + const result = await bundle(event.data); if (result) { postMessage(result); } @@ -33,7 +33,7 @@ self.addEventListener('message', async event => { } }); -const commonCompilerOptions = { +const common_options = { dev: true, }; @@ -42,8 +42,6 @@ let cached = { ssr: {} }; -let currentToken; - const is_svelte_module = id => id === 'svelte' || id.startsWith('svelte/'); const cache = new Map(); @@ -60,7 +58,7 @@ function fetch_if_uncached(url) { return cache.get(url); } -async function getBundle(mode, cache, lookup) { +async function get_bundle(mode, cache, lookup) { let bundle; const all_warnings = []; @@ -107,7 +105,7 @@ async function getBundle(mode, cache, lookup) { format: 'esm', name, filename: name + '.svelte' - }, commonCompilerOptions)); + }, common_options)); new_cache[id] = { code, result }; @@ -137,12 +135,10 @@ async function getBundle(mode, cache, lookup) { return { bundle, cache: new_cache, error: null, warnings: all_warnings }; } -async function bundle(components) { +async function bundle({ id, components }) { // console.clear(); console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold'); - const token = currentToken = {}; - const lookup = {}; components.forEach(component => { const path = `./${component.name}.${component.type}`; @@ -154,16 +150,11 @@ async function bundle(components) { let error; try { - dom = await getBundle('dom', cached.dom, lookup); + dom = await get_bundle('dom', cached.dom, lookup); if (dom.error) { throw dom.error; } - if (token !== currentToken) { - console.error(`aborted`); - return; - } - cached.dom = dom.cache; let uid = 1; @@ -180,10 +171,8 @@ async function bundle(components) { sourcemap: true })).output[0]; - if (token !== currentToken) return; - const ssr = false // TODO how can we do SSR? - ? await getBundle('ssr', cached.ssr, lookup) + ? await get_bundle('ssr', cached.ssr, lookup) : null; if (ssr) { @@ -193,8 +182,6 @@ async function bundle(components) { } } - if (token !== currentToken) return; - const ssr_result = ssr ? (await ssr.bundle.generate({ format: 'iife', @@ -206,6 +193,7 @@ async function bundle(components) { : null; return { + id, imports: dom_result.imports, import_map, dom: dom_result, @@ -218,6 +206,7 @@ async function bundle(components) { delete e.toString; return { + id, imports: [], import_map, dom: null, diff --git a/site/static/workers/compiler.js b/site/static/workers/compiler.js index e919097310..3d62eaded3 100644 --- a/site/static/workers/compiler.js +++ b/site/static/workers/compiler.js @@ -20,20 +20,19 @@ self.addEventListener('message', async event => { await ready; postMessage(compile(event.data)); break; - } }); -const commonCompilerOptions = { +const common_options = { dev: false, css: false }; -function compile({ id, source, options, entry }) { +function compile({ id, source, options }) { try { - const { js, css, stats, vars } = svelte.compile( + const { js, css } = svelte.compile( source, - Object.assign({}, commonCompilerOptions, options) + Object.assign({}, common_options, options) ); return {