]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: hydration for slots
authordaiwei <daiwei521@126.com>
Sat, 26 Apr 2025 14:08:55 +0000 (22:08 +0800)
committerdaiwei <daiwei521@126.com>
Sun, 27 Apr 2025 02:37:07 +0000 (10:37 +0800)
packages/compiler-vapor/src/generators/operation.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/server-renderer/__tests__/render.spec.ts
packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts
packages/server-renderer/__tests__/ssrScopeId.spec.ts
packages/server-renderer/__tests__/ssrSlot.spec.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts

index 4247bc6fecac29a230ac87e21e4d59ab8aa1543b..a3bf5cc21937a7ee271712c36071228cf906ae4d 100644 (file)
@@ -44,7 +44,7 @@ export function genOperationWithInsertionState(
 ): CodeFragment[] {
   const [frag, push] = buildCodeFragment()
   if (isBlockOperation(oper) && oper.parent) {
-    push(...genInsertionstate(oper, context))
+    push(...genInsertionState(oper, context))
   }
   push(...genOperation(oper, context))
   return frag
@@ -152,7 +152,7 @@ export function genEffect(
   return frag
 }
 
-function genInsertionstate(
+function genInsertionState(
   operation: InsertionStateTypes,
   context: CodegenContext,
 ): CodeFragment[] {
index 016872b56a51da3a0ba836870ac4e14eda227859..14faf569c005f3b0c2d7b07ef82899decd902732 100644 (file)
@@ -1531,6 +1531,20 @@ describe('Vapor Mode hydration', () => {
           `<span></span>` +
           `</div>`,
       )
+
+      data.value.splice(0, 1)
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[-->` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<span>d</span>` +
+          `<!--]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
     })
 
     test('consecutive v-for with anchor insertion', async () => {
@@ -1583,20 +1597,377 @@ describe('Vapor Mode hydration', () => {
           `<span></span>` +
           `</div>`,
       )
+
+      data.value.splice(0, 2)
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[-->` +
+          `<span>c</span>` +
+          `<span>d</span>` +
+          `<!--]-->` +
+          `<!--[-->` +
+          `<span>c</span>` +
+          `<span>d</span>` +
+          `<!--]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
     })
 
-    // TODO wait for slots hydration support
-    test.todo('v-for on component', async () => {})
+    test('v-for on component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-for="item in data" :key="item"/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><div>comp</div></template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
 
-    // TODO wait for slots hydration support
-    test.todo('on fragment component', async () => {})
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<div>comp</div>` +
+          `<div>comp</div>` +
+          `<div>comp</div>` +
+          `<!--]-->` +
+          `</div>`,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<div>comp</div>` +
+          `<div>comp</div>` +
+          `<div>comp</div>` +
+          `<div>comp</div>` +
+          `<!--]-->` +
+          `</div>`,
+      )
+    })
+
+    test('v-for on component with slots', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-for="item in data" :key="item">
+              <span>{{ item }}</span>
+            </components.Child>
+          </div>
+        </template>`,
+        {
+          Child: `<template><slot/></template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<!--[--><span>a</span><!--]--><!--slot-->` +
+          `<!--[--><span>b</span><!--]--><!--slot-->` +
+          `<!--[--><span>c</span><!--]--><!--slot-->` +
+          `<!--]-->` +
+          `</div>`,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<!--[--><span>a</span><!--]--><!--slot-->` +
+          `<!--[--><span>b</span><!--]--><!--slot-->` +
+          `<!--[--><span>c</span><!--]--><!--slot-->` +
+          `<span>d</span><!--slot-->` +
+          `<!--]-->` +
+          `</div>`,
+      )
+    })
+
+    test('on fragment component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-for="item in data" :key="item"/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><div>foo</div>-bar-</template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<!--[--><div>foo</div>-bar-<!--]-->` +
+          `<!--[--><div>foo</div>-bar-<!--]-->` +
+          `<!--[--><div>foo</div>-bar-<!--]-->` +
+          `<!--]-->` +
+          `</div>`,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<!--[--><div>foo</div>-bar-<!--]-->` +
+          `<!--[--><div>foo</div>-bar-<!--]-->` +
+          `<!--[--><div>foo</div>-bar-<!--]-->` +
+          `<div>foo</div>-bar-` +
+          `<!--]-->` +
+          `</div>`,
+      )
+    })
 
     // TODO wait for vapor TransitionGroup support
     // v-for inside TransitionGroup does not render as a fragment
     test.todo('v-for in TransitionGroup', async () => {})
   })
 
-  test.todo('slots')
+  describe('slots', () => {
+    test('basic slot', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot/></template>`,
+        },
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[--><span>foo</span><!--]--><!--slot-->`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[--><span>bar</span><!--]--><!--slot-->`,
+      )
+    })
+
+    test('named slot', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo>
+              <span>{{data}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot name="foo"/></template>`,
+        },
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[--><span>foo</span><!--]--><!--slot-->`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[--><span>bar</span><!--]--><!--slot-->`,
+      )
+    })
+
+    test('named slot with v-if', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo v-if="data">
+              <span>{{data}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot name="foo"/></template>`,
+        },
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[--><span>foo</span><!--]--><!--slot-->`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(`<!--[--><!--]--><!--slot-->`)
+    })
+
+    test('named slot with v-if and v-for', async () => {
+      const data = reactive({
+        show: true,
+        items: ['a', 'b', 'c'],
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo v-if="data.show">
+              <span v-for="item in data.items" :key="item">{{item}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot name="foo"/></template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<!--[--><span>a</span><span>b</span><span>c</span><!--]-->` +
+          `<!--]-->` +
+          `<!--slot-->`,
+      )
+
+      data.show = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[--><!--[--><!--]--><!--]--><!--slot-->`,
+      )
+    })
+
+    test('with anchor insertion', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span/>
+            <span>{{data}}</span>
+            <span/>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot/></template>`,
+        },
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<span></span>` +
+          `<span>foo</span>` +
+          `<span></span>` +
+          `<!--]-->` +
+          `<!--slot-->`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<span></span>` +
+          `<span>bar</span>` +
+          `<span></span>` +
+          `<!--]-->` +
+          `<!--slot-->`,
+      )
+    })
+
+    test('with multi level anchor insertion', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span/>
+            <span>{{data}}</span>
+            <span/>
+          </components.Child>
+        </template>`,
+        {
+          Child: `
+          <template>
+            <div/>
+              <div/>
+              <slot/>
+              <div/>
+            </div>
+          </template>`,
+        },
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<div></div>` +
+          `<div></div>` +
+          `<!--[-->` +
+          `<span></span>` +
+          `<span>foo</span>` +
+          `<span></span>` +
+          `<!--]-->` +
+          `<!--slot-->` +
+          `<div></div>` +
+          `<!--]-->`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<div></div>` +
+          `<div></div>` +
+          `<!--[-->` +
+          `<span></span>` +
+          `<span>bar</span>` +
+          `<span></span>` +
+          `<!--]-->` +
+          `<!--slot-->` +
+          `<div></div>` +
+          `<!--]-->`,
+      )
+    })
+
+    // problem is next child is incorrect after slot
+    test.todo('mixed slot and text node', async () => {
+      const data = reactive({
+        text: 'foo',
+        msg: 'hi',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.text}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><div><slot/>{{data.msg}}</div></template>`,
+        },
+        data,
+      )
+
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><!--[--><span>foo</span><!--]--><!--slot-->hi</div>"`,
+      )
+    })
+
+    test.todo('mixed slot and element', async () => {
+      const data = reactive({
+        text: 'foo',
+        msg: 'hi',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.text}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><div><slot/><div>{{data.msg}}</div></div></template>`,
+        },
+        data,
+      )
+
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><!--hi--><span>foo</span><!--]--><!--slot--><div>hi</div></div>"`,
+      )
+    })
+
+    // mixed slot and component
+    // mixed slot and fragment component
+    // mixed slot and v-if
+    // mixed slot and v-for
+  })
 
   // test('element with ref', () => {
   //   const el = ref()
