import {
type Component,
- type ComponentInternalInstance,
type ConcreteComponent,
type Data,
type GenericComponent,
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'
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
* @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
}
/**
* @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 {
vapor?: boolean
uid: number
type: GenericComponent
+ root: GenericComponentInstance | null
parent: GenericComponentInstance | null
appContext: GenericAppContext
/**
): 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
{ 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
type LifecycleHook,
} from './component'
export { type NormalizedPropsOptions } from './componentProps'
-
+/**
+ * @internal
+ */
+export { type VaporInteropInterface } from './vaporInterop'
+/**
+ * @internal
+ */
+export { type RendererInternals } from './renderer'
/**
* @internal
*/
createAppAPI,
type AppMountFn,
type AppUnmountFn,
- type VaporInVDOMInterface,
} from './apiCreateApp'
/**
* @internal
type AppMountFn,
type AppUnmountFn,
type CreateAppFunction,
- type VaporInVDOMInterface,
createAppAPI,
} from './apiCreateApp'
import { setRef } from './rendererTemplateRef'
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> {
r: RemoveFn
m: MoveFn
mt: MountComponentFn
+ umt: UnmountComponentFn
mc: MountChildrenFn
pc: PatchChildrenFn
pbc: PatchBlockChildrenFn
optimized: boolean,
) => void
+export type UnmountComponentFn = (
+ instance: ComponentInternalInstance,
+ parentSuspense: SuspenseBoundary | null,
+ doRemove?: boolean,
+) => void
+
type ProcessTextOrCommentFn = (
n1: VNode | null,
n2: VNode,
if (
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
(parent &&
+ parent.vnode &&
isAsyncWrapper(parent.vnode) &&
parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
) {
hostRemove(end)
}
- const unmountComponent = (
- instance: ComponentInternalInstance,
- parentSuspense: SuspenseBoundary | null,
- doRemove?: boolean,
+ const unmountComponent: UnmountComponentFn = (
+ instance,
+ parentSuspense,
+ doRemove,
) => {
if (__DEV__ && instance.type.__hmrId) {
unregisterHMR(instance)
m: move,
r: remove,
mt: mountComponent,
+ umt: unmountComponent,
mc: mountChildren,
pc: patchChildren,
pbc: patchBlockChildren,
return {
render,
hydrate,
+ internals,
createApp: createAppAPI(
mountApp,
unmountApp,
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. ` +
--- /dev/null
+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
+}
* @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
let enabledHydration = false
-function ensureRenderer() {
+function ensureRenderer(): Renderer<Element | ShadowRoot> {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
/**
* @internal
*/
-export function normalizeContainer<T extends ParentNode>(
+function normalizeContainer<T extends ParentNode>(
container: T | string,
): T | null {
if (isString(container)) {
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
*/
export class VaporFragment {
nodes: Block
anchor?: Node
+ insert?: (parent: ParentNode, anchor: Node | null) => void
+ remove?: () => void
constructor(nodes: Block) {
this.nodes = nodes
}
} 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)
}
}
}
} 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()
import {
+ type ComponentInternalInstance,
type ComponentInternalOptions,
type ComponentPropsOptions,
EffectScope,
$?: DynamicPropsSource[]
}
-type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
+export type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
$?: DynamicSlotSource[]
}
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(
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
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
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) {
? 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)
- }
}
/**
*/
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)) {
if (rawProps) {
renderEffect(() => {
- setDynamicProps(el, [resolveDynamicProps(rawProps)])
+ setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
})
}
if (rawSlots.$) {
// TODO dynamic slot fragment
} else {
- insert(getSlot(rawSlots, 'default')!(), el)
+ insert(getSlot(rawSlots as RawSlots, 'default')!(), el)
}
}
}
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.
return rawProps[key]()
}
}
- return merged
+ if (merged && merged.length) {
+ return merged
+ }
}
export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
)
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),
+ }
+ }
+ },
+}
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'
}
}
-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
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 = () => {
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)
},
}
+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
}