nuxt-devtools-observatory 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dist/assets/index-BBp7Dvrp.css +1 -0
- package/client/dist/assets/index-mEAMrJUv.js +17 -0
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +1 -0
- package/client/src/views/ComposableTracker.vue +3 -0
- package/client/src/views/FetchDashboard.vue +40 -20
- package/client/src/views/ProvideInjectGraph.vue +252 -58
- package/client/src/views/RenderHeatmap.vue +705 -157
- package/client/src/views/TransitionTimeline.vue +7 -2
- package/dist/module.json +1 -1
- package/dist/runtime/composables/provide-inject-registry.js +4 -0
- package/dist/runtime/composables/render-registry.d.ts +1 -0
- package/dist/runtime/composables/render-registry.js +48 -2
- package/package.json +1 -1
- package/client/dist/assets/index-CVI-arV5.js +0 -17
- package/client/dist/assets/index-RdqCF5ft.css +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
2
|
+
import { ref, computed, watch } from 'vue'
|
|
3
3
|
import { useObservatoryData, type InjectEntry, type ProvideEntry } from '../stores/observatory'
|
|
4
4
|
|
|
5
5
|
interface TreeNodeData {
|
|
6
6
|
id: string
|
|
7
7
|
label: string
|
|
8
8
|
type: 'provider' | 'consumer' | 'both' | 'error'
|
|
9
|
-
provides: Array<{ key: string; val: string; reactive: boolean }>
|
|
9
|
+
provides: Array<{ key: string; val: string; raw: unknown; reactive: boolean; complex: boolean }>
|
|
10
10
|
injects: Array<{ key: string; from: string | null; ok: boolean }>
|
|
11
11
|
children: TreeNodeData[]
|
|
12
12
|
}
|
|
@@ -62,6 +62,47 @@ function stringifyValue(value: unknown) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function isComplexValue(value: unknown) {
|
|
66
|
+
return typeof value === 'object' && value !== null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatValuePreview(value: unknown) {
|
|
70
|
+
if (value === null) {
|
|
71
|
+
return 'null'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return `Array(${value.length})`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof value === 'object') {
|
|
79
|
+
const keys = Object.keys(value as Record<string, unknown>)
|
|
80
|
+
return keys.length ? `{ ${keys.join(', ')} }` : '{}'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return stringifyValue(value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatValueDetail(value: unknown) {
|
|
87
|
+
if (!isComplexValue(value)) {
|
|
88
|
+
return stringifyValue(value)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(value, null, 2)
|
|
93
|
+
} catch {
|
|
94
|
+
return stringifyValue(value)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function provideValueId(nodeId: string, key: string, index: number) {
|
|
99
|
+
return `${nodeId}:${key}:${index}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function basename(file: string) {
|
|
103
|
+
return file.split('/').pop() ?? file
|
|
104
|
+
}
|
|
105
|
+
|
|
65
106
|
function componentId(entry: ProvideEntry | InjectEntry) {
|
|
66
107
|
return String(entry.componentUid)
|
|
67
108
|
}
|
|
@@ -80,7 +121,7 @@ const nodes = computed<TreeNodeData[]>(() => {
|
|
|
80
121
|
|
|
81
122
|
const created: TreeNodeData = {
|
|
82
123
|
id,
|
|
83
|
-
label: entry.componentFile,
|
|
124
|
+
label: basename(entry.componentFile),
|
|
84
125
|
type: 'consumer',
|
|
85
126
|
provides: [],
|
|
86
127
|
injects: [],
|
|
@@ -96,8 +137,10 @@ const nodes = computed<TreeNodeData[]>(() => {
|
|
|
96
137
|
const node = ensureNode(entry)
|
|
97
138
|
node.provides.push({
|
|
98
139
|
key: entry.key,
|
|
99
|
-
val:
|
|
140
|
+
val: formatValuePreview(entry.valueSnapshot),
|
|
141
|
+
raw: entry.valueSnapshot,
|
|
100
142
|
reactive: entry.isReactive,
|
|
143
|
+
complex: isComplexValue(entry.valueSnapshot),
|
|
101
144
|
})
|
|
102
145
|
}
|
|
103
146
|
|
|
@@ -140,6 +183,23 @@ const nodes = computed<TreeNodeData[]>(() => {
|
|
|
140
183
|
|
|
141
184
|
const activeFilter = ref('all')
|
|
142
185
|
const selectedNode = ref<TreeNodeData | null>(null)
|
|
186
|
+
const expandedProvideValues = ref<Set<string>>(new Set())
|
|
187
|
+
|
|
188
|
+
watch(selectedNode, () => {
|
|
189
|
+
expandedProvideValues.value = new Set()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
function toggleProvideValue(id: string) {
|
|
193
|
+
const next = new Set(expandedProvideValues.value)
|
|
194
|
+
|
|
195
|
+
if (next.has(id)) {
|
|
196
|
+
next.delete(id)
|
|
197
|
+
} else {
|
|
198
|
+
next.add(id)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
expandedProvideValues.value = next
|
|
202
|
+
}
|
|
143
203
|
|
|
144
204
|
const allKeys = computed(() => {
|
|
145
205
|
const keys = new Set<string>()
|
|
@@ -157,6 +217,45 @@ const allKeys = computed(() => {
|
|
|
157
217
|
return [...keys]
|
|
158
218
|
})
|
|
159
219
|
|
|
220
|
+
const visibleNodes = computed<TreeNodeData[]>(() => {
|
|
221
|
+
function prune(node: TreeNodeData): TreeNodeData | null {
|
|
222
|
+
const visibleChildren = node.children.map(prune).filter(Boolean) as TreeNodeData[]
|
|
223
|
+
const selfMatches = matchesFilter(node, activeFilter.value)
|
|
224
|
+
|
|
225
|
+
if (!selfMatches && !visibleChildren.length) {
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
...node,
|
|
231
|
+
children: visibleChildren,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return nodes.value.map(prune).filter(Boolean) as TreeNodeData[]
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
watch([visibleNodes, selectedNode], ([currentNodes, currentSelected]) => {
|
|
239
|
+
if (!currentSelected) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ids = new Set<string>()
|
|
244
|
+
|
|
245
|
+
function collect(nodesToVisit: TreeNodeData[]) {
|
|
246
|
+
nodesToVisit.forEach((node) => {
|
|
247
|
+
ids.add(node.id)
|
|
248
|
+
collect(node.children)
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
collect(currentNodes)
|
|
253
|
+
|
|
254
|
+
if (!ids.has(currentSelected.id)) {
|
|
255
|
+
selectedNode.value = null
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
160
259
|
const layout = computed<LayoutNode[]>(() => {
|
|
161
260
|
const flat: LayoutNode[] = []
|
|
162
261
|
const pad = H_GAP
|
|
@@ -183,7 +282,7 @@ const layout = computed<LayoutNode[]>(() => {
|
|
|
183
282
|
|
|
184
283
|
let left = pad
|
|
185
284
|
|
|
186
|
-
for (const root of
|
|
285
|
+
for (const root of visibleNodes.value) {
|
|
187
286
|
const leaves = countLeaves(root)
|
|
188
287
|
place(root, 0, left, null)
|
|
189
288
|
left += leaves * (NODE_W + H_GAP) + H_GAP * 2
|
|
@@ -192,7 +291,7 @@ const layout = computed<LayoutNode[]>(() => {
|
|
|
192
291
|
return flat
|
|
193
292
|
})
|
|
194
293
|
|
|
195
|
-
const canvasW = computed(() => layout.value.reduce((max, node) => Math.max(max, node.x + NODE_W / 2 + 20),
|
|
294
|
+
const canvasW = computed(() => layout.value.reduce((max, node) => Math.max(max, node.x + NODE_W / 2 + 20), 520))
|
|
196
295
|
const canvasH = computed(() => layout.value.reduce((max, node) => Math.max(max, node.y + NODE_H / 2 + 20), 200))
|
|
197
296
|
|
|
198
297
|
const edges = computed<Edge[]>(() => {
|
|
@@ -243,38 +342,40 @@ const edges = computed<Edge[]>(() => {
|
|
|
243
342
|
<span class="dot" style="background: var(--red)"></span>
|
|
244
343
|
<span>missing provider</span>
|
|
245
344
|
</div>
|
|
246
|
-
<div
|
|
247
|
-
<
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
'is-selected': selectedNode?.id === layoutNode.data.id
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<span v-if="layoutNode.data.injects.some((entry) => !entry.ok)" class="badge badge-err badge-xs">!</span>
|
|
345
|
+
<div v-if="layout.length" class="canvas-stage">
|
|
346
|
+
<div class="canvas-wrap" :style="{ width: `${canvasW}px`, height: `${canvasH}px` }">
|
|
347
|
+
<svg class="edges-svg" :width="canvasW" :height="canvasH" :viewBox="`0 0 ${canvasW} ${canvasH}`">
|
|
348
|
+
<path
|
|
349
|
+
v-for="edge in edges"
|
|
350
|
+
:key="edge.id"
|
|
351
|
+
:d="`M ${edge.x1},${edge.y1} C ${edge.x1},${(edge.y1 + edge.y2) / 2} ${edge.x2},${(edge.y1 + edge.y2) / 2} ${edge.x2},${edge.y2}`"
|
|
352
|
+
class="edge"
|
|
353
|
+
fill="none"
|
|
354
|
+
/>
|
|
355
|
+
</svg>
|
|
356
|
+
<div
|
|
357
|
+
v-for="layoutNode in layout"
|
|
358
|
+
:key="layoutNode.data.id"
|
|
359
|
+
class="graph-node"
|
|
360
|
+
:class="{ 'is-selected': selectedNode?.id === layoutNode.data.id }"
|
|
361
|
+
:style="{
|
|
362
|
+
left: `${layoutNode.x - NODE_W / 2}px`,
|
|
363
|
+
top: `${layoutNode.y - NODE_H / 2}px`,
|
|
364
|
+
width: `${NODE_W}px`,
|
|
365
|
+
'--node-color': nodeColor(layoutNode.data),
|
|
366
|
+
}"
|
|
367
|
+
@click="selectedNode = layoutNode.data"
|
|
368
|
+
>
|
|
369
|
+
<span class="node-dot" :style="{ background: nodeColor(layoutNode.data) }"></span>
|
|
370
|
+
<span class="mono node-label">{{ layoutNode.data.label }}</span>
|
|
371
|
+
<span v-if="layoutNode.data.provides.length" class="badge badge-ok badge-xs">+{{ layoutNode.data.provides.length }}</span>
|
|
372
|
+
<span v-if="layoutNode.data.injects.some((entry) => !entry.ok)" class="badge badge-err badge-xs">!</span>
|
|
373
|
+
</div>
|
|
276
374
|
</div>
|
|
277
375
|
</div>
|
|
376
|
+
<div v-else class="graph-empty">
|
|
377
|
+
{{ connected ? 'No components match the current provide/inject filter.' : 'Waiting for connection to the Nuxt app…' }}
|
|
378
|
+
</div>
|
|
278
379
|
</div>
|
|
279
380
|
|
|
280
381
|
<div v-if="selectedNode" class="detail-panel">
|
|
@@ -283,26 +384,45 @@ const edges = computed<Edge[]>(() => {
|
|
|
283
384
|
<button @click="selectedNode = null">×</button>
|
|
284
385
|
</div>
|
|
285
386
|
|
|
286
|
-
<div v-if="selectedNode.provides.length">
|
|
387
|
+
<div v-if="selectedNode.provides.length" class="detail-section">
|
|
287
388
|
<div class="section-label">provides ({{ selectedNode.provides.length }})</div>
|
|
288
|
-
<div
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
389
|
+
<div class="detail-list">
|
|
390
|
+
<div
|
|
391
|
+
v-for="(entry, index) in selectedNode.provides"
|
|
392
|
+
:key="provideValueId(selectedNode.id, entry.key, index)"
|
|
393
|
+
class="provide-row"
|
|
394
|
+
>
|
|
395
|
+
<div class="row-main">
|
|
396
|
+
<span class="mono text-sm row-key">{{ entry.key }}</span>
|
|
397
|
+
<span class="mono text-sm muted row-value-preview" :title="entry.val">{{ entry.val }}</span>
|
|
398
|
+
<span class="badge" :class="entry.reactive ? 'badge-ok' : 'badge-gray'">
|
|
399
|
+
{{ entry.reactive ? 'reactive' : 'static' }}
|
|
400
|
+
</span>
|
|
401
|
+
<button
|
|
402
|
+
v-if="entry.complex"
|
|
403
|
+
class="row-toggle mono"
|
|
404
|
+
@click="toggleProvideValue(provideValueId(selectedNode.id, entry.key, index))"
|
|
405
|
+
>
|
|
406
|
+
{{ expandedProvideValues.has(provideValueId(selectedNode.id, entry.key, index)) ? 'hide' : 'view' }}
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
409
|
+
<pre
|
|
410
|
+
v-if="entry.complex && expandedProvideValues.has(provideValueId(selectedNode.id, entry.key, index))"
|
|
411
|
+
class="value-box"
|
|
412
|
+
>{{ formatValueDetail(entry.raw) }}</pre>
|
|
413
|
+
</div>
|
|
296
414
|
</div>
|
|
297
415
|
</div>
|
|
298
416
|
|
|
299
|
-
<div v-if="selectedNode.injects.length" :style="{ marginTop: selectedNode.provides.length ? '10px' : '0' }">
|
|
417
|
+
<div v-if="selectedNode.injects.length" class="detail-section" :style="{ marginTop: selectedNode.provides.length ? '10px' : '0' }">
|
|
300
418
|
<div class="section-label">injects ({{ selectedNode.injects.length }})</div>
|
|
301
|
-
<div
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
419
|
+
<div class="detail-list">
|
|
420
|
+
<div v-for="entry in selectedNode.injects" :key="entry.key" class="inject-row" :class="{ 'inject-miss': !entry.ok }">
|
|
421
|
+
<span class="mono text-sm row-key">{{ entry.key }}</span>
|
|
422
|
+
<span v-if="entry.ok" class="badge badge-ok">resolved</span>
|
|
423
|
+
<span v-else class="badge badge-err">no provider</span>
|
|
424
|
+
<span class="mono muted text-sm row-from" :title="entry.from ?? 'undefined'">{{ entry.from ?? 'undefined' }}</span>
|
|
425
|
+
</div>
|
|
306
426
|
</div>
|
|
307
427
|
</div>
|
|
308
428
|
|
|
@@ -361,6 +481,13 @@ const edges = computed<Edge[]>(() => {
|
|
|
361
481
|
margin-bottom: 12px;
|
|
362
482
|
}
|
|
363
483
|
|
|
484
|
+
.canvas-stage {
|
|
485
|
+
display: flex;
|
|
486
|
+
justify-content: center;
|
|
487
|
+
align-items: flex-start;
|
|
488
|
+
min-width: 100%;
|
|
489
|
+
}
|
|
490
|
+
|
|
364
491
|
.dot {
|
|
365
492
|
width: 8px;
|
|
366
493
|
height: 8px;
|
|
@@ -413,11 +540,6 @@ const edges = computed<Edge[]>(() => {
|
|
|
413
540
|
background: color-mix(in srgb, var(--node-color) 8%, transparent);
|
|
414
541
|
}
|
|
415
542
|
|
|
416
|
-
.graph-node.is-dimmed {
|
|
417
|
-
opacity: 0.2;
|
|
418
|
-
pointer-events: none;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
543
|
.node-dot {
|
|
422
544
|
width: 7px;
|
|
423
545
|
height: 7px;
|
|
@@ -448,6 +570,7 @@ const edges = computed<Edge[]>(() => {
|
|
|
448
570
|
display: flex;
|
|
449
571
|
flex-direction: column;
|
|
450
572
|
gap: 4px;
|
|
573
|
+
min-height: 0;
|
|
451
574
|
}
|
|
452
575
|
|
|
453
576
|
.detail-empty {
|
|
@@ -462,6 +585,15 @@ const edges = computed<Edge[]>(() => {
|
|
|
462
585
|
flex-shrink: 0;
|
|
463
586
|
}
|
|
464
587
|
|
|
588
|
+
.graph-empty {
|
|
589
|
+
display: flex;
|
|
590
|
+
align-items: center;
|
|
591
|
+
justify-content: center;
|
|
592
|
+
min-height: 180px;
|
|
593
|
+
color: var(--text3);
|
|
594
|
+
font-size: 12px;
|
|
595
|
+
}
|
|
596
|
+
|
|
465
597
|
.detail-header {
|
|
466
598
|
display: flex;
|
|
467
599
|
align-items: center;
|
|
@@ -478,16 +610,70 @@ const edges = computed<Edge[]>(() => {
|
|
|
478
610
|
margin: 8px 0 5px;
|
|
479
611
|
}
|
|
480
612
|
|
|
613
|
+
.detail-section {
|
|
614
|
+
display: flex;
|
|
615
|
+
flex-direction: column;
|
|
616
|
+
min-height: 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.detail-list {
|
|
620
|
+
display: flex;
|
|
621
|
+
flex-direction: column;
|
|
622
|
+
gap: 3px;
|
|
623
|
+
overflow: auto;
|
|
624
|
+
max-height: 220px;
|
|
625
|
+
padding-right: 2px;
|
|
626
|
+
}
|
|
627
|
+
|
|
481
628
|
.provide-row {
|
|
482
629
|
display: flex;
|
|
483
|
-
|
|
484
|
-
gap:
|
|
630
|
+
flex-direction: column;
|
|
631
|
+
gap: 6px;
|
|
485
632
|
padding: 5px 8px;
|
|
486
633
|
background: var(--bg2);
|
|
487
634
|
border-radius: var(--radius);
|
|
488
635
|
margin-bottom: 3px;
|
|
489
636
|
}
|
|
490
637
|
|
|
638
|
+
.row-main {
|
|
639
|
+
display: flex;
|
|
640
|
+
align-items: center;
|
|
641
|
+
gap: 8px;
|
|
642
|
+
min-width: 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.row-key {
|
|
646
|
+
min-width: 100px;
|
|
647
|
+
color: var(--text2);
|
|
648
|
+
flex-shrink: 0;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.row-value-preview {
|
|
652
|
+
flex: 1;
|
|
653
|
+
min-width: 0;
|
|
654
|
+
overflow: hidden;
|
|
655
|
+
text-overflow: ellipsis;
|
|
656
|
+
white-space: nowrap;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.row-toggle {
|
|
660
|
+
padding: 2px 8px;
|
|
661
|
+
font-size: 10px;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.value-box {
|
|
665
|
+
font-family: var(--mono);
|
|
666
|
+
font-size: 11px;
|
|
667
|
+
color: var(--text2);
|
|
668
|
+
background: rgb(0 0 0 / 10%);
|
|
669
|
+
border-radius: var(--radius);
|
|
670
|
+
padding: 8px 10px;
|
|
671
|
+
white-space: pre-wrap;
|
|
672
|
+
word-break: break-word;
|
|
673
|
+
overflow: auto;
|
|
674
|
+
max-height: 180px;
|
|
675
|
+
}
|
|
676
|
+
|
|
491
677
|
.inject-row {
|
|
492
678
|
display: flex;
|
|
493
679
|
align-items: center;
|
|
@@ -498,6 +684,14 @@ const edges = computed<Edge[]>(() => {
|
|
|
498
684
|
margin-bottom: 3px;
|
|
499
685
|
}
|
|
500
686
|
|
|
687
|
+
.row-from {
|
|
688
|
+
margin-left: auto;
|
|
689
|
+
min-width: 0;
|
|
690
|
+
overflow: hidden;
|
|
691
|
+
text-overflow: ellipsis;
|
|
692
|
+
white-space: nowrap;
|
|
693
|
+
}
|
|
694
|
+
|
|
501
695
|
.inject-miss {
|
|
502
696
|
background: rgb(226 75 74 / 8%);
|
|
503
697
|
}
|