Skip to content

Commit d1bf35c

Browse files
committed
workflow(sfc-playground): support multiple files
1 parent 2e3984f commit d1bf35c

File tree

10 files changed

+559
-162
lines changed

10 files changed

+559
-162
lines changed

packages/compiler-sfc/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { parse as babelParse } from '@babel/parser'
1111
export { walkIdentifiers } from './compileScript'
1212
import MagicString from 'magic-string'
1313
export { MagicString }
14+
export { walk } from 'estree-walker'
1415

1516
// Types
1617
export {

packages/sfc-playground/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ body {
2929
background-color: #f8f8f8;
3030
--nav-height: 50px;
3131
--font-code: 'Source Code Pro', monospace;
32+
--color-branding: #3ca877;
33+
--color-branding-dark: #416f9c;
3234
}
3335
3436
.wrapper {

packages/sfc-playground/src/Message.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ function formatMessage(err: string | Error): string {
3838
border-radius: 6px;
3939
font-family: var(--font-code);
4040
white-space: pre-wrap;
41+
max-height: calc(100% - 50px);
42+
overflow-y: scroll;
4143
}
4244
4345
.msg.err {
Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,40 @@
11
<template>
2-
<CodeMirror @change="onChange" :value="initialCode" />
2+
<FileSelector/>
3+
<div class="editor-container">
4+
<CodeMirror @change="onChange" :value="activeCode" :mode="activeMode" />
35
<Message :err="store.errors[0]" />
6+
</div>
47
</template>
58

69
<script setup lang="ts">
10+
import FileSelector from './FileSelector.vue'
711
import CodeMirror from '../codemirror/CodeMirror.vue'
812
import Message from '../Message.vue'
913
import { store } from '../store'
1014
import { debounce } from '../utils'
15+
import { ref, watch, computed } from 'vue'
1116
1217
const onChange = debounce((code: string) => {
13-
store.code = code
18+
store.activeFile.code = code
1419
}, 250)
1520
16-
const initialCode = store.code
17-
</script>
21+
const activeCode = ref(store.activeFile.code)
22+
const activeMode = computed(
23+
() => (store.activeFilename.endsWith('.js') ? 'javascript' : 'htmlmixed')
24+
)
25+
26+
watch(
27+
() => store.activeFilename,
28+
() => {
29+
activeCode.value = store.activeFile.code
30+
}
31+
)
32+
</script>
33+
34+
<style scoped>
35+
.editor-container {
36+
height: calc(100% - 35px);
37+
overflow: hidden;
38+
position: relative;
39+
}
40+
</style>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<template>
2+
<div class="file-selector">
3+
<div
4+
v-for="(file, i) in Object.keys(store.files)"
5+
class="file"
6+
:class="{ active: store.activeFilename === file }"
7+
@click="setActive(file)">
8+
<span class="label">{{ file }}</span>
9+
<span v-if="i > 0" class="remove" @click.stop="deleteFile(file)">
10+
<svg width="12" height="12" viewBox="0 0 24 24" class="svelte-cghqrp"><line stroke="#999" x1="18" y1="6" x2="6" y2="18"></line><line stroke="#999" x1="6" y1="6" x2="18" y2="18"></line></svg>
11+
</span>
12+
</div>
13+
<div v-if="pending" class="file" >
14+
<input
15+
v-model="pendingFilename"
16+
spellcheck="false"
17+
@keyup.enter="doneAddFile"
18+
@keyup.esc="cancelAddFile"
19+
@vnodeMounted="focus">
20+
</div>
21+
<button class="add" @click="startAddFile">+</button>
22+
</div>
23+
</template>
24+
25+
<script setup lang="ts">
26+
import { store, addFile, deleteFile, setActive } from '../store'
27+
import { ref } from 'vue'
28+
import type { VNode } from 'vue'
29+
30+
const pending = ref(false)
31+
const pendingFilename = ref('Comp.vue')
32+
33+
function startAddFile() {
34+
pending.value = true
35+
}
36+
37+
function cancelAddFile() {
38+
pending.value = false
39+
}
40+
41+
function focus({ el }: VNode) {
42+
(el as HTMLInputElement).focus()
43+
}
44+
45+
function doneAddFile() {
46+
const filename = pendingFilename.value
47+
48+
if (!filename.endsWith('.vue') && !filename.endsWith('.js')) {
49+
store.errors = [`Playground only supports .vue or .js files.`]
50+
return
51+
}
52+
53+
if (filename in store.files) {
54+
store.errors = [`File "${filename}" already exists.`]
55+
return
56+
}
57+
58+
store.errors = []
59+
pending.value = false
60+
addFile(filename)
61+
pendingFilename.value = 'Comp.vue'
62+
}
63+
</script>
64+
65+
<style scoped>
66+
.file-selector {
67+
box-sizing: border-box;
68+
border-bottom: 1px solid #ddd;
69+
background-color: white;
70+
}
71+
.file {
72+
display: inline-block;
73+
font-size: 13px;
74+
font-family: var(--font-code);
75+
cursor: pointer;
76+
color: #999;
77+
box-sizing: border-box;
78+
}
79+
.file.active {
80+
color: var(--color-branding);
81+
border-bottom: 3px solid var(--color-branding);
82+
cursor: text;
83+
}
84+
.file span {
85+
display: inline-block;
86+
padding: 8px 10px 6px;
87+
}
88+
.file input {
89+
width: 80px;
90+
outline: none;
91+
border: 1px solid #ccc;
92+
border-radius: 3px;
93+
padding: 4px 6px;
94+
margin-left: 6px;
95+
}
96+
.file .remove {
97+
display: inline-block;
98+
vertical-align: middle;
99+
line-height: 12px;
100+
cursor: pointer;
101+
padding-left: 0;
102+
}
103+
.add {
104+
margin: 0;
105+
font-size: 20px;
106+
font-family: var(--font-code);
107+
color: #999;
108+
border: none;
109+
outline: none;
110+
background-color: transparent;
111+
cursor: pointer;
112+
vertical-align: middle;
113+
margin-left: 6px;
114+
}
115+
.add:hover {
116+
color: var(--color-branding);
117+
}
118+
</style>

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
</div>
55

66
<div class="output-container">
7-
<Preview v-if="mode === 'preview'" :code="store.compiled.executed" />
7+
<Preview v-if="mode === 'preview'" />
88
<CodeMirror
99
v-else
1010
readonly
1111
:mode="mode === 'css' ? 'css' : 'javascript'"
12-
:value="store.compiled[mode]"
12+
:value="store.activeFile.compiled[mode]"
1313
/>
1414
</div>
1515
</template>
@@ -20,9 +20,9 @@ import CodeMirror from '../codemirror/CodeMirror.vue'
2020
import { store } from '../store'
2121
import { ref } from 'vue'
2222
23-
type Modes = 'preview' | 'executed' | 'js' | 'css' | 'template'
23+
type Modes = 'preview' | 'js' | 'css'
2424
25-
const modes: Modes[] = ['preview', 'js', 'css', 'template', 'executed']
25+
const modes: Modes[] = ['preview', 'js', 'css']
2626
const mode = ref<Modes>('preview')
2727
</script>
2828

@@ -35,14 +35,15 @@ const mode = ref<Modes>('preview')
3535
.tab-buttons {
3636
box-sizing: border-box;
3737
border-bottom: 1px solid #ddd;
38+
background-color: white;
3839
}
3940
.tab-buttons button {
4041
margin: 0;
4142
font-size: 13px;
42-
font-family: 'Source Code Pro', monospace;
43+
font-family: var(--font-code);
4344
border: none;
4445
outline: none;
45-
background-color: #f8f8f8;
46+
background-color: transparent;
4647
padding: 8px 16px 6px;
4748
text-transform: uppercase;
4849
cursor: pointer;
@@ -51,7 +52,7 @@ const mode = ref<Modes>('preview')
5152
}
5253
5354
button.active {
54-
color: #42b983;
55-
border-bottom: 3px solid #42b983;
55+
color: var(--color-branding-dark);
56+
border-bottom: 3px solid var(--color-branding-dark);
5657
}
5758
</style>

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

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111

1212
<script setup lang="ts">
1313
import Message from '../Message.vue'
14-
import { ref, onMounted, onUnmounted, watchEffect, defineProps } from 'vue'
14+
import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
1515
import srcdoc from './srcdoc.html?raw'
1616
import { PreviewProxy } from './PreviewProxy'
17-
import { sandboxVueURL } from '../store'
18-
19-
const props = defineProps<{ code: string }>()
17+
import { MAIN_FILE, SANDBOX_VUE_URL } from '../store'
18+
import { compileModulesForPreview } from './moduleCompiler'
2019
2120
const iframe = ref()
2221
const runtimeError = ref()
@@ -25,32 +24,35 @@ const runtimeWarning = ref()
2524
let proxy: PreviewProxy
2625
2726
async function updatePreview() {
28-
if (!props.code?.trim()) {
29-
return
30-
}
27+
runtimeError.value = null
28+
runtimeWarning.value = null
3129
try {
32-
proxy.eval(`
33-
${props.code}
34-
35-
if (window.vueApp) {
36-
window.vueApp.unmount()
37-
}
38-
const container = document.getElementById('app')
39-
container.innerHTML = ''
40-
41-
import { createApp as _createApp } from "${sandboxVueURL}"
42-
const app = window.vueApp = _createApp(__comp)
43-
44-
app.config.errorHandler = e => console.error(e)
45-
46-
app.mount(container)
47-
`)
30+
const modules = compileModulesForPreview()
31+
console.log(`successfully compiled ${modules.length} modules.`)
32+
// reset modules
33+
await proxy.eval(`
34+
window.__modules__ = {}
35+
window.__css__ = ''
36+
`)
37+
// evaluate modules
38+
for (const mod of modules) {
39+
await proxy.eval(mod)
40+
}
41+
// reboot
42+
await proxy.eval(`
43+
import { createApp as _createApp } from "${SANDBOX_VUE_URL}"
44+
if (window.__app__) {
45+
window.__app__.unmount()
46+
document.getElementById('app').innerHTML = ''
47+
}
48+
document.getElementById('__sfc-styles').innerHTML = window.__css__
49+
const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default)
50+
app.config.errorHandler = e => console.error(e)
51+
app.mount('#app')
52+
`)
4853
} catch (e) {
49-
runtimeError.value = e.message
50-
return
54+
runtimeError.value = e.stack
5155
}
52-
runtimeError.value = null
53-
runtimeWarning.value = null
5456
}
5557
5658
onMounted(() => {
@@ -59,7 +61,6 @@ onMounted(() => {
5961
// pending_imports = progress;
6062
},
6163
on_error: (event: any) => {
62-
// push_logs({ level: 'error', args: [event.value] });
6364
runtimeError.value = event.value
6465
},
6566
on_unhandled_rejection: (event: any) => {
@@ -69,10 +70,17 @@ onMounted(() => {
6970
},
7071
on_console: (log: any) => {
7172
if (log.level === 'error') {
72-
runtimeError.value = log.args.join('')
73+
if (log.args[0] instanceof Error) {
74+
runtimeError.value = log.args[0].stack
75+
} else {
76+
runtimeError.value = log.args
77+
}
7378
} else if (log.level === 'warn') {
7479
if (log.args[0].toString().includes('[Vue warn]')) {
75-
runtimeWarning.value = log.args.join('').replace(/\[Vue warn\]:/, '').trim()
80+
runtimeWarning.value = log.args
81+
.join('')
82+
.replace(/\[Vue warn\]:/, '')
83+
.trim()
7684
}
7785
}
7886
},
@@ -88,9 +96,9 @@ onMounted(() => {
8896
})
8997
9098
iframe.value.addEventListener('load', () => {
91-
proxy.handle_links();
99+
proxy.handle_links()
92100
watchEffect(updatePreview)
93-
});
101+
})
94102
})
95103
96104
onUnmounted(() => {

0 commit comments

Comments
 (0)