refactor: vapor hydration

This commit is contained in:
daiwei 2025-08-11 11:49:09 +08:00
parent 61eaf4eb9e
commit c511b9a5d7
44 changed files with 2145 additions and 3042 deletions

View File

@ -163,6 +163,7 @@ export interface ComponentNode extends BaseElementNode {
| MemoExpression // when cached by v-memo | MemoExpression // when cached by v-memo
| undefined | undefined
ssrCodegenNode?: CallExpression ssrCodegenNode?: CallExpression
anchor?: string
} }
export interface SlotOutletNode extends BaseElementNode { export interface SlotOutletNode extends BaseElementNode {
@ -172,6 +173,7 @@ export interface SlotOutletNode extends BaseElementNode {
| CacheExpression // when cached by v-once | CacheExpression // when cached by v-once
| undefined | undefined
ssrCodegenNode?: CallExpression ssrCodegenNode?: CallExpression
anchor?: string
} }
export interface TemplateNode extends BaseElementNode { export interface TemplateNode extends BaseElementNode {
@ -287,6 +289,7 @@ export interface IfNode extends Node {
type: NodeTypes.IF type: NodeTypes.IF
branches: IfBranchNode[] branches: IfBranchNode[]
codegenNode?: IfConditionalExpression | CacheExpression // <div v-if v-once> codegenNode?: IfConditionalExpression | CacheExpression // <div v-if v-once>
anchor?: string
} }
export interface IfBranchNode extends Node { export interface IfBranchNode extends Node {
@ -306,6 +309,7 @@ export interface ForNode extends Node {
parseResult: ForParseResult parseResult: ForParseResult
children: TemplateChildNode[] children: TemplateChildNode[]
codegenNode?: ForCodegenNode codegenNode?: ForCodegenNode
anchor?: string
} }
export interface ForParseResult { export interface ForParseResult {

View File

@ -167,6 +167,7 @@ function createCodegenContext(
ssr = false, ssr = false,
isTS = false, isTS = false,
inSSR = false, inSSR = false,
vapor = false,
}: CodegenOptions, }: CodegenOptions,
): CodegenContext { ): CodegenContext {
const context: CodegenContext = { const context: CodegenContext = {
@ -182,6 +183,7 @@ function createCodegenContext(
ssr, ssr,
isTS, isTS,
inSSR, inSSR,
vapor,
source: ast.source, source: ast.source,
code: ``, code: ``,
column: 1, column: 1,

View File

@ -220,6 +220,11 @@ interface SharedTransformCodegenOptions {
* @default 'template.vue.html' * @default 'template.vue.html'
*/ */
filename?: string filename?: string
/**
* Indicates vapor component
*/
vapor?: boolean
} }
export interface TransformOptions export interface TransformOptions

View File

@ -146,6 +146,7 @@ export function createTransformContext(
slotted = true, slotted = true,
ssr = false, ssr = false,
inSSR = false, inSSR = false,
vapor = false,
ssrCssVars = ``, ssrCssVars = ``,
bindingMetadata = EMPTY_OBJ, bindingMetadata = EMPTY_OBJ,
inline = false, inline = false,
@ -173,6 +174,7 @@ export function createTransformContext(
slotted, slotted,
ssr, ssr,
inSSR, inSSR,
vapor,
ssrCssVars, ssrCssVars,
bindingMetadata, bindingMetadata,
inline, inline,

View File

@ -253,6 +253,7 @@ function doCompileTemplate({
slotted, slotted,
sourceMap: true, sourceMap: true,
...compilerOptions, ...compilerOptions,
vapor,
hmr: !isProd, hmr: !isProd,
nodeTransforms: nodeTransforms.concat( nodeTransforms: nodeTransforms.concat(
compilerOptions.nodeTransforms || [], compilerOptions.nodeTransforms || [],

View File

@ -39,7 +39,6 @@ describe('ssr: components', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent) _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent)
_push(\`<!--dynamic-component-->\`)
}" }"
`) `)
@ -50,7 +49,6 @@ describe('ssr: components', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent) _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent)
_push(\`<!--dynamic-component-->\`)
}" }"
`) `)
}) })
@ -246,8 +244,7 @@ describe('ssr: components', () => {
_ssrRenderList(list, (i) => { _ssrRenderList(list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--><!--for--></div>\`) _push(\`<!--]--></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -270,8 +267,7 @@ describe('ssr: components', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--><!--for--></div>\`) _push(\`<!--]--></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -365,7 +361,6 @@ describe('ssr: components', () => {
_push(\`\`) _push(\`\`)
if (false) { if (false) {
_push(\`<div\${_scopeId}></div>\`) _push(\`<div\${_scopeId}></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -396,50 +396,4 @@ describe('ssr: element', () => {
`) `)
}) })
}) })
describe('dynamic anchor', () => {
test('two consecutive components', () => {
expect(
getCompiledString(`
<div>
<div/>
<Comp1/>
<Comp2/>
<div/>
</div>
`),
).toMatchInlineSnapshot(`
"\`<div><div></div><!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
_push(\`<!--]]--><!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
_push(\`<!--]]--><div></div></div>\`"
`)
})
test('multiple consecutive components', () => {
expect(
getCompiledString(`
<div>
<div/>
<Comp1/>
<Comp2/>
<Comp3/>
<Comp4/>
<div/>
</div>
`),
).toMatchInlineSnapshot(`
"\`<div><div></div><!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
_push(\`<!--]]--><!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
_push(\`<!--]]--><!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp3, null, null, _parent))
_push(\`<!--]]--><!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp4, null, null, _parent))
_push(\`<!--]]--><div></div></div>\`"
`)
})
})
}) })

View File

@ -29,7 +29,6 @@ describe('ssr: attrs fallthrough', () => {
_push(\`<!--[-->\`) _push(\`<!--[-->\`)
if (true) { if (true) {
_push(\`<div></div>\`) _push(\`<div></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -70,14 +70,12 @@ describe('ssr: inject <style vars>', () => {
const _cssVars = { style: { color: _ctx.color }} const _cssVars = { style: { color: _ctx.color }}
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`) _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!--[--><div\${ _push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars) _ssrRenderAttrs(_cssVars)
}></div><div\${ }></div><div\${
_ssrRenderAttrs(_cssVars) _ssrRenderAttrs(_cssVars)
}></div><!--]-->\`) }></div><!--]-->\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)

View File

@ -153,7 +153,6 @@ describe('ssr: <slot>', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (true) { if (true) {
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true) _ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -15,7 +15,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--for--><!--]-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })
@ -33,7 +33,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--for--></ul>\`) _push(\`</ul>\`)
}" }"
`) `)
}) })
@ -52,10 +52,8 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--for-->\`)
if (false) { if (false) {
_push(\`<div></div>\`) _push(\`<div></div>\`)
_push(\`<!--if-->\`)
} }
_push(\`</ul>\`) _push(\`</ul>\`)
}" }"
@ -76,7 +74,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--for--></ul>\`) _push(\`</ul>\`)
}" }"
`) `)
}) })
@ -98,7 +96,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--for--></\${_ctx.someTag}>\`) _push(\`</\${_ctx.someTag}>\`)
}" }"
`) `)
}) })
@ -120,14 +118,11 @@ describe('transition-group', () => {
_ssrRenderList(10, (i) => { _ssrRenderList(10, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--for-->\`)
_ssrRenderList(10, (i) => { _ssrRenderList(10, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--for-->\`)
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div>ok</div>\`) _push(\`<div>ok</div>\`)
_push(\`<!--if-->\`)
} }
_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(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })
@ -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(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })
@ -51,9 +51,9 @@ describe('ssr: v-for', () => {
_ssrInterpolate(j) _ssrInterpolate(j)
}</div>\`) }</div>\`)
}) })
_push(\`<!--]--><!--for--></div>\`) _push(\`<!--]--></div>\`)
}) })
_push(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })
@ -68,7 +68,7 @@ describe('ssr: v-for', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`) _push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
}) })
_push(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })
@ -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(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })
@ -107,7 +107,7 @@ describe('ssr: v-for', () => {
_ssrInterpolate(i + 1) _ssrInterpolate(i + 1)
}</span><!--]-->\`) }</span><!--]-->\`)
}) })
_push(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })
@ -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(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
}" }"
`) `)
}) })

View File

@ -8,7 +8,6 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -24,7 +23,6 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -40,10 +38,8 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)
@ -57,10 +53,8 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--if-->\`)
} else if (_ctx.bar) { } else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -76,13 +70,10 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--if-->\`)
} else if (_ctx.bar) { } else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`) _push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)
@ -95,7 +86,6 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[-->hello<!--]-->\`) _push(\`<!--[-->hello<!--]-->\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -112,7 +102,6 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -129,7 +118,6 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`) _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -149,8 +137,7 @@ describe('ssr: v-if', () => {
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -169,10 +156,8 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`) _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)

View File

@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : "" : _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
}></option>\`) }></option>\`)
}) })
_push(\`<!--]--><!--for--></select></div>\`) _push(\`<!--]--></select></div>\`)
}" }"
`) `)
@ -91,7 +91,6 @@ describe('ssr: v-model', () => {
? _ssrLooseContain(_ctx.model, _ctx.i) ? _ssrLooseContain(_ctx.model, _ctx.i)
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : "" : _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
}></option>\`) }></option>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -191,7 +190,7 @@ describe('ssr: v-model', () => {
_ssrInterpolate(item) _ssrInterpolate(item)
}</option>\`) }</option>\`)
}) })
_push(\`<!--]--><!--for--></optgroup></select></div>\`) _push(\`<!--]--></optgroup></select></div>\`)
}" }"
`) `)
@ -217,7 +216,6 @@ describe('ssr: v-model', () => {
}>\${ }>\${
_ssrInterpolate(_ctx.item) _ssrInterpolate(_ctx.item)
}</option>\`) }</option>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -252,8 +250,7 @@ describe('ssr: v-model', () => {
_ssrInterpolate(item) _ssrInterpolate(item)
}</option>\`) }</option>\`)
}) })
_push(\`<!--]--><!--for-->\`) _push(\`<!--]-->\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -287,13 +284,12 @@ describe('ssr: v-model', () => {
}>\${ }>\${
_ssrInterpolate(item) _ssrInterpolate(item)
}</option>\`) }</option>\`)
_push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
_push(\`<!--]-->\`) _push(\`<!--]-->\`)
}) })
_push(\`<!--]--><!--for--></optgroup></select></div>\`) _push(\`<!--]--></optgroup></select></div>\`)
}" }"
`) `)
}) })

View File

@ -0,0 +1,322 @@
// import {
// BLOCK_APPEND_ANCHOR_LABEL,
// BLOCK_INSERTION_ANCHOR_LABEL,
// BLOCK_PREPEND_ANCHOR_LABEL,
// } from '@vue/shared'
import { getCompiledString } from './utils'
describe('insertion anchors', () => {
describe('prepend', () => {
test('prepend anchor with component', () => {
expect(
getCompiledString('<div><Comp/><Comp/><span/></div>', { vapor: true }),
).toMatchInlineSnapshot(`
"\`<div><!--[p-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--p]--><!--[p-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--p]--><span></span></div>\`"
`)
})
test('prepend anchor with component in ssr slot vnode fallback', () => {
expect(
getCompiledString(
`<component :is="'div'">
<div><Comp/><Comp/><span/></div>
</component>`,
{ vapor: true },
),
).toMatchInlineSnapshot(`
"\`<!--[a-->\`)
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`<div\${_scopeId}><!--[p-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
_push(\`<!--p]--><!--[p-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
_push(\`<!--p]--><span\${_scopeId}></span></div>\`)
} else {
return [
_createVNode("div", null, [
_createCommentVNode("[p"),
_createVNode(_component_Comp),
_createCommentVNode("p]"),
_createCommentVNode("[p"),
_createVNode(_component_Comp),
_createCommentVNode("p]"),
_createVNode("span")
])
]
}
}),
_: 1 /* STABLE */
}), _parent)
_push(\`<!--dynamic-component--><!--a]-->\`"
`)
})
test('prepend anchor with slot', () => {
expect(
getCompiledString('<div><slot name="foo"/><slot/><span/></div>', {
vapor: true,
}),
).toMatchInlineSnapshot(`
"\`<div><!--[p-->\`)
_ssrRenderSlot(_ctx.$slots, "foo", {}, null, _push, _parent)
_push(\`<!--slot--><!--p]--><!--[p-->\`)
_ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent)
_push(\`<!--slot--><!--p]--><span></span></div>\`"
`)
})
test('prepend anchor with slot in ssr slot vnode fallback', () => {
expect(
getCompiledString(
`<component :is="'div'">
<div><slot name="foo"/><slot/><span/></div>
</component>`,
{ vapor: true },
),
).toMatchInlineSnapshot(`
"\`<!--[a-->\`)
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`<div\${_scopeId}><!--[p-->\`)
_ssrRenderSlot(_ctx.$slots, "foo", {}, null, _push, _parent, _scopeId)
_push(\`<!--slot--><!--p]--><!--[p-->\`)
_ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent, _scopeId)
_push(\`<!--slot--><!--p]--><span\${_scopeId}></span></div>\`)
} else {
return [
_createVNode("div", null, [
_createCommentVNode("[p"),
_renderSlot(_ctx.$slots, "foo"),
_createCommentVNode("p]"),
_createCommentVNode("[p"),
_renderSlot(_ctx.$slots, "default"),
_createCommentVNode("p]"),
_createVNode("span")
])
]
}
}),
_: 3 /* FORWARDED */
}), _parent)
_push(\`<!--dynamic-component--><!--a]-->\`"
`)
})
test('prepend anchor with v-if', () => {
expect(
getCompiledString('<div><span v-if="foo"/><span/></div>', {
vapor: true,
}),
).toMatchInlineSnapshot(`
"\`<div><!--[p-->\`)
if (_ctx.foo) {
_push(\`<span></span>\`)
_push(\`<!--if-->\`)
} else {
_push(\`<!---->\`)
}
_push(\`<!--p]--><span></span></div>\`"
`)
})
test('prepend anchor with v-if in ssr slot vnode fallback', () => {
expect(
getCompiledString(
`<component :is="'div'">
<div><span v-if="foo"/><span/></div>
</component>`,
{ vapor: true },
),
).toMatchInlineSnapshot(`
"\`<!--[a-->\`)
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`<div\${_scopeId}><!--[p-->\`)
if (_ctx.foo) {
_push(\`<span\${_scopeId}></span>\`)
_push(\`<!--if-->\`)
} else {
_push(\`<!---->\`)
}
_push(\`<!--p]--><span\${_scopeId}></span></div>\`)
} else {
return [
_createVNode("div", null, [
_createCommentVNode("[p"),
(_ctx.foo)
? (_openBlock(), _createBlock("span", { key: 0 }))
: _createCommentVNode("v-if", true),
_createCommentVNode("p]"),
_createVNode("span")
])
]
}
}),
_: 1 /* STABLE */
}), _parent)
_push(\`<!--dynamic-component--><!--a]-->\`"
`)
})
test('prepend anchor with v-for', () => {
expect(
getCompiledString('<div><span v-for="item in items"/><span/></div>', {
vapor: true,
}),
).toMatchInlineSnapshot(`
"\`<div><!--[p--><!--[-->\`)
_ssrRenderList(_ctx.items, (item) => {
_push(\`<span></span>\`)
})
_push(\`<!--]--><!--for--><!--p]--><span></span></div>\`"
`)
})
test('prepend anchor with v-for in ssr slot vnode fallback', () => {
expect(
getCompiledString(
`<component :is="'div'">
<div><span v-for="item in items"/><span/></div>
</component>`,
{ vapor: true },
),
).toMatchInlineSnapshot(`
"\`<!--[a-->\`)
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`<div\${_scopeId}><!--[p--><!--[-->\`)
_ssrRenderList(_ctx.items, (item) => {
_push(\`<span\${_scopeId}></span>\`)
})
_push(\`<!--]--><!--for--><!--p]--><span\${_scopeId}></span></div>\`)
} else {
return [
_createVNode("div", null, [
_createCommentVNode("[p"),
(_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.items, (item) => {
return (_openBlock(), _createBlock("span"))
}), 256 /* UNKEYED_FRAGMENT */)),
_createCommentVNode("p]"),
_createVNode("span")
])
]
}
}),
_: 1 /* STABLE */
}), _parent)
_push(\`<!--dynamic-component--><!--a]-->\`"
`)
})
})
// TODO add more tests
describe('insertion anchor', () => {
test('insertion anchor with component', () => {
expect(
getCompiledString('<div><span/><Comp/><span/></div>', { vapor: true }),
).toMatchInlineSnapshot(`
"\`<div><span></span><!--[i-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--i]--><span></span></div>\`"
`)
})
})
// TODO add more tests
describe('append', () => {
test('append anchor', () => {
expect(
getCompiledString('<div><span/><Comp/><Comp/></div>', { vapor: true }),
).toMatchInlineSnapshot(`
"\`<div><span></span><!--[a-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--a]--><!--[a-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--a]--></div>\`"
`)
})
})
test('mixed anchors', () => {
expect(
getCompiledString('<div><Comp/><span/><Comp/><span/><Comp/></div>', {
vapor: true,
}),
).toMatchInlineSnapshot(`
"\`<div><!--[p-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--p]--><span></span><!--[i-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--i]--><span></span><!--[a-->\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent))
_push(\`<!--a]--></div>\`"
`)
})
test('mixed anchors in ssr slot vnode fallback', () => {
expect(
getCompiledString(
`<component :is="'div'"><Comp/><span/><Comp/><span/><Comp/></component>`,
{
vapor: true,
},
),
).toMatchInlineSnapshot(`
"\`<!--[a-->\`)
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
_push(\`<span\${_scopeId}></span>\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
_push(\`<span\${_scopeId}></span>\`)
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
} else {
return [
_createCommentVNode("[p"),
_createVNode(_component_Comp),
_createCommentVNode("p]"),
_createVNode("span"),
_createCommentVNode("[i"),
_createVNode(_component_Comp),
_createCommentVNode("i]"),
_createVNode("span"),
_createCommentVNode("[a"),
_createVNode(_component_Comp),
_createCommentVNode("a]")
]
}
}),
_: 1 /* STABLE */
}), _parent)
_push(\`<!--dynamic-component--><!--a]-->\`"
`)
})
})
describe.todo('block anchors', () => {
test('if', () => {})
test('if in ssr slot vnode fallback', () => {})
test('for', () => {})
test('for in ssr slot vnode fallback', () => {})
test('slot', () => {})
test('slot in ssr slot vnode fallback', () => {})
test('dynamic component', () => {})
test('dynamic in ssr slot vnode fallback', () => {})
})

