diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index a81c6b433..80532e9ed 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -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 { 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 { diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index a34a72be0..06f6e51ea 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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 | 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 diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index a1afae620..53e0b5a3e 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -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 diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 4a1d44727..36614206b 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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 diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 0a745a90b..99be57828 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -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 { render: RootRenderFunction createApp: CreateAppFunction + internals: RendererInternals } export interface HydrationRenderer extends Renderer { @@ -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 index 000000000..a0c8eb90d --- /dev/null +++ b/packages/runtime-core/src/vaporInterop.ts @@ -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 +} diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index a8c5340cd..17efef80a 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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 diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 71e8fcb2e..51c72fe2e 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -73,7 +73,7 @@ let renderer: Renderer | HydrationRenderer let enabledHydration = false -function ensureRenderer() { +function ensureRenderer(): Renderer { return ( renderer || (renderer = createRenderer(rendererOptions)) @@ -230,7 +230,7 @@ function injectCompilerOptionsCheck(app: App) { /** * @internal */ -export function normalizeContainer( +function normalizeContainer( 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 */ diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index bd4948f89..d36348905 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -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() diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 7049041f8..bcf101cc8 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,4 +1,5 @@ import { + type ComponentInternalInstance, type ComponentInternalOptions, type ComponentPropsOptions, EffectScope, @@ -135,7 +136,7 @@ export type LooseRawProps = Record< $?: DynamicPropsSource[] } -type LooseRawSlots = Record & { +export type LooseRawSlots = Record & { $?: 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. diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index e3e1d6a32..a5e9daad2 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -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 = { + get: getAttrFromRawProps, + has: hasAttrFromRawProps, + ownKeys: getKeysFromRawProps, + getOwnPropertyDescriptor(target, key: string) { + if (hasAttrFromRawProps(target, key)) { + return { + configurable: true, + enumerable: true, + get: () => getAttrFromRawProps(target, key), + } + } + }, +} diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 2830e9b05..02a6ebfbb 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -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 = { - 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 = () => { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index aed0365cf..040239e23 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -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 = { }, } -export const vaporInteropPlugin: Plugin = app => { - app.config.vapor = 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._context.vapor = extend(vaporInteropImpl) + const internals = ensureRenderer().internals + app._context.vdomMount = createVDOMComponent.bind(null, internals) + app._context.vdomUnmount = internals.umt }