]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-ssr): proper scope analysis for ssr vnode slot fallback (#7184)
authoredison <daiwei521@126.com>
Tue, 24 Oct 2023 17:01:29 +0000 (01:01 +0800)
committerGitHub <noreply@github.com>
Tue, 24 Oct 2023 17:01:29 +0000 (01:01 +0800)
close #7095

packages/compiler-core/src/transforms/vSlot.ts
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts

index c4416dd45f77d188d67660635c5ae8427dba876d..ffa90ea1171f93736a1ca5eb6eb1f676d1cbff5f 100644 (file)
@@ -100,11 +100,12 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
 
 export type SlotFnBuilder = (
   slotProps: ExpressionNode | undefined,
+  vForExp: ExpressionNode | undefined,
   slotChildren: TemplateChildNode[],
   loc: SourceLocation
 ) => FunctionExpression
 
-const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
+const buildClientSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) =>
   createFunctionExpression(
     props,
     children,
@@ -149,7 +150,7 @@ export function buildSlots(
     slotsProperties.push(
       createObjectProperty(
         arg || createSimpleExpression('default', true),
-        buildSlotFn(exp, children, loc)
+        buildSlotFn(exp, undefined, children, loc)
       )
     )
   }
@@ -201,11 +202,17 @@ export function buildSlots(
       hasDynamicSlots = true
     }
 
-    const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
+    const vFor = findDir(slotElement, 'for')
+    const slotFunction = buildSlotFn(
+      slotProps,
+      vFor?.exp,
+      slotChildren,
+      slotLoc
+    )
+
     // check if this slot is conditional (v-if/v-for)
     let vIf: DirectiveNode | undefined
     let vElse: DirectiveNode | undefined
-    let vFor: DirectiveNode | undefined
     if ((vIf = findDir(slotElement, 'if'))) {
       hasDynamicSlots = true
       dynamicSlots.push(
@@ -257,7 +264,7 @@ export function buildSlots(
           createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
         )
       }
-    } else if ((vFor = findDir(slotElement, 'for'))) {
+    } else if (vFor) {
       hasDynamicSlots = true
       const parseResult =
         vFor.parseResult ||
@@ -306,7 +313,7 @@ export function buildSlots(
       props: ExpressionNode | undefined,
       children: TemplateChildNode[]
     ) => {
-      const fn = buildSlotFn(props, children, loc)
+      const fn = buildSlotFn(props, undefined, children, loc)
       if (__COMPAT__ && context.compatConfig) {
         fn.isNonScopedSlot = true
       }
index 9391c01e37ee511ab3942892ba7cc7fb01d51f56..a8ea08a53498b544292d0e02cb5155e92bdfb7f3 100644 (file)
@@ -181,11 +181,14 @@ describe('ssr: components', () => {
     })
 
     test('v-for slot', () => {
-      expect(
-        compile(`<foo>
-        <template v-for="key in names" v-slot:[key]="{ msg }">{{ msg + key + bar }}</template>
-      </foo>`).code
-      ).toMatchInlineSnapshot(`
+      const { code } = compile(`<foo>
+      <template v-for="(key, index) in names" v-slot:[key]="{ msg }">{{ msg + key + index + bar }}</template>
+    </foo>`)
+      expect(code).not.toMatch(`_ctx.msg`)
+      expect(code).not.toMatch(`_ctx.key`)
+      expect(code).not.toMatch(`_ctx.index`)
+      expect(code).toMatch(`_ctx.bar`)
+      expect(code).toMatchInlineSnapshot(`
         "const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\")
         const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\")
 
@@ -193,15 +196,15 @@ describe('ssr: components', () => {
           const _component_foo = _resolveComponent(\\"foo\\")
 
           _push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [
-            _renderList(_ctx.names, (key) => {
+            _renderList(_ctx.names, (key, index) => {
               return {
                 name: key,
                 fn: _withCtx(({ msg }, _push, _parent, _scopeId) => {
                   if (_push) {
-                    _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
+                    _push(\`\${_ssrInterpolate(msg + key + index + _ctx.bar)}\`)
                   } else {
                     return [
-                      _createTextVNode(_toDisplayString(msg + _ctx.key + _ctx.bar), 1 /* TEXT */)
+                      _createTextVNode(_toDisplayString(msg + key + index + _ctx.bar), 1 /* TEXT */)
                     ]
                   }
                 })
index 93cae7db3c2628ae99e11d8a183246156580aee8..7a12cb29009d00a2bf108fb696b5f00873337bd8 100644 (file)
@@ -125,8 +125,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
     // fallback in case the child is render-fn based). Store them in an array
     // for later use.
     if (clonedNode.children.length) {
-      buildSlots(clonedNode, context, (props, children) => {
-        vnodeBranches.push(createVNodeSlotBranch(props, children, context))
+      buildSlots(clonedNode, context, (props, vFor, children) => {
+        vnodeBranches.push(
+          createVNodeSlotBranch(props, vFor, children, context)
+        )
         return createFunctionExpression(undefined)
       })
     }
@@ -150,7 +152,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
     const wipEntries: WIPSlotEntry[] = []
     wipMap.set(node, wipEntries)
 
-    const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
+    const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => {
       const param0 = (props && stringifyExpression(props)) || `_`
       const fn = createFunctionExpression(
         [param0, `_push`, `_parent`, `_scopeId`],
@@ -277,6 +279,7 @@ const vnodeDirectiveTransforms = {
 
 function createVNodeSlotBranch(
   props: ExpressionNode | undefined,
+  vForExp: ExpressionNode | undefined,
   children: TemplateChildNode[],
   parentContext: TransformContext
 ): ReturnStatement {
@@ -303,8 +306,8 @@ function createVNodeSlotBranch(
     tag: 'template',
     tagType: ElementTypes.TEMPLATE,
     isSelfClosing: false,
-    // important: provide v-slot="props" on the wrapper for proper
-    // scope analysis
+    // important: provide v-slot="props" and v-for="exp" on the wrapper for
+    // proper scope analysis
     props: [
       {
         type: NodeTypes.DIRECTIVE,
@@ -313,6 +316,14 @@ function createVNodeSlotBranch(
         arg: undefined,
         modifiers: [],
         loc: locStub
+      },
+      {
+        type: NodeTypes.DIRECTIVE,
+        name: 'for',
+        exp: vForExp,
+        arg: undefined,
+        modifiers: [],
+        loc: locStub
       }
     ],
     children,
index 207e9348eef5b9ed9fc5bea2ae564803197d58d4..e7efbe1fb73e05dab1834794c4973ba83d79cdd8 100644 (file)
@@ -36,20 +36,24 @@ export function ssrTransformSuspense(
         wipSlots: []
       }
       wipMap.set(node, wipEntry)
-      wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => {
-        const fn = createFunctionExpression(
-          [],
-          undefined, // no return, assign body later
-          true, // newline
-          false, // suspense slots are not treated as normal slots
-          loc
-        )
-        wipEntry.wipSlots.push({
-          fn,
-          children
-        })
-        return fn
-      }).slots
+      wipEntry.slotsExp = buildSlots(
+        node,
+        context,
+        (_props, _vForExp, children, loc) => {
+          const fn = createFunctionExpression(
+            [],
+            undefined, // no return, assign body later
+            true, // newline
+            false, // suspense slots are not treated as normal slots
+            loc
+          )
+          wipEntry.wipSlots.push({
+            fn,
+            children
+          })
+          return fn
+        }
+      ).slots
     }
   }
 }