nuxt-devtools-observatory 0.1.12 → 0.1.14

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.
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, defineComponent, h, ref, watch, type VNode } from 'vue'
3
- import { useObservatoryData, type RenderEntry } from '../stores/observatory'
3
+ import { useObservatoryData, getObservatoryOrigin, type RenderEntry, type RenderEvent } from '../stores/observatory'
4
4
 
5
5
  interface ComponentNode {
6
6
  id: string
@@ -13,11 +13,13 @@ interface ComponentNode {
13
13
  mountCount: number
14
14
  avgMs: number
15
15
  triggers: string[]
16
+ timeline: RenderEvent[]
16
17
  children: ComponentNode[]
17
18
  parentId?: string
18
19
  parentLabel?: string
19
20
  isPersistent: boolean
20
21
  isHydrationMount: boolean
22
+ route: string
21
23
  }
22
24
 
23
25
  const TreeNode = defineComponent({
@@ -128,6 +130,20 @@ const TreeNode = defineComponent({
128
130
  )
129
131
  : null,
130
132
  h('span', { class: 'tree-metric-pill' }, `${metric} ${metricLabel}`),
133
+ node.file && node.file !== 'unknown'
134
+ ? h(
135
+ 'button',
136
+ {
137
+ class: 'tree-jump-btn',
138
+ title: `Open ${node.file} in editor`,
139
+ onClick: (e: MouseEvent) => {
140
+ e.stopPropagation()
141
+ openInEditor(node.file)
142
+ },
143
+ },
144
+ '↗'
145
+ )
146
+ : null,
131
147
  ]),
132
148
  ]
133
149
  ),
@@ -156,6 +172,8 @@ const TreeNode = defineComponent({
156
172
  const { renders, connected } = useObservatoryData()
157
173
 
158
174
  const activeMode = ref<'count' | 'time'>('count')
175
+ // Route filter — '' means show all routes
176
+ const activeRoute = ref('')
159
177
  // Separate thresholds per mode so switching modes doesn't produce nonsense results.
160
178
  // Count: flag components that rendered 3+ times (1 hydration mount is normal).
161
179
  // Time: flag components averaging 16ms+ (one animation frame budget).
@@ -222,10 +240,12 @@ function buildNodes(entries: RenderEntry[]) {
222
240
  mountCount: entry.mountCount ?? 1,
223
241
  avgMs: entry.avgMs,
224
242
  triggers: entry.triggers.map(formatTrigger),
243
+ timeline: entry.timeline ?? [],
225
244
  children: [],
226
245
  parentId: entry.parentUid !== undefined ? String(entry.parentUid) : undefined,
227
246
  isPersistent: Boolean(entry.isPersistent),
228
247
  isHydrationMount: Boolean(entry.isHydrationMount),
248
+ route: entry.route ?? '/',
229
249
  })
230
250
  }
231
251
 
@@ -392,11 +412,24 @@ function subtreeHasHotNode(node: ComponentNode): boolean {
392
412
  return isHot(node) || node.children.some((child) => subtreeHasHotNode(child))
393
413
  }
394
414
 
415
+ function nodeMatchesRoute(node: ComponentNode): boolean {
416
+ if (!activeRoute.value) return true
417
+ // A component is visible for a route if it was first seen on that route
418
+ // OR if any of its timeline events happened on that route.
419
+ if (node.route === activeRoute.value) return true
420
+ return node.timeline.some((e) => e.route === activeRoute.value)
421
+ }
422
+
423
+ function subtreeMatchesRoute(node: ComponentNode): boolean {
424
+ return nodeMatchesRoute(node) || node.children.some((child) => subtreeMatchesRoute(child))
425
+ }
426
+
395
427
  function isVisibleRoot(node: ComponentNode, searchTerm: string): boolean {
396
428
  const matchesCurrentSearch = treeMatches(node, searchTerm)
397
429
  const matchesCurrentHeat = !activeHotOnly.value || subtreeHasHotNode(node)
430
+ const matchesCurrentRoute = !activeRoute.value || subtreeMatchesRoute(node)
398
431
 
399
- return matchesCurrentSearch && matchesCurrentHeat
432
+ return matchesCurrentSearch && matchesCurrentHeat && matchesCurrentRoute
400
433
  }
401
434
 
