Просмотр исходного кода

feat(runtime-vapor): warning with component stack

三咲智子 Kevin Deng 2 лет назад
Родитель
Сommit
05f4ade4d9

+ 2 - 2
packages/runtime-vapor/__tests__/renderEffect.spec.ts

@@ -180,7 +180,7 @@ describe('renderEffect', () => {
     }).rejects.toThrow('error in beforeUpdate')
 
     expect(
-      '[Vue warn] Unhandled error during execution of beforeUpdate hook',
+      '[Vue warn]: Unhandled error during execution of beforeUpdate hook',
     ).toHaveBeenWarned()
   })
 
@@ -210,7 +210,7 @@ describe('renderEffect', () => {
     }).rejects.toThrow('error in updated')
 
     expect(
-      '[Vue warn] Unhandled error during execution of updated',
+      '[Vue warn]: Unhandled error during execution of updated',
     ).toHaveBeenWarned()
   })
 

+ 39 - 5
packages/runtime-vapor/src/component.ts

@@ -31,7 +31,10 @@ import type { Data } from '@vue/shared'
 export type Component = FunctionalComponent | ObjectComponent
 
 export type SetupFn = (props: any, ctx: SetupContext) => Block | Data | void
-export type FunctionalComponent = SetupFn & Omit<ObjectComponent, 'setup'>
+export type FunctionalComponent = SetupFn &
+  Omit<ObjectComponent, 'setup'> & {
+    displayName?: string
+  }
 
 export type SetupContext<E = EmitsOptions> = E extends any
   ? {
@@ -96,15 +99,46 @@ export function createSetupContext(
   }
 }
 
