Skip to content

Commit ee4186e

Browse files
committed
fix(ssr): fix hydration error on falsy v-if inside transition/keep-alive
fix #5352
1 parent c65b805 commit ee4186e

11 files changed

+119
-53
lines changed

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

+79-33
Original file line numberDiff line numberDiff line change
@@ -280,46 +280,92 @@ describe('ssr: components', () => {
280280
`)
281281
})
282282

283-
test('built-in fallthroughs', () => {
284-
expect(compile(`<transition><div/></transition>`).code)
285-
.toMatchInlineSnapshot(`
286-
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
283+
describe('built-in fallthroughs', () => {
284+
test('transition', () => {
285+
expect(compile(`<transition><div/></transition>`).code)
286+
.toMatchInlineSnapshot(`
287+
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
288+
289+
return function ssrRender(_ctx, _push, _parent, _attrs) {
290+
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
291+
}"
292+
`)
293+
})
287294

288-
return function ssrRender(_ctx, _push, _parent, _attrs) {
289-
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
290-
}"
291-
`)
295+
test('keep-alive', () => {
296+
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
297+
.toMatchInlineSnapshot(`
298+
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
299+
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
292300
293-
// should inject attrs if root with coomments
294-
expect(compile(`<!--root--><transition><div/></transition>`).code)
295-
.toMatchInlineSnapshot(`
296-
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
301+
return function ssrRender(_ctx, _push, _parent, _attrs) {
302+
const _component_foo = _resolveComponent(\\"foo\\")
297303
298-
return function ssrRender(_ctx, _push, _parent, _attrs) {
299-
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
300-
}"
301-
`)
304+
_push(_ssrRenderComponent(_component_foo, _attrs, null, _parent))
305+
}"
306+
`)
307+
})
302308

303-
// should not inject attrs if not root
304-
expect(compile(`<div/><transition><div/></transition>`).code)
305-
.toMatchInlineSnapshot(`
306-
"
307-
return function ssrRender(_ctx, _push, _parent, _attrs) {
308-
_push(\`<!--[--><div></div><div></div><!--]-->\`)
309-
}"
310-
`)
309+
test('should inject attrs if root with coomments', () => {
310+
expect(compile(`<!--root--><transition><div/></transition>`).code)
311+
.toMatchInlineSnapshot(`
312+
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
311313
312-
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
313-
.toMatchInlineSnapshot(`
314-
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
315-
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
314+
return function ssrRender(_ctx, _push, _parent, _attrs) {
315+
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
316+
}"
317+
`)
318+
})
316319

317-
return function ssrRender(_ctx, _push, _parent, _attrs) {
318-
const _component_foo = _resolveComponent(\\"foo\\")
320+
test('should not inject attrs if not root', () => {
321+
expect(compile(`<div/><transition><div/></transition>`).code)
322+
.toMatchInlineSnapshot(`
323+
"
324+
return function ssrRender(_ctx, _push, _parent, _attrs) {
325+
_push(\`<!--[--><div></div><div></div><!--]-->\`)
326+
}"
327+
`)
328+
})
319329

320-
_push(_ssrRenderComponent(_component_foo, _attrs, null, _parent))
321-
}"
322-
`)
330+
// #5352
331+
test('should push marker string if is slot root', () => {
332+
expect(
333+
compile(`<foo><transition><div v-if="false"/></transition></foo>`)
334+
.code
335+
).toMatchInlineSnapshot(`
336+
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Transition: _Transition, createVNode: _createVNode } = require(\\"vue\\")
337+
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
338+
339+
return function ssrRender(_ctx, _push, _parent, _attrs) {
340+
const _component_foo = _resolveComponent(\\"foo\\")
341+
342+
_push(_ssrRenderComponent(_component_foo, _attrs, {
343+
default: _withCtx((_, _push, _parent, _scopeId) => {
344+
if (_push) {
345+
_push(\`\`)
346+
if (false) {
347+
_push(\`<div\${_scopeId}></div>\`)
348+
} else {
349+
_push(\`<!---->\`)
350+
}
351+
} else {
352+
return [
353+
_createVNode(_Transition, null, {
354+
default: _withCtx(() => [
355+
false
356+
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
357+
: _createCommentVNode(\\"v-if\\", true)
358+
]),
359+
_: 1 /* STABLE */
360+
})
361+
]
362+
}
363+
}),
364+
_: 1 /* STABLE */
365+
}, _parent))
366+
}"
367+
`)
368+
})
323369
})
324370

325371
// transition-group should flatten and concat its children fragments into

packages/compiler-ssr/src/ssrCodegenTransform.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
5151

5252
const isFragment =
5353
ast.children.length > 1 && ast.children.some(c => !isText(c))
54-
processChildren(ast.children, context, isFragment)
54+
processChildren(ast, context, isFragment)
5555
ast.codegenNode = createBlockStatement(context.body)
5656

5757
// Finalize helpers.
@@ -125,15 +125,20 @@ function createChildContext(
125125
)
126126
}
127127

128+
interface Container {
129+
children: TemplateChildNode[]
130+
}
131+
128132
export function processChildren(
129-
children: TemplateChildNode[],
133+
parent: Container,
130134
context: SSRTransformContext,
131135
asFragment = false,
132136
disableNestedFragments = false
133137
) {
134138
if (asFragment) {
135139
context.pushStringPart(`<!--[-->`)
136140
}
141+
const { children } = parent
137142
for (let i = 0; i < children.length; i++) {
138143
const child = children[i]
139144
switch (child.type) {
@@ -143,7 +148,7 @@ export function processChildren(
143148
ssrProcessElement(child, context)
144149
break
145150
case ElementTypes.COMPONENT:
146-
ssrProcessComponent(child, context)
151+
ssrProcessComponent(child, context, parent)
147152
break
148153
case ElementTypes.SLOT:
149154
ssrProcessSlotOutlet(child, context)
@@ -208,12 +213,12 @@ export function processChildren(
208213
}
209214

210215
export function processChildrenAsStatement(
211-
children: TemplateChildNode[],
216+
parent: Container,
212217
parentContext: SSRTransformContext,
213218
asFragment = false,
214219
withSlotScopeId = parentContext.withSlotScopeId
215220
): BlockStatement {
216221
const childContext = createChildContext(parentContext, withSlotScopeId)
217-
processChildren(children, childContext, asFragment)
222+
processChildren(parent, childContext, asFragment)
218223
return createBlockStatement(childContext.body)
219224
}

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

+15-4
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ import { buildSSRProps } from './ssrTransformElement'
5858
// pass and complete them in the 2nd pass.
5959
const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
6060

61+
const WIP_SLOT = Symbol()
62+
6163
interface WIPSlotEntry {
64+
type: typeof WIP_SLOT
6265
fn: FunctionExpression
6366
children: TemplateChildNode[]
6467
vnodeBranch: ReturnStatement
@@ -143,6 +146,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
143146
loc
144147
)
145148
wipEntries.push({
149+
type: WIP_SLOT,
146150
fn,
147151
children,
148152
// also collect the corresponding vnode branch built earlier
@@ -182,7 +186,8 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
182186

183187
export function ssrProcessComponent(
184188
node: ComponentNode,
185-
context: SSRTransformContext
189+
context: SSRTransformContext,
190+
parent: { children: TemplateChildNode[] }
186191
) {
187192
const component = componentTypeMap.get(node)!
188193
if (!node.ssrCodegenNode) {
@@ -196,21 +201,27 @@ export function ssrProcessComponent(
196201
} else {
197202
// real fall-through: Transition / KeepAlive
198203
// just render its children.
199-
processChildren(node.children, context)
204+
// #5352: if is at root level of a slot, push an empty string.
205+
// this does not affect the final output, but avoids all-comment slot
206+
// content of being treated as empty by ssrRenderSlot().
207+
if ((parent as WIPSlotEntry).type === WIP_SLOT) {
208+
context.pushStringPart(``)
209+
}
210+
processChildren(node, context)
200211
}
201212
} else {
202213
// finish up slot function expressions from the 1st pass.
203214
const wipEntries = wipMap.get(node) || []
204215
for (let i = 0; i < wipEntries.length; i++) {
205-
const { fn, children, vnodeBranch } = wipEntries[i]
216+
const { fn, vnodeBranch } = wipEntries[i]
206217
// For each slot, we generate two branches: one SSR-optimized branch and
207218
// one normal vnode-based branch. The branches are taken based on the
208219
// presence of the 2nd `_push` argument (which is only present if the slot
209220
// is called by `_ssrRenderSlot`.
210221
fn.body = createIfStatement(
211222
createSimpleExpression(`_push`, false),
212223
processChildrenAsStatement(
213-
children,
224+
wipEntries[i],
214225
context,
215226
false,
216227
true /* withSlotScopeId */

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ export function ssrProcessElement(
428428
if (rawChildren) {
429429
context.pushStringPart(rawChildren)
430430
} else if (node.children.length) {
431-
processChildren(node.children, context)
431+
processChildren(node, context)
432432
}
433433

434434
if (!isVoidTag(node.tag)) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function ssrProcessSlotOutlet(
6565
// has fallback content
6666
if (node.children.length) {
6767
const fallbackRenderFn = createFunctionExpression([])
68-
fallbackRenderFn.body = processChildrenAsStatement(node.children, context)
68+
fallbackRenderFn.body = processChildrenAsStatement(node, context)
6969
// _renderSlot(slots, name, props, fallback, ...)
7070
renderCall.arguments[3] = fallbackRenderFn
7171
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export function ssrProcessSuspense(
6666
}
6767
const { slotsExp, wipSlots } = wipEntry
6868
for (let i = 0; i < wipSlots.length; i++) {
69-
const { fn, children } = wipSlots[i]
70-
fn.body = processChildrenAsStatement(children, context)
69+
const slot = wipSlots[i]
70+
slot.fn.body = processChildrenAsStatement(slot, context)
7171
}
7272
// _push(ssrRenderSuspense(slots))
7373
context.pushStatement(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function ssrProcessTeleport(
5858
false, // isSlot
5959
node.loc
6060
)
61-
contentRenderFn.body = processChildrenAsStatement(node.children, context)
61+
contentRenderFn.body = processChildrenAsStatement(node, context)
6262
context.pushStatement(
6363
createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
6464
`_push`,

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function ssrProcessTransitionGroup(
1414
context.pushStringPart(`>`)
1515

1616
processChildren(
17-
node.children,
17+
node,
1818
context,
1919
false,
2020
/**
@@ -31,11 +31,11 @@ export function ssrProcessTransitionGroup(
3131
} else {
3232
// static tag
3333
context.pushStringPart(`<${tag.value!.content}>`)
34-
processChildren(node.children, context, false, true)
34+
processChildren(node, context, false, true)
3535
context.pushStringPart(`</${tag.value!.content}>`)
3636
}
3737
} else {
3838
// fragment
39-
processChildren(node.children, context, true, true)
39+
processChildren(node, context, true, true)
4040
}
4141
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function ssrProcessFor(
3333
createForLoopParams(node.parseResult)
3434
)
3535
renderLoop.body = processChildrenAsStatement(
36-
node.children,
36+
node,
3737
context,
3838
needFragmentWrapper
3939
)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,5 @@ function processIfBranch(
7272
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
7373
// optimize away nested fragments when the only child is a ForNode
7474
!(children.length === 1 && children[0].type === NodeTypes.FOR)
75-
return processChildrenAsStatement(children, context, needFragmentWrapper)
75+
return processChildrenAsStatement(branch, context, needFragmentWrapper)
7676
}

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,9 @@ export function ssrRenderSlotInner(
8484

8585
const commentRE = /<!--.*?-->/g
8686
function isComment(item: SSRBufferItem) {
87-
return typeof item === 'string' && !item.replace(commentRE, '').trim()
87+
return (
88+
typeof item === 'string' &&
89+
commentRE.test(item) &&
90+
!item.replace(commentRE, '').trim()
91+
)
8892
}

0 commit comments

Comments
 (0)