Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 83 additions & 13 deletions packages/kit/src/runtime/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import type {
ComponentTreeNodeTag,
ComponentTreeNodeSnapshot,
} from '../protocol/messages'
import { isVaporApp } from './vapor'
import {
isForBlock,
isForFragment,
isInteropFragment,
isTeleportFragment,
isVaporApp,
isVaporFragment,
isVaporInstance,
} from './vapor'
import type { AppId, AppRef, ComponentId, InstanceRef, RuntimeEvent, VueTypeMap } from './types'

export interface ComponentRecord {
Expand Down Expand Up @@ -51,6 +59,8 @@ export type { AppSnapshot, ComponentTreeNodeTag } from '../protocol/messages'
export type ComponentSnapshot = ComponentTreeNodeSnapshot

interface ComponentRecordOverrides {
// Vapor `v-for` keys live on the enclosing `ForBlock`, not on the instance.
renderKey?: unknown
inactive?: boolean
tags?: ComponentTreeNodeTag[]
domOrder?: number[]
Expand All @@ -62,6 +72,7 @@ export interface WalkedComponentChild {
instance: InstanceRef
tags?: ComponentTreeNodeTag[]
inactive?: boolean
renderKey?: unknown
}

// Cross-task caches must observe child-list mutations, including equal-length
Expand Down Expand Up @@ -219,7 +230,9 @@ export class RuntimeRegistry {
existing.parentId = parentId
existing.name = readComponentName(instance)
existing.file = readComponentFile(instance)
existing.renderKey = readComponentRenderKey(instance)
existing.renderKey =
readComponentRenderKey(instance, overrides.renderKey) ??
(isVaporInstance(instance) ? existing.renderKey : undefined)
existing.inactive = overrides.inactive ?? isComponentInactive(instance)
existing.isFragment = isFragmentComponent(instance, app)
existing.hidden = isComponentHidden(instance)
Expand All @@ -239,7 +252,7 @@ export class RuntimeRegistry {
parentId,
name: readComponentName(instance),
file: readComponentFile(instance),
renderKey: readComponentRenderKey(instance),
renderKey: readComponentRenderKey(instance, overrides.renderKey),
inactive: overrides.inactive ?? isComponentInactive(instance),
isFragment: isFragmentComponent(instance, app),
hidden: isComponentHidden(instance),
Expand Down Expand Up @@ -296,6 +309,7 @@ export class RuntimeRegistry {
walk(child.instance, instance, depth + 1, {
inactive: overrides.inactive || child.inactive,
tags: child.tags,
renderKey: child.renderKey,
})
}

Expand Down Expand Up @@ -413,6 +427,7 @@ export class RuntimeRegistry {
{
inactive: parent.inactive || child.inactive,
tags: child.tags,
renderKey: child.renderKey,
},
cache,
)
Expand Down Expand Up @@ -484,11 +499,17 @@ function readComponentFile(instance: object): string | undefined {
return type ? readStringProperty(type, '__file') : undefined
}

function readComponentRenderKey(instance: object): string | number | undefined {
const vnode = readObjectProperty(instance, 'vnode')
if (!vnode) return

const value = readUnknownProperty(vnode, 'key')
function readComponentRenderKey(
instance: object,
walkedKey?: unknown,
): string | number | undefined {
let value = walkedKey
if (value == null) {
const vapor = isVaporInstance(instance)
const keyOwner = vapor ? instance : readObjectProperty(instance, 'vnode')
if (!keyOwner) return
value = readUnknownProperty(keyOwner, vapor ? '$key' : 'key')
}
if (value == null) return
if (typeof value === 'number') return value
if (typeof value === 'string') return `'${value}'`
Expand All @@ -508,6 +529,8 @@ function isComponentInactive(instance: object): boolean {
}

function isFragmentComponent(instance: object, app: AppRecord): boolean {
if (isVaporInstance(instance)) return isVaporFragmentRoot(readUnknownProperty(instance, 'block'))

const subtree = readObjectProperty(instance, 'subTree')
if (!subtree) return false
return readUnknownProperty(subtree, 'type') === app.vueTypes?.Fragment
Expand Down Expand Up @@ -585,11 +608,56 @@ function readComponentDomOrder(
return indexList.reverse()
}

// Mirrors VDOM: a root renders as a fragment when it is a list or a `v-for`.
function isVaporFragmentRoot(block: unknown): boolean {
if (Array.isArray(block)) return block.length > 1 || isVaporFragmentRoot(block[0])
if (!isVaporFragment(block)) return false
if (isInteropFragment(block)) return false
return isForFragment(block) || isVaporFragmentRoot(readUnknownProperty(block, 'nodes'))
}

function getInternalInstanceChildren(instance: object): WalkedComponentChild[] {
if (isVaporInstance(instance)) {
const block =
readUnknownProperty(instance, 'pendingBlock') ?? readUnknownProperty(instance, 'block')
return getInternalInstanceChildrenFromBlock(block)
}
const subtree = readObjectProperty(instance, 'subTree')
return subtree ? getInternalInstanceChildrenFromVNode(subtree) : []
}

const TELEPORT_TAG: ComponentTreeNodeTag = {
label: 'teleport',
textColor: 0xffffff,
backgroundColor: 0x8b5cf6,
}

// Walks a Vapor `Block` (Node | VaporComponentInstance | VaporFragment | Block[]).
function getInternalInstanceChildrenFromBlock(
block: unknown,
tags: ComponentTreeNodeTag[] = [],
renderKey?: unknown,
): WalkedComponentChild[] {
if (block == null || typeof block !== 'object') return []
if (Array.isArray(block)) {
return block.flatMap((child) => getInternalInstanceChildrenFromBlock(child, tags, renderKey))
}
if (isVaporInstance(block)) {
return !isBeingDestroyed(block) && !isComponentHidden(block)
? [{ instance: block, tags, renderKey }]
: []
}
if (!isVaporFragment(block)) return []
if (isInteropFragment(block)) {
return block.vnode ? getInternalInstanceChildrenFromVNode(block.vnode, tags) : []
}
const nextTags =
isTeleportFragment(block) && !tags.includes(TELEPORT_TAG) ? [...tags, TELEPORT_TAG] : tags
// Only a component directly inside a `v-for` item takes the item key.
const nextKey = isForBlock(block) ? readUnknownProperty(block, 'key') : undefined
return getInternalInstanceChildrenFromBlock(block.nodes, nextTags, nextKey)
}

function getInternalInstanceChildrenFromVNode(
vnode: object,
tags: ComponentTreeNodeTag[] = [],
Expand Down Expand Up @@ -639,12 +707,14 @@ function getKeepAliveCachedInstances(instance: object): InstanceRef[] {
const cache = readUnknownProperty(instance, '__v_cache')
if (!(cache instanceof Map)) return []

// VDOM caches vnodes; Vapor caches instances or interop fragments wrapping a vnode.
return Array.from(cache.values())
.map((vnode) =>
vnode != null && typeof vnode === 'object'
? readObjectProperty(vnode, 'component')
: undefined,
)
.map((entry) => {
if (entry == null || typeof entry !== 'object') return
if (isVaporInstance(entry)) return entry
const vnode = isInteropFragment(entry) ? entry.vnode : entry
return vnode ? readObjectProperty(vnode, 'component') : undefined
})
.filter((component): component is InstanceRef => !!component)
}

Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/runtime/vapor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export function isForFragment(value: unknown): value is VaporFragmentLike {
return hasFragmentBit(value, VF.FOR)
}

export function isForBlock(value: unknown): value is VaporFragmentLike {
return hasFragmentBit(value, VF.FOR_BLOCK)
}

function hasFragmentBit(value: unknown, bit: number): boolean {
return isVaporFragment(value) && (value.__vf & bit) !== 0
}
Expand Down
18 changes: 10 additions & 8 deletions plans/vapor-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Tick each item when it lands. Phase 0 landed in #1174.

### Phase 1 — Component tree (core feature)

- [ ] Generalise `getInternalInstanceChildren(instance)`:
- [x] Generalise `getInternalInstanceChildren(instance)`:
- VDOM: unchanged (`subTree`).
- Vapor: `walkBlockForChildren(instance.block)`:
- `Node` → skip
Expand All @@ -104,15 +104,17 @@ Tick each item when it lands. Phase 0 landed in #1174.
- fragment with bit 64 → `getInternalInstanceChildrenFromVNode(frag.vnode)`
- other fragment → recurse `nodes` (teleport included)
- pending suspense → `instance.pendingBlock` if present
- [ ] In `getInternalInstanceChildrenFromVNode`, a `vnode.component` that is Vapor already
- [x] In `getInternalInstanceChildrenFromVNode`, a `vnode.component` that is Vapor already
works (it's returned as the child) — the Vapor walk above then descends from it.
- [ ] Skip the interop wrapper on `vnode.vi`; collapse `VaporAsyncComponentWrapper` the same
way VDOM async wrappers are treated.
- [ ] KeepAlive: relax `isKeepAlive` to accept `type.__isKeepAlive` on Vapor instances;
- [x] Skip the interop wrapper on `vnode.vi`; collapse `VaporAsyncComponentWrapper` the same
way VDOM async wrappers are treated. Nothing needed: `vnode.vi` is a factory function, so
the wrapper is never reached, and VDOM does not collapse async wrappers either.
- [x] KeepAlive: relax `isKeepAlive` to accept `type.__isKeepAlive` on Vapor instances;
for cache entries use `entry.vapor ? entry : entry.vnode?.component`.
- [ ] Fragment flag for Vapor: `Array.isArray(block)` or a multi-node fragment root.
Render key: `instance.$key` if present.
- [ ] Tags: add `teleport` tag for children found under a teleport fragment, and
- [x] Fragment flag for Vapor: `Array.isArray(block)` or a multi-node fragment root.
Render key: `instance.$key` if present. `v-for` keys live on the `ForBlock`, so the
walk passes them down as `WalkedComponentChild.renderKey`.
- [x] Tags: add `teleport` tag for children found under a teleport fragment, and
`v-for`/`v-if` are not shown (same as VDOM).

### Phase 2 — Tree liveness (no add/remove events)
Expand Down
Loading
Loading