wip(ssr): ssr helper codegen

This commit is contained in:
Evan You
2020-02-03 17:47:06 -05:00
parent d1d81cf1f9
commit b685805a26
18 changed files with 374 additions and 253 deletions

View File

@@ -103,6 +103,7 @@ export interface RootNode extends Node {
imports: ImportItem[]
cached: number
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
ssrHelpers?: symbol[]
}
export type ElementNode =

View File

@@ -74,7 +74,7 @@ function createCodegenContext(
ast: RootNode,
{
mode = 'function',
prefixIdentifiers = mode === 'module' || mode === 'cjs',
prefixIdentifiers = mode === 'module',
sourceMap = false,
filename = `template.vue.html`,
scopeId = null,
@@ -169,7 +169,6 @@ export function generate(
const {
mode,
push,
helper,
prefixIdentifiers,
indent,
deindent,
@@ -182,58 +181,10 @@ export function generate(
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// preambles
if (mode === 'function' || mode === 'cjs') {
const VueBinding = mode === 'function' ? `Vue` : `require("vue")`
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (hasHelpers) {
if (prefixIdentifiers) {
push(
`const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
genHoists(ast.hoists, context)
newline()
push(`return `)
if (mode === 'module') {
genModulePreamble(ast, context, genScopeId)
} else {
// generate import statements for helpers
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
if (ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
}
if (hasHelpers) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
if (genScopeId) {
push(`const withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export `)
genFunctionPreamble(ast, context)
}
// enter render function
@@ -315,6 +266,82 @@ export function generate(
}
}
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const { mode, helper, prefixIdentifiers, push, newline } = context
const VueBinding = mode === 'function' ? `Vue` : `require("vue")`
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (ast.helpers.length > 0) {
if (prefixIdentifiers) {
push(`const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
// generate variables for ssr helpers
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
// ssr guaruntees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(helper)
.join(', ')} } = require("@vue/server-renderer")\n`
)
}
genHoists(ast.hoists, context)
newline()
push(`return `)
}
function genModulePreamble(
ast: RootNode,
context: CodegenContext,
genScopeId: boolean
) {
const { push, helper, newline, scopeId } = context
// generate import statements for helpers
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
if (ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
}
if (ast.helpers.length) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
push(
`import { ${ast.ssrHelpers
.map(helper)
.join(', ')} } from "@vue/server-renderer"\n`
)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
if (genScopeId) {
push(`const withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export `)
}
function genAssets(
assets: string[],
type: 'component' | 'directive',

View File

@@ -33,6 +33,7 @@ export { transformOn } from './transforms/vOn'
// exported for compiler-ssr
export { processIfBranches } from './transforms/vIf'
export { processForNode } from './transforms/vFor'
export {
transformExpression,
processExpression

View File

@@ -23,7 +23,6 @@ export interface ParserOptions {
// this number is based on the map above, but it should be pre-computed
// to avoid the cost on every parse() call.
maxCRNameLength?: number
onError?: (error: CompilerError) => void
}
@@ -32,6 +31,8 @@ export interface TransformOptions {
directiveTransforms?: { [name: string]: DirectiveTransform | undefined }
isBuiltInComponent?: (tag: string) => symbol | void
// 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'
@@ -48,6 +49,7 @@ export interface TransformOptions {
// analysis to determine if a handler is safe to cache.
// - Default: false
cacheHandlers?: boolean
ssr?: boolean
onError?: (error: CompilerError) => void
}
@@ -61,13 +63,6 @@ export interface CodegenOptions {
// `require('vue')`.
// - Default: 'function'
mode?: 'module' | 'function' | 'cjs'
// Prefix suitable identifiers with _ctx.
// 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 source map?
// - Default: false
sourceMap?: boolean
@@ -76,7 +71,9 @@ export interface CodegenOptions {
filename?: string
// SFC scoped styles ID
scopeId?: string | null
// generate SSR specific code?
// we need to know about this to generate proper preambles
prefixIdentifiers?: boolean
// generate ssr-specific code?
ssr?: boolean
}

View File

@@ -115,6 +115,7 @@ function createTransformContext(
nodeTransforms = [],
directiveTransforms = {},
isBuiltInComponent = NOOP,
ssr = false,
onError = defaultOnError
}: TransformOptions
): TransformContext {
@@ -126,6 +127,7 @@ function createTransformContext(
nodeTransforms,
directiveTransforms,
isBuiltInComponent,
ssr,
onError,
// state
@@ -256,10 +258,19 @@ export function transform(root: RootNode, options: TransformOptions) {
if (options.hoistStatic) {
hoistStatic(root, context)
}
finalizeRoot(root, context)
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.cached = context.cached
}
function finalizeRoot(root: RootNode, context: TransformContext) {
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
const child = children[0]
@@ -304,13 +315,6 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
} else {
// no children = noop. codegen will return null.
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.cached = context.cached
}
export function traverseChildren(
@@ -359,13 +363,17 @@ export function traverseNode(
switch (node.type) {
case NodeTypes.COMMENT:
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
context.helper(TO_DISPLAY_STRING)
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards

View File

@@ -17,7 +17,10 @@ import {
ForCodegenNode,
ElementCodegenNode,
SlotOutletCodegenNode,
SlotOutletNode
SlotOutletNode,
ElementNode,
DirectiveNode,
ForNode
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
@@ -41,141 +44,163 @@ import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { helper, addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [source])
const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
const codegenNode = createSequenceExpression([
// fragment blocks disable tracking since they always diff their children
createCallExpression(helper(OPEN_BLOCK), [`false`]),
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
`null`,
renderExp,
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
const { helper } = context
return processForNode(node, dir, context, (forNode, parseResult) => {
// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source
])
]) as ForCodegenNode
const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
forNode.codegenNode = createSequenceExpression([
// fragment blocks disable tracking since they always diff their children
createCallExpression(helper(OPEN_BLOCK), [`false`]),
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
`null`,
renderExp,
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
])
]) as ForCodegenNode
context.replaceNode({
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
codegenNode
})
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
// finish the codegen now that all children have been traversed
let childBlock
const isTemplate = isTemplateNode(node)
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
return () => {
// finish the codegen now that all children have been traversed
let childBlock
const isTemplate = isTemplateNode(node)
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
)
: null
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (isTemplate) {
// <template v-for="...">
// should generate a fragment block for each loop
childBlock = createBlockExpression(
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`,
node.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`
]),
context
)
: null
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (isTemplate) {
// <template v-for="...">
// should generate a fragment block for each loop
childBlock = createBlockExpression(
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`,
node.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`
]),
context
)
} else {
// Normal element v-for. Directly use the child's codegenNode
// arguments, but replace createVNode() with createBlock()
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} else {
codegenNode.callee = helper(CREATE_BLOCK)
// Normal element v-for. Directly use the child's codegenNode
// arguments, but replace createVNode() with createBlock()
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} else {
codegenNode.callee = helper(CREATE_BLOCK)
}
childBlock = createBlockExpression(codegenNode, context)
}
childBlock = createBlockExpression(codegenNode, context)
}
renderExp.arguments.push(
createFunctionExpression(
createForLoopParams(parseResult),
childBlock,
true /* force newline */
renderExp.arguments.push(
createFunctionExpression(
createForLoopParams(parseResult),
childBlock,
true /* force newline */
)
)
)
}
}
})
}
)
// target-agnostic transform used for both Client and SSR
export function processForNode(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (
forNode: ForNode,
parseResult: ForParseResult
) => (() => void) | undefined
) {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
const forNode: ForNode = {
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
}
context.replaceNode(forNode)
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
const onExit = processCodegen && processCodegen(forNode, parseResult)
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
if (onExit) onExit()
}
}
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// This regex doesn't cover the case if key or index aliases have destructuring,
// but those do not make sense in the first place, so this works in practice.

View File

@@ -71,7 +71,8 @@ export const transformIf = createStructuralDirectiveTransform(
}
)
export const processIfBranches = (
// target-agnostic transform used for both Client and SSR
export function processIfBranches(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
@@ -79,8 +80,8 @@ export const processIfBranches = (
node: IfNode,
branch: IfBranchNode,
isRoot: boolean
) => (() => void) | void
) => {
) => (() => void) | undefined
) {
if (
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())