402
435
  function pruneVisibleTree(node: ComponentNode, searchTerm: string): ComponentNode | null {
@@ -406,8 +439,9 @@ function pruneVisibleTree(node: ComponentNode, searchTerm: string): ComponentNod
406
439
 
407
440
  const matchesCurrentSearch = !searchTerm || matchesSearch(node, searchTerm) || visibleChildren.length > 0
408
441
  const matchesCurrentHeat = !activeHotOnly.value || isHot(node) || visibleChildren.length > 0
442
+ const matchesCurrentRoute = !activeRoute.value || nodeMatchesRoute(node) || visibleChildren.length > 0
409
443
 
410
- if (!matchesCurrentSearch || !matchesCurrentHeat) {
444
+ if (!matchesCurrentSearch || !matchesCurrentHeat || !matchesCurrentRoute) {
411
445
  return null
412
446
  }
413
447
 
@@ -459,6 +493,17 @@ const appEntries = computed(() =>
459
493
  }))
460
494
  )
461
495
 
496
+ const knownRoutes = computed(() => {
497
+ const routes = new Set<string>()
498
+ for (const node of allComponents.value) {
499
+ if (node.route) routes.add(node.route)
500
+ for (const event of node.timeline) {
501
+ if (event.route) routes.add(event.route)
502
+ }
503
+ }
504
+ return [...routes].sort()
505
+ })
506
+
462
507
  const activeSelected = computed(() => allComponents.value.find((node) => node.id === activeSelectedId.value) ?? null)
463
508
  const totalRenders = computed(() => allComponents.value.reduce((sum, node) => sum + node.rerenders + node.mountCount, 0))
464
509
  const hotCount = computed(() => allComponents.value.filter((node) => isHot(node)).length)
@@ -617,9 +662,25 @@ function basename(file: string) {
617
662
  )
618
663
  }
619
664
 
665
+ function openInEditor(file: string) {
666
+ if (!file || file === 'unknown') return
667
+ const origin = getObservatoryOrigin()
668
+ if (!origin) return
669
+ window.top?.postMessage({ type: 'observatory:open-in-editor', file }, origin)
670
+ }
671
+
620
672
  function pathLabel(node: ComponentNode) {
621
673
  return node.path.join(' / ')
622
674
  }
675
+
676
+ function formatMs(ms: number): string {
677
+ return ms < 1 ? '<1ms' : `${ms.toFixed(1)}ms`
678
+ }
679
+
680
+ function formatTimestamp(t: number): string {
681
+ // t is performance.now() — show as relative seconds from page load
682
+ return `+${(t / 1000).toFixed(2)}s`
683
+ }
623
684
  </script>
624
685
 
625
686
  <template>
