2020-02-02 13:05:27 +08:00
|
|
|
import {
|
|
|
|
type BlockStatement,
|
|
|
|
type CallExpression,
|
2024-08-08 23:05:21 +08:00
|
|
|
type CompilerError,
|
2020-02-02 13:05:27 +08:00
|
|
|
type CompilerOptions,
|
2020-02-03 10:47:10 +08:00
|
|
|
ElementTypes,
|
2020-02-04 04:51:41 +08:00
|
|
|
type IfStatement,
|
2024-08-08 23:05:21 +08:00
|
|
|
type JSChildNode,
|
2023-12-26 19:39:47 +08:00
|
|
|
NodeTypes,
|
2020-02-02 13:05:27 +08:00
|
|
|
type RootNode,
|
2020-05-02 05:04:36 +08:00
|
|
|
type TemplateChildNode,
|
2020-02-02 13:05:27 +08:00
|
|
|
type TemplateLiteral,
|
2020-02-03 11:28:54 +08:00
|
|
|
createBlockStatement,
|
2020-07-13 06:04:09 +08:00
|
|
|
createCallExpression,
|
|
|
|
createCompoundExpression,
|
2022-08-29 11:09:21 +08:00
|
|
|
createRoot,
|
2020-07-13 06:04:09 +08:00
|
|
|
createSimpleExpression,
|
2020-02-02 13:05:27 +08:00
|
|
|
createTemplateLiteral,
|
2020-07-13 06:04:09 +08:00
|
|
|
createTransformContext,
|
|
|
|
isText,
|
|
|
|
processExpression,
|
2020-02-02 13:05:27 +08:00
|
|
|
} from '@vue/compiler-dom'
|
2020-02-07 06:45:34 +08:00
|
|
|
import { escapeHtml, isString } from '@vue/shared'
|
2020-11-18 07:54:47 +08:00
|
|
|
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'
|
2020-05-02 05:04:36 +08:00
|
|
|
import { SSRErrorCodes, createSSRCompilerError } from './errors'
|
2020-02-02 13:05:27 +08:00
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
2024-08-08 23:05:21 +08:00
|
|
|
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
|
|
|
|
2020-11-18 07:54:47 +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) {
|
2022-08-29 11:09:21 +08:00
|
|
|
const cssContext = createTransformContext(createRoot([]), options)
|
2020-07-13 06:04:09 +08:00
|
|
|
const varsExp = processExpression(
|
|
|
|
createSimpleExpression(options.ssrCssVars, false),
|
2022-08-29 11:09:21 +08:00
|
|
|
cssContext,
|
2020-07-13 06:04:09 +08:00
|
|
|
)
|
|
|
|
context.body.push(
|
2020-11-18 07:54:47 +08:00
|
|
|
createCompoundExpression([`const _cssVars = { style: `, varsExp, `}`]),
|
2020-07-13 06:04:09 +08:00
|
|
|
)
|
2022-09-27 17:06:24 +08:00
|
|
|
Array.from(cssContext.helpers.keys()).forEach(helper => {
|
2022-11-14 09:44:02 +08:00
|
|
|
ast.helpers.add(helper)
|
2022-09-27 17:06:24 +08:00
|
|
|
})
|
2020-07-13 06:04:09 +08:00
|
|
|
}
|
|
|
|
|
2020-03-13 10:19:41 +08:00
|
|
|
const isFragment =
|
|
|
|
ast.children.length > 1 && ast.children.some(c => !isText(c))
|
2022-05-18 09:28:18 +08:00
|
|
|
processChildren(ast, context, isFragment)
|
2020-02-03 10:47:10 +08:00
|
|
|
ast.codegenNode = createBlockStatement(context.body)
|
2020-02-06 00:20:50 +08:00
|
|
|
|
|
|
|
// 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(
|
2022-11-14 09:44:02 +08:00
|
|
|
new Set([
|
|
|
|
...Array.from(ast.helpers).filter(h => h in ssrHelpers),
|
|
|
|
...context.helpers,
|
|
|
|
]),
|
2021-07-20 06:24:18 +08:00
|
|
|
)
|
|
|
|
|
2022-11-14 09:44:02 +08:00
|
|
|
ast.helpers = new Set(Array.from(ast.helpers).filter(h => !(h in ssrHelpers)))
|
2020-02-02 13:05:27 +08:00
|
|
|
}
|
|
|
|
|
2024-08-08 23:05:21 +08:00
|
|
|
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-02 13:05:27 +08:00
|
|
|
|
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,
|
2024-08-08 23:05:21 +08:00
|
|
|
): SSRTransformContext {
|
2020-02-02 13:05:27 +08:00
|
|
|
const body: BlockStatement['body'] = []
|
|
|
|
let currentString: TemplateLiteral | null = null
|
|
|
|
|
|
|
|
return {
|
2020-02-07 14:06:51 +08:00
|
|
|
root,
|
2020-02-03 11:28:54 +08:00
|
|
|
options,
|
2020-02-02 13:05:27 +08:00
|
|
|
body,
|
2020-02-04 06:47:06 +08:00
|
|
|
helpers,
|
2020-02-07 06:45:34 +08:00
|
|
|
withSlotScopeId,
|
2020-02-27 04:05:11 +08:00
|
|
|
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
|
|
|
|
},
|
2024-08-08 23:05:21 +08:00
|
|
|
pushStringPart(part) {
|
2020-02-02 13:05:27 +08:00
|
|
|
if (!currentString) {
|
2020-02-04 04:51:41 +08:00
|
|
|
const currentCall = createCallExpression(`_push`)
|
|
|
|
body.push(currentCall)
|
2020-02-02 13:05:27 +08:00
|
|
|
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
|
|
|
},
|
2024-08-08 23:05:21 +08:00
|
|
|
pushStatement(statement) {
|
2020-02-04 04:51:41 +08:00
|
|
|
// close current string
|
|
|
|
currentString = null
|
|
|
|
body.push(statement)
|
2020-02-02 13:05:27 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-05-18 09:28:18 +08:00
|
|
|
interface Container {
|
|
|
|
children: TemplateChildNode[]
|
|
|
|
}
|
|
|
|
|
2020-02-04 04:51:41 +08:00
|
|
|
export function processChildren(
|
2022-05-18 09:28:18 +08:00
|
|
|
parent: Container,
|
2020-03-13 10:19:41 +08:00
|
|
|
context: SSRTransformContext,
|
2020-11-28 01:22:14 +08:00
|
|
|
asFragment = false,
|
|
|
|
disableNestedFragments = false,
|
2024-09-20 16:46:45 +08:00
|
|
|
disableComment = false,
|
2025-04-22 21:20:44 +08:00
|
|
|
asDynamic = false,
|
2024-08-08 23:05:21 +08:00
|
|
|
): void {
|
2025-04-22 21:20:44 +08:00
|
|
|
if (asDynamic) {
|
|
|
|
context.pushStringPart(`<!--[[-->`)
|
|
|
|
}
|
2020-03-13 10:19:41 +08:00
|
|
|
if (asFragment) {
|
2020-03-13 23:55:04 +08:00
|
|
|
context.pushStringPart(`<!--[-->`)
|
2020-03-13 10:19:41 +08:00
|
|
|
}
|
2025-04-22 21:20:44 +08:00
|
|
|
|
2022-05-18 09:28:18 +08:00
|
|
|
const { children } = parent
|
2020-02-02 13:05:27 +08:00
|
|
|
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
|
|
|
|
}
|
2020-05-02 05:04:36 +08:00
|
|
|
switch (child.type) {
|
|
|
|
case NodeTypes.ELEMENT:
|
|
|
|
switch (child.tagType) {
|
|
|
|
case ElementTypes.ELEMENT:
|
|
|
|
ssrProcessElement(child, context)
|
|
|
|
break
|
|
|
|
case ElementTypes.COMPONENT:
|
2022-05-18 09:28:18 +08:00
|
|
|
ssrProcessComponent(child, context, parent)
|
2020-05-02 05:04:36 +08:00
|
|
|
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.
|
2024-09-20 16:46:45 +08:00
|
|
|
if (!disableComment) {
|
|
|
|
context.pushStringPart(`<!--${child.content}-->`)
|
|
|
|
}
|
2020-05-02 05:04:36 +08:00
|
|
|
break
|
|
|
|
case NodeTypes.INTERPOLATION:
|
|
|
|
context.pushStringPart(
|
|
|
|
createCallExpression(context.helper(SSR_INTERPOLATE), [
|
|
|
|
child.content,
|
|
|
|
]),
|
|
|
|
)
|
|
|
|
break
|
|
|
|
case NodeTypes.IF:
|
2024-09-20 16:46:45 +08:00
|
|
|
ssrProcessIf(child, context, disableNestedFragments, disableComment)
|
2020-05-02 05:04:36 +08:00
|
|
|
break
|
|
|
|
case NodeTypes.FOR:
|
2020-11-28 01:22:14 +08:00
|
|
|
ssrProcessFor(child, context, disableNestedFragments)
|
2020-05-02 05:04:36 +08:00
|
|
|
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
|
2020-02-02 13:05:27 +08:00
|
|
|
}
|
|
|
|
}
|
2020-03-13 10:19:41 +08:00
|
|
|
if (asFragment) {
|
2020-03-13 23:55:04 +08:00
|
|
|
context.pushStringPart(`<!--]-->`)
|
2020-03-13 10:19:41 +08:00
|
|
|
}
|
2025-04-22 21:20:44 +08:00
|
|
|
if (asDynamic) {
|
|
|
|
context.pushStringPart(`<!--]]-->`)
|
|
|
|
}
|
2020-02-02 13:05:27 +08:00
|
|
|
}
|
2020-02-07 06:45:34 +08:00
|
|
|
|
|
|
|
export function processChildrenAsStatement(
|
2022-05-18 09:28:18 +08:00
|
|
|
parent: Container,
|
2020-02-07 06:45:34 +08:00
|
|
|
parentContext: SSRTransformContext,
|
2020-03-13 10:19:41 +08:00
|
|
|
asFragment = false,
|
2024-08-08 23:05:21 +08:00
|
|
|
withSlotScopeId: boolean = parentContext.withSlotScopeId,
|
2020-02-07 06:45:34 +08:00
|
|
|
): BlockStatement {
|
|
|
|
const childContext = createChildContext(parentContext, withSlotScopeId)
|
2022-05-18 09:28:18 +08:00
|
|
|
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 isStaticElement = (c: TemplateChildNode): boolean =>
|
|
|
|
c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 (isStaticElement(node)) return false
|
|
|
|
// 2. Must be inside a parent element
|
|
|
|
if (!parent.tag) return false
|
|
|
|
|
|
|
|
const children = parent.children
|
|
|
|
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 (isStaticElement(children[i])) {
|
|
|
|
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 (isStaticElement(children[i])) {
|
|
|
|
hasStaticNextSibling = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!hasStaticNextSibling) return false
|
|
|
|
|
|
|
|
// 5. Check for a consecutive dynamic sibling (immediately before or after)
|
|
|
|
let hasConsecutiveDynamicNodes = false
|
|
|
|
if (index > 0 && !isStaticElement(children[index - 1])) {
|
|
|
|
hasConsecutiveDynamicNodes = true
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
!hasConsecutiveDynamicNodes &&
|
|
|
|
index < len - 1 &&
|
|
|
|
!isStaticElement(children[index + 1])
|
|
|
|
) {
|
|
|
|
hasConsecutiveDynamicNodes = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only process as dynamic if all conditions are met
|
|
|
|
return hasConsecutiveDynamicNodes
|
|
|
|
}
|