]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: implement defineVaporCustomElement
authordaiwei <daiwei521@126.com>
Fri, 24 Oct 2025 03:53:18 +0000 (11:53 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 24 Oct 2025 03:55:32 +0000 (11:55 +0800)
packages/runtime-dom/src/apiCustomElement.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/src/apiDefineVaporCustomElement.ts [new file with mode: 0644]

index 6b1c8f0cae8a91875a783a0df66957970d85a069..b8e5811700d29671062a26cd29366a6e59e54200 100644 (file)
@@ -3,7 +3,6 @@ import {
   type Component,
   type ComponentCustomElementInterface,
   type ComponentInjectOptions,
-  type ComponentInternalInstance,
   type ComponentObjectPropsOptions,
   type ComponentOptions,
   type ComponentOptionsBase,
@@ -19,6 +18,7 @@ import {
   type EmitsOptions,
   type EmitsToProps,
   type ExtractPropTypes,
+  type GenericComponentInstance,
   type MethodOptions,
   type RenderFunction,
   type SetupContext,
@@ -200,7 +200,9 @@ const BaseClass = (
 
 type InnerComponentDef = ConcreteComponent & CustomElementOptions
 
-export class VueElement
+export abstract class VueElementBase<
+    Def extends CustomElementOptions & { props?: any } = InnerComponentDef,
+  >
   extends BaseClass
   implements ComponentCustomElementInterface
 {
@@ -208,7 +210,7 @@ export class VueElement
   /**
    * @internal
    */
-  _instance: ComponentInternalInstance | null = null
+  _instance: GenericComponentInstance | null = null
   /**
    * @internal
    */
@@ -220,41 +222,48 @@ export class VueElement
   /**
    * @internal
    */
-  _nonce: string | undefined = this._def.nonce
-
+  _nonce: string | undefined
   /**
    * @internal
    */
   _teleportTargets?: Set<Element>
 
-  private _connected = false
-  private _resolved = false
-  private _numberProps: Record<string, true> | null = null
-  private _styleChildren = new WeakSet()
-  private _pendingResolve: Promise<void> | undefined
-  private _parent: VueElement | undefined
+  protected _def: Def
+  protected _props: Record<string, any>
+  protected _createApp: CreateAppFunction<Element>
+  protected _connected = false
+  protected _resolved = false
+  protected _numberProps: Record<string, true> | null = null
+  protected _styleChildren: WeakSet<object> = new WeakSet()
+  protected _pendingResolve: Promise<void> | undefined
+  protected _parent: VueElementBase | undefined
+
   /**
    * dev only
    */
-  private _styles?: HTMLStyleElement[]
+  protected _styles?: HTMLStyleElement[]
   /**
    * dev only
    */
-  private _childStyles?: Map<string, HTMLStyleElement[]>
-  private _ob?: MutationObserver | null = null
-  private _slots?: Record<string, Node[]>
+  protected _childStyles?: Map<string, HTMLStyleElement[]>
+  protected _ob?: MutationObserver | null = null
+  protected _slots?: Record<string, Node[]>
 
   constructor(
     /**
      * Component def - note this may be an AsyncWrapper, and this._def will
      * be overwritten by the inner component when resolved.
      */
-    private _def: InnerComponentDef,
-    private _props: Record<string, any> = {},
-    private _createApp: CreateAppFunction<Element> = createApp,
+    def: Def,
+    props: Record<string, any> = {},
+    createAppFn: CreateAppFunction<Element> = createApp,
   ) {
     super()
-    if (this.shadowRoot && _createApp !== createApp) {
+    this._def = def
+    this._props = props
+    this._createApp = createAppFn
+    this._nonce = def.nonce
+    if (this.shadowRoot && createAppFn !== createApp) {
       this._root = this.shadowRoot
     } else {
       if (__DEV__ && this.shadowRoot) {
@@ -263,9 +272,9 @@ export class VueElement
             `defined as hydratable. Use \`defineSSRCustomElement\`.`,
         )
       }
-      if (_def.shadowRoot !== false) {
+      if (def.shadowRoot !== false) {
         this.attachShadow(
-          extend({}, _def.shadowRootOptions, {
+          extend({}, def.shadowRootOptions, {
             mode: 'open',
           }) as ShadowRootInit,
         )
@@ -291,7 +300,7 @@ export class VueElement
     while (
       (parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
     ) {
-      if (parent instanceof VueElement) {
+      if (parent instanceof VueElementBase) {
         this._parent = parent
         break
       }
@@ -299,7 +308,7 @@ export class VueElement
 
     if (!this._instance) {
       if (this._resolved) {
-        this._mount(this._def)
+        this._mountComponent(this._def)
       } else {
         if (parent && parent._pendingResolve) {
           this._pendingResolve = parent._pendingResolve.then(() => {
@@ -313,14 +322,22 @@ export class VueElement
     }
   }
 
-  private _setParent(parent = this._parent) {
-    if (parent) {
-      this._instance!.parent = parent._instance
+  protected abstract _mountComponent(def: Def): void
+  protected abstract _updateComponent(): void
+  protected abstract _unmountComponent(): void
+
+  protected _setParent(
+    parent: VueElementBase | undefined = this._parent,
+  ): void {
+    if (parent && this._instance) {
+      this._instance.parent = parent._instance
       this._inheritParentContext(parent)
     }
   }
 
-  private _inheritParentContext(parent = this._parent) {
+  protected _inheritParentContext(
+    parent: VueElementBase | undefined = this._parent,
+  ): void {
     // #13212, the provides object of the app context must inherit the provides
     // object from the parent element so we can inject values from both places
     if (parent && this._app) {
@@ -339,10 +356,7 @@ export class VueElement
           this._ob.disconnect()
           this._ob = null
         }
-        // unmount
-        this._app && this._app.unmount()
-        if (this._instance) this._instance.ce = undefined
-        this._app = this._instance = null
+        this._unmountComponent()
         if (this._teleportTargets) {
           this._teleportTargets.clear()
           this._teleportTargets = undefined
@@ -375,7 +389,7 @@ export class VueElement
 
     this._ob.observe(this, { attributes: true })
 
-    const resolve = (def: InnerComponentDef, isAsync = false) => {
+    const resolve = (def: Def) => {
       this._resolved = true
       this._pendingResolve = undefined
 
@@ -410,51 +424,23 @@ export class VueElement
       }
 
       // initial mount
-      this._mount(def)
+      this._mountComponent(def)
     }
 
     const asyncDef = (this._def as ComponentOptions).__asyncLoader
     if (asyncDef) {
-      this._pendingResolve = asyncDef().then((def: InnerComponentDef) => {
-        def.configureApp = this._def.configureApp
-        resolve((this._def = def), true)
+      const { configureApp } = this._def
+      this._pendingResolve = asyncDef().then((def: any) => {
+        def.configureApp = configureApp
+        this._def = def
+        resolve(def)
       })
     } else {
       resolve(this._def)
     }
   }
 
-  private _mount(def: InnerComponentDef) {
-    if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
-      // @ts-expect-error
-      def.name = 'VueElement'
-    }
-    this._app = this._createApp(def)
-    // inherit before configureApp to detect context overwrites
-    this._inheritParentContext()
-    if (def.configureApp) {
-      def.configureApp(this._app)
-    }
-    this._app._ceVNode = this._createVNode()
-    this._app.mount(this._root)
-
-    // apply expose after mount
-    const exposed = this._instance && this._instance.exposed
-    if (!exposed) return
-    for (const key in exposed) {
-      if (!hasOwn(this, key)) {
-        // exposed properties are readonly
-        Object.defineProperty(this, key, {
-          // unwrap ref to be consistent with public instance behavior
-          get: () => unref(exposed[key]),
-        })
-      } else if (__DEV__) {
-        warn(`Exposed property "${key}" already exists on custom element.`)
-      }
-    }
-  }
-
-  private _resolveProps(def: InnerComponentDef) {
+  private _resolveProps(def: Def): void {
     const { props } = def
     const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
 
@@ -478,7 +464,7 @@ export class VueElement
     }
   }
 
-  protected _setAttr(key: string): void {
+  private _setAttr(key: string): void {
     if (key.startsWith('data-v-')) return
     const has = this.hasAttribute(key)
     let value = has ? this.getAttribute(key) : REMOVAL
@@ -511,12 +497,12 @@ export class VueElement
       } else {
         this._props[key] = val
         // support set key on ceVNode
-        if (key === 'key' && this._app) {
+        if (key === 'key' && this._app && this._app._ceVNode) {
           this._app._ceVNode!.key = val
         }
       }
       if (shouldUpdate && this._instance) {
-        this._update()
+        this._updateComponent()
       }
       // reflect
       if (shouldReflect) {
@@ -537,69 +523,10 @@ export class VueElement
     }
   }
 
-  private _update() {
-    const vnode = this._createVNode()
-    if (this._app) vnode.appContext = this._app._context
-    render(vnode, this._root)
-  }
-
-  private _createVNode(): VNode<any, any> {
-    const baseProps: VNodeProps = {}
-    if (!this.shadowRoot) {
-      baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
-        this._renderSlots.bind(this)
-    }
-    const vnode = createVNode(this._def, extend(baseProps, this._props))
-    if (!this._instance) {
-      vnode.ce = instance => {
-        this._instance = instance
-        instance.ce = this
-        instance.isCE = true // for vue-i18n backwards compat
-        // HMR
-        if (__DEV__) {
-          instance.ceReload = newStyles => {
-            // always reset styles
-            if (this._styles) {
-              this._styles.forEach(s => this._root.removeChild(s))
-              this._styles.length = 0
-            }
-            this._applyStyles(newStyles)
-            this._instance = null
-            this._update()
-          }
-        }
-
-        const dispatch = (event: string, args: any[]) => {
-          this.dispatchEvent(
-            new CustomEvent(
-              event,
-              isPlainObject(args[0])
-                ? extend({ detail: args }, args[0])
-                : { detail: args },
-            ),
-          )
-        }
-
-        // intercept emit
-        instance.emit = (event: string, ...args: any[]) => {
-          // dispatch both the raw and hyphenated versions of an event
-          // to match Vue behavior
-          dispatch(event, args)
-          if (hyphenate(event) !== event) {
-            dispatch(hyphenate(event), args)
-          }
-        }
-
-        this._setParent()
-      }
-    }
-    return vnode
-  }
-
-  private _applyStyles(
+  protected _applyStyles(
     styles: string[] | undefined,
     owner?: ConcreteComponent,
-  ) {
+  ): void {
     if (!styles) return
     if (owner) {
       if (owner === this._def || this._styleChildren.has(owner)) {
@@ -635,7 +562,7 @@ export class VueElement
    * Only called when shadowRoot is false
    */
   private _parseSlots() {
-    const slots: VueElement['_slots'] = (this._slots = {})
+    const slots: VueElementBase['_slots'] = (this._slots = {})
     let n
     while ((n = this.firstChild)) {
       const slotName =
@@ -648,7 +575,7 @@ export class VueElement
   /**
    * Only called when shadowRoot is false
    */
-  private _renderSlots() {
+  protected _renderSlots(): void {
     const outlets = this._getSlots()
     const scopeId = this._instance!.type.__scopeId
     for (let i = 0; i < outlets.length; i++) {
@@ -690,6 +617,7 @@ export class VueElement
       return res
     }, [])
   }
+
   /**
    * @internal
    */
@@ -715,9 +643,102 @@ export class VueElement
   }
 }
 
-export function useHost(caller?: string): VueElement | null {
+export class VueElement extends VueElementBase<InnerComponentDef> {
+  protected _mountComponent(def: InnerComponentDef): void {
+    if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
+      // @ts-expect-error
+      def.name = 'VueElement'
+    }
+    this._app = this._createApp(def)
+    this._inheritParentContext()
+    if (def.configureApp) {
+      def.configureApp(this._app)
+    }
+    this._app._ceVNode = this._createVNode()
+    this._app.mount(this._root)
+
+    const exposed = this._instance && this._instance.exposed
+    if (!exposed) return
+    for (const key in exposed) {
+      if (!hasOwn(this, key)) {
+        Object.defineProperty(this, key, {
+          get: () => unref(exposed[key]),
+        })
+      } else if (__DEV__) {
+        warn(`Exposed property "${key}" already exists on custom element.`)
+      }
+    }
+  }
+
+  protected _updateComponent(): void {
+    if (!this._app) return
+    const vnode = this._createVNode()
+    vnode.appContext = this._app._context
+    render(vnode, this._root)
+  }
+
+  protected _unmountComponent(): void {
+    if (this._app) {
+      this._app.unmount()
+    }
+    if (this._instance && this._instance.ce) {
+      this._instance.ce = undefined
+    }
+    this._app = this._instance = null
+  }
+
+  private _createVNode(): VNode<any, any> {
+    const baseProps: VNodeProps = {}
+    if (!this.shadowRoot) {
+      baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
+        this._renderSlots.bind(this)
+    }
+    const vnode = createVNode(this._def, extend(baseProps, this._props))
+    if (!this._instance) {
+      vnode.ce = instance => {
+        this._instance = instance
+        instance.ce = this
+        instance.isCE = true
+        if (__DEV__) {
+          instance.ceReload = newStyles => {
+            if (this._styles) {
+              this._styles.forEach(s => this._root.removeChild(s))
+              this._styles.length = 0
+            }
+            this._applyStyles(newStyles)
+            this._instance = null
+            this._updateComponent()
+          }
+        }
+
+        const dispatch = (event: string, args: any[]) => {
+          this.dispatchEvent(
+            new CustomEvent(
+              event,
+              isPlainObject(args[0])
+                ? extend({ detail: args }, args[0])
+                : { detail: args },
+            ),
+          )
+        }
+
+        instance.emit = (event: string, ...args: any[]) => {
+          dispatch(event, args)
+          if (hyphenate(event) !== event) {
+            dispatch(hyphenate(event), args)
+          }
+        }
+
+        this._setParent()
+      }
+    }
+    return vnode
+  }
+}
+
+export function useHost(caller?: string): VueElementBase | null {
   const instance = getCurrentInstance()
-  const el = instance && (instance.ce as VueElement)
+  const el = instance && (instance.ce as VueElementBase)
   if (el) {
     return el
   } else if (__DEV__) {
index c4518b00291d268752ceba871571223984e63373..0d76547019265cc93ee5f1658d4696e26a647e69 100644 (file)
@@ -261,6 +261,7 @@ export {
   useShadowRoot,
   useHost,
   VueElement,
+  VueElementBase,
   type VueElementConstructor,
   type CustomElementOptions,
 } from './apiCustomElement'
diff --git a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts
new file mode 100644 (file)
index 0000000..bda741a
--- /dev/null
@@ -0,0 +1,49 @@
+import { extend, isPlainObject } from '@vue/shared'
+import { defineVaporComponent } from '.'
+import {
+  type CreateAppFunction,
+  type CustomElementOptions,
+  VueElementBase,
+} from '@vue/runtime-dom'
+import type { ObjectVaporComponent, VaporComponent } from './component'
+
+export type VaporElementConstructor<P = {}> = {
+  new (initialProps?: Record<string, any>): VaporElement & P
+}
+
+// TODO type inference
+
+/*@__NO_SIDE_EFFECTS__*/
+export function defineCustomElement(
+  options: any,
+  extraOptions?: Omit<ObjectVaporComponent, 'setup'>,
+  /**
+   * @internal
+   */
+  _createApp?: CreateAppFunction<Element>,
+): VaporElementConstructor {
+  let Comp = defineVaporComponent(options, extraOptions)
+  if (isPlainObject(Comp)) Comp = extend({}, Comp, extraOptions)
+  class VaporCustomElement extends VaporElement {
+    static def = Comp
+    constructor(initialProps?: Record<string, any>) {
+      super(Comp, initialProps, _createApp)
+    }
+  }
+
+  return VaporCustomElement
+}
+
+type VaporInnerComponentDef = VaporComponent & CustomElementOptions
+
+export class VaporElement extends VueElementBase<VaporInnerComponentDef> {
+  protected _mountComponent(def: VaporInnerComponentDef): void {
+    throw new Error('Method not implemented.')
+  }
+  protected _updateComponent(): void {
+    throw new Error('Method not implemented.')
+  }
+  protected _unmountComponent(): void {
+    throw new Error('Method not implemented.')
+  }
+}