-export interface ObjectComponent {
-  props?: ComponentPropsOptions
+export interface ObjectComponent extends ComponentInternalOptions {
+  setup?: SetupFn
   inheritAttrs?: boolean
+  props?: ComponentPropsOptions
   emits?: EmitsOptions
-  setup?: SetupFn
   render?(ctx: any): Block
+
+  name?: string
   vapor?: boolean
 }
 
+// Note: can't mark this whole interface internal because some public interfaces
+// extend it.
+export interface ComponentInternalOptions {
+  /**
+   * @internal
+   */
+  __scopeId?: string
+  /**
+   * @internal
+   */
+  __cssModules?: Data
+  /**
+   * @internal
+   */
+  __hmrId?: string
+  /**
+   * Compat build only, for bailing out of certain compatibility behavior
+   */
+  __isBuiltIn?: boolean
+  /**
+   * This one should be exposed so that devtools can make use of it
+   */
+  __file?: string
+  /**
+   * name inferred from filename
+   */
+  __name?: string
+}
+
 type LifecycleHook<TFn = Function> = TFn[] | null
 
 export const componentKey = Symbol(__DEV__ ? `componentKey` : ``)
@@ -121,7 +155,7 @@ export interface ComponentInternalInstance {
 
   provides: Data
   scope: EffectScope
-  component: FunctionalComponent | ObjectComponent
+  component: Component
   comps: Set<ComponentInternalInstance>
   dirs: Map<Node, DirectiveBinding[]>
 

+ 4 - 0
packages/runtime-vapor/src/index.ts

@@ -41,6 +41,10 @@ export {
   getCurrentWatcher,
 } from '@vue/reactivity'
 
+import { NOOP } from '@vue/shared'
+import { warn as _warn } from './warning'
+export const warn = (__DEV__ ? _warn : NOOP) as typeof _warn
+
 export { nextTick } from './scheduler'
 export {
   getCurrentInstance,

+ 198 - 1
packages/runtime-vapor/src/warning.ts

@@ -1,3 +1,200 @@
+import {
+  type Component,
+  type ComponentInternalInstance,
+  currentInstance,
+} from './component'
+import { isFunction, isString } from '@vue/shared'
+import { isRef, pauseTracking, resetTracking, toRaw } from '@vue/reactivity'
+import { VaporErrorCodes, callWithErrorHandling } from './errorHandling'
+import type { NormalizedRawProps } from './componentProps'
+
+type TraceEntry = {
+  instance: ComponentInternalInstance
+  recurseCount: number
+}
+
+type ComponentTraceStack = TraceEntry[]
+
 export function warn(msg: string, ...args: any[]) {
-  console.warn(`[Vue warn] ${msg}`, ...args)
+  // avoid props formatting or warn handler tracking deps that might be mutated
+  // during patch, leading to infinite recursion.
+  pauseTracking()
+
+  const instance = currentInstance
+  const appWarnHandler = instance && instance.appContext.config.warnHandler
+  const trace = getComponentTrace()
+
+  if (appWarnHandler) {
+    callWithErrorHandling(
+      appWarnHandler,
+      instance,
+      VaporErrorCodes.APP_WARN_HANDLER,
+      [
+        msg + args.map(a => a.toString?.() ?? JSON.stringify(a)).join(''),
+        instance,
+        trace
+          .map(
+            ({ instance }) =>
+              `at <${formatComponentName(instance, instance.component)}>`,
+          )
+          .join('\n'),
+        trace,
+      ],
+    )
+  } else {
+    const warnArgs = [`[Vue warn]: ${msg}`, ...args]
+    /* istanbul ignore if */
+    if (
+      trace.length &&
+      // avoid spamming console during tests
+      !__TEST__
+    ) {
+      warnArgs.push(`\n`, ...formatTrace(trace))
+    }
+    console.warn(...warnArgs)
+  }
+
+  resetTracking()
+}
+
+export function getComponentTrace(): ComponentTraceStack {
+  let instance = currentInstance
+  if (!instance) return []
+
+  // we can't just use the stack because it will be incomplete during updates
+  // that did not start from the root. Re-construct the parent chain using
+  // instance parent pointers.
+  const stack: ComponentTraceStack = []
+
+  while (instance) {
+    const last = stack[0]
+    if (last && last.instance === instance) {
+      last.recurseCount++
+    } else {
+      stack.push({
+        instance,
+        recurseCount: 0,
+      })
+    }
+    instance = instance.parent
+  }
+
+  return stack
+}
+
+function formatTrace(trace: ComponentTraceStack): any[] {
+  const logs: any[] = []
+  trace.forEach((entry, i) => {
+    logs.push(...(i === 0 ? [] : [`\n`]), ...formatTraceEntry(entry))
+  })
+  return logs
+}
+
+function formatTraceEntry({ instance, recurseCount }: TraceEntry): any[] {
+  const postfix =
+    recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
+  const isRoot = instance ? instance.parent == null : false
+  const open = ` at <${formatComponentName(
+    instance,
+    instance.component,
+    isRoot,
+  )}`
+  const close = `>` + postfix
+  return instance.rawProps.length
+    ? [open, ...formatProps(instance.rawProps), close]
+    : [open + close]
+}
+
+function formatProps(rawProps: NormalizedRawProps): any[] {
+  const fullProps: Record<string, any> = {}
+  for (const props of rawProps) {
+    if (isFunction(props)) {
+      const propsObj = props()
+      for (const key in propsObj) {
+        fullProps[key] = propsObj[key]
+      }
+    } else {
+      for (const key in props) {
+        fullProps[key] = props[key]()
+      }
+    }
+  }
+
+  const res: any[] = []
+  Object.keys(fullProps)
+    .slice(0, 3)
+    .forEach(key => res.push(...formatProp(key, fullProps[key])))
+
+  if (fullProps.length > 3) {
+    res.push(` ...`)
+  }
+
+  return res
+}
+
+function formatProp(key: string, value: unknown, raw?: boolean): any {
+  if (isString(value)) {
+    value = JSON.stringify(value)
+    return raw ? value : [`${key}=${value}`]
+  } else if (
+    typeof value === 'number' ||
+    typeof value === 'boolean' ||
+    value == null
+  ) {
+    return raw ? value : [`${key}=${value}`]
+  } else if (isRef(value)) {
+    value = formatProp(key, toRaw(value.value), true)
+    return raw ? value : [`${key}=Ref<`, value, `>`]
+  } else if (isFunction(value)) {
+    return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]
+  } else {
+    value = toRaw(value)
+    return raw ? value : [`${key}=`, value]
+  }
+}
+
+export function getComponentName(
+  Component: Component,
+  includeInferred = true,
+): string | false | undefined {
+  return isFunction(Component)
+    ? Component.displayName || Component.name
+    : Component.name || (includeInferred && Component.__name)
+}
+
+export function formatComponentName(
+  instance: ComponentInternalInstance | null,
+  Component: Component,
+  isRoot = false,
+): string {
+  let name = getComponentName(Component)
+  if (!name && Component.__file) {
+    const match = Component.__file.match(/([^/\\]+)\.\w+$/)
+    if (match) {
+      name = match[1]
+    }
+  }
+
+  // TODO registry
+  // if (!name && instance && instance.parent) {
+  //   // try to infer the name based on reverse resolution
+  //   const inferFromRegistry = (registry: Record<string, any> | undefined) => {
+  //     for (const key in registry) {
+  //       if (registry[key] === Component) {
+  //         return key
+  //       }
+  //     }
+  //   }
+  //   name =
+  //     inferFromRegistry(
+  //       instance.components ||
+  //         (instance.parent.type as ComponentOptions).components,
+  //     ) || inferFromRegistry(instance.appContext.components)
+  // }
+
+  return name ? classify(name) : isRoot ? `App` : `Anonymous`
 }
+
+const classifyRE = /(?:^|[-_])(\w)/g
+const classify = (str: string): string =>
+  str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')

+ 23 - 0
playground/src/warning.js

@@ -0,0 +1,23 @@
+import { createComponent, warn } from 'vue/vapor'
+
+export default {
+  vapor: true,
+  setup() {
+    return createComponent(Comp, [
+      {
+        msg: () => 'hello',
+        onClick: () => () => {},
+      },
+      () => ({ foo: 'world', msg: 'msg' }),
+    ])
+  },
+}
+
+const Comp = {
+  name: 'Comp',
+  vapor: true,
+  props: ['msg', 'foo'],
+  setup() {
+    warn('hello')
+  },
+}