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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,6 @@ describe('ssr: components', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) {
_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) {
_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) => {
_push(\`<span\${_scopeId}></span>\`)
})
_push(\`<!--]--><!--for--></div>\`)
_push(\`<!--if-->\`)
_push(\`<!--]--></div>\`)
} else {
_push(\`<!---->\`)
}
@ -270,8 +267,7 @@ describe('ssr: components', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<span\${_scopeId}></span>\`)
})
_push(\`<!--]--><!--for--></div>\`)
_push(\`<!--if-->\`)
_push(\`<!--]--></div>\`)
} else {
_push(\`<!---->\`)
}
@ -365,7 +361,6 @@ describe('ssr: components', () => {
_push(\`\`)
if (false) {
_push(\`<div\${_scopeId}></div>\`)
_push(\`<!--if-->\`)
} else {
_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(\`<!--[-->\`)
if (true) {
_push(\`<div></div>\`)
_push(\`<!--if-->\`)
} else {
_push(\`<!---->\`)
}

View File

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

View File

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

View File

@ -15,7 +15,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for--><!--]-->\`)
_push(\`<!--]-->\`)
}"
`)
})
@ -33,7 +33,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for--></ul>\`)
_push(\`</ul>\`)
}"
`)
})
@ -52,10 +52,8 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for-->\`)
if (false) {
_push(\`<div></div>\`)
_push(\`<!--if-->\`)
}
_push(\`</ul>\`)
}"
@ -76,7 +74,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for--></ul>\`)
_push(\`</ul>\`)
}"
`)
})
@ -98,7 +96,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for--></\${_ctx.someTag}>\`)
_push(\`</\${_ctx.someTag}>\`)
}"
`)
})
@ -120,14 +118,11 @@ describe('transition-group', () => {
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for-->\`)
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for-->\`)
if (_ctx.ok) {
_push(\`<div>ok</div>\`)
_push(\`<!--if-->\`)
}
_push(\`<!--]-->\`)
}"

View File

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

View File

@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
}></option>\`)
})
_push(\`<!--]--><!--for--></select></div>\`)
_push(\`<!--]--></select></div>\`)
}"
`)
@ -91,7 +91,6 @@ describe('ssr: v-model', () => {
? _ssrLooseContain(_ctx.model, _ctx.i)
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
}></option>\`)
_push(\`<!--if-->\`)
} else {
_push(\`<!---->\`)
}
@ -191,7 +190,7 @@ describe('ssr: v-model', () => {
_ssrInterpolate(item)
}</option>\`)
})
_push(\`<!--]--><!--for--></optgroup></select></div>\`)
_push(\`<!--]--></optgroup></select></div>\`)
}"
`)
@ -217,7 +216,6 @@ describe('ssr: v-model', () => {
}>\${
_ssrInterpolate(_ctx.item)
}</option>\`)
_push(\`<!--if-->\`)
} else {
_push(\`<!---->\`)
}
@ -252,8 +250,7 @@ describe('ssr: v-model', () => {
_ssrInterpolate(item)
}</option>\`)
})
_push(\`<!--]--><!--for-->\`)
_push(\`<!--if-->\`)
_push(\`<!--]-->\`)
} else {
_push(\`<!---->\`)
}
@ -287,13 +284,12 @@ describe('ssr: v-model', () => {
}>\${
_ssrInterpolate(item)
}</option>\`)
_push(\`<!--if-->\`)
} else {
_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'
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
// fallthrough attr. This results in less noise in generated snapshots
// 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(
/_push\(\`<div\${\s*_ssrRenderAttrs\(_attrs\)\s*}>([^]*)<\/div>\`\)/,
)

View File

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

View File

@ -43,6 +43,7 @@ import {
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
import {
type SSRTransformContext,
processBlockNodeAnchor,
processChildren,
processChildrenAsStatement,
} from '../ssrCodegenTransform'
@ -271,11 +272,14 @@ export function ssrProcessComponent(
// dynamic component (`resolveDynamicComponent` call)
// the codegen node is a `renderVNode` call
context.pushStatement(node.ssrCodegenNode)
// anchor for dynamic component for vapor hydration
// anchor for vapor dynamic component
if (context.options.vapor) {
context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`)
}
}
}
}
export const rawOptionsMap: WeakMap<RootNode, CompilerOptions> = new WeakMap<
RootNode,
@ -339,6 +343,11 @@ function createVNodeSlotBranch(
loc: locStub,
codegenNode: undefined,
}
if (parentContext.vapor) {
injectVaporInsertionAnchors(children)
}
subTransform(wrapperNode, subOptions, parentContext)
return createReturnStatement(children)
}
@ -380,6 +389,71 @@ function subTransform(
// - 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 {
if (isArray(v)) {
return v.map(clone)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -168,33 +168,19 @@ function genInsertionState(
operation: InsertionStateTypes,
context: CodegenContext,
): CodeFragment[] {
const { seenChildIndexes } = context
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),
)
}
const { parent, anchor } = operation
return [
NEWLINE,
...genCall(
context.helper('setInsertionState'),
`n${parent}`,
insertionAnchor,
index ? `${index}` : undefined,
anchor == null
? 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
let offset = 0
let ifBranchCount = 0
let prev: [variable: string, elementIndex: number] | undefined
for (const [index, child] of children.entries()) {
if (child.isIfBranch) ifBranchCount++
if (child.flags & DynamicFlag.NON_TEMPLATE) {
offset--
}
@ -87,29 +85,11 @@ export function genChildren(
pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
}
} 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) {
pushBlock(...genCall(helper('child'), from, childIndex))
pushBlock(...genCall(helper('child'), from))
} else {
// 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) {
init = genCall(helper('next'), init)
} else if (elementIndex > 1) {

View File

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

View File

@ -70,30 +70,12 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
if (prevDynamics.length) {
if (hasStaticTemplate) {
// each dynamic child gets its own placeholder node.
// this makes it easier to locate the corresponding node during hydration.
for (let i = 0; i < prevDynamics.length; i++) {
const idx = index - prevDynamics.length + i
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
}
}
context.childrenTemplate[index - prevDynamics.length] = `<!>`
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
const anchor = (prevDynamics[0].anchor = context.increaseId())
registerInsertion(prevDynamics, context, anchor)
} else {
registerInsertion(
prevDynamics,
context,
-1 /* prepend */,
getChildIndex(children, prevDynamics[0]),
)
registerInsertion(prevDynamics, context, -1 /* prepend */)
}
prevDynamics = []
}
@ -102,12 +84,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
}
if (prevDynamics.length) {
registerInsertion(
prevDynamics,
context,
undefined,
getChildIndex(children, prevDynamics[0]),
)
registerInsertion(prevDynamics, context, -2 /* append */)
}
}
@ -115,7 +92,6 @@ function registerInsertion(
dynamics: IRDynamicInfo[],
context: TransformContext,
anchor?: number,
childIndex?: number,
) {
for (const child of dynamics) {
if (child.template != null) {
@ -124,27 +100,12 @@ function registerInsertion(
type: IRNodeTypes.INSERT_NODE,
elements: dynamics.map(child => child.id!),
parent: context.reference(),
anchor,
anchor: anchor === -2 ? undefined : anchor,
})
} else if (child.operation && isBlockOperation(child.operation)) {
// block types
child.operation.parent = context.reference()
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 = {}
container.innerHTML = await renderToString(h(App), ctx)
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></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--><!--if--></div>',
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer.innerHTML).toBe(
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
@ -617,7 +617,7 @@ describe('SSR hydration', () => {
toggle.value = false
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('')
})
@ -660,21 +660,21 @@ describe('SSR hydration', () => {
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></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--><!--if--></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><!--if--></div>')
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
expect(teleportContainer.innerHTML).toBe('')
})
@ -713,7 +713,7 @@ describe('SSR hydration', () => {
// 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(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe('')
@ -721,7 +721,7 @@ describe('SSR hydration', () => {
// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<div><!--[[--><!--teleport start--><!--teleport end--><!--]]--></div>',
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
expect(teleportContainer2.innerHTML).toBe('')
@ -1005,7 +1005,7 @@ describe('SSR hydration', () => {
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><!--[[--><span>1</span><!--]]--><!--[[--><span>2</span><!--]]--></div>"`,
`"<div><span>1</span><span>2</span></div>"`,
)
// reset asyncDeps from ssr
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 () => {
const id = 'child-reload'
const Child = {
@ -1923,14 +1893,14 @@ describe('SSR hydration', () => {
const root = document.createElement('div')
root.innerHTML = await renderToString(h(App))
createSSRApp(App).mount(root)
expect(root.innerHTML).toBe('<div><!--[[--><div>foo</div><!--]]--></div>')
expect(root.innerHTML).toBe('<div><div>foo</div></div>')
reload(id, {
__hmrId: id,
template: `<div>bar</div>`,
})
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 () => {

View File

@ -32,7 +32,6 @@ import {
isRenderableAttrValue,
isReservedProp,
isString,
isVaporAnchor,
normalizeClass,
normalizeCssVarValue,
normalizeStyle,
@ -118,7 +117,7 @@ export function createHydrationFunctions(
o: {
patchProp,
createText,
nextSibling: next,
nextSibling,
parentNode,
remove,
insert,
@ -126,15 +125,6 @@ export function createHydrationFunctions(
},
} = 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) => {
if (!container.hasChildNodes()) {
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
@ -161,10 +151,6 @@ export function createHydrationFunctions(
slotScopeIds: string[] | null,
optimized = false,
): Node | null => {
// skip vapor mode specific anchors
if (isVaporAnchor(node)) {
node = nextSibling(node)!
}
optimized = optimized || !!vnode.dynamicChildren
const isFragmentStart = isComment(node) && node.data === '['
const onMismatch = () =>
@ -486,7 +472,7 @@ export function createHydrationFunctions(
// The SSRed DOM contains more nodes than it should. Remove them.
const cur = next
next = nextSibling(next)
next = next.nextSibling
remove(cur)
}
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
@ -592,7 +578,7 @@ export function createHydrationFunctions(
}
}
return nextSibling(el)
return el.nextSibling
}
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 {
insertionAnchor,
insertionChildIndex,
insertionParent,
resetInsertionState,
setInsertionState,
} from '../insertionState'
import {
__next,
__nthChild,
_child,
_next,
createTextNode,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
} 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 currentHydrationNode: Node | null = null
@ -24,21 +30,13 @@ export function setCurrentHydrationNode(node: Node | null): void {
function findParentSibling(n: Node): Node | null {
if (!n.parentNode) return null
let next = n.parentNode.nextSibling
while (next && isComment(next, BLOCK_END_ANCHOR_LABEL)) {
next = next.nextElementSibling
}
return next ? next : findParentSibling(n.parentNode)
return n.parentNode.nextSibling || findParentSibling(n.parentNode)
}
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
const ret = next || node.$ps || (node.$ps = findParentSibling(node))
const ret =
node.nextSibling || node.$ps || (node.$ps = findParentSibling(node))
if (ret) setCurrentHydrationNode(ret)
}
@ -54,22 +52,26 @@ function performHydration<T>(
locateHydrationNode = locateHydrationNodeImpl
// optimize anchor cache lookup
;(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
}
enableHydrationNodeLookup()
isHydrating = true
isHydratingStack.push((isHydrating = true))
setup()
const res = fn()
cleanup()
currentHydrationNode = null
isHydrating = false
isHydratingStack.pop()
isHydrating = isHydratingStack[isHydratingStack.length - 1] || false
disableHydrationNodeLookup()
return res
}
export function withHydration(container: ParentNode, fn: () => void): void {
const setup = () => setInsertionState(container, 0)
// @ts-expect-error
const setup = () => setInsertionState(container, -1)
const cleanup = () => resetInsertionState()
return performHydration(fn, setup, cleanup)
}
@ -136,20 +138,32 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
function locateHydrationNodeImpl(): void {
let node: Node | null
// prepend / firstChild
if (insertionAnchor === 0) {
const n = insertionParent!.$np || 0
node = __nthChild(insertionParent!, n)
insertionParent!.$np = n + 1
// @ts-expect-error
if (insertionAnchor === -1) {
// firstChild
node = _child(insertionParent!)!
} else if (insertionAnchor === 0) {
// prepend
node = insertionParent!.$pa = locateHydrationNodeByAnchor(
insertionParent!.$pa || _child(insertionParent!),
BLOCK_PREPEND_ANCHOR_LABEL,
)!
} else if (insertionAnchor) {
// `insertionAnchor` is a Node, it is the DOM node to hydrate
// Template: `...<span/><!----><span/>...`// `insertionAnchor` is the placeholder
// SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
node = insertionAnchor
// insertion anchor
node = insertionParent!.$ia = locateHydrationNodeByAnchor(
insertionParent!.$ia || _child(insertionParent!),
BLOCK_INSERTION_ANCHOR_LABEL,
)!
} else if (insertionAnchor === null) {
// append anchor
node = insertionParent!.$aa = locateHydrationNodeByAnchor(
insertionParent!.$aa || _child(insertionParent!),
BLOCK_APPEND_ANCHOR_LABEL,
)!
} else {
node = currentHydrationNode
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 {
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 {
BLOCK_END_ANCHOR_LABEL,
BLOCK_START_ANCHOR_LABEL,
BLOCK_INSERTION_ANCHOR_LABEL,
BLOCK_PREPEND_ANCHOR_LABEL,
isVaporAnchor,
} from '@vue/shared'
@ -25,6 +25,29 @@ export function querySelector(selectors: string): Element | null {
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__ */
export function _child(node: ParentNode): Node {
return node.firstChild!
@ -60,16 +83,19 @@ export function _child(node: ParentNode): Node {
*/
/*! #__NO_SIDE_EFFECTS__ */
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
// since server-side rendering doesn't generate whitespace placeholder text nodes,
// if firstChild is null, manually insert a text node and return it
if (offset === -1 && !node.firstChild) {
if (offset === -1 && !n) {
node.textContent = ' '
return node.firstChild!
}
let n = offset ? __nthChild(node, offset) : node.firstChild!
while (n && (isComment(n, '[') || isVaporAnchor(n))) {
// skip block node
n = skipBlockNodes(n) as ChildNode
if (isComment(n, '[')) {
n = locateEndAnchor(n)!.nextSibling!
} else {
@ -90,7 +116,7 @@ export function _nthChild(node: Node, i: number): Node {
*/
/*! #__NO_SIDE_EFFECTS__ */
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++) {
n = __next(n) as ChildNode
}
@ -105,7 +131,7 @@ export function _next(node: Node): Node {
/**
* 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
* return a comment node or an internal node of a fragment instead of skipping
* the entire fragment block.
@ -144,20 +170,13 @@ export function _next(node: Node): Node {
*/
/*! #__NO_SIDE_EFFECTS__ */
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
else if (isComment(node, '[')) {
if (isComment(node, '[')) {
node = locateEndAnchor(node)!
}
node = skipBlockNodes(node)
let n = node.nextSibling!
while (n && isNonHydrationNode(n)) {
n = n.nextSibling!

View File

@ -1,14 +1,11 @@
export let insertionParent:
| (ParentNode & {
// number of prepends - hydration only
// consecutive prepends need to skip nodes that were prepended earlier
// each prepend increases the value of $prepend
$np?: number
$pa?: Node
$ia?: Node
$aa?: Node
})
| undefined
export let insertionAnchor: Node | 0 | undefined
export let insertionChildIndex: number | undefined
export let insertionAnchor: Node | 0 | undefined | null
/**
* This function is called before a block type that requires insertion
@ -17,14 +14,12 @@ export let insertionChildIndex: number | undefined
*/
export function setInsertionState(
parent: ParentNode,
anchor?: Node | 0,
offset?: number,
anchor?: Node | 0 | null,
): void {
insertionParent = parent
insertionAnchor = anchor
insertionChildIndex = offset
}
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(
`<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` +
`<!--slot--></div></div>`,
`</div></div>`,
)
// test fallback
@ -461,7 +461,7 @@ function testRender(type: string, render: typeof renderToString) {
}),
),
).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(
`<div>parent<div class="child">` +
`<!--[--><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(
`<div>parent<div class="child">` +
`<!--[--><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(
`<div><!--[--><!--[-->hello<!--]--><!--slot--><!--]--><!--slot--></div>`,
`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`,
)
})
@ -593,7 +593,7 @@ function testRender(type: string, render: typeof renderToString) {
expect(await render(app)).toBe(
// 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(
// 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"/>`,
}
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(
`<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>`,
}),
),
).toBe(
`<div><!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
)
).toBe(`<div><!--[--><span>slot</span><!--]--></div>`)
})
test('resolved to component with v-show', async () => {
@ -32,7 +30,7 @@ describe('ssr: dynamic component', () => {
}),
),
).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>`,
}),
),
).toBe(`<p><span>slot</span></p><!--dynamic-component-->`)
).toBe(`<p><span>slot</span></p>`)
})
test('resolve to component vnode', async () => {
@ -62,9 +60,7 @@ describe('ssr: dynamic component', () => {
template: `<component :is="vnode"><span>slot</span></component>`,
}),
),
).toBe(
`<div>test<!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
)
).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`)
})
test('resolve to element vnode', async () => {
@ -79,6 +75,6 @@ describe('ssr: dynamic 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))
expect(result).toBe(
`<!--[--><div parent wrapper-s child></div><!--]--><!--slot-->`,
)
expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
})
// #2892
@ -152,8 +150,8 @@ describe('ssr: scopedId runtime behavior', () => {
const result = await renderToString(createApp(Root))
expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` +
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
`<!--slot--></div>`,
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
`</div>`,
)
})
@ -267,8 +265,8 @@ describe('ssr: scopedId runtime behavior', () => {
const result = await renderToString(createApp(Root))
expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` +
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
`<!--slot--></div>`,
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
`</div>`,
)
})
})

View File

@ -16,7 +16,7 @@ describe('ssr: slot', () => {
template: `<one>hello</one>`,
}),
),
).toBe(`<div><!--[-->hello<!--]--><!--slot--></div>`)
).toBe(`<div><!--[-->hello<!--]--></div>`)
})
test('element slot', async () => {
@ -27,7 +27,7 @@ describe('ssr: slot', () => {
template: `<one><div>hi</div></one>`,
}),
),
).toBe(`<div><!--[--><div>hi</div><!--]--><!--slot--></div>`)
).toBe(`<div><!--[--><div>hi</div><!--]--></div>`)
})
test('empty slot', async () => {
@ -42,7 +42,7 @@ describe('ssr: slot', () => {
template: `<one><template v-if="false"/></one>`,
}),
),
).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
).toBe(`<div><!--[--><!--]--></div>`)
})
test('empty slot (manual comments)', async () => {
@ -57,7 +57,7 @@ describe('ssr: slot', () => {
template: `<one><!--hello--></one>`,
}),
),
).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
).toBe(`<div><!--[--><!--]--></div>`)
})
test('empty slot (multi-line comments)', async () => {
@ -72,7 +72,7 @@ describe('ssr: slot', () => {
template: `<one><!--he\nllo--></one>`,
}),
),
).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
).toBe(`<div><!--[--><!--]--></div>`)
})
test('multiple elements', async () => {
@ -83,7 +83,7 @@ describe('ssr: slot', () => {
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 () => {
@ -94,9 +94,7 @@ describe('ssr: slot', () => {
template: `<one><template v-if="true">hello</template></one>`,
}),
),
).toBe(
`<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--><!--slot--></div>`,
)
).toBe(`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`)
})
test('fragment slot (template v-if + multiple elements)', async () => {
@ -108,7 +106,7 @@ describe('ssr: slot', () => {
}),
),
).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>`,
}),
),
).toBe(`<div>foo</div><!--if-->`)
).toBe(`<div>foo</div>`)
})
// #9933
@ -185,7 +183,7 @@ describe('ssr: slot', () => {
`,
}),
),
).toBe(`<div><!--[--> new header <!--]--><!--slot--></div>`)
).toBe(`<div><!--[--> new header <!--]--></div>`)
})
// #11326
@ -204,9 +202,7 @@ describe('ssr: slot', () => {
template: `<ButtonComp><Wrap><div v-if="false">hello</div></Wrap></ButtonComp>`,
}),
),
).toBe(
`<button><!--[--><div><!--[--><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
)
).toBe(`<button><!--[--><div><!--[--><!--]--></div><!--]--></button>`)
expect(
await renderToString(
@ -223,7 +219,7 @@ describe('ssr: slot', () => {
}),
),
).toBe(
`<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
`<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`,
)
expect(
@ -237,6 +233,6 @@ describe('ssr: slot', () => {
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,
renderVNodeChildren,
} from '../render'
import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared'
import { isArray } from '@vue/shared'
const { ensureValidVNode } = ssrUtils
@ -37,7 +37,7 @@ export function ssrRenderSlot(
parentComponent,
slotScopeId,
)
push(`<!--]--><!--${SLOT_ANCHOR_LABEL}-->`)
push(`<!--]-->`)
}
export function ssrRenderSlotInner(
@ -104,7 +104,7 @@ export function ssrRenderSlotInner(
if (
transition &&
slotBuffer[0] === '<!--[-->' &&
(slotBuffer[end - 1] as string).startsWith('<!--]-->')
slotBuffer[end - 1] === '<!--]-->'
) {
start++
end--

View File

@ -1,17 +1,24 @@
export const BLOCK_START_ANCHOR_LABEL = '[['
export const BLOCK_END_ANCHOR_LABEL = ']]'
export const BLOCK_INSERTION_ANCHOR_LABEL = 'i'
export const BLOCK_APPEND_ANCHOR_LABEL = 'a'
export const BLOCK_PREPEND_ANCHOR_LABEL = 'p'
export const IF_ANCHOR_LABEL: string = 'if'
export const ELSE_IF_ANCHOR_LABEL: string = 'else-if'
export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component'
export const FOR_ANCHOR_LABEL: string = 'for'
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
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 {
@ -27,5 +34,5 @@ export function isVaporFragmentAnchor(node: Node): node is Comment {
}
export function isVaporAnchor(node: Node): node is Comment {
return isBlockAnchor(node) || isVaporFragmentAnchor(node)
return isVaporFragmentAnchor(node) || isInsertionAnchor(node)
}