diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index dfc77d6c..0c6d2bda 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -1,7 +1,7 @@ import { VNode } from './vnode' import { Data, ComponentInternalInstance, Component } from './component' import { isString, isFunction } from '@vue/shared' -import { toRaw } from '@vue/reactivity' +import { toRaw, isRef, pauseTracking, resumeTracking } from '@vue/reactivity' import { callWithErrorHandling, ErrorCodes } from './errorHandling' type ComponentVNode = VNode & { @@ -26,6 +26,10 @@ export function popWarningContext() { } export function warn(msg: string, ...args: any[]) { + // avoid props formatting or warn handler tracking deps that might be mutated + // during patch, leading to infinite recursion. + pauseTracking() + const instance = stack.length ? stack[stack.length - 1].component : null const appWarnHandler = instance && instance.appContext.config.warnHandler const trace = getComponentTrace() @@ -38,32 +42,25 @@ export function warn(msg: string, ...args: any[]) { [ msg + args.join(''), instance && instance.renderProxy, - formatTrace(trace).join('') + trace + .map(({ vnode }) => `at <${formatComponentName(vnode)}>`) + .join('\n'), + trace ] ) - return + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args] + if ( + trace.length && + // avoid spamming console during tests + (typeof process === 'undefined' || process.env.NODE_ENV !== 'test') + ) { + warnArgs.push(`\n`, ...formatTrace(trace)) + } + console.warn(...warnArgs) } - console.warn(`[Vue warn]: ${msg}`, ...args) - // avoid spamming console during tests - if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - return - } - if (!trace.length) { - return - } - if (trace.length > 1 && console.groupCollapsed) { - console.groupCollapsed('at', ...formatTraceEntry(trace[0])) - const logs: string[] = [] - trace.slice(1).forEach((entry, i) => { - if (i !== 0) logs.push('\n') - logs.push(...formatTraceEntry(entry, i + 1)) - }) - console.log(...logs) - console.groupEnd() - } else { - console.log(...formatTrace(trace)) - } + resumeTracking() } function getComponentTrace(): ComponentTraceStack { @@ -95,27 +92,18 @@ function getComponentTrace(): ComponentTraceStack { return normalizedStack } -function formatTrace(trace: ComponentTraceStack): string[] { - const logs: string[] = [] +function formatTrace(trace: ComponentTraceStack): any[] { + const logs: any[] = [] trace.forEach((entry, i) => { - const formatted = formatTraceEntry(entry, i) - if (i === 0) { - logs.push('at', ...formatted) - } else { - logs.push('\n', ...formatted) - } + logs.push(...(i === 0 ? [] : [`\n`]), ...formatTraceEntry(entry)) }) return logs } -function formatTraceEntry( - { vnode, recurseCount }: TraceEntry, - depth: number = 0 -): string[] { - const padding = depth === 0 ? '' : ' '.repeat(depth * 2 + 1) +function formatTraceEntry({ vnode, recurseCount }: TraceEntry): any[] { const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : `` - const open = padding + `<${formatComponentName(vnode)}` + const open = ` at <${formatComponentName(vnode)}` const close = `>` + postfix const rootLabel = vnode.component!.parent == null ? `(Root)` : `` return vnode.props @@ -129,25 +117,39 @@ const classify = (str: string): string => function formatComponentName(vnode: ComponentVNode, file?: string): string { const Component = vnode.type - let name = isFunction(Component) ? Component.displayName : Component.name + let name = isFunction(Component) + ? Component.displayName || Component.name + : Component.name if (!name && file) { const match = file.match(/([^/\\]+)\.vue$/) if (match) { name = match[1] } } - return name ? classify(name) : 'AnonymousComponent' + return name ? classify(name) : 'Anonymous' } -function formatProps(props: Data): string[] { - const res: string[] = [] +function formatProps(props: Data): any[] { + const res: any[] = [] for (const key in props) { - const value = props[key] - if (isString(value)) { - res.push(`${key}=${JSON.stringify(value)}`) - } else { - res.push(`${key}=`, String(toRaw(value))) - } + res.push(...formatProp(key, props[key])) } return res } + +function formatProp(key: string, value: unknown): any[] +function formatProp(key: string, value: unknown, raw: true): any +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' || 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 { + value = toRaw(value) + return raw ? value : [`${key}=`, value] + } +}