workflow(sfc-playground): add ssr compile output
This commit is contained in:
parent
2e50acfbb8
commit
c771b1dc80
@ -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>
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
225
packages/sfc-playground/src/sfcCompiler.ts
Normal file
225
packages/sfc-playground/src/sfcCompiler.ts
Normal 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)
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user