warning.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import type { VNode } from './vnode'
  2. import {
  3. type ComponentInternalInstance,
  4. type ConcreteComponent,
  5. type Data,
  6. formatComponentName,
  7. } from './component'
  8. import { isFunction, isString } from '@vue/shared'
  9. import { isRef, pauseTracking, resetTracking, toRaw } from '@vue/reactivity'
  10. import { ErrorCodes, callWithErrorHandling } from './errorHandling'
  11. type ComponentVNode = VNode & {
  12. type: ConcreteComponent
  13. }
  14. const stack: VNode[] = []
  15. type TraceEntry = {
  16. vnode: ComponentVNode
  17. recurseCount: number
  18. }
  19. type ComponentTraceStack = TraceEntry[]
  20. export function pushWarningContext(vnode: VNode) {
  21. stack.push(vnode)
  22. }
  23. export function popWarningContext() {
  24. stack.pop()
  25. }
  26. let isWarning = false
  27. export function warn(msg: string, ...args: any[]) {
  28. if (isWarning) return
  29. isWarning = true
  30. // avoid props formatting or warn handler tracking deps that might be mutated
  31. // during patch, leading to infinite recursion.
  32. pauseTracking()
  33. const instance = stack.length ? stack[stack.length - 1].component : null
  34. const appWarnHandler = instance && instance.appContext.config.warnHandler
  35. const trace = getComponentTrace()
  36. if (appWarnHandler) {
  37. callWithErrorHandling(
  38. appWarnHandler,
  39. instance,
  40. ErrorCodes.APP_WARN_HANDLER,
  41. [
  42. // eslint-disable-next-line no-restricted-syntax
  43. msg + args.map(a => a.toString?.() ?? JSON.stringify(a)).join(''),
  44. instance && instance.proxy,
  45. trace
  46. .map(
  47. ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>`,
  48. )
  49. .join('\n'),
  50. trace,
  51. ],
  52. )
  53. } else {
  54. const warnArgs = [`[Vue warn]: ${msg}`, ...args]
  55. /* istanbul ignore if */
  56. if (
  57. trace.length &&
  58. // avoid spamming console during tests
  59. !__TEST__
  60. ) {
  61. warnArgs.push(`\n`, ...formatTrace(trace))
  62. }
  63. console.warn(...warnArgs)
  64. }
  65. resetTracking()
  66. isWarning = false
  67. }
  68. export function getComponentTrace(): ComponentTraceStack {
  69. let currentVNode: VNode | null = stack[stack.length - 1]
  70. if (!currentVNode) {
  71. return []
  72. }
  73. // we can't just use the stack because it will be incomplete during updates
  74. // that did not start from the root. Re-construct the parent chain using
  75. // instance parent pointers.
  76. const normalizedStack: ComponentTraceStack = []
  77. while (currentVNode) {
  78. const last = normalizedStack[0]
  79. if (last && last.vnode === currentVNode) {
  80. last.recurseCount++
  81. } else {
  82. normalizedStack.push({
  83. vnode: currentVNode as ComponentVNode,
  84. recurseCount: 0,
  85. })
  86. }
  87. const parentInstance: ComponentInternalInstance | null =
  88. currentVNode.component && currentVNode.component.parent
  89. currentVNode = parentInstance && parentInstance.vnode
  90. }
  91. return normalizedStack
  92. }
  93. /* istanbul ignore next */
  94. function formatTrace(trace: ComponentTraceStack): any[] {
  95. const logs: any[] = []
  96. trace.forEach((entry, i) => {
  97. logs.push(...(i === 0 ? [] : [`\n`]), ...formatTraceEntry(entry))
  98. })
  99. return logs
  100. }
  101. function formatTraceEntry({ vnode, recurseCount }: TraceEntry): any[] {
  102. const postfix =
  103. recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
  104. const isRoot = vnode.component ? vnode.component.parent == null : false
  105. const open = ` at <${formatComponentName(
  106. vnode.component,
  107. vnode.type,
  108. isRoot,
  109. )}`
  110. const close = `>` + postfix
  111. return vnode.props
  112. ? [open, ...formatProps(vnode.props), close]
  113. : [open + close]
  114. }
  115. /* istanbul ignore next */
  116. function formatProps(props: Data): any[] {
  117. const res: any[] = []
  118. const keys = Object.keys(props)
  119. keys.slice(0, 3).forEach(key => {
  120. res.push(...formatProp(key, props[key]))
  121. })
  122. if (keys.length > 3) {
  123. res.push(` ...`)
  124. }
  125. return res
  126. }
  127. function formatProp(key: string, value: unknown): any[]
  128. function formatProp(key: string, value: unknown, raw: true): any
  129. /* istanbul ignore next */
  130. function formatProp(key: string, value: unknown, raw?: boolean): any {
  131. if (isString(value)) {
  132. value = JSON.stringify(value)
  133. return raw ? value : [`${key}=${value}`]
  134. } else if (
  135. typeof value === 'number' ||
  136. typeof value === 'boolean' ||
  137. value == null
  138. ) {
  139. return raw ? value : [`${key}=${value}`]
  140. } else if (isRef(value)) {
  141. value = formatProp(key, toRaw(value.value), true)
  142. return raw ? value : [`${key}=Ref<`, value, `>`]
  143. } else if (isFunction(value)) {
  144. return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]
  145. } else {
  146. value = toRaw(value)
  147. return raw ? value : [`${key}=`, value]
  148. }
  149. }
  150. /**
  151. * @internal
  152. */
  153. export function assertNumber(val: unknown, type: string) {
  154. if (!__DEV__) return
  155. if (val === undefined) {
  156. return
  157. } else if (typeof val !== 'number') {
  158. warn(`${type} is not a valid number - ` + `got ${JSON.stringify(val)}.`)
  159. } else if (isNaN(val)) {
  160. warn(`${type} is NaN - ` + 'the duration expression might be incorrect.')
  161. }
  162. }