import {
type Component,
+ type ComponentInternalInstance,
type ConcreteComponent,
type Data,
type GenericComponent,
* @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): void
+ unmount(vnode: VNode, doRemove?: boolean): void
+ move(vnode: VNode, container: any, anchor: any): void
}
/**
// Note: can't mark this whole interface internal because some public interfaces
// extend it.
export interface ComponentInternalOptions {
+ /**
+ * indicates vapor component
+ */
+ __vapor?: boolean
/**
* @internal
*/
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
- const instance = getCurrentInstance()!
+ const keepAliveInstance = getCurrentInstance()!
// KeepAlive communicates with the instantiated renderer via the
// ctx where the renderer passes in its internals,
// and the KeepAlive instance exposes activate/deactivate implementations.
// The whole point of this is to avoid importing KeepAlive directly in the
// renderer to facilitate tree-shaking.
- const sharedContext = instance.ctx as KeepAliveContext
+ const sharedContext = keepAliveInstance.ctx as KeepAliveContext
// if the internal renderer is not registered, it indicates that this is server-side rendering,
// for KeepAlive, we just need to render its children
let current: VNode | null = null
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
- ;(instance as any).__v_cache = cache
+ ;(keepAliveInstance as any).__v_cache = cache
}
- const parentSuspense = instance.suspense
+ const parentSuspense = keepAliveInstance.suspense
const {
renderer: {
optimized,
) => {
const instance = vnode.component!
- move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
+ move(
+ vnode,
+ container,
+ anchor,
+ MoveType.ENTER,
+ keepAliveInstance,
+ parentSuspense,
+ )
// in case props have changed
patch(
instance.vnode,
invalidateMount(instance.m)
invalidateMount(instance.a)
- move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
+ move(
+ vnode,
+ storageContainer,
+ null,
+ MoveType.LEAVE,
+ keepAliveInstance,
+ parentSuspense,
+ )
queuePostRenderEffect(() => {
if (instance.da) {
invokeArrayFns(instance.da)
function unmount(vnode: VNode) {
// reset the shapeFlag so it can be properly unmounted
resetShapeFlag(vnode)
- _unmount(vnode, instance, parentSuspense, true)
+ _unmount(vnode, keepAliveInstance, parentSuspense, true)
}
function pruneCache(filter: (name: string) => boolean) {
if (pendingCacheKey != null) {
// if KeepAlive child is a Suspense, it needs to be cached after Suspense resolves
// avoid caching vnode that not been mounted
- if (isSuspense(instance.subTree.type)) {
+ if (isSuspense(keepAliveInstance.subTree.type)) {
queuePostRenderEffect(() => {
- cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
- }, instance.subTree.suspense)
+ cache.set(
+ pendingCacheKey!,
+ getInnerChild(keepAliveInstance.subTree),
+ )
+ }, keepAliveInstance.subTree.suspense)
} else {
- cache.set(pendingCacheKey, getInnerChild(instance.subTree))
+ cache.set(pendingCacheKey, getInnerChild(keepAliveInstance.subTree))
}
}
}
onBeforeUnmount(() => {
cache.forEach(cached => {
- const { subTree, suspense } = instance
+ const { subTree, suspense } = keepAliveInstance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type && cached.key === vnode.key) {
// current instance will be unmounted as part of keep-alive's unmount
container,
anchor === initialAnchor ? next(activeBranch!) : anchor,
MoveType.ENTER,
+ parentComponent,
)
queuePostFlushCb(effects)
}
}
if (!delayEnter) {
// move content from off-dom container to actual container
- move(pendingBranch!, container, anchor, MoveType.ENTER)
+ move(
+ pendingBranch!,
+ container,
+ anchor,
+ MoveType.ENTER,
+ parentComponent,
+ )
}
}
move(container, anchor, type) {
suspense.activeBranch &&
- move(suspense.activeBranch, container, anchor, type)
+ move(suspense.activeBranch, container, anchor, type, parentComponent)
suspense.container = container
},
container,
mainAnchor,
internals,
+ parentComponent,
TeleportMoveTypes.TOGGLE,
)
} else {
nextTarget,
null,
internals,
+ parentComponent,
TeleportMoveTypes.TARGET_CHANGE,
)
} else if (__DEV__) {
target,
targetAnchor,
internals,
+ parentComponent,
TeleportMoveTypes.TOGGLE,
)
}
container: RendererElement,
parentAnchor: RendererNode | null,
{ o: { insert }, m: move }: RendererInternals,
+ parentComponent: ComponentInternalInstance | null,
moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER,
): void {
// move target anchor if this is a target change.
container,
parentAnchor,
MoveType.REORDER,
+ parentComponent,
)
}
}
createAppAPI,
type AppMountFn,
type AppUnmountFn,
+ type VaporInVDOMInterface,
} from './apiCreateApp'
/**
* @internal
import {
type ComponentInternalInstance,
type ComponentOptions,
+ type ConcreteComponent,
type Data,
type LifecycleHook,
createComponentInstance,
type AppMountFn,
type AppUnmountFn,
type CreateAppFunction,
+ type VaporInVDOMInterface,
createAppAPI,
} from './apiCreateApp'
import { setRef } from './rendererTemplateRef'
container: RendererElement,
anchor: RendererNode | null,
type: MoveType,
+ parentComponent: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
) => void
optimized: boolean,
) => {
n2.slotScopeIds = slotScopeIds
- if (n1 == null) {
+
+ if ((n2.type as ConcreteComponent).__vapor) {
+ if (n1 == null) {
+ getVaporInterface(parentComponent).mount(
+ n2,
+ container,
+ anchor,
+ parentComponent,
+ )
+ } else {
+ getVaporInterface(parentComponent).update(n1, n2)
+ }
+ } else if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
- move(nextChild, container, anchor, MoveType.REORDER)
+ move(
+ nextChild,
+ container,
+ anchor,
+ MoveType.REORDER,
+ parentComponent,
+ )
} else {
j--
}
container,
anchor,
moveType,
+ parentComponent,
parentSuspense = null,
) => {
const { el, type, transition, children, shapeFlag } = vnode
if (shapeFlag & ShapeFlags.COMPONENT) {
- move(vnode.component!.subTree, container, anchor, moveType)
+ if ((type as ConcreteComponent).__vapor) {
+ getVaporInterface(parentComponent).move(vnode, container, anchor)
+ } else {
+ move(
+ vnode.component!.subTree,
+ container,
+ anchor,
+ moveType,
+ parentComponent,
+ )
+ }
return
}
}
if (shapeFlag & ShapeFlags.TELEPORT) {
- ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals)
+ ;(type as typeof TeleportImpl).move(
+ vnode,
+ container,
+ anchor,
+ internals,
+ parentComponent,
+ )
return
}
if (type === Fragment) {
hostInsert(el!, container, anchor)
for (let i = 0; i < (children as VNode[]).length; i++) {
- move((children as VNode[])[i], container, anchor, moveType)
+ move(
+ (children as VNode[])[i],
+ container,
+ anchor,
+ moveType,
+ parentComponent,
+ )
}
hostInsert(vnode.anchor!, container, anchor)
return
}
if (shapeFlag & ShapeFlags.COMPONENT) {
- unmountComponent(vnode.component!, parentSuspense, doRemove)
+ if ((type as ConcreteComponent).__vapor) {
+ getVaporInterface(parentComponent).unmount(vnode, doRemove)
+ } else {
+ unmountComponent(vnode.component!, parentSuspense, doRemove)
+ }
} else {
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
vnode.suspense!.unmount(parentSuspense, doRemove)
hooks[i].flags! |= SchedulerJobFlags.DISPOSED
}
}
+
+function getVaporInterface(
+ instance: ComponentInternalInstance | null,
+): VaporInVDOMInterface {
+ const res = instance!.appContext.config.vapor
+ if (__DEV__ && !res) {
+ warn(
+ `Vapor component found in vdom tree but vapor-in-vdom interop was not installed. ` +
+ `Make sure to install it:\n` +
+ `\`\`\`\nimport { vaporInteropPlugin } from 'vue'\n` +
+ `app.use(vaporInteropPlugin)\n` +
+ `\`\`\``,
+ )
+ }
+ return res!
+}
let app: App
function render(
- props: RawProps = {},
+ props: RawProps | undefined = undefined,
container: string | ParentNode = host,
) {
create(props)
return mount(container)
}
- function create(props: RawProps = {}) {
+ function create(props: RawProps | undefined = undefined) {
app?.unmount()
app = createVaporApp(component, props)
return res()
}
define(Comp).render()
// should not record watcher in detached scope
- expect(instance!.scope.effects.length).toBe(0)
+ // the 1 is the props validation effect
+ expect(instance!.scope.effects.length).toBe(1)
})
test('watchEffect should keep running if created in a detached scope', async () => {
}).render()
const i = instance as VaporComponentInstance
- expect(i.scope.effects.length).toBe(2)
+ // watchEffect + renderEffect + props validation effect
+ expect(i.scope.effects.length).toBe(3)
expect(host.innerHTML).toBe('<div>0</div>')
app.unmount()
remove,
} from './block'
import {
+ type ShallowRef,
markRaw,
pauseTracking,
proxyRefs,
simpleSetCurrentInstance(instance)
pauseTracking()
+ if (__DEV__) {
+ setupPropsValidation(instance)
+ }
+
const setupFn = isFunction(component) ? component : component.setup
const setupResult = setupFn
? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
props: Record<string, any>
attrs: Record<string, any>
propsDefaults: Record<string, any> | null
+ rawPropsRef?: ShallowRef<any> // to hold vnode props in vdom interop mode
slots: StaticSlots
: EMPTY_OBJ
if (__DEV__) {
- // validate props
- if (rawProps) setupPropsValidation(this)
// cache normalized options for dev only emit check
this.propsOptions = normalizePropsOptions(comp)
this.emitsOptions = normalizeEmitsOptions(comp)
// and also remove it from the parent's children list.
remove(instance.block, parentNode)
const parentInstance = instance.parent
+ instance.parent = null
if (isVaporComponent(parentInstance)) {
if (parentsWithUnmountedChildren) {
// for optimize children removal
} else {
removeItem(parentInstance.children, instance)
}
- instance.parent = null
}
}
// compiler-use only
export { insert, prepend, remove } from './block'
-export { createComponent, createComponentWithFallback } from './component'
+export {
+ createComponent,
+ createComponentWithFallback,
+ mountComponent,
+ unmountComponent,
+ type VaporComponentInstance,
+} from './component'
export { renderEffect } from './renderEffect'
export { createSlot } from './componentSlots'
export { template, children, next } from './dom/template'
applySelectModel,
applyDynamicModel,
} from './directives/vModel'
+export { vaporInteropPlugin } from './vdomInterop'
--- /dev/null
+import {
+ type GenericComponentInstance,
+ type Plugin,
+ type VNode,
+ type VaporInVDOMInterface,
+ currentInstance,
+ shallowRef,
+ simpleSetCurrentInstance,
+} from '@vue/runtime-dom'
+import {
+ type VaporComponentInstance,
+ createComponent,
+ mountComponent,
+ unmountComponent,
+} from './component'
+import { insert } from './block'
+
+const vaporInVDOMInterface: VaporInVDOMInterface = {
+ mount(
+ vnode: VNode,
+ container: ParentNode,
+ anchor: Node,
+ parentComponent: GenericComponentInstance | null,
+ ) {
+ const selfAnchor = (vnode.anchor = document.createComment('vapor'))
+ container.insertBefore(selfAnchor, anchor)
+ const prev = currentInstance
+ simpleSetCurrentInstance(parentComponent)
+ const propsRef = shallowRef(vnode.props)
+ // @ts-expect-error
+ const instance = (vnode.component = createComponent(vnode.type, {
+ $: [() => propsRef.value],
+ }))
+ instance.rawPropsRef = propsRef
+ mountComponent(instance, container, selfAnchor)
+ simpleSetCurrentInstance(prev)
+ return instance
+ },
+
+ update(n1: VNode, n2: VNode) {
+ n2.component = n1.component
+ ;(n2.component as any as VaporComponentInstance).rawPropsRef!.value =
+ n2.props
+ },
+
+ unmount(vnode: VNode, doRemove?: boolean) {
+ const container = doRemove ? vnode.anchor!.parentNode : undefined
+ unmountComponent(vnode.component as any, container)
+ },
+
+ move(vnode: VNode, container: ParentNode, anchor: Node) {
+ insert(vnode.component as any, container, anchor)
+ insert(vnode.anchor as any, container, anchor)
+ },
+}
+
+export const vaporInteropPlugin: Plugin = app => {
+ app.config.vapor = vaporInVDOMInterface
+}
-import './_entry'
+// import './_entry'
+import './interop'