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) {
|
||||
_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) {
|
||||
_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) => {
|
||||
_push(\`<span\${_scopeId}></span>\`)
|
||||
})
|
||||
_push(\`<!--]--></div>\`)
|
||||
_push(\`<!--]--><!--for--></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -267,7 +270,8 @@ describe('ssr: components', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<span\${_scopeId}></span>\`)
|
||||
})
|
||||
_push(\`<!--]--></div>\`)
|
||||
_push(\`<!--]--><!--for--></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -361,6 +365,7 @@ describe('ssr: components', () => {
|
|||
_push(\`\`)
|
||||
if (false) {
|
||||
_push(\`<div\${_scopeId}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_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(\`<!--[-->\`)
|
||||
if (true) {
|
||||
_push(\`<div></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ describe('ssr: inject <style vars>', () => {
|
|||
const _cssVars = { style: { color: _ctx.color }}
|
||||
if (_ctx.ok) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!--[--><div\${
|
||||
_ssrRenderAttrs(_cssVars)
|
||||
|
|
|
@ -153,6 +153,7 @@ describe('ssr: <slot>', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (true) {
|
||||
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--for--><!--]-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -33,7 +33,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`</ul>\`)
|
||||
_push(\`<!--for--></ul>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -52,8 +52,10 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--for-->\`)
|
||||
if (false) {
|
||||
_push(\`<div></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
}
|
||||
_push(\`</ul>\`)
|
||||
}"
|
||||
|
@ -74,7 +76,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`</ul>\`)
|
||||
_push(\`<!--for--></ul>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -96,7 +98,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`</\${_ctx.someTag}>\`)
|
||||
_push(\`<!--for--></\${_ctx.someTag}>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -118,11 +120,14 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(10, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--for-->\`)
|
||||
_ssrRenderList(10, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--for-->\`)
|
||||
if (_ctx.ok) {
|
||||
_push(\`<div>ok</div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
}
|
||||
_push(\`<!--]-->\`)
|
||||
}"
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -25,7 +25,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div>foo<span>bar</span></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -51,9 +51,9 @@ describe('ssr: v-for', () => {
|
|||
_ssrInterpolate(j)
|
||||
}</div>\`)
|
||||
})
|
||||
_push(\`<!--]--></div>\`)
|
||||
_push(\`<!--]--><!--for--></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -68,7 +68,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -85,7 +85,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<span>\${_ssrInterpolate(i)}</span>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -107,7 +107,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrInterpolate(i + 1)
|
||||
}</span><!--]-->\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -127,7 +127,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, ({ foo }, index) => {
|
||||
_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) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -23,6 +24,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -38,6 +40,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||
}
|
||||
|
@ -53,8 +56,10 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else if (_ctx.bar) {
|
||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -70,8 +75,10 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else if (_ctx.bar) {
|
||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
|
||||
}
|
||||
|
@ -82,15 +89,16 @@ describe('ssr: v-if', () => {
|
|||
test('<template v-if> (text)', () => {
|
||||
expect(compile(`<template v-if="foo">hello</template>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[-->hello<!--]-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
}"
|
||||
`)
|
||||
"
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[-->hello<!--]-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('<template v-if> (single element)', () => {
|
||||
|
@ -102,6 +110,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -118,6 +127,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -137,7 +147,8 @@ describe('ssr: v-if', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -156,6 +167,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
|
|||
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
|
||||
}></option>\`)
|
||||
})
|
||||
_push(\`<!--]--></select></div>\`)
|
||||
_push(\`<!--]--><!--for--></select></div>\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
|
@ -91,6 +91,7 @@ describe('ssr: v-model', () => {
|
|||
? _ssrLooseContain(_ctx.model, _ctx.i)
|
||||
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
|
||||
}></option>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
type IfStatement,
|
||||
type JSChildNode,
|
||||
NodeTypes,
|
||||
type PlainElementNode,
|
||||
type RootNode,
|
||||
type TemplateChildNode,
|
||||
type TemplateLiteral,
|
||||
|
@ -20,7 +21,12 @@ import {
|
|||
isText,
|
||||
processExpression,
|
||||
} 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 { ssrProcessIf } from './transforms/ssrVIf'
|
||||
import { ssrProcessFor } from './transforms/ssrVFor'
|
||||
|
@ -157,13 +163,33 @@ export function processChildren(
|
|||
asFragment = false,
|
||||
disableNestedFragments = false,
|
||||
disableComment = false,
|
||||
asDynamic = false,
|
||||
): void {
|
||||
if (asDynamic) {
|
||||
context.pushStringPart(`<!--${DYNAMIC_START_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
if (asFragment) {
|
||||
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++) {
|
||||
const child = children[i]
|
||||
if (inElement && shouldProcessChildAsDynamic(parent, child)) {
|
||||
processChildren(
|
||||
{ children: [child] },
|
||||
context,
|
||||
asFragment,
|
||||
disableNestedFragments,
|
||||
disableComment,
|
||||
true,
|
||||
)
|
||||
continue
|
||||
}
|
||||
switch (child.type) {
|
||||
case NodeTypes.ELEMENT:
|
||||
switch (child.tagType) {
|
||||
|
@ -237,6 +263,9 @@ export function processChildren(
|
|||
if (asFragment) {
|
||||
context.pushStringPart(`<!--]-->`)
|
||||
}
|
||||
if (asDynamic) {
|
||||
context.pushStringPart(`<!--${DYNAMIC_END_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
}
|
||||
|
||||
export function processChildrenAsStatement(
|
||||
|
@ -249,3 +278,147 @@ export function processChildrenAsStatement(
|
|||
processChildren(parent, childContext, asFragment)
|
||||
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,
|
||||
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 {
|
||||
ssrProcessTransition,
|
||||
|
@ -264,6 +271,8 @@ export function ssrProcessComponent(
|
|||
// dynamic component (`resolveDynamicComponent` call)
|
||||
// the codegen node is a `renderVNode` call
|
||||
context.pushStatement(node.ssrCodegenNode)
|
||||
// anchor for dynamic component for vapor hydration
|
||||
context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
processChildrenAsStatement,
|
||||
} from '../ssrCodegenTransform'
|
||||
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
|
||||
export const ssrTransformFor: NodeTransform =
|
||||
|
@ -49,4 +50,6 @@ export function ssrProcessFor(
|
|||
if (!disableNestedFragments) {
|
||||
context.pushStringPart(`<!--]-->`)
|
||||
}
|
||||
// v-for anchor for vapor hydration
|
||||
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
type SSRTransformContext,
|
||||
processChildrenAsStatement,
|
||||
} from '../ssrCodegenTransform'
|
||||
import { IF_ANCHOR_LABEL } from '@vue/shared'
|
||||
|
||||
// Plugin for the first transform pass, which simply constructs the AST node
|
||||
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
|
||||
|
@ -74,5 +75,16 @@ function processIfBranch(
|
|||
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
||||
// optimize away nested fragments when the only child is a ForNode
|
||||
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
||||
return processChildrenAsStatement(branch, context, needFragmentWrapper)
|
||||
const statement = processChildrenAsStatement(
|
||||
branch,
|
||||
context,
|
||||
needFragmentWrapper,
|
||||
)
|
||||
if (branch.condition) {
|
||||
// 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,
|
||||
): CodeFragment[] {
|
||||
const [frag, push] = buildCodeFragment()
|
||||
const { id, template, operation } = dynamic
|
||||
const { id, template, operation, dynamicChildOffset } = dynamic
|
||||
|
||||
if (id !== undefined && template !== undefined) {
|
||||
push(NEWLINE, `const n${id} = t${template}()`)
|
||||
push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
|
||||
push(...genDirectivesForElement(id, context))
|
||||
}
|
||||
|
||||
|
|
|
@ -259,6 +259,7 @@ export interface IRDynamicInfo {
|
|||
children: IRDynamicInfo[]
|
||||
template?: number
|
||||
hasDynamicChild?: boolean
|
||||
dynamicChildOffset?: number
|
||||
operation?: OperationNode
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => {
|
|||
|
||||
function processDynamicChildren(context: TransformContext<ElementNode>) {
|
||||
let prevDynamics: IRDynamicInfo[] = []
|
||||
let hasStaticTemplate = false
|
||||
let staticCount = 0
|
||||
const children = context.dynamic.children
|
||||
|
||||
for (const [index, child] of children.entries()) {
|
||||
|
@ -69,22 +69,36 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
|||
|
||||
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
|
||||
if (prevDynamics.length) {
|
||||
if (hasStaticTemplate) {
|
||||
context.childrenTemplate[index - prevDynamics.length] = `<!>`
|
||||
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
|
||||
const anchor = (prevDynamics[0].anchor = context.increaseId())
|
||||
registerInsertion(prevDynamics, context, anchor)
|
||||
if (staticCount) {
|
||||
// each dynamic child gets its own placeholder node.
|
||||
// this makes it easier to locate the corresponding node during hydration.
|
||||
for (let i = 0; i < prevDynamics.length; i++) {
|
||||
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 {
|
||||
registerInsertion(prevDynamics, context, -1 /* prepend */)
|
||||
}
|
||||
prevDynamics = []
|
||||
}
|
||||
hasStaticTemplate = true
|
||||
staticCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (prevDynamics.length) {
|
||||
registerInsertion(prevDynamics, context)
|
||||
context.dynamic.dynamicChildOffset = staticCount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -601,14 +601,14 @@ describe('SSR hydration', () => {
|
|||
const ctx: SSRContext = {}
|
||||
container.innerHTML = await renderToString(h(App), ctx)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
teleportContainer.innerHTML = ctx.teleports!['#target']
|
||||
|
||||
// hydrate
|
||||
createSSRApp(App).mount(container)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
expect(teleportContainer.innerHTML).toBe(
|
||||
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
|
||||
|
@ -617,7 +617,7 @@ describe('SSR hydration', () => {
|
|||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
|
||||
expect(teleportContainer.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
|
@ -660,21 +660,21 @@ describe('SSR hydration', () => {
|
|||
// server render
|
||||
container.innerHTML = await renderToString(h(App))
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
expect(teleportContainer.innerHTML).toBe('')
|
||||
|
||||
// hydrate
|
||||
createSSRApp(App).mount(container)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
|
||||
expect(`Hydration children mismatch`).toHaveBeenWarned()
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
|
||||
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 () => {
|
||||
const id = 'child-reload'
|
||||
const Child = {
|
||||
|
|
|
@ -187,6 +187,8 @@ export interface VaporInteropInterface {
|
|||
unmount(vnode: VNode, doRemove?: boolean): void
|
||||
move(vnode: 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
|
||||
vdomUnmount: UnmountComponentFn
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
Comment as VComment,
|
||||
type VNode,
|
||||
type VNodeHook,
|
||||
VaporSlot,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
invokeVNodeHook,
|
||||
|
@ -31,11 +32,16 @@ import {
|
|||
isRenderableAttrValue,
|
||||
isReservedProp,
|
||||
isString,
|
||||
isVaporAnchors,
|
||||
normalizeClass,
|
||||
normalizeStyle,
|
||||
stringifyStyle,
|
||||
} from '@vue/shared'
|
||||
import { type RendererInternals, needTransition } from './renderer'
|
||||
import {
|
||||
type RendererInternals,
|
||||
getVaporInterface,
|
||||
needTransition,
|
||||
} from './renderer'
|
||||
import { setRef } from './rendererTemplateRef'
|
||||
import {
|
||||
type SuspenseBoundary,
|
||||
|
@ -111,7 +117,7 @@ export function createHydrationFunctions(
|
|||
o: {
|
||||
patchProp,
|
||||
createText,
|
||||
nextSibling,
|
||||
nextSibling: next,
|
||||
parentNode,
|
||||
remove,
|
||||
insert,
|
||||
|
@ -119,6 +125,15 @@ export function createHydrationFunctions(
|
|||
},
|
||||
} = 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) => {
|
||||
if (!container.hasChildNodes()) {
|
||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
|
@ -145,6 +160,10 @@ export function createHydrationFunctions(
|
|||
slotScopeIds: string[] | null,
|
||||
optimized = false,
|
||||
): Node | null => {
|
||||
// skip vapor mode specific anchors
|
||||
if (isVaporAnchors(node)) {
|
||||
node = nextSibling(node)!
|
||||
}
|
||||
optimized = optimized || !!vnode.dynamicChildren
|
||||
const isFragmentStart = isComment(node) && node.data === '['
|
||||
const onMismatch = () =>
|
||||
|
@ -258,6 +277,12 @@ export function createHydrationFunctions(
|
|||
)
|
||||
}
|
||||
break
|
||||
case VaporSlot:
|
||||
getVaporInterface(parentComponent, vnode).hydrateSlot(
|
||||
vnode,
|
||||
parentNode(node)!,
|
||||
)
|
||||
break
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
if (
|
||||
|
@ -278,10 +303,6 @@ export function createHydrationFunctions(
|
|||
)
|
||||
}
|
||||
} 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
|
||||
// has .el set, the component will perform hydration instead of mount
|
||||
// on its sub-tree.
|
||||
|
@ -302,15 +323,23 @@ export function createHydrationFunctions(
|
|||
nextNode = nextSibling(node)
|
||||
}
|
||||
|
||||
mountComponent(
|
||||
vnode,
|
||||
container,
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
getContainerType(container),
|
||||
optimized,
|
||||
)
|
||||
// hydrate vapor component
|
||||
if ((vnode.type as ConcreteComponent).__vapor) {
|
||||
const vaporInterface = getVaporInterface(parentComponent, vnode)
|
||||
vaporInterface.hydrate(node, () => {
|
||||
vaporInterface.mount(vnode, container, null, parentComponent)
|
||||
})
|
||||
} else {
|
||||
mountComponent(
|
||||
vnode,
|
||||
container,
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
getContainerType(container),
|
||||
optimized,
|
||||
)
|
||||
}
|
||||
|
||||
// #3787
|
||||
// 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.
|
||||
const cur = next
|
||||
next = next.nextSibling
|
||||
next = nextSibling(next)
|
||||
remove(cur)
|
||||
}
|
||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
|
@ -553,7 +582,7 @@ export function createHydrationFunctions(
|
|||
}
|
||||
}
|
||||
|
||||
return el.nextSibling
|
||||
return nextSibling(el)
|
||||
}
|
||||
|
||||
const hydrateChildren = (
|
||||
|
|
|
@ -107,6 +107,7 @@ export interface Renderer<HostElement = RendererElement> {
|
|||
|
||||
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
|
||||
hydrate: RootHydrateFunction
|
||||
hydrateNode: ReturnType<typeof createHydrationFunctions>[1]
|
||||
}
|
||||
|
||||
export type ElementNamespace = 'svg' | 'mathml' | undefined
|
||||
|
@ -2546,6 +2547,7 @@ function baseCreateRenderer(
|
|||
return {
|
||||
render,
|
||||
hydrate,
|
||||
hydrateNode,
|
||||
internals,
|
||||
createApp: createAppAPI(
|
||||
mountApp,
|
||||
|
@ -2665,7 +2667,10 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void {
|
|||
}
|
||||
}
|
||||
|
||||
function getVaporInterface(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getVaporInterface(
|
||||
instance: ComponentInternalInstance | null,
|
||||
vnode: VNode,
|
||||
): VaporInteropInterface {
|
||||
|
|
|
@ -319,7 +319,7 @@ export * from './jsx'
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
export { ensureRenderer, normalizeContainer }
|
||||
export { ensureRenderer, ensureHydrationRenderer, normalizeContainer }
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,8 @@ import {
|
|||
insertionParent,
|
||||
resetInsertionState,
|
||||
} from './insertionState'
|
||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||
import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared'
|
||||
import { isHydrating } from './dom/hydration'
|
||||
|
||||
export function createDynamicComponent(
|
||||
getter: () => any,
|
||||
|
@ -19,15 +20,12 @@ export function createDynamicComponent(
|
|||
): VaporFragment {
|
||||
const _insertionParent = insertionParent
|
||||
const _insertionAnchor = insertionAnchor
|
||||
if (isHydrating) {
|
||||
locateHydrationNode()
|
||||
} else {
|
||||
resetInsertionState()
|
||||
}
|
||||
if (!isHydrating) resetInsertionState()
|
||||
|
||||
const frag = __DEV__
|
||||
? new DynamicFragment('dynamic-component')
|
||||
: new DynamicFragment()
|
||||
const frag =
|
||||
isHydrating || __DEV__
|
||||
? new DynamicFragment(DYNAMIC_COMPONENT_ANCHOR_LABEL)
|
||||
: new DynamicFragment()
|
||||
|
||||
renderEffect(() => {
|
||||
const value = getter()
|
||||
|
@ -46,6 +44,5 @@ export function createDynamicComponent(
|
|||
if (!isHydrating && _insertionParent) {
|
||||
insert(frag, _insertionParent, _insertionAnchor)
|
||||
}
|
||||
|
||||
return frag
|
||||
}
|
||||
|
|
|
@ -11,7 +11,13 @@ import {
|
|||
toReactive,
|
||||
toReadonly,
|
||||
} 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 {
|
||||
type Block,
|
||||
|
@ -24,7 +30,12 @@ import { currentInstance, isVaporComponent } from './component'
|
|||
import type { DynamicSlot } from './componentSlots'
|
||||
import { renderEffect } from './renderEffect'
|
||||
import { VaporVForFlags } from '../../shared/src/vaporFlags'
|
||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||
import {
|
||||
currentHydrationNode,
|
||||
isHydrating,
|
||||
locateHydrationNode,
|
||||
locateVaporFragmentAnchor,
|
||||
} from './dom/hydration'
|
||||
import {
|
||||
insertionAnchor,
|
||||
insertionParent,
|
||||
|
@ -87,8 +98,20 @@ export const createFor = (
|
|||
let oldBlocks: ForBlock[] = []
|
||||
let newBlocks: ForBlock[]
|
||||
let parent: ParentNode | undefined | null
|
||||
// TODO handle this in hydration
|
||||
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
|
||||
let parentAnchor: Node
|
||||
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 instance = currentInstance!
|
||||
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 { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||
import { isHydrating } from './dom/hydration'
|
||||
import {
|
||||
insertionAnchor,
|
||||
insertionParent,
|
||||
|
@ -15,17 +16,16 @@ export function createIf(
|
|||
): Block {
|
||||
const _insertionParent = insertionParent
|
||||
const _insertionAnchor = insertionAnchor
|
||||
if (isHydrating) {
|
||||
locateHydrationNode()
|
||||
} else {
|
||||
resetInsertionState()
|
||||
}
|
||||
if (!isHydrating) resetInsertionState()
|
||||
|
||||
let frag: Block
|
||||
if (once) {
|
||||
frag = condition() ? b1() : b2 ? b2() : []
|
||||
} 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))
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,13 @@ import {
|
|||
} from './component'
|
||||
import { createComment, createTextNode } from './dom/node'
|
||||
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
|
||||
import { isHydrating } from './dom/hydration'
|
||||
import {
|
||||
currentHydrationNode,
|
||||
isComment,
|
||||
isHydrating,
|
||||
locateHydrationNode,
|
||||
locateVaporFragmentAnchor,
|
||||
} from './dom/hydration'
|
||||
|
||||
export type Block =
|
||||
| Node
|
||||
|
@ -30,15 +36,20 @@ export class VaporFragment {
|
|||
}
|
||||
|
||||
export class DynamicFragment extends VaporFragment {
|
||||
anchor: Node
|
||||
anchor!: Node
|
||||
scope: EffectScope | undefined
|
||||
current?: BlockFn
|
||||
fallback?: BlockFn
|
||||
|
||||
constructor(anchorLabel?: string) {
|
||||
super([])
|
||||
this.anchor =
|
||||
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
|
||||
if (isHydrating) {
|
||||
locateHydrationNode()
|
||||
this.hydrate(anchorLabel!)
|
||||
} else {
|
||||
this.anchor =
|
||||
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
|
||||
}
|
||||
}
|
||||
|
||||
update(render?: BlockFn, key: any = render): void {
|
||||
|
@ -75,6 +86,22 @@ export class DynamicFragment extends VaporFragment {
|
|||
|
||||
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 {
|
||||
|
@ -126,7 +153,6 @@ export function insert(
|
|||
} else {
|
||||
// fragment
|
||||
if (block.insert) {
|
||||
// TODO handle hydration for vdom interop
|
||||
block.insert(parent, anchor)
|
||||
} else {
|
||||
insert(block.nodes, parent, anchor)
|
||||
|
|
|
@ -58,7 +58,13 @@ import {
|
|||
getSlot,
|
||||
} from './componentSlots'
|
||||
import { hmrReload, hmrRerender } from './hmr'
|
||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||
import {
|
||||
adoptTemplate,
|
||||
currentHydrationNode,
|
||||
isHydrating,
|
||||
locateHydrationNode,
|
||||
setCurrentHydrationNode,
|
||||
} from './dom/hydration'
|
||||
import {
|
||||
insertionAnchor,
|
||||
insertionParent,
|
||||
|
@ -157,7 +163,9 @@ export function createComponent(
|
|||
rawProps,
|
||||
rawSlots,
|
||||
)
|
||||
if (!isHydrating && _insertionParent) {
|
||||
|
||||
// `frag.insert` handles both hydration and mounting
|
||||
if (_insertionParent) {
|
||||
insert(frag, _insertionParent, _insertionAnchor)
|
||||
}
|
||||
return frag
|
||||
|
@ -486,7 +494,9 @@ export function createComponentWithFallback(
|
|||
resetInsertionState()
|
||||
}
|
||||
|
||||
const el = document.createElement(comp)
|
||||
const el = isHydrating
|
||||
? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
|
||||
: document.createElement(comp)
|
||||
// mark single root
|
||||
;(el as any).$root = isSingleRoot
|
||||
|
||||
|
@ -497,6 +507,7 @@ export function createComponentWithFallback(
|
|||
}
|
||||
|
||||
if (rawSlots) {
|
||||
isHydrating && setCurrentHydrationNode(el.firstChild)
|
||||
if (rawSlots.$) {
|
||||
// TODO dynamic slot fragment
|
||||
} 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 { rawPropsProxyHandlers } from './componentProps'
|
||||
import { currentInstance, isRef } from '@vue/runtime-dom'
|
||||
|
@ -9,7 +16,7 @@ import {
|
|||
insertionParent,
|
||||
resetInsertionState,
|
||||
} from './insertionState'
|
||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||
import { isHydrating } from './dom/hydration'
|
||||
|
||||
export type RawSlots = Record<string, VaporSlot> & {
|
||||
$?: DynamicSlotSource[]
|
||||
|
@ -98,11 +105,7 @@ export function createSlot(
|
|||
): Block {
|
||||
const _insertionParent = insertionParent
|
||||
const _insertionAnchor = insertionAnchor
|
||||
if (isHydrating) {
|
||||
locateHydrationNode()
|
||||
} else {
|
||||
resetInsertionState()
|
||||
}
|
||||
if (!isHydrating) resetInsertionState()
|
||||
|
||||
const instance = currentInstance as VaporComponentInstance
|
||||
const rawSlots = instance.rawSlots
|
||||
|
@ -111,7 +114,6 @@ export function createSlot(
|
|||
: EMPTY_OBJ
|
||||
|
||||
let fragment: DynamicFragment
|
||||
|
||||
if (isRef(rawSlots._)) {
|
||||
fragment = instance.appContext.vapor!.vdomSlot(
|
||||
rawSlots._,
|
||||
|
@ -121,7 +123,10 @@ export function createSlot(
|
|||
fallback,
|
||||
)
|
||||
} else {
|
||||
fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
|
||||
fragment =
|
||||
isHydrating || __DEV__
|
||||
? new DynamicFragment(SLOT_ANCHOR_LABEL)
|
||||
: new DynamicFragment()
|
||||
const isDynamicName = isFunction(name)
|
||||
const renderSlot = () => {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,12 @@ import {
|
|||
resetInsertionState,
|
||||
setInsertionState,
|
||||
} from '../insertionState'
|
||||
import { child, next } from './node'
|
||||
import {
|
||||
_nthChild,
|
||||
disableHydrationNodeLookup,
|
||||
enableHydrationNodeLookup,
|
||||
} from './node'
|
||||
import { isVaporAnchors } from '@vue/shared'
|
||||
|
||||
export let isHydrating = false
|
||||
export let currentHydrationNode: Node | null = null
|
||||
|
@ -16,33 +21,53 @@ export function setCurrentHydrationNode(node: Node | null): void {
|
|||
|
||||
let isOptimized = false
|
||||
|
||||
export function withHydration(container: ParentNode, fn: () => void): void {
|
||||
adoptTemplate = adoptTemplateImpl
|
||||
locateHydrationNode = locateHydrationNodeImpl
|
||||
function performHydration<T>(
|
||||
fn: () => T,
|
||||
setup: () => void,
|
||||
cleanup: () => void,
|
||||
): T {
|
||||
if (!isOptimized) {
|
||||
adoptTemplate = adoptTemplateImpl
|
||||
locateHydrationNode = locateHydrationNodeImpl
|
||||
|
||||
// optimize anchor cache lookup
|
||||
;(Comment.prototype as any).$fs = undefined
|
||||
;(Comment.prototype as any).$fe = undefined
|
||||
;(Node.prototype as any).$dp = undefined
|
||||
isOptimized = true
|
||||
}
|
||||
enableHydrationNodeLookup()
|
||||
isHydrating = true
|
||||
setInsertionState(container, 0)
|
||||
setup()
|
||||
const res = fn()
|
||||
resetInsertionState()
|
||||
cleanup()
|
||||
currentHydrationNode = null
|
||||
isHydrating = false
|
||||
disableHydrationNodeLookup()
|
||||
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 locateHydrationNode: () => void
|
||||
|
||||
type Anchor = Comment & {
|
||||
// cached matching fragment start to avoid repeated traversal
|
||||
// cached matching fragment end to avoid repeated traversal
|
||||
// on nested fragments
|
||||
$fs?: Anchor
|
||||
$fe?: Anchor
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -51,7 +76,7 @@ const isComment = (node: Node, data: string): node is Anchor =>
|
|||
*/
|
||||
function adoptTemplateImpl(node: Node, template: string): Node | null {
|
||||
if (!(template[0] === '<' && template[1] === '!')) {
|
||||
while (node.nodeType === 8) node = next(node)
|
||||
while (node.nodeType === 8) node = node.nextSibling!
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
|
@ -71,51 +96,27 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
|||
}
|
||||
}
|
||||
|
||||
currentHydrationNode = next(node)
|
||||
currentHydrationNode = node.nextSibling
|
||||
return node
|
||||
}
|
||||
|
||||
function locateHydrationNodeImpl() {
|
||||
let node: Node | null
|
||||
|
||||
// prepend / firstChild
|
||||
if (insertionAnchor === 0) {
|
||||
node = child(insertionParent!)
|
||||
} else {
|
||||
node = insertionParent!.firstChild
|
||||
} 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
|
||||
? insertionAnchor.previousSibling
|
||||
: insertionParent
|
||||
? insertionParent.lastChild
|
||||
: currentHydrationNode
|
||||
} else {
|
||||
node = currentHydrationNode
|
||||
|
||||
if (node && isComment(node, ']')) {
|
||||
// fragment backward search
|
||||
if (node.$fs) {
|
||||
// already cached matching fragment start
|
||||
node = node.$fs
|
||||
} else {
|
||||
let cur: Node | null = node
|
||||
let curFragEnd = node
|
||||
let fragDepth = 0
|
||||
node = null
|
||||
while (cur) {
|
||||
cur = cur.previousSibling
|
||||
if (cur) {
|
||||
if (isComment(cur, '[')) {
|
||||
curFragEnd.$fs = cur
|
||||
if (!fragDepth) {
|
||||
node = cur
|
||||
break
|
||||
} else {
|
||||
fragDepth--
|
||||
}
|
||||
} else if (isComment(cur, ']')) {
|
||||
curFragEnd = cur
|
||||
fragDepth++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if current hydration node is not under the current parent, or no
|
||||
// current node, find node by dynamic position or use the first child
|
||||
if (insertionParent && (!node || node.parentNode !== insertionParent)) {
|
||||
node = _nthChild(insertionParent, insertionParent.$dp || 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,3 +128,55 @@ function locateHydrationNodeImpl() {
|
|||
resetInsertionState()
|
||||
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__ */
|
||||
export function createTextNode(value = ''): Text {
|
||||
return document.createTextNode(value)
|
||||
|
@ -14,16 +21,175 @@ export function querySelector(selectors: string): Element | null {
|
|||
}
|
||||
|
||||
/*! #__NO_SIDE_EFFECTS__ */
|
||||
export function child(node: ParentNode): Node {
|
||||
export function _child(node: ParentNode): Node {
|
||||
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__ */
|
||||
export function nthChild(node: Node, i: number): Node {
|
||||
return node.childNodes[i]
|
||||
export function __child(node: ParentNode): Node {
|
||||
let n = node.firstChild!
|
||||
|
||||
if (isComment(n, '[')) {
|
||||
n = locateEndAnchor(n)!.nextSibling!
|
||||
}
|
||||
|
||||
while (n && isVaporAnchors(n)) {
|
||||
n = n.nextSibling!
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
/*! #__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!
|
||||
}
|
||||
|
||||
/**
|
||||
* 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__ */
|
||||
export function template(html: string, root?: boolean) {
|
||||
let node: Node
|
||||
return (): Node & { $root?: true } => {
|
||||
return (n?: number): Node & { $root?: true } => {
|
||||
if (isHydrating) {
|
||||
if (__DEV__ && !currentHydrationNode) {
|
||||
// TODO this should not happen
|
||||
throw new Error('No current hydration node')
|
||||
}
|
||||
return adoptTemplate(currentHydrationNode!, html)!
|
||||
node = adoptTemplate(currentHydrationNode!, html)!
|
||||
// dynamic node position, default is 0
|
||||
;(node as any).$dp = n || 0
|
||||
if (root) (node as any).$root = true
|
||||
return node
|
||||
}
|
||||
// fast path for text nodes
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
type App,
|
||||
type ComponentInternalInstance,
|
||||
type ConcreteComponent,
|
||||
type HydrationRenderer,
|
||||
MoveType,
|
||||
type Plugin,
|
||||
type RendererInternals,
|
||||
|
@ -12,6 +13,7 @@ import {
|
|||
createInternalObject,
|
||||
createVNode,
|
||||
currentInstance,
|
||||
ensureHydrationRenderer,
|
||||
ensureRenderer,
|
||||
isEmitListener,
|
||||
onScopeDispose,
|
||||
|
@ -36,6 +38,14 @@ import type { RawSlots, VaporSlot } from './componentSlots'
|
|||
import { renderEffect } from './renderEffect'
|
||||
import { createTextNode } from './dom/node'
|
||||
import { optimizePropertyLookup } from './dom/prop'
|
||||
import {
|
||||
currentHydrationNode,
|
||||
isHydrating,
|
||||
locateHydrationNode,
|
||||
locateVaporFragmentAnchor,
|
||||
setCurrentHydrationNode,
|
||||
hydrateNode as vaporHydrateNode,
|
||||
} from './dom/hydration'
|
||||
|
||||
// mounting vapor components and slots in vdom
|
||||
const vaporInteropImpl: Omit<
|
||||
|
@ -116,6 +126,18 @@ const vaporInteropImpl: Omit<
|
|||
insert(vnode.vb || (vnode.component 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<
|
||||
|
@ -142,6 +164,8 @@ const vaporSlotsProxyHandler: ProxyHandler<any> = {
|
|||
},
|
||||
}
|
||||
|
||||
let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
|
||||
|
||||
/**
|
||||
* Mount vdom component in vapor
|
||||
*/
|
||||
|
@ -187,16 +211,20 @@ function createVDOMComponent(
|
|||
}
|
||||
|
||||
frag.insert = (parentNode, anchor) => {
|
||||
if (!isMounted) {
|
||||
internals.mt(
|
||||
vnode,
|
||||
parentNode,
|
||||
anchor,
|
||||
parentInstance as any,
|
||||
null,
|
||||
undefined,
|
||||
false,
|
||||
)
|
||||
if (!isMounted || isHydrating) {
|
||||
if (isHydrating) {
|
||||
hydrateVNode(vnode, parentInstance as any)
|
||||
} else {
|
||||
internals.mt(
|
||||
vnode,
|
||||
parentNode,
|
||||
anchor,
|
||||
parentInstance as any,
|
||||
null,
|
||||
undefined,
|
||||
false,
|
||||
)
|
||||
}
|
||||
onScopeDispose(unmount, true)
|
||||
isMounted = true
|
||||
} else {
|
||||
|
@ -241,28 +269,32 @@ function renderVDOMSlot(
|
|||
isFunction(name) ? name() : name,
|
||||
props,
|
||||
)
|
||||
if ((vnode.children as any[]).length) {
|
||||
if (fallbackNodes) {
|
||||
remove(fallbackNodes, parentNode)
|
||||
fallbackNodes = undefined
|
||||
}
|
||||
internals.p(
|
||||
oldVNode,
|
||||
vnode,
|
||||
parentNode,
|
||||
anchor,
|
||||
parentComponent as any,
|
||||
)
|
||||
oldVNode = vnode
|
||||
if (isHydrating) {
|
||||
hydrateVNode(vnode!, parentComponent as any)
|
||||
} else {
|
||||
if (fallback && !fallbackNodes) {
|
||||
// mount fallback
|
||||
if (oldVNode) {
|
||||
internals.um(oldVNode, parentComponent as any, null, true)
|
||||
if ((vnode.children as any[]).length) {
|
||||
if (fallbackNodes) {
|
||||
remove(fallbackNodes, parentNode)
|
||||
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
|
||||
|
@ -289,6 +321,23 @@ function renderVDOMSlot(
|
|||
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 => {
|
||||
const internals = ensureRenderer().internals
|
||||
app._context.vapor = extend(vaporInteropImpl, {
|
||||
|
|
|
@ -446,7 +446,7 @@ function testRender(type: string, render: typeof renderToString) {
|
|||
).toBe(
|
||||
`<div>parent<div class="child">` +
|
||||
`<!--[--><span>from slot</span><!--]-->` +
|
||||
`</div></div>`,
|
||||
`<!--slot--></div></div>`,
|
||||
)
|
||||
|
||||
// test fallback
|
||||
|
@ -461,7 +461,7 @@ function testRender(type: string, render: typeof renderToString) {
|
|||
}),
|
||||
),
|
||||
).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(
|
||||
`<div>parent<div class="child">` +
|
||||
`<!--[--><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(
|
||||
`<div>parent<div class="child">` +
|
||||
`<!--[--><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(
|
||||
`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`,
|
||||
`<div><!--[--><!--[-->hello<!--]--><!--slot--><!--]--><!--slot--></div>`,
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -593,7 +593,7 @@ function testRender(type: string, render: typeof renderToString) {
|
|||
|
||||
expect(await render(app)).toBe(
|
||||
// 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(
|
||||
// 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"/>`,
|
||||
}
|
||||
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(
|
||||
`<span class="bar"></span>`,
|
||||
|
|
|
@ -14,7 +14,9 @@ describe('ssr: dynamic 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 () => {
|
||||
|
@ -30,7 +32,7 @@ describe('ssr: dynamic component', () => {
|
|||
}),
|
||||
),
|
||||
).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>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<p><span>slot</span></p>`)
|
||||
).toBe(`<p><span>slot</span></p><!--dynamic-component-->`)
|
||||
})
|
||||
|
||||
test('resolve to component vnode', async () => {
|
||||
|
@ -60,7 +62,9 @@ describe('ssr: dynamic 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 () => {
|
||||
|
@ -75,6 +79,6 @@ describe('ssr: dynamic 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))
|
||||
expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
|
||||
expect(result).toBe(
|
||||
`<!--[--><div parent wrapper-s child></div><!--]--><!--slot-->`,
|
||||
)
|
||||
})
|
||||
|
||||
// #2892
|
||||
|
@ -150,8 +152,8 @@ describe('ssr: scopedId runtime behavior', () => {
|
|||
const result = await renderToString(createApp(Root))
|
||||
expect(result).toBe(
|
||||
`<div class="wrapper" root slotted wrapper>` +
|
||||
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
|
||||
`</div>`,
|
||||
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
|
||||
`<!--slot--></div>`,
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -265,8 +267,8 @@ describe('ssr: scopedId runtime behavior', () => {
|
|||
const result = await renderToString(createApp(Root))
|
||||
expect(result).toBe(
|
||||
`<div class="wrapper" root slotted wrapper>` +
|
||||
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
|
||||
`</div>`,
|
||||
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
|
||||
`<!--slot--></div>`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('ssr: slot', () => {
|
|||
template: `<one>hello</one>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div><!--[-->hello<!--]--></div>`)
|
||||
).toBe(`<div><!--[-->hello<!--]--><!--slot--></div>`)
|
||||
})
|
||||
|
||||
test('element slot', async () => {
|
||||
|
@ -27,7 +27,7 @@ describe('ssr: slot', () => {
|
|||
template: `<one><div>hi</div></one>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div><!--[--><div>hi</div><!--]--></div>`)
|
||||
).toBe(`<div><!--[--><div>hi</div><!--]--><!--slot--></div>`)
|
||||
})
|
||||
|
||||
test('empty slot', async () => {
|
||||
|
@ -42,7 +42,7 @@ describe('ssr: slot', () => {
|
|||
template: `<one><template v-if="false"/></one>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div><!--[--><!--]--></div>`)
|
||||
).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
|
||||
})
|
||||
|
||||
test('empty slot (manual comments)', async () => {
|
||||
|
@ -57,7 +57,7 @@ describe('ssr: slot', () => {
|
|||
template: `<one><!--hello--></one>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div><!--[--><!--]--></div>`)
|
||||
).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
|
||||
})
|
||||
|
||||
test('empty slot (multi-line comments)', async () => {
|
||||
|
@ -72,7 +72,7 @@ describe('ssr: slot', () => {
|
|||
template: `<one><!--he\nllo--></one>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div><!--[--><!--]--></div>`)
|
||||
).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
|
||||
})
|
||||
|
||||
test('multiple elements', async () => {
|
||||
|
@ -83,7 +83,7 @@ describe('ssr: slot', () => {
|
|||
template: `<one><div>one</div><div>two</div></one>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--></div>`)
|
||||
).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--><!--slot--></div>`)
|
||||
})
|
||||
|
||||
test('fragment slot (template v-if)', async () => {
|
||||
|
@ -94,7 +94,9 @@ describe('ssr: slot', () => {
|
|||
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 () => {
|
||||
|
@ -106,7 +108,7 @@ describe('ssr: slot', () => {
|
|||
}),
|
||||
),
|
||||
).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>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div>foo</div>`)
|
||||
).toBe(`<div>foo</div><!--if-->`)
|
||||
})
|
||||
|
||||
// #9933
|
||||
|
@ -183,7 +185,7 @@ describe('ssr: slot', () => {
|
|||
`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<div><!--[--> new header <!--]--></div>`)
|
||||
).toBe(`<div><!--[--> new header <!--]--><!--slot--></div>`)
|
||||
})
|
||||
|
||||
// #11326
|
||||
|
@ -202,7 +204,9 @@ describe('ssr: slot', () => {
|
|||
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(
|
||||
await renderToString(
|
||||
|
@ -219,7 +223,7 @@ describe('ssr: slot', () => {
|
|||
}),
|
||||
),
|
||||
).toBe(
|
||||
`<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`,
|
||||
`<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
|
||||
)
|
||||
|
||||
expect(
|
||||
|
@ -233,6 +237,6 @@ describe('ssr: slot', () => {
|
|||
template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`,
|
||||
}),
|
||||
),
|
||||
).toBe(`<button><!--[--><!--]--></button>`)
|
||||
).toBe(`<button><!--[--><!--]--></button><!--dynamic-component-->`)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
type SSRBufferItem,
|
||||
renderVNodeChildren,
|
||||
} from '../render'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared'
|
||||
|
||||
const { ensureValidVNode } = ssrUtils
|
||||
|
||||
|
@ -37,7 +37,7 @@ export function ssrRenderSlot(
|
|||
parentComponent,
|
||||
slotScopeId,
|
||||
)
|
||||
push(`<!--]-->`)
|
||||
push(`<!--]--><!--${SLOT_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
|
||||
export function ssrRenderSlotInner(
|
||||
|
@ -104,7 +104,7 @@ export function ssrRenderSlotInner(
|
|||
if (
|
||||
transition &&
|
||||
slotBuffer[0] === '<!--[-->' &&
|
||||
slotBuffer[end - 1] === '<!--]-->'
|
||||
(slotBuffer[end - 1] as string).startsWith('<!--]-->')
|
||||
) {
|
||||
start++
|
||||
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 './typeUtils'
|
||||
export * from './subSequence'
|
||||
export * from './domAnchors'
|
||||
|
|
Loading…
Reference in New Issue