workflow(sfc-playground): add ssr compile output

This commit is contained in:
Evan You 2021-03-30 12:36:59 -04:00
parent 2e50acfbb8
commit c771b1dc80
5 changed files with 233 additions and 160 deletions

View File

@ -20,9 +20,9 @@ import CodeMirror from '../codemirror/CodeMirror.vue'
import { store } from '../store' import { store } from '../store'
import { ref } from 'vue' import { ref } from 'vue'
type Modes = 'preview' | 'js' | 'css' type Modes = 'preview' | 'js' | 'css' | 'ssr'
const modes: Modes[] = ['preview', 'js', 'css'] const modes: Modes[] = ['preview', 'js', 'css', 'ssr']
const mode = ref<Modes>('preview') const mode = ref<Modes>('preview')
</script> </script>

View File

@ -14,7 +14,7 @@ import Message from '../Message.vue'
import { ref, onMounted, onUnmounted, watchEffect } from 'vue' import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
import srcdoc from './srcdoc.html?raw' import srcdoc from './srcdoc.html?raw'
import { PreviewProxy } from './PreviewProxy' import { PreviewProxy } from './PreviewProxy'
import { MAIN_FILE, SANDBOX_VUE_URL } from '../store' import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
import { compileModulesForPreview } from './moduleCompiler' import { compileModulesForPreview } from './moduleCompiler'
const iframe = ref() const iframe = ref()

View File

@ -1,4 +1,5 @@
import { store, MAIN_FILE, SANDBOX_VUE_URL, File } from '../store' import { store, File } from '../store'
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
import { import {
babelParse, babelParse,
MagicString, MagicString,

View File

@ -0,0 +1,225 @@
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)
}

View File

@ -1,11 +1,5 @@
import { reactive, watchEffect } from 'vue' import { reactive, watchEffect } from 'vue'
import { import { compileFile, MAIN_FILE } from './sfcCompiler'
parse,
compileTemplate,
compileStyleAsync,
compileScript,
rewriteDefault
} from '@vue/compiler-sfc'
const welcomeCode = ` const welcomeCode = `
<template> <template>
@ -17,20 +11,13 @@ const msg = 'Hello World!'
</script> </script>
`.trim() `.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 { export class File {
filename: string filename: string
code: string code: string
compiled = { compiled = {
js: '', js: '',
css: '' css: '',
ssr: ''
} }
constructor(filename: string, code = '') { constructor(filename: string, code = '') {
@ -106,143 +93,3 @@ export function deleteFile(filename: string) {
delete store.files[filename] 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)
}