mirror of https://github.com/sveltejs/svelte
386 lines
8.3 KiB
386 lines
8.3 KiB
<script>
|
|
import { onMount, createEventDispatcher } from 'svelte';
|
|
import getLocationFromStack from '../../_utils/getLocationFromStack.js';
|
|
import { decode } from 'sourcemap-codec';
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let bundle;
|
|
export let dom;
|
|
export let ssr;
|
|
export let values_store;
|
|
export let props;
|
|
export let sourceError;
|
|
export let error;
|
|
|
|
export function setProp(prop, value) {
|
|
if (component) {
|
|
component[prop] = value;
|
|
}
|
|
}
|
|
|
|
let component;
|
|
|
|
const refs = {};
|
|
const importCache = {};
|
|
let pendingImports = 0;
|
|
let pending = false;
|
|
|
|
function fetchImport(id, curl) {
|
|
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`));
|
|
});
|
|
});
|
|
}
|
|
|
|
const namespaceSpecifier = /\*\s+as\s+(\w+)/;
|
|
const namedSpecifiers = /\{(.+)\}/;
|
|
|
|
function parseSpecifiers(specifiers) {
|
|
specifiers = specifiers.trim();
|
|
|
|
let match = namespaceSpecifier.exec(specifiers);
|
|
if (match) {
|
|
return {
|
|
namespace: true,
|
|
name: match[1]
|
|
};
|
|
}
|
|
|
|
let names = [];
|
|
|
|
specifiers = specifiers.replace(namedSpecifiers, (match, str) => {
|
|
names = str.split(',').map(name => {
|
|
const split = name.split('as');
|
|
const exported = split[0].trim();
|
|
const local = (split[1] || exported).trim();
|
|
|
|
return { local, exported };
|
|
});
|
|
|
|
return '';
|
|
});
|
|
|
|
match = /\w+/.exec(specifiers);
|
|
|
|
return {
|
|
namespace: false,
|
|
names,
|
|
default: match ? match[0] : null
|
|
};
|
|
}
|
|
|
|
let createComponent;
|
|
let init;
|
|
|
|
onMount(() => {
|
|
refs.child.addEventListener('load', () => {
|
|
const iframe = refs.child;
|
|
const body = iframe.contentDocument.body;
|
|
const evalInIframe = iframe.contentWindow.eval;
|
|
|
|
// intercept links, so that we can use #hashes inside the iframe
|
|
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.location.origin)) {
|
|
const url = new URL(el.href);
|
|
if (url.hash[0] === '#') {
|
|
iframe.contentWindow.location.hash = url.hash;
|
|
return;
|
|
}
|
|
}
|
|
|
|
window.open(el.href, '_blank');
|
|
});
|
|
|
|
let promise = null;
|
|
let updating = false;
|
|
|
|
let toDestroy = null;
|
|
|
|
const init = () => {
|
|
if (sourceError) return;
|
|
|
|
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]);
|
|
};
|
|
|
|
const ready = () => {
|
|
error = null;
|
|
|
|
if (toDestroy) {
|
|
removeStyles();
|
|
|
|
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();
|
|
} else {
|
|
pending = false;
|
|
createComponent();
|
|
}
|
|
};
|
|
|
|
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 };
|
|
}
|
|
|
|
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) {
|
|
// TODO show in UI
|
|
component = null;
|
|
|
|
const loc = getLocationFromStack(e.stack, dom.map);
|
|
if (loc) {
|
|
e.filename = loc.source;
|
|
e.loc = { line: loc.line, column: loc.column };
|
|
}
|
|
|
|
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 {
|
|
ready();
|
|
}
|
|
|
|
run = () => {
|
|
pending = false;
|
|
|
|
// TODO do we need to clear out SSR HTML?
|
|
createComponent();
|
|
};
|
|
}
|
|
|
|
bundle_handler = bundle => {
|
|
if (!bundle) return; // TODO can this ever happen?
|
|
if (promise) promise.cancel();
|
|
|
|
toDestroy = component;
|
|
component = null;
|
|
|
|
init();
|
|
};
|
|
|
|
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
|
|
}));
|
|
};
|
|
});
|
|
};
|
|
});
|
|
});
|
|
|
|
function noop(){}
|
|
let run = noop;
|
|
let bundle_handler = noop;
|
|
let props_handler = noop;
|
|
|
|
$: bundle_handler(bundle);
|
|
$: props_handler(props);
|
|
|
|
// pending https://github.com/sveltejs/svelte/issues/1889
|
|
$: {
|
|
$values_store;
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.iframe-container {
|
|
background-color: white;
|
|
border: none;
|
|
height: 100%;
|
|
}
|
|
|
|
iframe {
|
|
width: 100%;
|
|
height: 100%;
|
|
/* height: calc(100vh - var(--nav-h)); */
|
|
border: none;
|
|
display: block;
|
|
}
|
|
|
|
.greyed-out {
|
|
filter: grayscale(50%) blur(1px);
|
|
opacity: .25;
|
|
}
|
|
|
|
.overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: 1em;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.overlay p {
|
|
pointer-events: all;
|
|
}
|
|
|
|
.pending {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
text-align: center;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.pending button {
|
|
position: absolute;
|
|
margin-top: 6rem;
|
|
}
|
|
</style>
|
|
|
|
<div class="iframe-container">
|
|
<iframe title="Result" bind:this={refs.child} class="{error || pending || pendingImports ? 'greyed-out' : ''}" srcdoc='
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<link rel="stylesheet" href="/repl-viewer.css">
|
|
</head>
|
|
<body>
|
|
<script src="/curl.js"></script>
|
|
<script>curl.config({ dontAddFileExt: /./ });</script>
|
|
</body>
|
|
</html>
|
|
'></iframe>
|
|
</div>
|
|
|
|
<div class="overlay">
|
|
{#if error}
|
|
<p class="error message">
|
|
{#if error.loc}
|
|
<strong>
|
|
{#if error.filename}
|
|
<span class="filename" on:click="{() => dispatch('navigate', { filename: error.filename })}">{error.filename}</span>
|
|
{/if}
|
|
|
|
({error.loc.line}:{error.loc.column})
|
|
</strong>
|
|
{/if}
|
|
|
|
{error.message}
|
|
</p>
|
|
{:elseif pending}
|
|
<div class="pending" on:click={run}>
|
|
<button class="bg-second white">Click to run</button>
|
|
</div>
|
|
{:elseif pendingImports}
|
|
<p class="info message">loading {pendingImports} {pendingImports === 1 ? 'dependency' : 'dependencies'} from
|
|
https://bundle.run</p>
|
|
{/if}
|
|
</div> |