index c0727cb5d91e454c1ec3cdc4dde7570b313b45d9..c846cc87247f225e72edb19ea7a60bad8c87c397 100644 (file)
@@ -1,4 +1,4 @@
-import { isArray, isVaporFragmentEndAnchor } from '@vue/shared'
+import { isArray } from '@vue/shared'
 import {
   type VaporComponentInstance,
   isVaporComponent,
@@ -100,10 +100,10 @@ export class DynamicFragment extends VaporFragment {
     } else {
       // find next sibling dynamic fragment end anchor
       const anchor = nextVaporFragmentAnchor(currentHydrationNode!, label)!
-      if (anchor && isVaporFragmentEndAnchor(anchor)) {
+      if (anchor) {
         this.anchor = anchor
       } else if (__DEV__) {
-        // TODO warning
+        // TODO warning, should not happen
         warn(`DynamicFragment anchor not found...`)
       }
     }
index 74296e09466359d9fddc37305b032b4e99741010..093ed7fb082168a6a2e0e416350482bb05667221 100644 (file)
@@ -1,11 +1,22 @@
-import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
+import {
+  EMPTY_OBJ,
+  NO,
+  SLOT_ANCHOR_LABEL,
+  hasOwn,
+  isArray,
+  isFunction,
+} from '@vue/shared'
 import { type Block, type BlockFn, DynamicFragment, insert } from './block'
 import { rawPropsProxyHandlers } from './componentProps'
 import { currentInstance, isRef } from '@vue/runtime-dom'
 import type { LooseRawProps, VaporComponentInstance } from './component'
 import { renderEffect } from './renderEffect'
-import { insertionAnchor, insertionParent } from './insertionState'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
+import { isHydrating } from './dom/hydration'
 
 export type RawSlots = Record<string, VaporSlot> & {
   $?: DynamicSlotSource[]
@@ -94,9 +105,7 @@ export function createSlot(
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
-  if (isHydrating) {
-    locateHydrationNode()
-  }
+  if (!isHydrating) resetInsertionState()
 
   const instance = currentInstance as VaporComponentInstance
   const rawSlots = instance.rawSlots
@@ -115,7 +124,7 @@ export function createSlot(
       fallback,
     )
   } else {
-    fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
+    fragment = new DynamicFragment(SLOT_ANCHOR_LABEL)
     const isDynamicName = isFunction(name)
     const renderSlot = () => {
       const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
index f290954b7f10e254c0d1d830684989211b783ef5..2506a5c8fb25aed2df655f718df20701ccb084b6 100644 (file)
@@ -42,7 +42,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
 }
 
 export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: (isFragment?: boolean) => void
+export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
 
 type Anchor = Comment & {
   // cached matching fragment start to avoid repeated traversal
@@ -94,10 +94,16 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
   } else {
     node = insertionParent ? insertionParent.lastChild : currentHydrationNode
 
+    // if current node is fragment start anchor, find the next one
+    if (node && isComment(node, '[')) {
+      node = node.nextSibling
+    }
     // if the last child is a vapor fragment end anchor, find the previous one
-    if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
-      let previous = node.previousSibling
-      if (previous) node = previous
+    else if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
+      node = node.previousSibling
+      if (__DEV__ && !node) {
+        // TODO warning, should not happen
+      }
     }
 
     if (node && isComment(node, ']')) {
index d0a5223b2ff635b2df39b187f8f4438b01bfff38..03b7402ff922448ddf63032eaf764a8115790819 100644 (file)
@@ -446,7 +446,7 @@ function testRender(type: string, render: typeof renderToString) {
         ).toBe(
           `<div>parent<div class="child">` +
             `<!--[--><span>from slot</span><!--]-->` +
-            `</div></div>`,
+            `<!--slot--></div></div>`,
         )
 
         // test fallback
@@ -461,7 +461,7 @@ function testRender(type: string, render: typeof renderToString) {
             }),
           ),
         ).toBe(
-          `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`,
+          `<div>parent<div class="child"><!--[-->fallback<!--]--><!--slot--></div></div>`,
         )
       })
 
