Skip to content

Commit 158e706

Browse files
authored
fix(hmr): track original __vapor state during component mode switching (#14187)
1 parent 918c7ad commit 158e706

File tree

4 files changed

+110
-8
lines changed

4 files changed

+110
-8
lines changed

packages/runtime-core/src/components/BaseTransition.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
2121
import { PatchFlags, ShapeFlags, isArray, isFunction } from '@vue/shared'
2222
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
2323
import { isTeleport } from './Teleport'
24-
import { type RendererElement, getVaporInterface } from '../renderer'
24+
import {
25+
type RendererElement,
26+
getVaporInterface,
27+
isVaporComponent,
28+
} from '../renderer'
2529
import { SchedulerJobFlags } from '../scheduler'
2630

2731
type Hook<T = () => void> = T | T[]
@@ -140,7 +144,7 @@ export const BaseTransitionPropsValidators: Record<string, any> = {
140144
}
141145

142146
const recursiveGetSubtree = (instance: ComponentInternalInstance): VNode => {
143-
const subTree = instance.type.__vapor
147+
const subTree = isVaporComponent(instance.type)
144148
? (instance as any).block
145149
: instance.subTree
146150
return subTree.component ? recursiveGetSubtree(subTree.component) : subTree
@@ -559,7 +563,7 @@ function getInnerChild(vnode: VNode): VNode | undefined {
559563

560564
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void {
561565
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
562-
if ((vnode.type as ConcreteComponent).__vapor) {
566+
if (isVaporComponent(vnode.type as ConcreteComponent)) {
563567
getVaporInterface(vnode.component, vnode).setTransitionHooks(
564568
vnode.component,
565569
hooks,

packages/runtime-core/src/hmr.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ export const hmrDirtyComponents: Map<
2020
Set<GenericComponentInstance>
2121
> = new Map<ConcreteComponent, Set<GenericComponentInstance>>()
2222

23+
// During HMR reload, `updateComponentDef` mutates the old component definition
24+
// with the new one's properties (including `__vapor`). This causes issues when
25+
// a component switches between vapor and vdom modes, because the renderer still
26+
// holds references to the old VNodes whose `type` now has an incorrect `__vapor`
27+
// value. This Map tracks the original `__vapor` state of dirty components so
28+
// that operations like unmount/move/getNextHostNode can use the correct mode.
29+
export const hmrDirtyComponentsMode: Map<ConcreteComponent, boolean> = new Map<
30+
ConcreteComponent,
31+
boolean
32+
>()
33+
2334
export interface HMRRuntime {
2435
createRecord: typeof createRecord
2536
rerender: typeof rerender
@@ -152,6 +163,7 @@ function reload(id: string, newComp: HMRComponent): void {
152163
hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
153164
}
154165
dirtyInstances.add(instance)
166+
hmrDirtyComponentsMode.set(oldComp, !!isVapor)
155167

156168
// 3. invalidate options resolution cache
157169
instance.appContext.propsCache.delete(instance.type as any)
@@ -206,6 +218,7 @@ function reload(id: string, newComp: HMRComponent): void {
206218
// 5. make sure to cleanup dirty hmr components after update
207219
queuePostFlushCb(() => {
208220
hmrDirtyComponents.clear()
221+
hmrDirtyComponentsMode.clear()
209222
})
210223
}
211224

packages/runtime-core/src/renderer.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ import {
8383
type TeleportVNode,
8484
} from './components/Teleport'
8585
import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive'
86-
import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr'
86+
import {
87+
hmrDirtyComponentsMode,
88+
isHmrUpdating,
89+
registerHMR,
90+
unregisterHMR,
91+
} from './hmr'
8792
import { type RootHydrateFunction, createHydrationFunctions } from './hydration'
8893
import { invokeDirectiveHook } from './directives'
8994
import { endMeasure, startMeasure } from './profiling'
@@ -2177,7 +2182,7 @@ function baseCreateRenderer(
21772182
) => {
21782183
const { el, type, transition, children, shapeFlag } = vnode
21792184
if (shapeFlag & ShapeFlags.COMPONENT) {
2180-
if ((type as ConcreteComponent).__vapor) {
2185+
if (isVaporComponent(type as ConcreteComponent)) {
21812186
getVaporInterface(parentComponent, vnode).move(vnode, container, anchor)
21822187
} else {
21832188
move(
@@ -2309,7 +2314,7 @@ function baseCreateRenderer(
23092314
}
23102315

23112316
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
2312-
if ((vnode.type as ConcreteComponent).__vapor) {
2317+
if (isVaporComponent(vnode.type as ConcreteComponent)) {
23132318
getVaporInterface(parentComponent!, vnode).deactivate(
23142319
vnode,
23152320
(parentComponent!.ctx as KeepAliveContext).getStorageContainer(),
@@ -2332,7 +2337,7 @@ function baseCreateRenderer(
23322337
}
23332338

23342339
if (shapeFlag & ShapeFlags.COMPONENT) {
2335-
if ((type as ConcreteComponent).__vapor) {
2340+
if (isVaporComponent(type as ConcreteComponent)) {
23362341
getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove)
23372342
return
23382343
} else {
@@ -2539,7 +2544,7 @@ function baseCreateRenderer(
25392544

25402545
const getNextHostNode: NextFn = vnode => {
25412546
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
2542-
if ((vnode.type as ConcreteComponent).__vapor) {
2547+
if (isVaporComponent(vnode.type as ConcreteComponent)) {
25432548
return hostNextSibling(vnode.anchor!)
25442549
}
25452550
return getNextHostNode(vnode.component!.subTree)
@@ -2828,6 +2833,13 @@ export function getVaporInterface(
28282833
return res!
28292834
}
28302835

2836+
export function isVaporComponent(type: ConcreteComponent): boolean | undefined {
2837+
if (__DEV__ && isHmrUpdating && hmrDirtyComponentsMode.has(type)) {
2838+
return hmrDirtyComponentsMode.get(type)
2839+
}
2840+
return type.__vapor
2841+
}
2842+
28312843
/**
28322844
* shared between vdom and vapor
28332845
*/

packages/runtime-vapor/__tests__/hmr.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
type HMRRuntime,
33
computed,
4+
createApp,
5+
h,
46
nextTick,
57
onActivated,
68
onDeactivated,
@@ -20,6 +22,7 @@ import {
2022
renderEffect,
2123
setText,
2224
template,
25+
vaporInteropPlugin,
2326
withVaporCtx,
2427
} from '@vue/runtime-vapor'
2528
import { BindingTypes } from '@vue/compiler-core'
@@ -1060,4 +1063,74 @@ describe('hot module replacement', () => {
10601063
`"<div>child changed2</div><div>root changed</div>"`,
10611064
)
10621065
})
1066+
1067+
describe('switch vapor/vdom modes', () => {
1068+
test('vapor -> vdom', async () => {
1069+
const id = 'vapor-to-vdom'
1070+
const Comp = {
1071+
__vapor: true,
1072+
__hmrId: id,
1073+
render() {
1074+
return template('<div>foo</div>')()
1075+
},
1076+
}
1077+
createRecord(id, Comp)
1078+
1079+
const App = {
1080+
render() {
1081+
return h(Comp as any)
1082+
},
1083+
}
1084+
const root = document.createElement('div')
1085+
const app = createApp(App)
1086+
app.use(vaporInteropPlugin)
1087+
app.mount(root)
1088+
expect(root.innerHTML).toBe('<div>foo</div>')
1089+
1090+
// switch to vdom
1091+
reload(id, {
1092+
__hmrId: id,
1093+
render() {
1094+
return h('div', 'bar')
1095+
},
1096+
})
1097+
1098+
await nextTick()
1099+
expect(root.innerHTML).toBe('<div>bar</div>')
1100+
})
1101+
1102+
test('vdom -> vapor', async () => {
1103+
const id = 'vdom-to-vapor'
1104+
const Comp = {
1105+
__hmrId: id,
1106+
render() {
1107+
return h('div', 'foo')
1108+
},
1109+
}
1110+
createRecord(id, Comp)
1111+
1112+
const App = {
1113+
render() {
1114+
return h(Comp)
1115+
},
1116+
}
1117+
const root = document.createElement('div')
1118+
const app = createApp(App)
1119+
app.use(vaporInteropPlugin)
1120+
app.mount(root)
1121+
expect(root.innerHTML).toBe('<div>foo</div>')
1122+
1123+
// switch to vapor
1124+
reload(id, {
1125+
__vapor: true,
1126+
__hmrId: id,
1127+
render() {
1128+
return template('<div>bar</div>')()
1129+
},
1130+
})
1131+
1132+
await nextTick()
1133+
expect(root.innerHTML).toBe('<div>bar</div>')
1134+
})
1135+
})
10631136
})

0 commit comments

Comments
 (0)