From ee4186ef9ebbc45827b208f6f5b648dbf4337d1d Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 18 May 2022 09:28:18 +0800 Subject: [PATCH] fix(ssr): fix hydration error on falsy v-if inside transition/keep-alive fix #5352 --- .../__tests__/ssrComponent.spec.ts | 112 ++++++++++++------ .../compiler-ssr/src/ssrCodegenTransform.ts | 15 ++- .../src/transforms/ssrTransformComponent.ts | 19 ++- .../src/transforms/ssrTransformElement.ts | 2 +- .../src/transforms/ssrTransformSlotOutlet.ts | 2 +- .../src/transforms/ssrTransformSuspense.ts | 4 +- .../src/transforms/ssrTransformTeleport.ts | 2 +- .../transforms/ssrTransformTransitionGroup.ts | 6 +- .../compiler-ssr/src/transforms/ssrVFor.ts | 2 +- .../compiler-ssr/src/transforms/ssrVIf.ts | 2 +- .../src/helpers/ssrRenderSlot.ts | 6 +- 11 files changed, 119 insertions(+), 53 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 672af193e92..2f279c090d2 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -280,46 +280,92 @@ describe('ssr: components', () => { `) }) - test('built-in fallthroughs', () => { - expect(compile(`
`).code) - .toMatchInlineSnapshot(` - "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + describe('built-in fallthroughs', () => { + test('transition', () => { + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`
\`) + }" + `) + }) - return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`
\`) - }" - `) + test('keep-alive', () => { + expect(compile(``).code) + .toMatchInlineSnapshot(` + "const { resolveComponent: _resolveComponent } = require(\\"vue\\") + const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\") - // should inject attrs if root with coomments - expect(compile(`
`).code) - .toMatchInlineSnapshot(` - "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + return function ssrRender(_ctx, _push, _parent, _attrs) { + const _component_foo = _resolveComponent(\\"foo\\") - return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`
\`) - }" - `) + _push(_ssrRenderComponent(_component_foo, _attrs, null, _parent)) + }" + `) + }) - // should not inject attrs if not root - expect(compile(`
`).code) - .toMatchInlineSnapshot(` - " - return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(\`
\`) - }" - `) + test('should inject attrs if root with coomments', () => { + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") - expect(compile(``).code) - .toMatchInlineSnapshot(` - "const { resolveComponent: _resolveComponent } = require(\\"vue\\") - const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\") + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`
\`) + }" + `) + }) - return function ssrRender(_ctx, _push, _parent, _attrs) { - const _component_foo = _resolveComponent(\\"foo\\") + test('should not inject attrs if not root', () => { + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + " + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`
\`) + }" + `) + }) - _push(_ssrRenderComponent(_component_foo, _attrs, null, _parent)) - }" - `) + // #5352 + test('should push marker string if is slot root', () => { + expect( + compile(`
`) + .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(\`
\`) + } 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 diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts index c30b90aa131..a7f3ada15b4 100644 --- a/packages/compiler-ssr/src/ssrCodegenTransform.ts +++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts @@ -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) } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index dfb78136a9d..83d552103ca 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -58,7 +58,10 @@ import { buildSSRProps } from './ssrTransformElement' // pass and complete them in the 2nd pass. const wipMap = new WeakMap() +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 */ diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts index a082d94eb57..2c42a83ba37 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts @@ -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)) { diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts index 21c33831ccc..3486f355102 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts @@ -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 } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts index 33543326922..207e9348eef 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts @@ -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( diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTeleport.ts b/packages/compiler-ssr/src/transforms/ssrTransformTeleport.ts index d9cfdbb0274..f470ca711d4 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTeleport.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTeleport.ts @@ -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`, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index 80a26c98a49..378c4f333d4 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -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(``) } } else { // fragment - processChildren(node.children, context, true, true) + processChildren(node, context, true, true) } } diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts index 583873b66ff..0515993d475 100644 --- a/packages/compiler-ssr/src/transforms/ssrVFor.ts +++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts @@ -33,7 +33,7 @@ export function ssrProcessFor( createForLoopParams(node.parseResult) ) renderLoop.body = processChildrenAsStatement( - node.children, + node, context, needFragmentWrapper ) diff --git a/packages/compiler-ssr/src/transforms/ssrVIf.ts b/packages/compiler-ssr/src/transforms/ssrVIf.ts index 57f77eafd30..9de1d0e9a2d 100644 --- a/packages/compiler-ssr/src/transforms/ssrVIf.ts +++ b/packages/compiler-ssr/src/transforms/ssrVIf.ts @@ -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) } diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts index e421578d7af..9234e517325 100644 --- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts +++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts @@ -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() + ) }