2021-03-28 13:35:45 +08:00
|
|
|
import { reactive, watchEffect } from 'vue'
|
|
|
|
import {
|
|
|
|
parse,
|
|
|
|
compileTemplate,
|
|
|
|
compileStyleAsync,
|
|
|
|
compileScript,
|
2021-03-29 06:41:33 +08:00
|
|
|
rewriteDefault
|
2021-03-28 13:35:45 +08:00
|
|
|
} from '@vue/compiler-sfc'
|
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
const welcomeCode = `
|
2021-03-28 14:24:25 +08:00
|
|
|
<template>
|
|
|
|
<h1>{{ msg }}</h1>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
const msg = 'Hello World!'
|
|
|
|
</script>
|
|
|
|
`.trim()
|
2021-03-28 13:35:45 +08:00
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
export const MAIN_FILE = 'App.vue'
|
|
|
|
export const COMP_IDENTIFIER = `__sfc__`
|
|
|
|
|
2021-03-28 13:35:45 +08:00
|
|
|
// @ts-ignore
|
2021-03-29 06:41:33 +08:00
|
|
|
export const SANDBOX_VUE_URL = import.meta.env.PROD
|
2021-03-28 13:35:45 +08:00
|
|
|
? '/vue.runtime.esm-browser.js' // to be copied on build
|
|
|
|
: '/src/vue-dev-proxy'
|
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
export class File {
|
|
|
|
filename: string
|
|
|
|
code: string
|
|
|
|
compiled = {
|
2021-03-28 13:35:45 +08:00
|
|
|
js: '',
|
2021-03-29 06:41:33 +08:00
|
|
|
css: ''
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(filename: string, code = '') {
|
|
|
|
this.filename = filename
|
|
|
|
this.code = code
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Store {
|
|
|
|
files: Record<string, File>
|
|
|
|
activeFilename: string
|
|
|
|
readonly activeFile: File
|
|
|
|
errors: (string | Error)[]
|
|
|
|
}
|
|
|
|
|
2021-03-29 11:36:36 +08:00
|
|
|
let files: Store['files'] = {}
|
|
|
|
|
|
|
|
const savedFiles = location.hash.slice(1)
|
|
|
|
if (savedFiles) {
|
|
|
|
const saved = JSON.parse(decodeURIComponent(savedFiles))
|
|
|
|
for (const filename in saved) {
|
|
|
|
files[filename] = new File(filename, saved[filename])
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
files = {
|
|
|
|
'App.vue': new File(MAIN_FILE, welcomeCode)
|
|
|
|
}
|
|
|
|
}
|
2021-03-29 06:41:33 +08:00
|
|
|
|
|
|
|
export const store: Store = reactive({
|
|
|
|
files,
|
|
|
|
activeFilename: MAIN_FILE,
|
|
|
|
get activeFile() {
|
|
|
|
return store.files[store.activeFilename]
|
2021-03-28 13:35:45 +08:00
|
|
|
},
|
2021-03-29 06:41:33 +08:00
|
|
|
errors: []
|
|
|
|
})
|
|
|
|
|
2021-03-29 11:36:36 +08:00
|
|
|
watchEffect(() => compileFile(store.activeFile))
|
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
for (const file in store.files) {
|
|
|
|
if (file !== MAIN_FILE) {
|
|
|
|
compileFile(store.files[file])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
watchEffect(() => {
|
2021-03-29 11:36:36 +08:00
|
|
|
location.hash = encodeURIComponent(JSON.stringify(exportFiles()))
|
2021-03-28 13:35:45 +08:00
|
|
|
})
|
|
|
|
|
2021-03-29 11:36:36 +08:00
|
|
|
export function exportFiles() {
|
|
|
|
const exported: Record<string, string> = {}
|
|
|
|
for (const filename in store.files) {
|
|
|
|
exported[filename] = store.files[filename].code
|
|
|
|
}
|
|
|
|
return exported
|
|
|
|
}
|
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
export function setActive(filename: string) {
|
|
|
|
store.activeFilename = filename
|
|
|
|
}
|
|
|
|
|
|
|
|
export function addFile(filename: string) {
|
|
|
|
store.files[filename] = new File(filename)
|
|
|
|
setActive(filename)
|
|
|
|
}
|
2021-03-28 13:35:45 +08:00
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
export function deleteFile(filename: string) {
|
|
|
|
if (confirm(`Are you sure you want to delete ${filename}?`)) {
|
|
|
|
if (store.activeFilename === filename) {
|
|
|
|
store.activeFilename = MAIN_FILE
|
|
|
|
}
|
|
|
|
delete store.files[filename]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function compileFile({ filename, code, compiled }: File) {
|
2021-03-28 13:35:45 +08:00
|
|
|
if (!code.trim()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
if (filename.endsWith('.js')) {
|
|
|
|
compiled.js = code
|
|
|
|
return
|
|
|
|
}
|
2021-03-28 13:35:45 +08:00
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
const id = await hashId(filename)
|
2021-03-28 13:35:45 +08:00
|
|
|
const { errors, descriptor } = parse(code, { filename, sourceMap: true })
|
|
|
|
if (errors.length) {
|
|
|
|
store.errors = errors
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasScoped = descriptor.styles.some(s => s.scoped)
|
|
|
|
let finalCode = ''
|
|
|
|
|
|
|
|
if (
|
|
|
|
(descriptor.script && descriptor.script.lang) ||
|
|
|
|
(descriptor.scriptSetup && descriptor.scriptSetup.lang) ||
|
|
|
|
descriptor.styles.some(s => s.lang) ||
|
|
|
|
(descriptor.template && descriptor.template.lang)
|
|
|
|
) {
|
|
|
|
store.errors = [
|
|
|
|
'lang="x" pre-processors are not supported in the in-browser playground.'
|
|
|
|
]
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// script
|
|
|
|
if (descriptor.script || descriptor.scriptSetup) {
|
|
|
|
try {
|
|
|
|
const compiledScript = compileScript(descriptor, {
|
|
|
|
id,
|
|
|
|
refSugar: true,
|
|
|
|
inlineTemplate: true
|
|
|
|
})
|
|
|
|
finalCode +=
|
2021-03-29 06:41:33 +08:00
|
|
|
`\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
|
2021-03-28 13:35:45 +08:00
|
|
|
} catch (e) {
|
|
|
|
store.errors = [e]
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
2021-03-29 06:41:33 +08:00
|
|
|
finalCode += `\nconst ${COMP_IDENTIFIER} = {}`
|
2021-03-28 13:35:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// template
|
|
|
|
if (descriptor.template && !descriptor.scriptSetup) {
|
|
|
|
const templateResult = compileTemplate({
|
|
|
|
source: descriptor.template.content,
|
|
|
|
filename,
|
|
|
|
id,
|
|
|
|
scoped: hasScoped,
|
|
|
|
slotted: descriptor.slotted,
|
|
|
|
isProd: false
|
|
|
|
})
|
|
|
|
if (templateResult.errors.length) {
|
|
|
|
store.errors = templateResult.errors
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-28 14:17:55 +08:00
|
|
|
finalCode +=
|
|
|
|
`\n` +
|
2021-03-29 06:41:33 +08:00
|
|
|
templateResult.code.replace(
|
2021-03-28 14:17:55 +08:00
|
|
|
/\nexport (function|const) render/,
|
|
|
|
'$1 render'
|
|
|
|
)
|
2021-03-29 06:41:33 +08:00
|
|
|
finalCode += `\n${COMP_IDENTIFIER}.render = render`
|
2021-03-28 13:35:45 +08:00
|
|
|
}
|
|
|
|
if (hasScoped) {
|
2021-03-29 06:41:33 +08:00
|
|
|
finalCode += `\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(
|
2021-03-28 13:35:45 +08:00
|
|
|
`data-v-${id}`
|
|
|
|
)}`
|
|
|
|
}
|
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
if (finalCode) {
|
2021-03-29 12:32:28 +08:00
|
|
|
finalCode += `\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}`
|
2021-03-29 06:41:33 +08:00
|
|
|
finalCode += `\nexport default ${COMP_IDENTIFIER}`
|
|
|
|
compiled.js = finalCode.trimStart()
|
|
|
|
}
|
|
|
|
|
2021-03-28 13:35:45 +08:00
|
|
|
// styles
|
|
|
|
let css = ''
|
|
|
|
for (const style of descriptor.styles) {
|
|
|
|
if (style.module) {
|
|
|
|
// TODO error
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const styleResult = await compileStyleAsync({
|
|
|
|
source: style.content,
|
|
|
|
filename,
|
|
|
|
id,
|
|
|
|
scoped: style.scoped,
|
|
|
|
modules: !!style.module
|
|
|
|
})
|
|
|
|
if (styleResult.errors.length) {
|
|
|
|
// postcss uses pathToFileURL which isn't polyfilled in the browser
|
|
|
|
// ignore these errors for now
|
|
|
|
if (!styleResult.errors[0].message.includes('pathToFileURL')) {
|
|
|
|
store.errors = styleResult.errors
|
|
|
|
}
|
|
|
|
// proceed even if css compile errors
|
|
|
|
} else {
|
|
|
|
css += styleResult.code + '\n'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (css) {
|
|
|
|
compiled.css = css.trim()
|
|
|
|
} else {
|
2021-03-29 06:41:33 +08:00
|
|
|
compiled.css = '/* No <style> tags present */'
|
2021-03-28 13:35:45 +08:00
|
|
|
}
|
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
// clear errors
|
2021-03-28 13:35:45 +08:00
|
|
|
store.errors = []
|
2021-03-29 06:41:33 +08:00
|
|
|
}
|
2021-03-28 13:35:45 +08:00
|
|
|
|
2021-03-29 06:41:33 +08:00
|
|
|
async function hashId(filename: string) {
|
|
|
|
const msgUint8 = new TextEncoder().encode(filename) // encode as (utf-8) Uint8Array
|
|
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) // hash the message
|
|
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
|
|
|
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
|
|
|
|
return hashHex.slice(0, 8)
|
2021-03-28 13:35:45 +08:00
|
|
|
}
|