fix(hmr): handle possible duplicate component definitions with same id
fixes regression in vitepress
This commit is contained in:
parent
96b531bfa3
commit
aa8908a854
@ -36,9 +36,9 @@ describe('hot module replacement', () => {
|
||||
})
|
||||
|
||||
test('createRecord', () => {
|
||||
expect(createRecord('test1', {})).toBe(true)
|
||||
expect(createRecord('test1')).toBe(true)
|
||||
// if id has already been created, should return false
|
||||
expect(createRecord('test1', {})).toBe(false)
|
||||
expect(createRecord('test1')).toBe(false)
|
||||
})
|
||||
|
||||
test('rerender', async () => {
|
||||
@ -50,7 +50,7 @@ describe('hot module replacement', () => {
|
||||
__hmrId: childId,
|
||||
render: compileToFunction(`<div><slot/></div>`)
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
createRecord(childId)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
__hmrId: parentId,
|
||||
@ -62,7 +62,7 @@ describe('hot module replacement', () => {
|
||||
`<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
|
||||
)
|
||||
}
|
||||
createRecord(parentId, Parent)
|
||||
createRecord(parentId)
|
||||
|
||||
render(h(Parent), root)
|
||||
expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`)
|
||||
@ -128,7 +128,7 @@ describe('hot module replacement', () => {
|
||||
unmounted: unmountSpy,
|
||||
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
createRecord(childId)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
render: () => h(Child)
|
||||
@ -167,7 +167,7 @@ describe('hot module replacement', () => {
|
||||
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
|
||||
}
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
createRecord(childId)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
render: () => h(Child)
|
||||
@ -212,7 +212,7 @@ describe('hot module replacement', () => {
|
||||
},
|
||||
render: compileToFunction(template)
|
||||
}
|
||||
createRecord(id, Comp)
|
||||
createRecord(id)
|
||||
|
||||
render(h(Comp), root)
|
||||
expect(serializeInner(root)).toBe(
|
||||
@ -249,14 +249,14 @@ describe('hot module replacement', () => {
|
||||
},
|
||||
render: compileToFunction(`<div>{{ msg }}</div>`)
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
createRecord(childId)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
__hmrId: parentId,
|
||||
components: { Child },
|
||||
render: compileToFunction(`<Child msg="foo" />`)
|
||||
}
|
||||
createRecord(parentId, Parent)
|
||||
createRecord(parentId)
|
||||
|
||||
render(h(Parent), root)
|
||||
expect(serializeInner(root)).toBe(`<div>foo</div>`)
|
||||
@ -275,14 +275,14 @@ describe('hot module replacement', () => {
|
||||
__hmrId: childId,
|
||||
render: compileToFunction(`<div>child</div>`)
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
createRecord(childId)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
__hmrId: parentId,
|
||||
components: { Child },
|
||||
render: compileToFunction(`<Child class="test" />`)
|
||||
}
|
||||
createRecord(parentId, Parent)
|
||||
createRecord(parentId)
|
||||
|
||||
render(h(Parent), root)
|
||||
expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
|
||||
@ -302,7 +302,7 @@ describe('hot module replacement', () => {
|
||||
__hmrId: childId,
|
||||
render: compileToFunction(`<div>child</div>`)
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
createRecord(childId)
|
||||
|
||||
const components: ComponentOptions[] = []
|
||||
|
||||
@ -324,7 +324,7 @@ describe('hot module replacement', () => {
|
||||
}
|
||||
}
|
||||
|
||||
createRecord(parentId, parentComp)
|
||||
createRecord(parentId)
|
||||
}
|
||||
|
||||
const last = components[components.length - 1]
|
||||
@ -370,7 +370,7 @@ describe('hot module replacement', () => {
|
||||
</Child>
|
||||
`)
|
||||
}
|
||||
createRecord(parentId, Parent)
|
||||
createRecord(parentId)
|
||||
|
||||
render(h(Parent), root)
|
||||
expect(serializeInner(root)).toBe(
|
||||
@ -410,7 +410,7 @@ describe('hot module replacement', () => {
|
||||
return h('div')
|
||||
}
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
createRecord(childId)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
render: () => h(Child)
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
} from './component'
|
||||
import { queueJob, queuePostFlushCb } from './scheduler'
|
||||
import { extend } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
|
||||
export let isHmrUpdating = false
|
||||
|
||||
@ -43,58 +42,46 @@ if (__DEV__) {
|
||||
} as HMRRuntime
|
||||
}
|
||||
|
||||
type HMRRecord = {
|
||||
component: ComponentOptions
|
||||
instances: Set<ComponentInternalInstance>
|
||||
}
|
||||
|
||||
const map: Map<string, HMRRecord> = new Map()
|
||||
const map: Map<string, Set<ComponentInternalInstance>> = new Map()
|
||||
|
||||
export function registerHMR(instance: ComponentInternalInstance) {
|
||||
const id = instance.type.__hmrId!
|
||||
let record = map.get(id)
|
||||
if (!record) {
|
||||
createRecord(id, instance.type as ComponentOptions)
|
||||
createRecord(id)
|
||||
record = map.get(id)!
|
||||
}
|
||||
record.instances.add(instance)
|
||||
record.add(instance)
|
||||
}
|
||||
|
||||
export function unregisterHMR(instance: ComponentInternalInstance) {
|
||||
map.get(instance.type.__hmrId!)!.instances.delete(instance)
|
||||
map.get(instance.type.__hmrId!)!.delete(instance)
|
||||
}
|
||||
|
||||
function createRecord(
|
||||
id: string,
|
||||
component: ComponentOptions | ClassComponent
|
||||
): boolean {
|
||||
if (!component) {
|
||||
warn(
|
||||
`HMR API usage is out of date.\n` +
|
||||
`Please upgrade vue-loader/vite/rollup-plugin-vue or other relevant ` +
|
||||
`dependency that handles Vue SFC compilation.`
|
||||
)
|
||||
component = {}
|
||||
}
|
||||
function createRecord(id: string): boolean {
|
||||
if (map.has(id)) {
|
||||
return false
|
||||
}
|
||||
map.set(id, {
|
||||
component: isClassComponent(component) ? component.__vccOpts : component,
|
||||
instances: new Set()
|
||||
})
|
||||
map.set(id, new Set())
|
||||
return true
|
||||
}
|
||||
|
||||
type HMRComponent = ComponentOptions | ClassComponent
|
||||
|
||||
function normalizeClassComponent(component: HMRComponent): ComponentOptions {
|
||||
return isClassComponent(component) ? component.__vccOpts : component
|
||||
}
|
||||
|
||||
function rerender(id: string, newRender?: Function) {
|
||||
const record = map.get(id)
|
||||
if (!record) return
|
||||
if (newRender) record.component.render = newRender
|
||||
// Array.from creates a snapshot which avoids the set being mutated during
|
||||
// updates
|
||||
Array.from(record.instances).forEach(instance => {
|
||||
if (!record) {
|
||||
return
|
||||
}
|
||||
// Create a snapshot which avoids the set being mutated during updates
|
||||
;[...record].forEach(instance => {
|
||||
if (newRender) {
|
||||
instance.render = newRender as InternalRenderFunction
|
||||
normalizeClassComponent(instance.type as HMRComponent).render = newRender
|
||||
}
|
||||
instance.renderCache = []
|
||||
// this flag forces child components with slot content to update
|
||||
@ -104,40 +91,40 @@ function rerender(id: string, newRender?: Function) {
|
||||
})
|
||||
}
|
||||
|
||||
function reload(id: string, newComp: ComponentOptions | ClassComponent) {
|
||||
function reload(id: string, newComp: HMRComponent) {
|
||||
const record = map.get(id)
|
||||
if (!record) return
|
||||
// Array.from creates a snapshot which avoids the set being mutated during
|
||||
// updates
|
||||
const { component, instances } = record
|
||||
|
||||
if (!hmrDirtyComponents.has(component)) {
|
||||
newComp = normalizeClassComponent(newComp)
|
||||
|
||||
// create a snapshot which avoids the set being mutated during updates
|
||||
const instances = [...record]
|
||||
|
||||
for (const instance of instances) {
|
||||
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
|
||||
|
||||
if (!hmrDirtyComponents.has(oldComp)) {
|
||||
// 1. Update existing comp definition to match new one
|
||||
newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp
|
||||
extend(component, newComp)
|
||||
for (const key in component) {
|
||||
extend(oldComp, newComp)
|
||||
for (const key in oldComp) {
|
||||
if (key !== '__file' && !(key in newComp)) {
|
||||
delete (component as any)[key]
|
||||
delete (oldComp as any)[key]
|
||||
}
|
||||
}
|
||||
// 2. Mark component dirty. This forces the renderer to replace the component
|
||||
// on patch.
|
||||
hmrDirtyComponents.add(component)
|
||||
// 3. Make sure to unmark the component after the reload.
|
||||
queuePostFlushCb(() => {
|
||||
hmrDirtyComponents.delete(component)
|
||||
})
|
||||
// 2. mark definition dirty. This forces the renderer to replace the
|
||||
// component on patch.
|
||||
hmrDirtyComponents.add(oldComp)
|
||||
}
|
||||
|
||||
Array.from(instances).forEach(instance => {
|
||||
// invalidate options resolution cache
|
||||
// 3. invalidate options resolution cache
|
||||
instance.appContext.optionsCache.delete(instance.type as any)
|
||||
|
||||
// 4. actually update
|
||||
if (instance.ceReload) {
|
||||
// custom element
|
||||
hmrDirtyComponents.add(component)
|
||||
hmrDirtyComponents.add(oldComp)
|
||||
instance.ceReload((newComp as any).styles)
|
||||
hmrDirtyComponents.delete(component)
|
||||
hmrDirtyComponents.delete(oldComp)
|
||||
} else if (instance.parent) {
|
||||
// 4. Force the parent instance to re-render. This will cause all updated
|
||||
// components to be unmounted and re-mounted. Queue the update so that we
|
||||
@ -162,6 +149,15 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) {
|
||||
'[HMR] Root or manually mounted instance modified. Full reload required.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. make sure to cleanup dirty hmr components after update
|
||||
queuePostFlushCb(() => {
|
||||
for (const instance of instances) {
|
||||
hmrDirtyComponents.delete(
|
||||
normalizeClassComponent(instance.type as HMRComponent)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user