@@ -507,7 +507,7 @@ function testRender(type: string, render: typeof renderToString) {
         ).toBe(
           `<div>parent<div class="child">` +
             `<!--[--><span>from slot</span><!--]-->` +
-            `</div></div>`,
+            `<!--slot--></div></div>`,
         )
       })
 
@@ -525,7 +525,7 @@ function testRender(type: string, render: typeof renderToString) {
         expect(await render(app)).toBe(
           `<div>parent<div class="child">` +
             `<!--[--><span>from slot</span><!--]-->` +
-            `</div></div>`,
+            `<!--slot--></div></div>`,
         )
       })
 
@@ -572,7 +572,7 @@ function testRender(type: string, render: typeof renderToString) {
         })
 
         expect(await render(app)).toBe(
-          `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`,
+          `<div><!--[--><!--[-->hello<!--]--><!--slot--><!--]--><!--slot--></div>`,
         )
       })
 
@@ -593,7 +593,7 @@ function testRender(type: string, render: typeof renderToString) {
 
         expect(await render(app)).toBe(
           // should only have a single fragment
-          `<div><!--[--><!--]--></div>`,
+          `<div><!--[--><!--]--><!--slot--></div>`,
         )
       })
 
@@ -614,7 +614,7 @@ function testRender(type: string, render: typeof renderToString) {
 
         expect(await render(app)).toBe(
           // should only have a single fragment
-          `<div><!--[-->fallback<!--]--></div>`,
+          `<div><!--[-->fallback<!--]--><!--slot--></div>`,
         )
       })
     })
index b685fbfe1abe113acc8b0151871deefa5c3a7f7d..181720c5b364968cee58e5e3348af06bb01adfcb 100644 (file)
@@ -15,7 +15,7 @@ describe('ssr: dynamic component', () => {
         }),
       ),
     ).toBe(
-      `<div><!--[--><span>slot</span><!--]--></div><!--dynamic-component-->`,
+      `<div><!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
     )
   })
 
@@ -63,7 +63,7 @@ describe('ssr: dynamic component', () => {
         }),
       ),
     ).toBe(
-      `<div>test<!--[--><span>slot</span><!--]--></div><!--dynamic-component-->`,
+      `<div>test<!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
     )
   })
 
