]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hmr): handle possible duplicate component definitions with same id
authorEvan You <yyx990803@gmail.com>
Wed, 8 Sep 2021 22:36:21 +0000 (18:36 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 8 Sep 2021 22:36:21 +0000 (18:36 -0400)
fixes regression in vitepress

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

index a5d0d67d51d0891b513994c637500f6130d294df..0a5821c8d755a34003afa0b4f039083c4114adae 100644 (file)
@@ -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)
index c2fbee65afe70848d469dbc6676ea5d8a9d5df0f..358fa843b9b1cf0fe6e0f5f4d3ace14d6657a8bc 100644 (file)
@@ -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)
+      )
+    }
   })
 }