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.

364 lines
10 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,
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'
2020-02-07 06:45:34 +08:00
import { 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) {
context.pushStringPart(`<!--[[-->`)
}
if (asFragment) {
context.pushStringPart(`<!--[-->`)
}
2025-04-22 21:20:44 +08:00
const { children } = parent
for (let i = 0; i < children.length; i++) {
const child = children[i]
2025-04-22 21:20:44 +08:00
if (shouldProcessAsDynamic(parent, child)) {
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) {
context.pushStringPart(`<!--]]-->`)
}
}
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
/**
* Check if a node should be processed as dynamic.
* This is primarily used in Vapor mode hydration to wrap dynamic parts
* with markers (`<!--[[-->` and `<!--]]-->`).
*
* <element>
* <element/> // Static previous sibling
* <Comp/> // Dynamic node (current)
* <Comp/> // Dynamic next sibling
* <element/> // Static next sibling
* </element>
*/
function shouldProcessAsDynamic(
parent: { tag?: string; children: TemplateChildNode[] },
node: TemplateChildNode,
): boolean {
// 1. Must be a dynamic node type
if (isStaticChildNode(node)) return false
2025-04-22 21:20:44 +08:00
// 2. Must be inside a parent element
if (!parent.tag) return false
const children = parent.children.filter(
child => !(child.type === NodeTypes.TEXT && !child.content.trim()),
)
2025-04-22 21:20:44 +08:00
const len = children.length
const index = children.indexOf(node)
// 3. Check for a static previous sibling
let hasStaticPreviousSibling = false
if (index > 0) {
for (let i = index - 1; i >= 0; i--) {
if (isStaticChildNode(children[i])) {
2025-04-22 21:20:44 +08:00
hasStaticPreviousSibling = true
break
}
}
}
if (!hasStaticPreviousSibling) return false
// 4. Check for a static next sibling
let hasStaticNextSibling = false
if (index > -1 && index < len - 1) {
for (let i = index + 1; i < len; i++) {
if (isStaticChildNode(children[i])) {
2025-04-22 21:20:44 +08:00
hasStaticNextSibling = true
break
}
}
}
if (!hasStaticNextSibling) return false
// 5. Calculate the number and location of continuous dynamic nodes
let dynamicNodeCount = 1 // The current node is counted as one
let prevDynamicCount = 0
let nextDynamicCount = 0
// Count consecutive dynamic nodes forward
for (let i = index - 1; i >= 0; i--) {
if (!isStaticChildNode(children[i])) {
prevDynamicCount++
} else {
break
}
}
// Count consecutive dynamic nodes backwards
for (let i = index + 1; i < len; i++) {
if (!isStaticChildNode(children[i])) {
nextDynamicCount++
} else {
break
}
}
dynamicNodeCount = 1 + prevDynamicCount + nextDynamicCount
// For two consecutive dynamic nodes, mark both as dynamic
if (dynamicNodeCount === 2) {
return prevDynamicCount > 0 || nextDynamicCount > 0
2025-04-22 21:20:44 +08:00
}
// For three or more dynamic nodes, only mark the intermediate nodes 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
}