]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: test for app-level APIs
authorEvan You <yyx990803@gmail.com>
Tue, 3 Sep 2019 22:11:04 +0000 (18:11 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 3 Sep 2019 22:11:04 +0000 (18:11 -0400)
12 files changed:
packages/reactivity/__tests__/readonly.spec.ts
packages/runtime-core/__tests__/apiApp.spec.ts [new file with mode: 0644]
packages/runtime-core/__tests__/apiCreateApp.spec.ts [deleted file]
packages/runtime-core/__tests__/directives.spec.ts
packages/runtime-core/src/apiApp.ts [moved from packages/runtime-core/src/apiCreateApp.ts with 78% similarity]
packages/runtime-core/src/apiInject.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/directives.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/vnode.ts
packages/runtime-core/src/warning.ts

index 0c4ee1530ca19deba8459f4de98caeb2559d54e4..c2dbb3a8cb556654974d2b71da216d464d57d1d9 100644 (file)
@@ -86,6 +86,7 @@ describe('reactivity/readonly', () => {
       observed.a = 2
       expect(observed.a).toBe(1)
       expect(dummy).toBe(1)
+      expect(`target is readonly`).toHaveBeenWarned()
     })
 
     it('should trigger effects when unlocked', () => {
@@ -178,9 +179,11 @@ describe('reactivity/readonly', () => {
       observed[0].a = 2
       expect(observed[0].a).toBe(1)
       expect(dummy).toBe(1)
+      expect(`target is readonly`).toHaveBeenWarnedTimes(1)
       observed[0] = { a: 2 }
       expect(observed[0].a).toBe(1)
       expect(dummy).toBe(1)
+      expect(`target is readonly`).toHaveBeenWarnedTimes(2)
     })
 
     it('should trigger effects when unlocked', () => {
diff --git a/packages/runtime-core/__tests__/apiApp.spec.ts b/packages/runtime-core/__tests__/apiApp.spec.ts
new file mode 100644 (file)
index 0000000..de66cfe
--- /dev/null
@@ -0,0 +1,206 @@
+import {
+  createApp,
+  h,
+  nodeOps,
+  serializeInner,
+  mockWarn,
+  provide,
+  inject,
+  resolveComponent,
+  resolveDirective,
+  applyDirectives,
+  Plugin,
+  ref
+} from '@vue/runtime-test'
+
+describe('api: createApp', () => {
+  mockWarn()
+
+  test('mount', () => {
+    const Comp = {
+      props: {
+        count: {
+          default: 0
+        }
+      },
+      render() {
+        return this.count
+      }
+    }
+
+    const root1 = nodeOps.createElement('div')
+    createApp().mount(Comp, root1)
+    expect(serializeInner(root1)).toBe(`0`)
+
+    // mount with props
+    const root2 = nodeOps.createElement('div')
+    const app2 = createApp()
+    app2.mount(Comp, root2, { count: 1 })
+    expect(serializeInner(root2)).toBe(`1`)
+
+    // remount warning
+    const root3 = nodeOps.createElement('div')
+    app2.mount(Comp, root3)
+    expect(serializeInner(root3)).toBe(``)
+    expect(`already been mounted`).toHaveBeenWarned()
+  })
+
+  test('provide', () => {
+    const app = createApp()
+    app.provide('foo', 1)
+    app.provide('bar', 2)
+
+    const Root = {
+      setup() {
+        // test override
+        provide('foo', 3)
+        return () => h(Child)
+      }
+    }
+
+    const Child = {
+      setup() {
+        const foo = inject('foo')
+        const bar = inject('bar')
+        return () => `${foo},${bar}`
+      }
+    }
+
+    const root = nodeOps.createElement('div')
+    app.mount(Root, root)
+    expect(serializeInner(root)).toBe(`3,2`)
+  })
+
+  test('component', () => {
+    const app = createApp()
+    app.component('FooBar', () => 'foobar!')
+    app.component('BarBaz', () => 'barbaz!')
+
+    const Root = {
+      // local override
+      components: {
+        BarBaz: () => 'barbaz-local!'
+      },
+      setup() {
+        // resolve in setup
+        const FooBar = resolveComponent('foo-bar') as any
+        return () => {
+          // resolve in render
+          const BarBaz = resolveComponent('bar-baz') as any
+          return h('div', [h(FooBar), h(BarBaz)])
+        }
+      }
+    }
+
+    const root = nodeOps.createElement('div')
+    app.mount(Root, root)
+    expect(serializeInner(root)).toBe(`<div>foobar!barbaz-local!</div>`)
+  })
+
+  test('directive', () => {
+    const app = createApp()
+
+    const spy1 = jest.fn()
+    const spy2 = jest.fn()
+    const spy3 = jest.fn()
+    app.directive('FooBar', {
+      mounted: spy1
+    })
+    app.directive('BarBaz', {
+      mounted: spy2
+    })
+
+    const Root = {
+      // local override
+      directives: {
+        BarBaz: { mounted: spy3 }
+      },
+      setup() {
+        // resolve in setup
+        const FooBar = resolveDirective('foo-bar') as any
+        return () => {
+          // resolve in render
+          const BarBaz = resolveDirective('bar-baz') as any
+          return applyDirectives(h('div'), [[FooBar], [BarBaz]])
+        }
+      }
+    }
+
+    const root = nodeOps.createElement('div')
+    app.mount(Root, root)
+    expect(spy1).toHaveBeenCalled()
+    expect(spy2).not.toHaveBeenCalled()
+    expect(spy3).toHaveBeenCalled()
+  })
+
+  test('use', () => {
+    const PluginA: Plugin = app => app.provide('foo', 1)
+    const PluginB: Plugin = {
+      install: app => app.provide('bar', 2)
+    }
+
+    const app = createApp()
+    app.use(PluginA)
+    app.use(PluginB)
+
+    const Root = {
+      setup() {
+        const foo = inject('foo')
+        const bar = inject('bar')
+        return () => `${foo},${bar}`
+      }
+    }
+    const root = nodeOps.createElement('div')
+    app.mount(Root, root)
+    expect(serializeInner(root)).toBe(`1,2`)
+  })
+
+  test('config.errorHandler', () => {
+    const app = createApp()
+
+    const error = new Error()
+    const count = ref(0)
+
+    const handler = (app.config.errorHandler = jest.fn(
+      (err, instance, info) => {
+        expect(err).toBe(error)
+        expect((instance as any).count).toBe(count.value)
+        expect(info).toBe(`render function`)
+      }
+    ))
+
+    const Root = {
+      setup() {
+        const count = ref(0)
+        return {
+          count
+        }
+      },
+      render() {
+        throw error
+      }
+    }
+
+    app.mount(Root, nodeOps.createElement('div'))
+    expect(handler).toHaveBeenCalled()
+  })
+
+  test('config.warnHandler', () => {
+    const app = createApp()
+
+    const handler = (app.config.warnHandler = jest.fn(
+      (msg, instance, trace) => {}
+    ))
+
+    const Root = {
+      setup() {}
+    }
+
+    app.mount(Root, nodeOps.createElement('div'))
+    expect(handler).toHaveBeenCalled()
+  })
+
+  test.todo('mixin')
+
+  test.todo('config.optionsMergeStrategies')
+})
diff --git a/packages/runtime-core/__tests__/apiCreateApp.spec.ts b/packages/runtime-core/__tests__/apiCreateApp.spec.ts
deleted file mode 100644 (file)
index ff5e157..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-describe('api: createApp', () => {
-  test('mount', () => {})
-
-  test('provide', () => {})
-
-  test('component', () => {})
-
-  test('directive', () => {})
-
-  test('use', () => {})
-
-  test.todo('mixin')
-
-  test('config.errorHandler', () => {})
-
-  test('config.warnHandler', () => {})
-
-  test.todo('config.optionsMergeStrategies')
-})
index 6cb43e7297b49cca02e65a1059a22ffc2044e3da..586b39e320b85e43cca9b288cb40635fa7262e33 100644 (file)
@@ -109,20 +109,22 @@ describe('directives', () => {
       render() {
         _prevVnode = _vnode
         _vnode = applyDirectives(h('div', count.value), [
-          {
-            beforeMount,
-            mounted,
-            beforeUpdate,
-            updated,
-            beforeUnmount,
-            unmounted
-          },
-          // value
-          count.value,
-          // argument
-          'foo',
-          // modifiers
-          { ok: true }
+          [
+            {
+              beforeMount,
+              mounted,
+              beforeUpdate,
+              updated,
+              beforeUnmount,
+              unmounted
+            },
+            // value
+            count.value,
+            // argument
+            'foo',
+            // modifiers
+            { ok: true }
+          ]
         ])
         return _vnode
       }
similarity index 78%
rename from packages/runtime-core/src/apiCreateApp.ts
rename to packages/runtime-core/src/apiApp.ts
index b5a64c5557fac00c9244f26ffa0cf3d28c04fce4..7e0ed97232058cd965700e424d7bde13a3656ec8 100644 (file)
@@ -36,12 +36,12 @@ export interface AppConfig {
   performance: boolean
   errorHandler?: (
     err: Error,
-    instance: ComponentRenderProxy,
+    instance: ComponentRenderProxy | null,
     info: string
   ) => void
   warnHandler?: (
     msg: string,
-    instance: ComponentRenderProxy,
+    instance: ComponentRenderProxy | null,
     trace: string
   ) => void
 }
@@ -56,7 +56,7 @@ export interface AppContext {
 
 type PluginInstallFunction = (app: App) => any
 
-type Plugin =
+export type Plugin =
   | PluginInstallFunction
   | {
       install: PluginInstallFunction
@@ -82,6 +82,8 @@ export function createAppAPI(render: RootRenderFunction): () => App {
   return function createApp(): App {
     const context = createAppContext()
 
+    let isMounted = false
+
     const app: App = {
       get config() {
         return context.config
@@ -134,14 +136,20 @@ export function createAppAPI(render: RootRenderFunction): () => App {
         }
       },
 
-      mount(rootComponent, rootContainer, rootProps?: Data) {
-        const vnode = createVNode(rootComponent, rootProps)
-        // store app context on the root VNode.
-        // this will be set on the root instance on initial mount.
-        vnode.appContext = context
-        render(vnode, rootContainer)
-        return (vnode.component as ComponentInstance)
-          .renderProxy as ComponentRenderProxy
+      mount(rootComponent, rootContainer, rootProps?: Data): any {
+        if (!isMounted) {
+          const vnode = createVNode(rootComponent, rootProps)
+          // store app context on the root VNode.
+          // this will be set on the root instance on initial mount.
+          vnode.appContext = context
+          render(vnode, rootContainer)
+          isMounted = true
+          return (vnode.component as ComponentInstance).renderProxy
+        } else if (__DEV__) {
+          warn(
+            `App has already been mounted. Create a new app instance instead.`
+          )
+        }
       },
 
       provide(key, value) {
@@ -164,15 +172,21 @@ export function resolveAsset(type: 'components' | 'directives', name: string) {
   if (instance) {
     let camelized
     let capitalized
+    let res
     const local = (instance.type as any)[type]
-    const global = instance.appContext[type]
-    const res =
-      local[name] ||
-      local[(camelized = camelize(name))] ||
-      local[(capitalized = capitalize(name))] ||
-      global[name] ||
-      global[camelized] ||
-      global[capitalized]
+    if (local) {
+      res =
+        local[name] ||
+        local[(camelized = camelize(name))] ||
+        local[(capitalized = capitalize(camelized))]
+    }
+    if (!res) {
+      const global = instance.appContext[type]
+      res =
+        global[name] ||
+        global[camelized || (camelized = camelize(name))] ||
+        global[capitalized || capitalize(camelized)]
+    }
     if (__DEV__ && !res) {
       warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
     }
index ba9d7dd1176cf2755e53758187714fc6024eabde..3766d9f1cdf11a5d467004951f06b1a30703421f 100644 (file)
@@ -6,7 +6,7 @@ export interface InjectionKey<T> extends Symbol {}
 export function provide<T>(key: InjectionKey<T> | string, value: T) {
   if (!currentInstance) {
     if (__DEV__) {
-      warn(`provide() is used without an active component instance.`)
+      warn(`provide() can only be used inside setup().`)
     }
   } else {
     let provides = currentInstance.provides
@@ -27,17 +27,16 @@ export function provide<T>(key: InjectionKey<T> | string, value: T) {
 export function inject<T>(key: InjectionKey<T> | string): T | undefined
 export function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
 export function inject(key: InjectionKey<any> | string, defaultValue?: any) {
-  if (!currentInstance) {
-    // TODO warn
-  } else {
-    // TODO should also check for app-level provides
-    const provides = currentInstance.parent && currentInstance.provides
-    if (provides && key in provides) {
+  if (currentInstance) {
+    const provides = currentInstance.provides
+    if (key in provides) {
       return provides[key as any] as any
     } else if (defaultValue !== undefined) {
       return defaultValue
     } else if (__DEV__) {
       warn(`injection "${key}" not found.`)
     }
+  } else if (__DEV__) {
+    warn(`inject() can only be used inside setup().`)
   }
 }
index d003d2f533f521cb1ff3fee59d959780af000021..8ff7326225e92807e4588a91c91e51d55284903a 100644 (file)
@@ -13,7 +13,8 @@ import {
   callWithErrorHandling,
   callWithAsyncErrorHandling
 } from './errorHandling'
-import { AppContext, createAppContext, resolveAsset } from './apiCreateApp'
+import { AppContext, createAppContext, resolveAsset } from './apiApp'
+import { Directive } from './directives'
 
 export type Data = { [key: string]: unknown }
 
@@ -31,41 +32,42 @@ export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
 } & P &
   S
 
-type SetupFunction<Props, RawBindings> = (
-  props: Props,
-  ctx: SetupContext
-) => RawBindings | (() => VNodeChild)
-
 type RenderFunction<Props = {}, RawBindings = {}> = <
   Bindings extends UnwrapRef<RawBindings>
 >(
   this: ComponentRenderProxy<Props, Bindings>
 ) => VNodeChild
 
-interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
-  props?: undefined
-  setup?: SetupFunction<Props, RawBindings>
+interface ComponentOptionsBase<Props, RawBindings> {
+  setup?: (
+    props: Props,
+    ctx: SetupContext
+  ) => RawBindings | (() => VNodeChild) | void
   render?: RenderFunction<Props, RawBindings>
+  components?: Record<string, Component>
+  directives?: Record<string, Directive>
+  // TODO full 2.x options compat
+}
+
+interface ComponentOptionsWithoutProps<Props = {}, RawBindings = {}>
+  extends ComponentOptionsBase<Props, RawBindings> {
+  props?: undefined
 }
 
 interface ComponentOptionsWithArrayProps<
   PropNames extends string = string,
-  RawBindings = Data,
+  RawBindings = {},
   Props = { [key in PropNames]?: unknown }
-> {
+> extends ComponentOptionsBase<Props, RawBindings> {
   props: PropNames[]
-  setup?: SetupFunction<Props, RawBindings>
-  render?: RenderFunction<Props, RawBindings>
 }
 
 interface ComponentOptionsWithProps<
   PropsOptions = ComponentPropsOptions,
-  RawBindings = Data,
+  RawBindings = {},
   Props = ExtractPropTypes<PropsOptions>
-> {
+> extends ComponentOptionsBase<Props, RawBindings> {
   props: PropsOptions
-  setup?: SetupFunction<Props, RawBindings>
-  render?: RenderFunction<Props, RawBindings>
 }
 
 export type ComponentOptions =
@@ -105,7 +107,7 @@ interface SetupContext {
   emit: ((event: string, ...args: unknown[]) => void)
 }
 
-export type ComponentInstance<P = Data, S = Data> = {
+export type ComponentInstance<P = {}, S = {}> = {
   type: FunctionalComponent | ComponentOptions
   parent: ComponentInstance | null
   appContext: AppContext
@@ -193,12 +195,13 @@ export function createComponentInstance(
   vnode: VNode,
   parent: ComponentInstance | null
 ): ComponentInstance {
+  // inherit parent app context - or - if root, adopt from root vnode
+  const appContext =
+    (parent ? parent.appContext : vnode.appContext) || emptyAppContext
   const instance = {
     vnode,
     parent,
-    // inherit parent app context - or - if root, adopt from root vnode
-    appContext:
-      (parent ? parent.appContext : vnode.appContext) || emptyAppContext,
+    appContext,
     type: vnode.type as any,
     root: null as any, // set later so it can point to itself
     next: null,
@@ -209,7 +212,7 @@ export function createComponentInstance(
     propsProxy: null,
     setupContext: null,
     effects: null,
-    provides: parent ? parent.provides : {},
+    provides: parent ? parent.provides : Object.create(appContext.provides),
 
     // setup context properties
     data: EMPTY_OBJ,
index 065f7bbd9b197294d9f98cdeaa9cc6d3fb94c0b4..1c901de8e1e83a6af316e4c02815fd603f0675dc 100644 (file)
@@ -5,11 +5,10 @@ const comp = resolveComponent('comp')
 const foo = resolveDirective('foo')
 const bar = resolveDirective('bar')
 
-return applyDirectives(
-  h(comp),
+return applyDirectives(h(comp), [
   [foo, this.x],
   [bar, this.y]
-)
+])
 */
 
 import { VNode, cloneVNode } from './vnode'
@@ -22,7 +21,7 @@ import {
 } from './component'
 import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
 import { HostNode } from './createRenderer'
-import { resolveAsset } from './apiCreateApp'
+import { resolveAsset } from './apiApp'
 
 export interface DirectiveBinding {
   instance: ComponentRenderProxy | null
@@ -103,10 +102,7 @@ type DirectiveArguments = Array<
   | [Directive, any, string, DirectiveModifiers]
 >
 
-export function applyDirectives(
-  vnode: VNode,
-  ...directives: DirectiveArguments
-) {
+export function applyDirectives(vnode: VNode, directives: DirectiveArguments) {
   const instance = currentRenderingInstance
   if (instance !== null) {
     vnode = cloneVNode(vnode)
index 53cadc481adb3f0786e4f54210642a9ac63f3a02..42cc356dab000fe9566c7f18384aa897e68c38c4 100644 (file)
@@ -13,6 +13,8 @@ export const enum ErrorTypes {
   NATIVE_EVENT_HANDLER,
   COMPONENT_EVENT_HANDLER,
   DIRECTIVE_HOOK,
+  APP_ERROR_HANDLER,
+  APP_WARN_HANDLER,
   SCHEDULER
 }
 
@@ -38,6 +40,8 @@ export const ErrorTypeStrings: Record<number | string, string> = {
   [ErrorTypes.NATIVE_EVENT_HANDLER]: 'native event handler',
   [ErrorTypes.COMPONENT_EVENT_HANDLER]: 'component event handler',
   [ErrorTypes.DIRECTIVE_HOOK]: 'directive hook',
+  [ErrorTypes.APP_ERROR_HANDLER]: 'app errorHandler',
+  [ErrorTypes.APP_WARN_HANDLER]: 'app warnHandler',
   [ErrorTypes.SCHEDULER]:
     'scheduler flush. This may be a Vue internals bug. ' +
     'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue'
@@ -81,24 +85,34 @@ export function handleError(
   type: AllErrorTypes
 ) {
   const contextVNode = instance ? instance.vnode : null
-  let cur: ComponentInstance | null = instance && instance.parent
-  while (cur) {
-    const errorCapturedHooks = cur.ec
-    if (errorCapturedHooks !== null) {
-      for (let i = 0; i < errorCapturedHooks.length; i++) {
-        if (
-          errorCapturedHooks[i](
-            err,
-            instance && instance.renderProxy,
-            // in production the hook receives only the error code
-            __DEV__ ? ErrorTypeStrings[type] : type
-          )
-        ) {
-          return
+  if (instance) {
+    let cur: ComponentInstance | null = instance.parent
+    // the exposed instance is the render proxy to keep it consistent with 2.x
+    const exposedInstance = instance.renderProxy
+    // in production the hook receives only the error code
+    const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
+    while (cur) {
+      const errorCapturedHooks = cur.ec
+      if (errorCapturedHooks !== null) {
+        for (let i = 0; i < errorCapturedHooks.length; i++) {
+          if (errorCapturedHooks[i](err, exposedInstance, errorInfo)) {
+            return
+          }
         }
       }
+      cur = cur.parent
+    }
+    // app-level handling
+    const appErrorHandler = instance.appContext.config.errorHandler
+    if (appErrorHandler) {
+      callWithErrorHandling(
+        appErrorHandler,
+        null,
+        ErrorTypes.APP_ERROR_HANDLER,
+        [err, exposedInstance, errorInfo]
+      )
+      return
     }
-    cur = cur.parent
   }
   logError(err, type, contextVNode)
 }
index d34cb4248c8a8b15d56ee8e5a37a1c3508a4a7d5..59fc13bca3a8b93a8e856c81d50762521fa73c5f 100644 (file)
@@ -28,7 +28,7 @@ export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
 export { getCurrentInstance } from './component'
 
 // For custom renderers
-export { createAppAPI } from './apiCreateApp'
+export { createAppAPI } from './apiApp'
 export { createRenderer } from './createRenderer'
 export {
   handleError,
@@ -42,8 +42,8 @@ export { applyDirectives, resolveDirective } from './directives'
 
 // Types -----------------------------------------------------------------------
 
-export { App } from './apiCreateApp'
-export { VNode } from './vnode'
+export { App, AppConfig, AppContext, Plugin } from './apiApp'
+export { VNode, VNodeTypes } from './vnode'
 export { FunctionalComponent, ComponentInstance } from './component'
 export { RendererOptions } from './createRenderer'
 export { Slot, Slots } from './componentSlots'
index 1fe556819977613b4f630d0aa0add77740ee5b3f..358106459e434dce1a726033a73faca1a23509ed 100644 (file)
@@ -12,7 +12,7 @@ import { RawSlots } from './componentSlots'
 import { PatchFlags } from './patchFlags'
 import { ShapeFlags } from './shapeFlags'
 import { isReactive } from '@vue/reactivity'
-import { AppContext } from './apiCreateApp'
+import { AppContext } from './apiApp'
 
 export const Fragment = Symbol('Fragment')
 export const Text = Symbol('Text')
index b8301ba3462e01473e805188c6ce9ac1d46c2b8a..138b565b06d9630f47316a99d88c47754513e243 100644 (file)
@@ -21,13 +21,24 @@ export function popWarningContext() {
 }
 
 export function warn(msg: string, ...args: any[]) {
-  // TODO app level warn handler
+  const instance = stack.length ? stack[stack.length - 1].component : null
+  const appWarnHandler = instance && instance.appContext.config.warnHandler
+  const trace = getComponentTrace()
+
+  if (appWarnHandler) {
+    appWarnHandler(
+      msg + args.join(''),
+      instance && instance.renderProxy,
+      formatTrace(trace).join('')
+    )
+    return
+  }
+
   console.warn(`[Vue warn]: ${msg}`, ...args)
   // avoid spamming console during tests
   if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
     return
   }
-  const trace = getComponentTrace()
   if (!trace.length) {
     return
   }
@@ -41,16 +52,7 @@ export function warn(msg: string, ...args: any[]) {
     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)
+    console.log(...formatTrace(trace))
   }
 }
 
@@ -83,6 +85,19 @@ function getComponentTrace(): ComponentTraceStack {
   return normlaizedStack
 }
 
+function formatTrace(trace: ComponentTraceStack): string[] {
+  const logs: string[] = []
+  trace.forEach((entry, i) => {
+    const formatted = formatTraceEntry(entry, i)
+    if (i === 0) {
+      logs.push('at', ...formatted)
+    } else {
+      logs.push('\n', ...formatted)
+    }
+  })
+  return logs
+}
+
 function formatTraceEntry(
   { vnode, recurseCount }: TraceEntry,
   depth: number = 0