diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 2fde4560ec4..fb2fff86574 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -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(\`<!---->\`) } diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index f1d509acfb0..d344405f3ed 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -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>\`" + `) + }) + }) }) diff --git a/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts b/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts index 7b3d1962c3e..712c09d0946 100644 --- a/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts @@ -29,6 +29,7 @@ describe('ssr: attrs fallthrough', () => { _push(\`<!--[-->\`) if (true) { _push(\`<div></div>\`) + _push(\`<!--if-->\`) } else { _push(\`<!---->\`) } diff --git a/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts b/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts index 9e70dac0bdc..0666e8949cc 100644 --- a/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts @@ -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) diff --git a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts index 86863cfb85f..4d73dfe0827 100644 --- a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts @@ -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(\`<!---->\`) } diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index 82122e621c7..73d4331a7d7 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -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(\`<!--]-->\`) }" diff --git a/packages/compiler-ssr/__tests__/ssrVFor.spec.ts b/packages/compiler-ssr/__tests__/ssrVFor.spec.ts index 0d957265120..dad426de04c 100644 --- a/packages/compiler-ssr/__tests__/ssrVFor.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVFor.spec.ts @@ -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-->\`) }" `) }) diff --git a/packages/compiler-ssr/__tests__/ssrVIf.spec.ts b/packages/compiler-ssr/__tests__/ssrVIf.spec.ts index b544adadcf3..840d485088b 100644 --- a/packages/compiler-ssr/__tests__/ssrVIf.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVIf.spec.ts @@ -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>\`) } diff --git a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts index 0bf7673d00d..c88f6ba3182 100644 --- a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts @@ -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(\`<!---->\`) } diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts index 536cbb5c1e9..852e02820cc 100644 --- a/packages/compiler-ssr/src/ssrCodegenTransform.ts +++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts @@ -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 +} diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index cad1ee81028..419c2e4e49c 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -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}-->`) } } } diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts index 6537eee8287..251b1fef5c9 100644 --- a/packages/compiler-ssr/src/transforms/ssrVFor.ts +++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts @@ -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}-->`) } diff --git a/packages/compiler-ssr/src/transforms/ssrVIf.ts b/packages/compiler-ssr/src/transforms/ssrVIf.ts index 0e3880247a1..811252c0fab 100644 --- a/packages/compiler-ssr/src/transforms/ssrVIf.ts +++ b/packages/compiler-ssr/src/transforms/ssrVIf.ts @@ -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 } diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index e56676d8706..d4a8b6827bb 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -157,9 +157,9 @@ export function render(_ctx, $props, $emit, $attrs, $slots) { const _component_Comp = _resolveComponent("Comp") const n0 = t0() const n3 = t1() + const n2 = _child(n3) _setInsertionState(n3, 0) const n1 = _createComponentWithFallback(_component_Comp) - const n2 = _child(n3) _renderEffect(() => { _setText(n2, _toDisplayString(_ctx.bar)) _setProp(n3, "id", _ctx.foo) @@ -230,6 +230,30 @@ export function render(_ctx) { }" `; +exports[`compile > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = ` +"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("<div></div>") +const t1 = _template("<div><div></div><!><div></div><!><div><button></button></div></div>", true) + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n6 = t1() + const n5 = _next(_child(n6)) + const n7 = _nthChild(n6, 3) + const p0 = _next(n7) + const n4 = _child(p0) + _setInsertionState(n6, n5) + const n0 = _createComponentWithFallback(_component_Comp) + _setInsertionState(n6, n7) + const n1 = _createIf(() => (true), () => { + const n3 = t0() + return n3 + }) + _renderEffect(() => _setProp(n4, "disabled", _ctx.foo)) + return n6 +}" +`; + exports[`compile > static + dynamic root 1`] = ` "import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue'; const t0 = _template(" ") diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts index 33f399caa77..3a2ce41f0cd 100644 --- a/packages/compiler-vapor/__tests__/compile.spec.ts +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -220,4 +220,21 @@ describe('compile', () => { expect(code).matchSnapshot() }) }) + + describe('setInsertionState', () => { + test('next, child and nthChild should be above the setInsertionState', () => { + const code = compile(` + <div> + <div /> + <Comp /> + <div /> + <div v-if="true" /> + <div> + <button :disabled="foo" /> + </div> + </div> + `) + expect(code).toMatchSnapshot() + }) + }) }) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap index f2eade4bcdf..d823c4ed0af 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap @@ -43,6 +43,24 @@ export function render(_ctx) { }" `; +exports[`compiler: template ref transform > static ref (PROD) 1`] = ` +" + const _setTemplateRef = _createTemplateRefSetter() + const n0 = t0() + _setTemplateRef(n0, foo) + return n0 +" +`; + +exports[`compiler: template ref transform > static ref (inline mode) 1`] = ` +" + const _setTemplateRef = _createTemplateRefSetter() + const n0 = t0() + _setTemplateRef(n0, foo) + return n0 +" +`; + exports[`compiler: template ref transform > static ref 1`] = ` "import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue'; const t0 = _template("<div></div>", true) diff --git a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts index e656312356c..2d8ae8c960d 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts @@ -69,8 +69,8 @@ describe('compiler: children transform', () => { </div>`, ) // ensure the insertion anchor is generated before the insertion statement - expect(code).toMatch(`const n3 = _next(_child(n4)) - _setInsertionState(n4, n3)`) + expect(code).toMatch(`const n3 = _next(_child(n4))`) + expect(code).toMatch(`_setInsertionState(n4, n3)`) expect(code).toMatchSnapshot() }) }) diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts index 6be8f18779c..3dc9c80876b 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts @@ -1,3 +1,4 @@ +import { BindingTypes } from '@vue/compiler-dom' import { DynamicFlag, type ForIRNode, @@ -48,6 +49,16 @@ describe('compiler: template ref transform', () => { expect(code).contains('_setTemplateRef(n0, "foo")') }) + test('static ref (inline mode)', () => { + const { code } = compileWithTransformRef(`<div ref="foo" />`, { + inline: true, + bindingMetadata: { foo: BindingTypes.SETUP_REF }, + }) + expect(code).matchSnapshot() + // pass the actual ref + expect(code).contains('_setTemplateRef(n0, foo)') + }) + test('dynamic ref', () => { const { ir, code } = compileWithTransformRef(`<div :ref="foo" />`) diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts index b161b8f45d1..48e4b5cb890 100644 --- a/packages/compiler-vapor/src/generators/block.ts +++ b/packages/compiler-vapor/src/generators/block.ts @@ -52,7 +52,7 @@ export function genBlockContent( push(...genSelf(child, context)) } for (const child of dynamic.children) { - push(...genChildren(child, context, `n${child.id!}`)) + push(...genChildren(child, context, push, `n${child.id!}`)) } push(...genOperations(operation, context)) diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts index 4247bc6feca..a3bf5cc2193 100644 --- a/packages/compiler-vapor/src/generators/operation.ts +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -44,7 +44,7 @@ export function genOperationWithInsertionState( ): CodeFragment[] { const [frag, push] = buildCodeFragment() if (isBlockOperation(oper) && oper.parent) { - push(...genInsertionstate(oper, context)) + push(...genInsertionState(oper, context)) } push(...genOperation(oper, context)) return frag @@ -152,7 +152,7 @@ export function genEffect( return frag } -function genInsertionstate( +function genInsertionState( operation: InsertionStateTypes, context: CodegenContext, ): CodeFragment[] { diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 356c1ccbe15..5a066b09e9a 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -41,6 +41,7 @@ export function genSelf( export function genChildren( dynamic: IRDynamicInfo, context: CodegenContext, + pushBlock: (...items: CodeFragment[]) => number, from: string = `n${dynamic.id}`, ): CodeFragment[] { const { helper } = context @@ -72,17 +73,17 @@ export function genChildren( // p for "placeholder" variables that are meant for possible reuse by // other access paths const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}` - push(NEWLINE, `const ${variable} = `) + pushBlock(NEWLINE, `const ${variable} = `) if (prev) { if (elementIndex - prev[1] === 1) { - push(...genCall(helper('next'), prev[0])) + pushBlock(...genCall(helper('next'), prev[0])) } else { - push(...genCall(helper('nthChild'), from, String(elementIndex))) + pushBlock(...genCall(helper('nthChild'), from, String(elementIndex))) } } else { if (elementIndex === 0) { - push(...genCall(helper('child'), from)) + pushBlock(...genCall(helper('child'), from)) } else { // check if there's a node that we can reuse from let init = genCall(helper('child'), from) @@ -91,7 +92,7 @@ export function genChildren( } else if (elementIndex > 1) { init = genCall(helper('nthChild'), from, String(elementIndex)) } - push(...init) + pushBlock(...init) } } @@ -109,7 +110,7 @@ export function genChildren( if (childrenToGen.length) { for (const [child, from] of childrenToGen) { - push(...genChildren(child, context, from)) + push(...genChildren(child, context, pushBlock, from)) } } diff --git a/packages/compiler-vapor/src/generators/templateRef.ts b/packages/compiler-vapor/src/generators/templateRef.ts index a4d6d546ed3..af8facc57b1 100644 --- a/packages/compiler-vapor/src/generators/templateRef.ts +++ b/packages/compiler-vapor/src/generators/templateRef.ts @@ -2,6 +2,7 @@ import { genExpression } from './expression' import type { CodegenContext } from '../generate' import type { DeclareOldRefIRNode, SetTemplateRefIRNode } from '../ir' import { type CodeFragment, NEWLINE, genCall } from './utils' +import { BindingTypes, type SimpleExpressionNode } from '@vue/compiler-dom' export const setTemplateRefIdent = `_setTemplateRef` @@ -15,7 +16,7 @@ export function genSetTemplateRef( ...genCall( setTemplateRefIdent, // will be generated in root scope `n${oper.element}`, - genExpression(oper.value, context), + genRefValue(oper.value, context), oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined, oper.refFor && 'true', ), @@ -25,3 +26,20 @@ export function genSetTemplateRef( export function genDeclareOldRef(oper: DeclareOldRefIRNode): CodeFragment[] { return [NEWLINE, `let r${oper.id}`] } + +function genRefValue(value: SimpleExpressionNode, context: CodegenContext) { + // in inline mode there is no setupState object, so we can't use string + // keys to set the ref. Instead, we need to transform it to pass the + // actual ref instead. + if (!__BROWSER__ && value && context.options.inline) { + const binding = context.options.bindingMetadata[value.content] + if ( + binding === BindingTypes.SETUP_LET || + binding === BindingTypes.SETUP_REF || + binding === BindingTypes.SETUP_MAYBE_REF + ) { + return [value.content] + } + } + return genExpression(value, context) +} diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts index 790cd9d6fb1..da47438c2a8 100644 --- a/packages/compiler-vapor/src/transforms/transformChildren.ts +++ b/packages/compiler-vapor/src/transforms/transformChildren.ts @@ -70,10 +70,23 @@ 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) + // 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 */) } diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 56011d06359..793c11cede3 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -598,14 +598,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-->', @@ -614,7 +614,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('') }) @@ -657,21 +657,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('') }) @@ -1843,6 +1843,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() + }) + }) + describe('mismatch handling', () => { test('text node', () => { const { container } = mountWithHydration(`foo`, () => 'bar') diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index d8ae73fb69d..e34545f8b53 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -193,6 +193,7 @@ 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 vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any vdomUnmount: UnmountComponentFn diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index ef6f1918c31..8ed7b6af189 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -31,11 +31,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 +116,7 @@ export function createHydrationFunctions( o: { patchProp, createText, - nextSibling, + nextSibling: next, parentNode, remove, insert, @@ -119,6 +124,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 +159,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 = () => @@ -278,10 +296,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 +316,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 +473,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 +575,7 @@ export function createHydrationFunctions( } } - return el.nextSibling + return nextSibling(el) } const hydrateChildren = ( diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5a18d62a8e1..bb895a480ad 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -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 @@ -2524,6 +2525,7 @@ function baseCreateRenderer( return { render, hydrate, + hydrateNode, internals, createApp: createAppAPI( mountApp, @@ -2639,7 +2641,10 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void { } } -function getVaporInterface( +/** + * @internal + */ +export function getVaporInterface( instance: ComponentInternalInstance | null, vnode: VNode, ): VaporInteropInterface { diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 51c72fe2ed1..0500110a4e0 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -319,7 +319,7 @@ export * from './jsx' /** * @internal */ -export { ensureRenderer, normalizeContainer } +export { ensureRenderer, ensureHydrationRenderer, normalizeContainer } /** * @internal */ diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 6ba2bf895fb..fbc27f1d419 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1,9 +1,16 @@ import { createVaporSSRApp, delegateEvents } from '../src' -import { nextTick, ref } from '@vue/runtime-dom' +import { nextTick, reactive, ref } from '@vue/runtime-dom' import { compileScript, parse } from '@vue/compiler-sfc' import * as runtimeVapor from '../src' import * as runtimeDom from '@vue/runtime-dom' import * as VueServerRenderer from '@vue/server-renderer' +import { + DYNAMIC_COMPONENT_ANCHOR_LABEL, + FOR_ANCHOR_LABEL, + IF_ANCHOR_LABEL, + SLOT_ANCHOR_LABEL, + isString, +} from '@vue/shared' const Vue = { ...runtimeDom, ...runtimeVapor } @@ -11,7 +18,7 @@ function compile( sfc: string, data: runtimeDom.Ref<any>, components: Record<string, any> = {}, - ssr = false, + { vapor = true, ssr = false } = {}, ) { if (!sfc.includes(`<script`)) { sfc = @@ -25,7 +32,7 @@ function compile( isProd: true, inlineTemplate: true, genDefaultAs: '__sfc__', - vapor: true, + vapor, templateOptions: { ssr, }, @@ -47,19 +54,37 @@ function compile( ) } +async function testHydrationInterop( + code: string, + components?: Record<string, string | { code: string; vapor: boolean }>, + data?: any, +) { + return testHydration(code, components, data, { interop: true, vapor: false }) +} + async function testHydration( code: string, - components: Record<string, string> = {}, + components: Record<string, string | { code: string; vapor: boolean }> = {}, + data: any = ref('foo'), + { interop = false, vapor = true } = {}, ) { - const data = ref('foo') const ssrComponents: any = {} const clientComponents: any = {} for (const key in components) { - clientComponents[key] = compile(components[key], data, clientComponents) - ssrComponents[key] = compile(components[key], data, ssrComponents, true) + const comp = components[key] + const code = isString(comp) ? comp : comp.code + const isVaporComp = isString(comp) || !!comp.vapor + clientComponents[key] = compile(code, data, clientComponents, { + vapor: isVaporComp, + ssr: false, + }) + ssrComponents[key] = compile(code, data, ssrComponents, { + vapor: isVaporComp, + ssr: true, + }) } - const serverComp = compile(code, data, ssrComponents, true) + const serverComp = compile(code, data, ssrComponents, { vapor, ssr: true }) const html = await VueServerRenderer.renderToString( runtimeDom.createSSRApp(serverComp), ) @@ -67,8 +92,17 @@ async function testHydration( document.body.appendChild(container) container.innerHTML = html - const clientComp = compile(code, data, clientComponents) - const app = createVaporSSRApp(clientComp) + const clientComp = compile(code, data, clientComponents, { + vapor, + ssr: false, + }) + let app + if (interop) { + app = runtimeDom.createSSRApp(clientComp) + app.use(runtimeVapor.vaporInteropPlugin) + } else { + app = createVaporSSRApp(clientComp) + } app.mount(container) return { data, container } } @@ -78,72 +112,127 @@ const triggerEvent = (type: string, el: Element) => { el.dispatchEvent(event) } -describe('Vapor Mode hydration', () => { - delegateEvents('click') +delegateEvents('click') - beforeEach(() => { - document.body.innerHTML = '' - }) +beforeEach(() => { + document.body.innerHTML = '' +}) - test('root text', async () => { - const { data, container } = await testHydration(` +describe('Vapor Mode hydration', () => { + describe('text', () => { + test('root text', async () => { + const { data, container } = await testHydration(` <template>{{ data }}</template> `) - expect(container.innerHTML).toMatchInlineSnapshot(`"foo"`) + expect(container.innerHTML).toMatchInlineSnapshot(`"foo"`) - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot(`"bar"`) - }) + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot(`"bar"`) + }) - test('root comment', async () => { - const { container } = await testHydration(` - <template><!----></template> + test('consecutive text nodes', async () => { + const { data, container } = await testHydration(` + <template>{{ data }}{{ data }}</template> `) - expect(container.innerHTML).toBe('<!---->') - expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned() - }) + expect(container.innerHTML).toMatchInlineSnapshot(`"foofoo"`) - test('root with mixed element and text', async () => { - const { container, data } = await testHydration(` - <template> A<span>{{ data }}</span>{{ data }}</template> + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot(`"barbar"`) + }) + + test('consecutive text nodes with anchor insertion', async () => { + const { data, container } = await testHydration(` + <template><span/>{{ data }}{{ data }}<span/></template> `) - expect(container.innerHTML).toMatchInlineSnapshot( - `"<!--[--> A<span>foo</span>foo<!--]-->"`, - ) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<!--[--><span></span>foofoo<span></span><!--]-->"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<!--[--><span></span>barbar<span></span><!--]-->"`, + ) + }) + + test('mixed text nodes', async () => { + const { data, container } = await testHydration(` + <template>{{ data }}A{{ data }}B{{ data }}</template> + `) + expect(container.innerHTML).toMatchInlineSnapshot(`"fooAfooBfoo"`) - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot( - `"<!--[--> A<span>bar</span>bar<!--]-->"`, - ) - }) + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot(`"barAbarBbar"`) + }) - test('empty element', async () => { - const { container } = await testHydration(` - <template><div/></template> + test('mixed text nodes with anchor insertion', async () => { + const { data, container } = await testHydration(` + <template><span/>{{ data }}A{{ data }}B{{ data }}<span/></template> `) - expect(container.innerHTML).toBe('<div></div>') - expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<!--[--><span></span>fooAfooBfoo<span></span><!--]-->"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<!--[--><span></span>barAbarBbar<span></span><!--]-->"`, + ) + }) }) - test('element with binding and text children', async () => { - const { container, data } = await testHydration(` - <template><div :class="data">{{ data }}</div></template> + describe('element', () => { + test('root comment', async () => { + const { container } = await testHydration(` + <template><!----></template> `) - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div class="foo">foo</div>"`, - ) + expect(container.innerHTML).toBe('<!---->') + expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned() + }) - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div class="bar">bar</div>"`, - ) - }) + test('root with mixed element and text', async () => { + const { container, data } = await testHydration(` + <template> A<span>{{ data }}</span>{{ data }}</template> + `) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<!--[--> A<span>foo</span>foo<!--]-->"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<!--[--> A<span>bar</span>bar<!--]-->"`, + ) + }) + + test('empty element', async () => { + const { container } = await testHydration(` + <template><div/></template> + `) + expect(container.innerHTML).toBe('<div></div>') + expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned() + }) - test('element with elements children', async () => { - const { container } = await testHydration(` + test('element with binding and text children', async () => { + const { container, data } = await testHydration(` + <template><div :class="data">{{ data }}</div></template> + `) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div class="foo">foo</div>"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div class="bar">bar</div>"`, + ) + }) + + test('element with elements children', async () => { + const { container } = await testHydration(` <template> <div> <span>{{ data }}</span> @@ -151,99 +240,126 @@ describe('Vapor Mode hydration', () => { </div> </template> `) - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><span>foo</span><span class="foo"></span></div>"`, - ) - - // event handler - triggerEvent('click', container.querySelector('.foo')!) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span>foo</span><span class="foo"></span></div>"`, + ) + + // event handler + triggerEvent('click', container.querySelector('.foo')!) + + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span>bar</span><span class="bar"></span></div>"`, + ) + }) + + test('element with ref', async () => { + const { data, container } = await testHydration( + `<template> + <div ref="data">hi</div> + </template> + `, + {}, + ref(null), + ) - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><span>bar</span><span class="bar"></span></div>"`, - ) + expect(data.value).toBe(container.firstChild) + }) }) - test('basic component', async () => { - const { container, data } = await testHydration( - ` + describe('component', () => { + test('basic component', async () => { + const { container, data } = await testHydration( + ` <template><div><span></span><components.Child/></div></template> `, - { Child: `<template>{{ data }}</template>` }, - ) - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><span></span>foo</div>"`, - ) - - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><span></span>bar</div>"`, - ) - }) - - test('fragment component', async () => { - const { container, data } = await testHydration( - ` + { Child: `<template>{{ data }}</template>` }, + ) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span></span>foo</div>"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span></span>bar</div>"`, + ) + }) + + test('fragment component', async () => { + const { container, data } = await testHydration( + ` <template><div><span></span><components.Child/></div></template> `, - { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` }, - ) - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><span></span><!--[--><div>foo</div>-foo-<!--]--></div>"`, - ) - - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><span></span><!--[--><div>bar</div>-bar-<!--]--></div>"`, - ) - }) - - test('fragment component with prepend', async () => { - const { container, data } = await testHydration( - ` + { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` }, + ) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span></span><!--[--><div>foo</div>-foo-<!--]--></div>"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span></span><!--[--><div>bar</div>-bar-<!--]--></div>"`, + ) + }) + + test('fragment component with prepend', async () => { + const { container, data } = await testHydration( + ` <template><div><components.Child/><span></span></div></template> `, - { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` }, - ) - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><!--[--><div>foo</div>-foo-<!--]--><span></span></div>"`, - ) - - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><!--[--><div>bar</div>-bar-<!--]--><span></span></div>"`, - ) - }) - - test('nested fragment components', async () => { - const { container, data } = await testHydration( - ` + { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` }, + ) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><!--[--><div>foo</div>-foo-<!--]--><span></span></div>"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><!--[--><div>bar</div>-bar-<!--]--><span></span></div>"`, + ) + }) + + test('nested fragment components', async () => { + const { container, data } = await testHydration( + ` <template><div><components.Parent/><span></span></div></template> `, - { - Parent: `<template><div/><components.Child/><div/></template>`, - Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, - }, - ) - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><!--[--><div></div><!--[--><div>foo</div>-foo-<!--]--><div></div><!--]--><span></span></div>"`, - ) - - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot( - `"<div><!--[--><div></div><!--[--><div>bar</div>-bar-<!--]--><div></div><!--]--><span></span></div>"`, - ) - }) - - // problem is the <!> placeholder does not exist in SSR output - test.todo('component with anchor insertion', async () => { - const { container, data } = await testHydration( - ` - <template> + { + Parent: `<template><div/><components.Child/><div/></template>`, + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<div></div>` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<div></div>` + + `<!--]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<div></div>` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<div></div>` + + `<!--]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('component with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> <div> <span/> <components.Child/> @@ -251,20 +367,82 @@ describe('Vapor Mode hydration', () => { </div> </template> `, - { - Child: `<template>{{ data }}</template>`, - }, - ) - expect(container.innerHTML).toMatchInlineSnapshot() - - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot() - }) - - test.todo('consecutive component with anchor insertion', async () => { - const { container, data } = await testHydration( - `<template> + { + Child: `<template>{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span></span>foo<span></span></div>"`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><span></span>bar<span></span></div>"`, + ) + }) + + test('nested components with anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><components.Parent/></template> + `, + { + Parent: `<template><div><span/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div><span></span><div>foo</div><span></span></div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div><span></span><div>bar</div><span></span></div>`, + ) + }) + + test('nested components with multi level anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><div><span></span><components.Parent/><span></span></div></template> + `, + { + Parent: `<template><div><span/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<div>foo</div>` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<div>bar</div>` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + }) + + test('consecutive components with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> <div> <span/> <components.Child/> @@ -273,31 +451,1921 @@ describe('Vapor Mode hydration', () => { </div> </template> `, - { - Child: `<template>{{ data }}</template>`, - }, - ) - expect(container.innerHTML).toMatchInlineSnapshot() + { + Child: `<template>{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `foo` + + `<!--[[-->foo<!--]]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `bar` + + `<!--[[-->bar<!--]]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('nested consecutive components with anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><components.Parent/></template> + `, + { + Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>foo</div>` + + `<!--[[--><div>foo</div><!--]]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>bar</div>` + + `<!--[[--><div>bar</div><!--]]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('nested consecutive components with multi level anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><div><span></span><components.Parent/><span></span></div></template> + `, + { + Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<div>foo</div>` + + `<!--[[--><div>foo</div><!--]]-->` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<div>bar</div>` + + `<!--[[--><div>bar</div><!--]]-->` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + }) + + test('mixed component and element with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <components.Child/> + <span/> + <components.Child/> + <span/> + </div> + </template> + `, + { + Child: `<template>{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `foo` + + `<span></span>` + + `foo` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `bar` + + `<span></span>` + + `bar` + + `<span></span>` + + `</div>`, + ) + }) + + test('mixed component and text with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <components.Child/> + {{ data }} + <components.Child/> + <span/> + </div> + </template> + `, + { + Child: `<template>{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `foo` + + `<!--[[--> foo <!--]]-->` + + `foo` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `bar` + + `<!--[[--> bar <!--]]-->` + + `bar` + + `<span></span>` + + `</div>`, + ) + }) + + test('fragment component with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <components.Child/> + <span/> + </div> + </template> + `, + { + Child: `<template><div>{{ data }}</div>-{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo<!--]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar<!--]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('nested fragment component with anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><components.Parent/></template> + `, + { + Parent: `<template><div><span/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('nested fragment component with multi level anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><div><span/><components.Parent/><span/></div></template> + `, + { + Parent: `<template><div><span/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + }) + + test('consecutive fragment components with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <components.Child/> + <components.Child/> + <span/> + </div> + </template> + `, + { + Child: `<template><div>{{ data }}</div>-{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo<!--]-->` + + `<!--[[-->` + + `<!--[--><div>foo</div>-foo<!--]-->` + + `<!--]]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar<!--]-->` + + `<!--[[-->` + + `<!--[--><div>bar</div>-bar<!--]-->` + + `<!--]]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('nested consecutive fragment components with anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><components.Parent/></template> + `, + { + Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<!--[[-->` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<!--]]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<!--[[-->` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<!--]]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('nested consecutive fragment components with multi level anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><div><span></span><components.Parent/><span></span></div></template> + `, + { + Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`, + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<!--[[-->` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<!--]]-->` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<!--[[-->` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<!--]]-->` + + `<span></span>` + + `</div>` + + `<span></span>` + + `</div>`, + ) + }) + + test('nested consecutive fragment components with root level anchor insertion', async () => { + const { container, data } = await testHydration( + ` + <template><div><span></span><components.Parent/><span></span></div></template> + `, + { + Parent: `<template><components.Child/><components.Child/></template>`, + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<!--[--><div>foo</div>-foo-<!--]-->` + + `<!--]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<!--[--><div>bar</div>-bar-<!--]-->` + + `<!--]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('mixed fragment component and element with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <components.Child/> + <span/> + <components.Child/> + <span/> + </div> + </template> + `, + { + Child: `<template><div>{{ data }}</div>-{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo<!--]-->` + + `<span></span>` + + `<!--[--><div>foo</div>-foo<!--]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar<!--]-->` + + `<span></span>` + + `<!--[--><div>bar</div>-bar<!--]-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('mixed fragment component and text with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <components.Child/> + {{ data }} + <components.Child/> + <span/> + </div> + </template> + `, + { + Child: `<template><div>{{ data }}</div>-{{ data }}</template>`, + }, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>foo</div>-foo<!--]-->` + + ` <!--[[--> foo <!--]]--> ` + + `<!--[--><div>foo</div>-foo<!--]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>bar</div>-bar<!--]-->` + + ` <!--[[--> bar <!--]]--> ` + + `<!--[--><div>bar</div>-bar<!--]-->` + + `<span></span>` + + `</div>`, + ) + }) + }) - data.value = 'bar' - await nextTick() - expect(container.innerHTML).toMatchInlineSnapshot() + describe('dynamic component', () => { + const anchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL + + test('basic dynamic component', async () => { + const { container, data } = await testHydration( + `<template> + <component :is="components[data]"/> + </template>`, + { + foo: `<template><div>foo</div></template>`, + bar: `<template><div>bar</div></template>`, + }, + ref('foo'), + ) + expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`) + }) + + test('dynamic component with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <component :is="components[data]"/> + <span/> + </div> + </template>`, + { + foo: `<template><div>foo</div></template>`, + bar: `<template><div>bar</div></template>`, + }, + ref('foo'), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>foo</div><!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>bar</div><!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('consecutive dynamic components with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <component :is="components[data]"/> + <component :is="components[data]"/> + <span/> + </div> + </template>`, + { + foo: `<template><div>foo</div></template>`, + bar: `<template><div>bar</div></template>`, + }, + ref('foo'), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>foo</div><!--${anchorLabel}-->` + + `<!--[[--><div>foo</div><!--${anchorLabel}--><!--]]-->` + + `<span></span>` + + `</div>`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<div>bar</div><!--${anchorLabel}-->` + + `<!--[[--><div>bar</div><!--${anchorLabel}--><!--]]-->` + + `<span></span>` + + `</div>`, + ) + }) }) - test.todo('if') + describe('if', () => { + const anchorLabel = IF_ANCHOR_LABEL + + test('basic toggle - true -> false', async () => { + const data = ref(true) + const { container } = await testHydration( + `<template> + <div v-if="data">foo</div> + </template>`, + undefined, + data, + ) + expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`) + }) + + test('basic toggle - false -> true', async () => { + const data = ref(false) + const { container } = await testHydration( + `<template> + <div v-if="data">foo</div> + </template>`, + undefined, + data, + ) + // v-if="false" is rendered as <!----> in the server-rendered HTML + // it reused as anchor, so the anchor label is empty + expect(container.innerHTML).toBe(`<!---->`) + + data.value = true + await nextTick() + expect(container.innerHTML).toBe(`<div>foo</div><!---->`) + }) + + test('v-if/else-if/else chain - switch branches', async () => { + const data = ref('a') + const { container } = await testHydration( + `<template> + <div v-if="data === 'a'">foo</div> + <div v-else-if="data === 'b'">bar</div> + <div v-else>baz</div> + </template>`, + undefined, + data, + ) + expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`) + + data.value = 'b' + await nextTick() + expect(container.innerHTML).toBe( + `<div>bar</div><!--${anchorLabel}--><!--${anchorLabel}-->`, + ) + + data.value = 'c' + await nextTick() + expect(container.innerHTML).toBe( + `<div>baz</div><!--${anchorLabel}--><!--${anchorLabel}-->`, + ) + + data.value = 'a' + await nextTick() + expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`) + }) + + test('v-if/else-if/else chain - switch branches (PROD)', async () => { + try { + __DEV__ = false + const data = ref('a') + const { container } = await testHydration( + `<template> + <div v-if="data === 'a'">foo</div> + <div v-else-if="data === 'b'">bar</div> + <div v-else>baz</div> + </template>`, + undefined, + data, + ) + expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`) + + data.value = 'b' + await nextTick() + // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node, + // so it won't be rendered + expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`) + + data.value = 'c' + await nextTick() + // same as above + expect(container.innerHTML).toBe(`<div>baz</div><!--${anchorLabel}-->`) + + data.value = 'a' + await nextTick() + expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`) + } finally { + __DEV__ = true + } + }) + + test('nested if', async () => { + const data = reactive({ outer: true, inner: true }) + const { container } = await testHydration( + `<template> + <div v-if="data.outer"> + <span>outer</span> + <div v-if="data.inner">inner</div> + </div> + </template>`, + undefined, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span>outer</span>` + + `<div>inner</div><!--${anchorLabel}-->` + + `</div><!--${anchorLabel}-->`, + ) + + data.inner = false + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span>outer</span>` + + `<!--${anchorLabel}-->` + + `</div><!--${anchorLabel}-->`, + ) + + data.outer = false + await nextTick() + expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`) + }) + + test('on component', async () => { + const data = ref(true) + const { container } = await testHydration( + `<template> + <components.Child v-if="data"/> + </template>`, + { Child: `<template>foo</template>` }, + data, + ) + expect(container.innerHTML).toBe(`foo<!--${anchorLabel}-->`) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`) + }) + + test('v-if/else-if/else chain on component - switch branches', async () => { + const data = ref('a') + const { container } = await testHydration( + `<template> + <components.Child1 v-if="data === 'a'"/> + <components.Child2 v-else-if="data === 'b'"/> + <components.Child3 v-else/> + </template>`, + { + Child1: `<template><span>{{data}} child1</span></template>`, + Child2: `<template><span>{{data}} child2</span></template>`, + Child3: `<template><span>{{data}} child3</span></template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<span>a child1</span><!--${anchorLabel}-->`, + ) + + data.value = 'b' + await nextTick() + expect(container.innerHTML).toBe( + `<span>b child2</span><!--${anchorLabel}--><!--${anchorLabel}-->`, + ) + + data.value = 'c' + await nextTick() + expect(container.innerHTML).toBe( + `<span>c child3</span><!--${anchorLabel}--><!--${anchorLabel}-->`, + ) + + data.value = 'a' + await nextTick() + expect(container.innerHTML).toBe( + `<span>a child1</span><!--${anchorLabel}-->`, + ) + }) + + test('v-if/else-if/else chain on component - switch branches (PROD)', async () => { + try { + __DEV__ = false + const data = ref('a') + const { container } = await testHydration( + `<template> + <components.Child1 v-if="data === 'a'"/> + <components.Child2 v-else-if="data === 'b'"/> + <components.Child3 v-else/> + </template>`, + { + Child1: `<template><span>{{data}} child1</span></template>`, + Child2: `<template><span>{{data}} child2</span></template>`, + Child3: `<template><span>{{data}} child3</span></template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<span>a child1</span><!--${anchorLabel}-->`, + ) + + data.value = 'b' + await nextTick() + // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node, + // so it won't be rendered + expect(container.innerHTML).toBe( + `<span>b child2</span><!--${anchorLabel}-->`, + ) + + data.value = 'c' + await nextTick() + // same as above + expect(container.innerHTML).toBe( + `<span>c child3</span><!--${anchorLabel}-->`, + ) + + data.value = 'a' + await nextTick() + expect(container.innerHTML).toBe( + `<span>a child1</span><!--${anchorLabel}-->`, + ) + } finally { + __DEV__ = true + } + }) + + test('on component with anchor insertion', async () => { + const data = ref(true) + const { container } = await testHydration( + `<template> + <div> + <span/> + <components.Child v-if="data"/> + <span/> + </div> + </template>`, + { Child: `<template>foo</template>` }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `foo<!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('consecutive v-if on component with anchor insertion', async () => { + const data = ref(true) + const { container } = await testHydration( + `<template> + <div> + <span/> + <components.Child v-if="data"/> + <components.Child v-if="data"/> + <span/> + </div> + </template>`, + { Child: `<template>foo</template>` }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `foo<!--${anchorLabel}-->` + + `foo<!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--${anchorLabel}-->` + + `<!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('on fragment component', async () => { + const data = ref(true) + const { container } = await testHydration( + `<template> + <div> + <components.Child v-if="data"/> + </div> + </template>`, + { + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<!--[--><div>true</div>-true-<!--]-->` + + `<!--if-->` + + `</div>`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `<div><!--[--><!--]--><!--${anchorLabel}--></div>`, + ) + }) + + test('on fragment component with anchor insertion', async () => { + const data = ref(true) + const { container } = await testHydration( + `<template> + <div> + <span/> + <components.Child v-if="data"/> + <span/> + </div> + </template>`, + { + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>true</div>-true-<!--]-->` + + `<!--if-->` + + `<span></span>` + + `</div>`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><!--]-->` + + `<!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('consecutive v-if on fragment component with anchor insertion', async () => { + const data = ref(true) + const { container } = await testHydration( + `<template> + <div> + <span/> + <components.Child v-if="data"/> + <components.Child v-if="data"/> + <span/> + </div> + </template>`, + { + Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` + + `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><!--]--><!--${anchorLabel}-->` + + `<!--[--><!--]--><!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('on dynamic component with anchor insertion', async () => { + const dynamicComponentAnchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL + const data = ref(true) + const { container } = await testHydration( + `<template> + <div> + <span/> + <component :is="components.Child" v-if="data"/> + <span/> + </div> + </template>`, + { Child: `<template>foo</template>` }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `foo<!--${dynamicComponentAnchorLabel}--><!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--${anchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + }) - test.todo('for') + describe('for', () => { + const forAnchorLabel = FOR_ANCHOR_LABEL + const slotAnchorLabel = SLOT_ANCHOR_LABEL + + test('basic v-for', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span v-for="item in data" :key="item">{{ item }}</span> + </div> + </template>`, + undefined, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]--><!--${forAnchorLabel}-->` + + `</div>`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `</div>`, + ) + }) + + test('v-for with text node', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span v-for="item in data" :key="item">{{ item }}</span> + </div> + </template>`, + undefined, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<span>a</span><span>b</span><span>c</span>` + + `<!--]--><!--${forAnchorLabel}-->` + + `</div>`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<span>a</span><span>b</span><span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `</div>`, + ) + }) + + test('v-for with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <span v-for="item in data" :key="item">{{ item }}</span> + <span/> + </div> + </template>`, + undefined, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]--><!--${forAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value.splice(0, 1) + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('consecutive v-for with anchor insertion', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <span/> + <span v-for="item in data" :key="item">{{ item }}</span> + <span v-for="item in data" :key="item">{{ item }}</span> + <span/> + </div> + </template>`, + undefined, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]--><!--${forAnchorLabel}-->` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]--><!--${forAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `<!--[-->` + + `<span>a</span>` + + `<span>b</span>` + + `<span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.value.splice(0, 2) + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[-->` + + `<span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `<!--[-->` + + `<span>c</span>` + + `<!--]-->` + + `<span>d</span>` + + `<!--${forAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + + test('v-for on component', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <components.Child v-for="item in data" :key="item"/> + </div> + </template>`, + { + Child: `<template><div>comp</div></template>`, + }, + ref(['a', 'b', 'c']), + ) + + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<div>comp</div>` + + `<div>comp</div>` + + `<div>comp</div>` + + `<!--]--><!--${forAnchorLabel}-->` + + `</div>`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<div>comp</div>` + + `<div>comp</div>` + + `<div>comp</div>` + + `<!--]-->` + + `<div>comp</div>` + + `<!--${forAnchorLabel}-->` + + `</div>`, + ) + }) + + test('v-for on component with slots', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <components.Child v-for="item in data" :key="item"> + <span>{{ item }}</span> + </components.Child> + </div> + </template>`, + { + Child: `<template><slot/></template>`, + }, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--]--><!--${forAnchorLabel}-->` + + `</div>`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--]-->` + + `<span>d</span><!--${slotAnchorLabel}-->` + + `<!--${forAnchorLabel}-->` + + `</div>`, + ) + }) + + test('on fragment component', async () => { + const { container, data } = await testHydration( + `<template> + <div> + <components.Child v-for="item in data" :key="item"/> + </div> + </template>`, + { + Child: `<template><div>foo</div>-bar-</template>`, + }, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<!--[--><div>foo</div>-bar-<!--]-->` + + `<!--[--><div>foo</div>-bar-<!--]-->` + + `<!--[--><div>foo</div>-bar-<!--]-->` + + `<!--]--><!--${forAnchorLabel}-->` + + `</div>`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[-->` + + `<!--[--><div>foo</div>-bar-<!--]-->` + + `<!--[--><div>foo</div>-bar-<!--]-->` + + `<!--[--><div>foo</div>-bar-<!--]-->` + + `<!--]-->` + + `<div>foo</div>-bar-` + + `<!--${forAnchorLabel}-->` + + `</div>`, + ) + }) + }) - test.todo('slots') + describe('slots', () => { + const slotAnchorLabel = SLOT_ANCHOR_LABEL + const forAnchorLabel = FOR_ANCHOR_LABEL + test('basic slot', async () => { + const { data, container } = await testHydration( + `<template> + <components.Child> + <span>{{data}}</span> + </components.Child> + </template>`, + { + Child: `<template><slot/></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`, + ) + }) + + test('named slot', async () => { + const { data, container } = await testHydration( + `<template> + <components.Child> + <template #foo> + <span>{{data}}</span> + </template> + </components.Child> + </template>`, + { + Child: `<template><slot name="foo"/></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`, + ) + }) + + test('named slot with v-if', async () => { + const { data, container } = await testHydration( + `<template> + <components.Child> + <template #foo v-if="data"> + <span>{{data}}</span> + </template> + </components.Child> + </template>`, + { + Child: `<template><slot name="foo"/></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `<!--[--><!--]--><!--${slotAnchorLabel}-->`, + ) + }) + + test('named slot with v-if and v-for', async () => { + const data = reactive({ + show: true, + items: ['a', 'b', 'c'], + }) + const { container } = await testHydration( + `<template> + <components.Child> + <template #foo v-if="data.show"> + <span v-for="item in data.items" :key="item">{{item}}</span> + </template> + </components.Child> + </template>`, + { + Child: `<template><slot name="foo"/></template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<!--[-->` + + `<!--[--><span>a</span><span>b</span><span>c</span><!--]--><!--${forAnchorLabel}-->` + + `<!--]-->` + + `<!--${slotAnchorLabel}-->`, + ) + + data.show = false + await nextTick() + expect(container.innerHTML).toBe( + `<!--[--><!--[--><!--]--><!--]--><!--${slotAnchorLabel}-->`, + ) + }) + + test('with anchor insertion', async () => { + const { data, container } = await testHydration( + `<template> + <components.Child> + <span/> + <span>{{data}}</span> + <span/> + </components.Child> + </template>`, + { + Child: `<template><slot/></template>`, + }, + ) + expect(container.innerHTML).toBe( + `<!--[-->` + + `<span></span>` + + `<span>foo</span>` + + `<span></span>` + + `<!--]-->` + + `<!--${slotAnchorLabel}-->`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<!--[-->` + + `<span></span>` + + `<span>bar</span>` + + `<span></span>` + + `<!--]-->` + + `<!--${slotAnchorLabel}-->`, + ) + }) + + test('with multi level anchor insertion', async () => { + const { data, container } = await testHydration( + `<template> + <components.Child> + <span/> + <span>{{data}}</span> + <span/> + </components.Child> + </template>`, + { + Child: ` + <template> + <div/> + <div/> + <slot/> + <div/> + </div> + </template>`, + }, + ) + expect(container.innerHTML).toBe( + `<!--[-->` + + `<div></div>` + + `<div></div>` + + `<!--[-->` + + `<span></span>` + + `<span>foo</span>` + + `<span></span>` + + `<!--]-->` + + `<!--${slotAnchorLabel}-->` + + `<div></div>` + + `<!--]-->`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<!--[-->` + + `<div></div>` + + `<div></div>` + + `<!--[-->` + + `<span></span>` + + `<span>bar</span>` + + `<span></span>` + + `<!--]-->` + + `<!--${slotAnchorLabel}-->` + + `<div></div>` + + `<!--]-->`, + ) + }) + + test('mixed slot and text node', async () => { + const data = reactive({ + text: 'foo', + msg: 'hi', + }) + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.text}}</span> + </components.Child> + </template>`, + { + Child: `<template><div><slot/>{{data.msg}}</div></template>`, + }, + data, + ) + + expect(container.innerHTML).toBe( + `<div>` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `hi` + + `</div>`, + ) + + data.msg = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `bar` + + `</div>`, + ) + }) + + test('mixed root slot and text node', async () => { + const data = reactive({ + text: 'foo', + msg: 'hi', + }) + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.text}}</span> + </components.Child> + </template>`, + { + Child: `<template>{{data.text}}<slot/>{{data.msg}}</template>`, + }, + data, + ) + + expect(container.innerHTML).toBe( + `<!--[-->` + + `foo` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `hi` + + `<!--]-->`, + ) + + data.msg = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<!--[-->` + + `foo` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `bar` + + `<!--]-->`, + ) + }) + + test('mixed slot and element', async () => { + const data = reactive({ + text: 'foo', + msg: 'hi', + }) + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.text}}</span> + </components.Child> + </template>`, + { + Child: `<template><div><slot/><div>{{data.msg}}</div></div></template>`, + }, + data, + ) + + expect(container.innerHTML).toBe( + `<div>` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<div>hi</div>` + + `</div>`, + ) + + data.msg = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<div>bar</div>` + + `</div>`, + ) + }) + + test('mixed slot and component', async () => { + const data = reactive({ + msg1: 'foo', + msg2: 'bar', + }) + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.msg1}}</span> + </components.Child> + </template>`, + { + Child: ` + <template> + <div> + <components.Child2/> + <slot/> + <components.Child2/> + </div> + </template>`, + Child2: ` + <template> + <div>{{data.msg2}}</div> + </template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<div>bar</div>` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<div>bar</div>` + + `</div>`, + ) + + data.msg2 = 'hello' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<div>hello</div>` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<div>hello</div>` + + `</div>`, + ) + }) + + test('mixed slot and fragment component', async () => { + const data = reactive({ + msg1: 'foo', + msg2: 'bar', + }) + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.msg1}}</span> + </components.Child> + </template>`, + { + Child: ` + <template> + <div> + <components.Child2/> + <slot/> + <components.Child2/> + </div> + </template>`, + Child2: ` + <template> + <div>{{data.msg1}}</div> {{data.msg2}} + </template>`, + }, + data, + ) + expect(container.innerHTML).toBe( + `<div>` + + `<!--[--><div>foo</div> bar<!--]-->` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><div>foo</div> bar<!--]-->` + + `</div>`, + ) + + data.msg1 = 'hello' + data.msg2 = 'vapor' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<!--[--><div>hello</div> vapor<!--]-->` + + `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><div>hello</div> vapor<!--]-->` + + `</div>`, + ) + }) + + test('mixed slot and v-if', async () => { + const data = reactive({ + show: true, + msg: 'foo', + }) + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.msg}}</span> + </components.Child> + </template>`, + { + Child: ` + <template> + <div v-if="data.show">{{data.msg}}</div> + <slot/> + <div v-if="data.show">{{data.msg}}</div> + </template>`, + }, + data, + ) + + expect(container.innerHTML).toBe( + `<!--[-->` + + `<div>foo</div><!--if-->` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<div>foo</div><!--if-->` + + `<!--]-->`, + ) + + data.show = false + await nextTick() + expect(container.innerHTML).toBe( + `<!--[-->` + + `<!--if-->` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--if-->` + + `<!--]-->`, + ) + }) + + test('mixed slot and v-for', async () => { + const data = reactive({ + items: ['a', 'b', 'c'], + msg: 'foo', + }) + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.msg}}</span> + </components.Child> + </template>`, + { + Child: ` + <template> + <div v-for="item in data.items" :key="item">{{item}}</div> + <slot/> + <div v-for="item in data.items" :key="item">{{item}}</div> + </template>`, + }, + data, + ) + + expect(container.innerHTML).toBe( + `<!--[-->` + + `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` + + `<!--]-->`, + ) + + data.items.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `<!--[-->` + + `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` + + `<!--]-->`, + ) + }) + + test('consecutive slots', async () => { + const data = reactive({ + msg1: 'foo', + msg2: 'bar', + }) + + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.msg1}}</span> + <template #bar> + <span>{{data.msg2}}</span> + </template> + </components.Child> + </template>`, + { + Child: `<template><slot/><slot name="bar"/></template>`, + }, + data, + ) + + expect(container.innerHTML).toBe( + `<!--[-->` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--]-->`, + ) + + data.msg1 = 'hello' + data.msg2 = 'vapor' + await nextTick() + expect(container.innerHTML).toBe( + `<!--[-->` + + `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--]-->`, + ) + }) + + test('consecutive slots with anchor insertion', async () => { + const data = reactive({ + msg1: 'foo', + msg2: 'bar', + }) + + const { container } = await testHydration( + `<template> + <components.Child> + <span>{{data.msg1}}</span> + <template #bar> + <span>{{data.msg2}}</span> + </template> + </components.Child> + </template>`, + { + Child: `<template> + <div> + <span/> + <slot/> + <slot name="bar"/> + <span/> + </div> + </template>`, + }, + data, + ) + + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + + data.msg1 = 'hello' + data.msg2 = 'vapor' + await nextTick() + expect(container.innerHTML).toBe( + `<div>` + + `<span></span>` + + `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` + + `<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` + + `<span></span>` + + `</div>`, + ) + }) + }) - // test('element with ref', () => { - // const el = ref() - // const { vnode, container } = mountWithHydration('<div></div>', () => - // h('div', { ref: el }), - // ) - // expect(vnode.el).toBe(container.firstChild) - // expect(el.value).toBe(vnode.el) - // }) + describe.todo('transition', async () => { + test('transition appear', async () => {}) + test('transition appear with v-if', async () => {}) + test('transition appear with v-show', async () => {}) + test('transition appear w/ event listener', async () => {}) + }) + + describe.todo('async component') + + describe.todo('data-allow-mismatch') // test('with data-allow-mismatch component when using onServerPrefetch', async () => { // const Comp = { @@ -1791,3 +3859,90 @@ describe('Vapor Mode hydration', () => { test.todo('Teleport') test.todo('Suspense') }) + +describe('VDOM hydration interop', () => { + test('basic vapor component', async () => { + const data = ref(true) + const { container } = await testHydrationInterop( + `<script setup>const data = _data; const components = _components;</script> + <template> + <components.VaporChild/> + </template>`, + { + VaporChild: { + code: `<template>{{ data }}</template>`, + vapor: true, + }, + }, + data, + ) + + expect(container.innerHTML).toMatchInlineSnapshot(`"true"`) + + data.value = false + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot(`"false"`) + }) + + test('nested components (VDOM -> Vapor -> VDOM)', async () => { + const data = ref(true) + const { container } = await testHydrationInterop( + `<script setup>const data = _data; const components = _components;</script> + <template> + <components.VaporChild/> + </template>`, + { + VaporChild: { + code: `<template><components.VdomChild/></template>`, + vapor: true, + }, + VdomChild: { + code: `<script setup>const data = _data;</script> + <template>{{ data }}</template>`, + vapor: false, + }, + }, + data, + ) + + expect(container.innerHTML).toMatchInlineSnapshot(`"true"`) + + data.value = false + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot(`"false"`) + }) + + test('vapor slot render vdom component', async () => { + const data = ref(true) + const { container } = await testHydrationInterop( + `<script setup>const data = _data; const components = _components;</script> + <template> + <components.VaporChild> + <components.VdomChild/> + </components.VaporChild> + </template>`, + { + VaporChild: { + code: `<template><div><slot/></div></template>`, + vapor: true, + }, + VdomChild: { + code: `<script setup>const data = _data;</script> + <template>{{ data }}</template>`, + vapor: false, + }, + }, + data, + ) + + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><!--[-->true<!--]--><!--slot--></div>"`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"<div><!--[-->false<!--]--><!--slot--></div>"`, + ) + }) +}) diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 2126611d718..7ae689a0aee 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -1,9 +1,16 @@ import { resolveDynamicComponent } from '@vue/runtime-dom' -import { DynamicFragment, type VaporFragment } from './block' +import { DynamicFragment, type VaporFragment, insert } from './block' import { createComponentWithFallback } from './component' import { renderEffect } from './renderEffect' import type { RawProps } from './componentProps' import type { RawSlots } from './componentSlots' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' +import { isHydrating } from './dom/hydration' +import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared' export function createDynamicComponent( getter: () => any, @@ -11,9 +18,14 @@ export function createDynamicComponent( rawSlots?: RawSlots | null, isSingleRoot?: boolean, ): VaporFragment { - const frag = __DEV__ - ? new DynamicFragment('dynamic-component') - : new DynamicFragment() + const _insertionParent = insertionParent + const _insertionAnchor = insertionAnchor + if (!isHydrating) resetInsertionState() + + const frag = + isHydrating || __DEV__ + ? new DynamicFragment(DYNAMIC_COMPONENT_ANCHOR_LABEL) + : new DynamicFragment() renderEffect(() => { const value = getter() frag.update( @@ -27,5 +39,9 @@ export function createDynamicComponent( value, ) }) + + if (!isHydrating && _insertionParent) { + insert(frag, _insertionParent, _insertionAnchor) + } return frag } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 0cd8317532f..7138d01a6af 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -9,7 +9,13 @@ import { shallowRef, toReactive, } 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, @@ -22,8 +28,17 @@ 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 { insertionAnchor, insertionParent } from './insertionState' +import { + currentHydrationNode, + isHydrating, + locateHydrationNode, + locateVaporFragmentAnchor, +} from './dom/hydration' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' class ForBlock extends VaporFragment { scope: EffectScope | undefined @@ -71,15 +86,29 @@ export const createFor = ( const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor if (isHydrating) { - locateHydrationNode() + locateHydrationNode(true) + } else { + resetInsertionState() } let isMounted = false 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 diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index 71bfa32d5d3..3e370592b32 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -1,6 +1,11 @@ +import { IF_ANCHOR_LABEL } from '@vue/shared' import { type Block, type BlockFn, DynamicFragment, insert } from './block' -import { isHydrating, locateHydrationNode } from './dom/hydration' -import { insertionAnchor, insertionParent } from './insertionState' +import { isHydrating } from './dom/hydration' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' import { renderEffect } from './renderEffect' export function createIf( @@ -11,15 +16,16 @@ export function createIf( ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor - if (isHydrating) { - locateHydrationNode() - } + 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)) } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b782afd38d3..d6be89efc39 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -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(true) + 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) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 548babebf8b..e0396aa2e71 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -59,7 +59,11 @@ import { } from './componentSlots' import { hmrReload, hmrRerender } from './hmr' import { isHydrating, locateHydrationNode } from './dom/hydration' -import { insertionAnchor, insertionParent } from './insertionState' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' export { currentInstance } from '@vue/runtime-dom' @@ -142,6 +146,8 @@ export function createComponent( const _insertionAnchor = insertionAnchor if (isHydrating) { locateHydrationNode() + } else { + resetInsertionState() } // vdom interop enabled and component is not an explicit vapor component @@ -151,7 +157,9 @@ export function createComponent( rawProps, rawSlots, ) - if (!isHydrating && _insertionParent) { + + // `frag.insert` handles both hydration and mounting + if (_insertionParent) { insert(frag, _insertionParent, _insertionAnchor) } return frag @@ -270,7 +278,7 @@ export function createComponent( onScopeDispose(() => unmountComponent(instance), true) if (!isHydrating && _insertionParent) { - insert(instance.block, _insertionParent, _insertionAnchor) + mountComponent(instance, _insertionParent, _insertionAnchor) } return instance diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 74296e09466..3ab49867e8d 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,11 +1,22 @@ -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' import type { LooseRawProps, VaporComponentInstance } from './component' import { renderEffect } from './renderEffect' -import { insertionAnchor, insertionParent } from './insertionState' -import { isHydrating, locateHydrationNode } from './dom/hydration' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' +import { isHydrating } from './dom/hydration' export type RawSlots = Record<string, VaporSlot> & { $?: DynamicSlotSource[] @@ -94,9 +105,7 @@ export function createSlot( ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor - if (isHydrating) { - locateHydrationNode() - } + if (!isHydrating) resetInsertionState() const instance = currentInstance as VaporComponentInstance const rawSlots = instance.rawSlots @@ -105,7 +114,6 @@ export function createSlot( : EMPTY_OBJ let fragment: DynamicFragment - if (isRef(rawSlots._)) { fragment = instance.appContext.vapor!.vdomSlot( rawSlots._, @@ -115,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) @@ -145,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) } diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index d34d9db7da5..e3f666b5b26 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -5,7 +5,12 @@ import { resetInsertionState, setInsertionState, } from '../insertionState' -import { child, next } from './node' +import { + disableHydrationNodeLookup, + enableHydrationNodeLookup, + next, +} from './node' +import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared' export let isHydrating = false export let currentHydrationNode: Node | null = null @@ -16,25 +21,44 @@ 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 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 +export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void type Anchor = Comment & { // cached matching fragment start to avoid repeated traversal @@ -42,7 +66,7 @@ type Anchor = Comment & { $fs?: 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 +75,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__) { @@ -75,18 +99,31 @@ function adoptTemplateImpl(node: Node, template: string): Node | null { return node } -function locateHydrationNodeImpl() { - let node: Node | null +const hydrationPositionMap = new WeakMap<ParentNode, Node>() +function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) { + 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 = insertionParent + ? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild + : currentHydrationNode + + // if node is a vapor fragment anchor, find the previous one + if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) { + node = node.previousSibling + if (__DEV__ && !node) { + // this should not happen + throw new Error(`vapor fragment anchor previous node was not found.`) + } + } if (node && isComment(node, ']')) { // fragment backward search @@ -117,6 +154,11 @@ function locateHydrationNodeImpl() { } } } + + if (insertionParent && node) { + const prev = node.previousSibling + if (prev) hydrationPositionMap.set(insertionParent, prev) + } } if (__DEV__ && !node) { @@ -127,3 +169,51 @@ function locateHydrationNodeImpl() { resetInsertionState() currentHydrationNode = node } + +export function locateEndAnchor( + node: Node | null, + open = '[', + close = ']', +): Node | null { + let match = 0 + while (node) { + node = node.nextSibling + if (node && node.nodeType === 8) { + if ((node as Comment).data === open) match++ + if ((node as Comment).data === close) { + if (match === 0) { + return node + } else { + match-- + } + } + } + } + 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() +} diff --git a/packages/runtime-vapor/src/dom/node.ts b/packages/runtime-vapor/src/dom/node.ts index 83bc32c57f0..3f38c477a01 100644 --- a/packages/runtime-vapor/src/dom/node.ts +++ b/packages/runtime-vapor/src/dom/node.ts @@ -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 __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 nthChild(node: Node, i: number): Node { +export function _nthChild(node: Node, i: number): Node { return node.childNodes[i] } +/** + * Hydration-specific version of `nthChild`. + */ /*! #__NO_SIDE_EFFECTS__ */ -export function next(node: Node): Node { +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 +} diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a0..4d1f4e6f184 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -2,6 +2,7 @@ import { type App, type ComponentInternalInstance, type ConcreteComponent, + type HydrationRenderer, MoveType, type Plugin, type RendererInternals, @@ -11,6 +12,7 @@ import { type VaporInteropInterface, createVNode, currentInstance, + ensureHydrationRenderer, ensureRenderer, onScopeDispose, renderSlot, @@ -33,6 +35,12 @@ import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' +import { + currentHydrationNode, + isHydrating, + locateHydrationNode, + hydrateNode as vaporHydrateNode, +} from './dom/hydration' // mounting vapor components and slots in vdom const vaporInteropImpl: Omit< @@ -113,6 +121,8 @@ const vaporInteropImpl: Omit< insert(vnode.vb || (vnode.component as any), container, anchor) insert(vnode.anchor as any, container, anchor) }, + + hydrate: vaporHydrateNode, } const vaporSlotPropsProxyHandler: ProxyHandler< @@ -139,6 +149,8 @@ const vaporSlotsProxyHandler: ProxyHandler<any> = { }, } +let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined + /** * Mount vdom component in vapor */ @@ -176,16 +188,30 @@ function createVDOMComponent( } frag.insert = (parentNode, anchor) => { - if (!isMounted) { - internals.mt( - vnode, - parentNode, - anchor, - parentInstance as any, - null, - undefined, - false, - ) + if (!isMounted || isHydrating) { + if (isHydrating) { + ;( + vdomHydrateNode || + (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!) + )( + currentHydrationNode!, + vnode, + parentInstance as any, + null, + null, + false, + ) + } else { + internals.mt( + vnode, + parentNode, + anchor, + parentInstance as any, + null, + undefined, + false, + ) + } onScopeDispose(unmount, true) isMounted = true } else { @@ -230,28 +256,43 @@ function renderVDOMSlot( isFunction(name) ? name() : name, props, ) - if ((vnode.children as any[]).length) { - if (fallbackNodes) { - remove(fallbackNodes, parentNode) - fallbackNodes = undefined - } - internals.p( - oldVNode, + if (isHydrating) { + locateHydrationNode(true) + ;( + vdomHydrateNode || + (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!) + )( + currentHydrationNode!, vnode, - parentNode, - anchor, parentComponent as any, + null, + null, + false, ) - oldVNode = vnode } 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 + } + 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) } - insert((fallbackNodes = fallback(props)), parentNode, anchor) + oldVNode = null } - oldVNode = null } }) isMounted = true diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts index d0a5223b2ff..03b7402ff92 100644 --- a/packages/server-renderer/__tests__/render.spec.ts +++ b/packages/server-renderer/__tests__/render.spec.ts @@ -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>`, ) }) }) diff --git a/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts b/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts index e8cfa75e77c..cbf13db254e 100644 --- a/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts +++ b/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts @@ -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>`, diff --git a/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts index 0679c82168b..181720c5b36 100644 --- a/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts +++ b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts @@ -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-->`) }) }) diff --git a/packages/server-renderer/__tests__/ssrScopeId.spec.ts b/packages/server-renderer/__tests__/ssrScopeId.spec.ts index 4ceb865fb50..c4135e498b7 100644 --- a/packages/server-renderer/__tests__/ssrScopeId.spec.ts +++ b/packages/server-renderer/__tests__/ssrScopeId.spec.ts @@ -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>`, ) }) }) diff --git a/packages/server-renderer/__tests__/ssrSlot.spec.ts b/packages/server-renderer/__tests__/ssrSlot.spec.ts index 02872274ab6..d17e34bc7c0 100644 --- a/packages/server-renderer/__tests__/ssrSlot.spec.ts +++ b/packages/server-renderer/__tests__/ssrSlot.spec.ts @@ -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 @@ -170,7 +172,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( @@ -187,7 +191,7 @@ describe('ssr: slot', () => { }), ), ).toBe( - `<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`, + `<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`, ) expect( @@ -201,6 +205,6 @@ describe('ssr: slot', () => { template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`, }), ), - ).toBe(`<button><!--[--><!--]--></button>`) + ).toBe(`<button><!--[--><!--]--></button><!--dynamic-component-->`) }) }) diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts index 19aa4ce63b7..0733c823390 100644 --- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts +++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts @@ -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-- diff --git a/packages/shared/src/domAnchors.ts b/packages/shared/src/domAnchors.ts new file mode 100644 index 00000000000..f807ee169db --- /dev/null +++ b/packages/shared/src/domAnchors.ts @@ -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) +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index dc56faf8cf5..674bcdf96cd 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -13,3 +13,4 @@ export * from './looseEqual' export * from './toDisplayString' export * from './typeUtils' export * from './subSequence' +export * from './domAnchors' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc4e5a164ba..c72eaa1ab19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ catalogs: specifier: ^7.26.10 version: 7.26.10 '@vitejs/plugin-vue': - specifier: ^5.2.3 - version: 5.2.3 + specifier: https://pkg.pr.new/@vitejs/plugin-vue@c156992 + version: 5.2.1 estree-walker: specifier: ^2.0.2 version: 2.0.2 @@ -25,8 +25,8 @@ catalogs: specifier: ^1.2.1 version: 1.2.1 vite: - specifier: ^5.4.14 - version: 5.4.14 + specifier: ^6.1.0 + version: 6.2.4 importers: @@ -69,14 +69,14 @@ importers: specifier: ^6.1.4 version: 6.1.4 '@vitest/coverage-v8': - specifier: ^3.0.2 - version: 3.0.2(vitest@3.0.2) + specifier: ^3.0.9 + version: 3.0.9(vitest@3.0.9) '@vitest/eslint-plugin': - specifier: ^1.1.25 - version: 1.1.25(@typescript-eslint/utils@8.20.0(eslint@9.18.0)(typescript@5.6.2))(eslint@9.18.0)(typescript@5.6.2)(vitest@3.0.2) + specifier: ^1.1.38 + version: 1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.6.2))(eslint@9.23.0)(typescript@5.6.2)(vitest@3.0.9) '@vitest/ui': specifier: ^3.0.2 - version: 3.0.4(vitest@3.0.2) + version: 3.1.1(vitest@3.0.9) '@vue/consolidate': specifier: 1.0.0 version: 1.0.0 @@ -178,16 +178,16 @@ importers: version: 8.27.0(eslint@9.23.0)(typescript@5.6.2) vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vitest: - specifier: ^3.0.2 - version: 3.0.2(@types/node@22.10.7)(@vitest/ui@3.0.4)(jsdom@26.0.0)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + specifier: ^3.0.9 + version: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) packages-private/benchmark: dependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@3.5.13(typescript@5.6.2)) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.2)) connect: specifier: ^3.7.0 version: 3.7.0 @@ -196,7 +196,7 @@ importers: version: 2.0.4 vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) devDependencies: '@types/connect': specifier: ^3.4.38 @@ -227,26 +227,26 @@ importers: dependencies: '@vueuse/core': specifier: ^11.1.0 - version: 11.1.0(vue@packages+vue) + version: 11.3.0(vue@packages+vue) vue: specifier: workspace:* version: link:../../packages/vue devDependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) '@vue/compiler-sfc': specifier: workspace:* version: link:../../packages/compiler-sfc vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vite-hyper-config: specifier: ^0.4.0 - version: 0.4.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1)) + version: 0.4.1(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)) vite-plugin-inspect: specifier: ^0.8.7 - version: 0.8.7(rollup@4.31.0)(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1)) + version: 0.8.7(rollup@4.37.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)) packages-private/sfc-playground: dependencies: @@ -265,10 +265,10 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) packages-private/template-explorer: dependencies: @@ -289,7 +289,7 @@ importers: version: 3.4.38 '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) connect: specifier: ^3.7.0 version: 3.7.0 @@ -298,7 +298,7 @@ importers: version: 2.0.4 vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vue: specifier: workspace:* version: link:../../packages/vue @@ -307,10 +307,10 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: 5.2.3(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) vite: specifier: 'catalog:' - version: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vue: specifier: workspace:* version: link:../../packages/vue @@ -1115,6 +1115,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@puppeteer/browsers@2.8.0': resolution: {integrity: sha512-yTwt2KWRmCQAfhvbCRjebaSX8pV1//I0Y3g+A7f/eS7gf0l4eRJoUCvcYdVtboeU4CTOZQuqYbZNS8aBYb8ROQ==} engines: {node: '>=18'} @@ -1183,100 +1186,53 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.34.2': - resolution: {integrity: sha512-6Fyg9yQbwJR+ykVdT9sid1oc2ewejS6h4wzQltmJfSW53N60G/ah9pngXGANdy9/aaE/TcUFpWosdm7JXS1WTQ==} - cpu: [arm] - os: [android] - '@rollup/rollup-android-arm-eabi@4.37.0': resolution: {integrity: sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.2': - resolution: {integrity: sha512-K5GfWe+vtQ3kyEbihrimM38UgX57UqHp+oME7X/EX9Im6suwZfa7Hsr8AtzbJvukTpwMGs+4s29YMSO3rwWtsw==} - cpu: [arm64] - os: [android] - '@rollup/rollup-android-arm64@4.37.0': resolution: {integrity: sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.2': - resolution: {integrity: sha512-PSN58XG/V/tzqDb9kDGutUruycgylMlUE59f40ny6QIRNsTEIZsrNQTJKUN2keMMSmlzgunMFqyaGLmly39sug==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-arm64@4.37.0': resolution: {integrity: sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.2': - resolution: {integrity: sha512-gQhK788rQJm9pzmXyfBB84VHViDERhAhzGafw+E5mUpnGKuxZGkMVDa3wgDFKT6ukLC5V7QTifzsUKdNVxp5qQ==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.37.0': resolution: {integrity: sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.2': - resolution: {integrity: sha512-eiaHgQwGPpxLC3+zTAcdKl4VsBl3r0AiJOd1Um/ArEzAjN/dbPK1nROHrVkdnoE6p7Svvn04w3f/jEZSTVHunA==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.37.0': resolution: {integrity: sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.34.2': - resolution: {integrity: sha512-lhdiwQ+jf8pewYOTG4bag0Qd68Jn1v2gO1i0mTuiD+Qkt5vNfHVK/jrT7uVvycV8ZchlzXp5HDVmhpzjC6mh0g==} - cpu: [x64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.37.0': resolution: {integrity: sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.2': - resolution: {integrity: sha512-lfqTpWjSvbgQP1vqGTXdv+/kxIznKXZlI109WkIFPbud41bjigjNmOAAKoazmRGx+k9e3rtIdbq2pQZPV1pMig==} - cpu: [arm] - os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.37.0': resolution: {integrity: sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==} cpu: [arm] os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.34.2': - resolution: {integrity: sha512-RGjqULqIurqqv+NJTyuPgdZhka8ImMLB32YwUle2BPTDqDoXNgwFjdjQC59FbSk08z0IqlRJjrJ0AvDQ5W5lpw==} - cpu: [arm] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.37.0': resolution: {integrity: sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==} cpu: [arm] os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.34.2': - resolution: {integrity: sha512-ZvkPiheyXtXlFqHpsdgscx+tZ7hoR59vOettvArinEspq5fxSDSgfF+L5wqqJ9R4t+n53nyn0sKxeXlik7AY9Q==} - cpu: [arm64] - os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.37.0': resolution: {integrity: sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==} cpu: [arm64] os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.34.2': - resolution: {integrity: sha512-UlFk+E46TZEoxD9ufLKDBzfSG7Ki03fo6hsNRRRHF+KuvNZ5vd1RRVQm8YZlGsjcJG8R252XFK0xNPay+4WV7w==} - cpu: [arm64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.37.0': resolution: {integrity: sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==} @@ -1284,96 +1240,58 @@ packages: os: [linux] libc: [musl] - '@rollup/rollup-linux-loongarch64-gnu@4.34.2': - resolution: {integrity: sha512-hJhfsD9ykx59jZuuoQgYT1GEcNNi3RCoEmbo5OGfG8RlHOiVS7iVNev9rhLKh7UBYq409f4uEw0cclTXx8nh8Q==} - cpu: [loong64] - os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.37.0': resolution: {integrity: sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==} cpu: [loong64] os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.34.2': - resolution: {integrity: sha512-g/O5IpgtrQqPegvqopvmdCF9vneLE7eqYfdPWW8yjPS8f63DNam3U4ARL1PNNB64XHZDHKpvO2Giftf43puB8Q==} - cpu: [ppc64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': resolution: {integrity: sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==} cpu: [ppc64] os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.34.2': - resolution: {integrity: sha512-bSQijDC96M6PuooOuXHpvXUYiIwsnDmqGU8+br2U7iPoykNi9JtMUpN7K6xml29e0evK0/g0D1qbAUzWZFHY5Q==} - cpu: [riscv64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.37.0': resolution: {integrity: sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.37.0': resolution: {integrity: sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==} cpu: [riscv64] os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.34.2': - resolution: {integrity: sha512-49TtdeVAsdRuiUHXPrFVucaP4SivazetGUVH8CIxVsNsaPHV4PFkpLmH9LeqU/R4Nbgky9lzX5Xe1NrzLyraVA==} - cpu: [s390x] - os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.37.0': resolution: {integrity: sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==} cpu: [s390x] os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.34.2': - resolution: {integrity: sha512-j+jFdfOycLIQ7FWKka9Zd3qvsIyugg5LeZuHF6kFlXo6MSOc6R1w37YUVy8VpAKd81LMWGi5g9J25P09M0SSIw==} - cpu: [x64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.37.0': resolution: {integrity: sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==} cpu: [x64] os: [linux] - - '@rollup/rollup-linux-x64-musl@4.34.2': - resolution: {integrity: sha512-aDPHyM/D2SpXfSNCVWCxyHmOqN9qb7SWkY1+vaXqMNMXslZYnwh9V/UCudl6psyG0v6Ukj7pXanIpfZwCOEMUg==} - cpu: [x64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.37.0': resolution: {integrity: sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==} cpu: [x64] os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.34.2': - resolution: {integrity: sha512-LQRkCyUBnAo7r8dbEdtNU08EKLCJMgAk2oP5H3R7BnUlKLqgR3dUjrLBVirmc1RK6U6qhtDw29Dimeer8d5hzQ==} - cpu: [arm64] - os: [win32] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.37.0': resolution: {integrity: sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.34.2': - resolution: {integrity: sha512-wt8OhpQUi6JuPFkm1wbVi1BByeag87LDFzeKSXzIdGcX4bMLqORTtKxLoCbV57BHYNSUSOKlSL4BYYUghainYA==} - cpu: [ia32] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.37.0': resolution: {integrity: sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.2': - resolution: {integrity: sha512-rUrqINax0TvrPBXrFKg0YbQx18NpPN3NNrgmaao9xRNbTwek7lOXObhx8tQy8gelmQ/gLaGy1WptpU2eKJZImg==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.37.0': resolution: {integrity: sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==} cpu: [x64] @@ -1464,6 +1382,9 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -1571,21 +1492,25 @@ packages: resolution: {integrity: sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/rspack-resolver-binding-linux-arm64-musl@1.2.2': resolution: {integrity: sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/rspack-resolver-binding-linux-x64-gnu@1.2.2': resolution: {integrity: sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/rspack-resolver-binding-linux-x64-musl@1.2.2': resolution: {integrity: sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/rspack-resolver-binding-wasm32-wasi@1.2.2': resolution: {integrity: sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==} @@ -1602,8 +1527,9 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-vue@5.2.3': - resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} + '@vitejs/plugin-vue@https://pkg.pr.new/@vitejs/plugin-vue@c156992': + resolution: {tarball: https://pkg.pr.new/@vitejs/plugin-vue@c156992} + version: 5.2.1 engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 || ^6.0.0 @@ -1648,6 +1574,9 @@ packages: '@vitest/pretty-format@3.0.9': resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} + '@vitest/pretty-format@3.1.1': + resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} + '@vitest/runner@3.0.9': resolution: {integrity: sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==} @@ -1657,11 +1586,16 @@ packages: '@vitest/spy@3.0.9': resolution: {integrity: sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==} + '@vitest/ui@3.1.1': + resolution: {integrity: sha512-2HpiRIYg3dlvAJBV9RtsVswFgUSJK4Sv7QhpxoP0eBGkYwzGIKP34PjaV00AULQi9Ovl6LGyZfsetxDWY5BQdQ==} + peerDependencies: + vitest: 3.1.1 + '@vitest/utils@3.0.9': resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==} - '@vitest/utils@3.0.4': - resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} + '@vitest/utils@3.1.1': + resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==} '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} @@ -1679,9 +1613,35 @@ packages: resolution: {integrity: sha512-oTyUE+QHIzLw2PpV14GD/c7EohDyP64xCniWTcqcEmTd699eFqTIwOmtDYjcO1j3QgdXoJEoWv1/cCdLrRoOfg==} engines: {node: '>= 0.12.0'} + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + '@vue/repl@4.5.1': resolution: {integrity: sha512-YYXvFue2GOrZ6EWnoA8yQVKzdCIn45+tpwJHzMof1uwrgyYAVY9ynxCsDYeAuWcpaAeylg/nybhFuqiFy2uvYA==} + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vueuse/core@11.3.0': + resolution: {integrity: sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==} + + '@vueuse/metadata@11.3.0': + resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==} + + '@vueuse/shared@11.3.0': + resolution: {integrity: sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==} + '@zeit/schemas@2.36.0': resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} @@ -1825,6 +1785,13 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -2396,8 +2363,8 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - flatted@3.3.2: - resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} @@ -3098,6 +3065,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3167,10 +3137,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.1: - resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.3: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} @@ -3351,11 +3317,6 @@ packages: peerDependencies: rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - rollup@4.34.2: - resolution: {integrity: sha512-sBDUoxZEaqLu9QeNalL8v3jw6WjPku4wfZGyTU7l7m1oC+rpRihXc/n/H+4148ZkGz5Xli8CHMns//fFGKvpIQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - rollup@4.37.0: resolution: {integrity: sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3367,6 +3328,10 @@ packages: rspack-resolver@1.2.2: resolution: {integrity: sha512-Fwc19jMBA3g+fxDJH2B4WxwZjE0VaaOL7OX/A4Wn5Zv7bOD/vyPZhzXfaO73Xc2GAlfi96g5fGUa378WbIGfFw==} + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3437,8 +3402,8 @@ packages: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} - sirv@3.0.0: - resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} slice-ansi@5.0.0: @@ -3596,8 +3561,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.10: - resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} tinypool@1.0.2: @@ -3699,6 +3664,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-utils@0.2.4: resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} engines: {node: '>=18.12.0'} @@ -3723,15 +3692,67 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-hyper-config@0.4.1: + resolution: {integrity: sha512-w9D4g0+5Km8XCgkBY/BZrXZAl8FF2q1UpDXT/Fsm6VLEU5tkkzDCko8fjLPOaSbvirUJgbY5OsD5wuuZ6581Fg==} + engines: {node: '>=18.0.0'} + peerDependencies: + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.0.9: resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-inspect@0.8.7: + resolution: {integrity: sha512-/XXou3MVc13A5O9/2Nd6xczjrUwt7ZyI9h8pTnUMkr5SshLcb0PJUOVq2V+XVkdeU4njsqAtmK87THZuO2coGA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + vite@5.4.14: resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.2.4: + resolution: {integrity: sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true peerDependencies: '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' @@ -4355,6 +4376,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.28': {} + '@puppeteer/browsers@2.8.0': dependencies: debug: 4.4.0 @@ -4422,120 +4445,63 @@ snapshots: optionalDependencies: rollup: 4.37.0 - '@rollup/rollup-android-arm-eabi@4.34.2': - optional: true - '@rollup/rollup-android-arm-eabi@4.37.0': optional: true - '@rollup/rollup-android-arm64@4.34.2': - optional: true - '@rollup/rollup-android-arm64@4.37.0': optional: true - '@rollup/rollup-darwin-arm64@4.34.2': - optional: true - '@rollup/rollup-darwin-arm64@4.37.0': optional: true - '@rollup/rollup-darwin-x64@4.34.2': - optional: true - '@rollup/rollup-darwin-x64@4.37.0': optional: true - '@rollup/rollup-freebsd-arm64@4.34.2': - optional: true - '@rollup/rollup-freebsd-arm64@4.37.0': optional: true - '@rollup/rollup-freebsd-x64@4.34.2': - optional: true - '@rollup/rollup-freebsd-x64@4.37.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.2': - optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.37.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.2': - optional: true - '@rollup/rollup-linux-arm-musleabihf@4.37.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.37.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.2': - optional: true - '@rollup/rollup-linux-arm64-musl@4.37.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.37.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-riscv64-gnu@4.37.0': optional: true '@rollup/rollup-linux-riscv64-musl@4.37.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-s390x-gnu@4.37.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-x64-gnu@4.37.0': optional: true - '@rollup/rollup-linux-x64-musl@4.34.2': - optional: true - '@rollup/rollup-linux-x64-musl@4.37.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.2': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.37.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.2': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.37.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.2': - optional: true - '@rollup/rollup-win32-x64-msvc@4.37.0': optional: true @@ -4598,6 +4564,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.13.13 + '@types/doctrine@0.0.9': {} '@types/estree@1.0.6': {} @@ -4741,12 +4711,17 @@ snapshots: '@unrs/rspack-resolver-binding-win32-x64-msvc@1.2.2': optional: true - '@vitejs/plugin-vue@5.2.3(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0))(vue@packages+vue)': + '@vitejs/plugin-vue@https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.2))': dependencies: - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + vue: 3.5.13(typescript@5.6.2) + + '@vitejs/plugin-vue@https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue)': + dependencies: + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vue: link:packages/vue - '@vitest/coverage-v8@3.0.9(vitest@3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0))': + '@vitest/coverage-v8@3.0.9(vitest@3.0.9)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -4760,17 +4735,17 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0) + vitest: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.6.2))(eslint@9.23.0)(typescript@5.6.2)(vitest@3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0))': + '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.6.2))(eslint@9.23.0)(typescript@5.6.2)(vitest@3.0.9)': dependencies: '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.6.2) eslint: 9.23.0 optionalDependencies: typescript: 5.6.2 - vitest: 3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0) + vitest: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) '@vitest/expect@3.0.9': dependencies: @@ -4779,18 +4754,22 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.9(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0))': + '@vitest/mocker@3.0.9(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.0.9 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) '@vitest/pretty-format@3.0.9': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.1.1': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@3.0.9': dependencies: '@vitest/utils': 3.0.9 @@ -4806,21 +4785,32 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@3.1.1(vitest@3.0.9)': + dependencies: + '@vitest/utils': 3.1.1 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.12 + tinyrainbow: 2.0.0 + vitest: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + '@vitest/utils@3.0.9': dependencies: '@vitest/pretty-format': 3.0.9 loupe: 3.1.3 tinyrainbow: 2.0.0 - '@vitest/utils@3.0.4': + '@vitest/utils@3.1.1': dependencies: - '@vitest/pretty-format': 3.0.4 - loupe: 3.1.2 + '@vitest/pretty-format': 3.1.1 + loupe: 3.1.3 tinyrainbow: 2.0.0 '@vue/compiler-core@3.5.13': dependencies: - '@babel/parser': 7.26.2 + '@babel/parser': 7.26.10 '@vue/shared': 3.5.13 entities: 4.5.0 estree-walker: 2.0.2 @@ -4833,14 +4823,14 @@ snapshots: '@vue/compiler-sfc@3.5.13': dependencies: - '@babel/parser': 7.26.2 + '@babel/parser': 7.26.10 '@vue/compiler-core': 3.5.13 '@vue/compiler-dom': 3.5.13 '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 estree-walker: 2.0.2 magic-string: 0.30.17 - postcss: 8.5.1 + postcss: 8.5.3 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.13': @@ -4850,8 +4840,51 @@ snapshots: '@vue/consolidate@1.0.0': {} + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + '@vue/repl@4.5.1': {} + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.6.2))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.6.2) + + '@vue/shared@3.5.13': {} + + '@vueuse/core@11.3.0(vue@packages+vue)': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 11.3.0 + '@vueuse/shared': 11.3.0(vue@packages+vue) + vue-demi: 0.14.10(vue@packages+vue) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@11.3.0': {} + + '@vueuse/shared@11.3.0(vue@packages+vue)': + dependencies: + vue-demi: 0.14.10(vue@packages+vue) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@zeit/schemas@2.36.0': {} accepts@1.3.8: @@ -4988,6 +5021,13 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-from@1.1.2: + optional: true + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + bytes@3.0.0: {} cac@6.7.14: {} @@ -5636,7 +5676,7 @@ snapshots: flatted@3.3.1: {} - flatted@3.3.2: {} + flatted@3.3.3: {} foreground-child@3.3.0: dependencies: @@ -6326,6 +6366,8 @@ snapshots: path-to-regexp@3.3.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} pathval@2.0.0: {} @@ -6387,12 +6429,6 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.1: - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.3: dependencies: nanoid: 3.3.8 @@ -6634,31 +6670,6 @@ snapshots: '@rollup/plugin-inject': 5.0.5(rollup@4.37.0) rollup: 4.37.0 - rollup@4.34.2: - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.2 - '@rollup/rollup-android-arm64': 4.34.2 - '@rollup/rollup-darwin-arm64': 4.34.2 - '@rollup/rollup-darwin-x64': 4.34.2 - '@rollup/rollup-freebsd-arm64': 4.34.2 - '@rollup/rollup-freebsd-x64': 4.34.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.2 - '@rollup/rollup-linux-arm-musleabihf': 4.34.2 - '@rollup/rollup-linux-arm64-gnu': 4.34.2 - '@rollup/rollup-linux-arm64-musl': 4.34.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.2 - '@rollup/rollup-linux-riscv64-gnu': 4.34.2 - '@rollup/rollup-linux-s390x-gnu': 4.34.2 - '@rollup/rollup-linux-x64-gnu': 4.34.2 - '@rollup/rollup-linux-x64-musl': 4.34.2 - '@rollup/rollup-win32-arm64-msvc': 4.34.2 - '@rollup/rollup-win32-ia32-msvc': 4.34.2 - '@rollup/rollup-win32-x64-msvc': 4.34.2 - fsevents: 2.3.3 - rollup@4.37.0: dependencies: '@types/estree': 1.0.6 @@ -6701,6 +6712,8 @@ snapshots: '@unrs/rspack-resolver-binding-win32-arm64-msvc': 1.2.2 '@unrs/rspack-resolver-binding-win32-x64-msvc': 1.2.2 + run-applescript@7.0.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6780,13 +6793,13 @@ snapshots: sirv@2.0.4: dependencies: - '@polka/url': 1.0.0-next.25 + '@polka/url': 1.0.0-next.28 mrmime: 2.0.0 totalist: 3.0.1 - sirv@3.0.0: + sirv@3.0.1: dependencies: - '@polka/url': 1.0.0-next.25 + '@polka/url': 1.0.0-next.28 mrmime: 2.0.0 totalist: 3.0.1 @@ -6953,7 +6966,7 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.10: + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 @@ -7027,6 +7040,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + unplugin-utils@0.2.4: dependencies: pathe: 2.0.3 @@ -7052,13 +7067,48 @@ snapshots: vary@1.1.2: {} - vite-node@3.0.9(@types/node@22.13.13)(sass@1.86.0): + vite-hyper-config@0.4.1(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)): + dependencies: + cac: 6.7.14 + picocolors: 1.1.1 + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + vite-node: 2.1.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@2.1.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 1.1.2 + vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@3.0.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7073,10 +7123,10 @@ snapshots: - tsx - yaml - vite-plugin-inspect@0.8.7(rollup@4.31.0)(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1)): + vite-plugin-inspect@0.8.7(rollup@4.37.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)): dependencies: '@antfu/utils': 0.7.10 - '@rollup/pluginutils': 5.1.0(rollup@4.31.0) + '@rollup/pluginutils': 5.1.0(rollup@4.37.0) debug: 4.4.0 error-stack-parser-es: 0.1.5 fs-extra: 11.2.0 @@ -7084,25 +7134,38 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 2.0.4 - vite: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) transitivePeerDependencies: - rollup - supports-color - vite@5.4.14(@types/node@22.13.13)(sass@1.86.0): + vite@5.4.14(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0): dependencies: - esbuild: 0.24.2 - postcss: 8.5.1 - rollup: 4.34.2 + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.37.0 optionalDependencies: '@types/node': 22.13.13 fsevents: 2.3.3 sass: 1.86.0 + terser: 5.33.0 + + vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0): + dependencies: + esbuild: 0.25.1 + postcss: 8.5.3 + rollup: 4.37.0 + optionalDependencies: + '@types/node': 22.13.13 + fsevents: 2.3.3 + sass: 1.86.0 + terser: 5.33.0 + yaml: 2.7.0 - vitest@3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0): + vitest@3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.9 - '@vitest/mocker': 3.0.9(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0)) + '@vitest/mocker': 3.0.9(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)) '@vitest/pretty-format': 3.0.9 '@vitest/runner': 3.0.9 '@vitest/snapshot': 3.0.9 @@ -7118,11 +7181,12 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) - vite-node: 3.0.9(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + vite-node: 3.0.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.13.13 + '@vitest/ui': 3.1.1(vitest@3.0.9) jsdom: 26.0.0 transitivePeerDependencies: - jiti