diff --git a/packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts b/packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts
index d29be57cf..232b12e25 100644
--- a/packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts
@@ -18,7 +18,9 @@ describe('insertion anchors', () => {
expect(
getCompiledString(
`
-
+
+
+
`,
{ vapor: true },
),
@@ -70,7 +72,11 @@ describe('insertion anchors', () => {
expect(
getCompiledString(
`
-
+
+
+
+
+
`,
{ vapor: true },
),
@@ -137,13 +143,13 @@ describe('insertion anchors', () => {
expect(
getCompiledString(
`
-
-
-
-
-
-
- `,
+
+
+
+
+
+
+ `,
{ vapor: true },
),
).toMatchInlineSnapshot(`
@@ -247,7 +253,7 @@ describe('insertion anchors', () => {
expect(
getCompiledString(
`
-
+
@@ -363,7 +369,9 @@ describe('insertion anchors', () => {
expect(
getCompiledString(
`
-
+
+
+
`,
{ vapor: true },
),
@@ -444,7 +452,13 @@ describe('insertion anchors', () => {
test('mixed anchors in ssr slot vnode fallback', () => {
expect(
getCompiledString(
- ``,
+ `
+
+
+
+
+
+ `,
{
vapor: true,
},
@@ -454,24 +468,28 @@ describe('insertion anchors', () => {
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
+ _push(\`\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
- _push(\`\`)
+ _push(\`\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
- _push(\`\`)
+ _push(\`\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
+ _push(\`
\`)
} else {
return [
- _createCommentVNode("[p"),
- _createVNode(_component_Comp),
- _createCommentVNode("p]"),
- _createVNode("span"),
- _createCommentVNode("[i"),
- _createVNode(_component_Comp),
- _createCommentVNode("i]"),
- _createVNode("span"),
- _createCommentVNode("[a"),
- _createVNode(_component_Comp),
- _createCommentVNode("a]")
+ _createVNode("div", null, [
+ _createCommentVNode("[p"),
+ _createVNode(_component_Comp),
+ _createCommentVNode("p]"),
+ _createVNode("span"),
+ _createCommentVNode("[i"),
+ _createVNode(_component_Comp),
+ _createCommentVNode("i]"),
+ _createVNode("span"),
+ _createCommentVNode("[a"),
+ _createVNode(_component_Comp),
+ _createCommentVNode("a]")
+ ])
]
}
}),
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
index db63235ca..5137da0e2 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
@@ -15,6 +15,7 @@ import {
Namespaces,
type NodeTransform,
NodeTypes,
+ type PlainElementNode,
RESOLVE_DYNAMIC_COMPONENT,
type ReturnStatement,
type RootNode,
@@ -139,7 +140,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
if (clonedNode.children.length) {
buildSlots(clonedNode, context, (props, vFor, children) => {
vnodeBranches.push(
- createVNodeSlotBranch(props, vFor, children, context),
+ createVNodeSlotBranch(props, vFor, children, context, clonedNode),
)
return createFunctionExpression(undefined)
})
@@ -302,6 +303,7 @@ function createVNodeSlotBranch(
vFor: DirectiveNode | undefined,
children: TemplateChildNode[],
parentContext: TransformContext,
+ parent: TemplateChildNode,
): ReturnStatement {
// apply a sub-transform using vnode-based transforms.
const rawOptions = rawOptionsMap.get(parentContext.root)!
@@ -338,7 +340,7 @@ function createVNodeSlotBranch(
}
if (parentContext.vapor) {
- children = injectVaporInsertionAnchors(children)
+ children = injectVaporInsertionAnchors(children, parent)
}
const wrapperNode: TemplateNode = {
@@ -395,8 +397,12 @@ function subTransform(
function injectVaporInsertionAnchors(
children: TemplateChildNode[],
+ parent: TemplateChildNode,
): TemplateChildNode[] {
- processBlockNodeAnchor(children)
+ if (isElementWithChildren(parent)) {
+ processBlockNodeAnchor(children)
+ }
+
const newChildren: TemplateChildNode[] = new Array(children.length * 3)
let newIndex = 0
@@ -439,12 +445,10 @@ function injectVaporInsertionAnchors(
// copy branch nodes
for (let j = i; j <= lastBranchIndex; j++) {
- const node = children[j]
+ const node = children[j] as PlainElementNode
newChildren[newIndex++] = node
- if (isElementWithChildren(node)) {
- node.children = injectVaporInsertionAnchors(node.children)
- }
+ node.children = injectVaporInsertionAnchors(node.children, node)
}
// inject anchor after branch nodes
@@ -464,9 +468,7 @@ function injectVaporInsertionAnchors(
newChildren[newIndex++] = child
if (anchor) newChildren[newIndex++] = createAnchorComment(`${anchor}]`)
- if (isElementWithChildren(child)) {
- child.children = injectVaporInsertionAnchors(child.children)
- }
+ child.children = injectVaporInsertionAnchors(child.children, child)
}
newChildren.length = newIndex
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
index ecfd1a897..2173894b2 100644
--- a/packages/runtime-vapor/__tests__/hydration.spec.ts
+++ b/packages/runtime-vapor/__tests__/hydration.spec.ts
@@ -3027,6 +3027,43 @@ describe('Vapor Mode hydration', () => {
)
})
+ test('forwarded slot with fallback', async () => {
+ const data = reactive({
+ foo: 'foo',
+ })
+ const { container } = await testHydration(
+ `
+
+ `,
+ {
+ Parent: ``,
+ Child: `{{data.foo}}
`,
+ },
+ data,
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+
+ foo
+
+
"
+ `,
+ )
+
+ data.foo = 'foo1'
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+
+ foo1
+
+
"
+ `,
+ )
+ })
+
test('forwarded slot with empty content', async () => {
const data = reactive({
foo: 'foo',
@@ -3071,8 +3108,8 @@ describe('Vapor Mode hydration', () => {
`
""
`,
)
@@ -3083,8 +3120,8 @@ describe('Vapor Mode hydration', () => {
`
""
`,
)
diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts
index 9b37419e6..53fc4340d 100644
--- a/packages/runtime-vapor/src/fragment.ts
+++ b/packages/runtime-vapor/src/fragment.ts
@@ -145,6 +145,9 @@ export class DynamicFragment extends VaporFragment {
}
hydrate = (label: string, isEmpty: boolean = false): void => {
+ // avoid repeated hydration during rendering fallback
+ if (this.anchor) return
+
const createAnchor = () => {
const { parentNode, nextSibling } = findLastChild(this.nodes)!
parentNode!.insertBefore(
@@ -156,14 +159,8 @@ export class DynamicFragment extends VaporFragment {
// manually create anchors for:
// 1. else-if branch
- // 2. empty forwarded slot
// (not present in SSR output)
- if (
- label === ELSE_IF_ANCHOR_LABEL ||
- (this.nodes instanceof DynamicFragment &&
- this.nodes.forwarded &&
- !isValidBlock(this.nodes))
- ) {
+ if (label === ELSE_IF_ANCHOR_LABEL) {
createAnchor()
} else {
// for `v-if="false"`, the node will be an empty comment, use it as the anchor.
diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
index 19aa4ce63..0e2ee5d6c 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
@@ -5,7 +5,7 @@ import {
type SSRBufferItem,
renderVNodeChildren,
} from '../render'
-import { isArray } from '@vue/shared'
+import { isArray, isString } from '@vue/shared'
const { ensureValidVNode } = ssrUtils
@@ -83,7 +83,20 @@ export function ssrRenderSlotInner(
isEmptySlot = false
} else {
for (let i = 0; i < slotBuffer.length; i++) {
- if (!isComment(slotBuffer[i])) {
+ const buffer = slotBuffer[i]
+
+ // preserve empty slot anchor in vapor components
+ // DynamicFragment requires this anchor
+ if (
+ parentComponent.type.__vapor &&
+ isString(buffer) &&
+ buffer === ''
+ ) {
+ push(buffer)
+ continue
+ }
+
+ if (!isComment(buffer)) {
isEmptySlot = false
break
}