workflow(sfc-playground): support multiple files
This commit is contained in:
@@ -4,12 +4,12 @@
|
||||
</div>
|
||||
|
||||
<div class="output-container">
|
||||
<Preview v-if="mode === 'preview'" :code="store.compiled.executed" />
|
||||
<Preview v-if="mode === 'preview'" />
|
||||
<CodeMirror
|
||||
v-else
|
||||
readonly
|
||||
:mode="mode === 'css' ? 'css' : 'javascript'"
|
||||
:value="store.compiled[mode]"
|
||||
:value="store.activeFile.compiled[mode]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -20,9 +20,9 @@ import CodeMirror from '../codemirror/CodeMirror.vue'
|
||||
import { store } from '../store'
|
||||
import { ref } from 'vue'
|
||||
|
||||
type Modes = 'preview' | 'executed' | 'js' | 'css' | 'template'
|
||||
type Modes = 'preview' | 'js' | 'css'
|
||||
|
||||
const modes: Modes[] = ['preview', 'js', 'css', 'template', 'executed']
|
||||
const modes: Modes[] = ['preview', 'js', 'css']
|
||||
const mode = ref<Modes>('preview')
|
||||
</script>
|
||||
|
||||
@@ -35,14 +35,15 @@ const mode = ref<Modes>('preview')
|
||||
.tab-buttons {
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: white;
|
||||
}
|
||||
.tab-buttons button {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
font-family: var(--font-code);
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: #f8f8f8;
|
||||
background-color: transparent;
|
||||
padding: 8px 16px 6px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
@@ -51,7 +52,7 @@ const mode = ref<Modes>('preview')
|
||||
}
|
||||
|
||||
button.active {
|
||||
color: #42b983;
|
||||
border-bottom: 3px solid #42b983;
|
||||
color: var(--color-branding-dark);
|
||||
border-bottom: 3px solid var(--color-branding-dark);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from '../Message.vue'
|
||||
import { ref, onMounted, onUnmounted, watchEffect, defineProps } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
|
||||
import srcdoc from './srcdoc.html?raw'
|
||||
import { PreviewProxy } from './PreviewProxy'
|
||||
import { sandboxVueURL } from '../store'
|
||||
|
||||
const props = defineProps<{ code: string }>()
|
||||
import { MAIN_FILE, SANDBOX_VUE_URL } from '../store'
|
||||
import { compileModulesForPreview } from './moduleCompiler'
|
||||
|
||||
const iframe = ref()
|
||||
const runtimeError = ref()
|
||||
@@ -25,32 +24,35 @@ const runtimeWarning = ref()
|
||||
let proxy: PreviewProxy
|
||||
|
||||
async function updatePreview() {
|
||||
if (!props.code?.trim()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
proxy.eval(`
|
||||
${props.code}
|
||||
|
||||
if (window.vueApp) {
|
||||
window.vueApp.unmount()
|
||||
}
|
||||
const container = document.getElementById('app')
|
||||
container.innerHTML = ''
|
||||
|
||||
import { createApp as _createApp } from "${sandboxVueURL}"
|
||||
const app = window.vueApp = _createApp(__comp)
|
||||
|
||||
app.config.errorHandler = e => console.error(e)
|
||||
|
||||
app.mount(container)
|
||||
`)
|
||||
} catch (e) {
|
||||
runtimeError.value = e.message
|
||||
return
|
||||
}
|
||||
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__ = ''
|
||||
`)
|
||||
// evaluate modules
|
||||
for (const mod of modules) {
|
||||
await proxy.eval(mod)
|
||||
}
|
||||
// reboot
|
||||
await proxy.eval(`
|
||||
import { createApp as _createApp } from "${SANDBOX_VUE_URL}"
|
||||
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')
|
||||
`)
|
||||
} catch (e) {
|
||||
runtimeError.value = e.stack
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -59,7 +61,6 @@ onMounted(() => {
|
||||
// pending_imports = progress;
|
||||
},
|
||||
on_error: (event: any) => {
|
||||
// push_logs({ level: 'error', args: [event.value] });
|
||||
runtimeError.value = event.value
|
||||
},
|
||||
on_unhandled_rejection: (event: any) => {
|
||||
@@ -69,10 +70,17 @@ onMounted(() => {
|
||||
},
|
||||
on_console: (log: any) => {
|
||||
if (log.level === 'error') {
|
||||
runtimeError.value = log.args.join('')
|
||||
if (log.args[0] instanceof Error) {
|
||||
runtimeError.value = log.args[0].stack
|
||||
} else {
|
||||
runtimeError.value = log.args
|
||||
}
|
||||
} else if (log.level === 'warn') {
|
||||
if (log.args[0].toString().includes('[Vue warn]')) {
|
||||
runtimeWarning.value = log.args.join('').replace(/\[Vue warn\]:/, '').trim()
|
||||
runtimeWarning.value = log.args
|
||||
.join('')
|
||||
.replace(/\[Vue warn\]:/, '')
|
||||
.trim()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -88,9 +96,9 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
iframe.value.addEventListener('load', () => {
|
||||
proxy.handle_links();
|
||||
proxy.handle_links()
|
||||
watchEffect(updatePreview)
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
207
packages/sfc-playground/src/output/moduleCompiler.ts
Normal file
207
packages/sfc-playground/src/output/moduleCompiler.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { store, MAIN_FILE, SANDBOX_VUE_URL, File } from '../store'
|
||||
import { babelParse, MagicString, walk } from '@vue/compiler-sfc'
|
||||
import { babelParserDefaultPlugins } from '@vue/shared'
|
||||
import { Identifier, Node } from '@babel/types'
|
||||
|
||||
export function compileModulesForPreview() {
|
||||
return processFile(store.files[MAIN_FILE]).reverse()
|
||||
}
|
||||
|
||||
function processFile(file: File, seen = new Set<File>()) {
|
||||
if (seen.has(file)) {
|
||||
return []
|
||||
}
|
||||
seen.add(file)
|
||||
|
||||
const { js, css } = file.compiled
|
||||
const ast = babelParse(js, {
|
||||
sourceFilename: file.filename,
|
||||
sourceType: 'module',
|
||||
plugins: [...babelParserDefaultPlugins]
|
||||
}).program.body
|
||||
|
||||
const importedFiles = new Set<string>()
|
||||
const importToIdMap = new Map<string, string>()
|
||||
|
||||
const s = new MagicString(js)
|
||||
|
||||
function registerImport(source: string) {
|
||||
const filename = source.replace(/^\.\/+/, '')
|
||||
if (!(filename in store.files)) {
|
||||
throw new Error(`File "${filename}" does not exist.`)
|
||||
}
|
||||
if (importedFiles.has(filename)) {
|
||||
return importToIdMap.get(filename)
|
||||
}
|
||||
importedFiles.add(filename)
|
||||
const id = `__import_${importedFiles.size}__`
|
||||
importToIdMap.set(filename, id)
|
||||
s.prepend(`const ${id} = __modules__[${JSON.stringify(filename)}]\n`)
|
||||
return id
|
||||
}
|
||||
|
||||
s.prepend(
|
||||
`const mod = __modules__[${JSON.stringify(
|
||||
file.filename
|
||||
)}] = Object.create(null)\n\n`
|
||||
)
|
||||
|
||||
for (const node of ast) {
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
const source = node.source.value
|
||||
if (source === 'vue') {
|
||||
// rewrite Vue imports
|
||||
s.overwrite(
|
||||
node.source.start!,
|
||||
node.source.end!,
|
||||
`"${SANDBOX_VUE_URL}"`
|
||||
)
|
||||
} else if (source.startsWith('./')) {
|
||||
// rewrite the import to retrieve the import from global registry
|
||||
s.remove(node.start!, node.end!)
|
||||
|
||||
const id = registerImport(source)
|
||||
|
||||
for (const spec of node.specifiers) {
|
||||
if (spec.type === 'ImportDefaultSpecifier') {
|
||||
s.prependRight(
|
||||
node.start!,
|
||||
`const ${spec.local.name} = ${id}.default\n`
|
||||
)
|
||||
} else if (spec.type === 'ImportSpecifier') {
|
||||
s.prependRight(
|
||||
node.start!,
|
||||
`const ${spec.local.name} = ${id}.${
|
||||
(spec.imported as Identifier).name
|
||||
}\n`
|
||||
)
|
||||
} else {
|
||||
// namespace import
|
||||
s.prependRight(node.start!, `const ${spec.local.name} = ${id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'ExportDefaultDeclaration') {
|
||||
// export default -> mod.default = ...
|
||||
s.overwrite(node.start!, node.declaration.start!, 'mod.default = ')
|
||||
}
|
||||
|
||||
if (node.type === 'ExportNamedDeclaration') {
|
||||
if (node.source) {
|
||||
// export { foo } from '...' -> mode.foo = __import_x__.foo
|
||||
const id = registerImport(node.source.value)
|
||||
let code = ``
|
||||
for (const spec of node.specifiers) {
|
||||
if (spec.type === 'ExportSpecifier') {
|
||||
code += `mod.${(spec.exported as Identifier).name} = ${id}.${
|
||||
spec.local.name
|
||||
}\n`
|
||||
}
|
||||
}
|
||||
s.overwrite(node.start!, node.end!, code)
|
||||
} else if (node.declaration) {
|
||||
if (
|
||||
node.declaration.type === 'FunctionDeclaration' ||
|
||||
node.declaration.type === 'ClassDeclaration'
|
||||
) {
|
||||
// export function foo() {}
|
||||
const name = node.declaration.id!.name
|
||||
s.appendLeft(node.end!, `\nmod.${name} = ${name}\n`)
|
||||
} else if (node.declaration.type === 'VariableDeclaration') {
|
||||
// export const foo = 1, bar = 2
|
||||
for (const decl of node.declaration.declarations) {
|
||||
for (const { name } of extractIdentifiers(decl.id)) {
|
||||
s.appendLeft(node.end!, `\nmod.${name} = ${name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.remove(node.start!, node.declaration.start!)
|
||||
} else {
|
||||
let code = ``
|
||||
for (const spec of node.specifiers) {
|
||||
if (spec.type === 'ExportSpecifier') {
|
||||
code += `mod.${(spec.exported as Identifier).name} = ${
|
||||
spec.local.name
|
||||
}\n`
|
||||
}
|
||||
}
|
||||
s.overwrite(node.start!, node.end!, code)
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'ExportAllDeclaration') {
|
||||
const id = registerImport(node.source.value)
|
||||
s.overwrite(node.start!, node.end!, `Object.assign(mod, ${id})`)
|
||||
}
|
||||
}
|
||||
|
||||
// dynamic import
|
||||
walk(ast as any, {
|
||||
enter(node) {
|
||||
if (node.type === 'ImportExpression') {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// append CSS injection code
|
||||
if (css) {
|
||||
s.append(`\nwindow.__css__ += ${JSON.stringify(css)}`)
|
||||
}
|
||||
|
||||
const processed = [s.toString()]
|
||||
if (importedFiles.size) {
|
||||
for (const imported of importedFiles) {
|
||||
processed.push(...processFile(store.files[imported], seen))
|
||||
}
|
||||
}
|
||||
|
||||
// return a list of files to further process
|
||||
return processed
|
||||
}
|
||||
|
||||
function extractIdentifiers(
|
||||
param: Node,
|
||||
nodes: Identifier[] = []
|
||||
): Identifier[] {
|
||||
switch (param.type) {
|
||||
case 'Identifier':
|
||||
nodes.push(param)
|
||||
break
|
||||
|
||||
case 'MemberExpression':
|
||||
let object: any = param
|
||||
while (object.type === 'MemberExpression') {
|
||||
object = object.object
|
||||
}
|
||||
nodes.push(object)
|
||||
break
|
||||
|
||||
case 'ObjectPattern':
|
||||
param.properties.forEach(prop => {
|
||||
if (prop.type === 'RestElement') {
|
||||
extractIdentifiers(prop.argument, nodes)
|
||||
} else {
|
||||
extractIdentifiers(prop.value, nodes)
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'ArrayPattern':
|
||||
param.elements.forEach(element => {
|
||||
if (element) extractIdentifiers(element, nodes)
|
||||
})
|
||||
break
|
||||
|
||||
case 'RestElement':
|
||||
extractIdentifiers(param.argument, nodes)
|
||||
break
|
||||
|
||||
case 'AssignmentPattern':
|
||||
extractIdentifiers(param.left, nodes)
|
||||
break
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
@@ -8,76 +8,75 @@
|
||||
}
|
||||
</style>
|
||||
<style id="__sfc-styles"></style>
|
||||
<script>
|
||||
(function(){
|
||||
let scriptEl
|
||||
<script type="module">
|
||||
let scriptEl
|
||||
|
||||
function handle_message(ev) {
|
||||
let { action, cmd_id } = ev.data;
|
||||
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
|
||||
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 });
|
||||
function handle_message(ev) {
|
||||
let { action, cmd_id } = ev.data;
|
||||
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
|
||||
const send_reply = (payload) => send_message({ ...payload, cmd_id });
|
||||
const send_ok = window.send_ok = () => send_reply({ action: 'cmd_ok' });
|
||||
const send_error = (message, stack) => send_reply({ action: 'cmd_error', message, stack });
|
||||
|
||||
if (action === 'eval') {
|
||||
try {
|
||||
if (scriptEl) {
|
||||
document.head.removeChild(scriptEl)
|
||||
}
|
||||
scriptEl = document.createElement('script')
|
||||
scriptEl.setAttribute('type', 'module')
|
||||
scriptEl.innerHTML = ev.data.args.script
|
||||
document.head.appendChild(scriptEl)
|
||||
send_ok();
|
||||
} catch (e) {
|
||||
send_error(e.message, e.stack);
|
||||
if (action === 'eval') {
|
||||
try {
|
||||
if (scriptEl) {
|
||||
document.head.removeChild(scriptEl)
|
||||
}
|
||||
scriptEl = document.createElement('script')
|
||||
scriptEl.setAttribute('type', 'module')
|
||||
// send ok in the module script to ensure sequential evaluation
|
||||
// of multiple proxy.eval() calls
|
||||
scriptEl.innerHTML = ev.data.args.script + `\nwindow.send_ok()`
|
||||
document.head.appendChild(scriptEl)
|
||||
} catch (e) {
|
||||
send_error(e.message, e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
// 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;
|
||||
if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
if (el.href.startsWith(top_origin)) {
|
||||
const url = new URL(el.href);
|
||||
if (url.hash[0] === '#') {
|
||||
window.location.hash = url.hash;
|
||||
return;
|
||||
}
|
||||
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.open(el.href, '_blank');
|
||||
});
|
||||
send_ok();
|
||||
} catch(e) {
|
||||
send_error(e.message, e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handle_message, false);
|
||||
window.addEventListener('message', handle_message, false);
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
parent.postMessage({ action: 'error', value: error }, '*');
|
||||
}
|
||||
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 }, '*');
|
||||
});
|
||||
}).call(this);
|
||||
window.addEventListener("unhandledrejection", event => {
|
||||
parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*');
|
||||
});
|
||||
|
||||
let previous = { level: null, args: null };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user