wip(ssr): ssr helper codegen
This commit is contained in:
parent
d1d81cf1f9
commit
b685805a26
@ -103,6 +103,7 @@ export interface RootNode extends Node {
|
||||
imports: ImportItem[]
|
||||
cached: number
|
||||
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
|
||||
ssrHelpers?: symbol[]
|
||||
}
|
||||
|
||||
export type ElementNode =
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
|
@ -34,13 +34,13 @@ describe('ssr: element', () => {
|
||||
|
||||
test('v-text', () => {
|
||||
expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
|
||||
`"\`<div>\${interpolate(_ctx.foo)}</div>\`"`
|
||||
`"\`<div>\${_interpolate(_ctx.foo)}</div>\`"`
|
||||
)
|
||||
})
|
||||
|
||||
test('<textarea> with dynamic value', () => {
|
||||
expect(getCompiledString(`<textarea :value="foo"/>`)).toMatchInlineSnapshot(
|
||||
`"\`<textarea>\${interpolate(_ctx.foo)}</textarea>\`"`
|
||||
`"\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`"`
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { compile } from '../src'
|
||||
import { getCompiledString } from './utils'
|
||||
|
||||
describe('ssr: text', () => {
|
||||
@ -20,18 +21,25 @@ describe('ssr: text', () => {
|
||||
})
|
||||
|
||||
test('interpolation', () => {
|
||||
expect(getCompiledString(`foo {{ bar }} baz`)).toMatchInlineSnapshot(
|
||||
`"\`foo \${interpolate(_ctx.bar)} baz\`"`
|
||||
)
|
||||
expect(compile(`foo {{ bar }} baz`).code).toMatchInlineSnapshot(`
|
||||
"const { _interpolate } = require(\\"@vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent) {
|
||||
_push(\`foo \${_interpolate(_ctx.bar)} baz\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('nested elements with interpolation', () => {
|
||||
expect(
|
||||
getCompiledString(
|
||||
`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`
|
||||
)
|
||||
).toMatchInlineSnapshot(
|
||||
`"\`<div><span>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
|
||||
)
|
||||
compile(`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`)
|
||||
.code
|
||||
).toMatchInlineSnapshot(`
|
||||
"const { _interpolate } = require(\\"@vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent) {
|
||||
_push(\`<div><span>\${_interpolate(_ctx.foo)} bar</span><span>baz \${_interpolate(_ctx.qux)}</span></div>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
@ -22,20 +22,23 @@ export function compile(
|
||||
template: string,
|
||||
options: SSRCompilerOptions = {}
|
||||
): CodegenResult {
|
||||
// apply DOM-specific parsing options
|
||||
options = {
|
||||
mode: 'cjs',
|
||||
...options,
|
||||
// apply DOM-specific parsing options
|
||||
...parserOptions,
|
||||
...options
|
||||
ssr: true,
|
||||
// always prefix since compiler-ssr doesn't have size concern
|
||||
prefixIdentifiers: true,
|
||||
// disalbe optimizations that are unnecessary for ssr
|
||||
cacheHandlers: false,
|
||||
hoistStatic: false
|
||||
}
|
||||
|
||||
const ast = baseParse(template, options)
|
||||
|
||||
transform(ast, {
|
||||
...options,
|
||||
prefixIdentifiers: true,
|
||||
// disalbe optimizations that are unnecessary for ssr
|
||||
cacheHandlers: false,
|
||||
hoistStatic: false,
|
||||
nodeTransforms: [
|
||||
ssrTransformIf,
|
||||
ssrTransformFor,
|
||||
@ -57,10 +60,5 @@ export function compile(
|
||||
// by replacing ast.codegenNode.
|
||||
ssrCodegenTransform(ast, options)
|
||||
|
||||
return generate(ast, {
|
||||
mode: 'cjs',
|
||||
...options,
|
||||
ssr: true,
|
||||
prefixIdentifiers: true
|
||||
})
|
||||
return generate(ast, options)
|
||||
}
|
||||
|
@ -1,7 +1,21 @@
|
||||
import { registerRuntimeHelpers } from '@vue/compiler-dom'
|
||||
|
||||
export const INTERPOLATE = Symbol(`interpolate`)
|
||||
export const SSR_INTERPOLATE = Symbol(`interpolate`)
|
||||
export const SSR_RENDER_COMPONENT = Symbol(`renderComponent`)
|
||||
export const SSR_RENDER_SLOT = Symbol(`renderSlot`)
|
||||
export const SSR_RENDER_CLASS = Symbol(`renderClass`)
|
||||
export const SSR_RENDER_STYLE = Symbol(`renderStyle`)
|
||||
export const SSR_RENDER_PROPS = Symbol(`renderProps`)
|
||||
export const SSR_RENDER_LIST = Symbol(`renderList`)
|
||||
|
||||
// Note: these are helpers imported from @vue/server-renderer
|
||||
// make sure the names match!
|
||||
registerRuntimeHelpers({
|
||||
[INTERPOLATE]: `interpolate`
|
||||
[SSR_INTERPOLATE]: `_interpolate`,
|
||||
[SSR_RENDER_COMPONENT]: `_renderComponent`,
|
||||
[SSR_RENDER_SLOT]: `_renderSlot`,
|
||||
[SSR_RENDER_CLASS]: `_renderClass`,
|
||||
[SSR_RENDER_STYLE]: `_renderStyle`,
|
||||
[SSR_RENDER_PROPS]: `_renderProps`,
|
||||
[SSR_RENDER_LIST]: `_renderList`
|
||||
})
|
||||
|
@ -14,8 +14,9 @@ import {
|
||||
CallExpression
|
||||
} from '@vue/compiler-dom'
|
||||
import { isString, escapeHtml, NO } from '@vue/shared'
|
||||
import { INTERPOLATE } from './runtimeHelpers'
|
||||
import { SSR_INTERPOLATE } from './runtimeHelpers'
|
||||
import { processIf } from './transforms/ssrVIf'
|
||||
import { processFor } from './transforms/ssrVFor'
|
||||
|
||||
// Because SSR codegen output is completely different from client-side output
|
||||
// (e.g. multiple elements can be concatenated into a single template literal
|
||||
@ -37,17 +38,26 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
|
||||
}
|
||||
|
||||
ast.codegenNode = createBlockStatement(context.body)
|
||||
ast.ssrHelpers = [...context.helpers]
|
||||
}
|
||||
|
||||
export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
|
||||
|
||||
export function createSSRTransformContext(options: CompilerOptions) {
|
||||
function createSSRTransformContext(
|
||||
options: CompilerOptions,
|
||||
helpers: Set<symbol> = new Set()
|
||||
) {
|
||||
const body: BlockStatement['body'] = []
|
||||
let currentString: TemplateLiteral | null = null
|
||||
|
||||
return {
|
||||
options,
|
||||
body,
|
||||
helpers,
|
||||
helper<T extends symbol>(name: T): T {
|
||||
helpers.add(name)
|
||||
return name
|
||||
},
|
||||
pushStringPart(part: TemplateLiteral['elements'][0]) {
|
||||
if (!currentString) {
|
||||
const currentCall = createCallExpression(`_push`)
|
||||
@ -71,6 +81,13 @@ export function createSSRTransformContext(options: CompilerOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
export function createChildContext(
|
||||
parent: SSRTransformContext
|
||||
): SSRTransformContext {
|
||||
// ensure child inherits parent helpers
|
||||
return createSSRTransformContext(parent.options, parent.helpers)
|
||||
}
|
||||
|
||||
export function processChildren(
|
||||
children: TemplateChildNode[],
|
||||
context: SSRTransformContext
|
||||
@ -100,11 +117,13 @@ export function processChildren(
|
||||
} else if (child.type === NodeTypes.TEXT) {
|
||||
context.pushStringPart(escapeHtml(child.content))
|
||||
} else if (child.type === NodeTypes.INTERPOLATION) {
|
||||
context.pushStringPart(createCallExpression(INTERPOLATE, [child.content]))
|
||||
context.pushStringPart(
|
||||
createCallExpression(context.helper(SSR_INTERPOLATE), [child.content])
|
||||
)
|
||||
} else if (child.type === NodeTypes.IF) {
|
||||
processIf(child, context)
|
||||
} else if (child.type === NodeTypes.FOR) {
|
||||
// TODO
|
||||
processFor(child, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,16 @@
|
||||
import { NodeTransform } from '@vue/compiler-dom'
|
||||
import {
|
||||
createStructuralDirectiveTransform,
|
||||
ForNode,
|
||||
processForNode
|
||||
} from '@vue/compiler-dom'
|
||||
import { SSRTransformContext } from '../ssrCodegenTransform'
|
||||
|
||||
export const ssrTransformFor: NodeTransform = () => {}
|
||||
// Plugin for the first transform pass, which simply constructs the AST node
|
||||
export const ssrTransformFor = createStructuralDirectiveTransform(
|
||||
'for',
|
||||
processForNode
|
||||
)
|
||||
|
||||
// This is called during the 2nd transform pass to construct the SSR-sepcific
|
||||
// codegen nodes.
|
||||
export function processFor(node: ForNode, context: SSRTransformContext) {}
|
||||
|
@ -11,12 +11,11 @@ import {
|
||||
} from '@vue/compiler-dom'
|
||||
import {
|
||||
SSRTransformContext,
|
||||
createSSRTransformContext,
|
||||
createChildContext,
|
||||
processChildren
|
||||
} from '../ssrCodegenTransform'
|
||||
|
||||
// This is the plugin for the first transform pass, which simply constructs the
|
||||
// if node and its branches.
|
||||
// Plugin for the first transform pass, which simply constructs the AST node
|
||||
export const ssrTransformIf = createStructuralDirectiveTransform(
|
||||
/^(if|else|else-if)$/,
|
||||
processIfBranches
|
||||
@ -64,7 +63,7 @@ function processIfBranch(
|
||||
// TODO optimize away nested fragments when the only child is a ForNode
|
||||
const needFragmentWrapper =
|
||||
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
|
||||
const childContext = createSSRTransformContext(context.options)
|
||||
const childContext = createChildContext(context)
|
||||
if (needFragmentWrapper) {
|
||||
childContext.pushStringPart(`<!---->`)
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { escapeHtml, interpolate } from '../src'
|
||||
import { _interpolate } from '../src'
|
||||
import { escapeHtml } from '@vue/shared'
|
||||
|
||||
test('ssr: interpolate', () => {
|
||||
expect(interpolate(0)).toBe(`0`)
|
||||
expect(interpolate(`foo`)).toBe(`foo`)
|
||||
expect(interpolate(`<div>`)).toBe(`<div>`)
|
||||
expect(_interpolate(0)).toBe(`0`)
|
||||
expect(_interpolate(`foo`)).toBe(`foo`)
|
||||
expect(_interpolate(`<div>`)).toBe(`<div>`)
|
||||
// should escape interpolated values
|
||||
expect(interpolate([1, 2, 3])).toBe(
|
||||
expect(_interpolate([1, 2, 3])).toBe(
|
||||
escapeHtml(JSON.stringify([1, 2, 3], null, 2))
|
||||
)
|
||||
expect(
|
||||
interpolate({
|
||||
_interpolate({
|
||||
foo: 1,
|
||||
bar: `<div>`
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { renderProps, renderClass, renderStyle } from '../src'
|
||||
import { renderProps, renderClass, renderStyle } from '../src/renderProps'
|
||||
|
||||
describe('ssr: renderProps', () => {
|
||||
test('ignore reserved props', () => {
|
||||
|
@ -6,7 +6,12 @@ import {
|
||||
resolveComponent,
|
||||
ComponentOptions
|
||||
} from 'vue'
|
||||
import { renderToString, renderComponent, renderSlot, escapeHtml } from '../src'
|
||||
import { escapeHtml } from '@vue/shared'
|
||||
import {
|
||||
renderToString,
|
||||
renderComponent,
|
||||
renderSlot
|
||||
} from '../src/renderToString'
|
||||
|
||||
describe('ssr: renderToString', () => {
|
||||
test('should apply app context', async () => {
|
||||
|
@ -2,15 +2,19 @@
|
||||
export { renderToString } from './renderToString'
|
||||
|
||||
// internal
|
||||
export { renderComponent, renderSlot } from './renderToString'
|
||||
export { renderClass, renderStyle, renderProps } from './renderProps'
|
||||
export {
|
||||
renderComponent as _renderComponent,
|
||||
renderSlot as _renderSlot
|
||||
} from './renderToString'
|
||||
export {
|
||||
renderClass as _renderClass,
|
||||
renderStyle as _renderStyle,
|
||||
renderProps as _renderProps
|
||||
} from './renderProps'
|
||||
|
||||
// utils
|
||||
import { escapeHtml as _escapeHtml, toDisplayString } from '@vue/shared'
|
||||
import { escapeHtml, toDisplayString } from '@vue/shared'
|
||||
|
||||
// cast type to avoid dts dependency on @vue/shared (which is inlined)
|
||||
export const escapeHtml = _escapeHtml as (raw: string) => string
|
||||
|
||||
export function interpolate(value: unknown): string {
|
||||
export function _interpolate(value: unknown): string {
|
||||
return escapeHtml(toDisplayString(value))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user