-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
workflow(sfc-playground): add ssr compile output
- Loading branch information
Showing
5 changed files
with
233 additions
and
160 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Oops, something went wrong.