]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): fix hydration error on falsy v-if inside transition/keep-alive
authorEvan You <yyx990803@gmail.com>
Wed, 18 May 2022 01:28:18 +0000 (09:28 +0800)
committerEvan You <yyx990803@gmail.com>
Wed, 18 May 2022 01:28:18 +0000 (09:28 +0800)
fix #5352

packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts
packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts
packages/compiler-ssr/src/transforms/ssrTransformTeleport.ts
packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
packages/compiler-ssr/src/transforms/ssrVFor.ts
packages/compiler-ssr/src/transforms/ssrVIf.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts

index 672af193e92c4a4302dd4752ae1cd96d6b9cf092..2f279c090d20e75fbe233c543ea6df5f18b5b43e 100644 (file)
@@ -280,46 +280,92 @@ describe('ssr: components', () => {
       `)
     })
 
-    test('built-in fallthroughs', () => {
-      expect(compile(`<transition><div/></transition>`).code)
-        .toMatchInlineSnapshot(`
-        "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+    describe('built-in fallthroughs', () => {
+      test('transition', () => {
+        expect(compile(`<transition><div/></transition>`).code)
+          .toMatchInlineSnapshot(`
+                  "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+                  return function ssrRender(_ctx, _push, _parent, _attrs) {
+                    _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+                  }"
+              `)
+      })
 
-        return function ssrRender(_ctx, _push, _parent, _attrs) {
-          _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
-        }"
-      `)
+      test('keep-alive', () => {
+        expect(compile(`<keep-alive><foo/></keep-alive>`).code)
+          .toMatchInlineSnapshot(`
+                  "const { resolveComponent: _resolveComponent } = require(\\"vue\\")
+                  const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
 
-      // should inject attrs if root with coomments
-      expect(compile(`<!--root--><transition><div/></transition>`).code)
-        .toMatchInlineSnapshot(`
-        "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+                  return function ssrRender(_ctx, _push, _parent, _attrs) {
+                    const _component_foo = _resolveComponent(\\"foo\\")
 
-        return function ssrRender(_ctx, _push, _parent, _attrs) {
-          _push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
-        }"
-      `)
+                    _push(_ssrRenderComponent(_component_foo, _attrs, null, _parent))
+                  }"
+              `)
+      })
 
-      // should not inject attrs if not root
-      expect(compile(`<div/><transition><div/></transition>`).code)
-        .toMatchInlineSnapshot(`
-        "
-        return function ssrRender(_ctx, _push, _parent, _attrs) {
-          _push(\`<!--[--><div></div><div></div><!--]-->\`)
-        }"
-      `)
+      test('should inject attrs if root with coomments', () => {
+        expect(compile(`<!--root--><transition><div/></transition>`).code)
+          .toMatchInlineSnapshot(`
+                  "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
 
-      expect(compile(`<keep-alive><foo/></keep-alive>`).code)
-        .toMatchInlineSnapshot(`
-        "const { resolveComponent: _resolveComponent } = require(\\"vue\\")
-        const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
+                  return function ssrRender(_ctx, _push, _parent, _attrs) {
+                    _push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
+                  }"
+              `)
+      })
 
-        return function ssrRender(_ctx, _push, _parent, _attrs) {
-          const _component_foo = _resolveComponent(\\"foo\\")
+      test('should not inject attrs if not root', () => {
+        expect(compile(`<div/><transition><div/></transition>`).code)
+          .toMatchInlineSnapshot(`
+                  "
+                  return function ssrRender(_ctx, _push, _parent, _attrs) {
+                    _push(\`<!--[--><div></div><div></div><!--]-->\`)
+                  }"
+              `)
+      })
 
-          _push(_ssrRenderComponent(_component_foo, _attrs, null, _parent))
-        }"
-      `)
+      // #5352
+      test('should push marker string if is slot root', () => {
+        expect(
+          compile(`<foo><transition><div v-if="false"/></transition></foo>`)
+            .code
+        ).toMatchInlineSnapshot(`
+          "const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Transition: _Transition, createVNode: _createVNode } = require(\\"vue\\")
+          const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
+
+          return function ssrRender(_ctx, _push, _parent, _attrs) {
+            const _component_foo = _resolveComponent(\\"foo\\")
+
+            _push(_ssrRenderComponent(_component_foo, _attrs, {
+              default: _withCtx((_, _push, _parent, _scopeId) => {
+                if (_push) {
+                  _push(\`\`)
+                  if (false) {
+                    _push(\`<div\${_scopeId}></div>\`)
+                  } else {
+                    _push(\`<!---->\`)
+                  }
+                } else {
+                  return [
+                    _createVNode(_Transition, null, {
+                      default: _withCtx(() => [
+                        false
+                          ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
+                          : _createCommentVNode(\\"v-if\\", true)
+                      ]),
+                      _: 1 /* STABLE */
+                    })
+                  ]
+                }
+              }),
+              _: 1 /* STABLE */
+            }, _parent))
+          }"
+        `)
+      })
     })
 
     // transition-group should flatten and concat its children fragments into
index c30b90aa1313d5cc7d485a8758e41caa28dfba1b..a7f3ada15b4ed0f6f3a2c3ac1e589c4f8627a8eb 100644 (file)
@@ -51,7 +51,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
 
   const isFragment =
     ast.children.length > 1 && ast.children.some(c => !isText(c))
-  processChildren(ast.children, context, isFragment)
+  processChildren(ast, context, isFragment)
   ast.codegenNode = createBlockStatement(context.body)
 
   // Finalize helpers.
@@ -125,8 +125,12 @@ function createChildContext(
   )
 }
 
+interface Container {
+  children: TemplateChildNode[]
+}
+
 export function processChildren(
-  children: TemplateChildNode[],
+  parent: Container,
   context: SSRTransformContext,
   asFragment = false,
   disableNestedFragments = false
@@ -134,6 +138,7 @@ export function processChildren(
   if (asFragment) {
     context.pushStringPart(`<!--[-->`)
   }
+  const { children } = parent
   for (let i = 0; i < children.length; i++) {
     const child = children[i]
     switch (child.type) {
@@ -143,7 +148,7 @@ export function processChildren(
             ssrProcessElement(child, context)
             break
           case ElementTypes.COMPONENT:
-            ssrProcessComponent(child, context)
+            ssrProcessComponent(child, context, parent)
             break
           case ElementTypes.SLOT:
             ssrProcessSlotOutlet(child, context)
@@ -208,12 +213,12 @@ export function processChildren(
 }
 
 export function processChildrenAsStatement(
-  children: TemplateChildNode[],
+  parent: Container,
   parentContext: SSRTransformContext,
   asFragment = false,
   withSlotScopeId = parentContext.withSlotScopeId
 ): BlockStatement {
   const childContext = createChildContext(parentContext, withSlotScopeId)
-  processChildren(children, childContext, asFragment)
+  processChildren(parent, childContext, asFragment)
   return createBlockStatement(childContext.body)
 }
index dfb78136a9ddcb6f107af0f803933233af85425b..83d552103caf5c6de6eb83b0b93a58aa846c2845 100644 (file)
@@ -58,7 +58,10 @@ import { buildSSRProps } from './ssrTransformElement'
 // pass and complete them in the 2nd pass.
 const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
 
+const WIP_SLOT = Symbol()
+
 interface WIPSlotEntry {
+  type: typeof WIP_SLOT
   fn: FunctionExpression
   children: TemplateChildNode[]
   vnodeBranch: ReturnStatement
@@ -143,6 +146,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
         loc
       )
       wipEntries.push({
+        type: WIP_SLOT,
         fn,
         children,
         // also collect the corresponding vnode branch built earlier
@@ -182,7 +186,8 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
 
 export function ssrProcessComponent(
   node: ComponentNode,
-  context: SSRTransformContext
+  context: SSRTransformContext,
+  parent: { children: TemplateChildNode[] }
 ) {
   const component = componentTypeMap.get(node)!
   if (!node.ssrCodegenNode) {
@@ -196,13 +201,19 @@ export function ssrProcessComponent(
     } else {
       // real fall-through: Transition / KeepAlive
       // just render its children.
-      processChildren(node.children, context)
+      // #5352: if is at root level of a slot, push an empty string.
+      // this does not affect the final output, but avoids all-comment slot
+      // content of being treated as empty by ssrRenderSlot().
+      if ((parent as WIPSlotEntry).type === WIP_SLOT) {
+        context.pushStringPart(``)
+      }
+      processChildren(node, context)
     }
   } else {
     // finish up slot function expressions from the 1st pass.
     const wipEntries = wipMap.get(node) || []
     for (let i = 0; i < wipEntries.length; i++) {
-      const { fn, children, vnodeBranch } = wipEntries[i]
+      const { fn, vnodeBranch } = wipEntries[i]
       // For each slot, we generate two branches: one SSR-optimized branch and
       // one normal vnode-based branch. The branches are taken based on the
       // presence of the 2nd `_push` argument (which is only present if the slot
@@ -210,7 +221,7 @@ export function ssrProcessComponent(
       fn.body = createIfStatement(
         createSimpleExpression(`_push`, false),
         processChildrenAsStatement(
-          children,
+          wipEntries[i],
           context,
           false,
           true /* withSlotScopeId */
index a082d94eb575f3e308a0cb5923bf9706906258f0..2c42a83ba3708bf639f9ce78273abc907158550a 100644 (file)
@@ -428,7 +428,7 @@ export function ssrProcessElement(
   if (rawChildren) {
     context.pushStringPart(rawChildren)
   } else if (node.children.length) {
-    processChildren(node.children, context)
+    processChildren(node, context)
   }
 
   if (!isVoidTag(node.tag)) {
index 21c33831cccd228354a3cfc3d404fd0088dca996..3486f3551021667a14a9aadd42c3949296310769 100644 (file)
@@ -65,7 +65,7 @@ export function ssrProcessSlotOutlet(
   // has fallback content
   if (node.children.length) {
     const fallbackRenderFn = createFunctionExpression([])
-    fallbackRenderFn.body = processChildrenAsStatement(node.children, context)
+    fallbackRenderFn.body = processChildrenAsStatement(node, context)
     // _renderSlot(slots, name, props, fallback, ...)
     renderCall.arguments[3] = fallbackRenderFn
   }
index 33543326922af776118c2386b5fd5f53ae25e02a..207e9348eef5b9ed9fc5bea2ae564803197d58d4 100644 (file)
@@ -66,8 +66,8 @@ export function ssrProcessSuspense(
   }
   const { slotsExp, wipSlots } = wipEntry
   for (let i = 0; i < wipSlots.length; i++) {
-    const { fn, children } = wipSlots[i]
-    fn.body = processChildrenAsStatement(children, context)
+    const slot = wipSlots[i]
+    slot.fn.body = processChildrenAsStatement(slot, context)
   }
   // _push(ssrRenderSuspense(slots))
   context.pushStatement(
index d9cfdbb0274c05e0225524a3a538ab7731f26e48..f470ca711d4eaae248ea808adf79be7e3e3d5d67 100644 (file)
@@ -58,7 +58,7 @@ export function ssrProcessTeleport(
     false, // isSlot
     node.loc
   )
-  contentRenderFn.body = processChildrenAsStatement(node.children, context)
+  contentRenderFn.body = processChildrenAsStatement(node, context)
   context.pushStatement(
     createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
       `_push`,
index 80a26c98a4978c9f0a62c220d5bb725351b4a414..378c4f333d4e4cf2d3ba1318d3289d758f3958fe 100644 (file)
@@ -14,7 +14,7 @@ export function ssrProcessTransitionGroup(
       context.pushStringPart(`>`)
 
       processChildren(
-        node.children,
+        node,
         context,
         false,
         /**
@@ -31,11 +31,11 @@ export function ssrProcessTransitionGroup(
     } else {
       // static tag
       context.pushStringPart(`<${tag.value!.content}>`)
-      processChildren(node.children, context, false, true)
+      processChildren(node, context, false, true)
       context.pushStringPart(`</${tag.value!.content}>`)
     }
   } else {
     // fragment
-    processChildren(node.children, context, true, true)
+    processChildren(node, context, true, true)
   }
 }
index 583873b66ff83a2c8a204d60c953f550a9723bbe..0515993d4753778d9f29d526353d75028d6cbbb2 100644 (file)
@@ -33,7 +33,7 @@ export function ssrProcessFor(
     createForLoopParams(node.parseResult)
   )
   renderLoop.body = processChildrenAsStatement(
-    node.children,
+    node,
     context,
     needFragmentWrapper
   )
index 57f77eafd30ec8424128ff9d5ab170af6a5a6606..9de1d0e9a2d24087a582b5624bc554ba9f15635b 100644 (file)
@@ -72,5 +72,5 @@ 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(children, context, needFragmentWrapper)
+  return processChildrenAsStatement(branch, context, needFragmentWrapper)
 }
index e421578d7af5cd21f0d73287f47819c72a421cc5..9234e51732581b51cfc8a9da4656e37758d0a9f9 100644 (file)
@@ -84,5 +84,9 @@ export function ssrRenderSlotInner(
 
 const commentRE = /<!--.*?-->/g
 function isComment(item: SSRBufferItem) {
-  return typeof item === 'string' && !item.replace(commentRE, '').trim()
+  return (
+    typeof item === 'string' &&
+    commentRE.test(item) &&
+    !item.replace(commentRE, '').trim()
+  )
 }