workflow(sfc-playground): support import map
This commit is contained in:
parent
e097bd4dd5
commit
5ee7e6bc70
@ -24,7 +24,7 @@ body {
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
color:var(--base);
|
||||
color: var(--base);
|
||||
margin: 0;
|
||||
background-color: #f8f8f8;
|
||||
--base: #444;
|
||||
|
@ -14,7 +14,7 @@
|
||||
<li v-for="version of publishedVersions">
|
||||
<a @click="setVueVersion(version)">v{{ version }}</a>
|
||||
</li>
|
||||
<li><a @click="resetVueVersion">This Commit ({{ commit }})</a></li>
|
||||
<li><a @click="resetVueVersion">This Commit ({{ currentCommit }})</a></li>
|
||||
<li>
|
||||
<a href="https://app.netlify.com/sites/vue-sfc-playground/deploys" target="_blank">Commits History</a>
|
||||
</li>
|
||||
@ -51,8 +51,8 @@ import { downloadProject } from './download/download'
|
||||
import { setVersion, resetVersion } from './sfcCompiler'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const commit = __COMMIT__
|
||||
const activeVersion = ref(`@${commit}`)
|
||||
const currentCommit = __COMMIT__
|
||||
const activeVersion = ref(`@${currentCommit}`)
|
||||
const publishedVersions = ref<string[]>()
|
||||
const expanded = ref(false)
|
||||
|
||||
@ -72,7 +72,7 @@ async function setVueVersion(v: string) {
|
||||
|
||||
function resetVueVersion() {
|
||||
resetVersion()
|
||||
activeVersion.value = `@${commit}`
|
||||
activeVersion.value = `@${currentCommit}`
|
||||
expanded.value = false
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ const onChange = debounce((code: string) => {
|
||||
|
||||
const activeCode = ref(store.activeFile.code)
|
||||
const activeMode = computed(
|
||||
() => (store.activeFilename.endsWith('.js') ? 'javascript' : 'htmlmixed')
|
||||
() => (store.activeFilename.endsWith('.vue') ? 'htmlmixed' : 'javascript')
|
||||
)
|
||||
|
||||
watch(
|
||||
|
@ -45,8 +45,12 @@ function focus({ el }: VNode) {
|
||||
function doneAddFile() {
|
||||
const filename = pendingFilename.value
|
||||
|
||||
if (!filename.endsWith('.vue') && !filename.endsWith('.js')) {
|
||||
store.errors = [`Playground only supports .vue or .js files.`]
|
||||
if (
|
||||
!filename.endsWith('.vue') &&
|
||||
!filename.endsWith('.js') &&
|
||||
filename !== 'import-map.json'
|
||||
) {
|
||||
store.errors = [`Playground only supports *.vue, *.js files or import-map.json.`]
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1,60 +1,105 @@
|
||||
<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
|
||||
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}"
|
||||
// create sandbox on mount
|
||||
onMounted(createSandbox)
|
||||
|
||||
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
|
||||
// reset sandbox when import map changes
|
||||
watch(() => store.importMap, (importMap, prev) => {
|
||||
if (!importMap) {
|
||||
if (prev) {
|
||||
// import-map.json deleted
|
||||
createSandbox()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
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(() => {
|
||||
proxy = new PreviewProxy(iframe.value, {
|
||||
// reset sandbox when version changes
|
||||
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) => {
|
||||
// pending_imports = progress;
|
||||
},
|
||||
@ -93,19 +138,43 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
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%;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { store, File } from '../store'
|
||||
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
|
||||
import { MAIN_FILE } from '../sfcCompiler'
|
||||
import {
|
||||
babelParse,
|
||||
MagicString,
|
||||
@ -92,15 +92,6 @@ function processFile(file: File, seen = new Set<File>()) {
|
||||
}
|
||||
}
|
||||
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,7 +8,13 @@
|
||||
}
|
||||
</style>
|
||||
<style id="__sfc-styles"></style>
|
||||
<script type="module">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
let scriptEls = []
|
||||
|
||||
window.__modules__ = {}
|
||||
@ -224,6 +230,7 @@
|
||||
return null;
|
||||
}
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { store, File } from './store'
|
||||
import { SFCDescriptor, BindingMetadata } from '@vue/compiler-sfc'
|
||||
import * as defaultCompiler from '@vue/compiler-sfc'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const MAIN_FILE = 'App.vue'
|
||||
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
|
||||
: '/src/vue-dev-proxy'
|
||||
|
||||
export let SANDBOX_VUE_URL = defaultVueUrl
|
||||
export const vueRuntimeUrl = ref(defaultVueUrl)
|
||||
|
||||
export async function setVersion(version: string) {
|
||||
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([
|
||||
import(/* @vite-ignore */ compilerUrl),
|
||||
import(/* @vite-ignore */ runtimeUrl)
|
||||
])
|
||||
SFCCompiler = compiler
|
||||
SANDBOX_VUE_URL = runtimeUrl
|
||||
vueRuntimeUrl.value = runtimeUrl
|
||||
console.info(`Now using Vue version: ${version}`)
|
||||
}
|
||||
|
||||
export function resetVersion() {
|
||||
SFCCompiler = defaultCompiler
|
||||
SANDBOX_VUE_URL = defaultVueUrl
|
||||
vueRuntimeUrl.value = defaultVueUrl
|
||||
}
|
||||
|
||||
export async function compileFile({ filename, code, compiled }: File) {
|
||||
@ -41,7 +42,7 @@ export async function compileFile({ filename, code, compiled }: File) {
|
||||
return
|
||||
}
|
||||
|
||||
if (filename.endsWith('.js')) {
|
||||
if (!filename.endsWith('.vue')) {
|
||||
compiled.js = compiled.ssr = code
|
||||
store.errors = []
|
||||
return
|
||||
|
@ -30,6 +30,7 @@ interface Store {
|
||||
files: Record<string, File>
|
||||
activeFilename: string
|
||||
readonly activeFile: File
|
||||
readonly importMap: string | undefined
|
||||
errors: (string | Error)[]
|
||||
}
|
||||
|
||||
@ -53,6 +54,10 @@ export const store: Store = reactive({
|
||||
get activeFile() {
|
||||
return store.files[store.activeFilename]
|
||||
},
|
||||
get importMap() {
|
||||
const file = store.files['import-map.json']
|
||||
return file && file.code
|
||||
},
|
||||
errors: []
|
||||
})
|
||||
|
||||
@ -81,7 +86,17 @@ export function setActive(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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user