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)) {
 | 
			
		||||
    // 1. Update existing comp definition to match new one
 | 
			
		||||
    newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp
 | 
			
		||||
    extend(component, newComp)
 | 
			
		||||
    for (const key in component) {
 | 
			
		||||
      if (key !== '__file' && !(key in newComp)) {
 | 
			
		||||
        delete (component as any)[key]
 | 
			
		||||
  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
 | 
			
		||||
      extend(oldComp, newComp)
 | 
			
		||||
      for (const key in oldComp) {
 | 
			
		||||
        if (key !== '__file' && !(key in newComp)) {
 | 
			
		||||
          delete (oldComp as any)[key]
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // 2. mark definition dirty. This forces the renderer to replace the
 | 
			
		||||
      // component on patch.
 | 
			
		||||
      hmrDirtyComponents.add(oldComp)
 | 
			
		||||
    }
 | 
			
		||||
    // 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)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user