vue3-core/packages/compiler-ssr/src/ssrCodegenTransform.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

425 lines
12 KiB
TypeScript
Raw Normal View History

import {
type BlockStatement,
type CallExpression,
type CompilerError,
type CompilerOptions,
ElementTypes,
2020-02-04 04:51:41 +08:00
type IfStatement,
type JSChildNode,
NodeTypes,
2025-04-23 21:45:10 +08:00
type PlainElementNode,
type RootNode,
type TemplateChildNode,
type TemplateLiteral,
createBlockStatement,
2020-07-13 06:04:09 +08:00
createCallExpression,
createCompoundExpression,
createRoot,
2020-07-13 06:04:09 +08:00
createSimpleExpression,
createTemplateLiteral,
2020-07-13 06:04:09 +08:00
createTransformContext,
isText,
processExpression,
} from '@vue/compiler-dom'
2025-04-25 15:08:19 +08:00
import {
DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL,
escapeHtml,
isString,
} from '@vue/shared'
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
2020-02-06 10:04:40 +08:00
import { ssrProcessIf } from './transforms/ssrVIf'
import { ssrProcessFor } from './transforms/ssrVFor'
import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
2020-02-06 12:07:23 +08:00
import { ssrProcessComponent } from './transforms/ssrTransformComponent'
2020-02-07 06:45:34 +08:00
import { ssrProcessElement } from './transforms/ssrTransformElement'
import { SSRErrorCodes, createSSRCompilerError } from './errors'
// Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal
// instead of each getting a corresponding call), we need to apply an extra
// transform pass to convert the template AST into a fresh JS AST before
// passing it to codegen.
export function ssrCodegenTransform(
ast: RootNode,
options: CompilerOptions,
): void {
2020-02-07 14:06:51 +08:00
const context = createSSRTransformContext(ast, options)
2020-07-13 06:04:09 +08:00
// inject SFC <style> CSS variables
2020-07-13 06:04:09 +08:00
// we do this instead of inlining the expression to ensure the vars are
// only resolved once per render
if (options.ssrCssVars) {
const cssContext = createTransformContext(createRoot([]), options)
2020-07-13 06:04:09 +08:00
const varsExp = processExpression(
createSimpleExpression(options.ssrCssVars, false),
cssContext,
2020-07-13 06:04:09 +08:00
)
context.body.push(
createCompoundExpression([`const _cssVars = { style: `, varsExp, `}`]),
2020-07-13 06:04:09 +08:00
)
Array.from(cssContext.helpers.keys()).forEach(helper => {
ast.helpers.add(helper)
})
2020-07-13 06:04:09 +08:00
}
const isFragment =
ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast, context, isFragment)
ast.codegenNode = createBlockStatement(context.body)
// Finalize helpers.
// We need to separate helpers imported from 'vue' vs. '@vue/server-renderer'
2021-07-20 06:24:18 +08:00
ast.ssrHelpers = Array.from(
new Set([
...Array.from(ast.helpers).filter(h => h in ssrHelpers),
...context.helpers,
]),
2021-07-20 06:24:18 +08:00
)
ast.helpers = new Set(Array.from(ast.helpers).filter(h => !(h in ssrHelpers)))
}
export interface SSRTransformContext {
root: RootNode
options: CompilerOptions
body: (JSChildNode | IfStatement)[]
helpers: Set<symbol>
withSlotScopeId: boolean
onError: (error: CompilerError) => void
helper<T extends symbol>(name: T): T
pushStringPart(part: TemplateLiteral['elements'][0]): void
pushStatement(statement: IfStatement | CallExpression): void
}
2020-02-04 06:47:06 +08:00
function createSSRTransformContext(
2020-02-07 14:06:51 +08:00
root: RootNode,
2020-02-04 06:47:06 +08:00
options: CompilerOptions,
2020-02-07 06:45:34 +08:00
helpers: Set<symbol> = new Set(),
withSlotScopeId = false,
): SSRTransformContext {
const body: BlockStatement['body'] = []
let currentString: TemplateLiteral | null = null
return {
2020-02-07 14:06:51 +08:00
root,
options,
body,
2020-02-04 06:47:06 +08:00
helpers,
2020-02-07 06:45:34 +08:00
withSlotScopeId,
onError:
options.onError ||
(e => {
throw e
}),
2020-02-04 06:47:06 +08:00
helper<T extends symbol>(name: T): T {
helpers.add(name)
return name
},
pushStringPart(part) {
if (!currentString) {
2020-02-04 04:51:41 +08:00
const currentCall = createCallExpression(`_push`)
body.push(currentCall)
currentString = createTemplateLiteral([])
currentCall.arguments.push(currentString)
}
const bufferedElements = currentString.elements
const lastItem = bufferedElements[bufferedElements.length - 1]
if (isString(part) && isString(lastItem)) {
bufferedElements[bufferedElements.length - 1] += part
} else {
bufferedElements.push(part)
}
2020-02-04 04:51:41 +08:00
},
pushStatement(statement) {
2020-02-04 04:51:41 +08:00
// close current string
currentString = null
body.push(statement)
},
}
}
2020-02-07 06:45:34 +08:00
function createChildContext(
parent: SSRTransformContext,
withSlotScopeId = parent.withSlotScopeId,
2020-02-04 06:47:06 +08:00
): SSRTransformContext {
// ensure child inherits parent helpers
2020-02-07 06:45:34 +08:00
return createSSRTransformContext(
2020-02-07 14:06:51 +08:00
parent.root,
2020-02-07 06:45:34 +08:00
parent.options,
parent.helpers,
withSlotScopeId,
)
2020-02-04 06:47:06 +08:00
}
interface Container {
children: TemplateChildNode[]
}
2020-02-04 04:51:41 +08:00
export function processChildren(
parent: Container,
context: SSRTransformContext,
asFragment = false,
disableNestedFragments = false,
disableComment = false,
2025-04-22 21:20:44 +08:00
asDynamic = false,
): void {
2025-04-22 21:20:44 +08:00
if (asDynamic) {
2025-04-25 15:08:19 +08:00
context.pushStringPart(`<!--${DYNAMIC_START_ANCHOR_LABEL}-->`)
2025-04-22 21:20:44 +08:00
}
if (asFragment) {
context.pushStringPart(`<!--[-->`)
}
2025-04-22 21:20:44 +08:00
2025-04-23 21:45:10 +08:00
const { children, type, tagType } = parent as PlainElementNode
const inElement =
type === NodeTypes.ELEMENT && tagType === ElementTypes.ELEMENT
if (inElement) processChildrenDynamicInfo(children)
for (let i = 0; i < children.length; i++) {
const child = children[i]
2025-04-23 21:45:10 +08:00
if (inElement && shouldProcessChildAsDynamic(parent, child)) {
2025-04-22 21:20:44 +08:00
processChildren(
{ children: [child] },
context,
asFragment,
disableNestedFragments,
disableComment,
true,
)
continue
}
switch (child.type) {
case NodeTypes.ELEMENT:
switch (child.tagType) {
case ElementTypes.ELEMENT:
ssrProcessElement(child, context)
break
case ElementTypes.COMPONENT:
ssrProcessComponent(child, context, parent)
break
case ElementTypes.SLOT:
ssrProcessSlotOutlet(child, context)
break
case ElementTypes.TEMPLATE:
// TODO
break
default:
context.onError(
createSSRCompilerError(
SSRErrorCodes.X_SSR_INVALID_AST_NODE,
(child as any).loc,
),
)
// make sure we exhaust all possible types
const exhaustiveCheck: never = child
return exhaustiveCheck
}
break
case NodeTypes.TEXT:
context.pushStringPart(escapeHtml(child.content))
break
case NodeTypes.COMMENT:
// no need to escape comment here because the AST can only
// contain valid comments.
if (!disableComment) {
context.pushStringPart(`<!--${child.content}-->`)
}
break
case NodeTypes.INTERPOLATION:
context.pushStringPart(
createCallExpression(context.helper(SSR_INTERPOLATE), [
child.content,
]),
)
break
case NodeTypes.IF:
ssrProcessIf(child, context, disableNestedFragments, disableComment)
break
case NodeTypes.FOR:
ssrProcessFor(child, context, disableNestedFragments)
break
case NodeTypes.IF_BRANCH:
// no-op - handled by ssrProcessIf
break
case NodeTypes.TEXT_CALL:
case NodeTypes.COMPOUND_EXPRESSION:
// no-op - these two types can never appear as template child node since
// `transformText` is not used during SSR compile.
break
default:
context.onError(
createSSRCompilerError(
SSRErrorCodes.X_SSR_INVALID_AST_NODE,
(child as any).loc,
),
)
// make sure we exhaust all possible types
const exhaustiveCheck: never = child
return exhaustiveCheck
}
}
if (asFragment) {
context.pushStringPart(`<!--]-->`)
}
2025-04-22 21:20:44 +08:00
if (asDynamic) {
2025-04-25 15:08:19 +08:00
context.pushStringPart(`<!--${DYNAMIC_END_ANCHOR_LABEL}-->`)
2025-04-22 21:20:44 +08:00
}
}
2020-02-07 06:45:34 +08:00
export function processChildrenAsStatement(
parent: Container,
2020-02-07 06:45:34 +08:00
parentContext: SSRTransformContext,
asFragment = false,
withSlotScopeId: boolean = parentContext.withSlotScopeId,
2020-02-07 06:45:34 +08:00
): BlockStatement {
const childContext = createChildContext(parentContext, withSlotScopeId)
processChildren(parent, childContext, asFragment)
2020-02-07 06:45:34 +08:00
return createBlockStatement(childContext.body)
}
2025-04-22 21:20:44 +08:00
const isStaticChildNode = (c: TemplateChildNode): boolean =>
(c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT) ||
c.type === NodeTypes.TEXT ||
c.type === NodeTypes.COMMENT
2025-04-22 21:20:44 +08:00
2025-04-23 21:45:10 +08:00
interface DynamicInfo {
hasStaticPrevious: boolean
hasStaticNext: boolean
prevDynamicCount: number
nextDynamicCount: number
}
2025-04-22 21:20:44 +08:00
2025-04-23 21:45:10 +08:00
function processChildrenDynamicInfo(
children: (TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo })[],
): void {
const filteredChildren = children.filter(
child => !(child.type === NodeTypes.TEXT && !child.content.trim()),
)
2025-04-22 21:20:44 +08:00
2025-04-23 21:45:10 +08:00
for (let i = 0; i < filteredChildren.length; i++) {
const child = filteredChildren[i]
2025-04-24 16:58:18 +08:00
if (
isStaticChildNode(child) ||
2025-04-26 10:34:46 +08:00
// fragment has it's own anchor, which can be used to distinguish the boundary
isFragmentChild(child)
2025-04-24 16:58:18 +08:00
) {
continue
}
2025-04-23 21:45:10 +08:00
child._ssrDynamicInfo = {
hasStaticPrevious: false,
hasStaticNext: false,
prevDynamicCount: 0,
nextDynamicCount: 0,
}
const info = child._ssrDynamicInfo
// Calculate the previous static and dynamic node counts
let foundStaticPrev = false
let dynamicCountPrev = 0
for (let j = i - 1; j >= 0; j--) {
const prevChild = filteredChildren[j]
if (isStaticChildNode(prevChild)) {
foundStaticPrev = true
2025-04-22 21:20:44 +08:00
break
}
2025-04-23 21:45:10 +08:00
// if the previous child has dynamic info, use it
else if (prevChild._ssrDynamicInfo) {
foundStaticPrev = prevChild._ssrDynamicInfo.hasStaticPrevious
dynamicCountPrev = prevChild._ssrDynamicInfo.prevDynamicCount + 1
break
}
dynamicCountPrev++
2025-04-22 21:20:44 +08:00
}
2025-04-23 21:45:10 +08:00
info.hasStaticPrevious = foundStaticPrev
info.prevDynamicCount = dynamicCountPrev
2025-04-22 21:20:44 +08:00
2025-04-23 21:45:10 +08:00
// Calculate the number of static and dynamic nodes afterwards
let foundStaticNext = false
let dynamicCountNext = 0
for (let j = i + 1; j < filteredChildren.length; j++) {
const nextChild = filteredChildren[j]
if (isStaticChildNode(nextChild)) {
foundStaticNext = true
2025-04-22 21:20:44 +08:00
break
}
2025-04-23 21:45:10 +08:00
// if the next child has dynamic info, use it
else if (nextChild._ssrDynamicInfo) {
foundStaticNext = nextChild._ssrDynamicInfo.hasStaticNext
dynamicCountNext = nextChild._ssrDynamicInfo.nextDynamicCount + 1
break
}
dynamicCountNext++
2025-04-22 21:20:44 +08:00
}
2025-04-23 21:45:10 +08:00
info.hasStaticNext = foundStaticNext
info.nextDynamicCount = dynamicCountNext
2025-04-22 21:20:44 +08:00
}
2025-04-23 21:45:10 +08:00
}
2025-04-22 21:20:44 +08:00
2025-04-23 21:45:10 +08:00
/**
2025-04-26 10:34:46 +08:00
* Check if a node should be processed as dynamic child.
2025-04-23 21:45:10 +08:00
* This is primarily used in Vapor mode hydration to wrap dynamic parts
* with markers (`<!--[[-->` and `<!--]]-->`).
2025-04-26 10:34:46 +08:00
* The purpose is to distinguish the boundaries of nodes during vapor hydration
2025-04-23 21:45:10 +08:00
*
* 1. two consecutive dynamic nodes should only wrap the second one
* <element>
* <element/> // Static node
* <Comp/> // Dynamic node -> should NOT be wrapped
* <Comp/> // Dynamic node -> should be wrapped
* <element/> // Static node
* </element>
*
* 2. three or more consecutive dynamic nodes should only wrap the
* middle nodes, leaving the first and last static.
* <element>
* <element/> // Static node
* <Comp/> // Dynamic node -> should NOT be wrapped
* <Comp/> // Dynamic node -> should be wrapped
* <Comp/> // Dynamic node -> should be wrapped
* <Comp/> // Dynamic node -> should NOT be wrapped
* <element/> // Static node
2025-04-27 12:01:42 +08:00
* </element>
2025-04-23 21:45:10 +08:00
*/
function shouldProcessChildAsDynamic(
parent: { tag?: string; children: TemplateChildNode[] },
node: TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo },
): boolean {
// must be inside a parent element
if (!parent.tag) return false
2025-04-23 21:45:10 +08:00
// must has dynamic info
const { _ssrDynamicInfo: info } = node
if (!info) return false
2025-04-23 21:45:10 +08:00
const {
hasStaticPrevious,
hasStaticNext,
prevDynamicCount,
nextDynamicCount,
} = info
// must have static nodes on both sides
if (!hasStaticPrevious || !hasStaticNext) return false
2025-04-23 21:45:10 +08:00
const dynamicNodeCount = 1 + prevDynamicCount + nextDynamicCount
2025-04-23 21:45:10 +08:00
// For two consecutive dynamic nodes, mark the second one as dynamic
if (dynamicNodeCount === 2) {
2025-04-23 21:45:10 +08:00
return prevDynamicCount > 0
2025-04-22 21:20:44 +08:00
}
2025-04-23 21:45:10 +08:00
// For three or more dynamic nodes, mark the intermediate node as dynamic
else if (dynamicNodeCount >= 3) {
return prevDynamicCount > 0 && nextDynamicCount > 0
2025-04-22 21:20:44 +08:00
}
return false
2025-04-22 21:20:44 +08:00
}
2025-04-26 10:34:46 +08:00
function isFragmentChild(child: TemplateChildNode): boolean {
const { type } = child
return type === NodeTypes.IF || type === NodeTypes.FOR
}