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,46 +280,92 @@ describe('ssr: components', () => {
|
|||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('built-in fallthroughs', () => {
|
describe('built-in fallthroughs', () => {
|
||||||
expect(compile(`<transition><div/></transition>`).code)
|
test('transition', () => {
|
||||||
.toMatchInlineSnapshot(`
|
expect(compile(`<transition><div/></transition>`).code)
|
||||||
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
|
.toMatchInlineSnapshot(`
|
||||||
|
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
// should inject attrs if root with coomments
|
test('keep-alive', () => {
|
||||||
expect(compile(`<!--root--><transition><div/></transition>`).code)
|
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
|
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
|
||||||
|
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
|
const _component_foo = _resolveComponent(\\"foo\\")
|
||||||
}"
|
|
||||||
`)
|
|
||||||
|
|
||||||
// should not inject attrs if not root
|
_push(_ssrRenderComponent(_component_foo, _attrs, null, _parent))
|
||||||
expect(compile(`<div/><transition><div/></transition>`).code)
|
}"
|
||||||
.toMatchInlineSnapshot(`
|
`)
|
||||||
"
|
})
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
|
||||||
_push(\`<!--[--><div></div><div></div><!--]-->\`)
|
|
||||||
}"
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
|
test('should inject attrs if root with coomments', () => {
|
||||||
.toMatchInlineSnapshot(`
|
expect(compile(`<!--root--><transition><div/></transition>`).code)
|
||||||
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
|
.toMatchInlineSnapshot(`
|
||||||
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
|
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
const _component_foo = _resolveComponent(\\"foo\\")
|
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
_push(_ssrRenderComponent(_component_foo, _attrs, null, _parent))
|
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
|
// transition-group should flatten and concat its children fragments into
|
||||||
|
@ -51,7 +51,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
|
|||||||
|
|
||||||
const isFragment =
|
const isFragment =
|
||||||
ast.children.length > 1 && ast.children.some(c => !isText(c))
|
ast.children.length > 1 && ast.children.some(c => !isText(c))
|
||||||
processChildren(ast.children, context, isFragment)
|
processChildren(ast, context, isFragment)
|
||||||
ast.codegenNode = createBlockStatement(context.body)
|
ast.codegenNode = createBlockStatement(context.body)
|
||||||
|
|
||||||
// Finalize helpers.
|
// Finalize helpers.
|
||||||
@ -125,8 +125,12 @@ function createChildContext(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Container {
|
||||||
|
children: TemplateChildNode[]
|
||||||
|
}
|
||||||
|
|
||||||
export function processChildren(
|
export function processChildren(
|
||||||
children: TemplateChildNode[],
|
parent: Container,
|
||||||
context: SSRTransformContext,
|
context: SSRTransformContext,
|
||||||
asFragment = false,
|
asFragment = false,
|
||||||
disableNestedFragments = false
|
disableNestedFragments = false
|
||||||
@ -134,6 +138,7 @@ export function processChildren(
|
|||||||
if (asFragment) {
|
if (asFragment) {
|
||||||
context.pushStringPart(`<!--[-->`)
|
context.pushStringPart(`<!--[-->`)
|
||||||
}
|
}
|
||||||
|
const { children } = parent
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
const child = children[i]
|
const child = children[i]
|
||||||
switch (child.type) {
|
switch (child.type) {
|
||||||
@ -143,7 +148,7 @@ export function processChildren(
|
|||||||
ssrProcessElement(child, context)
|
ssrProcessElement(child, context)
|
||||||
break
|
break
|
||||||
case ElementTypes.COMPONENT:
|
case ElementTypes.COMPONENT:
|
||||||
ssrProcessComponent(child, context)
|
ssrProcessComponent(child, context, parent)
|
||||||
break
|
break
|
||||||
case ElementTypes.SLOT:
|
case ElementTypes.SLOT:
|
||||||
ssrProcessSlotOutlet(child, context)
|
ssrProcessSlotOutlet(child, context)
|
||||||
@ -208,12 +213,12 @@ export function processChildren(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function processChildrenAsStatement(
|
export function processChildrenAsStatement(
|
||||||
children: TemplateChildNode[],
|
parent: Container,
|
||||||
parentContext: SSRTransformContext,
|
parentContext: SSRTransformContext,
|
||||||
asFragment = false,
|
asFragment = false,
|
||||||
withSlotScopeId = parentContext.withSlotScopeId
|
withSlotScopeId = parentContext.withSlotScopeId
|
||||||
): BlockStatement {
|
): BlockStatement {
|
||||||
const childContext = createChildContext(parentContext, withSlotScopeId)
|
const childContext = createChildContext(parentContext, withSlotScopeId)
|
||||||
processChildren(children, childContext, asFragment)
|
processChildren(parent, childContext, asFragment)
|
||||||
return createBlockStatement(childContext.body)
|
return createBlockStatement(childContext.body)
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,10 @@ import { buildSSRProps } from './ssrTransformElement'
|
|||||||
// pass and complete them in the 2nd pass.
|
// pass and complete them in the 2nd pass.
|
||||||
const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
|
const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
|
||||||
|
|
||||||
|
const WIP_SLOT = Symbol()
|
||||||
|
|
||||||
interface WIPSlotEntry {
|
interface WIPSlotEntry {
|
||||||
|
type: typeof WIP_SLOT
|
||||||
fn: FunctionExpression
|
fn: FunctionExpression
|
||||||
children: TemplateChildNode[]
|
children: TemplateChildNode[]
|
||||||
vnodeBranch: ReturnStatement
|
vnodeBranch: ReturnStatement
|
||||||
@ -143,6 +146,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
|||||||
loc
|
loc
|
||||||
)
|
)
|
||||||
wipEntries.push({
|
wipEntries.push({
|
||||||
|
type: WIP_SLOT,
|
||||||
fn,
|
fn,
|
||||||
children,
|
children,
|
||||||
// also collect the corresponding vnode branch built earlier
|
// also collect the corresponding vnode branch built earlier
|
||||||
@ -182,7 +186,8 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
|||||||
|
|
||||||
export function ssrProcessComponent(
|
export function ssrProcessComponent(
|
||||||
node: ComponentNode,
|
node: ComponentNode,
|
||||||
context: SSRTransformContext
|
context: SSRTransformContext,
|
||||||
|
parent: { children: TemplateChildNode[] }
|
||||||
) {
|
) {
|
||||||
const component = componentTypeMap.get(node)!
|
const component = componentTypeMap.get(node)!
|
||||||
if (!node.ssrCodegenNode) {
|
if (!node.ssrCodegenNode) {
|
||||||
@ -196,13 +201,19 @@ export function ssrProcessComponent(
|
|||||||
} else {
|
} else {
|
||||||
// real fall-through: Transition / KeepAlive
|
// real fall-through: Transition / KeepAlive
|
||||||
// just render its children.
|
// 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 {
|
} else {
|
||||||
// finish up slot function expressions from the 1st pass.
|
// finish up slot function expressions from the 1st pass.
|
||||||
const wipEntries = wipMap.get(node) || []
|
const wipEntries = wipMap.get(node) || []
|
||||||
for (let i = 0; i < wipEntries.length; i++) {
|
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
|
// For each slot, we generate two branches: one SSR-optimized branch and
|
||||||
// one normal vnode-based branch. The branches are taken based on the
|
// 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
|
// presence of the 2nd `_push` argument (which is only present if the slot
|
||||||
@ -210,7 +221,7 @@ export function ssrProcessComponent(
|
|||||||
fn.body = createIfStatement(
|
fn.body = createIfStatement(
|
||||||
createSimpleExpression(`_push`, false),
|
createSimpleExpression(`_push`, false),
|
||||||
processChildrenAsStatement(
|
processChildrenAsStatement(
|
||||||
children,
|
wipEntries[i],
|
||||||
context,
|
context,
|
||||||
false,
|
false,
|
||||||
true /* withSlotScopeId */
|
true /* withSlotScopeId */
|
||||||
|
@ -428,7 +428,7 @@ export function ssrProcessElement(
|
|||||||
if (rawChildren) {
|
if (rawChildren) {
|
||||||
context.pushStringPart(rawChildren)
|
context.pushStringPart(rawChildren)
|
||||||
} else if (node.children.length) {
|
} else if (node.children.length) {
|
||||||
processChildren(node.children, context)
|
processChildren(node, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isVoidTag(node.tag)) {
|
if (!isVoidTag(node.tag)) {
|
||||||
|
@ -65,7 +65,7 @@ export function ssrProcessSlotOutlet(
|
|||||||
// has fallback content
|
// has fallback content
|
||||||
if (node.children.length) {
|
if (node.children.length) {
|
||||||
const fallbackRenderFn = createFunctionExpression([])
|
const fallbackRenderFn = createFunctionExpression([])
|
||||||
fallbackRenderFn.body = processChildrenAsStatement(node.children, context)
|
fallbackRenderFn.body = processChildrenAsStatement(node, context)
|
||||||
// _renderSlot(slots, name, props, fallback, ...)
|
// _renderSlot(slots, name, props, fallback, ...)
|
||||||
renderCall.arguments[3] = fallbackRenderFn
|
renderCall.arguments[3] = fallbackRenderFn
|
||||||
}
|
}
|
||||||
|
@ -66,8 +66,8 @@ export function ssrProcessSuspense(
|
|||||||
}
|
}
|
||||||
const { slotsExp, wipSlots } = wipEntry
|
const { slotsExp, wipSlots } = wipEntry
|
||||||
for (let i = 0; i < wipSlots.length; i++) {
|
for (let i = 0; i < wipSlots.length; i++) {
|
||||||
const { fn, children } = wipSlots[i]
|
const slot = wipSlots[i]
|
||||||
fn.body = processChildrenAsStatement(children, context)
|
slot.fn.body = processChildrenAsStatement(slot, context)
|
||||||
}
|
}
|
||||||
// _push(ssrRenderSuspense(slots))
|
// _push(ssrRenderSuspense(slots))
|
||||||
context.pushStatement(
|
context.pushStatement(
|
||||||
|
@ -58,7 +58,7 @@ export function ssrProcessTeleport(
|
|||||||
false, // isSlot
|
false, // isSlot
|
||||||
node.loc
|
node.loc
|
||||||
)
|
)
|
||||||
contentRenderFn.body = processChildrenAsStatement(node.children, context)
|
contentRenderFn.body = processChildrenAsStatement(node, context)
|
||||||
context.pushStatement(
|
context.pushStatement(
|
||||||
createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
|
createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
|
||||||
`_push`,
|
`_push`,
|
||||||
|
@ -14,7 +14,7 @@ export function ssrProcessTransitionGroup(
|
|||||||
context.pushStringPart(`>`)
|
context.pushStringPart(`>`)
|
||||||
|
|
||||||
processChildren(
|
processChildren(
|
||||||
node.children,
|
node,
|
||||||
context,
|
context,
|
||||||
false,
|
false,
|
||||||
/**
|
/**
|
||||||
@ -31,11 +31,11 @@ export function ssrProcessTransitionGroup(
|
|||||||
} else {
|
} else {
|
||||||
// static tag
|
// static tag
|
||||||
context.pushStringPart(`<${tag.value!.content}>`)
|
context.pushStringPart(`<${tag.value!.content}>`)
|
||||||
processChildren(node.children, context, false, true)
|
processChildren(node, context, false, true)
|
||||||
context.pushStringPart(`</${tag.value!.content}>`)
|
context.pushStringPart(`</${tag.value!.content}>`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// fragment
|
// fragment
|
||||||
processChildren(node.children, context, true, true)
|
processChildren(node, context, true, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ export function ssrProcessFor(
|
|||||||
createForLoopParams(node.parseResult)
|
createForLoopParams(node.parseResult)
|
||||||
)
|
)
|
||||||
renderLoop.body = processChildrenAsStatement(
|
renderLoop.body = processChildrenAsStatement(
|
||||||
node.children,
|
node,
|
||||||
context,
|
context,
|
||||||
needFragmentWrapper
|
needFragmentWrapper
|
||||||
)
|
)
|
||||||
|
@ -72,5 +72,5 @@ function processIfBranch(
|
|||||||
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
||||||
// optimize away nested fragments when the only child is a ForNode
|
// optimize away nested fragments when the only child is a ForNode
|
||||||
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
!(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
|
const commentRE = /<!--.*?-->/g
|
||||||
function isComment(item: SSRBufferItem) {
|
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