mirror of https://github.com/vuejs/core.git
Merge f2a5abeced
into bb4ae25793
This commit is contained in:
commit
8bb02a4dc1
|
@ -39,6 +39,7 @@ 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-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ 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-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -244,7 +246,8 @@ describe('ssr: components', () => {
|
||||||
_ssrRenderList(list, (i) => {
|
_ssrRenderList(list, (i) => {
|
||||||
_push(\`<span\${_scopeId}></span>\`)
|
_push(\`<span\${_scopeId}></span>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]--></div>\`)
|
_push(\`<!--]--><!--for--></div>\`)
|
||||||
|
_push(\`<!--if-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -267,7 +270,8 @@ describe('ssr: components', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<span\${_scopeId}></span>\`)
|
_push(\`<span\${_scopeId}></span>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]--></div>\`)
|
_push(\`<!--]--><!--for--></div>\`)
|
||||||
|
_push(\`<!--if-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -361,6 +365,7 @@ describe('ssr: components', () => {
|
||||||
_push(\`\`)
|
_push(\`\`)
|
||||||
if (false) {
|
if (false) {
|
||||||
_push(\`<div\${_scopeId}></div>\`)
|
_push(\`<div\${_scopeId}></div>\`)
|
||||||
|
_push(\`<!--if-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -396,4 +396,50 @@ 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>\`"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,6 +29,7 @@ describe('ssr: attrs fallthrough', () => {
|
||||||
_push(\`<!--[-->\`)
|
_push(\`<!--[-->\`)
|
||||||
if (true) {
|
if (true) {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
|
_push(\`<!--if-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ 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)
|
||||||
|
|
|
@ -153,6 +153,7 @@ 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(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe('transition-group', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--for--><!--]-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -33,7 +33,7 @@ describe('transition-group', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`</ul>\`)
|
_push(\`<!--for--></ul>\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -52,8 +52,10 @@ 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>\`)
|
||||||
}"
|
}"
|
||||||
|
@ -74,7 +76,7 @@ describe('transition-group', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`</ul>\`)
|
_push(\`<!--for--></ul>\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -96,7 +98,7 @@ describe('transition-group', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`</\${_ctx.someTag}>\`)
|
_push(\`<!--for--></\${_ctx.someTag}>\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -118,11 +120,14 @@ 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(\`<!--]-->\`)
|
||||||
}"
|
}"
|
||||||
|
|
|
@ -10,7 +10,7 @@ describe('ssr: v-for', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -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(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -51,9 +51,9 @@ describe('ssr: v-for', () => {
|
||||||
_ssrInterpolate(j)
|
_ssrInterpolate(j)
|
||||||
}</div>\`)
|
}</div>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]--></div>\`)
|
_push(\`<!--]--><!--for--></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -68,7 +68,7 @@ describe('ssr: v-for', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
|
_push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -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(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -107,7 +107,7 @@ describe('ssr: v-for', () => {
|
||||||
_ssrInterpolate(i + 1)
|
_ssrInterpolate(i + 1)
|
||||||
}</span><!--]-->\`)
|
}</span><!--]-->\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -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(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,6 +8,7 @@ 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(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -23,6 +24,7 @@ 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(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -38,6 +40,7 @@ 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>\`)
|
||||||
}
|
}
|
||||||
|
@ -53,8 +56,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(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -70,8 +75,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>\`)
|
||||||
}
|
}
|
||||||
|
@ -82,15 +89,16 @@ describe('ssr: v-if', () => {
|
||||||
test('<template v-if> (text)', () => {
|
test('<template v-if> (text)', () => {
|
||||||
expect(compile(`<template v-if="foo">hello</template>`).code)
|
expect(compile(`<template v-if="foo">hello</template>`).code)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"
|
"
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<!--[-->hello<!--]-->\`)
|
_push(\`<!--[-->hello<!--]-->\`)
|
||||||
} else {
|
_push(\`<!--if-->\`)
|
||||||
_push(\`<!---->\`)
|
} else {
|
||||||
}
|
_push(\`<!---->\`)
|
||||||
}"
|
}
|
||||||
`)
|
}"
|
||||||
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('<template v-if> (single element)', () => {
|
test('<template v-if> (single element)', () => {
|
||||||
|
@ -102,6 +110,7 @@ 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(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -118,6 +127,7 @@ 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(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -137,7 +147,8 @@ describe('ssr: v-if', () => {
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--]--><!--for-->\`)
|
||||||
|
_push(\`<!--if-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -156,6 +167,7 @@ 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>\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
|
||||||
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
|
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
|
||||||
}></option>\`)
|
}></option>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]--></select></div>\`)
|
_push(\`<!--]--><!--for--></select></div>\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ 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(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
type IfStatement,
|
type IfStatement,
|
||||||
type JSChildNode,
|
type JSChildNode,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
|
type PlainElementNode,
|
||||||
type RootNode,
|
type RootNode,
|
||||||
type TemplateChildNode,
|
type TemplateChildNode,
|
||||||
type TemplateLiteral,
|
type TemplateLiteral,
|
||||||
|
@ -20,7 +21,12 @@ import {
|
||||||
isText,
|
isText,
|
||||||
processExpression,
|
processExpression,
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import { escapeHtml, isString } from '@vue/shared'
|
import {
|
||||||
|
DYNAMIC_END_ANCHOR_LABEL,
|
||||||
|
DYNAMIC_START_ANCHOR_LABEL,
|
||||||
|
escapeHtml,
|
||||||
|
isString,
|
||||||
|
} from '@vue/shared'
|
||||||
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
|
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
|
||||||
import { ssrProcessIf } from './transforms/ssrVIf'
|
import { ssrProcessIf } from './transforms/ssrVIf'
|
||||||
import { ssrProcessFor } from './transforms/ssrVFor'
|
import { ssrProcessFor } from './transforms/ssrVFor'
|
||||||
|
@ -157,13 +163,33 @@ export function processChildren(
|
||||||
asFragment = false,
|
asFragment = false,
|
||||||
disableNestedFragments = false,
|
disableNestedFragments = false,
|
||||||
disableComment = false,
|
disableComment = false,
|
||||||
|
asDynamic = false,
|
||||||
): void {
|
): void {
|
||||||
|
if (asDynamic) {
|
||||||
|
context.pushStringPart(`<!--${DYNAMIC_START_ANCHOR_LABEL}-->`)
|
||||||
|
}
|
||||||
if (asFragment) {
|
if (asFragment) {
|
||||||
context.pushStringPart(`<!--[-->`)
|
context.pushStringPart(`<!--[-->`)
|
||||||
}
|
}
|
||||||
const { children } = parent
|
|
||||||
|
const { children, type, tagType } = parent as PlainElementNode
|
||||||
|
const inElement =
|
||||||
|
type === NodeTypes.ELEMENT && tagType === ElementTypes.ELEMENT
|
||||||
|
if (inElement) processChildrenDynamicInfo(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 && shouldProcessChildAsDynamic(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) {
|
||||||
|
@ -237,6 +263,9 @@ export function processChildren(
|
||||||
if (asFragment) {
|
if (asFragment) {
|
||||||
context.pushStringPart(`<!--]-->`)
|
context.pushStringPart(`<!--]-->`)
|
||||||
}
|
}
|
||||||
|
if (asDynamic) {
|
||||||
|
context.pushStringPart(`<!--${DYNAMIC_END_ANCHOR_LABEL}-->`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processChildrenAsStatement(
|
export function processChildrenAsStatement(
|
||||||
|
@ -249,3 +278,147 @@ export function processChildrenAsStatement(
|
||||||
processChildren(parent, childContext, asFragment)
|
processChildren(parent, childContext, asFragment)
|
||||||
return createBlockStatement(childContext.body)
|
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 DynamicInfo {
|
||||||
|
hasStaticPrevious: boolean
|
||||||
|
hasStaticNext: boolean
|
||||||
|
prevDynamicCount: number
|
||||||
|
nextDynamicCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function processChildrenDynamicInfo(
|
||||||
|
children: (TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo })[],
|
||||||
|
): void {
|
||||||
|
const filteredChildren = children.filter(
|
||||||
|
child => !(child.type === NodeTypes.TEXT && !child.content.trim()),
|
||||||
|
)
|
||||||
|
|
||||||
|
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._ssrDynamicInfo = {
|
||||||
|
hasStaticPrevious: false,
|
||||||
|
hasStaticNext: false,
|
||||||
|
prevDynamicCount: 0,
|
||||||
|
nextDynamicCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = child._ssrDynamicInfo
|
||||||
|
|
||||||
|
// Calculate the previous static and dynamic node counts
|
||||||
|
let foundStaticPrev = false
|
||||||
|
let dynamicCountPrev = 0
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
const prevChild = filteredChildren[j]
|
||||||
|
if (isStaticChildNode(prevChild)) {
|
||||||
|
foundStaticPrev = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// if the previous child has dynamic info, use it
|
||||||
|
else if (prevChild._ssrDynamicInfo) {
|
||||||
|
foundStaticPrev = prevChild._ssrDynamicInfo.hasStaticPrevious
|
||||||
|
dynamicCountPrev = prevChild._ssrDynamicInfo.prevDynamicCount + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dynamicCountPrev++
|
||||||
|
}
|
||||||
|
info.hasStaticPrevious = foundStaticPrev
|
||||||
|
info.prevDynamicCount = dynamicCountPrev
|
||||||
|
|
||||||
|
// Calculate the number of static and dynamic nodes afterwards
|
||||||
|
let foundStaticNext = false
|
||||||
|
let dynamicCountNext = 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 dynamic info, use it
|
||||||
|
else if (nextChild._ssrDynamicInfo) {
|
||||||
|
foundStaticNext = nextChild._ssrDynamicInfo.hasStaticNext
|
||||||
|
dynamicCountNext = nextChild._ssrDynamicInfo.nextDynamicCount + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dynamicCountNext++
|
||||||
|
}
|
||||||
|
info.hasStaticNext = foundStaticNext
|
||||||
|
info.nextDynamicCount = dynamicCountNext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a node should be processed as dynamic child.
|
||||||
|
* This is primarily used in Vapor mode hydration to wrap dynamic parts
|
||||||
|
* with markers (`<!--[[-->` and `<!--]]-->`).
|
||||||
|
* The purpose is to distinguish the boundaries of nodes during vapor hydration
|
||||||
|
*
|
||||||
|
* 1. two consecutive dynamic nodes should only wrap the second one
|
||||||
|
* <element>
|
||||||
|
* <element/> // Static node
|
||||||
|
* <Comp/> // Dynamic node -> should NOT be wrapped
|
||||||
|
* <Comp/> // Dynamic node -> should be wrapped
|
||||||
|
* <element/> // Static node
|
||||||
|
* </element>
|
||||||
|
*
|
||||||
|
* 2. three or more consecutive dynamic nodes should only wrap the
|
||||||
|
* middle nodes, leaving the first and last static.
|
||||||
|
* <element>
|
||||||
|
* <element/> // Static node
|
||||||
|
* <Comp/> // Dynamic node -> should NOT be wrapped
|
||||||
|
* <Comp/> // Dynamic node -> should be wrapped
|
||||||
|
* <Comp/> // Dynamic node -> should be wrapped
|
||||||
|
* <Comp/> // Dynamic node -> should NOT be wrapped
|
||||||
|
* <element/> // Static node
|
||||||
|
* </element>
|
||||||
|
*/
|
||||||
|
function shouldProcessChildAsDynamic(
|
||||||
|
parent: { tag?: string; children: TemplateChildNode[] },
|
||||||
|
node: TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo },
|
||||||
|
): boolean {
|
||||||
|
// must be inside a parent element
|
||||||
|
if (!parent.tag) return false
|
||||||
|
|
||||||
|
// must has dynamic info
|
||||||
|
const { _ssrDynamicInfo: info } = node
|
||||||
|
if (!info) return false
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasStaticPrevious,
|
||||||
|
hasStaticNext,
|
||||||
|
prevDynamicCount,
|
||||||
|
nextDynamicCount,
|
||||||
|
} = info
|
||||||
|
|
||||||
|
// must have static nodes on both sides
|
||||||
|
if (!hasStaticPrevious || !hasStaticNext) return false
|
||||||
|
|
||||||
|
const dynamicNodeCount = 1 + prevDynamicCount + nextDynamicCount
|
||||||
|
|
||||||
|
// For two consecutive dynamic nodes, mark the second one as dynamic
|
||||||
|
if (dynamicNodeCount === 2) {
|
||||||
|
return prevDynamicCount > 0
|
||||||
|
}
|
||||||
|
// For three or more dynamic nodes, mark the middle nodes as dynamic
|
||||||
|
else if (dynamicNodeCount >= 3) {
|
||||||
|
return prevDynamicCount > 0 && nextDynamicCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFragmentChild(child: TemplateChildNode): boolean {
|
||||||
|
const { type } = child
|
||||||
|
return type === NodeTypes.IF || type === NodeTypes.FOR
|
||||||
|
}
|
||||||
|
|
|
@ -55,7 +55,14 @@ import {
|
||||||
ssrProcessTransitionGroup,
|
ssrProcessTransitionGroup,
|
||||||
ssrTransformTransitionGroup,
|
ssrTransformTransitionGroup,
|
||||||
} from './ssrTransformTransitionGroup'
|
} from './ssrTransformTransitionGroup'
|
||||||
import { extend, isArray, isObject, isPlainObject, isSymbol } from '@vue/shared'
|
import {
|
||||||
|
DYNAMIC_COMPONENT_ANCHOR_LABEL,
|
||||||
|
extend,
|
||||||
|
isArray,
|
||||||
|
isObject,
|
||||||
|
isPlainObject,
|
||||||
|
isSymbol,
|
||||||
|
} from '@vue/shared'
|
||||||
import { buildSSRProps } from './ssrTransformElement'
|
import { buildSSRProps } from './ssrTransformElement'
|
||||||
import {
|
import {
|
||||||
ssrProcessTransition,
|
ssrProcessTransition,
|
||||||
|
@ -264,6 +271,8 @@ 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}-->`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
processChildrenAsStatement,
|
processChildrenAsStatement,
|
||||||
} from '../ssrCodegenTransform'
|
} from '../ssrCodegenTransform'
|
||||||
import { SSR_RENDER_LIST } from '../runtimeHelpers'
|
import { SSR_RENDER_LIST } from '../runtimeHelpers'
|
||||||
|
import { FOR_ANCHOR_LABEL } from '@vue/shared'
|
||||||
|
|
||||||
// Plugin for the first transform pass, which simply constructs the AST node
|
// Plugin for the first transform pass, which simply constructs the AST node
|
||||||
export const ssrTransformFor: NodeTransform =
|
export const ssrTransformFor: NodeTransform =
|
||||||
|
@ -49,4 +50,6 @@ export function ssrProcessFor(
|
||||||
if (!disableNestedFragments) {
|
if (!disableNestedFragments) {
|
||||||
context.pushStringPart(`<!--]-->`)
|
context.pushStringPart(`<!--]-->`)
|
||||||
}
|
}
|
||||||
|
// v-for anchor for vapor hydration
|
||||||
|
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
type SSRTransformContext,
|
type SSRTransformContext,
|
||||||
processChildrenAsStatement,
|
processChildrenAsStatement,
|
||||||
} from '../ssrCodegenTransform'
|
} from '../ssrCodegenTransform'
|
||||||
|
import { IF_ANCHOR_LABEL } from '@vue/shared'
|
||||||
|
|
||||||
// Plugin for the first transform pass, which simply constructs the AST node
|
// Plugin for the first transform pass, which simply constructs the AST node
|
||||||
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
|
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
|
||||||
|
@ -74,5 +75,16 @@ function processIfBranch(
|
||||||
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
||||||
// optimize away nested fragments when the only child is a ForNode
|
// optimize away nested fragments when the only child is a ForNode
|
||||||
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
||||||
return processChildrenAsStatement(branch, context, needFragmentWrapper)
|
const statement = processChildrenAsStatement(
|
||||||
|
branch,
|
||||||
|
context,
|
||||||
|
needFragmentWrapper,
|
||||||
|
)
|
||||||
|
if (branch.condition) {
|
||||||
|
// v-if/v-else-if anchor for vapor hydration
|
||||||
|
statement.body.push(
|
||||||
|
createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return statement
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,10 @@ export function genSelf(
|
||||||
context: CodegenContext,
|
context: CodegenContext,
|
||||||
): CodeFragment[] {
|
): CodeFragment[] {
|
||||||
const [frag, push] = buildCodeFragment()
|
const [frag, push] = buildCodeFragment()
|
||||||
const { id, template, operation } = dynamic
|
const { id, template, operation, dynamicChildOffset } = dynamic
|
||||||
|
|
||||||
if (id !== undefined && template !== undefined) {
|
if (id !== undefined && template !== undefined) {
|
||||||
push(NEWLINE, `const n${id} = t${template}()`)
|
push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
|
||||||
push(...genDirectivesForElement(id, context))
|
push(...genDirectivesForElement(id, context))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -259,6 +259,7 @@ export interface IRDynamicInfo {
|
||||||
children: IRDynamicInfo[]
|
children: IRDynamicInfo[]
|
||||||
template?: number
|
template?: number
|
||||||
hasDynamicChild?: boolean
|
hasDynamicChild?: boolean
|
||||||
|
dynamicChildOffset?: number
|
||||||
operation?: OperationNode
|
operation?: OperationNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => {
|
||||||
|
|
||||||
function processDynamicChildren(context: TransformContext<ElementNode>) {
|
function processDynamicChildren(context: TransformContext<ElementNode>) {
|
||||||
let prevDynamics: IRDynamicInfo[] = []
|
let prevDynamics: IRDynamicInfo[] = []
|
||||||
let hasStaticTemplate = false
|
let staticCount = 0
|
||||||
const children = context.dynamic.children
|
const children = context.dynamic.children
|
||||||
|
|
||||||
for (const [index, child] of children.entries()) {
|
for (const [index, child] of children.entries()) {
|
||||||
|
@ -69,22 +69,36 @@ 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 (staticCount) {
|
||||||
context.childrenTemplate[index - prevDynamics.length] = `<!>`
|
// each dynamic child gets its own placeholder node.
|
||||||
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
|
// this makes it easier to locate the corresponding node during hydration.
|
||||||
const anchor = (prevDynamics[0].anchor = context.increaseId())
|
for (let i = 0; i < prevDynamics.length; i++) {
|
||||||
registerInsertion(prevDynamics, context, anchor)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
registerInsertion(prevDynamics, context, -1 /* prepend */)
|
registerInsertion(prevDynamics, context, -1 /* prepend */)
|
||||||
}
|
}
|
||||||
prevDynamics = []
|
prevDynamics = []
|
||||||
}
|
}
|
||||||
hasStaticTemplate = true
|
staticCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevDynamics.length) {
|
if (prevDynamics.length) {
|
||||||
registerInsertion(prevDynamics, context)
|
registerInsertion(prevDynamics, context)
|
||||||
|
context.dynamic.dynamicChildOffset = staticCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--if--></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--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--if--></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></div>')
|
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></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--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--if--></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--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--if--></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></div>')
|
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
|
||||||
expect(teleportContainer.innerHTML).toBe('')
|
expect(teleportContainer.innerHTML).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1846,6 +1846,36 @@ 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 = {
|
||||||
|
|
|
@ -187,6 +187,8 @@ export interface VaporInteropInterface {
|
||||||
unmount(vnode: VNode, doRemove?: boolean): void
|
unmount(vnode: VNode, doRemove?: boolean): void
|
||||||
move(vnode: VNode, container: any, anchor: any): void
|
move(vnode: VNode, container: any, anchor: any): void
|
||||||
slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
|
slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
|
||||||
|
hydrate(node: Node, fn: () => void): void
|
||||||
|
hydrateSlot(vnode: VNode, container: any): void
|
||||||
|
|
||||||
vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
|
vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
|
||||||
vdomUnmount: UnmountComponentFn
|
vdomUnmount: UnmountComponentFn
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
Comment as VComment,
|
Comment as VComment,
|
||||||
type VNode,
|
type VNode,
|
||||||
type VNodeHook,
|
type VNodeHook,
|
||||||
|
VaporSlot,
|
||||||
createTextVNode,
|
createTextVNode,
|
||||||
createVNode,
|
createVNode,
|
||||||
invokeVNodeHook,
|
invokeVNodeHook,
|
||||||
|
@ -31,11 +32,16 @@ import {
|
||||||
isRenderableAttrValue,
|
isRenderableAttrValue,
|
||||||
isReservedProp,
|
isReservedProp,
|
||||||
isString,
|
isString,
|
||||||
|
isVaporAnchors,
|
||||||
normalizeClass,
|
normalizeClass,
|
||||||
normalizeStyle,
|
normalizeStyle,
|
||||||
stringifyStyle,
|
stringifyStyle,
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { type RendererInternals, needTransition } from './renderer'
|
import {
|
||||||
|
type RendererInternals,
|
||||||
|
getVaporInterface,
|
||||||
|
needTransition,
|
||||||
|
} from './renderer'
|
||||||
import { setRef } from './rendererTemplateRef'
|
import { setRef } from './rendererTemplateRef'
|
||||||
import {
|
import {
|
||||||
type SuspenseBoundary,
|
type SuspenseBoundary,
|
||||||
|
@ -111,7 +117,7 @@ export function createHydrationFunctions(
|
||||||
o: {
|
o: {
|
||||||
patchProp,
|
patchProp,
|
||||||
createText,
|
createText,
|
||||||
nextSibling,
|
nextSibling: next,
|
||||||
parentNode,
|
parentNode,
|
||||||
remove,
|
remove,
|
||||||
insert,
|
insert,
|
||||||
|
@ -119,6 +125,15 @@ export function createHydrationFunctions(
|
||||||
},
|
},
|
||||||
} = rendererInternals
|
} = rendererInternals
|
||||||
|
|
||||||
|
function nextSibling(node: Node) {
|
||||||
|
let n = next(node)
|
||||||
|
// skip vapor mode specific anchors
|
||||||
|
if (n && isVaporAnchors(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__) &&
|
||||||
|
@ -145,6 +160,10 @@ export function createHydrationFunctions(
|
||||||
slotScopeIds: string[] | null,
|
slotScopeIds: string[] | null,
|
||||||
optimized = false,
|
optimized = false,
|
||||||
): Node | null => {
|
): Node | null => {
|
||||||
|
// skip vapor mode specific anchors
|
||||||
|
if (isVaporAnchors(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 = () =>
|
||||||
|
@ -258,6 +277,12 @@ export function createHydrationFunctions(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case VaporSlot:
|
||||||
|
getVaporInterface(parentComponent, vnode).hydrateSlot(
|
||||||
|
vnode,
|
||||||
|
parentNode(node)!,
|
||||||
|
)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||||
if (
|
if (
|
||||||
|
@ -278,10 +303,6 @@ export function createHydrationFunctions(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||||
if ((vnode.type as ConcreteComponent).__vapor) {
|
|
||||||
throw new Error('Vapor component hydration is not supported yet.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// when setting up the render effect, if the initial vnode already
|
// when setting up the render effect, if the initial vnode already
|
||||||
// has .el set, the component will perform hydration instead of mount
|
// has .el set, the component will perform hydration instead of mount
|
||||||
// on its sub-tree.
|
// on its sub-tree.
|
||||||
|
@ -302,15 +323,23 @@ export function createHydrationFunctions(
|
||||||
nextNode = nextSibling(node)
|
nextNode = nextSibling(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
mountComponent(
|
// hydrate vapor component
|
||||||
vnode,
|
if ((vnode.type as ConcreteComponent).__vapor) {
|
||||||
container,
|
const vaporInterface = getVaporInterface(parentComponent, vnode)
|
||||||
null,
|
vaporInterface.hydrate(node, () => {
|
||||||
parentComponent,
|
vaporInterface.mount(vnode, container, null, parentComponent)
|
||||||
parentSuspense,
|
})
|
||||||
getContainerType(container),
|
} else {
|
||||||
optimized,
|
mountComponent(
|
||||||
)
|
vnode,
|
||||||
|
container,
|
||||||
|
null,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
getContainerType(container),
|
||||||
|
optimized,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// #3787
|
// #3787
|
||||||
// if component is async, it may get moved / unmounted before its
|
// if component is async, it may get moved / unmounted before its
|
||||||
|
@ -451,7 +480,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 = next.nextSibling
|
next = nextSibling(next)
|
||||||
remove(cur)
|
remove(cur)
|
||||||
}
|
}
|
||||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||||
|
@ -553,7 +582,7 @@ export function createHydrationFunctions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return el.nextSibling
|
return nextSibling(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateChildren = (
|
const hydrateChildren = (
|
||||||
|
|
|
@ -107,6 +107,7 @@ export interface Renderer<HostElement = RendererElement> {
|
||||||
|
|
||||||
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
|
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
|
||||||
hydrate: RootHydrateFunction
|
hydrate: RootHydrateFunction
|
||||||
|
hydrateNode: ReturnType<typeof createHydrationFunctions>[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ElementNamespace = 'svg' | 'mathml' | undefined
|
export type ElementNamespace = 'svg' | 'mathml' | undefined
|
||||||
|
@ -2546,6 +2547,7 @@ function baseCreateRenderer(
|
||||||
return {
|
return {
|
||||||
render,
|
render,
|
||||||
hydrate,
|
hydrate,
|
||||||
|
hydrateNode,
|
||||||
internals,
|
internals,
|
||||||
createApp: createAppAPI(
|
createApp: createAppAPI(
|
||||||
mountApp,
|
mountApp,
|
||||||
|
@ -2665,7 +2667,10 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVaporInterface(
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function getVaporInterface(
|
||||||
instance: ComponentInternalInstance | null,
|
instance: ComponentInternalInstance | null,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
): VaporInteropInterface {
|
): VaporInteropInterface {
|
||||||
|
|
|
@ -319,7 +319,7 @@ export * from './jsx'
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export { ensureRenderer, normalizeContainer }
|
export { ensureRenderer, ensureHydrationRenderer, normalizeContainer }
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,8 @@ import {
|
||||||
insertionParent,
|
insertionParent,
|
||||||
resetInsertionState,
|
resetInsertionState,
|
||||||
} from './insertionState'
|
} from './insertionState'
|
||||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared'
|
||||||
|
import { isHydrating } from './dom/hydration'
|
||||||
|
|
||||||
export function createDynamicComponent(
|
export function createDynamicComponent(
|
||||||
getter: () => any,
|
getter: () => any,
|
||||||
|
@ -19,15 +20,12 @@ export function createDynamicComponent(
|
||||||
): VaporFragment {
|
): VaporFragment {
|
||||||
const _insertionParent = insertionParent
|
const _insertionParent = insertionParent
|
||||||
const _insertionAnchor = insertionAnchor
|
const _insertionAnchor = insertionAnchor
|
||||||
if (isHydrating) {
|
if (!isHydrating) resetInsertionState()
|
||||||
locateHydrationNode()
|
|
||||||
} else {
|
|
||||||
resetInsertionState()
|
|
||||||
}
|
|
||||||
|
|
||||||
const frag = __DEV__
|
const frag =
|
||||||
? new DynamicFragment('dynamic-component')
|
isHydrating || __DEV__
|
||||||
: new DynamicFragment()
|
? new DynamicFragment(DYNAMIC_COMPONENT_ANCHOR_LABEL)
|
||||||
|
: new DynamicFragment()
|
||||||
|
|
||||||
renderEffect(() => {
|
renderEffect(() => {
|
||||||
const value = getter()
|
const value = getter()
|
||||||
|
@ -46,6 +44,5 @@ export function createDynamicComponent(
|
||||||
if (!isHydrating && _insertionParent) {
|
if (!isHydrating && _insertionParent) {
|
||||||
insert(frag, _insertionParent, _insertionAnchor)
|
insert(frag, _insertionParent, _insertionAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
return frag
|
return frag
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,13 @@ import {
|
||||||
toReactive,
|
toReactive,
|
||||||
toReadonly,
|
toReadonly,
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { getSequence, isArray, isObject, isString } from '@vue/shared'
|
import {
|
||||||
|
FOR_ANCHOR_LABEL,
|
||||||
|
getSequence,
|
||||||
|
isArray,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
} from '@vue/shared'
|
||||||
import { createComment, createTextNode } from './dom/node'
|
import { createComment, createTextNode } from './dom/node'
|
||||||
import {
|
import {
|
||||||
type Block,
|
type Block,
|
||||||
|
@ -24,7 +30,12 @@ import { currentInstance, isVaporComponent } from './component'
|
||||||
import type { DynamicSlot } from './componentSlots'
|
import type { DynamicSlot } from './componentSlots'
|
||||||
import { renderEffect } from './renderEffect'
|
import { renderEffect } from './renderEffect'
|
||||||
import { VaporVForFlags } from '../../shared/src/vaporFlags'
|
import { VaporVForFlags } from '../../shared/src/vaporFlags'
|
||||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
import {
|
||||||
|
currentHydrationNode,
|
||||||
|
isHydrating,
|
||||||
|
locateHydrationNode,
|
||||||
|
locateVaporFragmentAnchor,
|
||||||
|
} from './dom/hydration'
|
||||||
import {
|
import {
|
||||||
insertionAnchor,
|
insertionAnchor,
|
||||||
insertionParent,
|
insertionParent,
|
||||||
|
@ -87,8 +98,20 @@ export const createFor = (
|
||||||
let oldBlocks: ForBlock[] = []
|
let oldBlocks: ForBlock[] = []
|
||||||
let newBlocks: ForBlock[]
|
let newBlocks: ForBlock[]
|
||||||
let parent: ParentNode | undefined | null
|
let parent: ParentNode | undefined | null
|
||||||
// TODO handle this in hydration
|
let parentAnchor: Node
|
||||||
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
|
if (isHydrating) {
|
||||||
|
parentAnchor = locateVaporFragmentAnchor(
|
||||||
|
currentHydrationNode!,
|
||||||
|
FOR_ANCHOR_LABEL,
|
||||||
|
)!
|
||||||
|
if (__DEV__ && !parentAnchor) {
|
||||||
|
// this should not happen
|
||||||
|
throw new Error(`v-for fragment anchor node was not found.`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parentAnchor = __DEV__ ? createComment('for') : createTextNode()
|
||||||
|
}
|
||||||
|
|
||||||
const frag = new VaporFragment(oldBlocks)
|
const frag = new VaporFragment(oldBlocks)
|
||||||
const instance = currentInstance!
|
const instance = currentInstance!
|
||||||
const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE
|
const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { IF_ANCHOR_LABEL } from '@vue/shared'
|
||||||
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
|
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
|
||||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
import { isHydrating } from './dom/hydration'
|
||||||
import {
|
import {
|
||||||
insertionAnchor,
|
insertionAnchor,
|
||||||
insertionParent,
|
insertionParent,
|
||||||
|
@ -15,17 +16,16 @@ export function createIf(
|
||||||
): Block {
|
): Block {
|
||||||
const _insertionParent = insertionParent
|
const _insertionParent = insertionParent
|
||||||
const _insertionAnchor = insertionAnchor
|
const _insertionAnchor = insertionAnchor
|
||||||
if (isHydrating) {
|
if (!isHydrating) resetInsertionState()
|
||||||
locateHydrationNode()
|
|
||||||
} else {
|
|
||||||
resetInsertionState()
|
|
||||||
}
|
|
||||||
|
|
||||||
let frag: Block
|
let frag: Block
|
||||||
if (once) {
|
if (once) {
|
||||||
frag = condition() ? b1() : b2 ? b2() : []
|
frag = condition() ? b1() : b2 ? b2() : []
|
||||||
} else {
|
} else {
|
||||||
frag = __DEV__ ? new DynamicFragment('if') : new DynamicFragment()
|
frag =
|
||||||
|
isHydrating || __DEV__
|
||||||
|
? new DynamicFragment(IF_ANCHOR_LABEL)
|
||||||
|
: new DynamicFragment()
|
||||||
renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
|
renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,13 @@ import {
|
||||||
} from './component'
|
} from './component'
|
||||||
import { createComment, createTextNode } from './dom/node'
|
import { createComment, createTextNode } from './dom/node'
|
||||||
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
|
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
|
||||||
import { isHydrating } from './dom/hydration'
|
import {
|
||||||
|
currentHydrationNode,
|
||||||
|
isComment,
|
||||||
|
isHydrating,
|
||||||
|
locateHydrationNode,
|
||||||
|
locateVaporFragmentAnchor,
|
||||||
|
} from './dom/hydration'
|
||||||
|
|
||||||
export type Block =
|
export type Block =
|
||||||
| Node
|
| Node
|
||||||
|
@ -30,15 +36,20 @@ export class VaporFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DynamicFragment extends VaporFragment {
|
export class DynamicFragment extends VaporFragment {
|
||||||
anchor: Node
|
anchor!: Node
|
||||||
scope: EffectScope | undefined
|
scope: EffectScope | undefined
|
||||||
current?: BlockFn
|
current?: BlockFn
|
||||||
fallback?: BlockFn
|
fallback?: BlockFn
|
||||||
|
|
||||||
constructor(anchorLabel?: string) {
|
constructor(anchorLabel?: string) {
|
||||||
super([])
|
super([])
|
||||||
this.anchor =
|
if (isHydrating) {
|
||||||
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
|
locateHydrationNode()
|
||||||
|
this.hydrate(anchorLabel!)
|
||||||
|
} else {
|
||||||
|
this.anchor =
|
||||||
|
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(render?: BlockFn, key: any = render): void {
|
update(render?: BlockFn, key: any = render): void {
|
||||||
|
@ -75,6 +86,22 @@ export class DynamicFragment extends VaporFragment {
|
||||||
|
|
||||||
resetTracking()
|
resetTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hydrate(label: string): void {
|
||||||
|
// for `v-if="false"` the node will be an empty comment, use it as the anchor.
|
||||||
|
// otherwise, find next sibling vapor fragment anchor
|
||||||
|
if (isComment(currentHydrationNode!, '')) {
|
||||||
|
this.anchor = currentHydrationNode
|
||||||
|
} else {
|
||||||
|
const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
|
||||||
|
if (anchor) {
|
||||||
|
this.anchor = anchor
|
||||||
|
} else if (__DEV__) {
|
||||||
|
// this should not happen
|
||||||
|
throw new Error(`${label} fragment anchor node was not found.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
|
export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
|
||||||
|
@ -126,7 +153,6 @@ export function insert(
|
||||||
} else {
|
} else {
|
||||||
// fragment
|
// fragment
|
||||||
if (block.insert) {
|
if (block.insert) {
|
||||||
// TODO handle hydration for vdom interop
|
|
||||||
block.insert(parent, anchor)
|
block.insert(parent, anchor)
|
||||||
} else {
|
} else {
|
||||||
insert(block.nodes, parent, anchor)
|
insert(block.nodes, parent, anchor)
|
||||||
|
|
|
@ -58,7 +58,13 @@ import {
|
||||||
getSlot,
|
getSlot,
|
||||||
} from './componentSlots'
|
} from './componentSlots'
|
||||||
import { hmrReload, hmrRerender } from './hmr'
|
import { hmrReload, hmrRerender } from './hmr'
|
||||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
import {
|
||||||
|
adoptTemplate,
|
||||||
|
currentHydrationNode,
|
||||||
|
isHydrating,
|
||||||
|
locateHydrationNode,
|
||||||
|
setCurrentHydrationNode,
|
||||||
|
} from './dom/hydration'
|
||||||
import {
|
import {
|
||||||
insertionAnchor,
|
insertionAnchor,
|
||||||
insertionParent,
|
insertionParent,
|
||||||
|
@ -157,7 +163,9 @@ export function createComponent(
|
||||||
rawProps,
|
rawProps,
|
||||||
rawSlots,
|
rawSlots,
|
||||||
)
|
)
|
||||||
if (!isHydrating && _insertionParent) {
|
|
||||||
|
// `frag.insert` handles both hydration and mounting
|
||||||
|
if (_insertionParent) {
|
||||||
insert(frag, _insertionParent, _insertionAnchor)
|
insert(frag, _insertionParent, _insertionAnchor)
|
||||||
}
|
}
|
||||||
return frag
|
return frag
|
||||||
|
@ -486,7 +494,9 @@ export function createComponentWithFallback(
|
||||||
resetInsertionState()
|
resetInsertionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = document.createElement(comp)
|
const el = isHydrating
|
||||||
|
? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
|
||||||
|
: document.createElement(comp)
|
||||||
// mark single root
|
// mark single root
|
||||||
;(el as any).$root = isSingleRoot
|
;(el as any).$root = isSingleRoot
|
||||||
|
|
||||||
|
@ -497,6 +507,7 @@ export function createComponentWithFallback(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawSlots) {
|
if (rawSlots) {
|
||||||
|
isHydrating && setCurrentHydrationNode(el.firstChild)
|
||||||
if (rawSlots.$) {
|
if (rawSlots.$) {
|
||||||
// TODO dynamic slot fragment
|
// TODO dynamic slot fragment
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
|
import {
|
||||||
|
EMPTY_OBJ,
|
||||||
|
NO,
|
||||||
|
SLOT_ANCHOR_LABEL,
|
||||||
|
hasOwn,
|
||||||
|
isArray,
|
||||||
|
isFunction,
|
||||||
|
} from '@vue/shared'
|
||||||
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
|
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
|
||||||
import { rawPropsProxyHandlers } from './componentProps'
|
import { rawPropsProxyHandlers } from './componentProps'
|
||||||
import { currentInstance, isRef } from '@vue/runtime-dom'
|
import { currentInstance, isRef } from '@vue/runtime-dom'
|
||||||
|
@ -9,7 +16,7 @@ import {
|
||||||
insertionParent,
|
insertionParent,
|
||||||
resetInsertionState,
|
resetInsertionState,
|
||||||
} from './insertionState'
|
} from './insertionState'
|
||||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
import { isHydrating } from './dom/hydration'
|
||||||
|
|
||||||
export type RawSlots = Record<string, VaporSlot> & {
|
export type RawSlots = Record<string, VaporSlot> & {
|
||||||
$?: DynamicSlotSource[]
|
$?: DynamicSlotSource[]
|
||||||
|
@ -98,11 +105,7 @@ export function createSlot(
|
||||||
): Block {
|
): Block {
|
||||||
const _insertionParent = insertionParent
|
const _insertionParent = insertionParent
|
||||||
const _insertionAnchor = insertionAnchor
|
const _insertionAnchor = insertionAnchor
|
||||||
if (isHydrating) {
|
if (!isHydrating) resetInsertionState()
|
||||||
locateHydrationNode()
|
|
||||||
} else {
|
|
||||||
resetInsertionState()
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = currentInstance as VaporComponentInstance
|
const instance = currentInstance as VaporComponentInstance
|
||||||
const rawSlots = instance.rawSlots
|
const rawSlots = instance.rawSlots
|
||||||
|
@ -111,7 +114,6 @@ export function createSlot(
|
||||||
: EMPTY_OBJ
|
: EMPTY_OBJ
|
||||||
|
|
||||||
let fragment: DynamicFragment
|
let fragment: DynamicFragment
|
||||||
|
|
||||||
if (isRef(rawSlots._)) {
|
if (isRef(rawSlots._)) {
|
||||||
fragment = instance.appContext.vapor!.vdomSlot(
|
fragment = instance.appContext.vapor!.vdomSlot(
|
||||||
rawSlots._,
|
rawSlots._,
|
||||||
|
@ -121,7 +123,10 @@ export function createSlot(
|
||||||
fallback,
|
fallback,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
|
fragment =
|
||||||
|
isHydrating || __DEV__
|
||||||
|
? new DynamicFragment(SLOT_ANCHOR_LABEL)
|
||||||
|
: new DynamicFragment()
|
||||||
const isDynamicName = isFunction(name)
|
const isDynamicName = isFunction(name)
|
||||||
const renderSlot = () => {
|
const renderSlot = () => {
|
||||||
const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
|
const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
|
||||||
|
@ -151,7 +156,12 @@ export function createSlot(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isHydrating && _insertionParent) {
|
if (
|
||||||
|
_insertionParent &&
|
||||||
|
(!isHydrating ||
|
||||||
|
// for vdom interop fragment, `fragment.insert` handles both hydration and mounting
|
||||||
|
fragment.insert)
|
||||||
|
) {
|
||||||
insert(fragment, _insertionParent, _insertionAnchor)
|
insert(fragment, _insertionParent, _insertionAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,12 @@ import {
|
||||||
resetInsertionState,
|
resetInsertionState,
|
||||||
setInsertionState,
|
setInsertionState,
|
||||||
} from '../insertionState'
|
} from '../insertionState'
|
||||||
import { child, next } from './node'
|
import {
|
||||||
|
_nthChild,
|
||||||
|
disableHydrationNodeLookup,
|
||||||
|
enableHydrationNodeLookup,
|
||||||
|
} from './node'
|
||||||
|
import { isVaporAnchors } from '@vue/shared'
|
||||||
|
|
||||||
export let isHydrating = false
|
export let isHydrating = false
|
||||||
export let currentHydrationNode: Node | null = null
|
export let currentHydrationNode: Node | null = null
|
||||||
|
@ -16,33 +21,53 @@ export function setCurrentHydrationNode(node: Node | null): void {
|
||||||
|
|
||||||
let isOptimized = false
|
let isOptimized = false
|
||||||
|
|
||||||
export function withHydration(container: ParentNode, fn: () => void): void {
|
function performHydration<T>(
|
||||||
adoptTemplate = adoptTemplateImpl
|
fn: () => T,
|
||||||
locateHydrationNode = locateHydrationNodeImpl
|
setup: () => void,
|
||||||
|
cleanup: () => void,
|
||||||
|
): T {
|
||||||
if (!isOptimized) {
|
if (!isOptimized) {
|
||||||
|
adoptTemplate = adoptTemplateImpl
|
||||||
|
locateHydrationNode = locateHydrationNodeImpl
|
||||||
|
|
||||||
// optimize anchor cache lookup
|
// optimize anchor cache lookup
|
||||||
;(Comment.prototype as any).$fs = undefined
|
;(Comment.prototype as any).$fe = undefined
|
||||||
|
;(Node.prototype as any).$dp = undefined
|
||||||
isOptimized = true
|
isOptimized = true
|
||||||
}
|
}
|
||||||
|
enableHydrationNodeLookup()
|
||||||
isHydrating = true
|
isHydrating = true
|
||||||
setInsertionState(container, 0)
|
setup()
|
||||||
const res = fn()
|
const res = fn()
|
||||||
resetInsertionState()
|
cleanup()
|
||||||
currentHydrationNode = null
|
currentHydrationNode = null
|
||||||
isHydrating = false
|
isHydrating = false
|
||||||
|
disableHydrationNodeLookup()
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function withHydration(container: ParentNode, fn: () => void): void {
|
||||||
|
const setup = () => setInsertionState(container, 0)
|
||||||
|
const cleanup = () => resetInsertionState()
|
||||||
|
return performHydration(fn, setup, cleanup)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hydrateNode(node: Node, fn: () => void): void {
|
||||||
|
const setup = () => (currentHydrationNode = node)
|
||||||
|
const cleanup = () => {}
|
||||||
|
return performHydration(fn, setup, cleanup)
|
||||||
|
}
|
||||||
|
|
||||||
export let adoptTemplate: (node: Node, template: string) => Node | null
|
export let adoptTemplate: (node: Node, template: string) => Node | null
|
||||||
export let locateHydrationNode: () => void
|
export let locateHydrationNode: () => void
|
||||||
|
|
||||||
type Anchor = Comment & {
|
type Anchor = Comment & {
|
||||||
// cached matching fragment start to avoid repeated traversal
|
// cached matching fragment end to avoid repeated traversal
|
||||||
// on nested fragments
|
// on nested fragments
|
||||||
$fs?: Anchor
|
$fe?: Anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
const isComment = (node: Node, data: string): node is Anchor =>
|
export const isComment = (node: Node, data: string): node is Anchor =>
|
||||||
node.nodeType === 8 && (node as Comment).data === data
|
node.nodeType === 8 && (node as Comment).data === data
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,7 +76,7 @@ const isComment = (node: Node, data: string): node is Anchor =>
|
||||||
*/
|
*/
|
||||||
function adoptTemplateImpl(node: Node, template: string): Node | null {
|
function adoptTemplateImpl(node: Node, template: string): Node | null {
|
||||||
if (!(template[0] === '<' && template[1] === '!')) {
|
if (!(template[0] === '<' && template[1] === '!')) {
|
||||||
while (node.nodeType === 8) node = next(node)
|
while (node.nodeType === 8) node = node.nextSibling!
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
@ -71,51 +96,27 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentHydrationNode = next(node)
|
currentHydrationNode = node.nextSibling
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
function locateHydrationNodeImpl() {
|
function locateHydrationNodeImpl() {
|
||||||
let node: Node | null
|
let node: Node | null
|
||||||
|
|
||||||
// prepend / firstChild
|
// prepend / firstChild
|
||||||
if (insertionAnchor === 0) {
|
if (insertionAnchor === 0) {
|
||||||
node = child(insertionParent!)
|
node = insertionParent!.firstChild
|
||||||
} else {
|
} 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
|
node = insertionAnchor
|
||||||
? insertionAnchor.previousSibling
|
} else {
|
||||||
: insertionParent
|
node = currentHydrationNode
|
||||||
? insertionParent.lastChild
|
|
||||||
: currentHydrationNode
|
|
||||||
|
|
||||||
if (node && isComment(node, ']')) {
|
// if current hydration node is not under the current parent, or no
|
||||||
// fragment backward search
|
// current node, find node by dynamic position or use the first child
|
||||||
if (node.$fs) {
|
if (insertionParent && (!node || node.parentNode !== insertionParent)) {
|
||||||
// already cached matching fragment start
|
node = _nthChild(insertionParent, insertionParent.$dp || 0)
|
||||||
node = node.$fs
|
|
||||||
} else {
|
|
||||||
let cur: Node | null = node
|
|
||||||
let curFragEnd = node
|
|
||||||
let fragDepth = 0
|
|
||||||
node = null
|
|
||||||
while (cur) {
|
|
||||||
cur = cur.previousSibling
|
|
||||||
if (cur) {
|
|
||||||
if (isComment(cur, '[')) {
|
|
||||||
curFragEnd.$fs = cur
|
|
||||||
if (!fragDepth) {
|
|
||||||
node = cur
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
fragDepth--
|
|
||||||
}
|
|
||||||
} else if (isComment(cur, ']')) {
|
|
||||||
curFragEnd = cur
|
|
||||||
fragDepth++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,3 +128,55 @@ function locateHydrationNodeImpl() {
|
||||||
resetInsertionState()
|
resetInsertionState()
|
||||||
currentHydrationNode = node
|
currentHydrationNode = node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function locateEndAnchor(
|
||||||
|
node: Anchor,
|
||||||
|
open = '[',
|
||||||
|
close = ']',
|
||||||
|
): Node | null {
|
||||||
|
// already cached matching end
|
||||||
|
if (node.$fe) {
|
||||||
|
return node.$fe
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack: Anchor[] = [node]
|
||||||
|
while ((node = node.nextSibling as Anchor) && stack.length > 0) {
|
||||||
|
if (node.nodeType === 8) {
|
||||||
|
if (node.data === open) {
|
||||||
|
stack.push(node)
|
||||||
|
} else if (node.data === close) {
|
||||||
|
const matchingOpen = stack.pop()!
|
||||||
|
matchingOpen.$fe = node
|
||||||
|
if (stack.length === 0) return node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNonHydrationNode(node: Node): boolean {
|
||||||
|
return (
|
||||||
|
// empty text node
|
||||||
|
isEmptyTextNode(node) ||
|
||||||
|
// vdom fragment end anchor (`<!--]-->`)
|
||||||
|
isComment(node, ']') ||
|
||||||
|
// vapor mode specific anchors
|
||||||
|
isVaporAnchors(node)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function locateVaporFragmentAnchor(
|
||||||
|
node: Node,
|
||||||
|
anchorLabel: string,
|
||||||
|
): Comment | undefined {
|
||||||
|
let n = node.nextSibling
|
||||||
|
while (n) {
|
||||||
|
if (isComment(n, anchorLabel)) return n
|
||||||
|
n = n.nextSibling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmptyTextNode(node: Node): node is Text {
|
||||||
|
return node.nodeType === 3 && !(node as Text).data.trim()
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration'
|
||||||
|
import {
|
||||||
|
DYNAMIC_END_ANCHOR_LABEL,
|
||||||
|
DYNAMIC_START_ANCHOR_LABEL,
|
||||||
|
isVaporAnchors,
|
||||||
|
} from '@vue/shared'
|
||||||
|
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
export function createTextNode(value = ''): Text {
|
export function createTextNode(value = ''): Text {
|
||||||
return document.createTextNode(value)
|
return document.createTextNode(value)
|
||||||
|
@ -14,16 +21,175 @@ export function querySelector(selectors: string): Element | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
export function child(node: ParentNode): Node {
|
export function _child(node: ParentNode): Node {
|
||||||
return node.firstChild!
|
return node.firstChild!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydration-specific version of `child`.
|
||||||
|
*
|
||||||
|
* This function skips leading fragment anchors to find the first node relevant
|
||||||
|
* for hydration matching against the client-side template structure.
|
||||||
|
*
|
||||||
|
* Problem:
|
||||||
|
* Template: `<div><slot />{{ msg }}</div>`
|
||||||
|
*
|
||||||
|
* Client Compiled Code (Simplified):
|
||||||
|
* const n2 = t0() // n2 = `<div> </div>`
|
||||||
|
* const n1 = _child(n2) // n1 = text node
|
||||||
|
* // ... slot creation ...
|
||||||
|
* _renderEffect(() => _setText(n1, _ctx.msg))
|
||||||
|
*
|
||||||
|
* SSR Output: `<div><!--[-->slot content<!--]-->Actual Text Node</div>`
|
||||||
|
*
|
||||||
|
* Hydration Mismatch:
|
||||||
|
* - During hydration, `n2` refers to the SSR `<div>`.
|
||||||
|
* - `_child(n2)` would return `<!--[-->`.
|
||||||
|
* - The client code expects `n1` to be the text node, but gets the comment.
|
||||||
|
* The subsequent `_setText(n1, ...)` would fail or target the wrong node.
|
||||||
|
*
|
||||||
|
* Solution (`__child`):
|
||||||
|
* - `__child(n2)` is used during hydration. It skips the SSR fragment anchors
|
||||||
|
* (`<!--[-->...<!--]-->`) and any other non-content nodes to find the
|
||||||
|
* "Actual Text Node", correctly matching the client's expectation for `n1`.
|
||||||
|
*/
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
export function nthChild(node: Node, i: number): Node {
|
export function __child(node: ParentNode): Node {
|
||||||
return node.childNodes[i]
|
let n = node.firstChild!
|
||||||
|
|
||||||
|
if (isComment(n, '[')) {
|
||||||
|
n = locateEndAnchor(n)!.nextSibling!
|
||||||
|
}
|
||||||
|
|
||||||
|
while (n && isVaporAnchors(n)) {
|
||||||
|
n = n.nextSibling!
|
||||||
|
}
|
||||||
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
export function next(node: Node): Node {
|
export function _nthChild(node: Node, i: number): Node {
|
||||||
|
return node.childNodes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydration-specific version of `nthChild`.
|
||||||
|
*/
|
||||||
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
|
export function __nthChild(node: Node, i: number): Node {
|
||||||
|
let n = node.firstChild!
|
||||||
|
for (let start = 0; start < i; start++) {
|
||||||
|
n = __next(n) as ChildNode
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
|
function _next(node: Node): Node {
|
||||||
return node.nextSibling!
|
return node.nextSibling!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydration-specific version of `next`.
|
||||||
|
*
|
||||||
|
* SSR comment anchors (fragments `<!--[-->...<!--]-->`, dynamic `<!--[[-->...<!--]]-->`)
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* Template: `<div>Node1<!>Node2</div>` (where <!> is a dynamic component placeholder)
|
||||||
|
*
|
||||||
|
* Client Compiled Code (Simplified):
|
||||||
|
* const n2 = t0() // n2 = `<div>Node1<!---->Node2</div>`
|
||||||
|
* const n1 = _next(_child(n2)) // n1 = _next(Node1) returns `<!---->`
|
||||||
|
* _setInsertionState(n2, n1) // insertion anchor is `<!---->`
|
||||||
|
* const n0 = _createComponent(_ctx.Comp) // inserted before `<!---->`
|
||||||
|
*
|
||||||
|
* SSR Output: `<div>Node1<!--[-->Node3 Node4<!--]-->Node2</div>`
|
||||||
|
*
|
||||||
|
* Hydration Mismatch:
|
||||||
|
* - During hydration, `n2` refers to the SSR `<div>`.
|
||||||
|
* - `_child(n2)` returns `Node1`.
|
||||||
|
* - `_next(Node1)` would return `<!--[-->`.
|
||||||
|
* - The client logic expects `n1` to be the node *after* `Node1` in its structure
|
||||||
|
* (the placeholder), but gets the fragment start anchor `<!--[-->` from SSR.
|
||||||
|
* - Using `<!--[-->` as the insertion anchor for hydrating the component is incorrect.
|
||||||
|
*
|
||||||
|
* Solution (`__next`):
|
||||||
|
* - During hydration, `next.impl` is `__next`.
|
||||||
|
* - `n1 = __next(Node1)` is called.
|
||||||
|
* - `__next` recognizes that the immediate sibling `<!--[-->` is a fragment start anchor.
|
||||||
|
* - It skips the entire fragment block (`<!--[-->Node3 Node4<!--]-->`).
|
||||||
|
* - It returns the node immediately *after* the fragment's end anchor, which is `Node2`.
|
||||||
|
* - This correctly identifies the logical "next sibling" anchor (`Node2`) in the SSR structure,
|
||||||
|
* allowing the component to be hydrated correctly relative to `Node1` and `Node2`.
|
||||||
|
*
|
||||||
|
* This function ensures traversal correctly skips over non-hydration nodes and
|
||||||
|
* treats entire fragment/dynamic blocks (when starting *from* their beginning anchor)
|
||||||
|
* as single logical units to find the next actual sibling node for hydration matching.
|
||||||
|
*/
|
||||||
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
|
export function __next(node: Node): Node {
|
||||||
|
// process dynamic node (<!--[[-->...<!--]]-->) as a single node
|
||||||
|
if (isComment(node, DYNAMIC_START_ANCHOR_LABEL)) {
|
||||||
|
node = locateEndAnchor(
|
||||||
|
node,
|
||||||
|
DYNAMIC_START_ANCHOR_LABEL,
|
||||||
|
DYNAMIC_END_ANCHOR_LABEL,
|
||||||
|
)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// process fragment (<!--[-->...<!--]-->) as a single node
|
||||||
|
else if (isComment(node, '[')) {
|
||||||
|
node = locateEndAnchor(node)!
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = node.nextSibling!
|
||||||
|
while (n && isNonHydrationNode(n)) {
|
||||||
|
n = n.nextSibling!
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
type DelegatedFunction<T extends (...args: any[]) => any> = T & {
|
||||||
|
impl: T
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
|
export const child: DelegatedFunction<typeof _child> = node => {
|
||||||
|
return child.impl(node)
|
||||||
|
}
|
||||||
|
child.impl = _child
|
||||||
|
|
||||||
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
|
export const next: DelegatedFunction<typeof _next> = node => {
|
||||||
|
return next.impl(node)
|
||||||
|
}
|
||||||
|
next.impl = _next
|
||||||
|
|
||||||
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
|
export const nthChild: DelegatedFunction<typeof _nthChild> = (node, i) => {
|
||||||
|
return nthChild.impl(node, i)
|
||||||
|
}
|
||||||
|
nthChild.impl = _nthChild
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables hydration-specific node lookup behavior.
|
||||||
|
*
|
||||||
|
* Temporarily switches the implementations of the exported
|
||||||
|
* `child`, `next`, and `nthChild` functions to their hydration-specific
|
||||||
|
* versions (`__child`, `__next`, `__nthChild`). This allows traversal
|
||||||
|
* logic to correctly handle SSR comment anchors during hydration.
|
||||||
|
*/
|
||||||
|
export function enableHydrationNodeLookup(): void {
|
||||||
|
child.impl = __child
|
||||||
|
next.impl = __next
|
||||||
|
nthChild.impl = __nthChild
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableHydrationNodeLookup(): void {
|
||||||
|
child.impl = _child
|
||||||
|
next.impl = _next
|
||||||
|
nthChild.impl = _nthChild
|
||||||
|
}
|
||||||
|
|
|
@ -6,13 +6,17 @@ let t: HTMLTemplateElement
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
export function template(html: string, root?: boolean) {
|
export function template(html: string, root?: boolean) {
|
||||||
let node: Node
|
let node: Node
|
||||||
return (): Node & { $root?: true } => {
|
return (n?: number): Node & { $root?: true } => {
|
||||||
if (isHydrating) {
|
if (isHydrating) {
|
||||||
if (__DEV__ && !currentHydrationNode) {
|
if (__DEV__ && !currentHydrationNode) {
|
||||||
// TODO this should not happen
|
// TODO this should not happen
|
||||||
throw new Error('No current hydration node')
|
throw new Error('No current hydration node')
|
||||||
}
|
}
|
||||||
return adoptTemplate(currentHydrationNode!, html)!
|
node = adoptTemplate(currentHydrationNode!, html)!
|
||||||
|
// dynamic node position, default is 0
|
||||||
|
;(node as any).$dp = n || 0
|
||||||
|
if (root) (node as any).$root = true
|
||||||
|
return node
|
||||||
}
|
}
|
||||||
// fast path for text nodes
|
// fast path for text nodes
|
||||||
if (html[0] !== '<') {
|
if (html[0] !== '<') {
|
||||||
|
|
|
@ -1,4 +1,16 @@
|
||||||
export let insertionParent: ParentNode | undefined
|
export let insertionParent:
|
||||||
|
| (ParentNode & {
|
||||||
|
// dynamic node position - hydration only
|
||||||
|
// indicates the position where dynamic nodes begin within the parent
|
||||||
|
// during hydration, static nodes before this index are skipped
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// const t0 = _template("<div><span></span><span></span></div>", true)
|
||||||
|
// const n4 = t0(2) // n4.$dp = 2
|
||||||
|
// The first 2 nodes are static, dynamic nodes start from index 2
|
||||||
|
$dp?: number
|
||||||
|
})
|
||||||
|
| undefined
|
||||||
export let insertionAnchor: Node | 0 | undefined
|
export let insertionAnchor: Node | 0 | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
type App,
|
type App,
|
||||||
type ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
type ConcreteComponent,
|
type ConcreteComponent,
|
||||||
|
type HydrationRenderer,
|
||||||
MoveType,
|
MoveType,
|
||||||
type Plugin,
|
type Plugin,
|
||||||
type RendererInternals,
|
type RendererInternals,
|
||||||
|
@ -12,6 +13,7 @@ import {
|
||||||
createInternalObject,
|
createInternalObject,
|
||||||
createVNode,
|
createVNode,
|
||||||
currentInstance,
|
currentInstance,
|
||||||
|
ensureHydrationRenderer,
|
||||||
ensureRenderer,
|
ensureRenderer,
|
||||||
isEmitListener,
|
isEmitListener,
|
||||||
onScopeDispose,
|
onScopeDispose,
|
||||||
|
@ -36,6 +38,14 @@ import type { RawSlots, VaporSlot } from './componentSlots'
|
||||||
import { renderEffect } from './renderEffect'
|
import { renderEffect } from './renderEffect'
|
||||||
import { createTextNode } from './dom/node'
|
import { createTextNode } from './dom/node'
|
||||||
import { optimizePropertyLookup } from './dom/prop'
|
import { optimizePropertyLookup } from './dom/prop'
|
||||||
|
import {
|
||||||
|
currentHydrationNode,
|
||||||
|
isHydrating,
|
||||||
|
locateHydrationNode,
|
||||||
|
locateVaporFragmentAnchor,
|
||||||
|
setCurrentHydrationNode,
|
||||||
|
hydrateNode as vaporHydrateNode,
|
||||||
|
} from './dom/hydration'
|
||||||
|
|
||||||
// mounting vapor components and slots in vdom
|
// mounting vapor components and slots in vdom
|
||||||
const vaporInteropImpl: Omit<
|
const vaporInteropImpl: Omit<
|
||||||
|
@ -116,6 +126,18 @@ const vaporInteropImpl: Omit<
|
||||||
insert(vnode.vb || (vnode.component as any), container, anchor)
|
insert(vnode.vb || (vnode.component as any), container, anchor)
|
||||||
insert(vnode.anchor as any, container, anchor)
|
insert(vnode.anchor as any, container, anchor)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hydrate: vaporHydrateNode,
|
||||||
|
hydrateSlot(vnode, container) {
|
||||||
|
const { slot } = vnode.vs!
|
||||||
|
const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
|
||||||
|
const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
|
||||||
|
vaporHydrateNode(slotBlock, () => {
|
||||||
|
const anchor = locateVaporFragmentAnchor(currentHydrationNode!, 'slot')!
|
||||||
|
vnode.el = vnode.anchor = anchor
|
||||||
|
insert((vnode.vb = slotBlock), container, anchor)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaporSlotPropsProxyHandler: ProxyHandler<
|
const vaporSlotPropsProxyHandler: ProxyHandler<
|
||||||
|
@ -142,6 +164,8 @@ const vaporSlotsProxyHandler: ProxyHandler<any> = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mount vdom component in vapor
|
* Mount vdom component in vapor
|
||||||
*/
|
*/
|
||||||
|
@ -187,16 +211,20 @@ function createVDOMComponent(
|
||||||
}
|
}
|
||||||
|
|
||||||
frag.insert = (parentNode, anchor) => {
|
frag.insert = (parentNode, anchor) => {
|
||||||
if (!isMounted) {
|
if (!isMounted || isHydrating) {
|
||||||
internals.mt(
|
if (isHydrating) {
|
||||||
vnode,
|
hydrateVNode(vnode, parentInstance as any)
|
||||||
parentNode,
|
} else {
|
||||||
anchor,
|
internals.mt(
|
||||||
parentInstance as any,
|
vnode,
|
||||||
null,
|
parentNode,
|
||||||
undefined,
|
anchor,
|
||||||
false,
|
parentInstance as any,
|
||||||
)
|
null,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
onScopeDispose(unmount, true)
|
onScopeDispose(unmount, true)
|
||||||
isMounted = true
|
isMounted = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -241,28 +269,32 @@ function renderVDOMSlot(
|
||||||
isFunction(name) ? name() : name,
|
isFunction(name) ? name() : name,
|
||||||
props,
|
props,
|
||||||
)
|
)
|
||||||
if ((vnode.children as any[]).length) {
|
if (isHydrating) {
|
||||||
if (fallbackNodes) {
|
hydrateVNode(vnode!, parentComponent as any)
|
||||||
remove(fallbackNodes, parentNode)
|
|
||||||
fallbackNodes = undefined
|
|
||||||
}
|
|
||||||
internals.p(
|
|
||||||
oldVNode,
|
|
||||||
vnode,
|
|
||||||
parentNode,
|
|
||||||
anchor,
|
|
||||||
parentComponent as any,
|
|
||||||
)
|
|
||||||
oldVNode = vnode
|
|
||||||
} else {
|
} else {
|
||||||
if (fallback && !fallbackNodes) {
|
if ((vnode.children as any[]).length) {
|
||||||
// mount fallback
|
if (fallbackNodes) {
|
||||||
if (oldVNode) {
|
remove(fallbackNodes, parentNode)
|
||||||
internals.um(oldVNode, parentComponent as any, null, true)
|
fallbackNodes = undefined
|
||||||
}
|
}
|
||||||
insert((fallbackNodes = fallback(props)), parentNode, anchor)
|
internals.p(
|
||||||
|
oldVNode,
|
||||||
|
vnode,
|
||||||
|
parentNode,
|
||||||
|
anchor,
|
||||||
|
parentComponent as any,
|
||||||
|
)
|
||||||
|
oldVNode = vnode
|
||||||
|
} else {
|
||||||
|
if (fallback && !fallbackNodes) {
|
||||||
|
// mount fallback
|
||||||
|
if (oldVNode) {
|
||||||
|
internals.um(oldVNode, parentComponent as any, null, true)
|
||||||
|
}
|
||||||
|
insert((fallbackNodes = fallback(props)), parentNode, anchor)
|
||||||
|
}
|
||||||
|
oldVNode = null
|
||||||
}
|
}
|
||||||
oldVNode = null
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
isMounted = true
|
isMounted = true
|
||||||
|
@ -289,6 +321,23 @@ function renderVDOMSlot(
|
||||||
return frag
|
return frag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hydrateVNode(
|
||||||
|
vnode: VNode,
|
||||||
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
) {
|
||||||
|
locateHydrationNode()
|
||||||
|
if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
|
||||||
|
const nextNode = vdomHydrateNode(
|
||||||
|
currentHydrationNode!,
|
||||||
|
vnode,
|
||||||
|
parentComponent,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
setCurrentHydrationNode(nextNode)
|
||||||
|
}
|
||||||
|
|
||||||
export const vaporInteropPlugin: Plugin = app => {
|
export const vaporInteropPlugin: Plugin = app => {
|
||||||
const internals = ensureRenderer().internals
|
const internals = ensureRenderer().internals
|
||||||
app._context.vapor = extend(vaporInteropImpl, {
|
app._context.vapor = extend(vaporInteropImpl, {
|
||||||
|
|
|
@ -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><!--]-->` +
|
||||||
`</div></div>`,
|
`<!--slot--></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<!--]--></div></div>`,
|
`<div>parent<div class="child"><!--[-->fallback<!--]--><!--slot--></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><!--]-->` +
|
||||||
`</div></div>`,
|
`<!--slot--></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><!--]-->` +
|
||||||
`</div></div>`,
|
`<!--slot--></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<!--]--><!--]--></div>`,
|
`<div><!--[--><!--[-->hello<!--]--><!--slot--><!--]--><!--slot--></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><!--[--><!--]--></div>`,
|
`<div><!--[--><!--]--><!--slot--></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<!--]--></div>`,
|
`<div><!--[-->fallback<!--]--><!--slot--></div>`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,7 +25,7 @@ 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>`,
|
`<div class="foo bar"></div><!--if-->`,
|
||||||
)
|
)
|
||||||
expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
|
expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
|
||||||
`<span class="bar"></span>`,
|
`<span class="bar"></span>`,
|
||||||
|
|
|
@ -14,7 +14,9 @@ describe('ssr: dynamic component', () => {
|
||||||
template: `<component :is="'one'"><span>slot</span></component>`,
|
template: `<component :is="'one'"><span>slot</span></component>`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(`<div><!--[--><span>slot</span><!--]--></div>`)
|
).toBe(
|
||||||
|
`<div><!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolved to component with v-show', async () => {
|
test('resolved to component with v-show', async () => {
|
||||||
|
@ -30,7 +32,7 @@ describe('ssr: dynamic component', () => {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(
|
).toBe(
|
||||||
`<div><!--[--><div style=\"display:none;\"><!--[-->hi<!--]--></div><!--]--></div>`,
|
`<div><!--[--><div style="display:none;"><!--[-->hi<!--]--></div><!--dynamic-component--><!--]--></div><!--dynamic-component-->`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -41,7 +43,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>`)
|
).toBe(`<p><span>slot</span></p><!--dynamic-component-->`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolve to component vnode', async () => {
|
test('resolve to component vnode', async () => {
|
||||||
|
@ -60,7 +62,9 @@ describe('ssr: dynamic component', () => {
|
||||||
template: `<component :is="vnode"><span>slot</span></component>`,
|
template: `<component :is="vnode"><span>slot</span></component>`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`)
|
).toBe(
|
||||||
|
`<div>test<!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolve to element vnode', async () => {
|
test('resolve to element vnode', async () => {
|
||||||
|
@ -75,6 +79,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>`)
|
).toBe(`<div id="test"><span>slot</span></div><!--dynamic-component-->`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -68,7 +68,9 @@ describe('ssr: scopedId runtime behavior', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await renderToString(createApp(Comp))
|
const result = await renderToString(createApp(Comp))
|
||||||
expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
|
expect(result).toBe(
|
||||||
|
`<!--[--><div parent wrapper-s child></div><!--]--><!--slot-->`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// #2892
|
// #2892
|
||||||
|
@ -150,8 +152,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><!--]--><!--]-->` +
|
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
|
||||||
`</div>`,
|
`<!--slot--></div>`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -265,8 +267,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><!--]--><!--]-->` +
|
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
|
||||||
`</div>`,
|
`<!--slot--></div>`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe('ssr: slot', () => {
|
||||||
template: `<one>hello</one>`,
|
template: `<one>hello</one>`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(`<div><!--[-->hello<!--]--></div>`)
|
).toBe(`<div><!--[-->hello<!--]--><!--slot--></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><!--]--></div>`)
|
).toBe(`<div><!--[--><div>hi</div><!--]--><!--slot--></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><!--[--><!--]--></div>`)
|
).toBe(`<div><!--[--><!--]--><!--slot--></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><!--[--><!--]--></div>`)
|
).toBe(`<div><!--[--><!--]--><!--slot--></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><!--[--><!--]--></div>`)
|
).toBe(`<div><!--[--><!--]--><!--slot--></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><!--]--></div>`)
|
).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--><!--slot--></div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fragment slot (template v-if)', async () => {
|
test('fragment slot (template v-if)', async () => {
|
||||||
|
@ -94,7 +94,9 @@ describe('ssr: slot', () => {
|
||||||
template: `<one><template v-if="true">hello</template></one>`,
|
template: `<one><template v-if="true">hello</template></one>`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`)
|
).toBe(
|
||||||
|
`<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--><!--slot--></div>`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fragment slot (template v-if + multiple elements)', async () => {
|
test('fragment slot (template v-if + multiple elements)', async () => {
|
||||||
|
@ -106,7 +108,7 @@ describe('ssr: slot', () => {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(
|
).toBe(
|
||||||
`<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--]--></div>`,
|
`<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--><!--slot--></div>`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -135,7 +137,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>`)
|
).toBe(`<div>foo</div><!--if-->`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// #9933
|
// #9933
|
||||||
|
@ -183,7 +185,7 @@ describe('ssr: slot', () => {
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(`<div><!--[--> new header <!--]--></div>`)
|
).toBe(`<div><!--[--> new header <!--]--><!--slot--></div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// #11326
|
// #11326
|
||||||
|
@ -202,7 +204,9 @@ 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(`<button><!--[--><div><!--[--><!--]--></div><!--]--></button>`)
|
).toBe(
|
||||||
|
`<button><!--[--><div><!--[--><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
|
||||||
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await renderToString(
|
await renderToString(
|
||||||
|
@ -219,7 +223,7 @@ describe('ssr: slot', () => {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(
|
).toBe(
|
||||||
`<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`,
|
`<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -233,6 +237,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>`)
|
).toBe(`<button><!--[--><!--]--></button><!--dynamic-component-->`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
type SSRBufferItem,
|
type SSRBufferItem,
|
||||||
renderVNodeChildren,
|
renderVNodeChildren,
|
||||||
} from '../render'
|
} from '../render'
|
||||||
import { isArray } from '@vue/shared'
|
import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared'
|
||||||
|
|
||||||
const { ensureValidVNode } = ssrUtils
|
const { ensureValidVNode } = ssrUtils
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export function ssrRenderSlot(
|
||||||
parentComponent,
|
parentComponent,
|
||||||
slotScopeId,
|
slotScopeId,
|
||||||
)
|
)
|
||||||
push(`<!--]-->`)
|
push(`<!--]--><!--${SLOT_ANCHOR_LABEL}-->`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ssrRenderSlotInner(
|
export function ssrRenderSlotInner(
|
||||||
|
@ -104,7 +104,7 @@ export function ssrRenderSlotInner(
|
||||||
if (
|
if (
|
||||||
transition &&
|
transition &&
|
||||||
slotBuffer[0] === '<!--[-->' &&
|
slotBuffer[0] === '<!--[-->' &&
|
||||||
slotBuffer[end - 1] === '<!--]-->'
|
(slotBuffer[end - 1] as string).startsWith('<!--]-->')
|
||||||
) {
|
) {
|
||||||
start++
|
start++
|
||||||
end--
|
end--
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
export const DYNAMIC_START_ANCHOR_LABEL = '[['
|
||||||
|
export const DYNAMIC_END_ANCHOR_LABEL = ']]'
|
||||||
|
|
||||||
|
export const IF_ANCHOR_LABEL: string = '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 isDynamicAnchor(node: Node): node is Comment {
|
||||||
|
if (node.nodeType !== 8) return false
|
||||||
|
|
||||||
|
const data = (node as Comment).data
|
||||||
|
return (
|
||||||
|
data === DYNAMIC_START_ANCHOR_LABEL || data === DYNAMIC_END_ANCHOR_LABEL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVaporFragmentAnchor(node: Node): node is Comment {
|
||||||
|
if (node.nodeType !== 8) return false
|
||||||
|
|
||||||
|
const data = (node as Comment).data
|
||||||
|
return (
|
||||||
|
data === IF_ANCHOR_LABEL ||
|
||||||
|
data === FOR_ANCHOR_LABEL ||
|
||||||
|
data === SLOT_ANCHOR_LABEL ||
|
||||||
|
data === DYNAMIC_COMPONENT_ANCHOR_LABEL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVaporAnchors(node: Node): node is Comment {
|
||||||
|
return isDynamicAnchor(node) || isVaporFragmentAnchor(node)
|
||||||
|
}
|
|
@ -13,3 +13,4 @@ export * from './looseEqual'
|
||||||
export * from './toDisplayString'
|
export * from './toDisplayString'
|
||||||
export * from './typeUtils'
|
export * from './typeUtils'
|
||||||
export * from './subSequence'
|
export * from './subSequence'
|
||||||
|
export * from './domAnchors'
|
||||||
|
|
Loading…
Reference in New Issue