nuxt-devtools-observatory 0.1.0

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.
Files changed (33) hide show
  1. package/README.md +209 -0
  2. package/client/dist/assets/index-C76d764s.js +17 -0
  3. package/client/dist/assets/index-yIuOV1_N.css +1 -0
  4. package/client/dist/index.html +47 -0
  5. package/client/index.html +46 -0
  6. package/client/src/App.vue +114 -0
  7. package/client/src/main.ts +5 -0
  8. package/client/src/stores/observatory.ts +65 -0
  9. package/client/src/style.css +261 -0
  10. package/client/src/views/ComposableTracker.vue +347 -0
  11. package/client/src/views/FetchDashboard.vue +492 -0
  12. package/client/src/views/ProvideInjectGraph.vue +481 -0
  13. package/client/src/views/RenderHeatmap.vue +492 -0
  14. package/client/src/views/TransitionTimeline.vue +527 -0
  15. package/client/tsconfig.json +16 -0
  16. package/client/vite.config.ts +12 -0
  17. package/dist/module.d.mts +38 -0
  18. package/dist/module.json +12 -0
  19. package/dist/module.mjs +562 -0
  20. package/dist/runtime/composables/composable-registry.d.ts +40 -0
  21. package/dist/runtime/composables/composable-registry.js +135 -0
  22. package/dist/runtime/composables/fetch-registry.d.ts +63 -0
  23. package/dist/runtime/composables/fetch-registry.js +83 -0
  24. package/dist/runtime/composables/provide-inject-registry.d.ts +57 -0
  25. package/dist/runtime/composables/provide-inject-registry.js +96 -0
  26. package/dist/runtime/composables/render-registry.d.ts +36 -0
  27. package/dist/runtime/composables/render-registry.js +85 -0
  28. package/dist/runtime/composables/transition-registry.d.ts +21 -0
  29. package/dist/runtime/composables/transition-registry.js +125 -0
  30. package/dist/runtime/plugin.d.ts +2 -0
  31. package/dist/runtime/plugin.js +66 -0
  32. package/dist/types.d.mts +3 -0
  33. package/package.json +89 -0
