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

View File

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

View File

@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
}></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 NOT be wrapped
* <element/> // Static node
* </element>
*/
function shouldProcessChildAsDynamic(
parent: { tag?: string; children: TemplateChildNode[] },

View File

@ -49,8 +49,7 @@ export function ssrProcessFor(
)
if (!disableNestedFragments) {
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)
// skip if:
// - dynamic anchors (`<!--[[-->`, `<!--][-->`)
// - dynamic fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
// - vapor fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
if (n && (isDynamicAnchor(n) || isVaporFragmentEndAnchor(n))) {
n = next(n)
}

View File

@ -4,7 +4,12 @@ import { compileScript, parse } from '@vue/compiler-sfc'
import * as runtimeVapor from '../src'
import * as runtimeDom from '@vue/runtime-dom'
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 }
@ -1438,6 +1443,9 @@ describe('Vapor Mode hydration', () => {
})
describe('for', () => {
const forAnchorLabel = FOR_ANCHOR_LABEL
const slotAnchorLabel = SLOT_ANCHOR_LABEL
test('basic v-for', async () => {
const { container, data } = await testHydration(
`<template>
@ -1454,7 +1462,7 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`<!--]--><!--${forAnchorLabel}-->` +
`</div>`,
)
@ -1466,8 +1474,9 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`</div>`,
)
})
@ -1483,13 +1492,23 @@ describe('Vapor Mode hydration', () => {
ref(['a', 'b', 'c']),
)
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')
await nextTick()
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>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`<!--]--><!--${forAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
@ -1526,8 +1545,9 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
@ -1540,8 +1560,9 @@ describe('Vapor Mode hydration', () => {
`<!--[-->` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
@ -1567,12 +1588,12 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`<!--]--><!--${forAnchorLabel}-->` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`<!--]--><!--${forAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
@ -1586,14 +1607,16 @@ describe('Vapor Mode hydration', () => {
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
@ -1605,12 +1628,14 @@ describe('Vapor Mode hydration', () => {
`<span></span>` +
`<!--[-->` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<!--[-->` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span>d</span>` +
`<!--${forAnchorLabel}-->` +
`<span></span>` +
`</div>`,
)
@ -1635,7 +1660,7 @@ describe('Vapor Mode hydration', () => {
`<div>comp</div>` +
`<div>comp</div>` +
`<div>comp</div>` +
`<!--]-->` +
`<!--]--><!--${forAnchorLabel}-->` +
`</div>`,
)
@ -1647,8 +1672,9 @@ describe('Vapor Mode hydration', () => {
`<div>comp</div>` +
`<div>comp</div>` +
`<div>comp</div>` +
`<div>comp</div>` +
`<!--]-->` +
`<div>comp</div>` +
`<!--${forAnchorLabel}-->` +
`</div>`,
)
})
@ -1670,10 +1696,10 @@ describe('Vapor Mode hydration', () => {
expect(container.innerHTML).toBe(
`<div>` +
`<!--[-->` +
`<!--[--><span>a</span><!--]--><!--slot-->` +
`<!--[--><span>b</span><!--]--><!--slot-->` +
`<!--[--><span>c</span><!--]--><!--slot-->` +
`<!--]-->` +
`<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--]--><!--${forAnchorLabel}-->` +
`</div>`,
)
@ -1682,11 +1708,12 @@ describe('Vapor Mode hydration', () => {
expect(container.innerHTML).toBe(
`<div>` +
`<!--[-->` +
`<!--[--><span>a</span><!--]--><!--slot-->` +
`<!--[--><span>b</span><!--]--><!--slot-->` +
`<!--[--><span>c</span><!--]--><!--slot-->` +
`<span>d</span><!--slot-->` +
`<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
`<!--]-->` +
`<span>d</span><!--${slotAnchorLabel}-->` +
`<!--${forAnchorLabel}-->` +
`</div>`,
)
})
@ -1709,7 +1736,7 @@ describe('Vapor Mode hydration', () => {
`<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--[--><div>foo</div>-bar-<!--]-->` +
`<!--]-->` +
`<!--]--><!--${forAnchorLabel}-->` +
`</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-` +
`<!--${forAnchorLabel}-->` +
`</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', () => {
const slotAnchorLabel = SLOT_ANCHOR_LABEL
const forAnchorLabel = FOR_ANCHOR_LABEL
test('basic slot', async () => {
const { data, container } = await testHydration(
`<template>
@ -1745,13 +1771,13 @@ describe('Vapor Mode hydration', () => {
},
)
expect(container.innerHTML).toBe(
`<!--[--><span>foo</span><!--]--><!--slot-->`,
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<!--[--><span>bar</span><!--]--><!--slot-->`,
`<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
)
})
@ -1769,13 +1795,13 @@ describe('Vapor Mode hydration', () => {
},
)
expect(container.innerHTML).toBe(
`<!--[--><span>foo</span><!--]--><!--slot-->`,
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<!--[--><span>bar</span><!--]--><!--slot-->`,
`<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
)
})
@ -1793,12 +1819,14 @@ describe('Vapor Mode hydration', () => {
},
)
expect(container.innerHTML).toBe(
`<!--[--><span>foo</span><!--]--><!--slot-->`,
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
)
data.value = false
await nextTick()
expect(container.innerHTML).toBe(`<!--[--><!--]--><!--slot-->`)
expect(container.innerHTML).toBe(
`<!--[--><!--]--><!--${slotAnchorLabel}-->`,
)
})
test('named slot with v-if and v-for', async () => {
@ -1821,15 +1849,15 @@ describe('Vapor Mode hydration', () => {
)
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
await nextTick()
expect(container.innerHTML).toBe(
`<!--[--><!--[--><!--]--><!--]--><!--slot-->`,
`<!--[--><!--[--><!--]--><!--]--><!--${slotAnchorLabel}-->`,
)
})
@ -1852,7 +1880,7 @@ describe('Vapor Mode hydration', () => {
`<span>foo</span>` +
`<span></span>` +
`<!--]-->` +
`<!--slot-->`,
`<!--${slotAnchorLabel}-->`,
)
data.value = 'bar'
@ -1863,7 +1891,7 @@ describe('Vapor Mode hydration', () => {
`<span>bar</span>` +
`<span></span>` +
`<!--]-->` +
`<!--slot-->`,
`<!--${slotAnchorLabel}-->`,
)
})
@ -1896,7 +1924,7 @@ describe('Vapor Mode hydration', () => {
`<span>foo</span>` +
`<span></span>` +
`<!--]-->` +
`<!--slot-->` +
`<!--${slotAnchorLabel}-->` +
`<div></div>` +
`<!--]-->`,
)
@ -1912,14 +1940,13 @@ describe('Vapor Mode hydration', () => {
`<span>bar</span>` +
`<span></span>` +
`<!--]-->` +
`<!--slot-->` +
`<!--${slotAnchorLabel}-->` +
`<div></div>` +
`<!--]-->`,
)
})
// problem is next child is incorrect after slot
test.todo('mixed slot and text node', async () => {
test('mixed slot and text node', async () => {
const data = reactive({
text: 'foo',
msg: 'hi',
@ -1937,11 +1964,11 @@ describe('Vapor Mode hydration', () => {
)
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({
text: 'foo',
msg: 'hi',
@ -1959,14 +1986,272 @@ describe('Vapor Mode hydration', () => {
)
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
// mixed slot and fragment component
// mixed slot and v-if
// mixed slot and v-for
test('mixed slot and 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.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', () => {

View File

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

View File

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

View File

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

View File

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

View File

@ -22,10 +22,42 @@ export function querySelector(selectors: string): Element | null {
}
/*! #__NO_SIDE_EFFECTS__ */
export function child(node: ParentNode): Node {
export function _child(node: ParentNode): Node {
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__ */
export function _nthChild(node: Node, i: number): Node {
return node.childNodes[i]
@ -56,9 +88,13 @@ export function __next(node: Node): Node {
return n
}
type ChildFn = (node: ParentNode) => Node
type NextFn = (node: Node) => Node
type NthChildFn = (node: Node, i: number) => Node
interface DelegatedChildFunction extends ChildFn {
impl: ChildFn
}
interface DelegatedNextFunction extends NextFn {
impl: NextFn
}
@ -66,6 +102,12 @@ interface DelegatedNthChildFunction extends NthChildFn {
impl: NthChildFn
}
/*! #__NO_SIDE_EFFECTS__ */
export const child: DelegatedChildFunction = node => {
return child.impl(node)
}
child.impl = _child
/*! #__NO_SIDE_EFFECTS__ */
export const next: DelegatedNextFunction = node => {
return next.impl(node)
@ -90,11 +132,13 @@ nthChild.impl = _nthChild
// of `next` and `nthChild`. After hydration is complete, their implementations
// are restored to the original versions.
export function enableHydrationNodeLookup(): void {
child.impl = __child
next.impl = __next
nthChild.impl = __nthChild
}
export function disableHydrationNodeLookup(): void {
child.impl = _child
next.impl = _next
nthChild.impl = _nthChild
}
@ -112,15 +156,10 @@ function isNonHydrationNode(node: Node) {
)
}
export function nextVaporFragmentAnchor(
export function findVaporFragmentAnchor(
node: Node,
anchorLabel: string,
): Comment | null {
node = handleWrappedNode(node)
if (isComment(node, anchorLabel)) {
return node as Comment
}
let n = node.nextSibling
while (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
/**

View File

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