Browse Source

feat(vapor): bridge VDOM Teleport/Suspense in dynamic component interop

daiwei 1 month ago
parent
commit
689818b88e

+ 63 - 0
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -1,6 +1,8 @@
 import {
   KeepAlive,
   type ShallowRef,
+  Suspense,
+  Teleport,
   createApp,
   createVNode,
   defineComponent,
@@ -1429,4 +1431,65 @@ describe('vdomInterop', () => {
       expect(html()).toBe('slot text<!--if-->')
     })
   })
+
+  describe('Teleport', () => {
+    test('mounts VDOM Teleport from createDynamicComponent', async () => {
+      const target = document.createElement('div')
+      target.id = 'interop-teleport-target'
+      document.body.appendChild(target)
+
+      try {
+        const VaporChild = defineVaporComponent({
+          setup() {
+            return createDynamicComponent(
+              () => Teleport,
+              { to: () => '#interop-teleport-target' },
+              {
+                default: () => template('<span>teleported</span>')(),
+              },
+              true,
+            )
+          },
+        })
+
+        define({
+          setup() {
+            return () => h(VaporChild as any)
+          },
+        }).render()
+
+        await nextTick()
+        expect(target.innerHTML).toContain('<span>teleported</span>')
+      } finally {
+        target.remove()
+      }
+    })
+  })
+
+  describe('Suspense', () => {
+    test('mounts VDOM Suspense from createDynamicComponent', async () => {
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createDynamicComponent(
+            () => Suspense,
+            null,
+            {
+              default: () => template('<span>resolved</span>')(),
+              fallback: () => template('<span>fallback</span>')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      await nextTick()
+      expect(html()).toContain('<span>resolved</span>')
+    })
+  })
 })

+ 39 - 2
packages/runtime-vapor/src/vdomInterop.ts

@@ -2,6 +2,7 @@ import {
   type App,
   type ComponentInternalInstance,
   type ConcreteComponent,
+  type FunctionalComponent,
   type HydrationRenderer,
   type KeepAliveContext,
   MoveType,
@@ -473,9 +474,11 @@ function createVDOMComponent(
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
 ): VaporFragment {
+  const useBridge = shouldUseRendererBridge(component)
+  const comp = useBridge ? ensureRendererBridge(component) : component
   const frag = new VaporFragment([])
   const vnode = (frag.vnode = createVNode(
-    component,
+    comp,
     rawProps && extend({}, new Proxy(rawProps, rawPropsProxyHandlers)),
   ))
 
@@ -485,7 +488,7 @@ function createVDOMComponent(
   }
 
   const wrapper = new VaporComponentInstance(
-    { props: component.props },
+    useBridge ? (comp as any) : { props: component.props },
     rawProps as RawProps,
     rawSlots as RawSlots,
     parentComponent ? parentComponent.appContext : undefined,
@@ -619,6 +622,40 @@ function createVDOMComponent(
   return frag
 }
 
+const rendererBridgeCache = new WeakMap<
+  ConcreteComponent,
+  FunctionalComponent
+>()
+
+/**
+ * Teleport/Suspense are renderer primitives (`__isTeleport` / `__isSuspense`),
+ * not regular components with their own render pipeline.
+ *
+ * We wrap them with a tiny functional bridge so they can pass through the
+ * interop component mount path while preserving built-in vnode semantics.
+ */
+function shouldUseRendererBridge(
+  component: ConcreteComponent & {
+    __isTeleport?: boolean
+    __isSuspense?: boolean
+  },
+): boolean {
+  return !!(component.__isTeleport || component.__isSuspense)
+}
+
+function ensureRendererBridge(
+  component: ConcreteComponent,
+): FunctionalComponent {
+  let bridge = rendererBridgeCache.get(component)
+  if (!bridge) {
+    rendererBridgeCache.set(
+      component,
+      (bridge = (props, { slots }) => createVNode(component, props, slots)),
+    )
+  }
+  return bridge
+}
+
 /**
  * Mount vdom slot in vapor
  */