Просмотр исходного кода

fix(compiler-vapor): hoist asset components used in slots (#14850)

edison 4 недель назад
Родитель
Сommit
16304cf6ff

+ 35 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap

@@ -104,6 +104,41 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
 }"
 `;
 
+exports[`compiler: element transform > component > hoist asset component also used in component slot 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, withVaporCtx as _withVaporCtx, createAssetComponent as _createAssetComponent } from 'vue';
+
+export function render(_ctx) {
+  const _component_Child = _resolveComponent("Child")
+  const n0 = _createComponentWithFallback(_component_Child)
+  const n2 = _createAssetComponent("Parent", null, {
+    "default": _withVaporCtx(() => {
+      const n1 = _createComponentWithFallback(_component_Child)
+      return n1
+    })
+  })
+  return [n0, n2]
+}"
+`;
+
+exports[`compiler: element transform > component > hoist asset component also used in conditional component slot 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, createIf as _createIf, withVaporCtx as _withVaporCtx, createAssetComponent as _createAssetComponent } from 'vue';
+
+export function render(_ctx) {
+  const _component_Child = _resolveComponent("Child")
+  const n0 = _createComponentWithFallback(_component_Child)
+  const n5 = _createAssetComponent("Parent", null, {
+    "default": _withVaporCtx(() => {
+      const n1 = _createIf(() => (_ctx.ok), () => {
+        const n3 = _createComponentWithFallback(_component_Child)
+        return n3
+      }, null, 1)
+      return n1
+    })
+  })
+  return [n0, n5]
+}"
+`;
+
 exports[`compiler: element transform > component > props merging: class 1`] = `
 "import { createAssetComponent as _createAssetComponent } from 'vue';
 

+ 3 - 2
packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap

@@ -141,10 +141,11 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: transform slot > forwarded slots > <slot w/ nested component> 1`] = `
-"import { createSlot as _createSlot, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, createAssetComponent as _createAssetComponent } from 'vue';
+"import { resolveComponent as _resolveComponent, createSlot as _createSlot, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback } from 'vue';
 
 export function render(_ctx) {
-  const n2 = _createAssetComponent("Comp", null, {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
     "default": _withVaporCtx(() => {
       const n1 = _createComponentWithFallback(_component_Comp, null, {
         "default": _withVaporCtx(() => {

+ 44 - 0
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts

@@ -9,6 +9,7 @@ import {
   transformVFor,
   transformVIf,
   transformVOn,
+  transformVSlot,
 } from '../../src'
 import {
   type BindingMetadata,
@@ -30,6 +31,21 @@ const compileWithElementTransform = makeCompile({
   },
 })
 
+const compileWithElementAndSlotTransform = makeCompile({
+  nodeTransforms: [
+    transformVIf,
+    transformVFor,
+    transformElement,
+    transformVSlot,
+    transformText,
+    transformChildren,
+  ],
+  directiveTransforms: {
+    bind: transformVBind,
+    on: transformVOn,
+  },
+})
+
 describe('compiler: element transform', () => {
   describe('component', () => {
     test('create single-use asset component with inline resolve', () => {
@@ -73,6 +89,34 @@ describe('compiler: element transform', () => {
       expect(helpers).not.toContain('createAssetComponent')
     })
 
+    test('hoist asset component also used in component slot', () => {
+      const { code, helpers } = compileWithElementAndSlotTransform(
+        `<Child /><Parent><Child /></Parent>`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).toContain(
+        'const _component_Child = _resolveComponent("Child")',
+      )
+      expect(code).toContain('_createComponentWithFallback(_component_Child)')
+      expect(code).not.toContain('_createAssetComponent("Child"')
+      expect(helpers).toContain('resolveComponent')
+      expect(helpers).toContain('createComponentWithFallback')
+    })
+
+    test('hoist asset component also used in conditional component slot', () => {
+      const { code, helpers } = compileWithElementAndSlotTransform(
+        `<Child /><Parent><template #default><Child v-if="ok" /></template></Parent>`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).toContain(
+        'const _component_Child = _resolveComponent("Child")',
+      )
+      expect(code).toContain('_createComponentWithFallback(_component_Child)')
+      expect(code).not.toContain('_createAssetComponent("Child"')
+      expect(helpers).toContain('resolveComponent')
+      expect(helpers).toContain('createComponentWithFallback')
+    })
+
     test('resolve implicitly self-referencing component', () => {
       const { code, helpers } = compileWithElementTransform(`<Example/>`, {
         filename: `/foo/bar/Example.vue?vue&type=template`,

+ 44 - 14
packages/compiler-vapor/src/generators/block.ts

@@ -1,5 +1,10 @@
-import type { BlockIRNode, CoreHelper, IRDynamicInfo } from '../ir'
-import { IRNodeTypes, type OperationNode, isBlockOperation } from '../ir'
+import type { BlockIRNode, CoreHelper, IRDynamicInfo, IRSlots } from '../ir'
+import {
+  IRNodeTypes,
+  IRSlotType,
+  type OperationNode,
+  isBlockOperation,
+} from '../ir'
 import {
   type CodeFragment,
   DELIMITERS_ARRAY,
@@ -181,6 +186,8 @@ function collectSingleUseAssetComponents(block: BlockIRNode): Set<string> {
   const usageMap = new Map<string, AssetComponentUsage>()
   const seenOperations = new Set<OperationNode>()
 
+  // createAssetComponent is only emitted from the root block. Nested blocks,
+  // including component slots, still need the hoisted resolveComponent binding.
   visitBlock(block, true)
 
   const names = new Set<string>()
@@ -223,19 +230,20 @@ function collectSingleUseAssetComponents(block: BlockIRNode): Set<string> {
     }
     seenOperations.add(operation)
 
-    if (
-      operation.type === IRNodeTypes.CREATE_COMPONENT_NODE &&
-      operation.asset
-    ) {
-      const usage = usageMap.get(operation.tag) || {
-        count: 0,
-        root: false,
-      }
-      usage.count++
-      if (rootCandidate) {
-        usage.root = true
+    if (operation.type === IRNodeTypes.CREATE_COMPONENT_NODE) {
+      if (operation.asset) {
+        const usage = usageMap.get(operation.tag) || {
+          count: 0,
+          root: false,
+        }
+        usage.count++
+        if (rootCandidate) {
+          usage.root = true
+        }
+        usageMap.set(operation.tag, usage)
       }
-      usageMap.set(operation.tag, usage)
+
+      visitSlots(operation.slots)
       return
     }
 
@@ -263,4 +271,26 @@ function collectSingleUseAssetComponents(block: BlockIRNode): Set<string> {
         break
     }
   }
+
+  function visitSlots(slots: IRSlots[]) {
+    for (const slot of slots) {
+      switch (slot.slotType) {
+        case IRSlotType.STATIC:
+          for (const name in slot.slots) {
+            visitBlock(slot.slots[name], false)
+          }
+          break
+        case IRSlotType.DYNAMIC:
+        case IRSlotType.LOOP:
+          visitBlock(slot.fn, false)
+          break
+        case IRSlotType.CONDITIONAL:
+          visitSlots([slot.positive])
+          if (slot.negative) {
+            visitSlots([slot.negative])
+          }
+          break
+      }
+    }
+  }
 }