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.
svelte/sites/svelte-5-preview/src/lib/Output/Viewer.svelte

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>