devtools.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. /* eslint-disable no-restricted-globals */
  2. import type { App } from './apiCreateApp'
  3. import { Comment, Fragment, Static, Text } from './vnode'
  4. import type { ComponentInternalInstance } from './component'
  5. interface AppRecord {
  6. id: number
  7. app: App
  8. version: string
  9. types: Record<string, string | Symbol>
  10. }
  11. enum DevtoolsHooks {
  12. APP_INIT = 'app:init',
  13. APP_UNMOUNT = 'app:unmount',
  14. COMPONENT_UPDATED = 'component:updated',
  15. COMPONENT_ADDED = 'component:added',
  16. COMPONENT_REMOVED = 'component:removed',
  17. COMPONENT_EMIT = 'component:emit',
  18. PERFORMANCE_START = 'perf:start',
  19. PERFORMANCE_END = 'perf:end',
  20. }
  21. export interface DevtoolsHook {
  22. enabled?: boolean
  23. emit: (event: string, ...payload: any[]) => void
  24. on: (event: string, handler: Function) => void
  25. once: (event: string, handler: Function) => void
  26. off: (event: string, handler: Function) => void
  27. appRecords: AppRecord[]
  28. /**
  29. * Added at https://github.com/vuejs/devtools/commit/f2ad51eea789006ab66942e5a27c0f0986a257f9
  30. * Returns whether the arg was buffered or not
  31. */
  32. cleanupBuffer?: (matchArg: unknown) => boolean
  33. }
  34. export let devtools: DevtoolsHook
  35. let buffer: { event: string; args: any[] }[] = []
  36. let devtoolsNotInstalled = false
  37. function emit(event: string, ...args: any[]) {
  38. if (devtools) {
  39. devtools.emit(event, ...args)
  40. } else if (!devtoolsNotInstalled) {
  41. buffer.push({ event, args })
  42. }
  43. }
  44. export function setDevtoolsHook(hook: DevtoolsHook, target: any): void {
  45. devtools = hook
  46. if (devtools) {
  47. devtools.enabled = true
  48. buffer.forEach(({ event, args }) => devtools.emit(event, ...args))
  49. buffer = []
  50. } else if (
  51. // handle late devtools injection - only do this if we are in an actual
  52. // browser environment to avoid the timer handle stalling test runner exit
  53. // (#4815)
  54. typeof window !== 'undefined' &&
  55. // some envs mock window but not fully
  56. window.HTMLElement &&
  57. // also exclude jsdom
  58. // eslint-disable-next-line no-restricted-syntax
  59. !window.navigator?.userAgent?.includes('jsdom')
  60. ) {
  61. const replay = (target.__VUE_DEVTOOLS_HOOK_REPLAY__ =
  62. target.__VUE_DEVTOOLS_HOOK_REPLAY__ || [])
  63. replay.push((newHook: DevtoolsHook) => {
  64. setDevtoolsHook(newHook, target)
  65. })
  66. // clear buffer after 3s - the user probably doesn't have devtools installed
  67. // at all, and keeping the buffer will cause memory leaks (#4738)
  68. setTimeout(() => {
  69. if (!devtools) {
  70. target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null
  71. devtoolsNotInstalled = true
  72. buffer = []
  73. }
  74. }, 3000)
  75. } else {
  76. // non-browser env, assume not installed
  77. devtoolsNotInstalled = true
  78. buffer = []
  79. }
  80. }
  81. export function devtoolsInitApp(app: App, version: string): void {
  82. emit(DevtoolsHooks.APP_INIT, app, version, {
  83. Fragment,
  84. Text,
  85. Comment,
  86. Static,
  87. })
  88. }
  89. export function devtoolsUnmountApp(app: App): void {
  90. emit(DevtoolsHooks.APP_UNMOUNT, app)
  91. }
  92. export const devtoolsComponentAdded: DevtoolsComponentHook =
  93. /*@__PURE__*/ createDevtoolsComponentHook(DevtoolsHooks.COMPONENT_ADDED)
  94. export const devtoolsComponentUpdated: DevtoolsComponentHook =
  95. /*@__PURE__*/ createDevtoolsComponentHook(DevtoolsHooks.COMPONENT_UPDATED)
  96. const _devtoolsComponentRemoved = /*@__PURE__*/ createDevtoolsComponentHook(
  97. DevtoolsHooks.COMPONENT_REMOVED,
  98. )
  99. export const devtoolsComponentRemoved = (
  100. component: ComponentInternalInstance,
  101. ): void => {
  102. if (
  103. devtools &&
  104. typeof devtools.cleanupBuffer === 'function' &&
  105. // remove the component if it wasn't buffered
  106. !devtools.cleanupBuffer(component)
  107. ) {
  108. _devtoolsComponentRemoved(component)
  109. }
  110. }
  111. type DevtoolsComponentHook = (component: ComponentInternalInstance) => void
  112. /*! #__NO_SIDE_EFFECTS__ */
  113. function createDevtoolsComponentHook(
  114. hook: DevtoolsHooks,
  115. ): DevtoolsComponentHook {
  116. return (component: ComponentInternalInstance) => {
  117. emit(
  118. hook,
  119. component.appContext.app,
  120. component.uid,
  121. component.parent ? component.parent.uid : undefined,
  122. component,
  123. )
  124. }
  125. }
  126. export const devtoolsPerfStart: DevtoolsPerformanceHook =
  127. /*@__PURE__*/ createDevtoolsPerformanceHook(DevtoolsHooks.PERFORMANCE_START)
  128. export const devtoolsPerfEnd: DevtoolsPerformanceHook =
  129. /*@__PURE__*/ createDevtoolsPerformanceHook(DevtoolsHooks.PERFORMANCE_END)
  130. type DevtoolsPerformanceHook = (
  131. component: ComponentInternalInstance,
  132. type: string,
  133. time: number,
  134. ) => void
  135. function createDevtoolsPerformanceHook(
  136. hook: DevtoolsHooks,
  137. ): DevtoolsPerformanceHook {
  138. return (component: ComponentInternalInstance, type: string, time: number) => {
  139. emit(hook, component.appContext.app, component.uid, component, type, time)
  140. }
  141. }
  142. export function devtoolsComponentEmit(
  143. component: ComponentInternalInstance,
  144. event: string,
  145. params: any[],
  146. ): void {
  147. emit(
  148. DevtoolsHooks.COMPONENT_EMIT,
  149. component.appContext.app,
  150. component,
  151. event,
  152. params,
  153. )
  154. }