hmr.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /* eslint-disable no-restricted-globals */
  2. import {
  3. type ClassComponent,
  4. type ComponentInternalInstance,
  5. type ComponentOptions,
  6. type ConcreteComponent,
  7. type InternalRenderFunction,
  8. isClassComponent,
  9. } from './component'
  10. import { SchedulerJobFlags, queueJob, queuePostFlushCb } from './scheduler'
  11. import { extend, getGlobalThis } from '@vue/shared'
  12. type HMRComponent = ComponentOptions | ClassComponent
  13. export let isHmrUpdating = false
  14. export const setHmrUpdating = (v: boolean): boolean => {
  15. try {
  16. return isHmrUpdating
  17. } finally {
  18. isHmrUpdating = v
  19. }
  20. }
  21. export const hmrDirtyComponents: Map<
  22. ConcreteComponent,
  23. Set<ComponentInternalInstance>
  24. > = new Map<ConcreteComponent, Set<ComponentInternalInstance>>()
  25. export interface HMRRuntime {
  26. createRecord: typeof createRecord
  27. rerender: typeof rerender
  28. reload: typeof reload
  29. }
  30. // Expose the HMR runtime on the global object
  31. // This makes it entirely tree-shakable without polluting the exports and makes
  32. // it easier to be used in toolings like vue-loader
  33. // Note: for a component to be eligible for HMR it also needs the __hmrId option
  34. // to be set so that its instances can be registered / removed.
  35. if (__DEV__) {
  36. getGlobalThis().__VUE_HMR_RUNTIME__ = {
  37. createRecord: tryWrap(createRecord),
  38. rerender: tryWrap(rerender),
  39. reload: tryWrap(reload),
  40. } as HMRRuntime
  41. }
  42. const map: Map<
  43. string,
  44. {
  45. // the initial component definition is recorded on import - this allows us
  46. // to apply hot updates to the component even when there are no actively
  47. // rendered instance.
  48. initialDef: ComponentOptions
  49. instances: Set<ComponentInternalInstance>
  50. }
  51. > = new Map()
  52. export function registerHMR(instance: ComponentInternalInstance): void {
  53. const id = instance.type.__hmrId!
  54. let record = map.get(id)
  55. if (!record) {
  56. createRecord(id, instance.type as HMRComponent)
  57. record = map.get(id)!
  58. }
  59. record.instances.add(instance)
  60. }
  61. export function unregisterHMR(instance: ComponentInternalInstance): void {
  62. map.get(instance.type.__hmrId!)!.instances.delete(instance)
  63. }
  64. function createRecord(id: string, initialDef: HMRComponent): boolean {
  65. if (map.has(id)) {
  66. return false
  67. }
  68. map.set(id, {
  69. initialDef: normalizeClassComponent(initialDef),
  70. instances: new Set(),
  71. })
  72. return true
  73. }
  74. function normalizeClassComponent(component: HMRComponent): ComponentOptions {
  75. return isClassComponent(component) ? component.__vccOpts : component
  76. }
  77. function rerender(id: string, newRender?: Function): void {
  78. const record = map.get(id)
  79. if (!record) {
  80. return
  81. }
  82. // update initial record (for not-yet-rendered component)
  83. record.initialDef.render = newRender
  84. // Create a snapshot which avoids the set being mutated during updates
  85. ;[...record.instances].forEach(instance => {
  86. if (newRender) {
  87. instance.render = newRender as InternalRenderFunction
  88. normalizeClassComponent(instance.type as HMRComponent).render = newRender
  89. }
  90. instance.renderCache = []
  91. // this flag forces child components with slot content to update
  92. isHmrUpdating = true
  93. // #13771 don't update if the job is already disposed
  94. if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
  95. instance.update()
  96. }
  97. isHmrUpdating = false
  98. })
  99. }
  100. function reload(id: string, newComp: HMRComponent): void {
  101. const record = map.get(id)
  102. if (!record) return
  103. newComp = normalizeClassComponent(newComp)
  104. // update initial def (for not-yet-rendered components)
  105. updateComponentDef(record.initialDef, newComp)
  106. // create a snapshot which avoids the set being mutated during updates
  107. const instances = [...record.instances]
  108. for (let i = 0; i < instances.length; i++) {
  109. const instance = instances[i]
  110. const oldComp = normalizeClassComponent(instance.type as HMRComponent)
  111. let dirtyInstances = hmrDirtyComponents.get(oldComp)
  112. if (!dirtyInstances) {
  113. // 1. Update existing comp definition to match new one
  114. if (oldComp !== record.initialDef) {
  115. updateComponentDef(oldComp, newComp)
  116. }
  117. // 2. mark definition dirty. This forces the renderer to replace the
  118. // component on patch.
  119. hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
  120. }
  121. dirtyInstances.add(instance)
  122. // 3. invalidate options resolution cache
  123. instance.appContext.propsCache.delete(instance.type as any)
  124. instance.appContext.emitsCache.delete(instance.type as any)
  125. instance.appContext.optionsCache.delete(instance.type as any)
  126. // 4. actually update
  127. if (instance.ceReload) {
  128. // custom element
  129. dirtyInstances.add(instance)
  130. instance.ceReload((newComp as any).styles)
  131. dirtyInstances.delete(instance)
  132. } else if (instance.parent) {
  133. // 4. Force the parent instance to re-render. This will cause all updated
  134. // components to be unmounted and re-mounted. Queue the update so that we
  135. // don't end up forcing the same parent to re-render multiple times.
  136. queueJob(() => {
  137. // vite-plugin-vue/issues/599
  138. // don't update if the job is already disposed
  139. if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
  140. isHmrUpdating = true
  141. instance.parent!.update()
  142. isHmrUpdating = false
  143. // #6930, #11248 avoid infinite recursion
  144. dirtyInstances.delete(instance)
  145. }
  146. })
  147. } else if (instance.appContext.reload) {
  148. // root instance mounted via createApp() has a reload method
  149. instance.appContext.reload()
  150. } else if (typeof window !== 'undefined') {
  151. // root instance inside tree created via raw render(). Force reload.
  152. window.location.reload()
  153. } else {
  154. console.warn(
  155. '[HMR] Root or manually mounted instance modified. Full reload required.',
  156. )
  157. }
  158. // update custom element child style
  159. if (instance.root.ce && instance !== instance.root) {
  160. instance.root.ce._removeChildStyle(oldComp)
  161. }
  162. }
  163. // 5. make sure to cleanup dirty hmr components after update
  164. queuePostFlushCb(() => {
  165. hmrDirtyComponents.clear()
  166. })
  167. }
  168. function updateComponentDef(
  169. oldComp: ComponentOptions,
  170. newComp: ComponentOptions,
  171. ) {
  172. extend(oldComp, newComp)
  173. for (const key in oldComp) {
  174. if (key !== '__file' && !(key in newComp)) {
  175. delete oldComp[key]
  176. }
  177. }
  178. }
  179. function tryWrap(fn: (id: string, arg: any) => any): Function {
  180. return (id: string, arg: any) => {
  181. try {
  182. return fn(id, arg)
  183. } catch (e: any) {
  184. console.error(e)
  185. console.warn(
  186. `[HMR] Something went wrong during Vue component hot-reload. ` +
  187. `Full reload required.`,
  188. )
  189. }
  190. }
  191. }