fix(ssr): fix hydration error on falsy v-if inside transition/keep-alive
fix #5352
This commit is contained in:
parent
c65b805ef1
commit
ee4186ef9e
@ -280,7 +280,8 @@ describe('ssr: components', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test('built-in fallthroughs', () => {
|
||||
describe('built-in fallthroughs', () => {
|
||||
test('transition', () => {
|
||||
expect(compile(`<transition><div/></transition>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
|
||||
@ -289,26 +290,9 @@ describe('ssr: components', () => {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
// should inject attrs if root with coomments
|
||||
expect(compile(`<!--root--><transition><div/></transition>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
// should not inject attrs if not root
|
||||
expect(compile(`<div/><transition><div/></transition>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_push(\`<!--[--><div></div><div></div><!--]-->\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
test('keep-alive', () => {
|
||||
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
|
||||
@ -322,6 +306,68 @@ describe('ssr: components', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test('should inject attrs if root with coomments', () => {
|
||||
expect(compile(`<!--root--><transition><div/></transition>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('should not inject attrs if not root', () => {
|
||||
expect(compile(`<div/><transition><div/></transition>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_push(\`<!--[--><div></div><div></div><!--]-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
// #5352
|
||||
test('should push marker string if is slot root', () => {
|
||||
expect(
|
||||
compile(`<foo><transition><div v-if="false"/></transition></foo>`)
|
||||
.code
|
||||
).toMatchInlineSnapshot(`
|
||||
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Transition: _Transition, createVNode: _createVNode } = require(\\"vue\\")
|
||||
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
const _component_foo = _resolveComponent(\\"foo\\")
|
||||
|
||||
_push(_ssrRenderComponent(_component_foo, _attrs, {
|
||||
default: _withCtx((_, _push, _parent, _scopeId) => {
|
||||
if (_push) {
|
||||
_push(\`\`)
|
||||
if (false) {
|
||||
_push(\`<div\${_scopeId}></div>\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
} else {
|
||||
return [
|
||||
_createVNode(_Transition, null, {
|
||||
default: _withCtx(() => [
|
||||
false
|
||||
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
|
||||
: _createCommentVNode(\\"v-if\\", true)
|
||||
]),
|
||||
_: 1 /* STABLE */
|
||||
})
|
||||
]
|
||||
}
|
||||
}),
|
||||
_: 1 /* STABLE */
|
||||
}, _parent))
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
// transition-group should flatten and concat its children fragments into
|
||||
// a single one
|
||||
describe('transition-group', () => {
|
||||
|
@ -51,7 +51,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
|
||||
|
||||
const isFragment =
|
||||
ast.children.length > 1 && ast.children.some(c => !isText(c))
|
||||
processChildren(ast.children, context, isFragment)
|
||||
processChildren(ast, context, isFragment)
|
||||
ast.codegenNode = createBlockStatement(context.body)
|
||||
|
||||
// Finalize helpers.
|
||||
@ -125,8 +125,12 @@ function createChildContext(
|
||||
)
|
||||
}
|
||||
|
||||
interface Container {
|
||||
children: TemplateChildNode[]
|
||||
}
|
||||
|
||||
export function processChildren(
|
||||
children: TemplateChildNode[],
|
||||
parent: Container,
|
||||
context: SSRTransformContext,
|
||||
asFragment = false,
|
||||
disableNestedFragments = false
|
||||
@ -134,6 +138,7 @@ export function processChildren(
|
||||
if (asFragment) {
|
||||
context.pushStringPart(`<!--[-->`)
|
||||
}
|
||||
const { children } = parent
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
switch (child.type) {
|
||||
@ -143,7 +148,7 @@ export function processChildren(
|
||||
ssrProcessElement(child, context)
|
||||
break
|
||||
case ElementTypes.COMPONENT:
|
||||
ssrProcessComponent(child, context)
|
||||
ssrProcessComponent(child, context, parent)
|
||||
break
|
||||
case ElementTypes.SLOT:
|
||||
ssrProcessSlotOutlet(child, context)
|
||||
@ -208,12 +213,12 @@ export function processChildren(
|
||||
}
|
||||
|
||||
export function processChildrenAsStatement(
|
||||
children: TemplateChildNode[],
|
||||
parent: Container,
|
||||
parentContext: SSRTransformContext,
|
||||
asFragment = false,
|
||||
withSlotScopeId = parentContext.withSlotScopeId
|
||||
): BlockStatement {
|
||||
const childContext = createChildContext(parentContext, withSlotScopeId)
|
||||
processChildren(children, childContext, asFragment)
|
||||
processChildren(parent, childContext, asFragment)
|
||||
return createBlockStatement(childContext.body)
|
||||
}
|
||||
|
@ -58,7 +58,10 @@ import { buildSSRProps } from './ssrTransformElement'
|
||||
// pass and complete them in the 2nd pass.
|
||||
const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
|
||||
|
||||
const WIP_SLOT = Symbol()
|
||||
|
||||
interface WIPSlotEntry {
|
||||
type: typeof WIP_SLOT
|
||||
fn: FunctionExpression
|
||||
children: TemplateChildNode[]
|
||||
vnodeBranch: ReturnStatement
|
||||
@ -143,6 +146,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||
loc
|
||||
)
|
||||
wipEntries.push({
|
||||
type: WIP_SLOT,
|
||||
fn,
|
||||
children,
|
||||
// also collect the corresponding vnode branch built earlier
|
||||
@ -182,7 +186,8 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||
|
||||
export function ssrProcessComponent(
|
||||
node: ComponentNode,
|
||||
context: SSRTransformContext
|
||||
context: SSRTransformContext,
|
||||
parent: { children: TemplateChildNode[] }
|
||||
) {
|
||||
const component = componentTypeMap.get(node)!
|
||||
if (!node.ssrCodegenNode) {
|
||||
@ -196,13 +201,19 @@ export function ssrProcessComponent(
|
||||
} else {
|
||||
// real fall-through: Transition / KeepAlive
|
||||
// just render its children.
|
||||
processChildren(node.children, context)
|
||||
// #5352: if is at root level of a slot, push an empty string.
|
||||
// this does not affect the final output, but avoids all-comment slot
|
||||
// content of being treated as empty by ssrRenderSlot().
|
||||
if ((parent as WIPSlotEntry).type === WIP_SLOT) {
|
||||
context.pushStringPart(``)
|
||||
}
|
||||
processChildren(node, context)
|
||||
}
|
||||
} else {
|
||||
// finish up slot function expressions from the 1st pass.
|
||||
const wipEntries = wipMap.get(node) || []
|
||||
for (let i = 0; i < wipEntries.length; i++) {
|
||||
const { fn, children, vnodeBranch } = wipEntries[i]
|
||||
const { fn, vnodeBranch } = wipEntries[i]
|
||||
// For each slot, we generate two branches: one SSR-optimized branch and
|
||||
// one normal vnode-based branch. The branches are taken based on the
|
||||
// presence of the 2nd `_push` argument (which is only present if the slot
|
||||
@ -210,7 +221,7 @@ export function ssrProcessComponent(
|
||||
fn.body = createIfStatement(
|
||||
createSimpleExpression(`_push`, false),
|
||||
processChildrenAsStatement(
|
||||
children,
|
||||
wipEntries[i],
|
||||
context,
|
||||
false,
|
||||
true /* withSlotScopeId */
|
||||
|
@ -428,7 +428,7 @@ export function ssrProcessElement(
|
||||
if (rawChildren) {
|
||||
context.pushStringPart(rawChildren)
|
||||
} else if (node.children.length) {
|
||||
processChildren(node.children, context)
|
||||
processChildren(node, context)
|
||||
}
|
||||
|
||||
if (!isVoidTag(node.tag)) {
|
||||
|
@ -65,7 +65,7 @@ export function ssrProcessSlotOutlet(
|
||||
// has fallback content
|
||||
if (node.children.length) {
|
||||
const fallbackRenderFn = createFunctionExpression([])
|
||||
fallbackRenderFn.body = processChildrenAsStatement(node.children, context)
|
||||
fallbackRenderFn.body = processChildrenAsStatement(node, context)
|
||||
// _renderSlot(slots, name, props, fallback, ...)
|
||||
renderCall.arguments[3] = fallbackRenderFn
|
||||
}
|
||||
|
@ -66,8 +66,8 @@ export function ssrProcessSuspense(
|
||||
}
|
||||
const { slotsExp, wipSlots } = wipEntry
|
||||
for (let i = 0; i < wipSlots.length; i++) {
|
||||
const { fn, children } = wipSlots[i]
|
||||
fn.body = processChildrenAsStatement(children, context)
|
||||
const slot = wipSlots[i]
|
||||
slot.fn.body = processChildrenAsStatement(slot, context)
|
||||
}
|
||||
// _push(ssrRenderSuspense(slots))
|
||||
context.pushStatement(
|
||||
|
@ -58,7 +58,7 @@ export function ssrProcessTeleport(
|
||||
false, // isSlot
|
||||
node.loc
|
||||
)
|
||||
contentRenderFn.body = processChildrenAsStatement(node.children, context)
|
||||
contentRenderFn.body = processChildrenAsStatement(node, context)
|
||||
context.pushStatement(
|
||||
createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
|
||||
`_push`,
|
||||
|
@ -14,7 +14,7 @@ export function ssrProcessTransitionGroup(
|
||||
context.pushStringPart(`>`)
|
||||
|
||||
processChildren(
|
||||
node.children,
|
||||
node,
|
||||
context,
|
||||
false,
|
||||
/**
|
||||
@ -31,11 +31,11 @@ export function ssrProcessTransitionGroup(
|
||||
} else {
|
||||
// static tag
|
||||
context.pushStringPart(`<${tag.value!.content}>`)
|
||||
processChildren(node.children, context, false, true)
|
||||
processChildren(node, context, false, true)
|
||||
context.pushStringPart(`</${tag.value!.content}>`)
|
||||
}
|
||||
} else {
|
||||
// fragment
|
||||
processChildren(node.children, context, true, true)
|
||||
processChildren(node, context, true, true)
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export function ssrProcessFor(
|
||||
createForLoopParams(node.parseResult)
|
||||
)
|
||||
renderLoop.body = processChildrenAsStatement(
|
||||
node.children,
|
||||
node,
|
||||
context,
|
||||
needFragmentWrapper
|
||||
)
|
||||
|
@ -72,5 +72,5 @@ function processIfBranch(
|
||||
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
||||
// optimize away nested fragments when the only child is a ForNode
|
||||
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
||||
return processChildrenAsStatement(children, context, needFragmentWrapper)
|
||||
return processChildrenAsStatement(branch, context, needFragmentWrapper)
|
||||
}
|
||||
|
@ -84,5 +84,9 @@ export function ssrRenderSlotInner(
|
||||
|
||||
const commentRE = /<!--.*?-->/g
|
||||
function isComment(item: SSRBufferItem) {
|
||||
return typeof item === 'string' && !item.replace(commentRE, '').trim()
|
||||
return (
|
||||
typeof item === 'string' &&
|
||||
commentRE.test(item) &&
|
||||
!item.replace(commentRE, '').trim()
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user