mirror of https://github.com/sveltejs/svelte
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
7.5 KiB
338 lines
7.5 KiB
<script>
|
|
import { get_repl_context } from '$lib/context.js';
|
|
import { BROWSER } from 'esm-env';
|
|
import { onMount } from 'svelte';
|
|
import Message from '../Message.svelte';
|
|
import PaneWithPanel from './PaneWithPanel.svelte';
|
|
import ReplProxy from './ReplProxy.js';
|
|
import Console from './console/Console.svelte';
|
|
import getLocationFromStack from './get-location-from-stack';
|
|
import srcdoc from './srcdoc/index.html?raw';
|
|
import ErrorOverlay from './ErrorOverlay.svelte';
|
|
|
|
/** @type {import('$lib/types').MessageDetails | null} */
|
|
export let error;
|
|
/** @type {string | null} */
|
|
export let status;
|
|
export let relaxed = false;
|
|
export let injectedJS = '';
|
|
export let injectedCSS = '';
|
|
|
|
/** @type {'light' | 'dark'} */
|
|
export let theme;
|
|
|
|
const { bundle } = get_repl_context();
|
|
|
|
/** @type {import('./console/console').Log[]} */
|
|
let logs = [];
|
|
|
|
/** @type {import('./console/console').Log[][]} */
|
|
let log_group_stack = [];
|
|
|
|
let current_log_group = logs;
|
|
|
|
/** @type {HTMLIFrameElement} */
|
|
let iframe;
|
|
let pending_imports = 0;
|
|
let pending = false;
|
|
|
|
/** @type {ReplProxy | null} */
|
|
let proxy = null;
|
|
|
|
let ready = false;
|
|
let inited = false;
|
|
|
|
let log_height = 90;
|
|
/** @type {number} */
|
|
let prev_height;
|
|
|
|
/** @type {import('./console/console').Log} */
|
|
let last_console_event;
|
|
|
|
onMount(() => {
|
|
proxy = new ReplProxy(iframe, {
|
|
on_fetch_progress: (progress) => {
|
|
pending_imports = progress;
|
|
},
|
|
on_error: (event) => {
|
|
push_logs({ level: 'error', args: [event.value] });
|
|
},
|
|
on_unhandled_rejection: (event) => {
|
|
let error = event.value;
|
|
if (typeof error === 'string') error = { message: error };
|
|
error.message = 'Uncaught (in promise): ' + error.message;
|
|
push_logs({ level: 'error', args: [error] });
|
|
},
|
|
on_console: (log) => {
|
|
if (log.level === 'clear') {
|
|
clear_logs();
|
|
push_logs(log);
|
|
} else if (log.duplicate) {
|
|
increment_duplicate_log();
|
|
} else {
|
|
push_logs(log);
|
|
}
|
|
},
|
|
on_console_group: (action) => {
|
|
group_logs(action.label, false);
|
|
},
|
|
on_console_group_end: () => {
|
|
ungroup_logs();
|
|
},
|
|
on_console_group_collapsed: (action) => {
|
|
group_logs(action.label, true);
|
|
}
|
|
});
|
|
|
|
iframe.addEventListener('load', () => {
|
|
proxy?.handle_links();
|
|
ready = true;
|
|
});
|
|
|
|
return () => {
|
|
proxy?.destroy();
|
|
};
|
|
});
|
|
|
|
$: if (ready) proxy?.iframe_command('set_theme', { theme });
|
|
|
|
/**
|
|
* @param {import('$lib/types').Bundle | null} $bundle
|
|
*/
|
|
async function apply_bundle($bundle) {
|
|
if (!$bundle) return;
|
|
|
|
try {
|
|
clear_logs();
|
|
|
|
if (!$bundle.error) {
|
|
await proxy?.eval(`
|
|
${injectedJS}
|
|
|
|
${styles}
|
|
|
|
{
|
|
const styles = document.querySelectorAll('style[id^=svelte-]');
|
|
|
|
let i = styles.length;
|
|
while (i--) styles[i].parentNode.removeChild(styles[i]);
|
|
|
|
if (window.__unmount_previous) {
|
|
try {
|
|
window.__unmount_previous();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
document.body.innerHTML = '';
|
|
window._svelteTransitionManager = null;
|
|
}
|
|
|
|
const __repl_exports = ${$bundle.client?.code};
|
|
{
|
|
const { mount, unmount, App, untrack } = __repl_exports;
|
|
|
|
const console_methods = ['log', 'error', 'trace', 'assert', 'warn', 'table', 'group'];
|
|
|
|
// The REPL hooks up to the console to provide a virtual console. However, the implementation
|
|
// needs to stringify the console to pass over a MessageChannel, which means that the object
|
|
// can get deeply read and tracked by accident when using the console. We can avoid this by
|
|
// ensuring we untrack the main console methods.
|
|
|
|
const original = {};
|
|
|
|
for (const method of console_methods) {
|
|
original[method] = console[method];
|
|
console[method] = function (...v) {
|
|
return untrack(() => original[method].apply(this, v));
|
|
}
|
|
}
|
|
const component = mount(App, { target: document.body });
|
|
window.__unmount_previous = () => {
|
|
for (const method of console_methods) {
|
|
console[method] = original[method];
|
|
}
|
|
unmount(component);
|
|
}
|
|
}
|
|
//# sourceURL=playground:output
|
|
`);
|
|
error = null;
|
|
}
|
|
} catch (e) {
|
|
// @ts-ignore
|
|
show_error(e);
|
|
}
|
|
|
|
inited = true;
|
|
}
|
|
|
|
$: if (ready) apply_bundle($bundle);
|
|
|
|
$: styles =
|
|
injectedCSS &&
|
|
`{
|
|
const style = document.createElement('style');
|
|
style.textContent = ${JSON.stringify(injectedCSS)};
|
|
document.head.appendChild(style);
|
|
}`;
|
|
|
|
/**
|
|
* @param {import('$lib/types').Error & { loc: { line: number; column: number } }} e
|
|
*/
|
|
function show_error(e) {
|
|
const map = $bundle?.client?.map;
|
|
|
|
// @ts-ignore INVESTIGATE
|
|
const loc = map && getLocationFromStack(e.stack, map);
|
|
if (loc) {
|
|
e.filename = loc.source;
|
|
e.loc = { line: loc.line, column: loc.column ?? 0 };
|
|
}
|
|
|
|
error = e;
|
|
}
|
|
|
|
/**
|
|
* @param {import('./console/console').Log} log
|
|
*/
|
|
function push_logs(log) {
|
|
current_log_group.push((last_console_event = log));
|
|
logs = logs;
|
|
}
|
|
|
|
/**
|
|
* @param {string} label
|
|
* @param {boolean} collapsed
|
|
*/
|
|
function group_logs(label, collapsed) {
|
|
/** @type {import('./console/console').Log} */
|
|
const group_log = { level: 'group', label, collapsed, logs: [] };
|
|
current_log_group.push({ level: 'group', label, collapsed, logs: [] });
|
|
// TODO: Investigate
|
|
log_group_stack.push(current_log_group);
|
|
current_log_group = group_log.logs ?? [];
|
|
logs = logs;
|
|
}
|
|
|
|
function ungroup_logs() {
|
|
const last = log_group_stack.pop();
|
|
|
|
if (last) current_log_group = last;
|
|
}
|
|
|
|
function increment_duplicate_log() {
|
|
const last_log = current_log_group[current_log_group.length - 1];
|
|
|
|
if (last_log) {
|
|
last_log.count = (last_log.count || 1) + 1;
|
|
logs = logs;
|
|
} else {
|
|
last_console_event.count = 1;
|
|
push_logs(last_console_event);
|
|
}
|
|
}
|
|
|
|
function on_toggle_console() {
|
|
if (log_height < 90) {
|
|
prev_height = log_height;
|
|
log_height = 90;
|
|
} else {
|
|
log_height = prev_height || 45;
|
|
}
|
|
}
|
|
|
|
function clear_logs() {
|
|
current_log_group = logs = [];
|
|
}
|
|
</script>
|
|
|
|
<div class="iframe-container">
|
|
<PaneWithPanel pos="90%" panel="Console">
|
|
<div slot="main">
|
|
<iframe
|
|
title="Result"
|
|
class:inited
|
|
bind:this={iframe}
|
|
sandbox={[
|
|
'allow-popups-to-escape-sandbox',
|
|
'allow-scripts',
|
|
'allow-popups',
|
|
'allow-forms',
|
|
'allow-pointer-lock',
|
|
'allow-top-navigation',
|
|
'allow-modals',
|
|
relaxed ? 'allow-same-origin' : ''
|
|
].join(' ')}
|
|
class={error || pending || pending_imports ? 'greyed-out' : ''}
|
|
srcdoc={BROWSER ? srcdoc : ''}
|
|
/>
|
|
|
|
{#if $bundle?.error}
|
|
<ErrorOverlay error={$bundle.error} />
|
|
{/if}
|
|
</div>
|
|
|
|
<div slot="panel-header">
|
|
<button on:click|stopPropagation={clear_logs}>
|
|
{#if logs.length > 0}
|
|
({logs.length})
|
|
{/if}
|
|
Clear
|
|
</button>
|
|
</div>
|
|
|
|
<section slot="panel-body">
|
|
<Console {logs} {theme} on:clear={clear_logs} />
|
|
</section>
|
|
</PaneWithPanel>
|
|
|
|
<div class="overlay">
|
|
{#if error}
|
|
<Message kind="error" details={error} />
|
|
{:else if status || !$bundle}
|
|
<Message kind="info" truncate>{status || 'loading Svelte compiler...'}</Message>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.iframe-container {
|
|
position: absolute;
|
|
background-color: var(--sk-back-1, white);
|
|
border: none;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
iframe {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
display: block;
|
|
}
|
|
|
|
.greyed-out {
|
|
filter: grayscale(50%) blur(1px);
|
|
opacity: 0.25;
|
|
}
|
|
|
|
button {
|
|
color: var(--sk-text-2, #999);
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
display: block;
|
|
}
|
|
|
|
button:hover {
|
|
color: var(--sk-text-1, #333);
|
|
}
|
|
|
|
.overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 100%;
|
|
}
|
|
</style>
|