workflow(sfc-playground): support import map
This commit is contained in:
parent
e097bd4dd5
commit
5ee7e6bc70
@ -24,7 +24,7 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
color:var(--base);
|
color: var(--base);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
--base: #444;
|
--base: #444;
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<li v-for="version of publishedVersions">
|
<li v-for="version of publishedVersions">
|
||||||
<a @click="setVueVersion(version)">v{{ version }}</a>
|
<a @click="setVueVersion(version)">v{{ version }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a @click="resetVueVersion">This Commit ({{ commit }})</a></li>
|
<li><a @click="resetVueVersion">This Commit ({{ currentCommit }})</a></li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://app.netlify.com/sites/vue-sfc-playground/deploys" target="_blank">Commits History</a>
|
<a href="https://app.netlify.com/sites/vue-sfc-playground/deploys" target="_blank">Commits History</a>
|
||||||
</li>
|
</li>
|
||||||
@ -51,8 +51,8 @@ import { downloadProject } from './download/download'
|
|||||||
import { setVersion, resetVersion } from './sfcCompiler'
|
import { setVersion, resetVersion } from './sfcCompiler'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
const commit = __COMMIT__
|
const currentCommit = __COMMIT__
|
||||||
const activeVersion = ref(`@${commit}`)
|
const activeVersion = ref(`@${currentCommit}`)
|
||||||
const publishedVersions = ref<string[]>()
|
const publishedVersions = ref<string[]>()
|
||||||
const expanded = ref(false)
|
const expanded = ref(false)
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ async function setVueVersion(v: string) {
|
|||||||
|
|
||||||
function resetVueVersion() {
|
function resetVueVersion() {
|
||||||
resetVersion()
|
resetVersion()
|
||||||
activeVersion.value = `@${commit}`
|
activeVersion.value = `@${currentCommit}`
|
||||||
expanded.value = false
|
expanded.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const onChange = debounce((code: string) => {
|
|||||||
|
|
||||||
const activeCode = ref(store.activeFile.code)
|
const activeCode = ref(store.activeFile.code)
|
||||||
const activeMode = computed(
|
const activeMode = computed(
|
||||||
() => (store.activeFilename.endsWith('.js') ? 'javascript' : 'htmlmixed')
|
() => (store.activeFilename.endsWith('.vue') ? 'htmlmixed' : 'javascript')
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
@ -45,8 +45,12 @@ function focus({ el }: VNode) {
|
|||||||
function doneAddFile() {
|
function doneAddFile() {
|
||||||
const filename = pendingFilename.value
|
const filename = pendingFilename.value
|
||||||
|
|
||||||
if (!filename.endsWith('.vue') && !filename.endsWith('.js')) {
|
if (
|
||||||
store.errors = [`Playground only supports .vue or .js files.`]
|
!filename.endsWith('.vue') &&
|
||||||
|
!filename.endsWith('.js') &&
|
||||||
|
filename !== 'import-map.json'
|
||||||
|
) {
|
||||||
|
store.errors = [`Playground only supports *.vue, *.js files or import-map.json.`]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,60 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<iframe
|
<div class="preview-container" ref="container">
|
||||||
id="preview"
|
</div>
|
||||||
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 :err="runtimeError" />
|
||||||
<Message v-if="!runtimeError" :warn="runtimeWarning" />
|
<Message v-if="!runtimeError" :warn="runtimeWarning" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Message from '../Message.vue'
|
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 type { WatchStopHandle } from 'vue'
|
||||||
import srcdoc from './srcdoc.html?raw'
|
import srcdoc from './srcdoc.html?raw'
|
||||||
import { PreviewProxy } from './PreviewProxy'
|
import { PreviewProxy } from './PreviewProxy'
|
||||||
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
|
import { MAIN_FILE, vueRuntimeUrl } from '../sfcCompiler'
|
||||||
import { compileModulesForPreview } from './moduleCompiler'
|
import { compileModulesForPreview } from './moduleCompiler'
|
||||||
|
import { store } from '../store'
|
||||||
|
|
||||||
const iframe = ref()
|
const container = ref()
|
||||||
const runtimeError = ref()
|
const runtimeError = ref()
|
||||||
const runtimeWarning = ref()
|
const runtimeWarning = ref()
|
||||||
|
|
||||||
|
let sandbox: HTMLIFrameElement
|
||||||
let proxy: PreviewProxy
|
let proxy: PreviewProxy
|
||||||
let updateHandle: WatchStopHandle
|
let stopUpdateWatcher: WatchStopHandle
|
||||||
|
|
||||||
async function updatePreview() {
|
// create sandbox on mount
|
||||||
runtimeError.value = null
|
onMounted(createSandbox)
|
||||||
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 "${SANDBOX_VUE_URL}"
|
|
||||||
|
|
||||||
if (window.__app__) {
|
// reset sandbox when import map changes
|
||||||
window.__app__.unmount()
|
watch(() => store.importMap, (importMap, prev) => {
|
||||||
document.getElementById('app').innerHTML = ''
|
if (!importMap) {
|
||||||
}
|
if (prev) {
|
||||||
|
// import-map.json deleted
|
||||||
document.getElementById('__sfc-styles').innerHTML = window.__css__
|
createSandbox()
|
||||||
const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default)
|
}
|
||||||
app.config.errorHandler = e => console.error(e)
|
return
|
||||||
app.mount('#app')`.trim()
|
|
||||||
])
|
|
||||||
} catch (e) {
|
|
||||||
runtimeError.value = e.stack
|
|
||||||
}
|
}
|
||||||
}
|
try {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
// reset sandbox when version changes
|
||||||
proxy = new PreviewProxy(iframe.value, {
|
watch(vueRuntimeUrl, createSandbox)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
store.errors = [`Syntax error in import-map.json: ${e.message}`]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
on_fetch_progress: (progress: any) => {
|
||||||
// pending_imports = progress;
|
// pending_imports = progress;
|
||||||
},
|
},
|
||||||
@ -93,19 +138,43 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
iframe.value.addEventListener('load', () => {
|
sandbox.addEventListener('load', () => {
|
||||||
proxy.handle_links()
|
proxy.handle_links()
|
||||||
updateHandle = watchEffect(updatePreview)
|
stopUpdateWatcher = watchEffect(updatePreview)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
async function updatePreview() {
|
||||||
proxy.destroy()
|
runtimeError.value = null
|
||||||
updateHandle && updateHandle()
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.preview-container,
|
||||||
iframe {
|
iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { store, File } from '../store'
|
import { store, File } from '../store'
|
||||||
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
|
import { MAIN_FILE } from '../sfcCompiler'
|
||||||
import {
|
import {
|
||||||
babelParse,
|
babelParse,
|
||||||
MagicString,
|
MagicString,
|
||||||
@ -92,15 +92,6 @@ function processFile(file: File, seen = new Set<File>()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.remove(node.start!, node.end!)
|
s.remove(node.start!, node.end!)
|
||||||
} else {
|
|
||||||
if (source === 'vue') {
|
|
||||||
// rewrite Vue imports
|
|
||||||
s.overwrite(
|
|
||||||
node.source.start!,
|
|
||||||
node.source.end!,
|
|
||||||
`"${SANDBOX_VUE_URL}"`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,222 +8,229 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<style id="__sfc-styles"></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) => {
|
<script>
|
||||||
Object.defineProperty(mod, key, {
|
(() => {
|
||||||
enumerable: true,
|
let scriptEls = []
|
||||||
configurable: true,
|
|
||||||
get
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.__dynamic_import__ = key => {
|
window.__modules__ = {}
|
||||||
return Promise.resolve(window.__modules__[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handle_message(ev) {
|
window.__export__ = (mod, key, get) => {
|
||||||
let { action, cmd_id } = ev.data;
|
Object.defineProperty(mod, key, {
|
||||||
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
|
enumerable: true,
|
||||||
const send_reply = (payload) => send_message({ ...payload, cmd_id });
|
configurable: true,
|
||||||
const send_ok = () => send_reply({ action: 'cmd_ok' });
|
get
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'catch_clicks') {
|
window.__dynamic_import__ = key => {
|
||||||
try {
|
return Promise.resolve(window.__modules__[key])
|
||||||
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
|
async function handle_message(ev) {
|
||||||
let el = event.target;
|
let { action, cmd_id } = ev.data;
|
||||||
while (el && el.nodeName !== 'A') el = el.parentNode;
|
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
|
||||||
if (!el || el.nodeName !== 'A') return;
|
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 (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
|
if (action === 'eval') {
|
||||||
|
try {
|
||||||
event.preventDefault();
|
if (scriptEls.length) {
|
||||||
|
scriptEls.forEach(el => {
|
||||||
if (el.href.startsWith(top_origin)) {
|
document.head.removeChild(el)
|
||||||
const url = new URL(el.href);
|
})
|
||||||
if (url.hash[0] === '#') {
|
scriptEls.length = 0
|
||||||
window.location.hash = url.hash;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(el.href, '_blank');
|
let { script: scripts } = ev.data.args
|
||||||
});
|
if (typeof scripts === 'string') scripts = [scripts]
|
||||||
send_ok();
|
|
||||||
} catch(e) {
|
|
||||||
send_error(e.message, e.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('message', handle_message, false);
|
for (const script of scripts) {
|
||||||
|
const scriptEl = document.createElement('script')
|
||||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
scriptEl.setAttribute('type', 'module')
|
||||||
parent.postMessage({ action: 'error', value: error }, '*');
|
// send ok in the module script to ensure sequential evaluation
|
||||||
}
|
// of multiple proxy.eval() calls
|
||||||
|
const done = new Promise((resolve, reject) => {
|
||||||
window.addEventListener("unhandledrejection", event => {
|
window.__next__ = resolve
|
||||||
parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*');
|
scriptEl.onerror = reject
|
||||||
});
|
})
|
||||||
|
scriptEl.innerHTML = script + `\nwindow.__next__()`
|
||||||
let previous = { level: null, args: null };
|
document.head.appendChild(scriptEl)
|
||||||
|
scriptEls.push(scriptEl)
|
||||||
['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => {
|
await done
|
||||||
const original = console[level];
|
}
|
||||||
console[level] = (...args) => {
|
window.__next__ = undefined
|
||||||
const msg = String(args[0])
|
send_ok()
|
||||||
if (msg.includes('You are running a development build of Vue')) {
|
} catch (e) {
|
||||||
return
|
send_error(e.message, e.stack);
|
||||||
}
|
|
||||||
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);
|
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);
|
||||||
{ 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);
|
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 }, '*');
|
||||||
|
});
|
||||||
|
|
||||||
|
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' }, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 timers = new Map();
|
const original_assert = console.assert;
|
||||||
const original_time = console.time;
|
console.assert = (condition, ...args) => {
|
||||||
const original_timelog = console.timeLog;
|
if (condition) {
|
||||||
const original_timeend = console.timeEnd;
|
const stack = new Error().stack;
|
||||||
|
parent.postMessage({ action: 'console', level: 'assert', args, stack }, '*');
|
||||||
|
}
|
||||||
|
original_assert(condition, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
console.time = (label = 'default') => {
|
const counter = new Map();
|
||||||
original_time(label);
|
const original_count = console.count;
|
||||||
timers.set(label, performance.now());
|
const original_countreset = console.countReset;
|
||||||
}
|
|
||||||
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.count = (label = 'default') => {
|
||||||
console.assert = (condition, ...args) => {
|
counter.set(label, (counter.get(label) || 0) + 1);
|
||||||
if (condition) {
|
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;
|
const stack = new Error().stack;
|
||||||
parent.postMessage({ action: 'console', level: 'assert', args, stack }, '*');
|
parent.postMessage({ action: 'console', level: 'trace', args, stack }, '*');
|
||||||
|
original_trace(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
function stringify(args) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(args);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { store, File } from './store'
|
import { store, File } from './store'
|
||||||
import { SFCDescriptor, BindingMetadata } from '@vue/compiler-sfc'
|
import { SFCDescriptor, BindingMetadata } from '@vue/compiler-sfc'
|
||||||
import * as defaultCompiler from '@vue/compiler-sfc'
|
import * as defaultCompiler from '@vue/compiler-sfc'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export const MAIN_FILE = 'App.vue'
|
export const MAIN_FILE = 'App.vue'
|
||||||
export const COMP_IDENTIFIER = `__sfc__`
|
export const COMP_IDENTIFIER = `__sfc__`
|
||||||
@ -16,23 +17,23 @@ const defaultVueUrl = import.meta.env.PROD
|
|||||||
? '/vue.runtime.esm-browser.js' // to be copied on build
|
? '/vue.runtime.esm-browser.js' // to be copied on build
|
||||||
: '/src/vue-dev-proxy'
|
: '/src/vue-dev-proxy'
|
||||||
|
|
||||||
export let SANDBOX_VUE_URL = defaultVueUrl
|
export const vueRuntimeUrl = ref(defaultVueUrl)
|
||||||
|
|
||||||
export async function setVersion(version: string) {
|
export async function setVersion(version: string) {
|
||||||
const compilerUrl = `https://unpkg.com/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js`
|
const compilerUrl = `https://unpkg.com/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js`
|
||||||
const runtimeUrl = `https://cdn.skypack.dev/@vue/runtime-dom@${version}`
|
const runtimeUrl = `https://unpkg.com/@vue/runtime-dom@${version}/dist/runtime-dom.esm-browser.js`
|
||||||
const [compiler] = await Promise.all([
|
const [compiler] = await Promise.all([
|
||||||
import(/* @vite-ignore */ compilerUrl),
|
import(/* @vite-ignore */ compilerUrl),
|
||||||
import(/* @vite-ignore */ runtimeUrl)
|
import(/* @vite-ignore */ runtimeUrl)
|
||||||
])
|
])
|
||||||
SFCCompiler = compiler
|
SFCCompiler = compiler
|
||||||
SANDBOX_VUE_URL = runtimeUrl
|
vueRuntimeUrl.value = runtimeUrl
|
||||||
console.info(`Now using Vue version: ${version}`)
|
console.info(`Now using Vue version: ${version}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetVersion() {
|
export function resetVersion() {
|
||||||
SFCCompiler = defaultCompiler
|
SFCCompiler = defaultCompiler
|
||||||
SANDBOX_VUE_URL = defaultVueUrl
|
vueRuntimeUrl.value = defaultVueUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function compileFile({ filename, code, compiled }: File) {
|
export async function compileFile({ filename, code, compiled }: File) {
|
||||||
@ -41,7 +42,7 @@ export async function compileFile({ filename, code, compiled }: File) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filename.endsWith('.js')) {
|
if (!filename.endsWith('.vue')) {
|
||||||
compiled.js = compiled.ssr = code
|
compiled.js = compiled.ssr = code
|
||||||
store.errors = []
|
store.errors = []
|
||||||
return
|
return
|
||||||
|
@ -30,6 +30,7 @@ interface Store {
|
|||||||
files: Record<string, File>
|
files: Record<string, File>
|
||||||
activeFilename: string
|
activeFilename: string
|
||||||
readonly activeFile: File
|
readonly activeFile: File
|
||||||
|
readonly importMap: string | undefined
|
||||||
errors: (string | Error)[]
|
errors: (string | Error)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +54,10 @@ export const store: Store = reactive({
|
|||||||
get activeFile() {
|
get activeFile() {
|
||||||
return store.files[store.activeFilename]
|
return store.files[store.activeFilename]
|
||||||
},
|
},
|
||||||
|
get importMap() {
|
||||||
|
const file = store.files['import-map.json']
|
||||||
|
return file && file.code
|
||||||
|
},
|
||||||
errors: []
|
errors: []
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -81,7 +86,17 @@ export function setActive(filename: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addFile(filename: string) {
|
export function addFile(filename: string) {
|
||||||
store.files[filename] = new File(filename)
|
const file = (store.files[filename] = new File(filename))
|
||||||
|
|
||||||
|
if (filename === 'import-map.json') {
|
||||||
|
file.code = `
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}`.trim()
|
||||||
|
}
|
||||||
|
|
||||||
setActive(filename)
|
setActive(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user