_push(\`<span\${_scopeId}></span>\`)
})
_push(\`<!--]--></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
_push(\`<span\${_scopeId}></span>\`)
})
_push(\`<!--]--></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
_push(\`\`)
if (false) {
_push(\`<div\${_scopeId}></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
_push(\`<!--[-->\`)
if (true) {
_push(\`<div></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
const _cssVars = { style: { color: _ctx.color }}
if (_ctx.ok) {
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars)
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (true) {
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
})
if (false) {
_push(\`<div></div>\`)
+ _push(\`<!--$-->\`)
}
_push(\`</ul>\`)
}"
})
if (_ctx.ok) {
_push(\`<div>ok</div>\`)
+ _push(\`<!--$-->\`)
}
_push(\`<!--]-->\`)
}"
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
}
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+ _push(\`<!--$-->\`)
} else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+ _push(\`<!--$-->\`)
} else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
}
test('<template v-if> (text)', () => {
expect(compile(`<template v-if="foo">hello</template>`).code)
.toMatchInlineSnapshot(`
- "
- return function ssrRender(_ctx, _push, _parent, _attrs) {
- if (_ctx.foo) {
- _push(\`<!--[-->hello<!--]-->\`)
- } else {
- _push(\`<!---->\`)
- }
- }"
- `)
+ "
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.foo) {
+ _push(\`<!--[-->hello<!--]-->\`)
+ _push(\`<!--$-->\`)
+ } else {
+ _push(\`<!---->\`)
+ }
+ }"
+ `)
})
test('<template v-if> (single element)', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
_push(\`<div></div>\`)
})
_push(\`<!--]-->\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
}
? _ssrLooseContain(_ctx.model, _ctx.i)
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
}></option>\`)
+ _push(\`<!--$-->\`)
} else {
_push(\`<!---->\`)
}
for (let i = 0; i < filteredChildren.length; i++) {
const child = filteredChildren[i]
- if (isStaticChildNode(child)) continue
-
+ if (
+ isStaticChildNode(child) ||
+ // v-if has an anchor, which can be used to distinguish the boundary
+ child.type === NodeTypes.IF
+ ) {
+ continue
+ }
child._ssrDynamicInfo = {
hasStaticPrevious: false,
hasStaticNext: false,
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
// optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR)
- return processChildrenAsStatement(branch, context, needFragmentWrapper)
+ const statement = processChildrenAsStatement(
+ branch,
+ context,
+ needFragmentWrapper,
+ )
+ if (branch.condition) {
+ statement.body.push(createCallExpression(`_push`, ['`<!--$-->`']))
+ }
+ return statement
}
const ctx: SSRContext = {}
container.innerHTML = await renderToString(h(App), ctx)
expect(container.innerHTML).toBe(
- '<div><!--teleport start--><!--teleport end--></div>',
+ '<div><!--teleport start--><!--teleport end--><!--$--></div>',
)
teleportContainer.innerHTML = ctx.teleports!['#target']
// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
- '<div><!--teleport start--><!--teleport end--></div>',
+ '<div><!--teleport start--><!--teleport end--><!--$--></div>',
)
expect(teleportContainer.innerHTML).toBe(
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
toggle.value = false
await nextTick()
- expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+ expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
expect(teleportContainer.innerHTML).toBe('')
})
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe(
- '<div><!--teleport start--><!--teleport end--></div>',
+ '<div><!--teleport start--><!--teleport end--><!--$--></div>',
)
expect(teleportContainer.innerHTML).toBe('')
// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
- '<div><!--teleport start--><!--teleport end--></div>',
+ '<div><!--teleport start--><!--teleport end--><!--$--></div>',
)
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
expect(`Hydration children mismatch`).toHaveBeenWarned()
toggle.value = false
await nextTick()
- expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+ expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
expect(teleportContainer.innerHTML).toBe('')
})
return undefined
}
-export function isDynamicAnchor(node: Node): boolean {
+export function isDynamicAnchor(node: Node): node is Comment {
return isComment(node) && (node.data === '[[' || node.data === ']]')
}
+export function isDynamicFragmentEndAnchor(node: Node): node is Comment {
+ return isComment(node) && node.data === '$'
+}
+
export const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT
function nextSibling(node: Node) {
let n = next(node)
- // skip dynamic anchors
- if (n && isDynamicAnchor(n)) {
+ // skip if:
+ // - dynamic anchors (`<!--[-->`, `<!--]-->`)
+ // - dynamic fragment end anchors (`<!--$-->`)
+ if (n && (isDynamicAnchor(n) || isDynamicFragmentEndAnchor(n))) {
n = next(n)
}
return n
slotScopeIds: string[] | null,
optimized = false,
): Node | null => {
- if (isDynamicAnchor(node)) node = nextSibling(node)!
+ if (isDynamicAnchor(node) || isDynamicFragmentEndAnchor(node)) {
+ node = nextSibling(node)!
+ }
optimized = optimized || !!vnode.dynamicChildren
const isFragmentStart = isComment(node) && node.data === '['
const onMismatch = () =>
/**
* @internal
*/
-export { isDynamicAnchor } from './hydration'
+export { isDynamicAnchor, isDynamicFragmentEndAnchor } from './hydration'
expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
})
- test.todo('on component', async () => {})
+ test('on component', async () => {
+ const data = ref(true)
+ const { container } = await testHydration(
+ `<template>
+ <components.Child v-if="data"/>
+ </template>`,
+ { Child: `<template>foo</template>` },
+ data,
+ )
+ expect(container.innerHTML).toMatchInlineSnapshot(`"foo<!--if-->"`)
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
+ })
+
+ test('on component with anchor insertion', async () => {
+ const data = ref(true)
+ const { container } = await testHydration(
+ `<template>
+ <div>
+ <span/>
+ <components.Child v-if="data"/>
+ <span/>
+ </div>
+ </template>`,
+ { Child: `<template>foo</template>` },
+ data,
+ )
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span>foo<!--if--><span></span></div>"`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--if--><span></span></div>"`,
+ )
+ })
+
+ test('consecutive v-if on component with anchor insertion', async () => {
+ const data = ref(true)
+ const { container } = await testHydration(
+ `<template>
+ <div>
+ <span/>
+ <components.Child v-if="data"/>
+ <components.Child v-if="data"/>
+ <span/>
+ </div>
+ </template>`,
+ { Child: `<template>foo</template>` },
+ data,
+ )
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span>foo<!--if-->foo<!--if--><span></span></div>"`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--if--><!--if--><span></span></div>"`,
+ )
+ })
+
+ test('consecutive v-if on fragment component with anchor insertion', async () => {
+ const data = ref(true)
+ const { container } = await testHydration(
+ `<template>
+ <div>
+ <span/>
+ <components.Child v-if="data"/>
+ <components.Child v-if="data"/>
+ <span/>
+ </div>
+ </template>`,
+ {
+ Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--[--><div>true</div>-true-<!--]--><!--if--><!--[--><div>true</div>-true-<!--]--><!--if--><span></span></div>"`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--[--><!--]--><!--if--><!--[--><!--]--><!--if--><span></span></div>"`,
+ )
+ })
})
test.todo('for')
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
-import {
- currentHydrationNode,
- isComment,
- isHydrating,
- locateHydrationNode,
-} from './dom/hydration'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState'
import { renderEffect } from './renderEffect'
): Block {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
- let _currentHydrationNode
if (isHydrating) {
- locateHydrationNode()
- _currentHydrationNode = currentHydrationNode
+ locateHydrationNode(true)
}
let frag: Block
insert(frag, _insertionParent, _insertionAnchor)
}
- // if the current hydration node is a comment, use it as an anchor
- // otherwise need to insert the anchor node
- // OR adjust ssr output to add anchor for v-if
- else if (isHydrating && _currentHydrationNode) {
- const parentNode = _currentHydrationNode.parentNode
- if (parentNode) {
- if (isComment(_currentHydrationNode, '')) {
- if (__DEV__) _currentHydrationNode.data = 'if'
- ;(frag as DynamicFragment).anchor = _currentHydrationNode
- } else {
- parentNode.insertBefore(
- (frag as DynamicFragment).anchor,
- _currentHydrationNode.nextSibling,
- )
- }
- }
- }
-
return frag
}
mountComponent,
unmountComponent,
} from './component'
-import { createComment, createTextNode } from './dom/node'
+import { createComment, createTextNode, next } from './dom/node'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
-import { isHydrating } from './dom/hydration'
+import { currentHydrationNode, isComment, isHydrating } from './dom/hydration'
+import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
export type Block =
| Node
}
export class DynamicFragment extends VaporFragment {
- anchor: Node
+ anchor!: Node
scope: EffectScope | undefined
current?: BlockFn
fallback?: BlockFn
constructor(anchorLabel?: string) {
super([])
- this.anchor =
- __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+ if (isHydrating) {
+ this.hydrate(anchorLabel)
+ } else {
+ this.anchor =
+ __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+ }
}
update(render?: BlockFn, key: any = render): void {
resetTracking()
}
+
+ hydrate(label?: string): void {
+ // for v-if="false" the hydrationNode will be a empty comment node
+ // use it as anchor.
+ // otherwise, use the next sibling comment node as anchor
+ if (isComment(currentHydrationNode!, '')) {
+ this.anchor = currentHydrationNode
+ } else {
+ const anchor = next(currentHydrationNode!)
+ if (isDynamicFragmentEndAnchor(anchor)) {
+ this.anchor = anchor
+ } else if (__DEV__) {
+ // TODO warning
+ warn(`DynamicFragment anchor not found...`)
+ }
+ }
+ if (__DEV__ && label) (this.anchor as Comment).data = label
+ }
}
export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
-import { warn } from '@vue/runtime-dom'
+import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
import {
insertionAnchor,
insertionParent,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
next,
+ prev,
} from './node'
export let isHydrating = false
}
export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: () => void
+export let locateHydrationNode: (isFragment?: boolean) => void
type Anchor = Comment & {
// cached matching fragment start to avoid repeated traversal
return node
}
-function locateHydrationNodeImpl() {
+function locateHydrationNodeImpl(isFragment?: boolean) {
let node: Node | null
// prepend / firstChild
if (insertionAnchor === 0) {
node = insertionAnchor
} else {
node = insertionParent ? insertionParent.lastChild : currentHydrationNode
+
+ // if the last child is a comment, it is the anchor for the fragment
+ // so it need to find the previous node
+ if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
+ let previous = prev(node)
+ if (previous) node = previous
+ }
+
if (node && isComment(node, ']')) {
// fragment backward search
if (node.$fs) {
}
return null
}
+
+export function locateStartAnchor(
+ node: Node | null,
+ open = '[',
+ close = ']',
+): Node | null {
+ let match = 0
+ while (node) {
+ if (node.nodeType === 8) {
+ if ((node as Comment).data === close) match++
+ if ((node as Comment).data === open) {
+ if (match === 0) {
+ return node
+ } else {
+ match--
+ }
+ }
+ }
+ node = node.previousSibling
+ }
+ return null
+}
import { isDynamicAnchor } from '@vue/runtime-dom'
-import { isComment, isEmptyText, locateEndAnchor } from './hydration'
+import {
+ isComment,
+ isEmptyText,
+ locateEndAnchor,
+ locateStartAnchor,
+} from './hydration'
/*! #__NO_SIDE_EFFECTS__ */
export function createTextNode(value = ''): Text {
/*! #__NO_SIDE_EFFECTS__ */
function __next(node: Node): Node {
// treat dynamic node (<!--[[-->...<!--]]-->) as a single node
- if (node && isComment(node, '[[')) {
+ if (isComment(node, '[[')) {
node = locateEndAnchor(node, '[[', ']]')!
}
// treat dynamic node (<!--[-->...<!--]-->) as a single node
- else if (node && isComment(node, '[')) {
+ else if (isComment(node, '[')) {
node = locateEndAnchor(node)!
}
let n = node.nextSibling!
- // skip if:
- // - dynamic anchors (<!--[[-->, <!--]]-->)
- // - fragment end anchor (`<!--]-->`)
- // - empty text nodes
- while (n && (isDynamicAnchor(n) || isComment(n, ']') || isEmptyText(n))) {
+ while (n && isNonHydrationNode(n)) {
n = n.nextSibling!
}
return n
next.impl = _next
nthChild.impl = _nthChild
}
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function prev(node: Node): Node | null {
+ // treat dynamic node (<!--[[-->...<!--]]-->) as a single node
+ if (isComment(node, ']]')) {
+ node = locateStartAnchor(node, '[[', ']]')!
+ }
+
+ // treat dynamic node (<!--[-->...<!--]-->) as a single node
+ else if (isComment(node, ']')) {
+ node = locateStartAnchor(node)!
+ }
+
+ let n = node.previousSibling
+ while (n && isNonHydrationNode(n)) {
+ n = n.previousSibling
+ }
+ return n
+}
+
+function isNonHydrationNode(node: Node) {
+ return (
+ // empty text nodes, no need to hydrate
+ isEmptyText(node) ||
+ // dynamic anchors (<!--[[-->, <!--]]-->)
+ isDynamicAnchor(node) ||
+ // fragment end anchor (`<!--]-->`)
+ isComment(node, ']') ||
+ isDynamicFragmentAnchor(node)
+ )
+}
+
+function isDynamicFragmentAnchor(node: Node) {
+ return __DEV__
+ ? // v-if anchor (`<!--if-->`)
+ isComment(node, 'if') ||
+ // v-for anchor (`<!--for-->`)
+ isComment(node, 'for') ||
+ // v-slot anchor (`<!--slot-->`)
+ isComment(node, 'slot') ||
+ // dynamic-component anchor (`<!--dynamic-component-->`)
+ isComment(node, 'dynamic-component')
+ : // TODO ?
+ isComment(node, '$')
+}