wip: hydration for slots
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details

This commit is contained in:
daiwei 2025-04-27 12:01:42 +08:00
parent 2f002649d1
commit 700f49ee96
15 changed files with 441 additions and 99 deletions

View File

@ -246,7 +246,7 @@ describe('ssr: components', () => {
_ssrRenderList(list, (i) => { _ssrRenderList(list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--></div>\`) _push(\`<!--]--><!--for--></div>\`)
_push(\`<!--if-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
@ -270,7 +270,7 @@ describe('ssr: components', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--></div>\`) _push(\`<!--]--><!--for--></div>\`)
_push(\`<!--if-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)

View File

@ -10,7 +10,7 @@ describe('ssr: v-for', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
}" }"
`) `)
}) })
@ -25,7 +25,7 @@ describe('ssr: v-for', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div>foo<span>bar</span></div>\`) _push(\`<div>foo<span>bar</span></div>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
}" }"
`) `)
}) })
@ -51,9 +51,9 @@ describe('ssr: v-for', () => {
_ssrInterpolate(j) _ssrInterpolate(j)
}</div>\`) }</div>\`)
}) })
_push(\`<!--]--></div>\`) _push(\`<!--]--><!--for--></div>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
}" }"
`) `)
}) })
@ -68,7 +68,7 @@ describe('ssr: v-for', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`) _push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
}" }"
`) `)
}) })
@ -85,7 +85,7 @@ describe('ssr: v-for', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span>\${_ssrInterpolate(i)}</span>\`) _push(\`<span>\${_ssrInterpolate(i)}</span>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
}" }"
`) `)
}) })
@ -107,7 +107,7 @@ describe('ssr: v-for', () => {
_ssrInterpolate(i + 1) _ssrInterpolate(i + 1)
}</span><!--]-->\`) }</span><!--]-->\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
}" }"
`) `)
}) })
@ -127,7 +127,7 @@ describe('ssr: v-for', () => {
_ssrRenderList(_ctx.list, ({ foo }, index) => { _ssrRenderList(_ctx.list, ({ foo }, index) => {
_push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`) _push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
}" }"
`) `)
}) })

View File