View File

@ -1,10 +1,14 @@
import type { CompilerOptions } from '@vue/compiler-core'
import { compile } from '../src' import { compile } from '../src'
export function getCompiledString(src: string): string { export function getCompiledString(
src: string,
options?: CompilerOptions,
): string {
// Wrap src template in a root div so that it doesn't get injected // Wrap src template in a root div so that it doesn't get injected
// fallthrough attr. This results in less noise in generated snapshots // fallthrough attr. This results in less noise in generated snapshots
// but also means this util can only be used for non-root cases. // but also means this util can only be used for non-root cases.
const { code } = compile(`<div>${src}</div>`) const { code } = compile(`<div>${src}</div>`, options)
const match = code.match( const match = code.match(
/_push\(\`<div\${\s*_ssrRenderAttrs\(_attrs\)\s*}>([^]*)<\/div>\`\)/, /_push\(\`<div\${\s*_ssrRenderAttrs\(_attrs\)\s*}>([^]*)<\/div>\`\)/,
) )

View File

@ -22,8 +22,9 @@ import {
processExpression, processExpression,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
BLOCK_END_ANCHOR_LABEL, BLOCK_APPEND_ANCHOR_LABEL,
BLOCK_START_ANCHOR_LABEL, BLOCK_INSERTION_ANCHOR_LABEL,
BLOCK_PREPEND_ANCHOR_LABEL,
escapeHtml, escapeHtml,
isString, isString,
} from '@vue/shared' } from '@vue/shared'
@ -163,33 +164,23 @@ export function processChildren(
asFragment = false, asFragment = false,
disableNestedFragments = false, disableNestedFragments = false,
disableComment = false, disableComment = false,
asBlock = false,
): void { ): void {
if (asBlock) {
context.pushStringPart(`<!--${BLOCK_START_ANCHOR_LABEL}-->`)
}
if (asFragment) { if (asFragment) {
context.pushStringPart(`<!--[-->`) context.pushStringPart(`<!--[-->`)
} }
const { children, type, tagType } = parent as PlainElementNode const { children, type, tagType } = parent as PlainElementNode
const inElement =
type === NodeTypes.ELEMENT && tagType === ElementTypes.ELEMENT if (
if (inElement) processChildrenBlockInfo(children) context.options.vapor &&
type === NodeTypes.ELEMENT &&
tagType === ElementTypes.ELEMENT
) {
processBlockNodeAnchor(children)
}
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (inElement && shouldProcessChildAsBlock(parent, child)) {
processChildren(
{ children: [child] },
context,
asFragment,
disableNestedFragments,
disableComment,
true,
)
continue
}
switch (child.type) { switch (child.type) {
case NodeTypes.ELEMENT: case NodeTypes.ELEMENT:
switch (child.tagType) { switch (child.tagType) {
@ -197,14 +188,14 @@ export function processChildren(
ssrProcessElement(child, context) ssrProcessElement(child, context)
break break
case ElementTypes.COMPONENT: case ElementTypes.COMPONENT:
if (inElement) if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
context.pushStringPart(`<!--${BLOCK_START_ANCHOR_LABEL}-->`)
ssrProcessComponent(child, context, parent) ssrProcessComponent(child, context, parent)
if (inElement) if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
context.pushStringPart(`<!--${BLOCK_END_ANCHOR_LABEL}-->`)
break break
case ElementTypes.SLOT: case ElementTypes.SLOT:
if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
ssrProcessSlotOutlet(child, context) ssrProcessSlotOutlet(child, context)
if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
break break
case ElementTypes.TEMPLATE: case ElementTypes.TEMPLATE:
// TODO // TODO
@ -239,10 +230,14 @@ export function processChildren(
) )
break break
case NodeTypes.IF: case NodeTypes.IF:
if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
ssrProcessIf(child, context, disableNestedFragments, disableComment) ssrProcessIf(child, context, disableNestedFragments, disableComment)
if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
break break
case NodeTypes.FOR: case NodeTypes.FOR:
if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
ssrProcessFor(child, context, disableNestedFragments) ssrProcessFor(child, context, disableNestedFragments)
if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
break break
case NodeTypes.IF_BRANCH: case NodeTypes.IF_BRANCH:
// no-op - handled by ssrProcessIf // no-op - handled by ssrProcessIf
@ -267,9 +262,6 @@ export function processChildren(
if (asFragment) { if (asFragment) {
context.pushStringPart(`<!--]-->`) context.pushStringPart(`<!--]-->`)
} }
if (asBlock) {
context.pushStringPart(`<!--${BLOCK_END_ANCHOR_LABEL}-->`)
}
} }
export function processChildrenAsStatement( export function processChildrenAsStatement(
@ -283,117 +275,69 @@ export function processChildrenAsStatement(
return createBlockStatement(childContext.body) return createBlockStatement(childContext.body)
} }
const isStaticChildNode = (c: TemplateChildNode): boolean => export function processBlockNodeAnchor(children: TemplateChildNode[]): void {
(c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT) || let prevBlocks: (TemplateChildNode & { anchor?: string })[] = []
c.type === NodeTypes.TEXT || let hasStaticNode = false
c.type === NodeTypes.COMMENT for (const child of children) {
if (isBlockNode(child)) {
prevBlocks.push(child)
}
interface BlockInfo { if (isStaticNode(child)) {
hasStaticPrevious: boolean if (prevBlocks.length) {
hasStaticNext: boolean if (hasStaticNode) {
prevBlockCount: number // insertion anchor
nextBlockCount: number prevBlocks.forEach(
child => (child.anchor = BLOCK_INSERTION_ANCHOR_LABEL),
)
} else {
// prepend
prevBlocks.forEach(
child => (child.anchor = BLOCK_PREPEND_ANCHOR_LABEL),
)
}
prevBlocks = []
}
hasStaticNode = true
}
}
if (prevBlocks.length) {
// append anchor
prevBlocks.forEach(child => (child.anchor = BLOCK_APPEND_ANCHOR_LABEL))
}
} }
function processChildrenBlockInfo( function isBlockNode(child: TemplateChildNode): boolean {
children: (TemplateChildNode & { _ssrBlockInfo?: BlockInfo })[], return (
): void { child.type === NodeTypes.IF ||
const filteredChildren = children.filter( child.type === NodeTypes.FOR ||
child => !(child.type === NodeTypes.TEXT && !child.content.trim()), (child.type === NodeTypes.ELEMENT &&
(child.tagType === ElementTypes.COMPONENT ||
child.tagType === ElementTypes.SLOT ||
child.props.some(
p =>
p.name === 'if' ||
p.name === 'else-if' ||
p.name === 'else' ||
p.name === 'for',
)))
) )
for (let i = 0; i < filteredChildren.length; i++) {
const child = filteredChildren[i]
if (
isStaticChildNode(child) ||
// fragment has it's own anchor, which can be used to distinguish the boundary
isFragmentChild(child)
) {
continue
}
child._ssrBlockInfo = {
hasStaticPrevious: false,
hasStaticNext: false,
prevBlockCount: 0,
nextBlockCount: 0,
}
const info = child._ssrBlockInfo
// Calculate the previous static and block node counts
let foundStaticPrev = false
let blockCountPrev = 0
for (let j = i - 1; j >= 0; j--) {
const prevChild = filteredChildren[j]
if (isStaticChildNode(prevChild)) {
foundStaticPrev = true
break
}
// if the previous child has block info, use it
else if (prevChild._ssrBlockInfo) {
foundStaticPrev = prevChild._ssrBlockInfo.hasStaticPrevious
blockCountPrev = prevChild._ssrBlockInfo.prevBlockCount + 1
break
}
blockCountPrev++
}
info.hasStaticPrevious = foundStaticPrev
info.prevBlockCount = blockCountPrev
// Calculate the number of static and block nodes afterwards
let foundStaticNext = false
let blockCountNext = 0
for (let j = i + 1; j < filteredChildren.length; j++) {
const nextChild = filteredChildren[j]
if (isStaticChildNode(nextChild)) {
foundStaticNext = true
break
}
// if the next child has block info, use it
else if (nextChild._ssrBlockInfo) {
foundStaticNext = nextChild._ssrBlockInfo.hasStaticNext
blockCountNext = nextChild._ssrBlockInfo.nextBlockCount + 1
break
}
blockCountNext++
}
info.hasStaticNext = foundStaticNext
info.nextBlockCount = blockCountNext
}
} }
function shouldProcessChildAsBlock( function isStaticNode(child: TemplateChildNode): boolean {
parent: { tag?: string; children: TemplateChildNode[] }, return (
node: TemplateChildNode & { _ssrBlockInfo?: BlockInfo }, child.type === NodeTypes.TEXT ||
): boolean { child.type === NodeTypes.INTERPOLATION ||
// must be inside a parent element child.type === NodeTypes.COMMENT ||
if (!parent.tag) return false (child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT &&
// must has block info !child.props.some(
const { _ssrBlockInfo: info } = node p =>
if (!info) return false p.name === 'if' ||
p.name === 'else-if' ||
const { hasStaticPrevious, hasStaticNext, prevBlockCount, nextBlockCount } = p.name === 'else' ||
info p.name === 'for',
))
// must have static nodes on both sides )
if (!hasStaticPrevious || !hasStaticNext) return false
const blockNodeCount = 1 + prevBlockCount + nextBlockCount
// For two consecutive block nodes, mark the second one as block
if (blockNodeCount === 2) {
return prevBlockCount > 0
}
// For three or more block nodes, mark the middle nodes as block
else if (blockNodeCount >= 3) {
return prevBlockCount > 0 && nextBlockCount > 0
}
return false
}
function isFragmentChild(child: TemplateChildNode): boolean {
const { type } = child
return type === NodeTypes.IF || type === NodeTypes.FOR
} }

View File

@ -43,6 +43,7 @@ import {
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers' import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
import { import {
type SSRTransformContext, type SSRTransformContext,
processBlockNodeAnchor,
processChildren, processChildren,
processChildrenAsStatement, processChildrenAsStatement,
} from '../ssrCodegenTransform' } from '../ssrCodegenTransform'
@ -271,8 +272,11 @@ export function ssrProcessComponent(
// dynamic component (`resolveDynamicComponent` call) // dynamic component (`resolveDynamicComponent` call)
// the codegen node is a `renderVNode` call // the codegen node is a `renderVNode` call
context.pushStatement(node.ssrCodegenNode) context.pushStatement(node.ssrCodegenNode)
// anchor for dynamic component for vapor hydration
context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`) // anchor for vapor dynamic component
if (context.options.vapor) {
context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`)
}
} }
} }
} }
@ -339,6 +343,11 @@ function createVNodeSlotBranch(
loc: locStub, loc: locStub,
codegenNode: undefined, codegenNode: undefined,
} }
if (parentContext.vapor) {
injectVaporInsertionAnchors(children)
}
subTransform(wrapperNode, subOptions, parentContext) subTransform(wrapperNode, subOptions, parentContext)
return createReturnStatement(children) return createReturnStatement(children)
} }
@ -380,6 +389,71 @@ function subTransform(
// - hoists are not enabled for the client branch here // - hoists are not enabled for the client branch here
} }
function injectVaporInsertionAnchors(children: TemplateChildNode[]) {
processBlockNodeAnchor(children)
for (let i = 0; i < children.length; i++) {
const child = children[i]
switch (child.type) {
case NodeTypes.ELEMENT:
switch (child.tagType) {
case ElementTypes.COMPONENT:
case ElementTypes.SLOT:
if (child.anchor) {
children.splice(i, 0, {
type: NodeTypes.COMMENT,
content: `[${child.anchor}`,
loc: locStub,
})
children.splice(i + 2, 0, {
type: NodeTypes.COMMENT,
content: `${child.anchor}]`,
loc: locStub,
})
i += 2
}
break
default: {
const { props } = child
if (
props.some(
p =>
p.name === 'if' ||
p.name === 'else-if' ||
p.name === 'else' ||
p.name === 'for',
)
) {
// @ts-expect-error
if (child.anchor) {
children.splice(i, 0, {
type: NodeTypes.COMMENT,
// @ts-expect-error
content: `[${child.anchor}`,
loc: locStub,
})
children.splice(i + 2, 0, {
type: NodeTypes.COMMENT,
// @ts-expect-error
content: `${child.anchor}]`,
loc: locStub,
})
}
i += 2
break
}
}
}
}
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
injectVaporInsertionAnchors(child.children)
}
}
}
function clone(v: any): any { function clone(v: any): any {
if (isArray(v)) { if (isArray(v)) {
return v.map(clone) return v.map(clone)

View File

@ -16,6 +16,7 @@ import {
type SSRTransformContext, type SSRTransformContext,
processChildrenAsStatement, processChildrenAsStatement,
} from '../ssrCodegenTransform' } from '../ssrCodegenTransform'
import { SLOT_ANCHOR_LABEL } from '@vue/shared'
export const ssrTransformSlotOutlet: NodeTransform = (node, context) => { export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
if (isSlotOutlet(node)) { if (isSlotOutlet(node)) {
@ -93,4 +94,9 @@ export function ssrProcessSlotOutlet(
} }
context.pushStatement(node.ssrCodegenNode!) context.pushStatement(node.ssrCodegenNode!)
// anchor for vapor slot
if (context.options.vapor) {
context.pushStringPart(`<!--${SLOT_ANCHOR_LABEL}-->`)
}
} }

View File

@ -50,6 +50,9 @@ export function ssrProcessFor(
if (!disableNestedFragments) { if (!disableNestedFragments) {
context.pushStringPart(`<!--]-->`) context.pushStringPart(`<!--]-->`)
} }
// v-for anchor for vapor hydration
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`) // anchor for vapor v-for fragment
if (context.options.vapor) {
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
}
} }

