Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 29 additions & 33 deletions packages/repl/src/lib/Output/Viewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import Message from '../Message.svelte';
import PaneWithPanel from './PaneWithPanel.svelte';
import ReplProxy from './ReplProxy.js';
import Console, { type Log } from './console/Console.svelte';
import { type Log } from './console/Console.svelte';
import getLocationFromStack from './get-location-from-stack';
import srcdoc from './srcdoc/index.html?raw';
import ErrorOverlay from './ErrorOverlay.svelte';
import type { CompileError } from 'svelte/compiler';
import type { Bundle } from '../types';
import type { Writable } from 'svelte/store';
import DevTools from './devtools/DevTools.svelte';

export let error: Error | null;
/** status by Bundler class instance */
Expand Down Expand Up @@ -157,30 +158,34 @@

const __repl_exports = ${$bundle.client?.code};
{
const { mount, unmount, App, untrack } = __repl_exports;
const { mount, unmount, App } = __repl_exports;

const console_methods = ['log', 'error', 'trace', 'assert', 'warn', 'table', 'group'];
const render_app = () => {
const component = mount(App, { target: document.body });

// 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];
window.__unmount_previous = () => {
unmount(component);
}
unmount(component);
};

if (!window.initialize_devtools) {
window.initialize_devtools = () => {
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/public/target.js';
script.setAttribute('embedded', 'true');
script.setAttribute('cdn', 'https://cdn.jsdelivr.net/npm/chii/public');
document.head.appendChild(script);

script.onload = render_app;
};

setTimeout(() => {
window.dispatchEvent(new Event('preview_ready'));
}, 0);
} else {
render_app();
}

}
//# sourceURL=playground:output
`);
Expand Down Expand Up @@ -282,7 +287,7 @@
'allow-pointer-lock',
'allow-modals',
can_escape ? 'allow-popups-to-escape-sandbox' : '',
relaxed ? 'allow-same-origin' : ''
'allow-same-origin'
].join(' ')}
class={error || pending || pending_imports ? 'greyed-out' : ''}
srcdoc={BROWSER ? srcdoc : ''}
Expand All @@ -295,18 +300,9 @@

<div class="iframe-container">
{#if !onLog}
<PaneWithPanel pos="100%" panel="Console" {main}>
{#snippet header()}
<button class="raised" disabled={logs.length === 0} on:click|stopPropagation={clear_logs}>
{#if logs.length > 0}
({logs.length})
{/if}
Clear
</button>
{/snippet}

<PaneWithPanel pos="100%" panel="DevTools" {main}>
{#snippet body()}
<Console {logs} />
<DevTools {iframe} />
{/snippet}
</PaneWithPanel>
{:else}
Expand Down
31 changes: 31 additions & 0 deletions packages/repl/src/lib/Output/devtools/DevTools.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
const { iframe } = $props();

let devtools_iframe: HTMLElement;

$effect(() => {
if (iframe) {
const iframe_window = iframe.contentWindow;
iframe_window.addEventListener('preview_ready', () => {
iframe_window.ChiiDevtoolsIframe = devtools_iframe;
iframe_window.initialize_devtools();
});

window.addEventListener('message', (event) => {
if (typeof event.data === 'string') {
iframe_window.postMessage(event.data, event.origin);
}
});
}
});
</script>

<iframe title="Devtools" bind:this={devtools_iframe}></iframe>

<style>
iframe {
width: 100%;
height: 100%;
border: 0;
}
</style>
173 changes: 3 additions & 170 deletions packages/repl/src/lib/Output/srcdoc/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,6 @@
<script>
(function () {
function send(payload, origin = '*') {
if (payload.command === 'info' && payload.args[0] instanceof Error) {
const error = payload.args[0];

if (/^(CreatedAt|UpdatedAt|TracedAt)Error$/.test(error.name)) {
// structuredClone obliterates useful info
// TODO do this for all errors?
payload.args[0] = {
type: '__error',
name: error.name,
message: error.message,
stack: error.stack
};
}
}

parent.postMessage(payload, origin);
}

Expand Down Expand Up @@ -132,7 +117,9 @@
});
}

reply({ action: 'cmd_ok' });
if (action !== undefined) {
reply({ action: 'cmd_ok' });
}
} catch ({ message, stack }) {
reply({ action: 'cmd_error', message, stack });
}
Expand All @@ -145,160 +132,6 @@
window.addEventListener('unhandledrejection', (event) => {
send({ action: 'unhandledrejection', value: event.reason });
});

// Intercept console methods
const timers = new Map();
const counters = new Map();

function log(command, opts) {
try {
send({ action: 'console', command, ...opts });
} catch {
send({ action: 'console', command: 'unclonable' });
}
}

function stringify(args) {
try {
return JSON.stringify(args, (key, value) => {
// if we don't do this, our Set/Map from svelte/reactivity would show up wrong in the console
if (value instanceof Map) {
return { type: 'Map', value };
}

if (value instanceof Set) {
return { type: 'Set', value };
}

// if we don't handle bigints separately, they will cause JSON.stringify to blow up
if (typeof value === 'bigint') {
return { type: 'BigInt', value: value + '' };
}

return value;
});
} catch (error) {
return null;
}
}

/** @param {string} method */
function stack(method) {
return new Error().stack
.split('\n')
.filter((line) => {
if (/[(@]about:srcdoc/.test(line)) return false;
return true;
})
.slice(1)
.map((line) => {
line = line
.replace('console[method]', `console.${method}`)
.replace(/console\.<computed> \[as \w+\]/, `console.${method}`);

let match =
/^\s+at (.+) \((.+:\d+:\d+)\)/.exec(line) || /^(.+)@(.+:\d+:\d+)?/.exec(line);

if (match) {
return {
label: match[1],
location: match[2]
};
}

return null;
})
.filter((x) => x);
}

const can_dedupe = ['log', 'info', 'dir', 'warn', 'error', 'assert', 'trace'];

const methods = {
clear: () => log('clear'),
// TODO make the command 'push' and the level/type 'info'
log: (...args) => log('info', { args }),
info: (...args) => log('info', { args }),
dir: (...args) => log('info', { args: [args[0]], expanded: true }),
warn: (...args) => log('warn', { args, stack: stack('warn'), collapsed: true }),
error: (...args) => log('error', { args, stack: stack('error'), collapsed: true }),
assert: (condition, ...args) => {
if (condition) return;
log('error', {
args: ['Assertion failed:', ...args],
stack: stack('assert'),
collapsed: true
});
},
group: (...args) => log('group', { args, collapsed: false }),
groupCollapsed: (...args) => log('group', { args, collapsed: true }),
groupEnd: () => log('groupEnd'),
table: (...args) => {
const data = args[0];
if (data && typeof data === 'object') {
log('table', { data, columns: args[1] });
} else {
log('info', { args });
}
},
time: (label = 'default') => timers.set(label, performance.now()),
timeLog: (label = 'default') => {
const now = performance.now();
if (timers.has(label)) {
log('info', { args: [`${label}: ${now - timers.get(label)}ms`] });
} else {
log('warn', { args: [`Timer '${label}' does not exist`] });
}
},
timeEnd: (label = 'default') => {
const now = performance.now();
if (timers.has(label)) {
log('info', { args: [`${label}: ${now - timers.get(label)}ms`] });
} else {
log('warn', { args: [`Timer '${label}' does not exist`] });
}
timers.delete(label);
},
count: (label = 'default') => {
counters.set(label, (counters.get(label) || 0) + 1);
log('info', { args: [`${label}: ${counters.get(label)}`] });
},
countReset: (label = 'default') => {
if (counters.has(label)) {
counters.set(label, 0);
} else {
log('warn', { args: [`Count for '${label}' does not exist`] });
}
},
trace: (...args) => {
log('info', {
args: args.length === 0 ? ['console.trace'] : args,
stack: stack('trace'),
collapsed: false
});
}
};

let previous = '';

for (const method in methods) {
const original = console[method];

console[method] = (...args) => {
const stack = new Error().stack;

if (
previous === (previous = stringify({ method, args, stack })) &&
can_dedupe.includes(method) &&
args.every((arg) => !arg || typeof arg !== 'object')
) {
send({ action: 'console', command: 'duplicate' });
} else {
methods[method](...args);
}

original(...args);
};
}
})();
</script>
</head>
Expand Down
Loading