workflow: sfc playground
This commit is contained in:
57
packages/sfc-playground/src/output/Output.vue
Normal file
57
packages/sfc-playground/src/output/Output.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="tab-buttons">
|
||||
<button v-for="m of modes" :class="{ active: mode === m }" @click="mode = m">{{ m }}</button>
|
||||
</div>
|
||||
|
||||
<div class="output-container">
|
||||
<Preview v-if="mode === 'preview'" :code="store.compiled.executed" />
|
||||
<CodeMirror
|
||||
v-else
|
||||
readonly
|
||||
:mode="mode === 'css' ? 'css' : 'javascript'"
|
||||
:value="store.compiled[mode]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Preview from './Preview.vue'
|
||||
import CodeMirror from '../codemirror/CodeMirror.vue'
|
||||
import { store } from '../store'
|
||||
import { ref } from 'vue'
|
||||
|
||||
type Modes = 'preview' | 'executed' | 'js' | 'css' | 'template'
|
||||
|
||||
const modes: Modes[] = ['preview', 'js', 'css', 'template', 'executed']
|
||||
const mode = ref<Modes>('preview')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.output-container {
|
||||
height: calc(100% - 35px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.tab-buttons {
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.tab-buttons button {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: #f8f8f8;
|
||||
padding: 8px 16px 6px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button.active {
|
||||
color: #42b983;
|
||||
border-bottom: 3px solid #42b983;
|
||||
}
|
||||
</style>
|
||||
108
packages/sfc-playground/src/output/Preview.vue
Normal file
108
packages/sfc-playground/src/output/Preview.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<iframe
|
||||
id="preview"
|
||||
ref="iframe"
|
||||
sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation"
|
||||
:srcdoc="srcdoc"
|
||||
></iframe>
|
||||
<Message :err="runtimeError" />
|
||||
<Message v-if="!runtimeError" :warn="runtimeWarning" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from '../Message.vue'
|
||||
import { ref, onMounted, onUnmounted, watchEffect, defineProps } from 'vue'
|
||||
import srcdoc from './srcdoc.html?raw'
|
||||
import { PreviewProxy } from './PreviewProxy'
|
||||
import { sandboxVueURL } from '../store'
|
||||
|
||||
const props = defineProps<{ code: string }>()
|
||||
|
||||
const iframe = ref()
|
||||
const runtimeError = ref()
|
||||
const runtimeWarning = ref()
|
||||
|
||||
let proxy: PreviewProxy
|
||||
|
||||
async function updatePreview() {
|
||||
if (!props.code?.trim()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
proxy.eval(`
|
||||
${props.code}
|
||||
|
||||
if (window.vueApp) {
|
||||
window.vueApp.unmount()
|
||||
}
|
||||
const container = document.getElementById('app')
|
||||
container.innerHTML = ''
|
||||
|
||||
import { createApp as _createApp } from "${sandboxVueURL}"
|
||||
const app = window.vueApp = _createApp(__comp)
|
||||
|
||||
app.config.errorHandler = e => console.error(e)
|
||||
|
||||
app.mount(container)
|
||||
`)
|
||||
} catch (e) {
|
||||
runtimeError.value = e.message
|
||||
return
|
||||
}
|
||||
runtimeError.value = null
|
||||
runtimeWarning.value = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
proxy = new PreviewProxy(iframe.value, {
|
||||
on_fetch_progress: (progress: any) => {
|
||||
// pending_imports = progress;
|
||||
},
|
||||
on_error: (event: any) => {
|
||||
// push_logs({ level: 'error', args: [event.value] });
|
||||
runtimeError.value = event.value
|
||||
},
|
||||
on_unhandled_rejection: (event: any) => {
|
||||
let error = event.value
|
||||
if (typeof error === 'string') error = { message: error }
|
||||
runtimeError.value = 'Uncaught (in promise): ' + error.message
|
||||
},
|
||||
on_console: (log: any) => {
|
||||
if (log.level === 'error') {
|
||||
runtimeError.value = log.args.join('')
|
||||
} else if (log.level === 'warn') {
|
||||
if (log.args[0].toString().includes('[Vue warn]')) {
|
||||
runtimeWarning.value = log.args.join('').replace(/\[Vue warn\]:/, '').trim()
|
||||
}
|
||||
}
|
||||
},
|
||||
on_console_group: (action: any) => {
|
||||
// group_logs(action.label, false);
|
||||
},
|
||||
on_console_group_end: () => {
|
||||
// ungroup_logs();
|
||||
},
|
||||
on_console_group_collapsed: (action: any) => {
|
||||
// group_logs(action.label, true);
|
||||
}
|
||||
})
|
||||
|
||||
iframe.value.addEventListener('load', () => {
|
||||
proxy.handle_links();
|
||||
watchEffect(updatePreview)
|
||||
});
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
proxy.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
96
packages/sfc-playground/src/output/PreviewProxy.ts
Normal file
96
packages/sfc-playground/src/output/PreviewProxy.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// ReplProxy and srcdoc implementation from Svelte REPL
|
||||
// MIT License https://github.com/sveltejs/svelte-repl/blob/master/LICENSE
|
||||
|
||||
let uid = 1
|
||||
|
||||
export class PreviewProxy {
|
||||
iframe: HTMLIFrameElement
|
||||
handlers: Record<string, Function>
|
||||
pending_cmds: Map<
|
||||
number,
|
||||
{ resolve: (value: unknown) => void; reject: (reason?: any) => void }
|
||||
>
|
||||
handle_event: (e: any) => void
|
||||
|
||||
constructor(iframe: HTMLIFrameElement, handlers: Record<string, Function>) {
|
||||
this.iframe = iframe
|
||||
this.handlers = handlers
|
||||
|
||||
this.pending_cmds = new Map()
|
||||
|
||||
this.handle_event = e => this.handle_repl_message(e)
|
||||
window.addEventListener('message', this.handle_event, false)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
window.removeEventListener('message', this.handle_event)
|
||||
}
|
||||
|
||||
iframe_command(action: string, args: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cmd_id = uid++
|
||||
|
||||
this.pending_cmds.set(cmd_id, { resolve, reject })
|
||||
|
||||
this.iframe.contentWindow!.postMessage({ action, cmd_id, args }, '*')
|
||||
})
|
||||
}
|
||||
|
||||
handle_command_message(cmd_data: any) {
|
||||
let action = cmd_data.action
|
||||
let id = cmd_data.cmd_id
|
||||
let handler = this.pending_cmds.get(id)
|
||||
|
||||
if (handler) {
|
||||
this.pending_cmds.delete(id)
|
||||
if (action === 'cmd_error') {
|
||||
let { message, stack } = cmd_data
|
||||
let e = new Error(message)
|
||||
e.stack = stack
|
||||
handler.reject(e)
|
||||
}
|
||||
|
||||
if (action === 'cmd_ok') {
|
||||
handler.resolve(cmd_data.args)
|
||||
}
|
||||
} else {
|
||||
console.error('command not found', id, cmd_data, [
|
||||
...this.pending_cmds.keys()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
handle_repl_message(event: any) {
|
||||
if (event.source !== this.iframe.contentWindow) return
|
||||
|
||||
const { action, args } = event.data
|
||||
|
||||
switch (action) {
|
||||
case 'cmd_error':
|
||||
case 'cmd_ok':
|
||||
return this.handle_command_message(event.data)
|
||||
case 'fetch_progress':
|
||||
return this.handlers.on_fetch_progress(args.remaining)
|
||||
case 'error':
|
||||
return this.handlers.on_error(event.data)
|
||||
case 'unhandledrejection':
|
||||
return this.handlers.on_unhandled_rejection(event.data)
|
||||
case 'console':
|
||||
return this.handlers.on_console(event.data)
|
||||
case 'console_group':
|
||||
return this.handlers.on_console_group(event.data)
|
||||
case 'console_group_collapsed':
|
||||
return this.handlers.on_console_group_collapsed(event.data)
|
||||
case 'console_group_end':
|
||||
return this.handlers.on_console_group_end(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
eval(script: string) {
|
||||
return this.iframe_command('eval', { script })
|
||||
}
|
||||
|
||||
handle_links() {
|
||||
return this.iframe_command('catch_clicks', {})
|
||||
}
|
||||
}
|
||||
201
packages/sfc-playground/src/output/srcdoc.html
Normal file
201
packages/sfc-playground/src/output/srcdoc.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
</style>
|
||||
<style id="__sfc-styles"></style>
|
||||
<script>
|
||||
(function(){
|
||||
let scriptEl
|
||||
|
||||
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 {
|
||||
if (scriptEl) {
|
||||
document.head.removeChild(scriptEl)
|
||||
}
|
||||
scriptEl = document.createElement('script')
|
||||
scriptEl.setAttribute('type', 'module')
|
||||
scriptEl.innerHTML = ev.data.args.script
|
||||
document.head.appendChild(scriptEl)
|
||||
send_ok();
|
||||
} catch (e) {
|
||||
send_error(e.message, e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
send_ok();
|
||||
} catch(e) {
|
||||
send_error(e.message, e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handle_message, false);
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
parent.postMessage({ action: 'error', value: error }, '*');
|
||||
}
|
||||
|
||||
window.addEventListener("unhandledrejection", event => {
|
||||
parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*');
|
||||
});
|
||||
}).call(this);
|
||||
|
||||
let previous = { level: null, args: null };
|
||||
|
||||
['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => {
|
||||
const original = console[level];
|
||||
console[level] = (...args) => {
|
||||
if (String(args[0]).includes('You are running a development build of Vue')) {
|
||||
return
|
||||
}
|
||||
const stringifiedArgs = stringify(args);
|
||||
if (
|
||||
previous.level === level &&
|
||||
previous.args &&
|
||||
previous.args === stringifiedArgs
|
||||
) {
|
||||
parent.postMessage({ action: 'console', level, duplicate: true }, '*');
|
||||
} else {
|
||||
previous = { level, args: stringifiedArgs };
|
||||
|
||||
try {
|
||||
parent.postMessage({ action: 'console', level, args }, '*');
|
||||
} catch (err) {
|
||||
parent.postMessage({ action: 'console', level: 'unclonable' }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
original(...args);
|
||||
}
|
||||
});
|
||||
|
||||
[
|
||||
{ method: 'group', action: 'console_group' },
|
||||
{ method: 'groupEnd', action: 'console_group_end' },
|
||||
{ method: 'groupCollapsed', action: 'console_group_collapsed' },
|
||||
].forEach((group_action) => {
|
||||
const original = console[group_action.method];
|
||||
console[group_action.method] = (label) => {
|
||||
parent.postMessage({ action: group_action.action, label }, '*');
|
||||
|
||||
original(label);
|
||||
};
|
||||
});
|
||||
|
||||
const timers = new Map();
|
||||
const original_time = console.time;
|
||||
const original_timelog = console.timeLog;
|
||||
const original_timeend = console.timeEnd;
|
||||
|
||||
console.time = (label = 'default') => {
|
||||
original_time(label);
|
||||
timers.set(label, performance.now());
|
||||
}
|
||||
console.timeLog = (label = 'default') => {
|
||||
original_timelog(label);
|
||||
const now = performance.now();
|
||||
if (timers.has(label)) {
|
||||
parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*');
|
||||
} else {
|
||||
parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*');
|
||||
}
|
||||
}
|
||||
console.timeEnd = (label = 'default') => {
|
||||
original_timeend(label);
|
||||
const now = performance.now();
|
||||
if (timers.has(label)) {
|
||||
parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*');
|
||||
} else {
|
||||
parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*');
|
||||
}
|
||||
timers.delete(label);
|
||||
};
|
||||
|
||||
const original_assert = console.assert;
|
||||
console.assert = (condition, ...args) => {
|
||||
if (condition) {
|
||||
const stack = new Error().stack;
|
||||
parent.postMessage({ action: 'console', level: 'assert', args, stack }, '*');
|
||||
}
|
||||
original_assert(condition, ...args);
|
||||
};
|
||||
|
||||
const counter = new Map();
|
||||
const original_count = console.count;
|
||||
const original_countreset = console.countReset;
|
||||
|
||||
console.count = (label = 'default') => {
|
||||
counter.set(label, (counter.get(label) || 0) + 1);
|
||||
parent.postMessage({ action: 'console', level: 'system-log', args: `${label}: ${counter.get(label)}` }, '*');
|
||||
original_count(label);
|
||||
};
|
||||
|
||||
console.countReset = (label = 'default') => {
|
||||
if (counter.has(label)) {
|
||||
counter.set(label, 0);
|
||||
} else {
|
||||
parent.postMessage({ action: 'console', level: 'system-warn', args: `Count for '${label}' does not exist` }, '*');
|
||||
}
|
||||
original_countreset(label);
|
||||
};
|
||||
|
||||
const original_trace = console.trace;
|
||||
|
||||
console.trace = (...args) => {
|
||||
const stack = new Error().stack;
|
||||
parent.postMessage({ action: 'console', level: 'trace', args, stack }, '*');
|
||||
original_trace(...args);
|
||||
};
|
||||
|
||||
function stringify(args) {
|
||||
try {
|
||||
return JSON.stringify(args);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user