View File

@ -81,10 +81,12 @@ function processIfBranch(
needFragmentWrapper, needFragmentWrapper,
) )
// v-if/v-else-if/v-else anchor for vapor hydration // anchor for vapor v-if/v-else-if
statement.body.push( if (context.options.vapor) {
createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]), statement.body.push(
) createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
)
}
return statement return statement
} }

View File

@ -38,7 +38,7 @@ export function render(_ctx) {
"default": () => { "default": () => {
const n0 = _createIf(() => (true), () => { const n0 = _createIf(() => (true), () => {
const n3 = t0() const n3 = t0()
_setInsertionState(n3) _setInsertionState(n3, null)
const n2 = _createComponentWithFallback(_component_Bar) const n2 = _createComponentWithFallback(_component_Bar)
_withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]]) _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
return n3 return n3
@ -157,7 +157,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n0 = t0() const n0 = t0()
const n3 = t1() const n3 = t1()
const n2 = _child(n3, 1) const n2 = _child(n3)
_setInsertionState(n3, 0) _setInsertionState(n3, 0)
const n1 = _createComponentWithFallback(_component_Comp) const n1 = _createComponentWithFallback(_component_Comp)
_renderEffect(() => { _renderEffect(() => {
@ -220,9 +220,9 @@ export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n3 = t0() const n3 = t0()
const n1 = _child(n3) const n1 = _child(n3)
_setInsertionState(n1) _setInsertionState(n1, null)
const n0 = _createSlot("default", null) const n0 = _createSlot("default", null)
_setInsertionState(n3, null, 1) _setInsertionState(n3, null)
const n2 = _createComponentWithFallback(_component_Comp) const n2 = _createComponentWithFallback(_component_Comp)
return n3 return n3
}" }"

View File

@ -87,7 +87,7 @@ const t1 = _template("<div></div>", true)
export function render(_ctx) { export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_for_item0) => { const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n5 = t1() const n5 = t1()
_setInsertionState(n5) _setInsertionState(n5, null)
const n2 = _createFor(() => (_for_item0.value), (_for_item1) => { const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
const n4 = t0() const n4 = t0()
const x4 = _child(n4, -1) const x4 = _child(n4, -1)

View File

@ -144,12 +144,12 @@ const t3 = _template("<div></div>", true)
export function render(_ctx) { export function render(_ctx) {
const n8 = t3() const n8 = t3()
_setInsertionState(n8) _setInsertionState(n8, null)
const n0 = _createIf(() => (_ctx.foo), () => { const n0 = _createIf(() => (_ctx.foo), () => {
const n2 = t0() const n2 = t0()
return n2 return n2
}) })
_setInsertionState(n8) _setInsertionState(n8, null)
const n3 = _createIf(() => (_ctx.bar), () => { const n3 = _createIf(() => (_ctx.bar), () => {
const n5 = t1() const n5 = t1()
return n5 return n5

View File

@ -42,7 +42,7 @@ const t0 = _template("<div></div>", true)
export function render(_ctx) { export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n1 = t0() const n1 = t0()
_setInsertionState(n1) _setInsertionState(n1, null)
const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true) const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
return n1 return n1
}" }"

View File

@ -352,9 +352,9 @@ export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo") const _component_Foo = _resolveComponent("Foo")
const _component_Bar = _resolveComponent("Bar") const _component_Bar = _resolveComponent("Bar")
const n6 = t0() const n6 = t0()
_setInsertionState(n6) _setInsertionState(n6, null)
const n0 = _createSlot("foo", null) const n0 = _createSlot("foo", null)
_setInsertionState(n6) _setInsertionState(n6, null)
const n1 = _createIf(() => (true), () => { const n1 = _createIf(() => (true), () => {
const n3 = _createComponentWithFallback(_component_Foo) const n3 = _createComponentWithFallback(_component_Foo)
return n3 return n3

View File

@ -39,8 +39,6 @@ export class CodegenContext {
seenInlineHandlerNames: Record<string, number> = Object.create(null) seenInlineHandlerNames: Record<string, number> = Object.create(null)
seenChildIndexes: Map<number, number> = new Map()
block: BlockIRNode block: BlockIRNode
withId<T>( withId<T>(
fn: () => T, fn: () => T,
@ -88,6 +86,7 @@ export class CodegenContext {
isTS: false, isTS: false,
inSSR: false, inSSR: false,
inline: false, inline: false,
vapor: false,
bindingMetadata: {}, bindingMetadata: {},
expressionPlugins: [], expressionPlugins: [],
} }

View File

@ -168,33 +168,19 @@ function genInsertionState(
operation: InsertionStateTypes, operation: InsertionStateTypes,
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { seenChildIndexes } = context const { parent, anchor } = operation
const { parent, childIndex, anchor } = operation
const insertionAnchor =
anchor == null
? undefined
: anchor === -1 // -1 indicates prepend
? `0` // runtime anchor value for prepend
: `n${anchor}`
// the index of next block node, used to locate node during hydration
// only passed when anchor is null and childIndex > 0
let index: number | undefined
if (anchor == null && childIndex) {
const existingOffset = seenChildIndexes.get(parent!)
seenChildIndexes.set(
parent!,
(index = existingOffset ? existingOffset + 1 : childIndex),
)
}
return [ return [
NEWLINE, NEWLINE,
...genCall( ...genCall(
context.helper('setInsertionState'), context.helper('setInsertionState'),
`n${parent}`, `n${parent}`,
insertionAnchor, anchor == null
index ? `${index}` : undefined, ? undefined
: anchor === -1 // -1 indicates prepend
? `0` // runtime anchor value for prepend
: anchor === -2 // -2 indicates append
? `null` // runtime anchor value for append
: `n${anchor}`,
), ),
] ]
} }

View File

@ -53,11 +53,9 @@ export function genChildren(
const { children } = dynamic const { children } = dynamic
let offset = 0 let offset = 0
let ifBranchCount = 0
let prev: [variable: string, elementIndex: number] | undefined let prev: [variable: string, elementIndex: number] | undefined
for (const [index, child] of children.entries()) { for (const [index, child] of children.entries()) {
if (child.isIfBranch) ifBranchCount++
if (child.flags & DynamicFlag.NON_TEMPLATE) { if (child.flags & DynamicFlag.NON_TEMPLATE) {
offset-- offset--
} }
@ -87,29 +85,11 @@ export function genChildren(
pushBlock(...genCall(helper('nthChild'), from, String(elementIndex))) pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
} }
} else { } else {
// child index is used to find the child during hydration.
// if offset is not 0, we need to specify the offset to skip the dynamic
// children and get the correct child.
const asAnchor =
id !== undefined && children.some(child => child.anchor === id)
let childIndex =
offset === 0
? undefined
: // if the current node is used as insertionAnchor, subtract 1 here
// this ensures that insertionAnchor points to the current node itself
// rather than its next sibling, since insertionAnchor is used as the
// hydration node
`${
(asAnchor ? index - 1 : index) -
// treat v-if/v-else/v-else-if as a single node
ifBranchCount
}`
if (elementIndex === 0) { if (elementIndex === 0) {
pushBlock(...genCall(helper('child'), from, childIndex)) pushBlock(...genCall(helper('child'), from))
} else { } else {
// check if there's a node that we can reuse from // check if there's a node that we can reuse from
let init = genCall(helper('child'), from, childIndex) let init = genCall(helper('child'), from)
if (elementIndex === 1) { if (elementIndex === 1) {
init = genCall(helper('next'), init) init = genCall(helper('next'), init)
} else if (elementIndex > 1) { } else if (elementIndex > 1) {

View File

@ -69,7 +69,7 @@ export class TransformContext<T extends AllNode = AllNode> {
block: BlockIRNode = this.ir.block block: BlockIRNode = this.ir.block
options: Required< options: Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions> Omit<TransformOptions, 'vapor' | 'filename' | keyof CompilerCompatOptions>
> >
template: string = '' template: string = ''

View File

@ -70,30 +70,12 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) { if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
if (prevDynamics.length) { if (prevDynamics.length) {
if (hasStaticTemplate) { if (hasStaticTemplate) {
// each dynamic child gets its own placeholder node. context.childrenTemplate[index - prevDynamics.length] = `<!>`
// this makes it easier to locate the corresponding node during hydration. prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
for (let i = 0; i < prevDynamics.length; i++) { const anchor = (prevDynamics[0].anchor = context.increaseId())
const idx = index - prevDynamics.length + i registerInsertion(prevDynamics, context, anchor)
context.childrenTemplate[idx] = `<!>`
const dynamicChild = prevDynamics[i]
dynamicChild.flags -= DynamicFlag.NON_TEMPLATE
const anchor = (dynamicChild.anchor = context.increaseId())
if (
dynamicChild.operation &&
isBlockOperation(dynamicChild.operation)
) {
// block types
dynamicChild.operation.parent = context.reference()
dynamicChild.operation.anchor = anchor
}
}
} else { } else {
registerInsertion( registerInsertion(prevDynamics, context, -1 /* prepend */)
prevDynamics,
context,
-1 /* prepend */,
getChildIndex(children, prevDynamics[0]),
)
} }
prevDynamics = [] prevDynamics = []
} }
@ -102,12 +84,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
} }
if (prevDynamics.length) { if (prevDynamics.length) {
registerInsertion( registerInsertion(prevDynamics, context, -2 /* append */)
prevDynamics,
context,
undefined,
getChildIndex(children, prevDynamics[0]),
)
} }
} }
@ -115,7 +92,6 @@ function registerInsertion(
dynamics: IRDynamicInfo[], dynamics: IRDynamicInfo[],
context: TransformContext, context: TransformContext,
anchor?: number, anchor?: number,
childIndex?: number,
) { ) {
for (const child of dynamics) { for (const child of dynamics) {
if (child.template != null) { if (child.template != null) {
@ -124,27 +100,12 @@ function registerInsertion(
type: IRNodeTypes.INSERT_NODE, type: IRNodeTypes.INSERT_NODE,
elements: dynamics.map(child => child.id!), elements: dynamics.map(child => child.id!),
parent: context.reference(), parent: context.reference(),
anchor, anchor: anchor === -2 ? undefined : anchor,
}) })
} else if (child.operation && isBlockOperation(child.operation)) { } else if (child.operation && isBlockOperation(child.operation)) {
// block types // block types
child.operation.parent = context.reference() child.operation.parent = context.reference()
child.operation.anchor = anchor child.operation.anchor = anchor
child.operation.childIndex = childIndex
} }
} }
} }
function getChildIndex(
children: IRDynamicInfo[],
child: IRDynamicInfo,
): number {
let index = 0
for (const c of children) {
// treat v-if/v-else/v-else-if as a single node
if (c.isIfBranch) continue
if (c === child) break
index++
}
return index
}

View File

@ -601,14 +601,14 @@ describe('SSR hydration', () => {
const ctx: SSRContext = {} const ctx: SSRContext = {}
container.innerHTML = await renderToString(h(App), ctx) container.innerHTML = await renderToString(h(App), ctx)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></div>', '<div><!--teleport start--><!--teleport end--></div>',
) )
teleportContainer.innerHTML = ctx.teleports!['#target'] teleportContainer.innerHTML = ctx.teleports!['#target']
// hydrate // hydrate
createSSRApp(App).mount(container) createSSRApp(App).mount(container)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></div>', '<div><!--teleport start--><!--teleport end--></div>',
) )
expect(teleportContainer.innerHTML).toBe( expect(teleportContainer.innerHTML).toBe(
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->', '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
@ -617,7 +617,7 @@ describe('SSR hydration', () => {
toggle.value = false toggle.value = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>') expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
}) })
@ -660,21 +660,21 @@ describe('SSR hydration', () => {
// server render // server render
container.innerHTML = await renderToString(h(App)) container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></div>', '<div><!--teleport start--><!--teleport end--></div>',
) )
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
// hydrate // hydrate
createSSRApp(App).mount(container) createSSRApp(App).mount(container)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></div>', '<div><!--teleport start--><!--teleport end--></div>',
) )
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>') expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
expect(`Hydration children mismatch`).toHaveBeenWarned() expect(`Hydration children mismatch`).toHaveBeenWarned()
toggle.value = false toggle.value = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>') expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
}) })
@ -713,7 +713,7 @@ describe('SSR hydration', () => {
// server render // server render
container.innerHTML = await renderToString(h(App)) container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--[[--><!--teleport start--><!--teleport end--><!--]]--></div>', '<div><!--teleport start--><!--teleport end--></div>',
) )
expect(teleportContainer1.innerHTML).toBe('') expect(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe('') expect(teleportContainer2.innerHTML).toBe('')
@ -721,7 +721,7 @@ describe('SSR hydration', () => {
// hydrate // hydrate
createSSRApp(App).mount(container) createSSRApp(App).mount(container)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--[[--><!--teleport start--><!--teleport end--><!--]]--></div>', '<div><!--teleport start--><!--teleport end--></div>',
) )
expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>') expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
expect(teleportContainer2.innerHTML).toBe('') expect(teleportContainer2.innerHTML).toBe('')
@ -1005,7 +1005,7 @@ describe('SSR hydration', () => {
// server render // server render
container.innerHTML = await renderToString(h(App)) container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toMatchInlineSnapshot( expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><!--[[--><span>1</span><!--]]--><!--[[--><span>2</span><!--]]--></div>"`, `"<div><span>1</span><span>2</span></div>"`,
) )
// reset asyncDeps from ssr // reset asyncDeps from ssr
asyncDeps.length = 0 asyncDeps.length = 0
@ -1869,36 +1869,6 @@ describe('SSR hydration', () => {
} }
}) })
describe('dynamic anchor', () => {
test('two consecutive components', () => {
const Comp = {
render() {
return createTextVNode('foo')
},
}
const { vnode, container } = mountWithHydration(
`<div><span></span>foo<!--[[-->foo<!--]]--><span></span></div>`,
() => h('div', null, [h('span'), h(Comp), h(Comp), h('span')]),
)
expect(vnode.el).toBe(container.firstChild)
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
})
test('multiple consecutive components', () => {
const Comp = {
render() {
return createTextVNode('foo')
},
}
const { vnode, container } = mountWithHydration(
`<div><span></span>foo<!--[[-->foo<!--]]-->foo<span></span></div>`,
() => h('div', null, [h('span'), h(Comp), h(Comp), h(Comp), h('span')]),
)
expect(vnode.el).toBe(container.firstChild)
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
})
})
test('hmr reload child wrapped in KeepAlive', async () => { test('hmr reload child wrapped in KeepAlive', async () => {
const id = 'child-reload' const id = 'child-reload'
const Child = { const Child = {
@ -1923,14 +1893,14 @@ describe('SSR hydration', () => {
const root = document.createElement('div') const root = document.createElement('div')
root.innerHTML = await renderToString(h(App)) root.innerHTML = await renderToString(h(App))
createSSRApp(App).mount(root) createSSRApp(App).mount(root)
expect(root.innerHTML).toBe('<div><!--[[--><div>foo</div><!--]]--></div>') expect(root.innerHTML).toBe('<div><div>foo</div></div>')
reload(id, { reload(id, {
__hmrId: id, __hmrId: id,
template: `<div>bar</div>`, template: `<div>bar</div>`,
}) })
await nextTick() await nextTick()
expect(root.innerHTML).toBe('<div><!--[[--><div>bar</div><!--]]--></div>') expect(root.innerHTML).toBe('<div><div>bar</div></div>')
}) })
test('hmr root reload', async () => { test('hmr root reload', async () => {

View File

@ -32,7 +32,6 @@ import {
isRenderableAttrValue, isRenderableAttrValue,
isReservedProp, isReservedProp,
isString, isString,
isVaporAnchor,
normalizeClass, normalizeClass,
normalizeCssVarValue, normalizeCssVarValue,
normalizeStyle, normalizeStyle,
@ -118,7 +117,7 @@ export function createHydrationFunctions(
o: { o: {
patchProp, patchProp,
createText, createText,
nextSibling: next, nextSibling,
parentNode, parentNode,
remove, remove,
insert, insert,
@ -126,15 +125,6 @@ export function createHydrationFunctions(
}, },
} = rendererInternals } = rendererInternals
function nextSibling(node: Node) {
let n = next(node)
// skip vapor mode specific anchors
if (n && isVaporAnchor(n)) {
n = next(n)
}
return n
}
const hydrate: RootHydrateFunction = (vnode, container) => { const hydrate: RootHydrateFunction = (vnode, container) => {
if (!container.hasChildNodes()) { if (!container.hasChildNodes()) {
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
@ -161,10 +151,6 @@ export function createHydrationFunctions(
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized = false, optimized = false,
): Node | null => { ): Node | null => {
// skip vapor mode specific anchors
if (isVaporAnchor(node)) {
node = nextSibling(node)!
}
optimized = optimized || !!vnode.dynamicChildren optimized = optimized || !!vnode.dynamicChildren
const isFragmentStart = isComment(node) && node.data === '[' const isFragmentStart = isComment(node) && node.data === '['
const onMismatch = () => const onMismatch = () =>
@ -486,7 +472,7 @@ export function createHydrationFunctions(
// The SSRed DOM contains more nodes than it should. Remove them. // The SSRed DOM contains more nodes than it should. Remove them.
const cur = next const cur = next
next = nextSibling(next) next = next.nextSibling
remove(cur) remove(cur)
} }
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
@ -592,7 +578,7 @@ export function createHydrationFunctions(
} }
} }
return nextSibling(el) return el.nextSibling
} }
const hydrateChildren = ( const hydrateChildren = (

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,25 @@
import { warn } from '@vue/runtime-dom' import { warn } from '@vue/runtime-dom'
import { import {
insertionAnchor, insertionAnchor,
insertionChildIndex,
insertionParent, insertionParent,
resetInsertionState, resetInsertionState,
setInsertionState, setInsertionState,
} from '../insertionState' } from '../insertionState'
import { import {
__next, _child,
__nthChild, _next,
createTextNode, createTextNode,
disableHydrationNodeLookup, disableHydrationNodeLookup,
enableHydrationNodeLookup, enableHydrationNodeLookup,
} from './node' } from './node'
import { BLOCK_END_ANCHOR_LABEL, isVaporAnchor } from '@vue/shared' import {
BLOCK_APPEND_ANCHOR_LABEL,
BLOCK_INSERTION_ANCHOR_LABEL,
BLOCK_PREPEND_ANCHOR_LABEL,
isVaporAnchor,
} from '@vue/shared'
const isHydratingStack = [] as boolean[]
export let isHydrating = false export let isHydrating = false
export let currentHydrationNode: Node | null = null export let currentHydrationNode: Node | null = null
@ -24,21 +30,13 @@ export function setCurrentHydrationNode(node: Node | null): void {
function findParentSibling(n: Node): Node | null { function findParentSibling(n: Node): Node | null {
if (!n.parentNode) return null if (!n.parentNode) return null
let next = n.parentNode.nextSibling return n.parentNode.nextSibling || findParentSibling(n.parentNode)
while (next && isComment(next, BLOCK_END_ANCHOR_LABEL)) {
next = next.nextElementSibling
}
return next ? next : findParentSibling(n.parentNode)
} }
export function advanceHydrationNode(node: Node & { $ps?: Node | null }): void { export function advanceHydrationNode(node: Node & { $ps?: Node | null }): void {
let next = node.nextSibling
while (next && isComment(next, BLOCK_END_ANCHOR_LABEL)) {
next = next.nextSibling
}
// if no next sibling, find the next node in the parent chain // if no next sibling, find the next node in the parent chain
const ret = next || node.$ps || (node.$ps = findParentSibling(node)) const ret =
node.nextSibling || node.$ps || (node.$ps = findParentSibling(node))
if (ret) setCurrentHydrationNode(ret) if (ret) setCurrentHydrationNode(ret)
} }
@ -54,22 +52,26 @@ function performHydration<T>(
locateHydrationNode = locateHydrationNodeImpl locateHydrationNode = locateHydrationNodeImpl
// optimize anchor cache lookup // optimize anchor cache lookup
;(Comment.prototype as any).$fe = undefined ;(Comment.prototype as any).$fe = undefined
;(Node.prototype as any).$np = undefined ;(Node.prototype as any).$pa = undefined
;(Node.prototype as any).$ia = undefined
;(Node.prototype as any).$aa = undefined
isOptimized = true isOptimized = true
} }
enableHydrationNodeLookup() enableHydrationNodeLookup()
isHydrating = true isHydratingStack.push((isHydrating = true))
setup() setup()
const res = fn() const res = fn()
cleanup() cleanup()
currentHydrationNode = null currentHydrationNode = null
isHydrating = false isHydratingStack.pop()
isHydrating = isHydratingStack[isHydratingStack.length - 1] || false
disableHydrationNodeLookup() disableHydrationNodeLookup()
return res return res
} }
export function withHydration(container: ParentNode, fn: () => void): void { export function withHydration(container: ParentNode, fn: () => void): void {
const setup = () => setInsertionState(container, 0) // @ts-expect-error
const setup = () => setInsertionState(container, -1)
const cleanup = () => resetInsertionState() const cleanup = () => resetInsertionState()
return performHydration(fn, setup, cleanup) return performHydration(fn, setup, cleanup)
} }
@ -136,20 +138,32 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
function locateHydrationNodeImpl(): void { function locateHydrationNodeImpl(): void {
let node: Node | null let node: Node | null
// prepend / firstChild // @ts-expect-error
if (insertionAnchor === 0) { if (insertionAnchor === -1) {
const n = insertionParent!.$np || 0 // firstChild
node = __nthChild(insertionParent!, n) node = _child(insertionParent!)!
insertionParent!.$np = n + 1 } else if (insertionAnchor === 0) {
// prepend
node = insertionParent!.$pa = locateHydrationNodeByAnchor(
insertionParent!.$pa || _child(insertionParent!),
BLOCK_PREPEND_ANCHOR_LABEL,
)!
} else if (insertionAnchor) { } else if (insertionAnchor) {
// `insertionAnchor` is a Node, it is the DOM node to hydrate // insertion anchor
// Template: `...<span/><!----><span/>...`// `insertionAnchor` is the placeholder node = insertionParent!.$ia = locateHydrationNodeByAnchor(
// SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node insertionParent!.$ia || _child(insertionParent!),
node = insertionAnchor BLOCK_INSERTION_ANCHOR_LABEL,
)!
} else if (insertionAnchor === null) {
// append anchor
node = insertionParent!.$aa = locateHydrationNodeByAnchor(
insertionParent!.$aa || _child(insertionParent!),
BLOCK_APPEND_ANCHOR_LABEL,
)!
} else { } else {
node = currentHydrationNode node = currentHydrationNode
if (insertionParent && (!node || node.parentNode !== insertionParent)) { if (insertionParent && (!node || node.parentNode !== insertionParent)) {
node = __nthChild(insertionParent, insertionChildIndex || 0) node = _child(insertionParent)
} }
} }
@ -213,3 +227,20 @@ export function locateVaporFragmentAnchor(
export function isEmptyTextNode(node: Node): node is Text { export function isEmptyTextNode(node: Node): node is Text {
return node.nodeType === 3 && !(node as Text).data.trim() return node.nodeType === 3 && !(node as Text).data.trim()
} }
function locateHydrationNodeByAnchor(
node: Node,
anchorLabel: string,
): Node | null {
while (node) {
if (isComment(node, `[${anchorLabel}`)) return node.nextSibling
node = node.nextSibling!
}
if (__DEV__) {
throw new Error(
`Could not locate hydration node with anchor label: ${anchorLabel}`,
)
}
return null
}

View File

@ -1,7 +1,7 @@
import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration' import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration'
import { import {
BLOCK_END_ANCHOR_LABEL, BLOCK_INSERTION_ANCHOR_LABEL,
BLOCK_START_ANCHOR_LABEL, BLOCK_PREPEND_ANCHOR_LABEL,
isVaporAnchor, isVaporAnchor,
} from '@vue/shared' } from '@vue/shared'
@ -25,6 +25,29 @@ export function querySelector(selectors: string): Element | null {
return document.querySelector(selectors) return document.querySelector(selectors)
} }
function skipBlockNodes(node: Node): Node {
while (node) {
if (isComment(node, `[${BLOCK_PREPEND_ANCHOR_LABEL}`)) {
node = locateEndAnchor(
node,
`[${BLOCK_PREPEND_ANCHOR_LABEL}`,
`${BLOCK_PREPEND_ANCHOR_LABEL}]`,
)!
continue
} else if (isComment(node, `[${BLOCK_INSERTION_ANCHOR_LABEL}`)) {
node = locateEndAnchor(
node,
`[${BLOCK_INSERTION_ANCHOR_LABEL}`,
`${BLOCK_INSERTION_ANCHOR_LABEL}]`,
)!
continue
}
break
}
return node
}
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function _child(node: ParentNode): Node { export function _child(node: ParentNode): Node {
return node.firstChild! return node.firstChild!
@ -60,16 +83,19 @@ export function _child(node: ParentNode): Node {
*/ */
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function __child(node: ParentNode, offset?: number): Node { export function __child(node: ParentNode, offset?: number): Node {
let n = node.firstChild!
// when offset is -1, it means we need to get the text node of this element // when offset is -1, it means we need to get the text node of this element
// since server-side rendering doesn't generate whitespace placeholder text nodes, // since server-side rendering doesn't generate whitespace placeholder text nodes,
// if firstChild is null, manually insert a text node and return it // if firstChild is null, manually insert a text node and return it
if (offset === -1 && !node.firstChild) { if (offset === -1 && !n) {
node.textContent = ' ' node.textContent = ' '
return node.firstChild! return node.firstChild!
} }
let n = offset ? __nthChild(node, offset) : node.firstChild!
while (n && (isComment(n, '[') || isVaporAnchor(n))) { while (n && (isComment(n, '[') || isVaporAnchor(n))) {
// skip block node
n = skipBlockNodes(n) as ChildNode
if (isComment(n, '[')) { if (isComment(n, '[')) {
n = locateEndAnchor(n)!.nextSibling! n = locateEndAnchor(n)!.nextSibling!
} else { } else {
@ -90,7 +116,7 @@ export function _nthChild(node: Node, i: number): Node {
*/ */
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function __nthChild(node: Node, i: number): Node { export function __nthChild(node: Node, i: number): Node {
let n = node.firstChild! let n = __child(node as ParentNode)
for (let start = 0; start < i; start++) { for (let start = 0; start < i; start++) {
n = __next(n) as ChildNode n = __next(n) as ChildNode
} }
@ -105,7 +131,7 @@ export function _next(node: Node): Node {
/** /**
* Hydration-specific version of `next`. * Hydration-specific version of `next`.
* *
* SSR comment anchors (fragments `<!--[-->...<!--]-->`, block `<!--[[-->...<!--]]-->`) * SSR comment anchors (fragments `<!--[-->...<!--]-->`, block nodes `<!--[x-->...<!--x]-->`)
* disrupt standard `node.nextSibling` traversal during hydration. `_next` might * disrupt standard `node.nextSibling` traversal during hydration. `_next` might
* return a comment node or an internal node of a fragment instead of skipping * return a comment node or an internal node of a fragment instead of skipping
* the entire fragment block. * the entire fragment block.
@ -144,20 +170,13 @@ export function _next(node: Node): Node {
*/ */
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function __next(node: Node): Node { export function __next(node: Node): Node {
// process block node (<!--[[-->...<!--]]-->) as a single node
if (isComment(node, BLOCK_START_ANCHOR_LABEL)) {
node = locateEndAnchor(
node,
BLOCK_START_ANCHOR_LABEL,
BLOCK_END_ANCHOR_LABEL,
)!
}
// process fragment (<!--[-->...<!--]-->) as a single node // process fragment (<!--[-->...<!--]-->) as a single node
else if (isComment(node, '[')) { if (isComment(node, '[')) {
node = locateEndAnchor(node)! node = locateEndAnchor(node)!
} }
node = skipBlockNodes(node)
let n = node.nextSibling! let n = node.nextSibling!
while (n && isNonHydrationNode(n)) { while (n && isNonHydrationNode(n)) {
n = n.nextSibling! n = n.nextSibling!

View File

@ -1,14 +1,11 @@
export let insertionParent: export let insertionParent:
| (ParentNode & { | (ParentNode & {
// number of prepends - hydration only $pa?: Node
// consecutive prepends need to skip nodes that were prepended earlier $ia?: Node
// each prepend increases the value of $prepend $aa?: Node
$np?: number
}) })
| undefined | undefined
export let insertionAnchor: Node | 0 | undefined export let insertionAnchor: Node | 0 | undefined | null
export let insertionChildIndex: number | undefined
/** /**
* This function is called before a block type that requires insertion * This function is called before a block type that requires insertion
@ -17,14 +14,12 @@ export let insertionChildIndex: number | undefined
*/ */
export function setInsertionState( export function setInsertionState(
parent: ParentNode, parent: ParentNode,
anchor?: Node | 0, anchor?: Node | 0 | null,
offset?: number,
): void { ): void {
insertionParent = parent insertionParent = parent
insertionAnchor = anchor insertionAnchor = anchor
insertionChildIndex = offset
} }
export function resetInsertionState(): void { export function resetInsertionState(): void {
insertionParent = insertionAnchor = insertionChildIndex = undefined insertionParent = insertionAnchor = undefined
} }

View File

@ -446,7 +446,7 @@ function testRender(type: string, render: typeof renderToString) {
).toBe( ).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` + `<!--[--><span>from slot</span><!--]-->` +
`<!--slot--></div></div>`, `</div></div>`,
) )
// test fallback // test fallback
@ -461,7 +461,7 @@ function testRender(type: string, render: typeof renderToString) {
}), }),
), ),
).toBe( ).toBe(
`<div>parent<div class="child"><!--[-->fallback<!--]--><!--slot--></div></div>`, `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`,
) )
}) })
@ -507,7 +507,7 @@ function testRender(type: string, render: typeof renderToString) {
).toBe( ).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` + `<!--[--><span>from slot</span><!--]-->` +
`<!--slot--></div></div>`, `</div></div>`,
) )
}) })
@ -525,7 +525,7 @@ function testRender(type: string, render: typeof renderToString) {
expect(await render(app)).toBe( expect(await render(app)).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` + `<!--[--><span>from slot</span><!--]-->` +
`<!--slot--></div></div>`, `</div></div>`,
) )
}) })
@ -572,7 +572,7 @@ function testRender(type: string, render: typeof renderToString) {
}) })
expect(await render(app)).toBe( expect(await render(app)).toBe(
`<div><!--[--><!--[-->hello<!--]--><!--slot--><!--]--><!--slot--></div>`, `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`,
) )
}) })
@ -593,7 +593,7 @@ function testRender(type: string, render: typeof renderToString) {
expect(await render(app)).toBe( expect(await render(app)).toBe(
// should only have a single fragment // should only have a single fragment
`<div><!--[--><!--]--><!--slot--></div>`, `<div><!--[--><!--]--></div>`,
) )
}) })
@ -614,7 +614,7 @@ function testRender(type: string, render: typeof renderToString) {
expect(await render(app)).toBe( expect(await render(app)).toBe(
// should only have a single fragment // should only have a single fragment
`<div><!--[-->fallback<!--]--><!--slot--></div>`, `<div><!--[-->fallback<!--]--></div>`,
) )
}) })
}) })

View File

@ -25,10 +25,10 @@ describe('ssr: attr fallthrough', () => {
template: `<child :ok="ok" class="bar"/>`, template: `<child :ok="ok" class="bar"/>`,
} }
expect(await renderToString(createApp(Parent, { ok: true }))).toBe( expect(await renderToString(createApp(Parent, { ok: true }))).toBe(
`<div class="foo bar"></div><!--if-->`, `<div class="foo bar"></div>`,
) )
expect(await renderToString(createApp(Parent, { ok: false }))).toBe( expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
`<span class="bar"></span><!--if-->`, `<span class="bar"></span>`,
) )
}) })

View File

@ -14,9 +14,7 @@ describe('ssr: dynamic component', () => {
template: `<component :is="'one'"><span>slot</span></component>`, template: `<component :is="'one'"><span>slot</span></component>`,
}), }),
), ),
).toBe( ).toBe(`<div><!--[--><span>slot</span><!--]--></div>`)
`<div><!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
)
}) })
test('resolved to component with v-show', async () => { test('resolved to component with v-show', async () => {
@ -32,7 +30,7 @@ describe('ssr: dynamic component', () => {
}), }),
), ),
).toBe( ).toBe(
`<div><!--[--><div style="display:none;"><!--[-->hi<!--]--></div><!--dynamic-component--><!--]--></div><!--dynamic-component-->`, `<div><!--[--><div style="display:none;"><!--[-->hi<!--]--></div><!--]--></div>`,
) )
}) })
@ -43,7 +41,7 @@ describe('ssr: dynamic component', () => {
template: `<component :is="'p'"><span>slot</span></component>`, template: `<component :is="'p'"><span>slot</span></component>`,
}), }),
), ),
).toBe(`<p><span>slot</span></p><!--dynamic-component-->`) ).toBe(`<p><span>slot</span></p>`)
}) })
test('resolve to component vnode', async () => { test('resolve to component vnode', async () => {
@ -62,9 +60,7 @@ describe('ssr: dynamic component', () => {
template: `<component :is="vnode"><span>slot</span></component>`, template: `<component :is="vnode"><span>slot</span></component>`,
}), }),
), ),
).toBe( ).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`)
`<div>test<!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
)
}) })
test('resolve to element vnode', async () => { test('resolve to element vnode', async () => {
@ -79,6 +75,6 @@ describe('ssr: dynamic component', () => {
template: `<component :is="vnode"><span>slot</span></component>`, template: `<component :is="vnode"><span>slot</span></component>`,
}), }),
), ),
).toBe(`<div id="test"><span>slot</span></div><!--dynamic-component-->`) ).toBe(`<div id="test"><span>slot</span></div>`)
}) })
}) })

