Skip to content

Commit 6ba7ba4

Browse files
committed
feat: custom formatters
1 parent ffdb05e commit 6ba7ba4

File tree

5 files changed

+208
-1
lines changed

5 files changed

+208
-1
lines changed

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module.exports = {
2222
'!packages/template-explorer/**',
2323
'!packages/size-check/**',
2424
'!packages/runtime-core/src/profiling.ts',
25+
'!packages/runtome-core/src/customFormatter.ts',
2526
// DOM transitions are tested via e2e so no coverage is collected
2627
'!packages/runtime-dom/src/components/Transition*',
2728
// only called in browsers

packages/runtime-core/src/componentPublicInstance.ts

+5
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
247247
return true
248248
}
249249

250+
// for internal formatters to know that this is a Vue instance
251+
if (__DEV__ && key === '__isVue') {
252+
return true
253+
}
254+
250255
// data / props / ctx
251256
// This getter gets called for every property access on the render context
252257
// during render and is a major hotspot. The most expensive part of this
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { isReactive, isReadonly, isRef, Ref, toRaw } from '@vue/reactivity'
2+
import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared'
3+
import { ComponentInternalInstance, ComponentOptions } from './component'
4+
import { ComponentPublicInstance } from './componentPublicInstance'
5+
6+
export function initCustomFormatter() {
7+
if (!__DEV__ || !__BROWSER__) {
8+
return
9+
}
10+
11+
const vueStyle = { style: 'color:#3ba776' }
12+
const numberStyle = { style: 'color:#0b1bc9' }
13+
const stringStyle = { style: 'color:#b62e24' }
14+
const keywordStyle = { style: 'color:#9d288c' }
15+
16+
// custom formatter for Chrome
17+
// https://www.mattzeunert.com/2016/02/19/custom-chrome-devtools-object-formatters.html
18+
const formatter = {
19+
header(obj: unknown) {
20+
// TODO also format ComponentPublicInstance & ctx.slots/attrs in setup
21+
if (!isObject(obj)) {
22+
return null
23+
}
24+
25+
if (obj.__isVue) {
26+
return ['div', vueStyle, `VueInstance`]
27+
} else if (isRef(obj)) {
28+
return [
29+
'div',
30+
{},
31+
['span', vueStyle, genRefFlag(obj)],
32+
'<',
33+
formatValue(obj.value),
34+
`>`
35+
]
36+
} else if (isReactive(obj)) {
37+
return [
38+
'div',
39+
{},
40+
['span', vueStyle, 'Reactive'],
41+
'<',
42+
formatValue(obj),
43+
`>${isReadonly(obj) ? ` (readonly)` : ``}`
44+
]
45+
} else if (isReadonly(obj)) {
46+
return [
47+
'div',
48+
{},
49+
['span', vueStyle, 'Readonly'],
50+
'<',
51+
formatValue(obj),
52+
'>'
53+
]
54+
}
55+
return null
56+
},
57+
hasBody(obj: unknown) {
58+
return obj && (obj as any).__isVue
59+
},
60+
body(obj: unknown) {
61+
if (obj && (obj as any).__isVue) {
62+
return [
63+
'div',
64+
{},
65+
...formatInstance((obj as ComponentPublicInstance).$)
66+
]
67+
}
68+
}
69+
}
70+
71+
function formatInstance(instance: ComponentInternalInstance) {
72+
const blocks = []
73+
if (instance.type.props && instance.props) {
74+
blocks.push(createInstanceBlock('props', toRaw(instance.props)))
75+
}
76+
if (instance.setupState !== EMPTY_OBJ) {
77+
blocks.push(createInstanceBlock('setup', instance.setupState))
78+
}
79+
if (instance.data !== EMPTY_OBJ) {
80+
blocks.push(createInstanceBlock('data', toRaw(instance.data)))
81+
}
82+
const computed = extractKeys(instance, 'computed')
83+
if (computed) {
84+
blocks.push(createInstanceBlock('computed', computed))
85+
}
86+
const injected = extractKeys(instance, 'inject')
87+
if (injected) {
88+
blocks.push(createInstanceBlock('injected', injected))
89+
}
90+
91+
blocks.push([
92+
'div',
93+
{},
94+
[
95+
'span',
96+
{
97+
style: keywordStyle.style + ';opacity:0.66'
98+
},
99+
'$ (internal): '
100+
],
101+
['object', { object: instance }]
102+
])
103+
return blocks
104+
}
105+
106+
function createInstanceBlock(type: string, target: any) {
107+
target = extend({}, target)
108+
if (!Object.keys(target).length) {
109+
return ['span', {}]
110+
}
111+
return [
112+
'div',
113+
{ style: 'line-height:1.25em;margin-bottom:0.6em' },
114+
[
115+
'div',
116+
{
117+
style: 'color:#476582'
118+
},
119+
type
120+
],
121+
[
122+
'div',
123+
{
124+
style: 'padding-left:1.25em'
125+
},
126+
...Object.keys(target).map(key => {
127+
return [
128+
'div',
129+
{},
130+
['span', keywordStyle, key + ': '],
131+
formatValue(target[key], false)
132+
]
133+
})
134+
]
135+
]
136+
}
137+
138+
function formatValue(v: unknown, asRaw = true) {
139+
if (typeof v === 'number') {
140+
return ['span', numberStyle, v]
141+
} else if (typeof v === 'string') {
142+
return ['span', stringStyle, JSON.stringify(v)]
143+
} else if (typeof v === 'boolean') {
144+
return ['span', keywordStyle, v]
145+
} else if (isObject(v)) {
146+
return ['object', { object: asRaw ? toRaw(v) : v }]
147+
} else {
148+
return ['span', stringStyle, String(v)]
149+
}
150+
}
151+
152+
function extractKeys(instance: ComponentInternalInstance, type: string) {
153+
const Comp = instance.type
154+
if (isFunction(Comp)) {
155+
return
156+
}
157+
const extracted: Record<string, any> = {}
158+
for (const key in instance.ctx) {
159+
if (isKeyOfType(Comp, key, type)) {
160+
extracted[key] = instance.ctx[key]
161+
}
162+
}
163+
return extracted
164+
}
165+
166+
function isKeyOfType(Comp: ComponentOptions, key: string, type: string) {
167+
const opts = Comp[type]
168+
if (
169+
(isArray(opts) && opts.includes(key)) ||
170+
(isObject(opts) && key in opts)
171+
) {
172+
return true
173+
}
174+
if (Comp.extends && isKeyOfType(Comp.extends, key, type)) {
175+
return true
176+
}
177+
if (Comp.mixins && Comp.mixins.some(m => isKeyOfType(m, key, type))) {
178+
return true
179+
}
180+
}
181+
182+
function genRefFlag(v: Ref) {
183+
if (v._shallow) {
184+
return `ShallowRef`
185+
}
186+
if ((v as any).effect) {
187+
return `ComputedRef`
188+
}
189+
return `Ref`
190+
}
191+
192+
/* eslint-disable no-restricted-globals */
193+
if ((window as any).devtoolsFormatters) {
194+
;(window as any).devtoolsFormatters.push(formatter)
195+
} else {
196+
;(window as any).devtoolsFormatters = [formatter]
197+
}
198+
}

packages/runtime-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export {
9393
setTransitionHooks,
9494
getTransitionRawChildren
9595
} from './components/BaseTransition'
96+
export { initCustomFormatter } from './customFormatter'
9697

9798
// For devtools
9899
export { devtools, setDevtoolsHook } from './devtools'

packages/vue/src/dev.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { setDevtoolsHook } from '@vue/runtime-dom'
1+
import { setDevtoolsHook, initCustomFormatter } from '@vue/runtime-dom'
22
import { getGlobalThis } from '@vue/shared'
33

44
export function initDev() {
@@ -12,5 +12,7 @@ export function initDev() {
1212
`You are running a development build of Vue.\n` +
1313
`Make sure to use the production build (*.prod.js) when deploying for production.`
1414
)
15+
16+
initCustomFormatter()
1517
}
1618
}

0 commit comments

Comments
 (0)