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

@ -60,12 +60,16 @@ type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
export interface CodegenResult { export interface CodegenResult {
code: string code: string
preamble: string
ast: RootNode ast: RootNode
map?: RawSourceMap map?: RawSourceMap
} }
export interface CodegenContext export interface CodegenContext
extends Omit<Required<CodegenOptions>, 'bindingMetadata'> { extends Omit<
Required<CodegenOptions>,
'bindingMetadata' | 'inline' | 'inlinePropsIdentifier'
> {
source: string source: string
code: string code: string
line: number line: number
@ -199,12 +203,18 @@ export function generate(
const hasHelpers = ast.helpers.length > 0 const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module' const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
const isSetupInlined = !!options.inline
// preambles // preambles
// in setup() inline mode, the preamble is generated in a sub context
// and returned separately.
const preambleContext = isSetupInlined
? createCodegenContext(ast, options)
: context
if (!__BROWSER__ && mode === 'module') { if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, context, genScopeId) genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else { } else {
genFunctionPreamble(ast, context) genFunctionPreamble(ast, preambleContext)
} }
// binding optimizations // binding optimizations
@ -213,10 +223,17 @@ export function generate(
: `` : ``
// enter render function // enter render function
if (!ssr) { if (!ssr) {
if (genScopeId) { if (isSetupInlined) {
push(`const render = ${PURE_ANNOTATION}_withId(`) if (genScopeId) {
push(`${PURE_ANNOTATION}_withId(`)
}
push(`() => {`)
} else {
if (genScopeId) {
push(`const render = ${PURE_ANNOTATION}_withId(`)
}
push(`function render(_ctx, _cache${optimizeSources}) {`)
} }
push(`function render(_ctx, _cache${optimizeSources}) {`)
} else { } else {
if (genScopeId) { if (genScopeId) {
push(`const ssrRender = ${PURE_ANNOTATION}_withId(`) push(`const ssrRender = ${PURE_ANNOTATION}_withId(`)
@ -290,6 +307,7 @@ export function generate(
return { return {
ast, ast,
code: context.code, code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types // SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined map: context.map ? (context.map as any).toJSON() : undefined
} }
@ -356,7 +374,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
function genModulePreamble( function genModulePreamble(
ast: RootNode, ast: RootNode,
context: CodegenContext, context: CodegenContext,
genScopeId: boolean genScopeId: boolean,
inline?: boolean
) { ) {
const { const {
push, push,
@ -423,7 +442,10 @@ function genModulePreamble(
genHoists(ast.hoists, context) genHoists(ast.hoists, context)
newline() newline()
push(`export `)
if (!inline) {
push(`export `)
}
} }
function genAssets( function genAssets(

View File

@ -65,7 +65,39 @@ export interface BindingMetadata {
[key: string]: 'data' | 'props' | 'setup' | 'options' [key: string]: 'data' | 'props' | 'setup' | 'options'
} }
export interface TransformOptions { interface SharedTransformCodegenOptions {
/**
* Transform expressions like {{ foo }} to `_ctx.foo`.
* If this option is false, the generated code will be wrapped in a
* `with (this) { ... }` block.
* - This is force-enabled in module mode, since modules are by default strict
* and cannot use `with`
* @default mode === 'module'
*/
prefixIdentifiers?: boolean
/**
* Generate SSR-optimized render functions instead.
* The resulting function must be attached to the component via the
* `ssrRender` option instead of `render`.
*/
ssr?: boolean
/**
* Optional binding metadata analyzed from script - used to optimize
* binding access when `prefixIdentifiers` is enabled.
*/
bindingMetadata?: BindingMetadata
/**
* Compile the function for inlining inside setup().
* This allows the function to directly access setup() local bindings.
*/
inline?: boolean
/**
* Identifier for props in setup() inline mode.
*/
inlinePropsIdentifier?: string
}
export interface TransformOptions extends SharedTransformCodegenOptions {
/** /**
* An array of node transforms to be applied to every AST node. * An array of node transforms to be applied to every AST node.
*/ */
@ -128,26 +160,15 @@ export interface TransformOptions {
* SFC scoped styles ID * SFC scoped styles ID
*/ */
scopeId?: string | null scopeId?: string | null
/**
* Generate SSR-optimized render functions instead.
* The resulting function must be attached to the component via the
* `ssrRender` option instead of `render`.
*/
ssr?: boolean
/** /**
* SFC `<style vars>` injection string * SFC `<style vars>` injection string
* needed to render inline CSS variables on component root * needed to render inline CSS variables on component root
*/ */
ssrCssVars?: string ssrCssVars?: string
/**
* Optional binding metadata analyzed from script - used to optimize
* binding access when `prefixIdentifiers` is enabled.
*/
bindingMetadata?: BindingMetadata
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
export interface CodegenOptions { export interface CodegenOptions extends SharedTransformCodegenOptions {
/** /**
* - `module` mode will generate ES module import statements for helpers * - `module` mode will generate ES module import statements for helpers
* and export the render function as the default export. * and export the render function as the default export.
@ -189,11 +210,6 @@ export interface CodegenOptions {
* @default 'Vue' * @default 'Vue'
*/ */
runtimeGlobalName?: string runtimeGlobalName?: string
// we need to know this during codegen to generate proper preambles
prefixIdentifiers?: boolean
bindingMetadata?: BindingMetadata
// generate ssr-specific code?
ssr?: boolean
} }
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

View File

@ -29,6 +29,7 @@ export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``) export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``) export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``) export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
export const UNREF = Symbol(__DEV__ ? `unref` : ``)
// Name mapping for runtime helpers that need to be imported from 'vue' in // Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime! // generated code. Make sure these are correctly exported in the runtime!
@ -62,7 +63,8 @@ export const helperNameMap: any = {
[PUSH_SCOPE_ID]: `pushScopeId`, [PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`, [POP_SCOPE_ID]: `popScopeId`,
[WITH_SCOPE_ID]: `withScopeId`, [WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx` [WITH_CTX]: `withCtx`,
[UNREF]: `unref`
} }
export function registerRuntimeHelpers(helpers: any) { export function registerRuntimeHelpers(helpers: any) {

View File

@ -124,6 +124,8 @@ export function createTransformContext(
ssr = false, ssr = false,
ssrCssVars = ``, ssrCssVars = ``,
bindingMetadata = EMPTY_OBJ, bindingMetadata = EMPTY_OBJ,
inline = false,
inlinePropsIdentifier = `$props`,
onError = defaultOnError onError = defaultOnError
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
@ -142,6 +144,8 @@ export function createTransformContext(
ssr, ssr,
ssrCssVars, ssrCssVars,
bindingMetadata, bindingMetadata,
inline,
inlinePropsIdentifier,
onError, onError,
// state // state

View File

@ -28,6 +28,7 @@ import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { parse } from '@babel/parser' import { parse } from '@babel/parser'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { UNREF } from '../runtimeHelpers'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@ -97,12 +98,21 @@ export function processExpression(
return node return node
} }
const { bindingMetadata } = context const { inline, inlinePropsIdentifier, bindingMetadata } = context
const prefix = (raw: string) => { const prefix = (raw: string) => {
const source = hasOwn(bindingMetadata, raw) if (inline) {
? `$` + bindingMetadata[raw] // setup inline mode, it's either props or setup
: `_ctx` if (bindingMetadata[raw] !== 'setup') {
return `${source}.${raw}` return `${inlinePropsIdentifier}.${raw}`
} else {
return `${context.helperString(UNREF)}(${raw})`
}
} else {
const source = hasOwn(bindingMetadata, raw)
? `$` + bindingMetadata[raw]
: `_ctx`
return `${source}.${raw}`
}
} }
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.

View File

@ -464,14 +464,20 @@ describe('SFC compile <script setup>', () => {
compile(`<script setup> compile(`<script setup>
export const a = 1 export const a = 1
</script>`) </script>`)
).toThrow(`cannot contain non-type named exports`) ).toThrow(`cannot contain non-type named or * exports`)
expect(() =>
compile(`<script setup>
export * from './foo'
</script>`)
).toThrow(`cannot contain non-type named or * exports`)
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 1 const bar = 1
export { bar as default } export { bar as default }
</script>`) </script>`)
).toThrow(`cannot contain non-type named exports`) ).toThrow(`cannot contain non-type named or * exports`)
}) })
test('ref: non-assignment expressions', () => { test('ref: non-assignment expressions', () => {

View File

@ -27,13 +27,28 @@ import {
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map' import { RawSourceMap } from 'source-map'
import { genCssVarsCode, injectCssVarsCalls } from './genCssVars' import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
/** /**
* https://babeljs.io/docs/en/babel-parser#plugins * https://babeljs.io/docs/en/babel-parser#plugins
*/ */
babelParserPlugins?: ParserPlugin[] babelParserPlugins?: ParserPlugin[]
/**
* Enable ref: label sugar
* https://github.com/vuejs/rfcs/pull/228
* @default true
*/
refSugar?: boolean 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> = {} const hasWarned: Record<string, boolean> = {}
@ -356,10 +371,10 @@ export function compileScript(
const setupValue = scriptSetup.setup const setupValue = scriptSetup.setup
const hasExplicitSignature = typeof setupValue === 'string' const hasExplicitSignature = typeof setupValue === 'string'
let propsVar: string | undefined let propsIdentifier: string | undefined
let emitVar: string | undefined let emitIdentifier: string | undefined
let slotsVar: string | undefined let slotsIdentifier: string | undefined
let attrsVar: string | undefined let attrsIdentifier: string | undefined
let propsType = `{}` let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void` 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) { if (isTS) {
// <script setup="xxx" lang="ts"> // <script setup="xxx" lang="ts">
// parse the signature to extract the props/emit variables the user wants // additional identifiers are needed for TS in order to match declared
// we need them to find corresponding type declarations. // types
const params = ((signatureAST as ExpressionStatement)
.expression as ArrowFunctionExpression).params
if (params[0] && params[0].type === 'Identifier') {
propsASTNode = params[0]
propsVar = propsASTNode.name
}
if (params[1] && params[1].type === 'ObjectPattern') { if (params[1] && params[1].type === 'ObjectPattern') {
setupCtxASTNode = params[1] setupCtxASTNode = params[1]
for (const p of params[1].properties) { for (const p of params[1].properties) {
@ -409,11 +428,11 @@ export function compileScript(
p.value.type === 'Identifier' p.value.type === 'Identifier'
) { ) {
if (p.key.name === 'emit') { if (p.key.name === 'emit') {
emitVar = p.value.name emitIdentifier = p.value.name
} else if (p.key.name === 'slots') { } else if (p.key.name === 'slots') {
slotsVar = p.value.name slotsIdentifier = p.value.name
} else if (p.key.name === 'attrs') { } else if (p.key.name === 'attrs') {
attrsVar = p.value.name attrsIdentifier = p.value.name
} }
} }
} }
@ -560,16 +579,16 @@ export function compileScript(
typeNode.end! + startOffset typeNode.end! + startOffset
) )
if (typeNode.type === 'TSTypeLiteral') { if (typeNode.type === 'TSTypeLiteral') {
if (id.name === propsVar) { if (id.name === propsIdentifier) {
propsType = typeString propsType = typeString
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes) extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
} else if (id.name === slotsVar) { } else if (id.name === slotsIdentifier) {
slotsType = typeString slotsType = typeString
} else if (id.name === attrsVar) { } else if (id.name === attrsIdentifier) {
attrsType = typeString attrsType = typeString
} }
} else if ( } else if (
id.name === emitVar && id.name === emitIdentifier &&
typeNode.type === 'TSFunctionType' typeNode.type === 'TSFunctionType'
) { ) {
emitType = typeString emitType = typeString
@ -583,10 +602,10 @@ export function compileScript(
if ( if (
node.type === 'TSDeclareFunction' && node.type === 'TSDeclareFunction' &&
node.id && node.id &&
node.id.name === emitVar node.id.name === emitIdentifier
) { ) {
const index = node.id.start! + startOffset const index = node.id.start! + startOffset
s.overwrite(index, index + emitVar.length, '__emit__') s.overwrite(index, index + emitIdentifier.length, '__emit__')
emitType = `typeof __emit__` emitType = `typeof __emit__`
extractRuntimeEmits(node, typeDeclaredEmits) extractRuntimeEmits(node, typeDeclaredEmits)
} }
@ -681,7 +700,7 @@ export function compileScript(
} }
// 7. finalize setup argument signature. // 7. finalize setup argument signature.
let args = `` let args = options.inlineTemplate ? `$props` : ``
if (isTS) { if (isTS) {
if (slotsType === 'Slots') { if (slotsType === 'Slots') {
helperImports.add('Slots') helperImports.add('Slots')
@ -704,8 +723,8 @@ export function compileScript(
} }
args = ss.toString() args = ss.toString()
} }
} else { } else if (hasExplicitSignature) {
args = hasExplicitSignature ? (setupValue as string) : `` args = setupValue as string
} }
// 8. wrap setup code with function. // 8. wrap setup code with function.
@ -716,11 +735,9 @@ export function compileScript(
`\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n` `\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
) )
// generate return statement
const exposedBindings = { ...userImports, ...setupBindings } const exposedBindings = { ...userImports, ...setupBindings }
let returned = `{ ${Object.keys(exposedBindings).join(', ')} }`
// inject `useCssVars` calls // 9. inject `useCssVars` calls
if (hasCssVars) { if (hasCssVars) {
helperImports.add(`useCssVars`) helperImports.add(`useCssVars`)
for (const style of styles) { 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`) s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
// 9. finalize default export // 12. finalize default export
if (isTS) { if (isTS) {
// for TS, make sure the exported type is still valid type with // for TS, make sure the exported type is still valid type with
// correct props information // 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') const helpers = [...helperImports].filter(i => userImports[i] !== 'vue')
if (helpers.length) { if (helpers.length) {
s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`) 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() s.trim()
return { return {
...scriptSetup, ...scriptSetup,

View File

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