View File

@ -68,9 +68,7 @@ describe('ssr: scopedId runtime behavior', () => {
} }
const result = await renderToString(createApp(Comp)) const result = await renderToString(createApp(Comp))
expect(result).toBe( expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
`<!--[--><div parent wrapper-s child></div><!--]--><!--slot-->`,
)
}) })
// #2892 // #2892
@ -152,8 +150,8 @@ describe('ssr: scopedId runtime behavior', () => {
const result = await renderToString(createApp(Root)) const result = await renderToString(createApp(Root))
expect(result).toBe( expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` + `<div class="wrapper" root slotted wrapper>` +
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` + `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
`<!--slot--></div>`, `</div>`,
) )
}) })
@ -267,8 +265,8 @@ describe('ssr: scopedId runtime behavior', () => {
const result = await renderToString(createApp(Root)) const result = await renderToString(createApp(Root))
expect(result).toBe( expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` + `<div class="wrapper" root slotted wrapper>` +
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` + `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
`<!--slot--></div>`, `</div>`,
) )
}) })
}) })

View File

@ -16,7 +16,7 @@ describe('ssr: slot', () => {
template: `<one>hello</one>`, template: `<one>hello</one>`,
}), }),
), ),
).toBe(`<div><!--[-->hello<!--]--><!--slot--></div>`) ).toBe(`<div><!--[-->hello<!--]--></div>`)
}) })
test('element slot', async () => { test('element slot', async () => {
@ -27,7 +27,7 @@ describe('ssr: slot', () => {
template: `<one><div>hi</div></one>`, template: `<one><div>hi</div></one>`,
}), }),
), ),
).toBe(`<div><!--[--><div>hi</div><!--]--><!--slot--></div>`) ).toBe(`<div><!--[--><div>hi</div><!--]--></div>`)
}) })
test('empty slot', async () => { test('empty slot', async () => {
@ -42,7 +42,7 @@ describe('ssr: slot', () => {
template: `<one><template v-if="false"/></one>`, template: `<one><template v-if="false"/></one>`,
}), }),
), ),
).toBe(`<div><!--[--><!--]--><!--slot--></div>`) ).toBe(`<div><!--[--><!--]--></div>`)
}) })
test('empty slot (manual comments)', async () => { test('empty slot (manual comments)', async () => {
@ -57,7 +57,7 @@ describe('ssr: slot', () => {
template: `<one><!--hello--></one>`, template: `<one><!--hello--></one>`,
}), }),
), ),
).toBe(`<div><!--[--><!--]--><!--slot--></div>`) ).toBe(`<div><!--[--><!--]--></div>`)
}) })
test('empty slot (multi-line comments)', async () => { test('empty slot (multi-line comments)', async () => {
@ -72,7 +72,7 @@ describe('ssr: slot', () => {
template: `<one><!--he\nllo--></one>`, template: `<one><!--he\nllo--></one>`,
}), }),
), ),
).toBe(`<div><!--[--><!--]--><!--slot--></div>`) ).toBe(`<div><!--[--><!--]--></div>`)
}) })
test('multiple elements', async () => { test('multiple elements', async () => {
@ -83,7 +83,7 @@ describe('ssr: slot', () => {
template: `<one><div>one</div><div>two</div></one>`, template: `<one><div>one</div><div>two</div></one>`,
}), }),
), ),
).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--><!--slot--></div>`) ).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--></div>`)
}) })
test('fragment slot (template v-if)', async () => { test('fragment slot (template v-if)', async () => {
@ -94,9 +94,7 @@ describe('ssr: slot', () => {
template: `<one><template v-if="true">hello</template></one>`, template: `<one><template v-if="true">hello</template></one>`,
}), }),
), ),
).toBe( ).toBe(`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`)
`<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--><!--slot--></div>`,
)
}) })
test('fragment slot (template v-if + multiple elements)', async () => { test('fragment slot (template v-if + multiple elements)', async () => {
@ -108,7 +106,7 @@ describe('ssr: slot', () => {
}), }),
), ),
).toBe( ).toBe(
`<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--><!--slot--></div>`, `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--]--></div>`,
) )
}) })
@ -137,7 +135,7 @@ describe('ssr: slot', () => {
template: `<one><div v-if="true">foo</div></one>`, template: `<one><div v-if="true">foo</div></one>`,
}), }),
), ),
).toBe(`<div>foo</div><!--if-->`) ).toBe(`<div>foo</div>`)
}) })
// #9933 // #9933
@ -185,7 +183,7 @@ describe('ssr: slot', () => {
`, `,
}), }),
), ),
).toBe(`<div><!--[--> new header <!--]--><!--slot--></div>`) ).toBe(`<div><!--[--> new header <!--]--></div>`)
}) })
// #11326 // #11326
@ -204,9 +202,7 @@ describe('ssr: slot', () => {
template: `<ButtonComp><Wrap><div v-if="false">hello</div></Wrap></ButtonComp>`, template: `<ButtonComp><Wrap><div v-if="false">hello</div></Wrap></ButtonComp>`,
}), }),
), ),
).toBe( ).toBe(`<button><!--[--><div><!--[--><!--]--></div><!--]--></button>`)
`<button><!--[--><div><!--[--><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
)
expect( expect(
await renderToString( await renderToString(
@ -223,7 +219,7 @@ describe('ssr: slot', () => {
}), }),
), ),
).toBe( ).toBe(
`<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`, `<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`,
) )
expect( expect(
@ -237,6 +233,6 @@ describe('ssr: slot', () => {
template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`, template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`,
}), }),
), ),
).toBe(`<button><!--[--><!--]--></button><!--dynamic-component-->`) ).toBe(`<button><!--[--><!--]--></button>`)
}) })
}) })

View File

@ -5,7 +5,7 @@ import {
type SSRBufferItem, type SSRBufferItem,
renderVNodeChildren, renderVNodeChildren,
} from '../render' } from '../render'
import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared' import { isArray } from '@vue/shared'
const { ensureValidVNode } = ssrUtils const { ensureValidVNode } = ssrUtils
@ -37,7 +37,7 @@ export function ssrRenderSlot(
parentComponent, parentComponent,
slotScopeId, slotScopeId,
) )
push(`<!--]--><!--${SLOT_ANCHOR_LABEL}-->`) push(`<!--]-->`)
} }
export function ssrRenderSlotInner( export function ssrRenderSlotInner(
@ -104,7 +104,7 @@ export function ssrRenderSlotInner(
if ( if (
transition && transition &&
slotBuffer[0] === '<!--[-->' && slotBuffer[0] === '<!--[-->' &&
(slotBuffer[end - 1] as string).startsWith('<!--]-->') slotBuffer[end - 1] === '<!--]-->'
) { ) {
start++ start++
end-- end--

View File

@ -1,17 +1,24 @@
export const BLOCK_START_ANCHOR_LABEL = '[[' export const BLOCK_INSERTION_ANCHOR_LABEL = 'i'
export const BLOCK_END_ANCHOR_LABEL = ']]' export const BLOCK_APPEND_ANCHOR_LABEL = 'a'
export const BLOCK_PREPEND_ANCHOR_LABEL = 'p'
export const IF_ANCHOR_LABEL: string = 'if' export const IF_ANCHOR_LABEL: string = 'if'
export const ELSE_IF_ANCHOR_LABEL: string = 'else-if' export const ELSE_IF_ANCHOR_LABEL: string = 'else-if'
export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component' export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component'
export const FOR_ANCHOR_LABEL: string = 'for' export const FOR_ANCHOR_LABEL: string = 'for'
export const SLOT_ANCHOR_LABEL: string = 'slot' export const SLOT_ANCHOR_LABEL: string = 'slot'
export function isBlockAnchor(node: Node): node is Comment { export function isInsertionAnchor(node: Node): node is Comment {
if (node.nodeType !== 8) return false if (node.nodeType !== 8) return false
const data = (node as Comment).data const data = (node as Comment).data
return data === BLOCK_START_ANCHOR_LABEL || data === BLOCK_END_ANCHOR_LABEL return (
data === `[${BLOCK_INSERTION_ANCHOR_LABEL}` ||
data === `${BLOCK_INSERTION_ANCHOR_LABEL}]` ||
data === `[${BLOCK_APPEND_ANCHOR_LABEL}` ||
data === `${BLOCK_APPEND_ANCHOR_LABEL}]` ||
data === `[${BLOCK_PREPEND_ANCHOR_LABEL}` ||
data === `${BLOCK_PREPEND_ANCHOR_LABEL}]`
)
} }
export function isVaporFragmentAnchor(node: Node): node is Comment { export function isVaporFragmentAnchor(node: Node): node is Comment {
@ -27,5 +34,5 @@ export function isVaporFragmentAnchor(node: Node): node is Comment {
} }
export function isVaporAnchor(node: Node): node is Comment { export function isVaporAnchor(node: Node): node is Comment {
return isBlockAnchor(node) || isVaporFragmentAnchor(node) return isVaporFragmentAnchor(node) || isInsertionAnchor(node)
} }