nuxt-devtools-observatory 0.1.6 → 0.1.7
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-CgZM5MBX.js +17 -0
- package/client/dist/assets/{index-CjQU78-e.css → index-mu192QeW.css} +1 -1
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +144 -122
- package/client/src/views/ComposableTracker.vue +99 -58
- package/client/src/views/FetchDashboard.vue +102 -90
- package/client/src/views/ProvideInjectGraph.vue +149 -76
- package/client/src/views/RenderHeatmap.vue +194 -46
- package/dist/module.json +1 -1
- package/dist/module.mjs +42 -21
- package/dist/runtime/composables/fetch-registry.js +4 -1
- package/dist/runtime/composables/render-registry.js +50 -9
- package/package.json +5 -3
- package/client/dist/assets/index-lG5ffIvt.js +0 -17
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
* useObservatoryData — live bridge between the Nuxt app and the client SPA.
|
|
3
|
-
*
|
|
4
|
-
* The client SPA runs at localhost:4949 (cross-origin from the Nuxt app at
|
|
5
|
-
* localhost:3000). Direct window.top property access is blocked by the browser.
|
|
6
|
-
* However postMessage IS allowed cross-origin:
|
|
7
|
-
*
|
|
8
|
-
* iframe (4949) → window.top.postMessage({ type: 'observatory:request' }) → Nuxt page (3000)
|
|
9
|
-
* Nuxt plugin.ts → event.source.postMessage({ type: 'observatory:snapshot', data }) → iframe
|
|
10
|
-
*
|
|
11
|
-
* The plugin.ts listener is registered immediately on plugin init (not deferred
|
|
12
|
-
* to app:mounted) so requests sent before full hydration are answered correctly.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { ref, onUnmounted } from 'vue'
|
|
1
|
+
import { ref } from 'vue'
|
|
16
2
|
|
|
17
|
-
|
|
18
|
-
id: string
|
|
19
|
-
transitionName: string
|
|
20
|
-
parentComponent: string
|
|
21
|
-
direction: 'enter' | 'leave'
|
|
22
|
-
phase: 'entering' | 'entered' | 'leaving' | 'left' | 'enter-cancelled' | 'leave-cancelled' | 'interrupted'
|
|
23
|
-
startTime: number
|
|
24
|
-
endTime?: number
|
|
25
|
-
durationMs?: number
|
|
26
|
-
cancelled: boolean
|
|
27
|
-
appear: boolean
|
|
28
|
-
mode?: string
|
|
29
|
-
}
|
|
3
|
+
const POLL_MS = 500
|
|
30
4
|
|
|
31
5
|
export interface FetchEntry {
|
|
32
6
|
id: string
|
|
@@ -34,130 +8,178 @@ export interface FetchEntry {
|
|
|
34
8
|
url: string
|
|
35
9
|
status: 'pending' | 'ok' | 'error' | 'cached'
|
|
36
10
|
origin: 'ssr' | 'csr'
|
|
11
|
+
startTime: number
|
|
12
|
+
endTime?: number
|
|
37
13
|
ms?: number
|
|
38
14
|
size?: number
|
|
39
15
|
cached: boolean
|
|
40
16
|
payload?: unknown
|
|
17
|
+
error?: unknown
|
|
41
18
|
file?: string
|
|
42
19
|
line?: number
|
|
43
|
-
startOffset?: number
|
|
44
20
|
}
|
|
45
21
|
|
|
46
|
-
interface
|
|
22
|
+
export interface ProvideEntry {
|
|
23
|
+
key: string
|
|
24
|
+
componentName: string
|
|
25
|
+
componentFile: string
|
|
26
|
+
componentUid: number
|
|
27
|
+
parentUid?: number
|
|
28
|
+
parentFile?: string
|
|
29
|
+
isReactive: boolean
|
|
30
|
+
valueSnapshot: unknown
|
|
31
|
+
line: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InjectEntry {
|
|
35
|
+
key: string
|
|
36
|
+
componentName: string
|
|
37
|
+
componentFile: string
|
|
38
|
+
componentUid: number
|
|
39
|
+
parentUid?: number
|
|
40
|
+
parentFile?: string
|
|
41
|
+
resolved: boolean
|
|
42
|
+
resolvedFromFile?: string
|
|
43
|
+
resolvedFromUid?: number
|
|
44
|
+
line: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProvideInjectSnapshot {
|
|
48
|
+
provides: ProvideEntry[]
|
|
49
|
+
injects: InjectEntry[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ComposableEntry {
|
|
47
53
|
id: string
|
|
48
54
|
name: string
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
componentFile: string
|
|
56
|
+
componentUid: number
|
|
51
57
|
status: 'mounted' | 'unmounted'
|
|
52
58
|
leak: boolean
|
|
53
59
|
leakReason?: string
|
|
54
|
-
refs:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lifecycle: {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
injects: Array<{ key: string; from: string | null; ok: boolean }>
|
|
66
|
-
children: ProvideInjectNode[]
|
|
60
|
+
refs: Record<string, { type: 'ref' | 'computed' | 'reactive'; value: unknown }>
|
|
61
|
+
watcherCount: number
|
|
62
|
+
intervalCount: number
|
|
63
|
+
lifecycle: {
|
|
64
|
+
hasOnMounted: boolean
|
|
65
|
+
hasOnUnmounted: boolean
|
|
66
|
+
watchersCleaned: boolean
|
|
67
|
+
intervalsCleaned: boolean
|
|
68
|
+
}
|
|
69
|
+
file: string
|
|
70
|
+
line: number
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
interface
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
export interface RenderEntry {
|
|
74
|
+
uid: number
|
|
75
|
+
name: string
|
|
72
76
|
file: string
|
|
73
77
|
renders: number
|
|
78
|
+
totalMs: number
|
|
74
79
|
avgMs: number
|
|
75
|
-
triggers: string
|
|
76
|
-
|
|
80
|
+
triggers: Array<{ key: string; type: string; timestamp: number }>
|
|
81
|
+
rect?: { x: number; y: number; width: number; height: number; top: number; left: number }
|
|
82
|
+
children: number[]
|
|
83
|
+
parentUid?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TransitionEntry {
|
|
87
|
+
id: string
|
|
88
|
+
transitionName: string
|
|
89
|
+
parentComponent: string
|
|
90
|
+
direction: 'enter' | 'leave'
|
|
91
|
+
phase: 'entering' | 'entered' | 'leaving' | 'left' | 'enter-cancelled' | 'leave-cancelled' | 'interrupted'
|
|
92
|
+
startTime: number
|
|
93
|
+
endTime?: number
|
|
94
|
+
durationMs?: number
|
|
95
|
+
cancelled: boolean
|
|
96
|
+
appear: boolean
|
|
97
|
+
mode?: string
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
interface ObservatorySnapshot {
|
|
80
|
-
transitions?: TransitionEntry[]
|
|
81
101
|
fetch?: FetchEntry[]
|
|
102
|
+
provideInject?: ProvideInjectSnapshot
|
|
82
103
|
composables?: ComposableEntry[]
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
renders?: RenderEntry[]
|
|
105
|
+
transitions?: TransitionEntry[]
|
|
85
106
|
}
|
|
86
107
|
|
|
87
|
-
const
|
|
108
|
+
const fetchEntries = ref<FetchEntry[]>([])
|
|
109
|
+
const provideInject = ref<ProvideInjectSnapshot>({ provides: [], injects: [] })
|
|
110
|
+
const composables = ref<ComposableEntry[]>([])
|
|
111
|
+
const renders = ref<RenderEntry[]>([])
|
|
112
|
+
const transitions = ref<TransitionEntry[]>([])
|
|
113
|
+
const connected = ref(false)
|
|
88
114
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
let started = false
|
|
116
|
+
let parentOrigin = '*'
|
|
117
|
+
|
|
118
|
+
function cloneArray<T>(value: T[] | undefined): T[] {
|
|
119
|
+
return value ? value.map((item) => ({ ...item })) : []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getParentOrigin() {
|
|
123
|
+
if (typeof document === 'undefined' || !document.referrer) {
|
|
124
|
+
return '*'
|
|
99
125
|
}
|
|
100
126
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let data: ObservatorySnapshot | null = null
|
|
107
|
-
|
|
108
|
-
if (typeof event.data.data === 'string') {
|
|
109
|
-
try {
|
|
110
|
-
data = JSON.parse(event.data.data)
|
|
111
|
-
} catch (err) {
|
|
112
|
-
console.warn('Failed to parse observatory snapshot:', err)
|
|
113
|
-
|
|
114
|
-
data = null
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
data = event.data.data as ObservatorySnapshot
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
transitions.value = data?.transitions ?? []
|
|
121
|
-
fetches.value = data?.fetch ?? []
|
|
122
|
-
composables.value = data?.composables ?? []
|
|
123
|
-
|
|
124
|
-
// Always guarantee provideInject.value is an array
|
|
125
|
-
const pi = data?.provideInject
|
|
126
|
-
|
|
127
|
-
if (Array.isArray(pi)) {
|
|
128
|
-
provideInject.value = pi
|
|
129
|
-
} else if (pi && typeof pi === 'object') {
|
|
130
|
-
// If registry returns { provides, injects }, build a single node
|
|
131
|
-
provideInject.value = [
|
|
132
|
-
{
|
|
133
|
-
provides: Array.isArray(pi.provides) ? pi.provides : [],
|
|
134
|
-
injects: Array.isArray(pi.injects) ? pi.injects : [],
|
|
135
|
-
id: 'root',
|
|
136
|
-
label: 'Provide/Inject Root',
|
|
137
|
-
type: 'both',
|
|
138
|
-
children: [],
|
|
139
|
-
},
|
|
140
|
-
]
|
|
141
|
-
} else {
|
|
142
|
-
provideInject.value = []
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!Array.isArray(provideInject.value)) {
|
|
146
|
-
provideInject.value = []
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
renders.value = data?.renders ?? []
|
|
150
|
-
connected.value = true
|
|
127
|
+
try {
|
|
128
|
+
return new URL(document.referrer).origin
|
|
129
|
+
} catch {
|
|
130
|
+
return '*'
|
|
151
131
|
}
|
|
132
|
+
}
|
|
152
133
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
134
|
+
function requestSnapshot() {
|
|
135
|
+
window.top?.postMessage({ type: 'observatory:request' }, parentOrigin)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function onMessage(event: MessageEvent) {
|
|
139
|
+
if (event.data?.type !== 'observatory:snapshot') {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (parentOrigin !== '*' && event.origin !== parentOrigin) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
156
146
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
const data = event.data.data as ObservatorySnapshot
|
|
148
|
+
fetchEntries.value = cloneArray(data.fetch)
|
|
149
|
+
provideInject.value = data.provideInject
|
|
150
|
+
? {
|
|
151
|
+
provides: cloneArray(data.provideInject.provides),
|
|
152
|
+
injects: cloneArray(data.provideInject.injects),
|
|
153
|
+
}
|
|
154
|
+
: { provides: [], injects: [] }
|
|
155
|
+
composables.value = cloneArray(data.composables)
|
|
156
|
+
renders.value = cloneArray(data.renders)
|
|
157
|
+
transitions.value = cloneArray(data.transitions)
|
|
158
|
+
connected.value = true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ensureStarted() {
|
|
162
|
+
if (started || typeof window === 'undefined') {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
started = true
|
|
167
|
+
parentOrigin = getParentOrigin()
|
|
168
|
+
window.addEventListener('message', onMessage)
|
|
169
|
+
window.setInterval(requestSnapshot, POLL_MS)
|
|
170
|
+
requestSnapshot()
|
|
171
|
+
}
|
|
161
172
|
|
|
162
|
-
|
|
173
|
+
export function useObservatoryData() {
|
|
174
|
+
ensureStarted()
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
fetch: fetchEntries,
|
|
178
|
+
provideInject,
|
|
179
|
+
composables,
|
|
180
|
+
renders,
|
|
181
|
+
transitions,
|
|
182
|
+
connected,
|
|
183
|
+
refresh: requestSnapshot,
|
|
184
|
+
}
|
|
163
185
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed
|
|
3
|
-
import { useObservatoryData } from '../stores/observatory'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { useObservatoryData, type ComposableEntry as RuntimeComposableEntry } from '../stores/observatory'
|
|
4
4
|
|
|
5
|
-
interface
|
|
5
|
+
interface ComposableViewEntry {
|
|
6
6
|
id: string
|
|
7
7
|
name: string
|
|
8
8
|
component: string
|
|
@@ -21,49 +21,89 @@ interface ComposableEntry {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const { composables } = useObservatoryData()
|
|
25
|
-
|
|
24
|
+
const { composables: rawEntries, connected } = useObservatoryData()
|
|
25
|
+
|
|
26
|
+
const entries = computed<ComposableViewEntry[]>(() => {
|
|
27
|
+
const groups = new Map<string, RuntimeComposableEntry[]>()
|
|
28
|
+
|
|
29
|
+
for (const entry of rawEntries.value) {
|
|
30
|
+
const key = `${entry.name}::${entry.componentFile}`
|
|
31
|
+
const list = groups.get(key) ?? []
|
|
32
|
+
list.push(entry)
|
|
33
|
+
groups.set(key, list)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [...groups.entries()].map(([key, group]) => {
|
|
37
|
+
const latest = [...group].sort((a, b) => b.line - a.line)[0]
|
|
38
|
+
const leakReasons = [...new Set(group.map((entry) => entry.leakReason).filter(Boolean))]
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
id: key,
|
|
42
|
+
name: latest.name,
|
|
43
|
+
component: latest.componentFile,
|
|
44
|
+
instances: group.length,
|
|
45
|
+
status: group.some((entry) => entry.status === 'mounted') ? 'mounted' : 'unmounted',
|
|
46
|
+
leak: group.some((entry) => entry.leak),
|
|
47
|
+
leakReason: leakReasons.join(' · ') || undefined,
|
|
48
|
+
refs: Object.entries(latest.refs).map(([refKey, refValue]) => ({
|
|
49
|
+
key: refKey,
|
|
50
|
+
type: refValue.type,
|
|
51
|
+
val: typeof refValue.value === 'string' ? refValue.value : JSON.stringify(refValue.value),
|
|
52
|
+
})),
|
|
53
|
+
watchers: group.reduce((sum, entry) => sum + entry.watcherCount, 0),
|
|
54
|
+
intervals: group.reduce((sum, entry) => sum + entry.intervalCount, 0),
|
|
55
|
+
lifecycle: {
|
|
56
|
+
onMounted: group.some((entry) => entry.lifecycle.hasOnMounted),
|
|
57
|
+
onUnmounted: group.some((entry) => entry.lifecycle.hasOnUnmounted),
|
|
58
|
+
watchersCleaned: group.every((entry) => entry.lifecycle.watchersCleaned),
|
|
59
|
+
intervalsCleaned: group.every((entry) => entry.lifecycle.intervalsCleaned),
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
26
64
|
|
|
27
65
|
const filter = ref('all')
|
|
28
66
|
const search = ref('')
|
|
29
67
|
const expanded = ref<string | null>(null)
|
|
30
68
|
|
|
31
|
-
const counts = computed
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
})
|
|
69
|
+
const counts = computed(() => ({
|
|
70
|
+
mounted: entries.value.filter((entry) => entry.status === 'mounted').length,
|
|
71
|
+
leaks: entries.value.filter((entry) => entry.leak).length,
|
|
72
|
+
}))
|
|
37
73
|
|
|
38
|
-
const filtered = computed
|
|
39
|
-
return entries.value.filter((
|
|
40
|
-
if (filter.value === 'leak' && !
|
|
74
|
+
const filtered = computed(() => {
|
|
75
|
+
return entries.value.filter((entry) => {
|
|
76
|
+
if (filter.value === 'leak' && !entry.leak) {
|
|
41
77
|
return false
|
|
42
|
-
} else {
|
|
43
|
-
if (filter.value === 'unmounted' && e.status !== 'unmounted') {
|
|
44
|
-
return false
|
|
45
|
-
} else {
|
|
46
|
-
const q = search.value.toLowerCase()
|
|
47
|
-
|
|
48
|
-
if (q && !e.name.toLowerCase().includes(q) && !e.component.toLowerCase().includes(q)) {
|
|
49
|
-
return false
|
|
50
|
-
} else {
|
|
51
|
-
return true
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
78
|
}
|
|
79
|
+
|
|
80
|
+
if (filter.value === 'unmounted' && entry.status !== 'unmounted') {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const q = search.value.toLowerCase()
|
|
85
|
+
|
|
86
|
+
if (q && !entry.name.toLowerCase().includes(q) && !entry.component.toLowerCase().includes(q)) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true
|
|
55
91
|
})
|
|
56
92
|
})
|
|
57
93
|
|
|
58
|
-
function lifecycleRows(
|
|
94
|
+
function lifecycleRows(entry: ComposableViewEntry) {
|
|
59
95
|
return [
|
|
60
|
-
{ label: 'onMounted', ok:
|
|
61
|
-
{ label: 'onUnmounted', ok:
|
|
62
|
-
{
|
|
96
|
+
{ label: 'onMounted', ok: entry.lifecycle.onMounted, status: entry.lifecycle.onMounted ? 'registered' : 'not used' },
|
|
97
|
+
{ label: 'onUnmounted', ok: entry.lifecycle.onUnmounted, status: entry.lifecycle.onUnmounted ? 'registered' : 'missing' },
|
|
98
|
+
{
|
|
99
|
+
label: 'watchers cleaned',
|
|
100
|
+
ok: entry.lifecycle.watchersCleaned,
|
|
101
|
+
status: entry.lifecycle.watchersCleaned ? 'all stopped' : 'NOT stopped',
|
|
102
|
+
},
|
|
63
103
|
{
|
|
64
104
|
label: 'intervals cleared',
|
|
65
|
-
ok:
|
|
66
|
-
status:
|
|
105
|
+
ok: entry.lifecycle.intervalsCleaned,
|
|
106
|
+
status: entry.lifecycle.intervalsCleaned ? 'all cleared' : 'NOT cleared',
|
|
67
107
|
},
|
|
68
108
|
]
|
|
69
109
|
}
|
|
@@ -86,7 +126,7 @@ function lifecycleRows(e: ComposableEntry) {
|
|
|
86
126
|
</div>
|
|
87
127
|
<div class="stat-card">
|
|
88
128
|
<div class="stat-label">instances</div>
|
|
89
|
-
<div class="stat-val">{{ entries.reduce((
|
|
129
|
+
<div class="stat-val">{{ entries.reduce((sum, entry) => sum + entry.instances, 0) }}</div>
|
|
90
130
|
</div>
|
|
91
131
|
</div>
|
|
92
132
|
|
|
@@ -104,52 +144,53 @@ function lifecycleRows(e: ComposableEntry) {
|
|
|
104
144
|
|
|
105
145
|
<div class="list">
|
|
106
146
|
<div
|
|
107
|
-
v-for="
|
|
108
|
-
:key="
|
|
147
|
+
v-for="entry in filtered"
|
|
148
|
+
:key="entry.id"
|
|
109
149
|
class="comp-card"
|
|
110
|
-
:class="{ leak:
|
|
111
|
-
@click="expanded = expanded ===
|
|
150
|
+
:class="{ leak: entry.leak, expanded: expanded === entry.id }"
|
|
151
|
+
@click="expanded = expanded === entry.id ? null : entry.id"
|
|
112
152
|
>
|
|
113
153
|
<div class="comp-header">
|
|
114
|
-
<span class="mono bold" style="font-size: 12px">{{
|
|
115
|
-
<span class="muted text-sm" style="margin-left: 4px">{{
|
|
154
|
+
<span class="mono bold" style="font-size: 12px">{{ entry.name }}</span>
|
|
155
|
+
<span class="muted text-sm" style="margin-left: 4px">{{ entry.component }}</span>
|
|
116
156
|
<div class="comp-meta">
|
|
117
|
-
<span v-if="
|
|
118
|
-
<span v-if="
|
|
119
|
-
{{
|
|
157
|
+
<span v-if="entry.instances > 1" class="muted text-sm">{{ entry.instances }}×</span>
|
|
158
|
+
<span v-if="entry.watchers > 0 && !entry.leak" class="badge badge-warn">
|
|
159
|
+
{{ entry.watchers }} watcher{{ entry.watchers > 1 ? 's' : '' }}
|
|
120
160
|
</span>
|
|
121
|
-
<span v-if="
|
|
122
|
-
{{
|
|
161
|
+
<span v-if="entry.intervals > 0 && !entry.leak" class="badge badge-warn">
|
|
162
|
+
{{ entry.intervals }} interval{{ entry.intervals > 1 ? 's' : '' }}
|
|
123
163
|
</span>
|
|
124
|
-
<span v-if="
|
|
125
|
-
<span v-else-if="
|
|
164
|
+
<span v-if="entry.leak" class="badge badge-err">leak detected</span>
|
|
165
|
+
<span v-else-if="entry.status === 'mounted'" class="badge badge-ok">mounted</span>
|
|
126
166
|
<span v-else class="badge badge-gray">unmounted</span>
|
|
127
167
|
</div>
|
|
128
168
|
</div>
|
|
129
169
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<div v-if="e.leak" class="leak-banner">{{ e.leakReason }}</div>
|
|
170
|
+
<div v-if="expanded === entry.id" class="comp-detail" @click.stop>
|
|
171
|
+
<div v-if="entry.leak" class="leak-banner">{{ entry.leakReason }}</div>
|
|
133
172
|
|
|
134
173
|
<div class="section-label">reactive state</div>
|
|
135
|
-
<div v-for="
|
|
136
|
-
<span class="mono text-sm" style="min-width: 90px; color: var(--text2)">{{
|
|
174
|
+
<div v-for="refEntry in entry.refs" :key="refEntry.key" class="ref-row">
|
|
175
|
+
<span class="mono text-sm" style="min-width: 90px; color: var(--text2)">{{ refEntry.key }}</span>
|
|
137
176
|
<span class="mono text-sm muted" style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
|
|
138
|
-
{{
|
|
177
|
+
{{ refEntry.val }}
|
|
139
178
|
</span>
|
|
140
|
-
<span class="badge badge-info text-xs">{{
|
|
179
|
+
<span class="badge badge-info text-xs">{{ refEntry.type }}</span>
|
|
141
180
|
</div>
|
|
142
181
|
|
|
143
182
|
<div class="section-label" style="margin-top: 8px">lifecycle</div>
|
|
144
|
-
<div v-for="
|
|
145
|
-
<span class="lc-dot" :style="{ background:
|
|
146
|
-
<span class="muted text-sm" style="min-width: 110px">{{
|
|
147
|
-
<span class="text-sm" :style="{ color:
|
|
183
|
+
<div v-for="row in lifecycleRows(entry)" :key="row.label" class="lc-row">
|
|
184
|
+
<span class="lc-dot" :style="{ background: row.ok ? 'var(--teal)' : 'var(--red)' }"></span>
|
|
185
|
+
<span class="muted text-sm" style="min-width: 110px">{{ row.label }}</span>
|
|
186
|
+
<span class="text-sm" :style="{ color: row.ok ? 'var(--teal)' : 'var(--red)' }">{{ row.status }}</span>
|
|
148
187
|
</div>
|
|
149
188
|
</div>
|
|
150
189
|
</div>
|
|
151
190
|
|
|
152
|
-
<div v-if="!filtered.length" class="muted text-sm" style="padding: 16px 0">
|
|
191
|
+
<div v-if="!filtered.length" class="muted text-sm" style="padding: 16px 0">
|
|
192
|
+
{{ connected ? 'No composables recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
193
|
+
</div>
|
|
153
194
|
</div>
|
|
154
195
|
</div>
|
|
155
196
|
</template>
|