]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix: special handing inject insertion anchors of if node in ssr vnode-based slot
authordaiwei <daiwei521@126.com>
Tue, 12 Aug 2025 02:49:46 +0000 (10:49 +0800)
committerdaiwei <daiwei521@126.com>
Tue, 12 Aug 2025 02:49:46 +0000 (10:49 +0800)
packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

index 9fd5fed6acb6cfb7bdebb79ccd004418e6632e46..d29be57cfe42c1b8adc6fd1f401a9b9d6d179855 100644 (file)
@@ -1,8 +1,3 @@
-// import {
-//   BLOCK_APPEND_ANCHOR_LABEL,
-//   BLOCK_INSERTION_ANCHOR_LABEL,
-//   BLOCK_PREPEND_ANCHOR_LABEL,
-// } from '@vue/shared'
 import { getCompiledString } from './utils'
 
 describe('insertion anchors', () => {
@@ -109,29 +104,46 @@ describe('insertion anchors', () => {
       `)
     })
 
-    test('prepend anchor with v-if', () => {
+    test('prepend anchor with v-if/else-if/else', () => {
       expect(
-        getCompiledString('<div><span v-if="foo"/><span/></div>', {
-          vapor: true,
-        }),
+        getCompiledString(
+          `<div>
+            <span v-if="foo"/>
+            <span v-else-if="bar"/>
+            <span v-else/>
+            <span/>
+          </div>`,
+          {
+            vapor: true,
+          },
+        ),
       ).toMatchInlineSnapshot(`
         "\`<div><!--[p-->\`)
           if (_ctx.foo) {
             _push(\`<span></span>\`)
             _push(\`<!--if-->\`)
+          } else if (_ctx.bar) {
+            _push(\`<span></span>\`)
+            _push(\`<!--if-->\`)
           } else {
-            _push(\`<!---->\`)
+            _push(\`<span></span>\`)
+            _push(\`<!--if-->\`)
           }
           _push(\`<!--p]--><span></span></div>\`"
       `)
     })
 
-    test('prepend anchor with v-if in ssr slot vnode fallback', () => {
+    test('prepend anchor with v-if/else-if/else in ssr slot vnode fallback', () => {
       expect(
         getCompiledString(
           `<component :is="'div'">
-            <div><span v-if="foo"/><span/></div>
-          </component>`,
+              <div>
+                <span v-if="foo"/>
+                <span v-else-if="bar"/>
+                <span v-else/>
+                <span/>
+              </div>
+            </component>`,
           { vapor: true },
         ),
       ).toMatchInlineSnapshot(`
@@ -143,8 +155,12 @@ describe('insertion anchors', () => {
                 if (_ctx.foo) {
                   _push(\`<span\${_scopeId}></span>\`)
                   _push(\`<!--if-->\`)
+                } else if (_ctx.bar) {
+                  _push(\`<span\${_scopeId}></span>\`)
+                  _push(\`<!--if-->\`)
                 } else {
-                  _push(\`<!---->\`)
+                  _push(\`<span\${_scopeId}></span>\`)
+                  _push(\`<!--if-->\`)
                 }
                 _push(\`<!--p]--><span\${_scopeId}></span></div>\`)
               } else {
@@ -153,7 +169,170 @@ describe('insertion anchors', () => {
                     _createCommentVNode("[p"),
                     (_ctx.foo)
                       ? (_openBlock(), _createBlock("span", { key: 0 }))
-                      : _createCommentVNode("v-if", true),
+                      : (_ctx.bar)
+                        ? (_openBlock(), _createBlock("span", { key: 1 }))
+                        : (_openBlock(), _createBlock("span", { key: 2 })),
+                    _createCommentVNode("p]"),
+                    _createVNode("span")
+                  ])
+                ]
+              }
+            }),
+            _: 1 /* STABLE */
+          }), _parent)
+          _push(\`<!--dynamic-component--><!--a]-->\`"
+      `)
+    })
+
+    test('prepend anchor with nested v-if', () => {
+      expect(
+        getCompiledString(
+          `<div>
+            <span v-if="foo">
+              <span v-if="foo1" />
+              <span />
+            </span>
+            <span v-else-if="bar">
+              <span v-if="bar1" />
+              <span />
+            </span>
+            <span v-else>
+              <span v-if="bar2" />
+              <span />
+            </span>
+            <span />
+          </div>`,
+          {
+            vapor: true,
+          },
+        ),
+      ).toMatchInlineSnapshot(`
+        "\`<div><!--[p-->\`)
+          if (_ctx.foo) {
+            _push(\`<span><!--[p-->\`)
+            if (_ctx.foo1) {
+              _push(\`<span></span>\`)
+              _push(\`<!--if-->\`)
+            } else {
+              _push(\`<!---->\`)
+            }
+            _push(\`<!--p]--><span></span></span>\`)
+            _push(\`<!--if-->\`)
+          } else if (_ctx.bar) {
+            _push(\`<span><!--[p-->\`)
+            if (_ctx.bar1) {
+              _push(\`<span></span>\`)
+              _push(\`<!--if-->\`)
+            } else {
+              _push(\`<!---->\`)
+            }
+            _push(\`<!--p]--><span></span></span>\`)
+            _push(\`<!--if-->\`)
+          } else {
+            _push(\`<span><!--[p-->\`)
+            if (_ctx.bar2) {
+              _push(\`<span></span>\`)
+              _push(\`<!--if-->\`)
+            } else {
+              _push(\`<!---->\`)
+            }
+            _push(\`<!--p]--><span></span></span>\`)
+            _push(\`<!--if-->\`)
+          }
+          _push(\`<!--p]--><span></span></div>\`"
+      `)
+    })
+
+    test('prepend anchor with nested v-if in ssr slot vnode fallback', () => {
+      expect(
+        getCompiledString(
+          `<component :is="'div'">
+              <div>
+              <span v-if="foo">
+                <span v-if="foo1" />
+                <span />
+              </span>
+              <span v-else-if="bar">
+                <span v-if="bar1" />
+                <span />
+              </span>
+              <span v-else>
+                <span v-if="bar2" />
+                <span />
+              </span>
+              <span />
+            </div>
+          </component>`,
+          { vapor: true },
+        ),
+      ).toMatchInlineSnapshot(`
+        "\`<!--[a-->\`)
+          _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
+            default: _withCtx((_, _push, _parent, _scopeId) => {
+              if (_push) {
+                _push(\`<div\${_scopeId}><!--[p-->\`)
+                if (_ctx.foo) {
+                  _push(\`<span\${_scopeId}><!--[p-->\`)
+                  if (_ctx.foo1) {
+                    _push(\`<span\${_scopeId}></span>\`)
+                    _push(\`<!--if-->\`)
+                  } else {
+                    _push(\`<!---->\`)
+                  }
+                  _push(\`<!--p]--><span\${_scopeId}></span></span>\`)
+                  _push(\`<!--if-->\`)
+                } else if (_ctx.bar) {
+                  _push(\`<span\${_scopeId}><!--[p-->\`)
+                  if (_ctx.bar1) {
+                    _push(\`<span\${_scopeId}></span>\`)
+                    _push(\`<!--if-->\`)
+                  } else {
+                    _push(\`<!---->\`)
+                  }
+                  _push(\`<!--p]--><span\${_scopeId}></span></span>\`)
+                  _push(\`<!--if-->\`)
+                } else {
+                  _push(\`<span\${_scopeId}><!--[p-->\`)
+                  if (_ctx.bar2) {
+                    _push(\`<span\${_scopeId}></span>\`)
+                    _push(\`<!--if-->\`)
+                  } else {
+                    _push(\`<!---->\`)
+                  }
+                  _push(\`<!--p]--><span\${_scopeId}></span></span>\`)
+                  _push(\`<!--if-->\`)
+                }
+                _push(\`<!--p]--><span\${_scopeId}></span></div>\`)
+              } else {
+                return [
+                  _createVNode("div", null, [
+                    _createCommentVNode("[p"),
+                    (_ctx.foo)
+                      ? (_openBlock(), _createBlock("span", { key: 0 }, [
+                          _createCommentVNode("[p"),
+                          (_ctx.foo1)
+                            ? (_openBlock(), _createBlock("span", { key: 0 }))
+                            : _createCommentVNode("v-if", true),
+                          _createCommentVNode("p]"),
+                          _createVNode("span")
+                        ]))
+                      : (_ctx.bar)
+                        ? (_openBlock(), _createBlock("span", { key: 1 }, [
+                            _createCommentVNode("[p"),
+                            (_ctx.bar1)
+                              ? (_openBlock(), _createBlock("span", { key: 0 }))
+                              : _createCommentVNode("v-if", true),
+                            _createCommentVNode("p]"),
+                            _createVNode("span")
+                          ]))
+                        : (_openBlock(), _createBlock("span", { key: 2 }, [
+                            _createCommentVNode("[p"),
+                            (_ctx.bar2)
+                              ? (_openBlock(), _createBlock("span", { key: 0 }))
+                              : _createCommentVNode("v-if", true),
+                            _createCommentVNode("p]"),
+                            _createVNode("span")
+                          ])),
                     _createCommentVNode("p]"),
                     _createVNode("span")
                   ])
index 30ec8050ed80280ef6452771e16490d9968c823e..fc32850e7e25c36cebb89a5c52d2c4ff19ad7c92 100644 (file)
@@ -1,8 +1,11 @@
 import {
+  type AttributeNode,
   type BlockStatement,
   type CallExpression,
   type CompilerError,
   type CompilerOptions,
+  type DirectiveNode,
+  type ElementNode,
   ElementTypes,
   type IfStatement,
   type JSChildNode,
@@ -169,12 +172,11 @@ export function processChildren(
     context.pushStringPart(`<!--[-->`)
   }
 
-  const { children, type, tagType } = parent as PlainElementNode
+  const { children } = parent
 
   if (
     context.options.vapor &&
-    type === NodeTypes.ELEMENT &&
-    tagType === ElementTypes.ELEMENT
+    isElementWithChildren(parent as PlainElementNode)
   ) {
     processBlockNodeAnchor(children)
   }
@@ -308,6 +310,12 @@ export function processBlockNodeAnchor(children: TemplateChildNode[]): void {
   }
 }
 
+export function hasBlockDir(
+  props: Array<AttributeNode | DirectiveNode>,
+): boolean {
+  return props.some(p => p.name === 'if' || p.name === 'for')
+}
+
 function isBlockNode(child: TemplateChildNode): boolean {
   return (
     child.type === NodeTypes.IF ||
@@ -315,13 +323,7 @@ function isBlockNode(child: TemplateChildNode): boolean {
     (child.type === NodeTypes.ELEMENT &&
       (child.tagType === ElementTypes.COMPONENT ||
         child.tagType === ElementTypes.SLOT ||
-        child.props.some(
-          p =>
-            p.name === 'if' ||
-            p.name === 'else-if' ||
-            p.name === 'else' ||
-            p.name === 'for',
-        )))
+        hasBlockDir(child.props)))
   )
 }
 
@@ -332,12 +334,16 @@ function isStaticNode(child: TemplateChildNode): boolean {
     child.type === NodeTypes.COMMENT ||
     (child.type === NodeTypes.ELEMENT &&
       child.tagType === ElementTypes.ELEMENT &&
-      !child.props.some(
-        p =>
-          p.name === 'if' ||
-          p.name === 'else-if' ||
-          p.name === 'else' ||
-          p.name === 'for',
-      ))
+      !hasBlockDir(child.props))
+  )
+}
+
+export function isElementWithChildren(
+  node: TemplateChildNode,
+): node is ElementNode {
+  return (
+    node.type === NodeTypes.ELEMENT &&
+    node.tagType === ElementTypes.ELEMENT &&
+    node.children.length > 0
   )
 }
index 5af6041480de8b57e8d96a9b00c1e5c6bac61136..db63235caf5834aa00b35caf7ca7268c7327a37e 100644 (file)
@@ -8,7 +8,9 @@ import {
   type DirectiveNode,
   ElementTypes,
   type ExpressionNode,
+  type ForNode,
   type FunctionExpression,
+  type IfNode,
   type JSChildNode,
   Namespaces,
   type NodeTransform,
@@ -43,6 +45,7 @@ import {
 import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
 import {
   type SSRTransformContext,
+  isElementWithChildren,
   processBlockNodeAnchor,
   processChildren,
   processChildrenAsStatement,
@@ -333,6 +336,11 @@ function createVNodeSlotBranch(
   if (vFor) {
     wrapperProps.push(extend({}, vFor))
   }
+
+  if (parentContext.vapor) {
+    children = injectVaporInsertionAnchors(children)
+  }
+
   const wrapperNode: TemplateNode = {
     type: NodeTypes.ELEMENT,
     ns: Namespaces.HTML,
@@ -344,10 +352,6 @@ function createVNodeSlotBranch(
     codegenNode: undefined,
   }
 
-  if (parentContext.vapor) {
-    injectVaporInsertionAnchors(children)
-  }
-
   subTransform(wrapperNode, subOptions, parentContext)
   return createReturnStatement(children)
 }
@@ -389,69 +393,131 @@ function subTransform(
   // - hoists are not enabled for the client branch here
 }
 
-function injectVaporInsertionAnchors(children: TemplateChildNode[]) {
+function injectVaporInsertionAnchors(
+  children: TemplateChildNode[],
+): TemplateChildNode[] {
   processBlockNodeAnchor(children)
+  const newChildren: TemplateChildNode[] = new Array(children.length * 3)
+  let newIndex = 0
+
   for (let i = 0; i < children.length; i++) {
     const child = children[i]
-    switch (child.type) {
-      case NodeTypes.ELEMENT:
-        switch (child.tagType) {
-          case ElementTypes.COMPONENT:
-          case ElementTypes.SLOT:
-            if (child.anchor) {
-              children.splice(i, 0, {
-                type: NodeTypes.COMMENT,
-                content: `[${child.anchor}`,
-                loc: locStub,
-              })
-              children.splice(i + 2, 0, {
-                type: NodeTypes.COMMENT,
-                content: `${child.anchor}]`,
-                loc: locStub,
-              })
-              i += 2
-            }
-            break
-          default: {
-            const { props } = child
-            if (
-              props.some(
-                p =>
-                  p.name === 'if' ||
-                  p.name === 'else-if' ||
-                  p.name === 'else' ||
-                  p.name === 'for',
-              )
-            ) {
-              // @ts-expect-error
-              if (child.anchor) {
-                children.splice(i, 0, {
-                  type: NodeTypes.COMMENT,
-                  // @ts-expect-error
-                  content: `[${child.anchor}`,
-                  loc: locStub,
-                })
-                children.splice(i + 2, 0, {
-                  type: NodeTypes.COMMENT,
-                  // @ts-expect-error
-                  content: `${child.anchor}]`,
-                  loc: locStub,
-                })
+
+    if (child.type !== NodeTypes.ELEMENT) {
+      newChildren[newIndex++] = child
+      continue
+    }
+
+    const { tagType, props } = child
+    let anchor: string | undefined
+
+    if (tagType === ElementTypes.COMPONENT || tagType === ElementTypes.SLOT) {
+      anchor = child.anchor
+    } else if (tagType === ElementTypes.ELEMENT) {
+      let hasIf = false
+      let hasFor = false
+
+      for (const prop of props) {
+        if (prop.name === 'if') {
+          hasIf = true
+          break
+        } else if (prop.name === 'for') {
+          hasFor = true
+        }
+      }
+
+      if (hasIf) {
+        anchor = (child as any as IfNode).anchor
+        if (anchor) {
+          // find sibling else-if/else branches
+          // inject anchor after else-if/else branch if founded
+          // otherwise inject after if node
+          const lastBranchIndex = findLastIfBranchIndex(children, i)
+          if (lastBranchIndex > i) {
+            // inject anchor before if node
+            newChildren[newIndex++] = createAnchorComment(`[${anchor}`)
+
+            // copy branch nodes
+            for (let j = i; j <= lastBranchIndex; j++) {
+              const node = children[j]
+              newChildren[newIndex++] = node
+
+              if (isElementWithChildren(node)) {
+                node.children = injectVaporInsertionAnchors(node.children)
               }
-              i += 2
-              break
             }
+
+            // inject anchor after branch nodes
+            newChildren[newIndex++] = createAnchorComment(`${anchor}]`)
+
+            i = lastBranchIndex
+            continue
           }
         }
+      } else if (hasFor) {
+        anchor = (child as any as ForNode).anchor
+      }
     }
 
-    if (
-      child.type === NodeTypes.ELEMENT &&
-      child.tagType === ElementTypes.ELEMENT
-    ) {
-      injectVaporInsertionAnchors(child.children)
+    // inject anchor before and after the child
+    if (anchor) newChildren[newIndex++] = createAnchorComment(`[${anchor}`)
+    newChildren[newIndex++] = child
+    if (anchor) newChildren[newIndex++] = createAnchorComment(`${anchor}]`)
+
+    if (isElementWithChildren(child)) {
+      child.children = injectVaporInsertionAnchors(child.children)
+    }
+  }
+
+  newChildren.length = newIndex
+  return newChildren
+}
+
+function createAnchorComment(content: string): TemplateChildNode {
+  return {
+    type: NodeTypes.COMMENT,
+    content,
+    loc: locStub,
+  }
+}
+
+function findLastIfBranchIndex(
+  children: TemplateChildNode[],
+  ifIndex: number,
+): number {
+  let lastIndex = ifIndex
+
+  for (let i = ifIndex + 1; i < children.length; i++) {
+    const sibling = children[i]
+
+    if (sibling.type !== NodeTypes.ELEMENT) {
+      continue
+    }
+
+    let hasElseIf = false
+    let hasElse = false
+
+    for (const prop of sibling.props) {
+      if (prop.name === 'else-if') {
+        hasElseIf = true
+        break
+      } else if (prop.name === 'else') {
+        hasElse = true
+        break
+      }
+    }
+
+    if (hasElseIf || hasElse) {
+      lastIndex = i
+      if (hasElse) {
+        break
+      }
+    } else {
+      break
     }
   }
+
+  return lastIndex
 }
 
 function clone(v: any): any {