@@ -642,6 +703,10 @@ function pathLabel(node: ComponentNode) {
642
703
  <span class="mono text-sm">{{ activeThreshold }}{{ activeMode === 'count' ? '+ renders' : 'ms+' }}</span>
643
704
  </div>
644
705
  <button :class="{ active: activeHotOnly }" @click="activeHotOnly = !activeHotOnly">hot only</button>
706
+ <select v-model="activeRoute" class="route-select mono text-sm" title="Filter by route">
707
+ <option value="">all routes</option>
708
+ <option v-for="r in knownRoutes" :key="r" :value="r">{{ r }}</option>
709
+ </select>
645
710
  <button :class="{ active: frozen }" style="margin-left: auto" @click="toggleFreeze">
646
711
  {{ frozen ? 'unfreeze' : 'freeze snapshot' }}
647
712
  </button>
@@ -744,7 +809,17 @@ function pathLabel(node: ComponentNode) {
744
809
  <span class="muted text-sm">path</span>
745
810
  <span class="mono text-sm">{{ pathLabel(activeSelected) }}</span>
746
811
  <span class="muted text-sm">file</span>
747
- <span class="mono text-sm muted">{{ activeSelected.file }}</span>
812
+ <span class="mono text-sm muted" style="display: flex; align-items: center; gap: 6px">
813
+ {{ activeSelected.file }}
814
+ <button
815
+ v-if="activeSelected.file && activeSelected.file !== 'unknown'"
816
+ class="jump-btn"
817
+ title="Open in editor"
818
+ @click="openInEditor(activeSelected.file)"
819
+ >
820
+ open ↗
821
+ </button>
822
+ </span>
748
823
  <span class="muted text-sm">file name</span>
749
824
  <span class="mono text-sm">{{ basename(activeSelected.file) }}</span>
750
825
  <span class="muted text-sm">parent</span>
@@ -778,6 +853,26 @@ function pathLabel(node: ComponentNode) {
778
853
  <div class="section-label">triggers</div>
779
854
  <div v-for="trigger in activeSelected.triggers" :key="trigger" class="trigger-item mono text-sm">{{ trigger }}</div>
780
855
  <div v-if="!activeSelected.triggers.length" class="muted text-sm">no triggers recorded</div>
856
+
857
+ <div class="section-label" style="margin-top: 8px">
858
+ render timeline
859
+ <span class="muted" style="font-weight: 400; text-transform: none; letter-spacing: 0">
860
+ ({{ activeSelected.timeline.length }})
861
+ </span>
862
+ </div>
863
+ <div v-if="!activeSelected.timeline.length" class="muted text-sm">no timeline events yet</div>
864
+ <div v-else class="timeline-list">
865
+ <div v-for="(event, idx) in [...activeSelected.timeline].reverse().slice(0, 30)" :key="idx" class="timeline-row">
866
+ <span class="timeline-kind mono" :class="event.kind">{{ event.kind }}</span>
867
+ <span class="timeline-time mono muted">{{ formatTimestamp(event.t) }}</span>
868
+ <span class="timeline-dur mono">{{ formatMs(event.durationMs) }}</span>
869
+ <span v-if="event.triggerKey" class="timeline-trigger mono muted">{{ event.triggerKey }}</span>
870
+ <span class="timeline-route mono muted" style="margin-left: auto">{{ event.route }}</span>
871
+ </div>
872
+ <div v-if="activeSelected.timeline.length > 30" class="muted text-sm" style="padding: 2px 0">
873
+ … {{ activeSelected.timeline.length - 30 }} earlier events
874
+ </div>
875
+ </div>
781
876
  </template>
782
877
  <div v-else class="detail-empty">click a component to inspect</div>
783
878
  </aside>
@@ -1153,6 +1248,123 @@ function pathLabel(node: ComponentNode) {
1153
1248
  color: var(--text2);
1154
1249
  }
1155
1250
 
1251
+ :deep(.tree-jump-btn) {
1252
+ display: none;
1253
+ padding: 0 4px;
1254
+ border: none;
1255
+ background: transparent;
1256
+ color: var(--text3);
1257
+ font-size: 11px;
1258
+ cursor: pointer;
1259
+ line-height: 1;
1260
+ flex-shrink: 0;
1261
+ }
1262
+
1263
+ :deep(.tree-row:hover .tree-jump-btn),
1264
+ :deep(.tree-row.selected .tree-jump-btn) {
1265
+ display: inline-flex;
1266
+ }
1267
+
1268
+ :deep(.tree-jump-btn:hover) {
1269
+ color: var(--teal);
1270
+ }
1271
+
1272
+ .jump-btn {
1273
+ font-size: 10px;
1274
+ padding: 1px 6px;
1275
+ border: 0.5px solid var(--border);
1276
+ border-radius: var(--radius);
1277
+ background: transparent;
1278
+ color: var(--text3);
1279
+ cursor: pointer;
1280
+ flex-shrink: 0;
1281
+ font-family: var(--mono);
1282
+ }
1283
+
1284
+ .jump-btn:hover {
1285
+ border-color: var(--teal);
1286
+ color: var(--teal);
1287
+ background: color-mix(in srgb, var(--teal) 8%, transparent);
1288
+ }
1289
+
1290
+ .route-select {
1291
+ padding: 3px 7px;
1292
+ border: 0.5px solid var(--border);
1293
+ border-radius: var(--radius);
1294
+ background: var(--bg2);
1295
+ color: var(--text);
1296
+ font-size: 11px;
1297
+ cursor: pointer;
1298
+ max-width: 140px;
1299
+ }
1300
+
1301
+ .timeline-list {
1302
+ display: flex;
1303
+ flex-direction: column;
1304
+ gap: 1px;
1305
+ background: var(--bg2);
1306
+ border-radius: var(--radius);
1307
+ padding: 4px 8px;
1308
+ max-height: 200px;
1309
+ overflow-y: auto;
1310
+ }
1311
+
1312
+ .timeline-row {
1313
+ display: flex;
1314
+ align-items: center;
1315
+ gap: 6px;
1316
+ padding: 2px 0;
1317
+ font-size: 11px;
1318
+ border-bottom: 0.5px solid var(--border);
1319
+ min-width: 0;
1320
+ }
1321
+
1322
+ .timeline-row:last-child {
1323
+ border-bottom: none;
1324
+ }
1325
+
1326
+ .timeline-kind {
1327
+ flex-shrink: 0;
1328
+ font-size: 10px;
1329
+ font-weight: 500;
1330
+ min-width: 40px;
1331
+ }
1332
+
1333
+ .timeline-kind.mount {
1334
+ color: var(--teal);
1335
+ }
1336
+
1337
+ .timeline-kind.update {
1338
+ color: var(--amber);
1339
+ }
1340
+
1341
+ .timeline-time {
1342
+ flex-shrink: 0;
1343
+ min-width: 52px;
1344
+ color: var(--text3);
1345
+ }
1346
+
1347
+ .timeline-dur {
1348
+ flex-shrink: 0;
1349
+ min-width: 38px;
1350
+ color: var(--text2);
1351
+ }
1352
+
1353
+ .timeline-trigger {
1354
+ overflow: hidden;
1355
+ text-overflow: ellipsis;
1356
+ white-space: nowrap;
1357
+ color: var(--text3);
1358
+ flex: 1;
1359
+ min-width: 0;
1360
+ }
1361
+
1362
+ .timeline-route {
1363
+ flex-shrink: 0;
1364
+ color: var(--text3);
1365
+ font-size: 10px;
1366
+ }
1367
+
1156
1368
  @media (max-width: 1180px) {
1157
1369
  .inspector {
1158
1370
  grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
@@ -0,0 +1,124 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+
4
+ defineProps<{
5
+ value: unknown
6
+ compact?: boolean
7
+ }>()
8
+
9
+ const open = ref(false)
10
+
11
+ function truncate(s: string, max: number) {
12
+ return s.length > max ? s.slice(0, max) + '…' : s
13
+ }
14
+ </script>
15
+
16
+ <template>
17
+ <span class="vi">
18
+ <!-- null / undefined -->
19
+ <span v-if="value === null || value === undefined" class="vi-null">{{ String(value) }}</span>
20
+
21
+ <!-- boolean -->
22
+ <span v-else-if="typeof value === 'boolean'" class="vi-bool">{{ value }}</span>
23
+
24
+ <!-- number -->
25
+ <span v-else-if="typeof value === 'number'" class="vi-num">{{ value }}</span>
26
+
27
+ <!-- string -->
28
+ <span v-else-if="typeof value === 'string'" class="vi-str">"{{ compact ? truncate(value, 40) : value }}"</span>
29
+
30
+ <!-- array -->
31
+ <span v-else-if="Array.isArray(value)">
32
+ <span v-if="compact || !open">
33
+ <span class="vi-punc">[</span>
34
+ <span class="vi-dim">{{ value.length }} item{{ value.length !== 1 ? 's' : '' }}</span>
35
+ <span class="vi-punc">]</span>
36
+ <button v-if="!compact" class="vi-toggle" @click.stop="open = true">▸</button>
37
+ </span>
38
+ <span v-else class="vi-block">
39
+ <button class="vi-toggle" @click.stop="open = false">▾</button>
40
+ <span class="vi-punc">[</span>
41
+ <span v-for="(item, i) in value" :key="i" class="vi-indent">
42
+ <ValueInspector :value="item" />
43
+ <span v-if="i < value.length - 1" class="vi-punc">,</span>
44
+ </span>
45
+ <span class="vi-punc">]</span>
46
+ </span>
47
+ </span>
48
+
49
+ <!-- object -->
50
+ <span v-else-if="typeof value === 'object'">
51
+ <span v-if="compact || !open">
52
+ <span class="vi-punc">{</span>
53
+ <span class="vi-dim">
54
+ {{
55
+ Object.keys(value as object)
56
+ .slice(0, 3)
57
+ .join(', ')
58
+ }}{{ Object.keys(value as object).length > 3 ? '…' : '' }}
59
+ </span>
60
+ <span class="vi-punc">}</span>
61
+ <button v-if="!compact" class="vi-toggle" @click.stop="open = true">▸</button>
62
+ </span>
63
+ <span v-else class="vi-block">
64
+ <button class="vi-toggle" @click.stop="open = false">▾</button>
65
+ <span class="vi-punc">{</span>
66
+ <span v-for="(v, k, i) in value as Record<string, unknown>" :key="k" class="vi-indent">
67
+ <span class="vi-key">{{ k }}</span>
68
+ <span class="vi-punc">:</span>
69
+ <ValueInspector :value="v" />
70
+ <span v-if="i < Object.keys(value as object).length - 1" class="vi-punc">,</span>
71
+ </span>
72
+ <span class="vi-punc">}</span>
73
+ </span>
74
+ </span>
75
+
76
+ <!-- fallback -->
77
+ <span v-else class="vi-dim">{{ String(value) }}</span>
78
+ </span>
79
+ </template>
80
+
81
+ <style scoped>
82
+ .vi {
83
+ font-family: var(--font-mono);
84
+ font-size: 12px;
85
+ }
86
+ .vi-null {
87
+ color: #888;
88
+ }
89
+ .vi-bool {
90
+ color: #534ab7;
91
+ }
92
+ .vi-num {
93
+ color: #ba7517;
94
+ }
95
+ .vi-str {
96
+ color: #0f6e56;
97
+ }
98
+ .vi-key {
99
+ color: #185195;
100
+ }
101
+ .vi-punc {
102
+ color: #888;
103
+ }
104
+ .vi-dim {
105
+ color: #888;
106
+ font-style: italic;
107
+ }
108
+ .vi-toggle {
109
+ background: none;
110
+ border: none;
111
+ cursor: pointer;
112
+ font-size: 10px;
113
+ color: #888;
114
+ padding: 0 2px;
115
+ }
116
+ .vi-block {
117
+ display: inline-flex;
118
+ flex-direction: column;
119
+ }
120
+ .vi-indent {
121
+ padding-left: 12px;
122
+ display: block;
123
+ }
124
+ </style>
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "0.1.12",
7
+ "version": "0.1.14",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -362,7 +362,7 @@ function composableTrackerPlugin() {
362
362
  const args = path.node.arguments;
363
363
  const loc = path.node.loc;
364
364
  const meta = t.objectExpression([
365
- t.objectProperty(t.identifier("file"), t.stringLiteral(id.split("/").pop() ?? id)),
365
+ t.objectProperty(t.identifier("file"), t.stringLiteral(id)),
366
366
  t.objectProperty(t.identifier("line"), t.numericLiteral(loc?.start.line ?? 0))
367
367
  ]);
368
368
  path.replaceWith(
@@ -521,6 +521,9 @@ const module$1 = defineNuxtModule({
521
521
  if (!nuxt.options.dev) {
522
522
  return;
523
523
  }
524
+ if (!process.env.LAUNCH_EDITOR && !process.env.VITE_EDITOR) {
525
+ process.env.LAUNCH_EDITOR = "code";
526
+ }
524
527
  const resolver = createResolver(import.meta.url);
525
528
  nuxt.hook("vite:extendConfig", (config) => {
526
529
  const alias = config.resolve?.alias;
@@ -53,6 +53,7 @@ export declare function setupComposableRegistry(): {
53
53
  getRoute: () => string;
54
54
  update: (id: string, patch: Partial<ComposableEntry>) => void;
55
55
  getAll: () => ComposableEntry[];
56
+ editValue: (id: string, key: string, value: unknown) => void;
56
57
  };
57
58
  export declare function __trackComposable<T>(name: string, callFn: () => T, meta: {
58
59
  file: string;
@@ -9,12 +9,18 @@ export function setupComposableRegistry() {
9
9
  const rawRefs = /* @__PURE__ */ new Map();
10
10
  function computeSharedKeys(id, name) {
11
11
  const ownRaw = rawRefs.get(id);
12
- if (!ownRaw) return [];
12
+ if (!ownRaw) {
13
+ return [];
14
+ }
13
15
  const shared = /* @__PURE__ */ new Set();
14
16
  for (const [otherId, entry] of entries.value.entries()) {
15
- if (otherId === id || entry.name !== name) continue;
17
+ if (otherId === id || entry.name !== name) {
18
+ continue;
19
+ }
16
20
  const otherRaw = rawRefs.get(otherId);
17
- if (!otherRaw) continue;
21
+ if (!otherRaw) {
22
+ continue;
23
+ }
18
24
  for (const [key, obj] of Object.entries(ownRaw)) {
19
25
  if (key in otherRaw && otherRaw[key] === obj) {
20
26
  shared.add(key);
@@ -36,7 +42,9 @@ export function setupComposableRegistry() {
36
42
  }
37
43
  function registerLiveRefs(id, refs) {
38
44
  const prevStop = liveRefWatchers.get(id);
39
- if (prevStop) prevStop();
45
+ if (prevStop) {
46
+ prevStop();
47
+ }
40
48
  liveRefWatchers.delete(id);
41
49
  if (Object.keys(refs).length === 0) {
42
50
  liveRefs.delete(id);
@@ -68,7 +76,9 @@ export function setupComposableRegistry() {
68
76
  if (serialised !== prev[k]) {
69
77
  const history = entryHistory.get(id) ?? [];
70
78
  history.push({ t, key: k, value: safeValue(val) });
71
- if (history.length > MAX_HISTORY) history.shift();
79
+ if (history.length > MAX_HISTORY) {
80
+ history.shift();
81
+ }
72
82
  entryHistory.set(id, history);
73
83
  }
74
84
  }
@@ -168,7 +178,25 @@ export function setupComposableRegistry() {
168
178
  entries.value.clear();
169
179
  emit("composable:clear", {});
170
180
  }
171
- return { register, registerLiveRefs, registerRawRefs, onComposableChange, clear, setRoute, getRoute, update, getAll };
181
+ function editValue(id, key, value) {
182
+ const live = liveRefs.get(id);
183
+ if (!live) {
184
+ return;
185
+ }
186
+ const r = live[key];
187
+ if (!r) {
188
+ return;
189
+ }
190
+ const entry = entries.value.get(id);
191
+ if (!entry) {
192
+ return;
193
+ }
194
+ if (entry.refs[key]?.type === "computed") {
195
+ return;
196
+ }
197
+ r.value = value;
198
+ }
199
+ return { register, registerLiveRefs, registerRawRefs, onComposableChange, clear, setRoute, getRoute, update, getAll, editValue };
172
200
  }
173
201
  export function __trackComposable(name, callFn, meta) {
174
202
  if (!import.meta.dev) {
@@ -199,7 +227,9 @@ export function __trackComposable(name, callFn, meta) {
199
227
  patchedSetInterval.__obs = true;
200
228
  window.setInterval = patchedSetInterval;
201
229
  window.clearInterval = ((timerId) => {
202
- if (timerId !== void 0) clearedIntervals.add(timerId);
230
+ if (timerId !== void 0) {
231
+ clearedIntervals.add(timerId);
232
+ }
203
233
  originalClearInterval(timerId);
204
234
  });
205
235
  }
@@ -1,3 +1,15 @@
1
+ export interface RenderEvent {
2
+ /** 'mount' for initial mount, 'update' for reactive re-renders */
3
+ kind: 'mount' | 'update';
4
+ /** performance.now() timestamp */
5
+ t: number;
6
+ /** Duration in ms */
7
+ durationMs: number;
8
+ /** Reactive dep key that triggered this update, if known */
9
+ triggerKey?: string;
10
+ /** Route path this render happened on */
11
+ route: string;
12
+ }
1
13
  export interface RenderEntry {
2
14
  uid: number;
3
15
  name: string;
@@ -15,6 +27,8 @@ export interface RenderEntry {
15
27
  type: string;
16
28
  timestamp: number;
17
29
  }>;
30
+ /** Per-render timeline, capped at MAX_TIMELINE events (newest last) */
31
+ timeline: RenderEvent[];
18
32
  rect?: {
19
33
  x: number;
20
34
  y: number;
@@ -28,6 +42,8 @@ export interface RenderEntry {
28
42
  isPersistent: boolean;
29
43
  /** True if the first mount of this component happened during SSR hydration */
30
44
  isHydrationMount: boolean;
45
+ /** Route path this component was first seen on */
46
+ route: string;
31
47
  }
32
48
  /**
33
49
  * Sets up a render registry for tracking render-related metrics (e.g. rerenders, render time, etc.)
@@ -46,4 +62,5 @@ export declare function setupRenderRegistry(nuxtApp: {
46
62
  getAll: () => RenderEntry[];
47
63
  snapshot: () => RenderEntry[];
48
64
  reset: () => void;
65
+ setRoute: (path: string) => void;
49
66
  };