]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: custom formatters
authorEvan You <yyx990803@gmail.com>
Tue, 13 Oct 2020 15:50:09 +0000 (11:50 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 13 Oct 2020 15:50:24 +0000 (11:50 -0400)
jest.config.js
packages/runtime-core/src/componentPublicInstance.ts
packages/runtime-core/src/customFormatter.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts
packages/vue/src/dev.ts

index 8312acd55cacc743cf0e59e80212c68000957e2d..72e2205febbc1d2dd5e5c8294a42165531d304c1 100644 (file)
@@ -22,6 +22,7 @@ module.exports = {
     '!packages/template-explorer/**',
     '!packages/size-check/**',
     '!packages/runtime-core/src/profiling.ts',
+    '!packages/runtome-core/src/customFormatter.ts',
     // DOM transitions are tested via e2e so no coverage is collected
     '!packages/runtime-dom/src/components/Transition*',
     // only called in browsers
index e45fd7755f4eeaaf01d726f84a95840ea425e6f7..2ac9b8a47a1edb60317e8803c400d58d4002af50 100644 (file)
@@ -247,6 +247,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
       return true
     }
 
+    // for internal formatters to know that this is a Vue instance
+    if (__DEV__ && key === '__isVue') {
+      return true
+    }
+
     // data / props / ctx
     // This getter gets called for every property access on the render context
     // during render and is a major hotspot. The most expensive part of this
diff --git a/packages/runtime-core/src/customFormatter.ts b/packages/runtime-core/src/customFormatter.ts
new file mode 100644 (file)
index 0000000..706cef6
--- /dev/null
@@ -0,0 +1,198 @@
+import { isReactive, isReadonly, isRef, Ref, toRaw } from '@vue/reactivity'
+import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared'
+import { ComponentInternalInstance, ComponentOptions } from './component'
+import { ComponentPublicInstance } from './componentPublicInstance'
+
+export function initCustomFormatter() {
+  if (!__DEV__ || !__BROWSER__) {
+    return
+  }
+
+  const vueStyle = { style: 'color:#3ba776' }
+  const numberStyle = { style: 'color:#0b1bc9' }
+  const stringStyle = { style: 'color:#b62e24' }
+  const keywordStyle = { style: 'color:#9d288c' }
+
+  // custom formatter for Chrome
+  // https://www.mattzeunert.com/2016/02/19/custom-chrome-devtools-object-formatters.html
+  const formatter = {
+    header(obj: unknown) {
+      // TODO also format ComponentPublicInstance & ctx.slots/attrs in setup
+      if (!isObject(obj)) {
+        return null
+      }
+
+      if (obj.__isVue) {
+        return ['div', vueStyle, `VueInstance`]
+      } else if (isRef(obj)) {
+        return [
+          'div',
+          {},
+          ['span', vueStyle, genRefFlag(obj)],
+          '<',
+          formatValue(obj.value),
+          `>`
+        ]
+      } else if (isReactive(obj)) {
+        return [
+          'div',
+          {},
+          ['span', vueStyle, 'Reactive'],
+          '<',
+          formatValue(obj),
+          `>${isReadonly(obj) ? ` (readonly)` : ``}`
+        ]
+      } else if (isReadonly(obj)) {
+        return [
+          'div',
+          {},
+          ['span', vueStyle, 'Readonly'],
+          '<',
+          formatValue(obj),
+          '>'
+        ]
+      }
+      return null
+    },
+    hasBody(obj: unknown) {
+      return obj && (obj as any).__isVue
+    },
+    body(obj: unknown) {
+      if (obj && (obj as any).__isVue) {
+        return [
+          'div',
+          {},
+          ...formatInstance((obj as ComponentPublicInstance).$)
+        ]
+      }
+    }
+  }
+
+  function formatInstance(instance: ComponentInternalInstance) {
+    const blocks = []
+    if (instance.type.props && instance.props) {
+      blocks.push(createInstanceBlock('props', toRaw(instance.props)))
+    }
+    if (instance.setupState !== EMPTY_OBJ) {
+      blocks.push(createInstanceBlock('setup', instance.setupState))
+    }
+    if (instance.data !== EMPTY_OBJ) {
+      blocks.push(createInstanceBlock('data', toRaw(instance.data)))
+    }
+    const computed = extractKeys(instance, 'computed')
+    if (computed) {
+      blocks.push(createInstanceBlock('computed', computed))
+    }
+    const injected = extractKeys(instance, 'inject')
+    if (injected) {
+      blocks.push(createInstanceBlock('injected', injected))
+    }
+
+    blocks.push([
+      'div',
+      {},
+      [
+        'span',
+        {
+          style: keywordStyle.style + ';opacity:0.66'
+        },
+        '$ (internal): '
+      ],
+      ['object', { object: instance }]
+    ])
+    return blocks
+  }
+
+  function createInstanceBlock(type: string, target: any) {
+    target = extend({}, target)
+    if (!Object.keys(target).length) {
+      return ['span', {}]
+    }
+    return [
+      'div',
+      { style: 'line-height:1.25em;margin-bottom:0.6em' },
+      [
+        'div',
+        {
+          style: 'color:#476582'
+        },
+        type
+      ],
+      [
+        'div',
+        {
+          style: 'padding-left:1.25em'
+        },
+        ...Object.keys(target).map(key => {
+          return [
+            'div',
+            {},
+            ['span', keywordStyle, key + ': '],
+            formatValue(target[key], false)
+          ]
+        })
+      ]
+    ]
+  }
+
+  function formatValue(v: unknown, asRaw = true) {
+    if (typeof v === 'number') {
+      return ['span', numberStyle, v]
+    } else if (typeof v === 'string') {
+      return ['span', stringStyle, JSON.stringify(v)]
+    } else if (typeof v === 'boolean') {
+      return ['span', keywordStyle, v]
+    } else if (isObject(v)) {
+      return ['object', { object: asRaw ? toRaw(v) : v }]
+    } else {
+      return ['span', stringStyle, String(v)]
+    }
+  }
+
+  function extractKeys(instance: ComponentInternalInstance, type: string) {
+    const Comp = instance.type
+    if (isFunction(Comp)) {
+      return
+    }
+    const extracted: Record<string, any> = {}
+    for (const key in instance.ctx) {
+      if (isKeyOfType(Comp, key, type)) {
+        extracted[key] = instance.ctx[key]
+      }
+    }
+    return extracted
+  }
+
+  function isKeyOfType(Comp: ComponentOptions, key: string, type: string) {
+    const opts = Comp[type]
+    if (
+      (isArray(opts) && opts.includes(key)) ||
+      (isObject(opts) && key in opts)
+    ) {
+      return true
+    }
+    if (Comp.extends && isKeyOfType(Comp.extends, key, type)) {
+      return true
+    }
+    if (Comp.mixins && Comp.mixins.some(m => isKeyOfType(m, key, type))) {
+      return true
+    }
+  }
+
+  function genRefFlag(v: Ref) {
+    if (v._shallow) {
+      return `ShallowRef`
+    }
+    if ((v as any).effect) {
+      return `ComputedRef`
+    }
+    return `Ref`
+  }
+
+  /* eslint-disable no-restricted-globals */
+  if ((window as any).devtoolsFormatters) {
+    ;(window as any).devtoolsFormatters.push(formatter)
+  } else {
+    ;(window as any).devtoolsFormatters = [formatter]
+  }
+}
index 26c27d544e6ee37edb47b62b1b8f95d7afb3e68f..ca1cadf28d9074051b208e2e24c9c35b3cb3e9a3 100644 (file)
@@ -93,6 +93,7 @@ export {
   setTransitionHooks,
   getTransitionRawChildren
 } from './components/BaseTransition'
+export { initCustomFormatter } from './customFormatter'
 
 // For devtools
 export { devtools, setDevtoolsHook } from './devtools'
index bfa590fb9745d9d7e88d0d4cf0fb10171f785a41..4f26d0e3fdfd8398b50aaad4ef3da4c1ba06b0b1 100644 (file)
@@ -1,4 +1,4 @@
-import { setDevtoolsHook } from '@vue/runtime-dom'
+import { setDevtoolsHook, initCustomFormatter } from '@vue/runtime-dom'
 import { getGlobalThis } from '@vue/shared'
 
 export function initDev() {
@@ -12,5 +12,7 @@ export function initDev() {
       `You are running a development build of Vue.\n` +
         `Make sure to use the production build (*.prod.js) when deploying for production.`
     )
+
+    initCustomFormatter()
   }
 }