}></div><div\${
_ssrRenderAttrs(_cssVars)
}></div><!--]-->\`)
+ _push(\`<!--if-->\`)
}
}"
`)
_push(\`<!--if-->\`)
} else {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
+ _push(\`<!--if-->\`)
}
}"
`)
_push(\`<!--if-->\`)
} else {
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
+ _push(\`<!--if-->\`)
}
}"
`)
_push(\`<!--if-->\`)
} else {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+ _push(\`<!--if-->\`)
}
}"
`)
context,
needFragmentWrapper,
)
- if (branch.condition) {
- // v-if/v-else-if anchor for vapor hydration
- statement.body.push(
- createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
- )
- }
+
+ // v-if/v-else-if/v-else anchor for vapor hydration
+ statement.body.push(
+ createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
+ )
+
return statement
}
context: CodegenContext,
): CodeFragment[] {
const [frag, push] = buildCodeFragment()
- const { id, template, operation } = dynamic
+ const { id, template, operation, dynamicChildOffset } = dynamic
if (id !== undefined && template !== undefined) {
- push(NEWLINE, `const n${id} = t${template}()`)
+ push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
push(...genDirectivesForElement(id, context))
}
children: IRDynamicInfo[]
template?: number
hasDynamicChild?: boolean
+ dynamicChildOffset?: number
operation?: OperationNode
needsKey?: boolean
}
function processDynamicChildren(context: TransformContext<ElementNode>) {
let prevDynamics: IRDynamicInfo[] = []
- let hasStaticTemplate = false
+ let staticCount = 0
const children = context.dynamic.children
for (const [index, child] of children.entries()) {
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
if (prevDynamics.length) {
- if (hasStaticTemplate) {
+ if (staticCount) {
// each dynamic child gets its own placeholder node.
// this makes it easier to locate the corresponding node during hydration.
for (let i = 0; i < prevDynamics.length; i++) {
}
prevDynamics = []
}
- hasStaticTemplate = true
+ staticCount++
}
}
if (prevDynamics.length) {
- registerInsertion(prevDynamics, context)
+ registerInsertion(prevDynamics, context, undefined)
+ context.dynamic.dynamicChildOffset = staticCount
}
}
transition: TransitionHooks,
): void
hydrate(node: Node, fn: () => void): void
+ hydrateSlot(vnode: VNode, container: any): void
vdomMount: (
component: ConcreteComponent,
Comment as VComment,
type VNode,
type VNodeHook,
+ VaporSlot,
createTextVNode,
createVNode,
invokeVNodeHook,
)
}
break
+ case VaporSlot:
+ getVaporInterface(parentComponent, vnode).hydrateSlot(
+ vnode,
+ parentNode(node)!,
+ )
+ break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
if (
)
})
+ test('consecutive components with insertion parent', async () => {
+ const data = reactive({ foo: 'foo', bar: 'bar' })
+ const { container } = await testHydration(
+ `<template>
+ <div>
+ <components.Child1/>
+ <components.Child2/>
+ </div>
+ </template>
+ `,
+ {
+ Child1: `<template><span>{{ data.foo }}</span></template>`,
+ Child2: `<template><span>{{ data.bar }}</span></template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `<div><span>foo</span><span>bar</span></div>`,
+ )
+
+ data.foo = 'foo1'
+ data.bar = 'bar1'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div><span>foo1</span><span>bar1</span></div>`,
+ )
+ })
+
test('nested consecutive components with anchor insertion', async () => {
const { container, data } = await testHydration(
`
`</div>`,
)
})
+
+ test('dynamic component fallback', async () => {
+ const { container, data } = await testHydration(
+ `<template>
+ <component :is="'button'">
+ <span>{{ data }}</span>
+ </component>
+ </template>`,
+ {},
+ ref('foo'),
+ )
+
+ expect(container.innerHTML).toBe(
+ `<button><span>foo</span></button><!--${anchorLabel}-->`,
+ )
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<button><span>bar</span></button><!--${anchorLabel}-->`,
+ )
+ })
})
describe('if', () => {
)
})
+ test('consecutive component with insertion parent', async () => {
+ const data = reactive({
+ show: true,
+ foo: 'foo',
+ bar: 'bar',
+ })
+ const { container } = await testHydration(
+ `<template>
+ <div v-if="data.show">
+ <components.Child/>
+ <components.Child2/>
+ </div>
+ </template>`,
+ {
+ Child: `<template><span>{{data.foo}}</span></template>`,
+ Child2: `<template><span>{{data.bar}}</span></template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<span>foo</span>` +
+ `<span>bar</span>` +
+ `</div>` +
+ `<!--${anchorLabel}-->`,
+ )
+
+ data.show = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+ })
+
test('consecutive v-if on component with anchor insertion', async () => {
const data = ref(true)
const { container } = await testHydration(
`</div>`,
)
})
+
+ test('slot fallback', async () => {
+ const data = reactive({
+ foo: 'foo',
+ })
+ const { container } = await testHydration(
+ `<template>
+ <components.Child>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><slot><span>{{data.foo}}</span></slot></template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
+ )
+
+ data.foo = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
+ )
+ })
})
describe.todo('transition', async () => {
expect(container.innerHTML).toMatchInlineSnapshot(`"false"`)
})
+ test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
+ const data = ref(true)
+ const { container } = await testHydrationInterop(
+ `<script setup>const data = _data; const components = _components;</script>
+ <template>
+ <components.VaporChild/>
+ </template>`,
+ {
+ VaporChild: {
+ code: `<template><components.VdomChild/></template>`,
+ vapor: true,
+ },
+ VdomChild: {
+ code: `<script setup>const data = _data;</script>
+ <template><slot><span>{{data}}</span></slot></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>true</span><!--]--><!--slot-->"`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>false</span><!--]--><!--slot-->"`,
+ )
+ })
+
+ test('nested components (VDOM -> Vapor(with slot fallback) -> VDOM)', async () => {
+ const data = ref(true)
+ const { container } = await testHydrationInterop(
+ `<script setup>const data = _data; const components = _components;</script>
+ <template>
+ <components.VaporChild/>
+ </template>`,
+ {
+ VaporChild: {
+ code: `<template>
+ <components.VdomChild>
+ <template #default>
+ <span>{{data}} vapor fallback</span>
+ </template>
+ </components.VdomChild>
+ </template>`,
+ vapor: true,
+ },
+ VdomChild: {
+ code: `<script setup>const data = _data;</script>
+ <template><slot><span>vdom fallback</span></slot></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>true vapor fallback</span><!--]--><!--slot-->"`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>false vapor fallback</span><!--]--><!--slot-->"`,
+ )
+ })
+
test('vapor slot render vdom component', async () => {
const data = ref(true)
const { container } = await testHydrationInterop(
isHydrating,
locateHydrationNode,
locateVaporFragmentAnchor,
+ updateNextChildToHydrate,
} from './dom/hydration'
import { VaporFragment } from './fragment'
import {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
if (isHydrating) {
- locateHydrationNode(true)
+ locateHydrationNode()
} else {
resetInsertionState()
}
if (!isMounted) {
isMounted = true
for (let i = 0; i < newLength; i++) {
+ // TODO add tests
+ if (isHydrating && i > 0 && _insertionParent) {
+ updateNextChildToHydrate(_insertionParent)
+ }
mount(source, i)
}
} else {
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
import { createElement } from './dom/node'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+ adoptTemplate,
+ currentHydrationNode,
+ isHydrating,
+ locateHydrationNode,
+ setCurrentHydrationNode,
+} from './dom/hydration'
import { isVaporTeleport } from './components/Teleport'
import {
insertionAnchor,
resetInsertionState()
}
- const el = createElement(comp)
+ const el = isHydrating
+ ? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
+ : createElement(comp)
// mark single root
;(el as any).$root = isSingleRoot
}
if (rawSlots) {
+ isHydrating && setCurrentHydrationNode(el.firstChild)
if (rawSlots.$) {
// TODO dynamic slot fragment
} else {
setInsertionState,
} from '../insertionState'
import {
+ __next,
+ _nthChild,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
- next,
} from './node'
-import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared'
+import { isVaporAnchors } from '@vue/shared'
export let isHydrating = false
export let currentHydrationNode: Node | null = null
if (!isOptimized) {
adoptTemplate = adoptTemplateImpl
locateHydrationNode = locateHydrationNodeImpl
-
// optimize anchor cache lookup
- ;(Comment.prototype as any).$fs = undefined
+ ;(Comment.prototype as any).$fe = undefined
+ ;(Node.prototype as any).$dp = undefined
isOptimized = true
}
enableHydrationNodeLookup()
}
export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
+export let locateHydrationNode: (isFragment?: boolean) => void
type Anchor = Comment & {
- // cached matching fragment start to avoid repeated traversal
+ // cached matching fragment end to avoid repeated traversal
// on nested fragments
- $fs?: Anchor
+ $fe?: Anchor
}
export const isComment = (node: Node, data: string): node is Anchor =>
}
}
- currentHydrationNode = next(node)
+ currentHydrationNode = __next(node)
return node
}
-const hydrationPositionMap = new WeakMap<ParentNode, Node>()
+const childToHydrateMap = new WeakMap<ParentNode, Node>()
+
+export function updateNextChildToHydrate(parent: ParentNode): void {
+ let nextNode = childToHydrateMap.get(parent)
+ if (nextNode) {
+ nextNode = __next(nextNode)
+ if (nextNode) {
+ childToHydrateMap.set(parent, (currentHydrationNode = nextNode))
+ }
+ }
+}
-function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
+function locateHydrationNodeImpl(isFragment?: boolean): void {
let node: Node | null
// prepend / firstChild
if (insertionAnchor === 0) {
// SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
node = insertionAnchor
} else {
- node = insertionParent
- ? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild
- : currentHydrationNode
-
- // if node is a vapor fragment anchor, find the previous one
- if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) {
- node = node.previousSibling
- if (__DEV__ && !node) {
- // this should not happen
- throw new Error(`vapor fragment anchor previous node was not found.`)
- }
+ node = currentHydrationNode
+ if (insertionParent && (!node || node.parentNode !== insertionParent)) {
+ node =
+ childToHydrateMap.get(insertionParent) ||
+ _nthChild(insertionParent, insertionParent.$dp || 0)
}
- if (node && isComment(node, ']')) {
- // fragment backward search
- if (node.$fs) {
- // already cached matching fragment start
- node = node.$fs
- } else {
- let cur: Node | null = node
- let curFragEnd = node
- let fragDepth = 0
- node = null
- while (cur) {
- cur = cur.previousSibling
- if (cur) {
- if (isComment(cur, '[')) {
- curFragEnd.$fs = cur
- if (!fragDepth) {
- node = cur
- break
- } else {
- fragDepth--
- }
- } else if (isComment(cur, ']')) {
- curFragEnd = cur
- fragDepth++
- }
- }
- }
+ // locate slot fragment start anchor
+ if (isFragment && node && !isComment(node, '[')) {
+ node = locateVaporFragmentAnchor(node, '[')!
+ } else {
+ while (node && isNonHydrationNode(node)) {
+ node = node.nextSibling!
}
}
if (insertionParent && node) {
- const prev = node.previousSibling
- if (prev) hydrationPositionMap.set(insertionParent, prev)
+ const nextNode = node.nextSibling
+ if (nextNode) childToHydrateMap.set(insertionParent, nextNode)
}
}
}
export function locateEndAnchor(
- node: Node | null,
+ node: Anchor,
open = '[',
close = ']',
): Node | null {
- let match = 0
- while (node) {
- node = node.nextSibling
- if (node && node.nodeType === 8) {
- if ((node as Comment).data === open) match++
- if ((node as Comment).data === close) {
- if (match === 0) {
- return node
- } else {
- match--
- }
+ // already cached matching end
+ if (node.$fe) {
+ return node.$fe
+ }
+
+ const stack: Anchor[] = [node]
+ while ((node = node.nextSibling as Anchor) && stack.length > 0) {
+ if (node.nodeType === 8) {
+ if (node.data === open) {
+ stack.push(node)
+ } else if (node.data === close) {
+ const matchingOpen = stack.pop()!
+ matchingOpen.$fe = node
+ if (stack.length === 0) return node
}
}
}
+
return null
}
/*! #__NO_SIDE_EFFECTS__ */
export function template(html: string, root?: boolean) {
let node: Node
- return (): Node & { $root?: true } => {
+ return (n?: number): Node & { $root?: true } => {
if (isHydrating) {
if (__DEV__ && !currentHydrationNode) {
// TODO this should not happen
throw new Error('No current hydration node')
}
- return adoptTemplate(currentHydrationNode!, html)!
+ node = adoptTemplate(currentHydrationNode!, html)!
+ // dynamic node position, default is 0
+ ;(node as any).$dp = n || 0
+ return node
}
// fast path for text nodes
if (html[0] !== '<') {
isHydrating,
locateHydrationNode,
locateVaporFragmentAnchor,
+ setCurrentHydrationNode,
} from './dom/hydration'
import {
applyTransitionHooks,
constructor(anchorLabel?: string) {
super([])
if (isHydrating) {
- locateHydrationNode(true)
+ locateHydrationNode(anchorLabel === 'slot')
this.hydrate(anchorLabel!)
} else {
this.anchor =
parent && insert(this.nodes, parent, this.anchor)
}
+ if (isHydrating) {
+ setCurrentHydrationNode(this.anchor.nextSibling)
+ }
resetTracking()
}
hydrate(label: string): void {
// for `v-if="false"` the node will be an empty comment, use it as the anchor.
// otherwise, find next sibling vapor fragment anchor
- if (isComment(currentHydrationNode!, '')) {
+ if (label === 'if' && isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode
} else {
- const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
+ let anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
+ if (!anchor && (label === 'slot' || label === 'if')) {
+ // fallback to fragment end anchor for ssr vdom slot
+ anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')!
+ }
if (anchor) {
this.anchor = anchor
} else if (__DEV__) {
-export let insertionParent: ParentNode | undefined
+export let insertionParent:
+ | (ParentNode & {
+ // dynamic node position - hydration only
+ // indicates the position where dynamic nodes begin within the parent
+ // during hydration, static nodes before this index are skipped
+ //
+ // Example:
+ // const t0 = _template("<div><span></span><span></span></div>", true)
+ // const n4 = t0(2) // n4.$dp = 2
+ // The first 2 nodes are static, dynamic nodes start from index 2
+ $dp?: number
+ })
+ | undefined
export let insertionAnchor: Node | 0 | undefined
/**
import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
-import { createTextNode } from './dom/node'
+import { __next, createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
import {
currentHydrationNode,
isHydrating,
locateHydrationNode,
+ locateVaporFragmentAnchor,
+ setCurrentHydrationNode,
hydrateNode as vaporHydrateNode,
} from './dom/hydration'
import { DynamicFragment, VaporFragment, isFragment } from './fragment'
},
hydrate: vaporHydrateNode,
+ hydrateSlot(vnode, container) {
+ const { slot } = vnode.vs!
+ const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
+ const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
+ vaporHydrateNode(slotBlock, () => {
+ const anchor = locateVaporFragmentAnchor(currentHydrationNode!, 'slot')!
+ vnode.el = vnode.anchor = anchor
+ insert((vnode.vb = slotBlock), container, anchor)
+ })
+ },
}
const vaporSlotPropsProxyHandler: ProxyHandler<
if (transition) setVNodeTransitionHooks(vnode, transition)
if (isHydrating) {
- ;(
- vdomHydrateNode ||
- (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
- )(
- currentHydrationNode!,
- vnode,
- parentInstance as any,
- null,
- null,
- false,
- )
+ hydrateVNode(vnode, parentInstance as any)
} else {
internals.mt(
vnode,
ensureVaporSlotFallback(children, fallback as any)
isValidSlot = children.length > 0
}
-
if (isValidSlot) {
if (isHydrating) {
- locateHydrationNode(true)
- ;(
- vdomHydrateNode ||
- (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
- )(
- currentHydrationNode!,
+ hydrateVNode(vnode!, parentComponent as any)
+ } else {
+ if (fallbackNodes) {
+ remove(fallbackNodes, parentNode)
+ fallbackNodes = undefined
+ }
+ internals.p(
+ oldVNode,
vnode!,
+ parentNode,
+ anchor,
parentComponent as any,
null,
+ undefined,
null,
false,
)
- } else if (fallbackNodes) {
- remove(fallbackNodes, parentNode)
- fallbackNodes = undefined
}
- internals.p(
- oldVNode,
- vnode!,
- parentNode,
- anchor,
- parentComponent as any,
- null,
- undefined,
- null,
- false,
- )
oldVNode = vnode!
} else {
// for forwarded slot without its own fallback, use the fallback
parentNode,
anchor,
)
+ } else if (isHydrating) {
+ // update hydration node to the next sibling of the slot anchor
+ locateHydrationNode()
+ const nextNode = locateVaporFragmentAnchor(
+ currentHydrationNode!,
+ 'slot',
+ )
+ if (nextNode) setCurrentHydrationNode(__next(nextNode))
}
oldVNode = null
}
isMounted = true
} else {
// move
- internals.m(
- oldVNode!,
- parentNode,
- anchor,
- MoveType.REORDER,
- parentComponent as any,
- )
+ if (oldVNode && !isHydrating) {
+ internals.m(
+ oldVNode,
+ parentNode,
+ anchor,
+ MoveType.REORDER,
+ parentComponent as any,
+ )
+ }
}
frag.remove = parentNode => {
const frag = new VaporFragment([])
frag.insert = (parentNode, anchor) => {
fallbackNodes.forEach(vnode => {
- internals.p(null, vnode, parentNode, anchor, parentComponent)
+ // hydrate fallback
+ if (isHydrating) {
+ hydrateVNode(vnode, parentComponent as any)
+ } else {
+ internals.p(null, vnode, parentNode, anchor, parentComponent)
+ }
})
}
frag.remove = parentNode => {
// vapor slot
return fallbackNodes as Block
}
+
+function hydrateVNode(
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance | null,
+) {
+ // keep fragment start anchor, hydrateNode uses it to
+ // determine if node is a fragmentStart
+ locateHydrationNode()
+ if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
+ const nextNode = vdomHydrateNode(
+ currentHydrationNode!,
+ vnode,
+ parentComponent,
+ null,
+ null,
+ false,
+ )
+ setCurrentHydrationNode(nextNode)
+}
`<div class="foo bar"></div><!--if-->`,
)
expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
- `<span class="bar"></span>`,
+ `<span class="bar"></span><!--if-->`,
)
})