diff --git a/jest.config.js b/jest.config.js index 8312acd5..72e2205f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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 diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index e45fd775..2ac9b8a4 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -247,6 +247,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { 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 index 00000000..706cef69 --- /dev/null +++ b/packages/runtime-core/src/customFormatter.ts @@ -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 = {} + 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] + } +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 26c27d54..ca1cadf2 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -93,6 +93,7 @@ export { setTransitionHooks, getTransitionRawChildren } from './components/BaseTransition' +export { initCustomFormatter } from './customFormatter' // For devtools export { devtools, setDevtoolsHook } from './devtools' diff --git a/packages/vue/src/dev.ts b/packages/vue/src/dev.ts index bfa590fb..4f26d0e3 100644 --- a/packages/vue/src/dev.ts +++ b/packages/vue/src/dev.ts @@ -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() } }