Skip to content

Commit 9309b04

Browse files
committed
fix(ssr): fix hydration error for slot outlet inside transition
fix #3989
1 parent da49c86 commit 9309b04

File tree

6 files changed

+67
-14
lines changed

6 files changed

+67
-14
lines changed

packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { compile } from '../src'
2+
import { ssrHelpers, SSR_RENDER_SLOT_INNER } from '../src/runtimeHelpers'
23

34
describe('ssr: <slot>', () => {
45
test('basic', () => {
@@ -114,4 +115,16 @@ describe('ssr: <slot>', () => {
114115
}"
115116
`)
116117
})
118+
119+
test('inside transition', () => {
120+
const { code } = compile(`<transition><slot/></transition>`)
121+
expect(code).toMatch(ssrHelpers[SSR_RENDER_SLOT_INNER])
122+
expect(code).toMatchInlineSnapshot(`
123+
"const { ssrRenderSlotInner: _ssrRenderSlotInner } = require(\\"vue/server-renderer\\")
124+
125+
return function ssrRender(_ctx, _push, _parent, _attrs) {
126+
_ssrRenderSlotInner(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
127+
}"
128+
`)
129+
})
117130
})

packages/compiler-ssr/src/runtimeHelpers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`)
44
export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`)
55
export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`)
66
export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`)
7+
export const SSR_RENDER_SLOT_INNER = Symbol(`ssrRenderSlotInner`)
78
export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`)
89
export const SSR_RENDER_STYLE = Symbol(`ssrRenderStyle`)
910
export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
@@ -24,6 +25,7 @@ export const ssrHelpers = {
2425
[SSR_RENDER_VNODE]: `ssrRenderVNode`,
2526
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
2627
[SSR_RENDER_SLOT]: `ssrRenderSlot`,
28+
[SSR_RENDER_SLOT_INNER]: `ssrRenderSlotInner`,
2729
[SSR_RENDER_CLASS]: `ssrRenderClass`,
2830
[SSR_RENDER_STYLE]: `ssrRenderStyle`,
2931
[SSR_RENDER_ATTRS]: `ssrRenderAttrs`,

packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts

+24-6
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import {
44
processSlotOutlet,
55
createCallExpression,
66
SlotOutletNode,
7-
createFunctionExpression
7+
createFunctionExpression,
8+
NodeTypes,
9+
ElementTypes,
10+
resolveComponentType,
11+
TRANSITION
812
} from '@vue/compiler-dom'
9-
import { SSR_RENDER_SLOT } from '../runtimeHelpers'
13+
import { SSR_RENDER_SLOT, SSR_RENDER_SLOT_INNER } from '../runtimeHelpers'
1014
import {
1115
SSRTransformContext,
1216
processChildrenAsStatement
@@ -31,10 +35,24 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
3135
args.push(`"${context.scopeId}-s"`)
3236
}
3337

34-
node.ssrCodegenNode = createCallExpression(
35-
context.helper(SSR_RENDER_SLOT),
36-
args
37-
)
38+
let method = SSR_RENDER_SLOT
39+
40+
// #3989
41+
// check if this is a single slot inside a transition wrapper - since
42+
// transition will unwrap the slot fragment into a single vnode at runtime,
43+
// we need to avoid rendering the slot as a fragment.
44+
const parent = context.parent
45+
if (
46+
parent &&
47+
parent.type === NodeTypes.ELEMENT &&
48+
parent.tagType === ElementTypes.COMPONENT &&
49+
resolveComponentType(parent, context, true) === TRANSITION &&
50+
parent.children.filter(c => c.type === NodeTypes.ELEMENT).length === 1
51+
) {
52+
method = SSR_RENDER_SLOT_INNER
53+
}
54+
55+
node.ssrCodegenNode = createCallExpression(context.helper(method), args)
3856
}
3957
}
4058

packages/runtime-core/src/hydration.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function createHydrationFunctions(
113113
nextNode = onMismatch()
114114
} else {
115115
if ((node as Text).data !== vnode.children) {
116-
hasMismatch = true
116+
hasMismatch = true; debugger
117117
__DEV__ &&
118118
warn(
119119
`Hydration text mismatch:` +
@@ -351,7 +351,7 @@ export function createHydrationFunctions(
351351
)
352352
let hasWarned = false
353353
while (next) {
354-
hasMismatch = true
354+
hasMismatch = true; debugger
355355
if (__DEV__ && !hasWarned) {
356356
warn(
357357
`Hydration children mismatch in <${vnode.type as string}>: ` +
@@ -366,7 +366,7 @@ export function createHydrationFunctions(
366366
}
367367
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
368368
if (el.textContent !== vnode.children) {
369-
hasMismatch = true
369+
hasMismatch = true; debugger
370370
__DEV__ &&
371371
warn(
372372
`Hydration text content mismatch in <${
@@ -411,7 +411,7 @@ export function createHydrationFunctions(
411411
} else if (vnode.type === Text && !vnode.children) {
412412
continue
413413
} else {
414-
hasMismatch = true
414+
hasMismatch = true; debugger
415415
if (__DEV__ && !hasWarned) {
416416
warn(
417417
`Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
@@ -465,7 +465,7 @@ export function createHydrationFunctions(
465465
} else {
466466
// fragment didn't hydrate successfully, since we didn't get a end anchor
467467
// back. This should have led to node/children mismatch warnings.
468-
hasMismatch = true
468+
hasMismatch = true; debugger
469469
// since the anchor is missing, we need to create one and insert it
470470
insert((vnode.anchor = createComment(`]`)), container, next)
471471
return next
@@ -480,7 +480,7 @@ export function createHydrationFunctions(
480480
slotScopeIds: string[] | null,
481481
isFragment: boolean
482482
): Node | null => {
483-
hasMismatch = true
483+
hasMismatch = true; debugger
484484
__DEV__ &&
485485
warn(
486486
`Hydration node mismatch:\n- Client vnode:`,

packages/server-renderer/src/helpers/ssrRenderSlot.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ export function ssrRenderSlot(
2121
) {
2222
// template-compiled slots are always rendered as fragments
2323
push(`<!--[-->`)
24+
ssrRenderSlotInner(
25+
slots,
26+
slotName,
27+
slotProps,
28+
fallbackRenderFn,
29+
push,
30+
parentComponent,
31+
slotScopeId
32+
)
33+
push(`<!--]-->`)
34+
}
35+
36+
export function ssrRenderSlotInner(
37+
slots: Slots | SSRSlots,
38+
slotName: string,
39+
slotProps: Props,
40+
fallbackRenderFn: (() => void) | null,
41+
push: PushFn,
42+
parentComponent: ComponentInternalInstance,
43+
slotScopeId?: string
44+
) {
2445
const slotFn = slots[slotName]
2546
if (slotFn) {
2647
const slotBuffer: SSRBufferItem[] = []
@@ -59,7 +80,6 @@ export function ssrRenderSlot(
5980
} else if (fallbackRenderFn) {
6081
fallbackRenderFn()
6182
}
62-
push(`<!--]-->`)
6383
}
6484

6585
const commentRE = /^<!--.*-->$/

packages/server-renderer/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export {
1818
// internal runtime helpers
1919
export { renderVNode as ssrRenderVNode } from './render'
2020
export { ssrRenderComponent } from './helpers/ssrRenderComponent'
21-
export { ssrRenderSlot } from './helpers/ssrRenderSlot'
21+
export { ssrRenderSlot, ssrRenderSlotInner } from './helpers/ssrRenderSlot'
2222
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
2323
export {
2424
ssrRenderClass,

0 commit comments

Comments
 (0)