@ -147,7 +147,7 @@ describe('ssr: v-if', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]--><!--for-->\`)
_push(\`<!--if-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)

View File

@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : "" : _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
}></option>\`) }></option>\`)
}) })
_push(\`<!--]--></select></div>\`) _push(\`<!--]--><!--for--></select></div>\`)
}" }"
`) `)

View File

@ -381,6 +381,7 @@ function processChildrenDynamicInfo(
* <Comp/> // Dynamic node -> should be wrapped * <Comp/> // Dynamic node -> should be wrapped
* <Comp/> // Dynamic node -> should NOT be wrapped * <Comp/> // Dynamic node -> should NOT be wrapped
* <element/> // Static node * <element/> // Static node
* </element>
*/ */
function shouldProcessChildAsDynamic( function shouldProcessChildAsDynamic(
parent: { tag?: string; children: TemplateChildNode[] }, parent: { tag?: string; children: TemplateChildNode[] },

View File

@ -49,8 +49,7 @@ export function ssrProcessFor(
) )
if (!disableNestedFragments) { if (!disableNestedFragments) {
context.pushStringPart(`<!--]-->`) context.pushStringPart(`<!--]-->`)
} else {
// add anchor for non-fragment v-for
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
} }
// v-for anchor for vapor hydration
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
} }

View File

@ -125,7 +125,7 @@ export function createHydrationFunctions(
let n = next(node) let n = next(node)
// skip if: // skip if:
// - dynamic anchors (`<!--[[-->`, `<!--][-->`) // - dynamic anchors (`<!--[[-->`, `<!--][-->`)
// - dynamic fragment end anchors (e.g. `<!--if-->`, `<!--for-->`) // - vapor fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
if (n && (isDynamicAnchor(n) || isVaporFragmentEndAnchor(n))) { if (n && (isDynamicAnchor(n) || isVaporFragmentEndAnchor(n))) {
n = next(n) n = next(n)
} }

View File

@ -4,7 +4,12 @@ import { compileScript, parse } from '@vue/compiler-sfc'
import * as runtimeVapor from '../src' import * as runtimeVapor from '../src'
import * as runtimeDom from '@vue/runtime-dom' import * as runtimeDom from '@vue/runtime-dom'
import * as VueServerRenderer from '@vue/server-renderer' import * as VueServerRenderer from '@vue/server-renderer'
import { DYNAMIC_COMPONENT_ANCHOR_LABEL, IF_ANCHOR_LABEL } from '@vue/shared' import {
DYNAMIC_COMPONENT_ANCHOR_LABEL,
FOR_ANCHOR_LABEL,
IF_ANCHOR_LABEL,
SLOT_ANCHOR_LABEL,
} from '@vue/shared'
const Vue = { ...runtimeDom, ...runtimeVapor } const Vue = { ...runtimeDom, ...runtimeVapor }
@ -1438,6 +1443,9 @@ describe('Vapor Mode hydration', () => {
}) })
describe('for', () => { describe('for', () => {
const forAnchorLabel = FOR_ANCHOR_LABEL
const slotAnchorLabel = SLOT_ANCHOR_LABEL
test('basic v-for', async () => { test('basic v-for', async () => {
const { container, data } = await testHydration( const { container, data } = await testHydration(
`<template> `<template>
@ -1454,7 +1462,7 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<!--]-->` + `<!--]--><!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
@ -1466,8 +1474,9 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<span>d</span>` +
`<!--]-->` + `<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
}) })
@ -1483,13 +1492,23 @@ describe('Vapor Mode hydration', () => {
ref(['a', 'b', 'c']), ref(['a', 'b', 'c']),
) )
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<div><!--[--><span>a</span><span>b</span><span>c</span><!--]--></div>`, `<div>` +
`<!--[-->` +
`<span>a</span><span>b</span><span>c</span>` +
`<!--]--><!--${forAnchorLabel}-->` +
`</div>`,
) )
data.value.push('d') data.value.push('d')
await nextTick() await nextTick()
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<div><!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]--></div>`, `<div>` +
`<!--[-->` +
`<span>a</span><span>b</span><span>c</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`</div>`,
) )
}) })
@ -1512,7 +1531,7 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<!--]-->` + `<!--]--><!--${forAnchorLabel}-->` +
`<span></span>` + `<span></span>` +
`</div>`, `</div>`,
) )
@ -1526,8 +1545,9 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<span>d</span>` +
`<!--]-->` + `<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` + `<span></span>` +
`</div>`, `</div>`,
) )
@ -1540,8 +1560,9 @@ describe('Vapor Mode hydration', () => {
`<!--[-->` + `<!--[-->` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<span>d</span>` +
`<!--]-->` + `<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` + `<span></span>` +
`</div>`, `</div>`,
) )
@ -1567,12 +1588,12 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<!--]-->` + `<!--]--><!--${forAnchorLabel}-->` +
`<!--[-->` + `<!--[-->` +
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<!--]-->` + `<!--]--><!--${forAnchorLabel}-->` +
`<span></span>` + `<span></span>` +
`</div>`, `</div>`,
) )
@ -1586,14 +1607,16 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<span>d</span>` +
`<!--]-->` + `<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<!--[-->` + `<!--[-->` +
`<span>a</span>` + `<span>a</span>` +
`<span>b</span>` + `<span>b</span>` +
`<span>c</span>` + `<span>c</span>` +
`<span>d</span>` +
`<!--]-->` + `<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` + `<span></span>` +
`</div>`, `</div>`,
) )
@ -1605,12 +1628,14 @@ describe('Vapor Mode hydration', () => {
`<span></span>` + `<span></span>` +
`<!--[-->` + `<!--[-->` +
`<span>c</span>` + `<span>c</span>` +
`<span>d</span>` +
`<!--]-->` + `<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<!--[-->` + `<!--[-->` +
`<span>c</span>` + `<span>c</span>` +
`<span>d</span>` +
`<!--]-->` + `<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` + `<span></span>` +
`</div>`, `</div>`,
) )
@ -1635,7 +1660,7 @@ describe('Vapor Mode hydration', () => {
`<div>comp</div>` + `<div>comp</div>` +
`<div>comp</div>` + `<div>comp</div>` +
`<div>comp</div>` + `<div>comp</div>` +
`<!--]-->` + `<!--]--><!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
@ -1647,8 +1672,9 @@ describe('Vapor Mode hydration', () => {
`<div>comp</div>` + `<div>comp</div>` +
`<div>comp</div>` + `<div>comp</div>` +
`<div>comp</div>` + `<div>comp</div>` +
`<div>comp</div>` +
`<!--]-->` + `<!--]-->` +
`<div>comp</div>` +
`<!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
}) })
@ -1670,10 +1696,10 @@ describe('Vapor Mode hydration', () => {
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<div>` + `<div>` +
`<!--[-->` + `<!--[-->` +
`<!--[--><span>a</span><!--]--><!--slot-->` + `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>b</span><!--]--><!--slot-->` + `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>c</span><!--]--><!--slot-->` + `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--]-->` + `<!--]--><!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
@ -1682,11 +1708,12 @@ describe('Vapor Mode hydration', () => {
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<div>` + `<div>` +
`<!--[-->` + `<!--[-->` +
`<!--[--><span>a</span><!--]--><!--slot-->` + `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>b</span><!--]--><!--slot-->` + `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>c</span><!--]--><!--slot-->` + `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
`<span>d</span><!--slot-->` +
`<!--]-->` + `<!--]-->` +
`<span>d</span><!--${slotAnchorLabel}-->` +
`<!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
}) })
@ -1709,7 +1736,7 @@ describe('Vapor Mode hydration', () => {
`<!--[--><div>foo</div>-bar-<!--]-->` + `<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--[--><div>foo</div>-bar-<!--]-->` + `<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--[--><div>foo</div>-bar-<!--]-->` + `<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--]-->` + `<!--]--><!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
@ -1721,18 +1748,17 @@ describe('Vapor Mode hydration', () => {
`<!--[--><div>foo</div>-bar-<!--]-->` + `<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--[--><div>foo</div>-bar-<!--]-->` + `<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--[--><div>foo</div>-bar-<!--]-->` + `<!--[--><div>foo</div>-bar-<!--]-->` +
`<div>foo</div>-bar-` +
`<!--]-->` + `<!--]-->` +
`<div>foo</div>-bar-` +
`<!--${forAnchorLabel}-->` +
`</div>`, `</div>`,
) )
}) })
// TODO wait for vapor TransitionGroup support
// v-for inside TransitionGroup does not render as a fragment
test.todo('v-for in TransitionGroup', async () => {})
}) })
describe('slots', () => { describe('slots', () => {
const slotAnchorLabel = SLOT_ANCHOR_LABEL
const forAnchorLabel = FOR_ANCHOR_LABEL
test('basic slot', async () => { test('basic slot', async () => {
const { data, container } = await testHydration( const { data, container } = await testHydration(
`<template> `<template>
@ -1745,13 +1771,13 @@ describe('Vapor Mode hydration', () => {
}, },
) )
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<!--[--><span>foo</span><!--]--><!--slot-->`, `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
) )
data.value = 'bar' data.value = 'bar'
await nextTick() await nextTick()
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<!--[--><span>bar</span><!--]--><!--slot-->`, `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
) )
}) })
@ -1769,13 +1795,13 @@ describe('Vapor Mode hydration', () => {
}, },
) )
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<!--[--><span>foo</span><!--]--><!--slot-->`, `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
) )
data.value = 'bar' data.value = 'bar'
await nextTick() await nextTick()
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<!--[--><span>bar</span><!--]--><!--slot-->`, `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
) )
}) })
@ -1793,12 +1819,14 @@ describe('Vapor Mode hydration', () => {
}, },
) )
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<!--[--><span>foo</span><!--]--><!--slot-->`, `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
) )
data.value = false data.value = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe(`<!--[--><!--]--><!--slot-->`) expect(container.innerHTML).toBe(
`<!--[--><!--]--><!--${slotAnchorLabel}-->`,
)
}) })
test('named slot with v-if and v-for', async () => { test('named slot with v-if and v-for', async () => {
@ -1821,15 +1849,15 @@ describe('Vapor Mode hydration', () => {
) )
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<!--[-->` + `<!--[-->` +
`<!--[--><span>a</span><span>b</span><span>c</span><!--]-->` + `<!--[--><span>a</span><span>b</span><span>c</span><!--]--><!--${forAnchorLabel}-->` +
`<!--]-->` + `<!--]-->` +
`<!--slot-->`, `<!--${slotAnchorLabel}-->`,
) )
data.show = false data.show = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<!--[--><!--[--><!--]--><!--]--><!--slot-->`, `<!--[--><!--[--><!--]--><!--]--><!--${slotAnchorLabel}-->`,
) )
}) })
@ -1852,7 +1880,7 @@ describe('Vapor Mode hydration', () => {
`<span>foo</span>` + `<span>foo</span>` +
`<span></span>` + `<span></span>` +
`<!--]-->` + `<!--]-->` +
`<!--slot-->`, `<!--${slotAnchorLabel}-->`,
) )
data.value = 'bar' data.value = 'bar'
@ -1863,7 +1891,7 @@ describe('Vapor Mode hydration', () => {
`<span>bar</span>` + `<span>bar</span>` +
`<span></span>` + `<span></span>` +
`<!--]-->` + `<!--]-->` +
`<!--slot-->`, `<!--${slotAnchorLabel}-->`,
) )
}) })
@ -1896,7 +1924,7 @@ describe('Vapor Mode hydration', () => {
`<span>foo</span>` + `<span>foo</span>` +
`<span></span>` + `<span></span>` +
`<!--]-->` + `<!--]-->` +
`<!--slot-->` + `<!--${slotAnchorLabel}-->` +
`<div></div>` + `<div></div>` +
`<!--]-->`, `<!--]-->`,
) )
@ -1912,14 +1940,13 @@ describe('Vapor Mode hydration', () => {
`<span>bar</span>` + `<span>bar</span>` +
`<span></span>` + `<span></span>` +
`<!--]-->` + `<!--]-->` +
`<!--slot-->` + `<!--${slotAnchorLabel}-->` +
`<div></div>` + `<div></div>` +
`<!--]-->`, `<!--]-->`,
) )
}) })
// problem is next child is incorrect after slot test('mixed slot and text node', async () => {
test.todo('mixed slot and text node', async () => {
const data = reactive({ const data = reactive({
text: 'foo', text: 'foo',
msg: 'hi', msg: 'hi',
@ -1937,11 +1964,11 @@ describe('Vapor Mode hydration', () => {
) )
expect(container.innerHTML).toMatchInlineSnapshot( expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><!--[--><span>foo</span><!--]--><!--slot-->hi</div>"`, `"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->hi</div>"`,
) )
}) })
test.todo('mixed slot and element', async () => { test('mixed slot and element', async () => {
const data = reactive({ const data = reactive({
text: 'foo', text: 'foo',
msg: 'hi', msg: 'hi',
@ -1959,14 +1986,272 @@ describe('Vapor Mode hydration', () => {
) )
expect(container.innerHTML).toMatchInlineSnapshot( expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><!--hi--><span>foo</span><!--]--><!--slot--><div>hi</div></div>"`, `"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}--><div>hi</div></div>"`,
) )
}) })
// mixed slot and component test('mixed slot and component', async () => {
// mixed slot and fragment component const data = reactive({
// mixed slot and v-if msg1: 'foo',
// mixed slot and v-for msg2: 'bar',
})
const { container } = await testHydration(
`<template>
<components.Child>
<span>{{data.msg1}}</span>
</components.Child>
</template>`,
{
Child: `
<template>
<div>
<components.Child2/>
<slot/>
<components.Child2/>
</div>
</template>`,
Child2: `
<template>
<div>{{data.msg2}}</div>
</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<div>` +
`<div>bar</div>` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<div>bar</div>` +
`</div>`,
)
data.msg2 = 'hello'
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<div>hello</div>` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<div>hello</div>` +
`</div>`,
)
})
test('mixed slot and fragment component', async () => {
const data = reactive({
msg1: 'foo',
msg2: 'bar',
})
const { container } = await testHydration(
`<template>
<components.Child>
<span>{{data.msg1}}</span>
</components.Child>
</template>`,
{
Child: `
<template>
<div>
<components.Child2/>
<slot/>
<components.Child2/>
</div>
</template>`,
Child2: `
<template>
<div>{{data.msg1}}</div> {{data.msg2}}
</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<div>` +
`<!--[--><div>foo</div> bar<!--]-->` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><div>foo</div> bar<!--]-->` +
`</div>`,
)
data.msg1 = 'hello'
data.msg2 = 'vapor'
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<!--[--><div>hello</div> vapor<!--]-->` +
`<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><div>hello</div> vapor<!--]-->` +
`</div>`,
)
})
test('mixed slot and v-if', async () => {
const data = reactive({
show: true,
msg: 'foo',
})
const { container } = await testHydration(
`<template>
<components.Child>
<span>{{data.msg}}</span>
</components.Child>
</template>`,
{
Child: `
<template>
<div v-if="data.show">{{data.msg}}</div>
<slot/>
<div v-if="data.show">{{data.msg}}</div>
</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<!--[-->` +
`<div>foo</div><!--if-->` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<div>foo</div><!--if-->` +
`<!--]-->`,
)
data.show = false
await nextTick()
expect(container.innerHTML).toBe(
`<!--[-->` +
`<!--if-->` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--if-->` +
`<!--]-->`,
)
})
test('mixed slot and v-for', async () => {
const data = reactive({
items: ['a', 'b', 'c'],
msg: 'foo',
})
const { container } = await testHydration(
`<template>
<components.Child>
<span>{{data.msg}}</span>
</components.Child>
</template>`,
{
Child: `
<template>
<div v-for="item in data.items" :key="item">{{item}}</div>
<slot/>
<div v-for="item in data.items" :key="item">{{item}}</div>
</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<!--[-->` +
`<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` +
`<!--]-->`,
)
data.items.push('d')
await nextTick()
expect(container.innerHTML).toBe(
`<!--[-->` +
`<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` +
`<!--]-->`,
)
})
test('consecutive slots', async () => {
const data = reactive({
msg1: 'foo',
msg2: 'bar',
})
const { container } = await testHydration(
`<template>
<components.Child>
<span>{{data.msg1}}</span>
<template #bar>
<span>{{data.msg2}}</span>
</template>
</components.Child>
</template>`,
{
Child: `<template><slot/><slot name="bar"/></template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<!--[-->` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--]-->`,
)
data.msg1 = 'hello'
data.msg2 = 'vapor'
await nextTick()
expect(container.innerHTML).toBe(
`<!--[-->` +
`<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--]-->`,
)
})
test('consecutive slots with anchor insertion', async () => {
const data = reactive({
msg1: 'foo',
msg2: 'bar',
})
const { container } = await testHydration(
`<template>
<components.Child>
<span>{{data.msg1}}</span>
<template #bar>
<span>{{data.msg2}}</span>
</template>
</components.Child>
</template>`,
{
Child: `<template>
<div>
<span/>
<slot/>
<slot name="bar"/>
<span/>
</div>
</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
data.msg1 = 'hello'
data.msg2 = 'vapor'
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
})
}) })
// test('element with ref', () => { // test('element with ref', () => {

View File

@ -19,7 +19,7 @@ import {
import { import {
createComment, createComment,
createTextNode, createTextNode,
nextVaporFragmentAnchor, findVaporFragmentAnchor,
} from './dom/node' } from './dom/node'
import { import {
type Block, type Block,
@ -34,7 +34,6 @@ import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags' import { VaporVForFlags } from '../../shared/src/vaporFlags'
import { import {
currentHydrationNode, currentHydrationNode,
isComment,
isHydrating, isHydrating,
locateHydrationNode, locateHydrationNode,
} from './dom/hydration' } from './dom/hydration'
@ -99,15 +98,20 @@ export const createFor = (
let oldBlocks: ForBlock[] = [] let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[] let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null let parent: ParentNode | undefined | null
const parentAnchor = isHydrating let parentAnchor: Node
? // Use fragment end anchor if available, otherwise use the specific for anchor. if (isHydrating) {
nextVaporFragmentAnchor( parentAnchor = findVaporFragmentAnchor(
currentHydrationNode!, currentHydrationNode!,
isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL, FOR_ANCHOR_LABEL,
)! )!
: __DEV__ if (__DEV__ && !parentAnchor) {
? createComment('for') // TODO warn, should not happen
: createTextNode() warn(`createFor anchor not found...`)
}
} else {
parentAnchor = __DEV__ ? createComment('for') : createTextNode()
}
const frag = new VaporFragment(oldBlocks) const frag = new VaporFragment(oldBlocks)
const instance = currentInstance! const instance = currentInstance!
const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE

View File

@ -8,7 +8,7 @@ import {
import { import {
createComment, createComment,
createTextNode, createTextNode,
nextVaporFragmentAnchor, findVaporFragmentAnchor,
} from './dom/node' } from './dom/node'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
import { import {
@ -99,7 +99,7 @@ export class DynamicFragment extends VaporFragment {
this.anchor = currentHydrationNode this.anchor = currentHydrationNode
} else { } else {
// find next sibling dynamic fragment end anchor // find next sibling dynamic fragment end anchor
const anchor = nextVaporFragmentAnchor(currentHydrationNode!, label)! const anchor = findVaporFragmentAnchor(currentHydrationNode!, label)!
if (anchor) { if (anchor) {
this.anchor = anchor this.anchor = anchor
} else if (__DEV__) { } else if (__DEV__) {

View File

@ -59,7 +59,11 @@ import {
} from './componentSlots' } from './componentSlots'
import { hmrReload, hmrRerender } from './hmr' import { hmrReload, hmrRerender } from './hmr'
import { isHydrating, locateHydrationNode } from './dom/hydration' import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState' import {
insertionAnchor,
insertionParent,
resetInsertionState,
} from './insertionState'
export { currentInstance } from '@vue/runtime-dom' export { currentInstance } from '@vue/runtime-dom'
@ -142,6 +146,8 @@ export function createComponent(
const _insertionAnchor = insertionAnchor const _insertionAnchor = insertionAnchor
if (isHydrating) { if (isHydrating) {
locateHydrationNode() locateHydrationNode()
} else {
resetInsertionState()
} }
// vdom interop enabled and component is not an explicit vapor component // vdom interop enabled and component is not an explicit vapor component

View File

@ -6,7 +6,7 @@ import {
setInsertionState, setInsertionState,
} from '../insertionState' } from '../insertionState'
import { import {
child, _child,
disableHydrationNodeLookup, disableHydrationNodeLookup,
enableHydrationNodeLookup, enableHydrationNodeLookup,
next, next,
@ -28,6 +28,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
if (!isOptimized) { if (!isOptimized) {
// optimize anchor cache lookup // optimize anchor cache lookup
;(Comment.prototype as any).$fs = undefined ;(Comment.prototype as any).$fs = undefined
;(Node.prototype as any).$nc = undefined
isOptimized = true isOptimized = true
} }
enableHydrationNodeLookup() enableHydrationNodeLookup()
@ -87,19 +88,17 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
let node: Node | null let node: Node | null
// prepend / firstChild // prepend / firstChild
if (insertionAnchor === 0) { if (insertionAnchor === 0) {
node = child(insertionParent!) node = _child(insertionParent!)
} else if (insertionAnchor) { } else if (insertionAnchor) {
// for dynamic children, use insertionAnchor as the node // for dynamic children, use insertionAnchor as the node
node = insertionAnchor node = insertionAnchor
} else { } else {
node = insertionParent ? insertionParent.lastChild : currentHydrationNode node = insertionParent
? insertionParent.$nc || 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 the last child is a vapor fragment end anchor, find the previous one
else if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) { if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
node = node.previousSibling node = node.previousSibling
if (__DEV__ && !node) { if (__DEV__ && !node) {
// TODO warning, should not happen // TODO warning, should not happen
@ -135,6 +134,10 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
} }
} }
} }
if (insertionParent && node) {
insertionParent.$nc = node!.previousSibling
}
} }
if (__DEV__ && !node) { if (__DEV__ && !node) {

View File

@ -22,10 +22,42 @@ export function querySelector(selectors: string): Element | null {
} }
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function child(node: ParentNode): Node { export function _child(node: ParentNode): Node {
return node.firstChild! return node.firstChild!
} }
/*! #__NO_SIDE_EFFECTS__ */
export function __child(node: ParentNode): Node {
/**
* During hydration, the first child of a node not be the expected
* if the first child is slot
*
* for template code: `div><slot />{{ data }}</div>`
* - slot: 'slot',
* - data: 'hi',
*
* client side:
* const n2 = _template("<div> </div>")()
* const n1 = _child(n2) -> the text node
* _setInsertionState(n2, 0) -> slot fragment
*
* during hydration:
* const n2 = _template("<div><!--[-->slot<!--]--><!--slot-->Hi</div>")()
* const n1 = _child(n2) -> should be `Hi` instead of the slot fragment
* _setInsertionState(n2, 0) -> slot fragment
*/
let n = node.firstChild!
if (isComment(n, '[')) {
n = locateEndAnchor(n)!.nextSibling!
}
while (n && isVaporFragmentEndAnchor(n)) {
n = n.nextSibling!
}
return n
}
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function _nthChild(node: Node, i: number): Node { export function _nthChild(node: Node, i: number): Node {
return node.childNodes[i] return node.childNodes[i]
@ -56,9 +88,13 @@ export function __next(node: Node): Node {
return n return n
} }
type ChildFn = (node: ParentNode) => Node
type NextFn = (node: Node) => Node type NextFn = (node: Node) => Node
type NthChildFn = (node: Node, i: number) => Node type NthChildFn = (node: Node, i: number) => Node
interface DelegatedChildFunction extends ChildFn {
impl: ChildFn
}
interface DelegatedNextFunction extends NextFn { interface DelegatedNextFunction extends NextFn {
impl: NextFn impl: NextFn
} }
@ -66,6 +102,12 @@ interface DelegatedNthChildFunction extends NthChildFn {
impl: NthChildFn impl: NthChildFn
} }
/*! #__NO_SIDE_EFFECTS__ */
export const child: DelegatedChildFunction = node => {
return child.impl(node)
}
child.impl = _child
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export const next: DelegatedNextFunction = node => { export const next: DelegatedNextFunction = node => {
return next.impl(node) return next.impl(node)
@ -90,11 +132,13 @@ nthChild.impl = _nthChild
// of `next` and `nthChild`. After hydration is complete, their implementations // of `next` and `nthChild`. After hydration is complete, their implementations
// are restored to the original versions. // are restored to the original versions.
export function enableHydrationNodeLookup(): void { export function enableHydrationNodeLookup(): void {
child.impl = __child
next.impl = __next next.impl = __next
nthChild.impl = __nthChild nthChild.impl = __nthChild
} }
export function disableHydrationNodeLookup(): void { export function disableHydrationNodeLookup(): void {
child.impl = _child
next.impl = _next next.impl = _next
nthChild.impl = _nthChild nthChild.impl = _nthChild
} }
@ -112,15 +156,10 @@ function isNonHydrationNode(node: Node) {
) )
} }
export function nextVaporFragmentAnchor( export function findVaporFragmentAnchor(
node: Node, node: Node,
anchorLabel: string, anchorLabel: string,
): Comment | null { ): Comment | null {
node = handleWrappedNode(node)
if (isComment(node, anchorLabel)) {
return node as Comment
}
let n = node.nextSibling let n = node.nextSibling
while (n) { while (n) {
if (isComment(n, anchorLabel)) return n if (isComment(n, anchorLabel)) return n

View File

@ -1,4 +1,9 @@
export let insertionParent: ParentNode | undefined export let insertionParent:
| (ParentNode & {
// the next child node to be hydrated
$nc?: Node | null
})
| undefined
export let insertionAnchor: Node | 0 | undefined export let insertionAnchor: Node | 0 | undefined
/** /**

View File

@ -104,7 +104,7 @@ export function ssrRenderSlotInner(
if ( if (
transition && transition &&
slotBuffer[0] === '<!--[-->' && slotBuffer[0] === '<!--[-->' &&
slotBuffer[end - 1] === '<!--]-->' (slotBuffer[end - 1] as string).startsWith('<!--]-->')
) { ) {
start++ start++
end-- end--