Skip to content

Commit c771b1d

Browse files
committed
workflow(sfc-playground): add ssr compile output
1 parent 2e50acf commit c771b1d

File tree

5 files changed

+233
-160
lines changed

5 files changed

+233
-160
lines changed

packages/sfc-playground/src/output/Output.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import CodeMirror from '../codemirror/CodeMirror.vue'
2020
import { store } from '../store'
2121
import { ref } from 'vue'
2222
23-
type Modes = 'preview' | 'js' | 'css'
23+
type Modes = 'preview' | 'js' | 'css' | 'ssr'
2424
25-
const modes: Modes[] = ['preview', 'js', 'css']
25+
const modes: Modes[] = ['preview', 'js', 'css', 'ssr']
2626
const mode = ref<Modes>('preview')
2727
</script>
2828

packages/sfc-playground/src/output/Preview.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Message from '../Message.vue'
1414
import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
1515
import srcdoc from './srcdoc.html?raw'
1616
import { PreviewProxy } from './PreviewProxy'
17-
import { MAIN_FILE, SANDBOX_VUE_URL } from '../store'
17+
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
1818
import { compileModulesForPreview } from './moduleCompiler'
1919
2020
const iframe = ref()

packages/sfc-playground/src/output/moduleCompiler.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { store, MAIN_FILE, SANDBOX_VUE_URL, File } from '../store'
1+
import { store, File } from '../store'
2+
import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
23
import {
34
babelParse,
45
MagicString,
+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { store, File } from './store'
2+
import {
3+
parse,
4+
compileTemplate,
5+
compileStyleAsync,
6+
compileScript,
7+
rewriteDefault,
8+
SFCDescriptor,
9+
BindingMetadata
10+
} from '@vue/compiler-sfc'
11+
12+
export const MAIN_FILE = 'App.vue'
13+
export const COMP_IDENTIFIER = `__sfc__`
14+
15+
// @ts-ignore
16+
export const SANDBOX_VUE_URL = import.meta.env.PROD
17+
? '/vue.runtime.esm-browser.js' // to be copied on build
18+
: '/src/vue-dev-proxy'
19+
20+
export async function compileFile({ filename, code, compiled }: File) {
21+
if (!code.trim()) {
22+
return
23+
}
24+
25+
if (filename.endsWith('.js')) {
26+
compiled.js = compiled.ssr = code
27+
return
28+
}
29+
30+
const id = await hashId(filename)
31+
const { errors, descriptor } = parse(code, { filename, sourceMap: true })
32+
if (errors.length) {
33+
store.errors = errors
34+
return
35+
}
36+
37+
if (
38+
(descriptor.script && descriptor.script.lang) ||
39+
(descriptor.scriptSetup && descriptor.scriptSetup.lang) ||
40+
descriptor.styles.some(s => s.lang) ||
41+
(descriptor.template && descriptor.template.lang)
42+
) {
43+
store.errors = [
44+
'lang="x" pre-processors are not supported in the in-browser playground.'
45+
]
46+
return
47+
}
48+
49+
const hasScoped = descriptor.styles.some(s => s.scoped)
50+
let clientCode = ''
51+
let ssrCode = ''
52+
53+
const appendSharedCode = (code: string) => {
54+
clientCode += code
55+
ssrCode += code
56+
}
57+
58+
const clientScriptResult = doCompileScript(descriptor, id, false)
59+
if (!clientScriptResult) {
60+
return
61+
}
62+
const [clientScript, bindings] = clientScriptResult
63+
clientCode += clientScript
64+
65+
// script ssr only needs to be performed if using <script setup> where
66+
// the render fn is inlined.
67+
if (descriptor.scriptSetup) {
68+
const ssrScriptResult = doCompileScript(descriptor, id, true)
69+
if (!ssrScriptResult) {
70+
return
71+
}
72+
ssrCode += ssrScriptResult[0]
73+
} else {
74+
// when no <script setup> is used, the script result will be identical.
75+
ssrCode += clientScript
76+
}
77+
78+
// template
79+
// only need dedicated compilation if not using <script setup>
80+
if (descriptor.template && !descriptor.scriptSetup) {
81+
const clientTemplateResult = doCompileTemplate(
82+
descriptor,
83+
id,
84+
bindings,
85+
false
86+
)
87+
if (!clientTemplateResult) {
88+
return
89+
}
90+
clientCode += clientTemplateResult
91+
92+
const ssrTemplateResult = doCompileTemplate(descriptor, id, bindings, true)
93+
if (!ssrTemplateResult) {
94+
return
95+
}
96+
ssrCode += ssrTemplateResult
97+
}
98+
99+
if (hasScoped) {
100+
appendSharedCode(
101+
`\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`
102+
)
103+
}
104+
105+
if (clientCode || ssrCode) {
106+
appendSharedCode(
107+
`\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
108+
`\nexport default ${COMP_IDENTIFIER}`
109+
)
110+
compiled.js = clientCode.trimStart()
111+
compiled.ssr = ssrCode.trimStart()
112+
}
113+
114+
// styles
115+
let css = ''
116+
for (const style of descriptor.styles) {
117+
if (style.module) {
118+
// TODO error
119+
continue
120+
}
121+
122+
const styleResult = await compileStyleAsync({
123+
source: style.content,
124+
filename,
125+
id,
126+
scoped: style.scoped,
127+
modules: !!style.module
128+
})
129+
if (styleResult.errors.length) {
130+
// postcss uses pathToFileURL which isn't polyfilled in the browser
131+
// ignore these errors for now
132+
if (!styleResult.errors[0].message.includes('pathToFileURL')) {
133+
store.errors = styleResult.errors
134+
}
135+
// proceed even if css compile errors
136+
} else {
137+
css += styleResult.code + '\n'
138+
}
139+
}
140+
if (css) {
141+
compiled.css = css.trim()
142+
} else {
143+
compiled.css = '/* No <style> tags present */'
144+
}
145+
146+
// clear errors
147+
store.errors = []
148+
}
149+
150+
function doCompileScript(
151+
descriptor: SFCDescriptor,
152+
id: string,
153+
ssr: boolean
154+
): [string, BindingMetadata | undefined] | undefined {
155+
if (descriptor.script || descriptor.scriptSetup) {
156+
try {
157+
const compiledScript = compileScript(descriptor, {
158+
id,
159+
refSugar: true,
160+
inlineTemplate: true,
161+
templateOptions: {
162+
ssr,
163+
ssrCssVars: descriptor.cssVars
164+
}
165+
})
166+
let code = ''
167+
if (compiledScript.bindings) {
168+
code += `\n/* Analyzed bindings: ${JSON.stringify(
169+
compiledScript.bindings,
170+
null,
171+
2
172+
)} */`
173+
}
174+
code += `\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
175+
return [code, compiledScript.bindings]
176+
} catch (e) {
177+
store.errors = [e]
178+
return
179+
}
180+
} else {
181+
return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined]
182+
}
183+
}
184+
185+
function doCompileTemplate(
186+
descriptor: SFCDescriptor,
187+
id: string,
188+
bindingMetadata: BindingMetadata | undefined,
189+
ssr: boolean
190+
) {
191+
const templateResult = compileTemplate({
192+
source: descriptor.template!.content,
193+
filename: descriptor.filename,
194+
id,
195+
scoped: descriptor.styles.some(s => s.scoped),
196+
slotted: descriptor.slotted,
197+
ssr,
198+
ssrCssVars: descriptor.cssVars,
199+
isProd: false,
200+
compilerOptions: {
201+
bindingMetadata
202+
}
203+
})
204+
if (templateResult.errors.length) {
205+
store.errors = templateResult.errors
206+
return
207+
}
208+
209+
const fnName = ssr ? `ssrRender` : `render`
210+
211+
return (
212+
`\n${templateResult.code.replace(
213+
/\nexport (function|const) (render|ssrRender)/,
214+
`$1 ${fnName}`
215+
)}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}`
216+
)
217+
}
218+
219+
async function hashId(filename: string) {
220+
const msgUint8 = new TextEncoder().encode(filename) // encode as (utf-8) Uint8Array
221+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) // hash the message
222+
const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
223+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
224+
return hashHex.slice(0, 8)
225+
}

0 commit comments

Comments
 (0)