-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
358 additions
and
295 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,312 +1,34 @@ | ||
<script setup lang="ts"> | ||
import Message from '../Message.vue' | ||
import { | ||
type WatchStopHandle, | ||
inject, | ||
onMounted, | ||
onUnmounted, | ||
ref, | ||
useTemplateRef, | ||
watch, | ||
watchEffect, | ||
} from 'vue' | ||
import srcdoc from './srcdoc.html?raw' | ||
import { PreviewProxy } from './PreviewProxy' | ||
import { compileModulesForPreview } from './moduleCompiler' | ||
import { computed, inject, useTemplateRef } from 'vue' | ||
import { injectKeyProps } from '../../src/types' | ||
import Sandbox from './Sandbox.vue' | ||
const props = defineProps<{ show: boolean; ssr: boolean }>() | ||
const { store, clearConsole, theme, previewTheme, previewOptions } = | ||
inject(injectKeyProps)! | ||
const containerRef = useTemplateRef('container') | ||
const runtimeError = ref<string>() | ||
const runtimeWarning = ref<string>() | ||
let sandbox: HTMLIFrameElement | ||
let proxy: PreviewProxy | ||
let stopUpdateWatcher: WatchStopHandle | undefined | ||
// create sandbox on mount | ||
onMounted(createSandbox) | ||
// reset sandbox when import map changes | ||
watch( | ||
() => store.value.getImportMap(), | ||
() => { | ||
try { | ||
createSandbox() | ||
} catch (e: any) { | ||
store.value.errors = [e as Error] | ||
return | ||
} | ||
}, | ||
const sandboxTheme = computed(() => | ||
previewTheme.value ? theme.value : undefined, | ||
) | ||
function switchPreviewTheme() { | ||
if (!previewTheme.value) return | ||
const sandboxRef = useTemplateRef('sandbox') | ||
const container = computed(() => sandboxRef.value?.container) | ||
const html = sandbox.contentDocument?.documentElement | ||
if (html) { | ||
html.className = theme.value | ||
} else { | ||
// re-create sandbox | ||
createSandbox() | ||
} | ||
} | ||
// reset theme | ||
watch([theme, previewTheme], switchPreviewTheme) | ||
onUnmounted(() => { | ||
proxy.destroy() | ||
stopUpdateWatcher && stopUpdateWatcher() | ||
defineExpose({ | ||
reload: () => sandboxRef.value?.reload(), | ||
container, | ||
}) | ||
function createSandbox() { | ||
if (sandbox) { | ||
// clear prev sandbox | ||
proxy.destroy() | ||
stopUpdateWatcher && stopUpdateWatcher() | ||
containerRef.value?.removeChild(sandbox) | ||
} | ||
sandbox = document.createElement('iframe') | ||
sandbox.setAttribute( | ||
'sandbox', | ||
[ | ||
'allow-forms', | ||
'allow-modals', | ||
'allow-pointer-lock', | ||
'allow-popups', | ||
'allow-same-origin', | ||
'allow-scripts', | ||
'allow-top-navigation-by-user-activation', | ||
].join(' '), | ||
) | ||
const importMap = store.value.getImportMap() | ||
const sandboxSrc = srcdoc | ||
.replace( | ||
/<html>/, | ||
`<html class="${previewTheme.value ? theme.value : ''}">`, | ||
) | ||
.replace(/<!--IMPORT_MAP-->/, JSON.stringify(importMap)) | ||
.replace( | ||
/<!-- PREVIEW-OPTIONS-HEAD-HTML -->/, | ||
previewOptions.value?.headHTML || '', | ||
) | ||
.replace( | ||
/<!--PREVIEW-OPTIONS-PLACEHOLDER-HTML-->/, | ||
previewOptions.value?.placeholderHTML || '', | ||
) | ||
sandbox.srcdoc = sandboxSrc | ||
containerRef.value?.appendChild(sandbox) | ||
proxy = new PreviewProxy(sandbox, { | ||
on_fetch_progress: (progress: any) => { | ||
// pending_imports = progress; | ||
}, | ||
on_error: (event: any) => { | ||
const msg = | ||
event.value instanceof Error ? event.value.message : event.value | ||
if ( | ||
msg.includes('Failed to resolve module specifier') || | ||
msg.includes('Error resolving module specifier') | ||
) { | ||
runtimeError.value = | ||
msg.replace(/\. Relative references must.*$/, '') + | ||
`.\nTip: edit the "Import Map" tab to specify import paths for dependencies.` | ||
} else { | ||
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.duplicate) { | ||
return | ||
} | ||
if (log.level === 'error') { | ||
if (log.args[0] instanceof Error) { | ||
runtimeError.value = log.args[0].message | ||
} else { | ||
runtimeError.value = log.args[0] | ||
} | ||
} 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); | ||
}, | ||
}) | ||
sandbox.addEventListener('load', () => { | ||
proxy.handle_links() | ||
stopUpdateWatcher = watchEffect(updatePreview) | ||
switchPreviewTheme() | ||
}) | ||
} | ||
async function updatePreview() { | ||
if (import.meta.env.PROD && clearConsole.value) { | ||
console.clear() | ||
} | ||
runtimeError.value = undefined | ||
runtimeWarning.value = undefined | ||
let isSSR = props.ssr | ||
if (store.value.vueVersion) { | ||
const [major, minor, patch] = store.value.vueVersion | ||
.split('.') | ||
.map((v) => parseInt(v, 10)) | ||
if (major === 3 && (minor < 2 || (minor === 2 && patch < 27))) { | ||
alert( | ||
`The selected version of Vue (${store.value.vueVersion}) does not support in-browser SSR.` + | ||
` Rendering in client mode instead.`, | ||
) | ||
isSSR = false | ||
} | ||
} | ||
try { | ||
const { mainFile } = store.value | ||
// if SSR, generate the SSR bundle and eval it to render the HTML | ||
if (isSSR && mainFile.endsWith('.vue')) { | ||
const ssrModules = compileModulesForPreview(store.value, true) | ||
console.info( | ||
`[@vue/repl] successfully compiled ${ssrModules.length} modules for SSR.`, | ||
) | ||
await proxy.eval([ | ||
`const __modules__ = {};`, | ||
...ssrModules, | ||
`import { renderToString as _renderToString } from 'vue/server-renderer' | ||
import { createSSRApp as _createApp } from 'vue' | ||
const AppComponent = __modules__["${mainFile}"].default | ||
AppComponent.name = 'Repl' | ||
const app = _createApp(AppComponent) | ||
if (!app.config.hasOwnProperty('unwrapInjectedRef')) { | ||
app.config.unwrapInjectedRef = true | ||
} | ||
app.config.warnHandler = () => {} | ||
window.__ssr_promise__ = _renderToString(app).then(html => { | ||
document.body.innerHTML = '<div id="app">' + html + '</div>' + \`${ | ||
previewOptions.value?.bodyHTML || '' | ||
}\` | ||
}).catch(err => { | ||
console.error("SSR Error", err) | ||
}) | ||
`, | ||
]) | ||
} | ||
// compile code to simulated module system | ||
const modules = compileModulesForPreview(store.value) | ||
console.info( | ||
`[@vue/repl] successfully compiled ${modules.length} module${ | ||
modules.length > 1 ? `s` : `` | ||
}.`, | ||
) | ||
const codeToEval = [ | ||
`window.__modules__ = {};window.__css__ = [];` + | ||
`if (window.__app__) window.__app__.unmount();` + | ||
(isSSR | ||
? `` | ||
: `document.body.innerHTML = '<div id="app"></div>' + \`${ | ||
previewOptions.value?.bodyHTML || '' | ||
}\``), | ||
...modules, | ||
`document.querySelectorAll('style[css]').forEach(el => el.remove()) | ||
document.head.insertAdjacentHTML('beforeend', window.__css__.map(s => \`<style css>\${s}</style>\`).join('\\n'))`, | ||
] | ||
// if main file is a vue file, mount it. | ||
if (mainFile.endsWith('.vue')) { | ||
codeToEval.push( | ||
`import { ${ | ||
isSSR ? `createSSRApp` : `createApp` | ||
} as _createApp } from "vue" | ||
${previewOptions.value?.customCode?.importCode || ''} | ||
const _mount = () => { | ||
const AppComponent = __modules__["${mainFile}"].default | ||
AppComponent.name = 'Repl' | ||
const app = window.__app__ = _createApp(AppComponent) | ||
if (!app.config.hasOwnProperty('unwrapInjectedRef')) { | ||
app.config.unwrapInjectedRef = true | ||
} | ||
app.config.errorHandler = e => console.error(e) | ||
${previewOptions.value?.customCode?.useCode || ''} | ||
app.mount('#app') | ||
} | ||
if (window.__ssr_promise__) { | ||
window.__ssr_promise__.then(_mount) | ||
} else { | ||
_mount() | ||
}`, | ||
) | ||
} | ||
// eval code in sandbox | ||
await proxy.eval(codeToEval) | ||
} catch (e: any) { | ||
console.error(e) | ||
runtimeError.value = (e as Error).message | ||
} | ||
} | ||
/** | ||
* Reload the preview iframe | ||
*/ | ||
function reload() { | ||
sandbox.contentWindow?.location.reload() | ||
} | ||
defineExpose({ reload, container: containerRef }) | ||
</script> | ||
|
||
<template> | ||
<div | ||
v-show="show" | ||
ref="container" | ||
class="iframe-container" | ||
:class="{ [theme]: previewTheme }" | ||
/> | ||
<Message :err="(previewOptions?.showRuntimeError ?? true) && runtimeError" /> | ||
<Message | ||
v-if="!runtimeError && (previewOptions?.showRuntimeWarning ?? true)" | ||
:warn="runtimeWarning" | ||
<Sandbox | ||
ref="sandbox" | ||
:show="props.show" | ||
:store="store" | ||
:theme="sandboxTheme" | ||
:preview-options="previewOptions" | ||
:ssr="props.ssr" | ||
:clear-console="clearConsole" | ||
/> | ||
</template> | ||
|
||
<style scoped> | ||
.iframe-container, | ||
.iframe-container :deep(iframe) { | ||
width: 100%; | ||
height: 100%; | ||
border: none; | ||
background-color: #fff; | ||
} | ||
.iframe-container.dark :deep(iframe) { | ||
background-color: #1e1e1e; | ||
} | ||
</style> |
Oops, something went wrong.