@@ -0,0 +1,347 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+
4
+ interface RefEntry {
5
+ key: string
6
+ type: string
7
+ val: string
8
+ }
9
+ interface ComposableEntry {
10
+ id: string
11
+ name: string
12
+ component: string
13
+ instances: number
14
+ status: 'mounted' | 'unmounted'
15
+ leak: boolean
16
+ leakReason?: string
17
+ refs: RefEntry[]
18
+ watchers: number
19
+ intervals: number
20
+ lifecycle: { onMounted: boolean; onUnmounted: boolean; watchersCleaned: boolean; intervalsCleaned: boolean }
21
+ }
22
+
23
+ const entries = ref<ComposableEntry[]>([
24
+ {
25
+ id: '1',
26
+ name: 'useAuth',
27
+ component: 'App.vue',
28
+ instances: 1,
29
+ status: 'mounted',
30
+ leak: false,
31
+ refs: [
32
+ { key: 'user', type: 'ref', val: '{ id: "u_9x3k", role: "admin" }' },
33
+ { key: 'isLoggedIn', type: 'computed', val: 'true' },
34
+ ],
35
+ watchers: 1,
36
+ intervals: 0,
37
+ lifecycle: { onMounted: true, onUnmounted: true, watchersCleaned: true, intervalsCleaned: true },
38
+ },
39
+ {
40
+ id: '2',
41
+ name: 'useWebSocket',
42
+ component: 'Dashboard.vue',
43
+ instances: 1,
44
+ status: 'unmounted',
45
+ leak: true,
46
+ leakReason: 'socket.close() never called — 2 watchers still running after unmount',
47
+ refs: [
48
+ { key: 'socket', type: 'ref', val: 'WebSocket { readyState: 1 }' },
49
+ { key: 'messages', type: 'ref', val: 'Array(47)' },
50
+ ],
51
+ watchers: 2,
52
+ intervals: 0,
53
+ lifecycle: { onMounted: true, onUnmounted: false, watchersCleaned: false, intervalsCleaned: true },
54
+ },
55
+ {
56
+ id: '3',
57
+ name: 'usePoller',
58
+ component: 'StockTicker.vue',
59
+ instances: 2,
60
+ status: 'unmounted',
61
+ leak: true,
62
+ leakReason: 'setInterval #37 never cleared — still firing every 2000ms',
63
+ refs: [
64
+ { key: 'data', type: 'ref', val: '{ price: 142.5 }' },
65
+ { key: 'intervalId', type: 'ref', val: '37' },
66
+ ],
67
+ watchers: 0,
68
+ intervals: 1,
69
+ lifecycle: { onMounted: true, onUnmounted: false, watchersCleaned: true, intervalsCleaned: false },
70
+ },
71
+ {
72
+ id: '4',
73
+ name: 'useCart',
74
+ component: 'CartDrawer.vue',
75
+ instances: 1,
76
+ status: 'mounted',
77
+ leak: false,
78
+ refs: [
79
+ { key: 'items', type: 'ref', val: 'Array(3)' },
80
+ { key: 'total', type: 'computed', val: '248.50' },
81
+ ],
82
+ watchers: 0,
83
+ intervals: 0,
84
+ lifecycle: { onMounted: false, onUnmounted: false, watchersCleaned: true, intervalsCleaned: true },
85
+ },
86
+ {
87
+ id: '5',
88
+ name: 'useBreakpoint',
89
+ component: 'Layout.vue',
90
+ instances: 1,
91
+ status: 'mounted',
92
+ leak: false,
93
+ refs: [
94
+ { key: 'isMobile', type: 'computed', val: 'false' },
95
+ { key: 'width', type: 'ref', val: '1280' },
96
+ ],
97
+ watchers: 0,
98
+ intervals: 0,
99
+ lifecycle: { onMounted: true, onUnmounted: true, watchersCleaned: true, intervalsCleaned: true },
100
+ },
101
+ {
102
+ id: '6',
103
+ name: 'useIntersectionObserver',
104
+ component: 'LazyImage.vue',
105
+ instances: 4,
106
+ status: 'mounted',
107
+ leak: false,
108
+ refs: [{ key: 'isVisible', type: 'ref', val: 'true' }],
109
+ watchers: 0,
110
+ intervals: 0,
111
+ lifecycle: { onMounted: true, onUnmounted: true, watchersCleaned: true, intervalsCleaned: true },
112
+ },
113
+ ])
114
+
115
+ const filter = ref('all')
116
+ const search = ref('')
117
+ const expanded = ref<string | null>(null)
118
+
119
+ const counts = computed(() => ({
120
+ mounted: entries.value.filter((e) => e.status === 'mounted').length,
121
+ leaks: entries.value.filter((e) => e.leak).length,
122
+ }))
123
+
124
+ const filtered = computed(() => {
125
+ return entries.value.filter((e) => {
126
+ if (filter.value === 'leak' && !e.leak) return false
127
+ if (filter.value === 'unmounted' && e.status !== 'unmounted') return false
128
+ const q = search.value.toLowerCase()
129
+ if (q && !e.name.toLowerCase().includes(q) && !e.component.toLowerCase().includes(q)) return false
130
+ return true
131
+ })
132
+ })
133
+
134
+ function lifecycleRows(e: ComposableEntry) {
135
+ return [
136
+ { label: 'onMounted', ok: e.lifecycle.onMounted, status: e.lifecycle.onMounted ? 'registered' : 'not used' },
137
+ { label: 'onUnmounted', ok: e.lifecycle.onUnmounted, status: e.lifecycle.onUnmounted ? 'registered' : 'missing' },
138
+ { label: 'watchers cleaned', ok: e.lifecycle.watchersCleaned, status: e.lifecycle.watchersCleaned ? 'all stopped' : 'NOT stopped' },
139
+ {
140
+ label: 'intervals cleared',
141
+ ok: e.lifecycle.intervalsCleaned,
142
+ status: e.lifecycle.intervalsCleaned ? 'all cleared' : 'NOT cleared',
143
+ },
144
+ ]
145
+ }
146
+ </script>
147
+
148
+ <template>
149
+ <div class="view">
150
+ <div class="stats-row">
151
+ <div class="stat-card">
152
+ <div class="stat-label">total</div>
153
+ <div class="stat-val">{{ entries.length }}</div>
154
+ </div>
155
+ <div class="stat-card">
156
+ <div class="stat-label">mounted</div>
157
+ <div class="stat-val" style="color: var(--teal)">{{ counts.mounted }}</div>
158
+ </div>
159
+ <div class="stat-card">
160
+ <div class="stat-label">leaks</div>
161
+ <div class="stat-val" style="color: var(--red)">{{ counts.leaks }}</div>
162
+ </div>
163
+ <div class="stat-card">
164
+ <div class="stat-label">instances</div>
165
+ <div class="stat-val">{{ entries.reduce((a, e) => a + e.instances, 0) }}</div>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="toolbar">
170
+ <button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
171
+ <button :class="{ 'danger-active': filter === 'leak' }" @click="filter = 'leak'">leaks only</button>
172
+ <button :class="{ active: filter === 'unmounted' }" @click="filter = 'unmounted'">unmounted</button>
173
+ <input
174
+ v-model="search"
175
+ type="search"
176
+ placeholder="search composable or component…"
177
+ style="max-width: 220px; margin-left: auto"
178
+ />
179
+ </div>
180
+
181
+ <div class="list">
182
+ <div
183
+ v-for="e in filtered"
184
+ :key="e.id"
185
+ class="comp-card"
186
+ :class="{ leak: e.leak, expanded: expanded === e.id }"
187
+ @click="expanded = expanded === e.id ? null : e.id"
188
+ >
189
+ <div class="comp-header">
190
+ <span class="mono bold" style="font-size: 12px">{{ e.name }}</span>
191
+ <span class="muted text-sm" style="margin-left: 4px">{{ e.component }}</span>
192
+ <div class="comp-meta">
193
+ <span v-if="e.instances > 1" class="muted text-sm">{{ e.instances }}×</span>
194
+ <span v-if="e.watchers > 0 && !e.leak" class="badge badge-warn">
195
+ {{ e.watchers }} watcher{{ e.watchers > 1 ? 's' : '' }}
196
+ </span>
197
+ <span v-if="e.intervals > 0 && !e.leak" class="badge badge-warn">
198
+ {{ e.intervals }} interval{{ e.intervals > 1 ? 's' : '' }}
199
+ </span>
200
+ <span v-if="e.leak" class="badge badge-err">leak detected</span>
201
+ <span v-else-if="e.status === 'mounted'" class="badge badge-ok">mounted</span>
202
+ <span v-else class="badge badge-gray">unmounted</span>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Expanded detail -->
207
+ <div v-if="expanded === e.id" class="comp-detail" @click.stop>
208
+ <div v-if="e.leak" class="leak-banner">{{ e.leakReason }}</div>
209
+
210
+ <div class="section-label">reactive state</div>
211
+ <div v-for="r in e.refs" :key="r.key" class="ref-row">
212
+ <span class="mono text-sm" style="min-width: 90px; color: var(--text2)">{{ r.key }}</span>
213
+ <span class="mono text-sm muted" style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
214
+ {{ r.val }}
215
+ </span>
216
+ <span class="badge badge-info text-xs">{{ r.type }}</span>
217
+ </div>
218
+
219
+ <div class="section-label" style="margin-top: 8px">lifecycle</div>
220
+ <div v-for="lc in lifecycleRows(e)" :key="lc.label" class="lc-row">
221
+ <span class="lc-dot" :style="{ background: lc.ok ? 'var(--teal)' : 'var(--red)' }"></span>
222
+ <span class="muted text-sm" style="min-width: 110px">{{ lc.label }}</span>
223
+ <span class="text-sm" :style="{ color: lc.ok ? 'var(--teal)' : 'var(--red)' }">{{ lc.status }}</span>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <div v-if="!filtered.length" class="muted text-sm" style="padding: 16px 0">no composables match</div>
229
+ </div>
230
+ </div>
231
+ </template>
232
+
233
+ <style scoped>
234
+ .view {
235
+ display: flex;
236
+ flex-direction: column;
237
+ height: 100%;
238
+ overflow: hidden;
239
+ padding: 12px;
240
+ gap: 10px;
241
+ }
242
+
243
+ .stats-row {
244
+ display: grid;
245
+ grid-template-columns: repeat(4, minmax(0, 1fr));
246
+ gap: 8px;
247
+ flex-shrink: 0;
248
+ }
249
+
250
+ .toolbar {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 6px;
254
+ flex-shrink: 0;
255
+ flex-wrap: wrap;
256
+ }
257
+
258
+ .list {
259
+ flex: 1;
260
+ overflow: auto;
261
+ display: flex;
262
+ flex-direction: column;
263
+ gap: 6px;
264
+ }
265
+
266
+ .comp-card {
267
+ background: var(--bg3);
268
+ border: 0.5px solid var(--border);
269
+ border-radius: var(--radius-lg);
270
+ overflow: hidden;
271
+ cursor: pointer;
272
+ }
273
+
274
+ .comp-card:hover {
275
+ border-color: var(--text3);
276
+ }
277
+
278
+ .comp-card.leak {
279
+ border-left: 2px solid var(--red);
280
+ border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
281
+ }
282
+
283
+ .comp-card.expanded {
284
+ border-color: var(--purple);
285
+ }
286
+
287
+ .comp-header {
288
+ display: flex;
289
+ align-items: center;
290
+ gap: 8px;
291
+ padding: 9px 13px;
292
+ }
293
+
294
+ .comp-meta {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 6px;
298
+ margin-left: auto;
299
+ }
300
+
301
+ .comp-detail {
302
+ border-top: 0.5px solid var(--border);
303
+ padding: 10px 13px;
304
+ }
305
+
306
+ .leak-banner {
307
+ background: rgb(226 75 74 / 10%);
308
+ border-radius: var(--radius);
309
+ padding: 7px 10px;
310
+ font-size: 12px;
311
+ color: var(--red);
312
+ margin-bottom: 8px;
313
+ }
314
+
315
+ .section-label {
316
+ font-size: 10px;
317
+ font-weight: 500;
318
+ text-transform: uppercase;
319
+ letter-spacing: 0.4px;
320
+ color: var(--text3);
321
+ margin-bottom: 5px;
322
+ }
323
+
324
+ .ref-row {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 8px;
328
+ padding: 5px 8px;
329
+ background: var(--bg2);
330
+ border-radius: var(--radius);
331
+ margin-bottom: 3px;
332
+ }
333
+
334
+ .lc-row {
335
+ display: flex;
336
+ align-items: center;
337
+ gap: 8px;
338
+ padding: 4px 0;
339
+ }
340
+
341
+ .lc-dot {
342
+ width: 7px;
343
+ height: 7px;
344
+ border-radius: 50%;
345
+ flex-shrink: 0;
346
+ }
347
+ </style>