Skip to content

Commit aa8908a

Browse files
committed
fix(hmr): handle possible duplicate component definitions with same id
fixes regression in vitepress
1 parent 96b531b commit aa8908a

File tree

2 files changed

+66
-70
lines changed

2 files changed

+66
-70
lines changed

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

+15-15
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ describe('hot module replacement', () => {
3636
})
3737

3838
test('createRecord', () => {
39-
expect(createRecord('test1', {})).toBe(true)
39+
expect(createRecord('test1')).toBe(true)
4040
// if id has already been created, should return false
41-
expect(createRecord('test1', {})).toBe(false)
41+
expect(createRecord('test1')).toBe(false)
4242
})
4343

4444
test('rerender', async () => {
@@ -50,7 +50,7 @@ describe('hot module replacement', () => {
5050
__hmrId: childId,
5151
render: compileToFunction(`<div><slot/></div>`)
5252
}
53-
createRecord(childId, Child)
53+
createRecord(childId)
5454

5555
const Parent: ComponentOptions = {
5656
__hmrId: parentId,
@@ -62,7 +62,7 @@ describe('hot module replacement', () => {
6262
`<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
6363
)
6464
}
65-
createRecord(parentId, Parent)
65+
createRecord(parentId)
6666

6767
render(h(Parent), root)
6868
expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`)
@@ -128,7 +128,7 @@ describe('hot module replacement', () => {
128128
unmounted: unmountSpy,
129129
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
130130
}
131-
createRecord(childId, Child)
131+
createRecord(childId)
132132

133133
const Parent: ComponentOptions = {
134134
render: () => h(Child)
@@ -167,7 +167,7 @@ describe('hot module replacement', () => {
167167
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
168168
}
169169
}
170-
createRecord(childId, Child)
170+
createRecord(childId)
171171

172172
const Parent: ComponentOptions = {
173173
render: () => h(Child)
@@ -212,7 +212,7 @@ describe('hot module replacement', () => {
212212
},
213213
render: compileToFunction(template)
214214
}
215-
createRecord(id, Comp)
215+
createRecord(id)
216216

217217
render(h(Comp), root)
218218
expect(serializeInner(root)).toBe(
@@ -249,14 +249,14 @@ describe('hot module replacement', () => {
249249
},
250250
render: compileToFunction(`<div>{{ msg }}</div>`)
251251
}
252-
createRecord(childId, Child)
252+
createRecord(childId)
253253

254254
const Parent: ComponentOptions = {
255255
__hmrId: parentId,
256256
components: { Child },
257257
render: compileToFunction(`<Child msg="foo" />`)
258258
}
259-
createRecord(parentId, Parent)
259+
createRecord(parentId)
260260

261261
render(h(Parent), root)
262262
expect(serializeInner(root)).toBe(`<div>foo</div>`)
@@ -275,14 +275,14 @@ describe('hot module replacement', () => {
275275
__hmrId: childId,
276276
render: compileToFunction(`<div>child</div>`)
277277
}
278-
createRecord(childId, Child)
278+
createRecord(childId)
279279

280280
const Parent: ComponentOptions = {
281281
__hmrId: parentId,
282282
components: { Child },
283283
render: compileToFunction(`<Child class="test" />`)
284284
}
285-
createRecord(parentId, Parent)
285+
createRecord(parentId)
286286

287287
render(h(Parent), root)
288288
expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
@@ -302,7 +302,7 @@ describe('hot module replacement', () => {
302302
__hmrId: childId,
303303
render: compileToFunction(`<div>child</div>`)
304304
}
305-
createRecord(childId, Child)
305+
createRecord(childId)
306306

307307
const components: ComponentOptions[] = []
308308

@@ -324,7 +324,7 @@ describe('hot module replacement', () => {
324324
}
325325
}
326326

327-
createRecord(parentId, parentComp)
327+
createRecord(parentId)
328328
}
329329

330330
const last = components[components.length - 1]
@@ -370,7 +370,7 @@ describe('hot module replacement', () => {
370370
</Child>
371371
`)
372372
}
373-
createRecord(parentId, Parent)
373+
createRecord(parentId)
374374

375375
render(h(Parent), root)
376376
expect(serializeInner(root)).toBe(
@@ -410,7 +410,7 @@ describe('hot module replacement', () => {
410410
return h('div')
411411
}
412412
}
413-
createRecord(childId, Child)
413+
createRecord(childId)
414414

415415
const Parent: ComponentOptions = {
416416
render: () => h(Child)

packages/runtime-core/src/hmr.ts

+51-55
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
} from './component'
1010
import { queueJob, queuePostFlushCb } from './scheduler'
1111
import { extend } from '@vue/shared'
12-
import { warn } from './warning'
1312

1413
export let isHmrUpdating = false
1514

@@ -43,58 +42,46 @@ if (__DEV__) {
4342
} as HMRRuntime
4443
}
4544

46-
type HMRRecord = {
47-
component: ComponentOptions
48-
instances: Set<ComponentInternalInstance>
49-
}
50-
51-
const map: Map<string, HMRRecord> = new Map()
45+
const map: Map<string, Set<ComponentInternalInstance>> = new Map()
5246

5347
export function registerHMR(instance: ComponentInternalInstance) {
5448
const id = instance.type.__hmrId!
5549
let record = map.get(id)
5650
if (!record) {
57-
createRecord(id, instance.type as ComponentOptions)
51+
createRecord(id)
5852
record = map.get(id)!
5953
}
60-
record.instances.add(instance)
54+
record.add(instance)
6155
}
6256

6357
export function unregisterHMR(instance: ComponentInternalInstance) {
64-
map.get(instance.type.__hmrId!)!.instances.delete(instance)
58+
map.get(instance.type.__hmrId!)!.delete(instance)
6559
}
6660

67-
function createRecord(
68-
id: string,
69-
component: ComponentOptions | ClassComponent
70-
): boolean {
71-
if (!component) {
72-
warn(
73-
`HMR API usage is out of date.\n` +
74-
`Please upgrade vue-loader/vite/rollup-plugin-vue or other relevant ` +
75-
`dependency that handles Vue SFC compilation.`
76-
)
77-
component = {}
78-
}
61+
function createRecord(id: string): boolean {
7962
if (map.has(id)) {
8063
return false
8164
}
82-
map.set(id, {
83-
component: isClassComponent(component) ? component.__vccOpts : component,
84-
instances: new Set()
85-
})
65+
map.set(id, new Set())
8666
return true
8767
}
8868

69+
type HMRComponent = ComponentOptions | ClassComponent
70+
71+
function normalizeClassComponent(component: HMRComponent): ComponentOptions {
72+
return isClassComponent(component) ? component.__vccOpts : component
73+
}
74+
8975
function rerender(id: string, newRender?: Function) {
9076
const record = map.get(id)
91-
if (!record) return
92-
if (newRender) record.component.render = newRender
93-
// Array.from creates a snapshot which avoids the set being mutated during
94-
// updates
95-
Array.from(record.instances).forEach(instance => {
77+
if (!record) {
78+
return
79+
}
80+
// Create a snapshot which avoids the set being mutated during updates
81+
;[...record].forEach(instance => {
9682
if (newRender) {
9783
instance.render = newRender as InternalRenderFunction
84+
normalizeClassComponent(instance.type as HMRComponent).render = newRender
9885
}
9986
instance.renderCache = []
10087
// this flag forces child components with slot content to update
@@ -104,40 +91,40 @@ function rerender(id: string, newRender?: Function) {
10491
})
10592
}
10693

107-
function reload(id: string, newComp: ComponentOptions | ClassComponent) {
94+
function reload(id: string, newComp: HMRComponent) {
10895
const record = map.get(id)
10996
if (!record) return
110-
// Array.from creates a snapshot which avoids the set being mutated during
111-
// updates
112-
const { component, instances } = record
113-
114-
if (!hmrDirtyComponents.has(component)) {
115-
// 1. Update existing comp definition to match new one
116-
newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp
117-
extend(component, newComp)
118-
for (const key in component) {
119-
if (key !== '__file' && !(key in newComp)) {
120-
delete (component as any)[key]
97+
98+
newComp = normalizeClassComponent(newComp)
99+
100+
// create a snapshot which avoids the set being mutated during updates
101+
const instances = [...record]
102+
103+
for (const instance of instances) {
104+
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
105+
106+
if (!hmrDirtyComponents.has(oldComp)) {
107+
// 1. Update existing comp definition to match new one
108+
extend(oldComp, newComp)
109+
for (const key in oldComp) {
110+
if (key !== '__file' && !(key in newComp)) {
111+
delete (oldComp as any)[key]
112+
}
121113
}
114+
// 2. mark definition dirty. This forces the renderer to replace the
115+
// component on patch.
116+
hmrDirtyComponents.add(oldComp)
122117
}
123-
// 2. Mark component dirty. This forces the renderer to replace the component
124-
// on patch.
125-
hmrDirtyComponents.add(component)
126-
// 3. Make sure to unmark the component after the reload.
127-
queuePostFlushCb(() => {
128-
hmrDirtyComponents.delete(component)
129-
})
130-
}
131118

132-
Array.from(instances).forEach(instance => {
133-
// invalidate options resolution cache
119+
// 3. invalidate options resolution cache
134120
instance.appContext.optionsCache.delete(instance.type as any)
135121

122+
// 4. actually update
136123
if (instance.ceReload) {
137124
// custom element
138-
hmrDirtyComponents.add(component)
125+
hmrDirtyComponents.add(oldComp)
139126
instance.ceReload((newComp as any).styles)
140-
hmrDirtyComponents.delete(component)
127+
hmrDirtyComponents.delete(oldComp)
141128
} else if (instance.parent) {
142129
// 4. Force the parent instance to re-render. This will cause all updated
143130
// components to be unmounted and re-mounted. Queue the update so that we
@@ -162,6 +149,15 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) {
162149
'[HMR] Root or manually mounted instance modified. Full reload required.'
163150
)
164151
}
152+
}
153+
154+
// 5. make sure to cleanup dirty hmr components after update
155+
queuePostFlushCb(() => {
156+
for (const instance of instances) {
157+
hmrDirtyComponents.delete(
158+
normalizeClassComponent(instance.type as HMRComponent)
159+
)
160+
}
165161
})
166162
}
167163

0 commit comments

Comments
 (0)