]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): vdom in vapor interop
authorEvan You <evan@vuejs.org>
Tue, 4 Feb 2025 13:38:09 +0000 (21:38 +0800)
committerEvan You <evan@vuejs.org>
Tue, 4 Feb 2025 13:38:09 +0000 (21:38 +0800)
13 files changed:
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vaporInterop.ts [new file with mode: 0644]
packages/runtime-core/src/vnode.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/vdomInterop.ts

index a81c6b433b3c45b7f01179ad51e10e20238650b5..80532e9ed271bf57db20e55b77932cfe630b203b 100644 (file)
@@ -1,6 +1,5 @@
 import {
   type Component,
-  type ComponentInternalInstance,
   type ConcreteComponent,
   type Data,
   type GenericComponent,
@@ -17,7 +16,11 @@ import type {
   ComponentPublicInstance,
 } from './componentPublicInstance'
 import { type Directive, validateDirectiveName } from './directives'
-import type { ElementNamespace, RootRenderFunction } from './renderer'
+import type {
+  ElementNamespace,
+  RootRenderFunction,
+  UnmountComponentFn,
+} from './renderer'
 import type { InjectionKey } from './apiInject'
 import { warn } from './warning'
 import type { VNode } from './vnode'
@@ -29,6 +32,7 @@ import type { NormalizedPropsOptions } from './componentProps'
 import type { ObjectEmitsOptions } from './componentEmits'
 import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
 import type { DefineComponent } from './apiDefineComponent'
+import type { VaporInteropInterface } from './vaporInterop'
 
 export interface App<HostElement = any> {
   version: string
@@ -172,26 +176,6 @@ export interface AppConfig extends GenericAppConfig {
    * @deprecated use config.compilerOptions.isCustomElement
    */
   isCustomElement?: (tag: string) => boolean
-
-  /**
-   * @internal
-   */
-  vapor?: VaporInVDOMInterface
-}
-
-/**
- * @internal
- */
-export interface VaporInVDOMInterface {
-  mount(
-    vnode: VNode,
-    container: any,
-    anchor: any,
-    parentComponent: ComponentInternalInstance | null,
-  ): GenericComponentInstance // VaporComponentInstance
-  update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
-  unmount(vnode: VNode, doRemove?: boolean): void
-  move(vnode: VNode, container: any, anchor: any): void
 }
 
 /**
@@ -208,6 +192,19 @@ export interface GenericAppContext {
    * @internal
    */
   reload?: () => void
+
+  /**
+   * @internal vapor interop only, for creating vapor components in vdom
+   */
+  vapor?: VaporInteropInterface
+  /**
+   * @internal vapor interop only, for creating vdom components in vapor
+   */
+  vdomMount?: (component: ConcreteComponent, props?: any, slots?: any) => any
+  /**
+   * @internal
+   */
+  vdomUnmount?: UnmountComponentFn
 }
 
 export interface AppContext extends GenericAppContext {
index a34a72be05a95ec83fabaf1a4e3df031ace2a9df..06f6e51ea8009e60626736acc9132aff1a91c8c9 100644 (file)
@@ -339,6 +339,7 @@ export interface GenericComponentInstance {
   vapor?: boolean
   uid: number
   type: GenericComponent
+  root: GenericComponentInstance | null
   parent: GenericComponentInstance | null
   appContext: GenericAppContext
   /**
@@ -823,9 +824,15 @@ export function setupComponent(
 ): Promise<void> | undefined {
   isSSR && setInSSRSetupState(isSSR)
 
-  const { props, children } = instance.vnode
+  const { props, children, vi } = instance.vnode
   const isStateful = isStatefulComponent(instance)
-  initProps(instance, props, isStateful, isSSR)
+
+  if (vi) {
+    // Vapor interop override - use Vapor props/attrs proxy
+    vi(instance)
+  } else {
+    initProps(instance, props, isStateful, isSSR)
+  }
   initSlots(instance, children, optimized)
 
   const setupResult = isStateful
index a1afae6201a57033b70829aba8f68f86537c3c4e..53e0b5a3e7706eb5126c367e22d3c973bd961726 100644 (file)
@@ -454,7 +454,7 @@ export function updateHOCHostEl(
   { vnode, parent }: ComponentInternalInstance,
   el: typeof vnode.el, // HostNode
 ): void {
-  while (parent) {
+  while (parent && !parent.vapor) {
     const root = parent.subTree
     if (root.suspense && root.suspense.activeBranch === vnode) {
       root.el = vnode.el
index 4a1d4472762a490ca7b1b002beae4ada848b6604..36614206b22abe65bb605fb6b0c47136b9f012b7 100644 (file)
@@ -498,7 +498,14 @@ export {
   type LifecycleHook,
 } from './component'
 export { type NormalizedPropsOptions } from './componentProps'
-
+/**
+ * @internal
+ */
+export { type VaporInteropInterface } from './vaporInterop'
+/**
+ * @internal
+ */
+export { type RendererInternals } from './renderer'
 /**
  * @internal
  */
@@ -530,7 +537,6 @@ export {
   createAppAPI,
   type AppMountFn,
   type AppUnmountFn,
-  type VaporInVDOMInterface,
 } from './apiCreateApp'
 /**
  * @internal
index 0a745a90b85e4293dc09bdfac6f1c00a0deeb406..99be57828f44027b99be86ac3c6a458034861dba 100644 (file)
@@ -65,7 +65,6 @@ import {
   type AppMountFn,
   type AppUnmountFn,
   type CreateAppFunction,
-  type VaporInVDOMInterface,
   createAppAPI,
 } from './apiCreateApp'
 import { setRef } from './rendererTemplateRef'
@@ -96,10 +95,12 @@ import { isAsyncWrapper } from './apiAsyncComponent'
 import { isCompatEnabled } from './compat/compatConfig'
 import { DeprecationTypes } from './compat/compatConfig'
 import type { TransitionHooks } from './components/BaseTransition'
+import type { VaporInteropInterface } from './vaporInterop'
 
 export interface Renderer<HostElement = RendererElement> {
   render: RootRenderFunction<HostElement>
   createApp: CreateAppFunction<HostElement>
+  internals: RendererInternals
 }
 
 export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
@@ -175,6 +176,7 @@ export interface RendererInternals<
   r: RemoveFn
   m: MoveFn
   mt: MountComponentFn
+  umt: UnmountComponentFn
   mc: MountChildrenFn
   pc: PatchChildrenFn
   pbc: PatchBlockChildrenFn
@@ -271,6 +273,12 @@ export type MountComponentFn = (
   optimized: boolean,
 ) => void
 
+export type UnmountComponentFn = (
+  instance: ComponentInternalInstance,
+  parentSuspense: SuspenseBoundary | null,
+  doRemove?: boolean,
+) => void
+
 type ProcessTextOrCommentFn = (
   n1: VNode | null,
   n2: VNode,
@@ -1433,6 +1441,7 @@ function baseCreateRenderer(
         if (
           initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
           (parent &&
+            parent.vnode &&
             isAsyncWrapper(parent.vnode) &&
             parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
         ) {
@@ -2308,10 +2317,10 @@ function baseCreateRenderer(
     hostRemove(end)
   }
 
-  const unmountComponent = (
-    instance: ComponentInternalInstance,
-    parentSuspense: SuspenseBoundary | null,
-    doRemove?: boolean,
+  const unmountComponent: UnmountComponentFn = (
+    instance,
+    parentSuspense,
+    doRemove,
   ) => {
     if (__DEV__ && instance.type.__hmrId) {
       unregisterHMR(instance)
@@ -2437,6 +2446,7 @@ function baseCreateRenderer(
     m: move,
     r: remove,
     mt: mountComponent,
+    umt: unmountComponent,
     mc: mountChildren,
     pc: patchChildren,
     pbc: patchBlockChildren,
@@ -2494,6 +2504,7 @@ function baseCreateRenderer(
   return {
     render,
     hydrate,
+    internals,
     createApp: createAppAPI(
       mountApp,
       unmountApp,
@@ -2608,8 +2619,8 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void {
 
 function getVaporInterface(
   instance: ComponentInternalInstance | null,
-): VaporInVDOMInterface {
-  const res = instance!.appContext.config.vapor
+): VaporInteropInterface {
+  const res = instance!.appContext.vapor
   if (__DEV__ && !res) {
     warn(
       `Vapor component found in vdom tree but vapor-in-vdom interop was not installed. ` +
diff --git a/packages/runtime-core/src/vaporInterop.ts b/packages/runtime-core/src/vaporInterop.ts
new file mode 100644 (file)
index 0000000..a0c8eb9
--- /dev/null
@@ -0,0 +1,21 @@
+import type {
+  ComponentInternalInstance,
+  GenericComponentInstance,
+} from './component'
+import type { VNode } from './vnode'
+
+/**
+ * The vapor in vdom implementation is in runtime-vapor/src/vdomInterop.ts
+ * @internal
+ */
+export interface VaporInteropInterface {
+  mount(
+    vnode: VNode,
+    container: any,
+    anchor: any,
+    parentComponent: ComponentInternalInstance | null,
+  ): GenericComponentInstance // VaporComponentInstance
+  update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
+  unmount(vnode: VNode, doRemove?: boolean): void
+  move(vnode: VNode, container: any, anchor: any): void
+}
index a8c5340cd1fe167840f700e34a2a25b1909eb048..17efef80a6276bd9a17745ad411fe828e67a8818 100644 (file)
@@ -253,6 +253,10 @@ export interface VNode<
    * @internal custom element interception hook
    */
   ce?: (instance: ComponentInternalInstance) => void
+  /**
+   * @internal VDOM in Vapor interop hook
+   */
+  vi?: (instance: ComponentInternalInstance) => void
 }
 
 // Since v-if and v-for are the two possible ways node structure can dynamically
index 71e8fcb2e6310ffcce7f74000ce3ae97f927c831..51c72fe2ed16f5f78dfedcb371f673e5acdb3d73 100644 (file)
@@ -73,7 +73,7 @@ let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
 
 let enabledHydration = false
 
-function ensureRenderer() {
+function ensureRenderer(): Renderer<Element | ShadowRoot> {
   return (
     renderer ||
     (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
@@ -230,7 +230,7 @@ function injectCompilerOptionsCheck(app: App) {
 /**
  * @internal
  */
-export function normalizeContainer<T extends ParentNode>(
+function normalizeContainer<T extends ParentNode>(
   container: T | string,
 ): T | null {
   if (isString(container)) {
@@ -313,7 +313,13 @@ export * from '@vue/runtime-core'
 export * from './jsx'
 
 // VAPOR -----------------------------------------------------------------------
+// Everything below are exposed for vapor only and can change any time.
+// They are also trimmed from non-bundler builds.
 
+/**
+ * @internal
+ */
+export { ensureRenderer, normalizeContainer }
 /**
  * @internal
  */
index bd4948f894d08134c5359af9a6c3e0f98ba7feb0..d3634890546da811d18131bac5d5680d7ce45af0 100644 (file)
@@ -20,6 +20,8 @@ export type BlockFn = (...args: any[]) => Block
 export class VaporFragment {
   nodes: Block
   anchor?: Node
+  insert?: (parent: ParentNode, anchor: Node | null) => void
+  remove?: () => void
 
   constructor(nodes: Block) {
     this.nodes = nodes
@@ -118,7 +120,11 @@ export function insert(
     }
   } else {
     // fragment
-    insert(block.nodes, parent, anchor)
+    if (block.insert) {
+      block.insert(parent, anchor)
+    } else {
+      insert(block.nodes, parent, anchor)
+    }
     if (block.anchor) insert(block.anchor, parent, anchor)
   }
 }
@@ -151,7 +157,11 @@ export function remove(block: Block, parent: ParentNode): void {
     }
   } else {
     // fragment
-    remove(block.nodes, parent)
+    if (block.remove) {
+      block.remove()
+    } else {
+      remove(block.nodes, parent)
+    }
     if (block.anchor) remove(block.anchor, parent)
     if ((block as DynamicFragment).scope) {
       ;(block as DynamicFragment).scope!.stop()
index 7049041f8766ef6b0dde6c9df83f867c7c1d8df8..bcf101cc86653d8fecc5abc95db00fe87e6d71d4 100644 (file)
@@ -1,4 +1,5 @@
 import {
+  type ComponentInternalInstance,
   type ComponentInternalOptions,
   type ComponentPropsOptions,
   EffectScope,
@@ -135,7 +136,7 @@ export type LooseRawProps = Record<
   $?: DynamicPropsSource[]
 }
 
-type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
+export type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
   $?: DynamicSlotSource[]
 }
 
@@ -144,17 +145,23 @@ export function createComponent(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
-  appContext?: GenericAppContext,
+  appContext: GenericAppContext = (currentInstance &&
+    currentInstance.appContext) ||
+    emptyContext,
 ): VaporComponentInstance {
-  // check if we are the single root of the parent
-  // if yes, inject parent attrs as dynamic props source
-  // TODO avoid child overwriting parent
+  // vdom interop enabled and component is not an explicit vapor component
+  if (appContext.vdomMount && !component.__vapor) {
+    return appContext.vdomMount(component as any, rawProps, rawSlots)
+  }
+
   if (
     isSingleRoot &&
     component.inheritAttrs !== false &&
     isVaporComponent(currentInstance) &&
     currentInstance.hasFallthrough
   ) {
+    // check if we are the single root of the parent
+    // if yes, inject parent attrs as dynamic props source
     const attrs = currentInstance.attrs
     if (rawProps) {
       ;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
@@ -175,6 +182,10 @@ export function createComponent(
   if (__DEV__) {
     pushWarningContext(instance)
     startMeasure(instance, `init`)
+
+    // cache normalized options for dev only emit check
+    instance.propsOptions = normalizePropsOptions(component)
+    instance.emitsOptions = normalizeEmitsOptions(component)
   }
 
   const prev = currentInstance
@@ -287,8 +298,10 @@ export class VaporComponentInstance implements GenericComponentInstance {
   vapor: true
   uid: number
   type: VaporComponent
+  root: GenericComponentInstance | null
   parent: GenericComponentInstance | null
-  children: VaporComponentInstance[] // TODO handle vdom children
+  children: VaporComponentInstance[]
+  vdomChildren?: ComponentInternalInstance[]
   appContext: GenericAppContext
 
   block: Block
@@ -361,7 +374,8 @@ export class VaporComponentInstance implements GenericComponentInstance {
     this.vapor = true
     this.uid = nextUid()
     this.type = comp
-    this.parent = currentInstance // TODO proper parent source when inside vdom instance
+    this.parent = currentInstance
+    this.root = currentInstance ? currentInstance.root : this
     this.children = []
 
     if (currentInstance) {
@@ -418,12 +432,6 @@ export class VaporComponentInstance implements GenericComponentInstance {
         ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
         : rawSlots
       : EMPTY_OBJ
-
-    if (__DEV__) {
-      // cache normalized options for dev only emit check
-      this.propsOptions = normalizePropsOptions(comp)
-      this.emitsOptions = normalizeEmitsOptions(comp)
-    }
   }
 
   /**
@@ -448,8 +456,8 @@ export function isVaporComponent(
  */
 export function createComponentWithFallback(
   comp: VaporComponent | string,
-  rawProps?: RawProps | null,
-  rawSlots?: RawSlots | null,
+  rawProps?: LooseRawProps | null,
+  rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
 ): HTMLElement | VaporComponentInstance {
   if (!isString(comp)) {
@@ -462,7 +470,7 @@ export function createComponentWithFallback(
 
   if (rawProps) {
     renderEffect(() => {
-      setDynamicProps(el, [resolveDynamicProps(rawProps)])
+      setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
     })
   }
 
@@ -470,7 +478,7 @@ export function createComponentWithFallback(
     if (rawSlots.$) {
       // TODO dynamic slot fragment
     } else {
-      insert(getSlot(rawSlots, 'default')!(), el)
+      insert(getSlot(rawSlots as RawSlots, 'default')!(), el)
     }
   }
 
@@ -517,6 +525,14 @@ export function unmountComponent(
     }
     instance.children = EMPTY_ARR as any
 
+    if (instance.vdomChildren) {
+      const unmount = instance.appContext.vdomUnmount!
+      for (const c of instance.vdomChildren) {
+        unmount(c, null)
+      }
+      instance.vdomChildren = EMPTY_ARR as any
+    }
+
     if (parentNode) {
       // root remove: need to both remove this instance's DOM nodes
       // and also remove it from the parent's children list.
index e3e1d6a32c6a371f44354cb650742c4320a47f6e..a5e9daad229ca25c684614de03d195ebc2aee985 100644 (file)
@@ -199,7 +199,9 @@ export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
       return rawProps[key]()
     }
   }
-  return merged
+  if (merged && merged.length) {
+    return merged
+  }
 }
 
 export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
@@ -342,3 +344,18 @@ function propsDeleteDevTrap(_: any, key: string | symbol) {
   )
   return true
 }
+
+export const rawPropsProxyHandlers: ProxyHandler<RawProps> = {
+  get: getAttrFromRawProps,
+  has: hasAttrFromRawProps,
+  ownKeys: getKeysFromRawProps,
+  getOwnPropertyDescriptor(target, key: string) {
+    if (hasAttrFromRawProps(target, key)) {
+      return {
+        configurable: true,
+        enumerable: true,
+        get: () => getAttrFromRawProps(target, key),
+      }
+    }
+  },
+}
index 2830e9b054080861a184dfe6362933c60ae0a3b5..02a6ebfbbe0bbd8bd252163525c5deb6013e7fb0 100644 (file)
@@ -1,11 +1,6 @@
 import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
 import { type Block, type BlockFn, DynamicFragment } from './block'
-import {
-  type RawProps,
-  getAttrFromRawProps,
-  getKeysFromRawProps,
-  hasAttrFromRawProps,
-} from './componentProps'
+import { rawPropsProxyHandlers } from './componentProps'
 import { currentInstance } from '@vue/runtime-core'
 import type { LooseRawProps, VaporComponentInstance } from './component'
 import { renderEffect } from './renderEffect'
@@ -90,21 +85,6 @@ export function getSlot(
   }
 }
 
-const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
-  get: getAttrFromRawProps,
-  has: hasAttrFromRawProps,
-  ownKeys: getKeysFromRawProps,
-  getOwnPropertyDescriptor(target, key: string) {
-    if (hasAttrFromRawProps(target, key)) {
-      return {
-        configurable: true,
-        enumerable: true,
-        get: () => getAttrFromRawProps(target, key),
-      }
-    }
-  },
-}
-
 // TODO how to handle empty slot return blocks?
 // e.g. a slot renders a v-if node that may toggle inside.
 // we may need special handling by passing the fallback into the slot
@@ -119,7 +99,7 @@ export function createSlot(
   const isDynamicName = isFunction(name)
   const fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
   const slotProps = rawProps
-    ? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers)
+    ? new Proxy(rawProps, rawPropsProxyHandlers)
     : EMPTY_OBJ
 
   const renderSlot = () => {
index aed0365cf41c6b385ee7cc13c3547fb92cabf93f..040239e23338d52a4f3dc764db5b596971672901 100644 (file)
@@ -1,19 +1,29 @@
 import {
+  type ComponentInternalInstance,
+  type ConcreteComponent,
   type Plugin,
-  type VaporInVDOMInterface,
+  type RendererInternals,
+  type VaporInteropInterface,
+  createVNode,
   currentInstance,
+  ensureRenderer,
   shallowRef,
   simpleSetCurrentInstance,
 } from '@vue/runtime-dom'
 import {
-  type VaporComponentInstance,
+  type LooseRawProps,
+  type LooseRawSlots,
+  VaporComponentInstance,
   createComponent,
   mountComponent,
   unmountComponent,
 } from './component'
-import { insert } from './block'
+import { VaporFragment, insert } from './block'
+import { extend, remove } from '@vue/shared'
+import { type RawProps, rawPropsProxyHandlers } from './componentProps'
+import type { RawSlots } from './componentSlots'
 
-const vaporInVDOMInterface: VaporInVDOMInterface = {
+const vaporInteropImpl: VaporInteropInterface = {
   mount(vnode, container, anchor, parentComponent) {
     const selfAnchor = (vnode.anchor = document.createComment('vapor'))
     container.insertBefore(selfAnchor, anchor)
@@ -49,6 +59,63 @@ const vaporInVDOMInterface: VaporInVDOMInterface = {
   },
 }
 
+function createVDOMComponent(
+  internals: RendererInternals,
+  component: ConcreteComponent,
+  rawProps?: LooseRawProps | null,
+  rawSlots?: LooseRawSlots | null,
+): VaporFragment {
+  const frag = new VaporFragment([])
+  const vnode = createVNode(
+    component,
+    rawProps && new Proxy(rawProps, rawPropsProxyHandlers),
+  )
+  const wrapper = new VaporComponentInstance(
+    { props: component.props },
+    rawProps as RawProps,
+    rawSlots as RawSlots,
+  )
+
+  // overwrite how the vdom instance handles props
+  vnode.vi = (instance: ComponentInternalInstance) => {
+    instance.props = wrapper.props
+    instance.attrs = wrapper.attrs
+    // TODO slots
+  }
+
+  let isMounted = false
+  const parentInstance = currentInstance as VaporComponentInstance
+  frag.insert = (parent, anchor) => {
+    if (!isMounted) {
+      internals.mt(
+        vnode,
+        parent,
+        anchor,
+        parentInstance as any,
+        null,
+        undefined,
+        false,
+      )
+      ;(parentInstance.vdomChildren || (parentInstance.vdomChildren = [])).push(
+        vnode.component!,
+      )
+      isMounted = true
+    } else {
+      // TODO move
+    }
+  }
+  frag.remove = () => {
+    internals.umt(vnode.component!, null, true)
+    remove(parentInstance.vdomChildren!, vnode.component)
+    isMounted = false
+  }
+
+  return frag
+}
+
 export const vaporInteropPlugin: Plugin = app => {
-  app.config.vapor = vaporInVDOMInterface
+  app._context.vapor = extend(vaporInteropImpl)
+  const internals = ensureRenderer().internals
+  app._context.vdomMount = createVDOMComponent.bind(null, internals)
+  app._context.vdomUnmount = internals.umt
 }