Skip to content

Commit 5ee7e6b

Browse files
committed
workflow(sfc-playground): support import map
1 parent e097bd4 commit 5ee7e6b

File tree

9 files changed

+339
-252
lines changed

9 files changed

+339
-252
lines changed

packages/sfc-playground/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ body {
2424
font-size: 13px;
2525
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
2626
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
27-
color:var(--base);
27+
color: var(--base);
2828
margin: 0;
2929
background-color: #f8f8f8;
3030
--base: #444;

packages/sfc-playground/src/Header.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<li v-for="version of publishedVersions">
1515
<a @click="setVueVersion(version)">v{{ version }}</a>
1616
</li>
17-
<li><a @click="resetVueVersion">This Commit ({{ commit }})</a></li>
17+
<li><a @click="resetVueVersion">This Commit ({{ currentCommit }})</a></li>
1818
<li>
1919
<a href="https://app.netlify.com/sites/vue-sfc-playground/deploys" target="_blank">Commits History</a>
2020
</li>
@@ -51,8 +51,8 @@ import { downloadProject } from './download/download'
5151
import { setVersion, resetVersion } from './sfcCompiler'
5252
import { ref, onMounted } from 'vue'
5353
54-
const commit = __COMMIT__
55-
const activeVersion = ref(`@${commit}`)
54+
const currentCommit = __COMMIT__
55+
const activeVersion = ref(`@${currentCommit}`)
5656
const publishedVersions = ref<string[]>()
5757
const expanded = ref(false)
5858
@@ -72,7 +72,7 @@ async function setVueVersion(v: string) {
7272
7373
function resetVueVersion() {
7474
resetVersion()
75-
activeVersion.value = `@${commit}`
75+
activeVersion.value = `@${currentCommit}`
7676
expanded.value = false
7777
}
7878

packages/sfc-playground/src/editor/Editor.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const onChange = debounce((code: string) => {
2020
2121
const activeCode = ref(store.activeFile.code)
2222
const activeMode = computed(
23-
() => (store.activeFilename.endsWith('.js') ? 'javascript' : 'htmlmixed')
23+
() => (store.activeFilename.endsWith('.vue') ? 'htmlmixed' : 'javascript')
2424
)
2525
2626
watch(

packages/sfc-playground/src/editor/FileSelector.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ function focus({ el }: VNode) {
4545
function doneAddFile() {
4646
const filename = pendingFilename.value
4747
48-
if (!filename.endsWith('.vue') && !filename.endsWith('.js')) {
49-
store.errors = [`Playground only supports .vue or .js files.`]
48+
if (
49+
!filename.endsWith('.vue') &&
50+
!filename.endsWith('.js') &&
51+
filename !== 'import-map.json'
52+
) {
53+
store.errors = [`Playground only supports *.vue, *.js files or import-map.json.`]
5054
return
5155
}
5256

packages/sfc-playground/src/output/Preview.vue

Lines changed: 110 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,105 @@
11
<template>
2-
<iframe
3-
id="preview"
4-
ref="iframe"
5-
sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation"
6-
:srcdoc="srcdoc"
7-
></iframe>
2+
<div class="preview-container" ref="container">
3+
</div>
84
<Message :err="runtimeError" />
95
<Message v-if="!runtimeError" :warn="runtimeWarning" />
106
</template>
117

128
<script setup lang="ts">
139
import Message from '../Message.vue'
14-
import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
10+
import { ref, onMounted, onUnmounted, watchEffect, watch } from 'vue'
1511
import type { WatchStopHandle } from 'vue'
1612
import srcdoc from './srcdoc.html?raw'
1713
import { PreviewProxy } from './PreviewProxy'
18-
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
14+
import { MAIN_FILE, vueRuntimeUrl } from '../sfcCompiler'
1915
import { compileModulesForPreview } from './moduleCompiler'
16+
import { store } from '../store'
2017
21-
const iframe = ref()
18+
const container = ref()
2219
const runtimeError = ref()
2320
const runtimeWarning = ref()
2421
22+
let sandbox: HTMLIFrameElement
2523
let proxy: PreviewProxy
26-
let updateHandle: WatchStopHandle
24+
let stopUpdateWatcher: WatchStopHandle
2725
28-
async function updatePreview() {
29-
runtimeError.value = null
30-
runtimeWarning.value = null
26+
// create sandbox on mount
27+
onMounted(createSandbox)
28+
29+
// reset sandbox when import map changes
30+
watch(() => store.importMap, (importMap, prev) => {
31+
if (!importMap) {
32+
if (prev) {
33+
// import-map.json deleted
34+
createSandbox()
35+
}
36+
return
37+
}
3138
try {
32-
const modules = compileModulesForPreview()
33-
console.log(`successfully compiled ${modules.length} modules.`)
34-
// reset modules
35-
await proxy.eval([
36-
`window.__modules__ = {};window.__css__ = ''`,
37-
...modules,
38-
`
39-
import { createApp as _createApp } from "${SANDBOX_VUE_URL}"
39+
const map = JSON.parse(importMap)
40+
if (!map.imports) {
41+
store.errors = [
42+
`import-map.json is missing "imports" field.`
43+
]
44+
return
45+
}
46+
if (map.imports.vue) {
47+
store.errors = [
48+
'Select Vue versions using the top-right dropdown.\n' +
49+
'Specifying it in the import map has no effect.'
50+
]
51+
}
52+
createSandbox()
53+
} catch (e) {
54+
store.errors = [e]
55+
return
56+
}
57+
})
4058
41-
if (window.__app__) {
42-
window.__app__.unmount()
43-
document.getElementById('app').innerHTML = ''
44-
}
59+
// reset sandbox when version changes
60+
watch(vueRuntimeUrl, createSandbox)
4561
46-
document.getElementById('__sfc-styles').innerHTML = window.__css__
47-
const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default)
48-
app.config.errorHandler = e => console.error(e)
49-
app.mount('#app')`.trim()
50-
])
62+
onUnmounted(() => {
63+
proxy.destroy()
64+
stopUpdateWatcher && stopUpdateWatcher()
65+
})
66+
67+
function createSandbox() {
68+
if (sandbox) {
69+
// clear prev sandbox
70+
proxy.destroy()
71+
stopUpdateWatcher()
72+
container.value.removeChild(sandbox)
73+
}
74+
75+
sandbox = document.createElement('iframe')
76+
sandbox.setAttribute('sandbox', [
77+
'allow-forms',
78+
'allow-modals',
79+
'allow-pointer-lock',
80+
'allow-popups',
81+
'allow-same-origin',
82+
'allow-scripts',
83+
'allow-top-navigation-by-user-activation'
84+
].join(' '))
85+
86+
let importMap: Record<string, any>
87+
try {
88+
importMap = JSON.parse(store.importMap || `{}`)
5189
} catch (e) {
52-
runtimeError.value = e.stack
90+
store.errors = [`Syntax error in import-map.json: ${e.message}`]
91+
return
5392
}
54-
}
5593
56-
onMounted(() => {
57-
proxy = new PreviewProxy(iframe.value, {
94+
if (!importMap.imports) {
95+
importMap.imports = {}
96+
}
97+
importMap.imports.vue = vueRuntimeUrl.value
98+
const sandboxSrc = srcdoc.replace(/<!--IMPORT_MAP-->/, JSON.stringify(importMap))
99+
sandbox.setAttribute('srcdoc', sandboxSrc)
100+
container.value.appendChild(sandbox)
101+
102+
proxy = new PreviewProxy(sandbox, {
58103
on_fetch_progress: (progress: any) => {
59104
// pending_imports = progress;
60105
},
@@ -93,19 +138,43 @@ onMounted(() => {
93138
}
94139
})
95140
96-
iframe.value.addEventListener('load', () => {
141+
sandbox.addEventListener('load', () => {
97142
proxy.handle_links()
98-
updateHandle = watchEffect(updatePreview)
143+
stopUpdateWatcher = watchEffect(updatePreview)
99144
})
100-
})
145+
}
101146
102-
onUnmounted(() => {
103-
proxy.destroy()
104-
updateHandle && updateHandle()
105-
})
147+
async function updatePreview() {
148+
runtimeError.value = null
149+
runtimeWarning.value = null
150+
try {
151+
const modules = compileModulesForPreview()
152+
console.log(`successfully compiled ${modules.length} modules.`)
153+
// reset modules
154+
await proxy.eval([
155+
`window.__modules__ = {};window.__css__ = ''`,
156+
...modules,
157+
`
158+
import { createApp as _createApp } from "vue"
159+
160+
if (window.__app__) {
161+
window.__app__.unmount()
162+
document.getElementById('app').innerHTML = ''
163+
}
164+
165+
document.getElementById('__sfc-styles').innerHTML = window.__css__
166+
const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default)
167+
app.config.errorHandler = e => console.error(e)
168+
app.mount('#app')`.trim()
169+
])
170+
} catch (e) {
171+
runtimeError.value = e.stack
172+
}
173+
}
106174
</script>
107175

108176
<style>
177+
.preview-container,
109178
iframe {
110179
width: 100%;
111180
height: 100%;

packages/sfc-playground/src/output/moduleCompiler.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { store, File } from '../store'
2-
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
2+
import { MAIN_FILE } from '../sfcCompiler'
33
import {
44
babelParse,
55
MagicString,
@@ -92,15 +92,6 @@ function processFile(file: File, seen = new Set<File>()) {
9292
}
9393
}
9494
s.remove(node.start!, node.end!)
95-
} else {
96-
if (source === 'vue') {
97-
// rewrite Vue imports
98-
s.overwrite(
99-
node.source.start!,
100-
node.source.end!,
101-
`"${SANDBOX_VUE_URL}"`
102-
)
103-
}
10495
}
10596
}
10697
}

0 commit comments

Comments
 (0)