feat(compiler-sfc): compileScript inline render function mode

This commit is contained in:
Evan You
2020-11-10 16:28:34 -05:00
parent 3f99e239e0
commit 886ed7681d
8 changed files with 192 additions and 77 deletions

View File

@@ -27,13 +27,28 @@ import {
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
export interface SFCScriptCompileOptions {
/**
* https://babeljs.io/docs/en/babel-parser#plugins
*/
babelParserPlugins?: ParserPlugin[]
/**
* Enable ref: label sugar
* https://github.com/vuejs/rfcs/pull/228
* @default true
*/
refSugar?: boolean
/**
* Compile the template and inline the resulting render function
* directly inside setup().
* - Only affects <script setup>
* - This should only be used in production because it prevents the template
* from being hot-reloaded separately from component state.
*/
inlineTemplate?: boolean
templateOptions?: SFCTemplateCompileOptions
}
const hasWarned: Record<string, boolean> = {}
@@ -356,10 +371,10 @@ export function compileScript(
const setupValue = scriptSetup.setup
const hasExplicitSignature = typeof setupValue === 'string'
let propsVar: string | undefined
let emitVar: string | undefined
let slotsVar: string | undefined
let attrsVar: string | undefined
let propsIdentifier: string | undefined
let emitIdentifier: string | undefined
let slotsIdentifier: string | undefined
let attrsIdentifier: string | undefined
let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void`
@@ -390,16 +405,20 @@ export function compileScript(
)
}
// parse the signature to extract the identifiers users are assigning to
// the arguments. props identifier is always needed for inline mode
// template compilation
const params = ((signatureAST as ExpressionStatement)
.expression as ArrowFunctionExpression).params
if (params[0] && params[0].type === 'Identifier') {
propsASTNode = params[0]
propsIdentifier = propsASTNode.name
}
if (isTS) {
// <script setup="xxx" lang="ts">
// parse the signature to extract the props/emit variables the user wants
// we need them to find corresponding type declarations.
const params = ((signatureAST as ExpressionStatement)
.expression as ArrowFunctionExpression).params
if (params[0] && params[0].type === 'Identifier') {
propsASTNode = params[0]
propsVar = propsASTNode.name
}
// additional identifiers are needed for TS in order to match declared
// types
if (params[1] && params[1].type === 'ObjectPattern') {
setupCtxASTNode = params[1]
for (const p of params[1].properties) {
@@ -409,11 +428,11 @@ export function compileScript(
p.value.type === 'Identifier'
) {
if (p.key.name === 'emit') {
emitVar = p.value.name
emitIdentifier = p.value.name
} else if (p.key.name === 'slots') {
slotsVar = p.value.name
slotsIdentifier = p.value.name
} else if (p.key.name === 'attrs') {
attrsVar = p.value.name
attrsIdentifier = p.value.name
}
}
}
@@ -560,16 +579,16 @@ export function compileScript(
typeNode.end! + startOffset
)
if (typeNode.type === 'TSTypeLiteral') {
if (id.name === propsVar) {
if (id.name === propsIdentifier) {
propsType = typeString
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
} else if (id.name === slotsVar) {
} else if (id.name === slotsIdentifier) {
slotsType = typeString
} else if (id.name === attrsVar) {
} else if (id.name === attrsIdentifier) {
attrsType = typeString
}
} else if (
id.name === emitVar &&
id.name === emitIdentifier &&
typeNode.type === 'TSFunctionType'
) {
emitType = typeString
@@ -583,10 +602,10 @@ export function compileScript(
if (
node.type === 'TSDeclareFunction' &&
node.id &&
node.id.name === emitVar
node.id.name === emitIdentifier
) {
const index = node.id.start! + startOffset
s.overwrite(index, index + emitVar.length, '__emit__')
s.overwrite(index, index + emitIdentifier.length, '__emit__')
emitType = `typeof __emit__`
extractRuntimeEmits(node, typeDeclaredEmits)
}
@@ -681,7 +700,7 @@ export function compileScript(
}
// 7. finalize setup argument signature.
let args = ``
let args = options.inlineTemplate ? `$props` : ``
if (isTS) {
if (slotsType === 'Slots') {
helperImports.add('Slots')
@@ -704,8 +723,8 @@ export function compileScript(
}
args = ss.toString()
}
} else {
args = hasExplicitSignature ? (setupValue as string) : ``
} else if (hasExplicitSignature) {
args = setupValue as string
}
// 8. wrap setup code with function.
@@ -716,11 +735,9 @@ export function compileScript(
`\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
)
// generate return statement
const exposedBindings = { ...userImports, ...setupBindings }
let returned = `{ ${Object.keys(exposedBindings).join(', ')} }`
// inject `useCssVars` calls
// 9. inject `useCssVars` calls
if (hasCssVars) {
helperImports.add(`useCssVars`)
for (const style of styles) {
@@ -734,9 +751,58 @@ export function compileScript(
}
}
// 10. analyze binding metadata
if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
}
Object.keys(exposedBindings).forEach(key => {
bindingMetadata[key] = 'setup'
})
Object.keys(typeDeclaredProps).forEach(key => {
bindingMetadata[key] = 'props'
})
Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
// 11. generate return statement
let returned
if (options.inlineTemplate) {
if (sfc.template) {
// inline render function mode - we are going to compile the template and
// inline it right here
const { code, preamble, tips, errors } = compileTemplate({
...options.templateOptions,
filename,
source: sfc.template.content,
compilerOptions: {
inline: true,
inlinePropsIdentifier: propsIdentifier,
bindingMetadata
}
// TODO source map
})
if (tips.length) {
tips.forEach(warnOnce)
}
const err = errors[0]
if (typeof err === 'string') {
throw new Error(err)
} else if (err) {
throw err
}
if (preamble) {
s.prepend(preamble)
}
returned = code
} else {
returned = `() => {}`
}
} else {
// return bindings from setup
returned = `{ ${Object.keys(exposedBindings).join(', ')} }`
}
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
// 9. finalize default export
// 12. finalize default export
if (isTS) {
// for TS, make sure the exported type is still valid type with
// correct props information
@@ -759,24 +825,12 @@ export function compileScript(
}
}
// 10. finalize Vue helper imports
// 13. finalize Vue helper imports
const helpers = [...helperImports].filter(i => userImports[i] !== 'vue')
if (helpers.length) {
s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`)
}
// 11. expose bindings for template compiler optimization
if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
}
Object.keys(exposedBindings).forEach(key => {
bindingMetadata[key] = 'setup'
})
Object.keys(typeDeclaredProps).forEach(key => {
bindingMetadata[key] = 'props'
})
Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
s.trim()
return {
...scriptSetup,

View File

@@ -30,6 +30,7 @@ export interface TemplateCompiler {
export interface SFCTemplateCompileResults {
code: string
preamble?: string
source: string
tips: string[]
errors: (string | CompilerError)[]
@@ -168,7 +169,7 @@ function doCompileTemplate({
nodeTransforms = [transformAssetUrl, transformSrcset]
}
let { code, map } = compiler.compile(source, {
let { code, preamble, map } = compiler.compile(source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
@@ -192,7 +193,7 @@ function doCompileTemplate({
}
}
return { code, source, errors, tips: [], map }
return { code, preamble, source, errors, tips: [], map }
}
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {