workflow(sfc-playground): support import map

This commit is contained in:
Evan You 2021-03-31 13:31:00 -04:00
parent e097bd4dd5
commit 5ee7e6bc70
9 changed files with 348 additions and 261 deletions

View File

@ -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
} }

View File

@ -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(

View File

@ -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
} }

View File

@ -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
// reset sandbox when import map changes
watch(() => store.importMap, (importMap, prev) => {
if (!importMap) {
if (prev) {
// import-map.json deleted
createSandbox()
}
return
}
try { try {
const modules = compileModulesForPreview() const map = JSON.parse(importMap)
console.log(`successfully compiled ${modules.length} modules.`) if (!map.imports) {
// reset modules store.errors = [
await proxy.eval([ `import-map.json is missing "imports" field.`
`window.__modules__ = {};window.__css__ = ''`, ]
...modules, return
`
import { createApp as _createApp } from "${SANDBOX_VUE_URL}"
if (window.__app__) {
window.__app__.unmount()
document.getElementById('app').innerHTML = ''
} }
if (map.imports.vue) {
document.getElementById('__sfc-styles').innerHTML = window.__css__ store.errors = [
const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default) 'Select Vue versions using the top-right dropdown.\n' +
app.config.errorHandler = e => console.error(e) 'Specifying it in the import map has no effect.'
app.mount('#app')`.trim() ]
]) }
createSandbox()
} catch (e) { } catch (e) {
runtimeError.value = e.stack store.errors = [e]
return
} }
})
// 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)
} }
onMounted(() => { sandbox = document.createElement('iframe')
proxy = new PreviewProxy(iframe.value, { 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%;

View File

@ -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}"`
)
}
} }
} }
} }

View File

@ -8,7 +8,13 @@
} }
</style> </style>
<style id="__sfc-styles"></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 = [] let scriptEls = []
window.__modules__ = {} window.__modules__ = {}
@ -224,6 +230,7 @@
return null; return null;
} }
} }
})()
</script> </script>
</head> </head>
<body> <body>

View File

@ -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

View File

@ -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)
} }