<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>
+ <div class="preview-container" ref="container">
+</div>
<Message :err="runtimeError" />
<Message v-if="!runtimeError" :warn="runtimeWarning" />
</template>
<script setup lang="ts">
import Message from '../Message.vue'
-import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
+import { ref, onMounted, onUnmounted, watchEffect, watch } from 'vue'
import type { WatchStopHandle } from 'vue'
import srcdoc from './srcdoc.html?raw'
import { PreviewProxy } from './PreviewProxy'
-import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
+import { MAIN_FILE, vueRuntimeUrl } from '../sfcCompiler'
import { compileModulesForPreview } from './moduleCompiler'
+import { store } from '../store'
-const iframe = ref()
+const container = ref()
const runtimeError = ref()
const runtimeWarning = ref()
+let sandbox: HTMLIFrameElement
let proxy: PreviewProxy
-let updateHandle: WatchStopHandle
+let stopUpdateWatcher: WatchStopHandle
-async function updatePreview() {
- runtimeError.value = null
- runtimeWarning.value = null
+// create sandbox on mount
+onMounted(createSandbox)
+
+// reset sandbox when import map changes
+watch(() => store.importMap, (importMap, prev) => {
+ if (!importMap) {
+ if (prev) {
+ // import-map.json deleted
+ createSandbox()
+ }
+ return
+ }
try {
- const modules = compileModulesForPreview()
- console.log(`successfully compiled ${modules.length} modules.`)
- // reset modules
- await proxy.eval([
- `window.__modules__ = {};window.__css__ = ''`,
- ...modules,
- `
-import { createApp as _createApp } from "${SANDBOX_VUE_URL}"
+ const map = JSON.parse(importMap)
+ if (!map.imports) {
+ store.errors = [
+ `import-map.json is missing "imports" field.`
+ ]
+ return
+ }
+ if (map.imports.vue) {
+ store.errors = [
+ 'Select Vue versions using the top-right dropdown.\n' +
+ 'Specifying it in the import map has no effect.'
+ ]
+ }
+ createSandbox()
+ } catch (e) {
+ store.errors = [e]
+ return
+ }
+})
-if (window.__app__) {
- window.__app__.unmount()
- document.getElementById('app').innerHTML = ''
-}
+// reset sandbox when version changes
+watch(vueRuntimeUrl, createSandbox)
-document.getElementById('__sfc-styles').innerHTML = window.__css__
-const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default)
-app.config.errorHandler = e => console.error(e)
-app.mount('#app')`.trim()
- ])
+onUnmounted(() => {
+ proxy.destroy()
+ stopUpdateWatcher && stopUpdateWatcher()
+})
+
+function createSandbox() {
+ if (sandbox) {
+ // clear prev sandbox
+ proxy.destroy()
+ stopUpdateWatcher()
+ container.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(' '))
+
+ let importMap: Record<string, any>
+ try {
+ importMap = JSON.parse(store.importMap || `{}`)
} catch (e) {
- runtimeError.value = e.stack
+ store.errors = [`Syntax error in import-map.json: ${e.message}`]
+ return
}
-}
-onMounted(() => {
- proxy = new PreviewProxy(iframe.value, {
+ if (!importMap.imports) {
+ importMap.imports = {}
+ }
+ importMap.imports.vue = vueRuntimeUrl.value
+ const sandboxSrc = srcdoc.replace(/<!--IMPORT_MAP-->/, JSON.stringify(importMap))
+ sandbox.setAttribute('srcdoc', sandboxSrc)
+ container.value.appendChild(sandbox)
+
+ proxy = new PreviewProxy(sandbox, {
on_fetch_progress: (progress: any) => {
// pending_imports = progress;
},
}
})
- iframe.value.addEventListener('load', () => {
+ sandbox.addEventListener('load', () => {
proxy.handle_links()
- updateHandle = watchEffect(updatePreview)
+ stopUpdateWatcher = watchEffect(updatePreview)
})
-})
+}
-onUnmounted(() => {
- proxy.destroy()
- updateHandle && updateHandle()
-})
+async function updatePreview() {
+ runtimeError.value = null
+ runtimeWarning.value = null
+ try {
+ const modules = compileModulesForPreview()
+ console.log(`successfully compiled ${modules.length} modules.`)
+ // reset modules
+ await proxy.eval([
+ `window.__modules__ = {};window.__css__ = ''`,
+ ...modules,
+ `
+import { createApp as _createApp } from "vue"
+
+if (window.__app__) {
+ window.__app__.unmount()
+ document.getElementById('app').innerHTML = ''
+}
+
+document.getElementById('__sfc-styles').innerHTML = window.__css__
+const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default)
+app.config.errorHandler = e => console.error(e)
+app.mount('#app')`.trim()
+ ])
+ } catch (e) {
+ runtimeError.value = e.stack
+ }
+}
</script>
<style>
+.preview-container,
iframe {
width: 100%;
height: 100%;
}
</style>
<style id="__sfc-styles"></style>
- <script type="module">
- let scriptEls = []
- window.__modules__ = {}
+ <!-- ES Module Shims: Import maps polyfill for modules browsers without import maps support (all except Chrome 89+) -->
+ <script async src="https://ga.jspm.io/npm:es-module-shims@0.10.1/dist/es-module-shims.min.js"></script>
+ <script id="map" type="importmap"><!--IMPORT_MAP--></script>
- window.__export__ = (mod, key, get) => {
- Object.defineProperty(mod, key, {
- enumerable: true,
- configurable: true,
- get
- })
- }
+ <script>
+ (() => {
+ let scriptEls = []
- window.__dynamic_import__ = key => {
- return Promise.resolve(window.__modules__[key])
- }
+ window.__modules__ = {}
- async 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 });
+ window.__export__ = (mod, key, get) => {
+ Object.defineProperty(mod, key, {
+ enumerable: true,
+ configurable: true,
+ get
+ })
+ }
- if (action === 'eval') {
- try {
- if (scriptEls.length) {
- scriptEls.forEach(el => {
- document.head.removeChild(el)
- })
- scriptEls.length = 0
- }
+ window.__dynamic_import__ = key => {
+ return Promise.resolve(window.__modules__[key])
+ }
- let { script: scripts } = ev.data.args
- if (typeof scripts === 'string') scripts = [scripts]
-
- for (const script of scripts) {
- const scriptEl = document.createElement('script')
- scriptEl.setAttribute('type', 'module')
- // send ok in the module script to ensure sequential evaluation
- // of multiple proxy.eval() calls
- const done = new Promise((resolve, reject) => {
- window.__next__ = resolve
- scriptEl.onerror = reject
- })
- scriptEl.innerHTML = script + `\nwindow.__next__()`
- document.head.appendChild(scriptEl)
- scriptEls.push(scriptEl)
- await done
+ async 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 (scriptEls.length) {
+ scriptEls.forEach(el => {
+ document.head.removeChild(el)
+ })
+ scriptEls.length = 0
+ }
+
+ let { script: scripts } = ev.data.args
+ if (typeof scripts === 'string') scripts = [scripts]
+
+ for (const script of scripts) {
+ const scriptEl = document.createElement('script')
+ scriptEl.setAttribute('type', 'module')
+ // send ok in the module script to ensure sequential evaluation
+ // of multiple proxy.eval() calls
+ const done = new Promise((resolve, reject) => {
+ window.__next__ = resolve
+ scriptEl.onerror = reject
+ })
+ scriptEl.innerHTML = script + `\nwindow.__next__()`
+ document.head.appendChild(scriptEl)
+ scriptEls.push(scriptEl)
+ await done
+ }
+ window.__next__ = undefined
+ send_ok()
+ } catch (e) {
+ send_error(e.message, e.stack);
}
- window.__next__ = undefined
- 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;
+ 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.open(el.href, '_blank');
+ });
+ send_ok();
+ } catch(e) {
+ send_error(e.message, e.stack);
+ }
}
}
- }
- window.addEventListener('message', handle_message, false);
+ window.addEventListener('message', handle_message, false);
- window.onerror = function (msg, url, lineNo, columnNo, error) {
- parent.postMessage({ action: 'error', value: error }, '*');
- }
+ 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 }, '*');
+ });
- window.addEventListener("unhandledrejection", event => {
- parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*');
- });
+ let previous = { level: null, args: null };
- let previous = { level: null, args: null };
+ ['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => {
+ const original = console[level];
+ console[level] = (...args) => {
+ const msg = String(args[0])
+ if (msg.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' }, '*');
+ }
+ }
- ['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => {
- const original = console[level];
- console[level] = (...args) => {
- const msg = String(args[0])
- if (msg.includes('You are running a development build of Vue')) {
- return
+ 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`] }, '*');
}
- const stringifiedArgs = stringify(args);
- if (
- previous.level === level &&
- previous.args &&
- previous.args === stringifiedArgs
- ) {
- parent.postMessage({ action: 'console', level, duplicate: true }, '*');
+ }
+ 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 {
- previous = { level, args: stringifiedArgs };
+ parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*');
+ }
+ timers.delete(label);
+ };
- try {
- parent.postMessage({ action: 'console', level, args }, '*');
- } catch (err) {
- parent.postMessage({ action: 'console', level: 'unclonable' }, '*');
- }
+ 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);
+ };
- 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 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);
};
- });
- const timers = new Map();
- const original_time = console.time;
- const original_timelog = console.timeLog;
- const original_timeend = console.timeEnd;
+ 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);
+ };
- 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_trace = console.trace;
- const original_assert = console.assert;
- console.assert = (condition, ...args) => {
- if (condition) {
+ console.trace = (...args) => {
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;
+ 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>