--- /dev/null
+import { store, File } from './store'
+import {
+ parse,
+ compileTemplate,
+ compileStyleAsync,
+ compileScript,
+ rewriteDefault,
+ SFCDescriptor,
+ BindingMetadata
+} from '@vue/compiler-sfc'
+
+export const MAIN_FILE = 'App.vue'
+export const COMP_IDENTIFIER = `__sfc__`
+
+// @ts-ignore
+export const SANDBOX_VUE_URL = import.meta.env.PROD
+ ? '/vue.runtime.esm-browser.js' // to be copied on build
+ : '/src/vue-dev-proxy'
+
+export async function compileFile({ filename, code, compiled }: File) {
+ if (!code.trim()) {
+ return
+ }
+
+ if (filename.endsWith('.js')) {
+ compiled.js = compiled.ssr = code
+ return
+ }
+
+ const id = await hashId(filename)
+ const { errors, descriptor } = parse(code, { filename, sourceMap: true })
+ if (errors.length) {
+ store.errors = errors
+ return
+ }
+
+ 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
+ }
+
+ const hasScoped = descriptor.styles.some(s => s.scoped)
+ let clientCode = ''
+ let ssrCode = ''
+
+ const appendSharedCode = (code: string) => {
+ clientCode += code
+ ssrCode += code
+ }
+
+ const clientScriptResult = doCompileScript(descriptor, id, false)
+ if (!clientScriptResult) {
+ return
+ }
+ const [clientScript, bindings] = clientScriptResult
+ clientCode += clientScript
+
+ // script ssr only needs to be performed if using <script setup> where
+ // the render fn is inlined.
+ if (descriptor.scriptSetup) {
+ const ssrScriptResult = doCompileScript(descriptor, id, true)
+ if (!ssrScriptResult) {
+ return
+ }
+ ssrCode += ssrScriptResult[0]
+ } else {
+ // when no <script setup> is used, the script result will be identical.
+ ssrCode += clientScript
+ }
+
+ // template
+ // only need dedicated compilation if not using <script setup>
+ if (descriptor.template && !descriptor.scriptSetup) {
+ const clientTemplateResult = doCompileTemplate(
+ descriptor,
+ id,
+ bindings,
+ false
+ )
+ if (!clientTemplateResult) {
+ return
+ }
+ clientCode += clientTemplateResult
+
+ const ssrTemplateResult = doCompileTemplate(descriptor, id, bindings, true)
+ if (!ssrTemplateResult) {
+ return
+ }
+ ssrCode += ssrTemplateResult
+ }
+
+ if (hasScoped) {
+ appendSharedCode(
+ `\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`
+ )
+ }
+
+ if (clientCode || ssrCode) {
+ appendSharedCode(
+ `\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
+ `\nexport default ${COMP_IDENTIFIER}`
+ )
+ compiled.js = clientCode.trimStart()
+ compiled.ssr = ssrCode.trimStart()
+ }
+
+ // 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 {
+ compiled.css = '/* No <style> tags present */'
+ }
+
+ // clear errors
+ store.errors = []
+}
+
+function doCompileScript(
+ descriptor: SFCDescriptor,
+ id: string,
+ ssr: boolean
+): [string, BindingMetadata | undefined] | undefined {
+ if (descriptor.script || descriptor.scriptSetup) {
+ try {
+ const compiledScript = compileScript(descriptor, {
+ id,
+ refSugar: true,
+ inlineTemplate: true,
+ templateOptions: {
+ ssr,
+ ssrCssVars: descriptor.cssVars
+ }
+ })
+ let code = ''
+ if (compiledScript.bindings) {
+ code += `\n/* Analyzed bindings: ${JSON.stringify(
+ compiledScript.bindings,
+ null,
+ 2
+ )} */`
+ }
+ code += `\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
+ return [code, compiledScript.bindings]
+ } catch (e) {
+ store.errors = [e]
+ return
+ }
+ } else {
+ return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined]
+ }
+}
+
+function doCompileTemplate(
+ descriptor: SFCDescriptor,
+ id: string,
+ bindingMetadata: BindingMetadata | undefined,
+ ssr: boolean
+) {
+ const templateResult = compileTemplate({
+ source: descriptor.template!.content,
+ filename: descriptor.filename,
+ id,
+ scoped: descriptor.styles.some(s => s.scoped),
+ slotted: descriptor.slotted,
+ ssr,
+ ssrCssVars: descriptor.cssVars,
+ isProd: false,
+ compilerOptions: {
+ bindingMetadata
+ }
+ })
+ if (templateResult.errors.length) {
+ store.errors = templateResult.errors
+ return
+ }
+
+ const fnName = ssr ? `ssrRender` : `render`
+
+ return (
+ `\n${templateResult.code.replace(
+ /\nexport (function|const) (render|ssrRender)/,
+ `$1 ${fnName}`
+ )}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}`
+ )
+}
+
+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)
+}
import { reactive, watchEffect } from 'vue'
-import {
- parse,
- compileTemplate,
- compileStyleAsync,
- compileScript,
- rewriteDefault
-} from '@vue/compiler-sfc'
+import { compileFile, MAIN_FILE } from './sfcCompiler'
const welcomeCode = `
<template>
</script>
`.trim()
-export const MAIN_FILE = 'App.vue'
-export const COMP_IDENTIFIER = `__sfc__`
-
-// @ts-ignore
-export const SANDBOX_VUE_URL = import.meta.env.PROD
- ? '/vue.runtime.esm-browser.js' // to be copied on build
- : '/src/vue-dev-proxy'
-
export class File {
filename: string
code: string
compiled = {
js: '',
- css: ''
+ css: '',
+ ssr: ''
}
constructor(filename: string, code = '') {
delete store.files[filename]
}
}
-
-async function compileFile({ filename, code, compiled }: File) {
- if (!code.trim()) {
- return
- }
-
- if (filename.endsWith('.js')) {
- compiled.js = code
- return
- }
-
- const id = await hashId(filename)
- 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
- let compiledScript
- if (descriptor.script || descriptor.scriptSetup) {
- try {
- compiledScript = compileScript(descriptor, {
- id,
- refSugar: true,
- inlineTemplate: true
- })
- if (compiledScript.bindings) {
- finalCode += `\n/* Analyzed bindings: ${JSON.stringify(
- compiledScript.bindings,
- null,
- 2
- )} */`
- }
- finalCode +=
- `\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
- } catch (e) {
- store.errors = [e]
- return
- }
- } else {
- finalCode += `\nconst ${COMP_IDENTIFIER} = {}`
- }
-
- // template
- if (descriptor.template && !descriptor.scriptSetup) {
- const templateResult = compileTemplate({
- source: descriptor.template.content,
- filename,
- id,
- scoped: hasScoped,
- slotted: descriptor.slotted,
- isProd: false,
- compilerOptions: {
- bindingMetadata: compiledScript && compiledScript.bindings
- }
- })
- if (templateResult.errors.length) {
- store.errors = templateResult.errors
- return
- }
-
- finalCode +=
- `\n` +
- templateResult.code.replace(
- /\nexport (function|const) render/,
- '$1 render'
- )
- finalCode += `\n${COMP_IDENTIFIER}.render = render`
- }
- if (hasScoped) {
- finalCode += `\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(
- `data-v-${id}`
- )}`
- }
-
- if (finalCode) {
- finalCode += `\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}`
- finalCode += `\nexport default ${COMP_IDENTIFIER}`
- compiled.js = finalCode.trimStart()
- }
-
- // 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 {
- compiled.css = '/* No <style> tags present */'
- }
-
- // clear errors
- store.errors = []
-}
-
-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)
-}