]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): fix hydration error for slot outlet inside transition
authorEvan You <yyx990803@gmail.com>
Thu, 12 May 2022 07:06:32 +0000 (15:06 +0800)
committerEvan You <yyx990803@gmail.com>
Thu, 12 May 2022 07:06:32 +0000 (15:06 +0800)
fix #3989

packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts
packages/runtime-core/src/hydration.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts
packages/server-renderer/src/index.ts

index d4c6fee918a8735ea14ebd34c3ab35b3c072b4c4..695cfdf7f133937688d70a58eba8589ab155bdf3 100644 (file)
@@ -1,4 +1,5 @@
 import { compile } from '../src'
+import { ssrHelpers, SSR_RENDER_SLOT_INNER } from '../src/runtimeHelpers'
 
 describe('ssr: <slot>', () => {
   test('basic', () => {
@@ -114,4 +115,16 @@ describe('ssr: <slot>', () => {
       }"
     `)
   })
+
+  test('inside transition', () => {
+    const { code } = compile(`<transition><slot/></transition>`)
+    expect(code).toMatch(ssrHelpers[SSR_RENDER_SLOT_INNER])
+    expect(code).toMatchInlineSnapshot(`
+      "const { ssrRenderSlotInner: _ssrRenderSlotInner } = require(\\"vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _ssrRenderSlotInner(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
+      }"
+    `)
+  })
 })
index f0a6a2f290cdc922b91a0c3b65934b69f8ae3845..220af4854f67deb4c7d22d1e230eb4f83c0cbf16 100644 (file)
@@ -4,6 +4,7 @@ export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`)
 export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`)
 export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`)
 export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`)
+export const SSR_RENDER_SLOT_INNER = Symbol(`ssrRenderSlotInner`)
 export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`)
 export const SSR_RENDER_STYLE = Symbol(`ssrRenderStyle`)
 export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
@@ -24,6 +25,7 @@ export const ssrHelpers = {
   [SSR_RENDER_VNODE]: `ssrRenderVNode`,
   [SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
   [SSR_RENDER_SLOT]: `ssrRenderSlot`,
+  [SSR_RENDER_SLOT_INNER]: `ssrRenderSlotInner`,
   [SSR_RENDER_CLASS]: `ssrRenderClass`,
   [SSR_RENDER_STYLE]: `ssrRenderStyle`,
   [SSR_RENDER_ATTRS]: `ssrRenderAttrs`,
index 98020f6c179ae944dd723b64607275d8475cd21a..21c33831cccd228354a3cfc3d404fd0088dca996 100644 (file)
@@ -4,9 +4,13 @@ import {
   processSlotOutlet,
   createCallExpression,
   SlotOutletNode,
-  createFunctionExpression
+  createFunctionExpression,
+  NodeTypes,
+  ElementTypes,
+  resolveComponentType,
+  TRANSITION
 } from '@vue/compiler-dom'
-import { SSR_RENDER_SLOT } from '../runtimeHelpers'
+import { SSR_RENDER_SLOT, SSR_RENDER_SLOT_INNER } from '../runtimeHelpers'
 import {
   SSRTransformContext,
   processChildrenAsStatement
@@ -31,10 +35,24 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
       args.push(`"${context.scopeId}-s"`)
     }
 
-    node.ssrCodegenNode = createCallExpression(
-      context.helper(SSR_RENDER_SLOT),
-      args
-    )
+    let method = SSR_RENDER_SLOT
+
+    // #3989
+    // check if this is a single slot inside a transition wrapper - since
+    // transition will unwrap the slot fragment into a single vnode at runtime,
+    // we need to avoid rendering the slot as a fragment.
+    const parent = context.parent
+    if (
+      parent &&
+      parent.type === NodeTypes.ELEMENT &&
+      parent.tagType === ElementTypes.COMPONENT &&
+      resolveComponentType(parent, context, true) === TRANSITION &&
+      parent.children.filter(c => c.type === NodeTypes.ELEMENT).length === 1
+    ) {
+      method = SSR_RENDER_SLOT_INNER
+    }
+
+    node.ssrCodegenNode = createCallExpression(context.helper(method), args)
   }
 }
 
index 3d86f0f2494b66be5664fbbe19fff484de5e544b..ae0415aa82aea0f7086c51e3400b34ad1a4938ed 100644 (file)
@@ -113,7 +113,7 @@ export function createHydrationFunctions(
           nextNode = onMismatch()
         } else {
           if ((node as Text).data !== vnode.children) {
-            hasMismatch = true
+            hasMismatch = true; debugger
             __DEV__ &&
               warn(
                 `Hydration text mismatch:` +
@@ -351,7 +351,7 @@ export function createHydrationFunctions(
         )
         let hasWarned = false
         while (next) {
-          hasMismatch = true
+          hasMismatch = true; debugger
           if (__DEV__ && !hasWarned) {
             warn(
               `Hydration children mismatch in <${vnode.type as string}>: ` +
@@ -366,7 +366,7 @@ export function createHydrationFunctions(
         }
       } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
         if (el.textContent !== vnode.children) {
-          hasMismatch = true
+          hasMismatch = true; debugger
           __DEV__ &&
             warn(
               `Hydration text content mismatch in <${
@@ -411,7 +411,7 @@ export function createHydrationFunctions(
       } else if (vnode.type === Text && !vnode.children) {
         continue
       } else {
-        hasMismatch = true
+        hasMismatch = true; debugger
         if (__DEV__ && !hasWarned) {
           warn(
             `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
@@ -465,7 +465,7 @@ export function createHydrationFunctions(
     } else {
       // fragment didn't hydrate successfully, since we didn't get a end anchor
       // back. This should have led to node/children mismatch warnings.
-      hasMismatch = true
+      hasMismatch = true; debugger
       // since the anchor is missing, we need to create one and insert it
       insert((vnode.anchor = createComment(`]`)), container, next)
       return next
@@ -480,7 +480,7 @@ export function createHydrationFunctions(
     slotScopeIds: string[] | null,
     isFragment: boolean
   ): Node | null => {
-    hasMismatch = true
+    hasMismatch = true; debugger
     __DEV__ &&
       warn(
         `Hydration node mismatch:\n- Client vnode:`,
index 8b9a3971e2c4198049bf7566e6fe8da436429cd5..967b20311970e0f6f0694c59a8a48d09897230ae 100644 (file)
@@ -21,6 +21,27 @@ export function ssrRenderSlot(
 ) {
   // template-compiled slots are always rendered as fragments
   push(`<!--[-->`)
+  ssrRenderSlotInner(
+    slots,
+    slotName,
+    slotProps,
+    fallbackRenderFn,
+    push,
+    parentComponent,
+    slotScopeId
+  )
+  push(`<!--]-->`)
+}
+
+export function ssrRenderSlotInner(
+  slots: Slots | SSRSlots,
+  slotName: string,
+  slotProps: Props,
+  fallbackRenderFn: (() => void) | null,
+  push: PushFn,
+  parentComponent: ComponentInternalInstance,
+  slotScopeId?: string
+) {
   const slotFn = slots[slotName]
   if (slotFn) {
     const slotBuffer: SSRBufferItem[] = []
@@ -59,7 +80,6 @@ export function ssrRenderSlot(
   } else if (fallbackRenderFn) {
     fallbackRenderFn()
   }
-  push(`<!--]-->`)
 }
 
 const commentRE = /^<!--.*-->$/
index e8b716a865792b7f7c1b11dc73f8d17375d308ca..b6d973ea599541bd13291efd65b64d1cee2c1ac0 100644 (file)
@@ -18,7 +18,7 @@ export {
 // internal runtime helpers
 export { renderVNode as ssrRenderVNode } from './render'
 export { ssrRenderComponent } from './helpers/ssrRenderComponent'
-export { ssrRenderSlot } from './helpers/ssrRenderSlot'
+export { ssrRenderSlot, ssrRenderSlotInner } from './helpers/ssrRenderSlot'
 export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
 export {
   ssrRenderClass,