From fd018b83b5d28449a6b5340be2bdf66bb1c82c72 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 30 Aug 2019 10:36:30 -0400 Subject: [PATCH] feat: warning context --- packages/runtime-core/src/createRenderer.ts | 54 ++++++--- packages/runtime-core/src/shapeFlags.ts | 6 +- packages/runtime-core/src/warning.ts | 127 +++++++++++++++++++- 3 files changed, 163 insertions(+), 24 deletions(-) diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 0552a8381..91221e550 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -34,6 +34,7 @@ import { resolveProps } from './componentProps' import { resolveSlots } from './componentSlots' import { PatchFlags } from './patchFlags' import { ShapeFlags } from './shapeFlags' +import { pushWarningContext, popWarningContext, warn } from './warning' const prodEffectOptions = { scheduler: queueJob @@ -163,15 +164,7 @@ export function createRenderer(options: RendererOptions) { isSVG, optimized ) - } else { - if ( - __DEV__ && - !(shapeFlag & ShapeFlags.STATEFUL_COMPONENT) && - !(shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) - ) { - // TODO warn invalid node type - debugger - } + } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, @@ -181,8 +174,9 @@ export function createRenderer(options: RendererOptions) { isSVG, optimized ) + } else if (__DEV__) { + warn('Invalid VNode type:', n2.type, `(${typeof n2.type})`) } - break } } @@ -492,8 +486,8 @@ export function createRenderer(options: RendererOptions) { isSVG ) } - } else { - // TODO warn missing or invalid target + } else if (__DEV__) { + warn('Invalid Portal target on mount:', target, `(${typeof target})`) } } else { // update content @@ -518,8 +512,8 @@ export function createRenderer(options: RendererOptions) { move((children as VNode[])[i], nextTarget, null) } } - } else { - // TODO warn missing or invalid target + } else if (__DEV__) { + warn('Invalid Portal target on update:', target, `(${typeof target})`) } } } @@ -570,6 +564,10 @@ export function createRenderer(options: RendererOptions) { parentComponent )) + if (__DEV__) { + pushWarningContext(initialVNode) + } + // resolve props and slots for setup context const propsOptions = (initialVNode.type as any).props resolveProps(instance, initialVNode.props, propsOptions) @@ -601,6 +599,11 @@ export function createRenderer(options: RendererOptions) { // This is triggered by mutation of component's own state (next: null) // OR parent calling processComponent (next: VNode) const { next } = instance + + if (__DEV__) { + pushWarningContext(next || instance.vnode) + } + if (next !== null) { // update from parent next.component = instance @@ -646,8 +649,16 @@ export function createRenderer(options: RendererOptions) { if (instance.u !== null) { queuePostFlushCb(instance.u) } + + if (__DEV__) { + popWarningContext() + } } }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) + + if (__DEV__) { + popWarningContext() + } } function patchChildren( @@ -882,7 +893,13 @@ export function createRenderer(options: RendererOptions) { for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = normalizeVNode(c2[i])) if (nextChild.key != null) { - // TODO warn duplicate keys + if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) { + warn( + `Duplicate keys found during update:`, + JSON.stringify(nextChild.key), + `Make sure keys are unique.` + ) + } keyToNewIndexMap.set(nextChild.key, i) } } @@ -1093,11 +1110,10 @@ export function createRenderer(options: RendererOptions) { refs[ref] = value } else if (isRef(ref)) { ref.value = value - } else { - if (__DEV__ && !isFunction(ref)) { - // TODO warn invalid ref type - } + } else if (isFunction(ref)) { ref(value, refs) + } else if (__DEV__) { + warn('Invalid template ref type:', value, `(${typeof value})`) } } diff --git a/packages/runtime-core/src/shapeFlags.ts b/packages/runtime-core/src/shapeFlags.ts index 173aa9054..7d00837b4 100644 --- a/packages/runtime-core/src/shapeFlags.ts +++ b/packages/runtime-core/src/shapeFlags.ts @@ -6,7 +6,8 @@ export const enum ShapeFlags { STATEFUL_COMPONENT = 1 << 2, TEXT_CHILDREN = 1 << 3, ARRAY_CHILDREN = 1 << 4, - SLOTS_CHILDREN = 1 << 5 + SLOTS_CHILDREN = 1 << 5, + COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT } // but the flags are also exported as an actual object for external use @@ -16,5 +17,6 @@ export const PublicShapeFlags = { STATEFUL_COMPONENT: ShapeFlags.STATEFUL_COMPONENT, TEXT_CHILDREN: ShapeFlags.TEXT_CHILDREN, ARRAY_CHILDREN: ShapeFlags.ARRAY_CHILDREN, - SLOTS_CHILDREN: ShapeFlags.SLOTS_CHILDREN + SLOTS_CHILDREN: ShapeFlags.SLOTS_CHILDREN, + COMPONENT: ShapeFlags.COMPONENT } diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index 3553a4086..257a46f67 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -1,4 +1,125 @@ -export function warn(...args: any[]) { - // TODO - console.warn(...args) +import { VNode } from './vnode' +import { Data, ComponentInstance } from './component' +import { isString } from '@vue/shared' +import { toRaw } from '@vue/reactivity' + +let stack: VNode[] = [] + +type TraceEntry = { + vnode: VNode + recurseCount: number +} + +type ComponentTraceStack = TraceEntry[] + +export function pushWarningContext(vnode: VNode) { + stack.push(vnode) +} + +export function popWarningContext() { + stack.pop() +} + +export function warn(msg: string, ...args: any[]) { + // TODO app level warn handler + console.warn(`[Vue warn]: ${msg}`, ...args) + const trace = getComponentTrace() + 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 { + const logs: string[] = [] + trace.forEach((entry, i) => { + const formatted = formatTraceEntry(entry, i) + if (i === 0) { + logs.push('at', ...formatted) + } else { + logs.push('\n', ...formatted) + } + }) + console.log(...logs) + } +} + +function getComponentTrace(): ComponentTraceStack { + let currentVNode: VNode | null = stack[stack.length - 1] + if (!currentVNode) { + 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 normlaizedStack: ComponentTraceStack = [] + + while (currentVNode) { + const last = normlaizedStack[0] + if (last && last.vnode === currentVNode) { + last.recurseCount++ + } else { + normlaizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }) + } + const parentInstance: ComponentInstance | null = (currentVNode.component as ComponentInstance) + .parent + currentVNode = parentInstance && parentInstance.vnode + } + + return normlaizedStack +} + +function formatTraceEntry( + { vnode, recurseCount }: TraceEntry, + depth: number = 0 +): string[] { + const padding = depth === 0 ? '' : ' '.repeat(depth * 2 + 1) + const postfix = + recurseCount > 0 ? `... (${recurseCount} recursive calls)` : `` + const open = padding + `<${formatComponentName(vnode)}` + const close = `>` + postfix + const rootLabel = + (vnode.component as ComponentInstance).parent == null ? `(Root)` : `` + return vnode.props + ? [open, ...formatProps(vnode.props), close, rootLabel] + : [open + close, rootLabel] +} + +const classifyRE = /(?:^|[-_])(\w)/g +const classify = (str: string): string => + str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '') + +function formatComponentName(vnode: VNode, file?: string): string { + const Component = vnode.type as any + let name = Component.displayName || Component.name + if (!name && file) { + const match = file.match(/([^/\\]+)\.vue$/) + if (match) { + name = match[1] + } + } + return name ? classify(name) : 'AnonymousComponent' +} + +function formatProps(props: Data): string[] { + const res: string[] = [] + for (const key in props) { + const value = props[key] + if (isString(value)) { + res.push(`${key}=${JSON.stringify(value)}`) + } else { + res.push(`${key}=`, toRaw(value) as any) + } + } + return res }