index 4ceb865fb50faefdbb8cf9c674fa36ee887c9f43..c4135e498b78e73084668970da612641e6f6316d 100644 (file)
@@ -68,7 +68,9 @@ describe('ssr: scopedId runtime behavior', () => {
     }
 
     const result = await renderToString(createApp(Comp))
-    expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
+    expect(result).toBe(
+      `<!--[--><div parent wrapper-s child></div><!--]--><!--slot-->`,
+    )
   })
 
   // #2892
@@ -150,8 +152,8 @@ describe('ssr: scopedId runtime behavior', () => {
     const result = await renderToString(createApp(Root))
     expect(result).toBe(
       `<div class="wrapper" root slotted wrapper>` +
-        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
-        `</div>`,
+        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
+        `<!--slot--></div>`,
     )
   })
 
@@ -265,8 +267,8 @@ describe('ssr: scopedId runtime behavior', () => {
     const result = await renderToString(createApp(Root))
     expect(result).toBe(
       `<div class="wrapper" root slotted wrapper>` +
-        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
-        `</div>`,
+        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
+        `<!--slot--></div>`,
     )
   })
 })
index 0e3e3535641d73fb28627a5bc7cb28ec1b45bac6..d17e34bc7c0b54477c855d79cb4a3df7253374d0 100644 (file)
@@ -16,7 +16,7 @@ describe('ssr: slot', () => {
           template: `<one>hello</one>`,
         }),
       ),
-    ).toBe(`<div><!--[-->hello<!--]--></div>`)
+    ).toBe(`<div><!--[-->hello<!--]--><!--slot--></div>`)
   })
 
   test('element slot', async () => {
@@ -27,7 +27,7 @@ describe('ssr: slot', () => {
           template: `<one><div>hi</div></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><div>hi</div><!--]--></div>`)
+    ).toBe(`<div><!--[--><div>hi</div><!--]--><!--slot--></div>`)
   })
 
   test('empty slot', async () => {
@@ -42,7 +42,7 @@ describe('ssr: slot', () => {
           template: `<one><template v-if="false"/></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--]--></div>`)
+    ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
   })
 
   test('empty slot (manual comments)', async () => {
@@ -57,7 +57,7 @@ describe('ssr: slot', () => {
           template: `<one><!--hello--></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--]--></div>`)
+    ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
   })
 
   test('empty slot (multi-line comments)', async () => {
@@ -72,7 +72,7 @@ describe('ssr: slot', () => {
           template: `<one><!--he\nllo--></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--]--></div>`)
+    ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
   })
 
   test('multiple elements', async () => {
@@ -83,7 +83,7 @@ describe('ssr: slot', () => {
           template: `<one><div>one</div><div>two</div></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--></div>`)
+    ).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--><!--slot--></div>`)
   })
 
   test('fragment slot (template v-if)', async () => {
@@ -94,7 +94,9 @@ describe('ssr: slot', () => {
           template: `<one><template v-if="true">hello</template></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--></div>`)
+    ).toBe(
+      `<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--><!--slot--></div>`,
+    )
   })
 
   test('fragment slot (template v-if + multiple elements)', async () => {
@@ -106,7 +108,7 @@ describe('ssr: slot', () => {
         }),
       ),
     ).toBe(
-      `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--></div>`,
+      `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--><!--slot--></div>`,
     )
   })
 
@@ -171,7 +173,7 @@ describe('ssr: slot', () => {
         }),
       ),
     ).toBe(
-      `<button><!--[--><div><!--[--><!--]--></div><!--]--></button><!--dynamic-component-->`,
+      `<button><!--[--><div><!--[--><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
     )
 
     expect(
@@ -189,7 +191,7 @@ describe('ssr: slot', () => {
         }),
       ),
     ).toBe(
-      `<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button><!--dynamic-component-->`,
+      `<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
     )
 
     expect(
index 19aa4ce63b76c1a1118fd13a4dc32c6bae35533f..b8a57ae8d96a0b860df4a019964353ca97e69ccb 100644 (file)
@@ -5,7 +5,7 @@ import {
   type SSRBufferItem,
   renderVNodeChildren,
 } from '../render'
-import { isArray } from '@vue/shared'
+import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared'
 
 const { ensureValidVNode } = ssrUtils
 
@@ -37,7 +37,7 @@ export function ssrRenderSlot(
     parentComponent,
     slotScopeId,
   )
-  push(`<!--]-->`)
+  push(`<!--]--><!--${SLOT_ANCHOR_LABEL}-->`)
 }
 
 export function ssrRenderSlotInner(