nuxt-devtools-observatory 0.1.10 → 0.1.12

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.
@@ -9,12 +9,15 @@ interface ComponentNode {
9
9
  element?: string
10
10
  depth: number
11
11
  path: string[]
12
- renders: number
12
+ rerenders: number
13
+ mountCount: number
13
14
  avgMs: number
14
15
  triggers: string[]
15
16
  children: ComponentNode[]
16
17
  parentId?: string
17
18
  parentLabel?: string
19
+ isPersistent: boolean
20
+ isHydrationMount: boolean
18
21
  }
19
22
 
20
23
  const TreeNode = defineComponent({
@@ -23,45 +26,19 @@ const TreeNode = defineComponent({
23
26
  node: Object as () => ComponentNode,
24
27
  mode: String,
25
28
  threshold: Number,
26
- hotOnly: Boolean,
27
29
  selected: String,
28
- search: String,
29
30
  expandedIds: Object as () => Set<string>,
30
31
  },
31
32
  emits: ['select', 'toggle'],
32
33
  setup(props, { emit }): () => VNode | null {
33
34
  function nodeValue(node: ComponentNode) {
34
- return props.mode === 'count' ? node.renders : node.avgMs
35
+ return props.mode === 'count' ? node.rerenders + node.mountCount : node.avgMs
35
36
  }
36
37
 
37
38
  function isHot(node: ComponentNode) {
38
39
  return nodeValue(node) >= props.threshold!
39
40
  }
40
41
 
41
- function matchesSearch(node: ComponentNode, search: string): boolean {
42
- if (!search) {
43
- return true
44
- }
45
-
46
- const query = search.toLowerCase()
47
-
48
- return (
49
- node.label.toLowerCase().includes(query) ||
50
- node.file.toLowerCase().includes(query) ||
51
- node.path.some((segment) => segment.toLowerCase().includes(query)) ||
52
- node.triggers.some((trigger) => trigger.toLowerCase().includes(query))
53
- )
54
- }
55
-
56
- function shouldShow(node: ComponentNode, search: string): boolean {
57
- const selfMatches = matchesSearch(node, search)
58
- const childMatches = node.children.some((child) => shouldShow(child, search))
59
- const searchMatches = !search || selfMatches || childMatches
60
- const hotMatches = !props.hotOnly || isHot(node) || node.children.some((child) => shouldShow(child, ''))
61
-
62
- return searchMatches && hotMatches
63
- }
64
-
65
42
  function rowClass(node: ComponentNode) {
66
43
  return {
67
44
  selected: props.selected === node.id,
@@ -71,16 +48,9 @@ const TreeNode = defineComponent({
71
48
 
72
49
  return () => {
73
50
  const node = props.node!
74
- const search = props.search?.trim() ?? ''
75
-
76
- if (!shouldShow(node, search)) {
77
- return null
78
- }
79
-
80
51
  const expanded = props.expandedIds?.has(node.id) ?? false
81
- const visibleChildren = node.children.filter((child) => shouldShow(child, search))
82
- const canExpand = visibleChildren.length > 0
83
- const metric = props.mode === 'count' ? `${node.renders}` : `${node.avgMs.toFixed(1)}ms`
52
+ const canExpand = node.children.length > 0
53
+ const metric = props.mode === 'count' ? `${node.rerenders + node.mountCount}` : `${node.avgMs.toFixed(1)}ms`
84
54
  const metricLabel = props.mode === 'count' ? 'renders' : 'avg'
85
55
  const badges = []
86
56
  const normalizedElement = node.element?.toLowerCase()
@@ -90,7 +60,11 @@ const TreeNode = defineComponent({
90
60
  }
91
61
 
92
62
  if (node.file !== 'unknown') {
93
- const fileBadge = node.file.split('/').pop()?.replace(/\.vue$/i, '') ?? node.file
63
+ const fileBadge =
64
+ node.file
65
+ .split('/')
66
+ .pop()
67
+ ?.replace(/\.vue$/i, '') ?? node.file
94
68
 
95
69
  if (fileBadge !== node.label && !badges.includes(fileBadge)) {
96
70
  badges.push(fileBadge)
@@ -109,10 +83,11 @@ const TreeNode = defineComponent({
109
83
  },
110
84
  },
111
85
  [
86
+ h('span', { class: 'tree-rail', 'aria-hidden': 'true' }),
112
87
  h(
113
88
  'button',
114
89
  {
115
- class: 'tree-toggle',
90
+ class: ['tree-toggle', { empty: !canExpand }],
116
91
  disabled: !canExpand,
117
92
  onClick: (event: MouseEvent) => {
118
93
  event.stopPropagation()
@@ -122,9 +97,8 @@ const TreeNode = defineComponent({
122
97
  }
123
98
  },
124
99
  },
125
- canExpand ? (expanded ? '' : '') : '·'
100
+ canExpand ? (expanded ? '' : '') : ''
126
101
  ),
127
- h('span', { class: 'tree-rail', 'aria-hidden': 'true' }),
128
102
  h('div', { class: 'tree-copy' }, [
129
103
  h('span', { class: 'tree-name mono', title: node.label }, node.label),
130
104
  badges.length
@@ -135,21 +109,38 @@ const TreeNode = defineComponent({
135
109
  )
136
110
  : null,
137
111
  ]),
138
- h('div', { class: 'tree-metrics mono' }, h('span', { class: 'tree-metric-pill' }, `${metric} ${metricLabel}`)),
112
+ h('div', { class: 'tree-metrics mono' }, [
113
+ node.isPersistent
114
+ ? h(
115
+ 'span',
116
+ { class: 'tree-persistent-pill', title: 'Layout / persistent component — survives navigation' },
117
+ 'persistent'
118
+ )
119
+ : null,
120
+ node.isHydrationMount
121
+ ? h(
122
+ 'span',
123
+ {
124
+ class: 'tree-hydration-pill',
125
+ title: 'First mount was SSR hydration — not a user-triggered render',
126
+ },
127
+ 'hydrated'
128
+ )
129
+ : null,
130
+ h('span', { class: 'tree-metric-pill' }, `${metric} ${metricLabel}`),
131
+ ]),
139
132
  ]
140
133
  ),
141
- expanded && visibleChildren.length
134
+ expanded && canExpand
142
135
  ? h(
143
136
  'div',
144
137
  { class: 'tree-children' },
145
- visibleChildren.map((child) =>
138
+ node.children.map((child) =>
146
139
  h(TreeNode, {
147
140
  node: child,
148
141
  mode: props.mode,
149
142
  threshold: props.threshold,
150
- hotOnly: props.hotOnly,
151
143
  selected: props.selected,
152
- search: props.search,
153
144
  expandedIds: props.expandedIds,
154
145
  onSelect: (value: ComponentNode) => emit('select', value),
155
146
  onToggle: (value: string) => emit('toggle', value),
@@ -165,7 +156,19 @@ const TreeNode = defineComponent({
165
156
  const { renders, connected } = useObservatoryData()
166
157
 
167
158
  const activeMode = ref<'count' | 'time'>('count')
168
- const activeThreshold = ref(5)
159
+ // Separate thresholds per mode so switching modes doesn't produce nonsense results.
160
+ // Count: flag components that rendered 3+ times (1 hydration mount is normal).
161
+ // Time: flag components averaging 16ms+ (one animation frame budget).
162
+ const countThreshold = ref(3)
163
+ const timeThreshold = ref(16)
164
+ // Writable computed so the threshold slider can use v-model directly.
165
+ const activeThreshold = computed({
166
+ get: () => (activeMode.value === 'count' ? countThreshold.value : timeThreshold.value),
167
+ set: (val: number) => {
168
+ if (activeMode.value === 'count') countThreshold.value = val
169
+ else timeThreshold.value = val
170
+ },
171
+ })
169
172
  const activeHotOnly = ref(false)
170
173
  const frozen = ref(false)
171
174
  const search = ref('')
@@ -184,7 +187,10 @@ function displayLabel(entry: RenderEntry) {
184
187
  return entry.element
185
188
  }
186
189
 
187
- const basename = entry.file.split('/').pop()?.replace(/\.vue$/i, '')
190
+ const basename = entry.file
191
+ .split('/')
192
+ .pop()
193
+ ?.replace(/\.vue$/i, '')
188
194
 
189
195
  if (basename && basename !== 'unknown') {
190
196
  return basename
@@ -212,11 +218,14 @@ function buildNodes(entries: RenderEntry[]) {
212
218
  element: entry.element,
213
219
  depth: 0,
214
220
  path: [],
215
- renders: entry.renders,
221
+ rerenders: entry.rerenders ?? 0,
222
+ mountCount: entry.mountCount ?? 1,
216
223
  avgMs: entry.avgMs,
217
224
  triggers: entry.triggers.map(formatTrigger),
218
225
  children: [],
219
226
  parentId: entry.parentUid !== undefined ? String(entry.parentUid) : undefined,
227
+ isPersistent: Boolean(entry.isPersistent),
228
+ isHydrationMount: Boolean(entry.isHydrationMount),
220
229
  })
221
230
  }
222
231
 
@@ -291,12 +300,38 @@ function pathToNode(node: ComponentNode, targetId: string, trail: string[] = [])
291
300
  return null
292
301
  }
293
302
 
303
+ function findFirstHotNode(node: ComponentNode): ComponentNode | null {
304
+ if (isHot(node)) {
305
+ return node
306
+ }
307
+
308
+ for (const child of node.children) {
309
+ const match = findFirstHotNode(child)
310
+
311
+ if (match) {
312
+ return match
313
+ }
314
+ }
315
+
316
+ return null
317
+ }
318
+
294
319
  function defaultExpandedIds(root: ComponentNode | null) {
295
320
  if (!root) {
296
321
  return new Set<string>()
297
322
  }
298
323
 
299
- return new Set([root.id])
324
+ // Expand all nodes that have children — gives a fully-open tree on first load.
325
+ // The user can collapse individual branches as needed.
326
+ const expanded = new Set<string>()
327
+ function expandAll(node: ComponentNode) {
328
+ if (node.children.length > 0) {
329
+ expanded.add(node.id)
330
+ node.children.forEach(expandAll)
331
+ }
332
+ }
333
+ expandAll(root)
334
+ return expanded
300
335
  }
301
336
 
302
337
  function searchExpandedIds(root: ComponentNode | null, term: string) {
@@ -323,7 +358,7 @@ function searchExpandedIds(root: ComponentNode | null, term: string) {
323
358
  }
324
359
 
325
360
  function nodeValue(node: ComponentNode) {
326
- return activeMode.value === 'count' ? node.renders : node.avgMs
361
+ return activeMode.value === 'count' ? node.rerenders + node.mountCount : node.avgMs
327
362
  }
328
363
 
329
364
  function isHot(node: ComponentNode) {
@@ -364,6 +399,24 @@ function isVisibleRoot(node: ComponentNode, searchTerm: string): boolean {
364
399
  return matchesCurrentSearch && matchesCurrentHeat
365
400
  }
366
401
 
402
+ function pruneVisibleTree(node: ComponentNode, searchTerm: string): ComponentNode | null {
403
+ const visibleChildren = node.children
404
+ .map((child) => pruneVisibleTree(child, searchTerm))
405
+ .filter((child): child is ComponentNode => child !== null)
406
+
407
+ const matchesCurrentSearch = !searchTerm || matchesSearch(node, searchTerm) || visibleChildren.length > 0
408
+ const matchesCurrentHeat = !activeHotOnly.value || isHot(node) || visibleChildren.length > 0
409
+
410
+ if (!matchesCurrentSearch || !matchesCurrentHeat) {
411
+ return null
412
+ }
413
+
414
+ return {
415
+ ...node,
416
+ children: visibleChildren,
417
+ }
418
+ }
419
+
367
420
  const displayEntries = computed(() => (frozen.value ? frozenSnapshot.value : renders.value))
368
421
  const rootNodes = computed(() => buildNodes(displayEntries.value))
369
422
  const rootMap = computed(() => new Map(rootNodes.value.map((node) => [node.id, node])))
@@ -383,12 +436,18 @@ const activeRoot = computed(() => {
383
436
  return filteredRoots.value[0] ?? rootNodes.value[0] ?? null
384
437
  })
385
438
 
439
+ const visibleActiveRoot = computed(() => {
440
+ const term = search.value.trim()
441
+
442
+ return activeRoot.value ? pruneVisibleTree(activeRoot.value, term) : null
443
+ })
444
+
386
445
  const visibleTreeRoots = computed(() => {
387
- if (!activeRoot.value) {
446
+ if (!visibleActiveRoot.value) {
388
447
  return []
389
448
  }
390
449
 
391
- return [activeRoot.value]
450
+ return [visibleActiveRoot.value]
392
451
  })
393
452
 
394
453
  const appEntries = computed(() =>
@@ -401,7 +460,7 @@ const appEntries = computed(() =>
401
460
  )
402
461
 
403
462
  const activeSelected = computed(() => allComponents.value.find((node) => node.id === activeSelectedId.value) ?? null)
404
- const totalRenders = computed(() => allComponents.value.reduce((sum, node) => sum + node.renders, 0))
463
+ const totalRenders = computed(() => allComponents.value.reduce((sum, node) => sum + node.rerenders + node.mountCount, 0))
405
464
  const hotCount = computed(() => allComponents.value.filter((node) => isHot(node)).length)
406
465
  const avgTime = computed(() => {
407
466
  const components = allComponents.value.filter((node) => node.avgMs > 0)
@@ -476,6 +535,34 @@ watch(search, (term) => {
476
535
  expandedIds.value = defaultExpandedIds(activeRoot.value)
477
536
  })
478
537
 
538
+ watch([activeHotOnly, activeThreshold, activeMode, filteredRoots], () => {
539
+ if (!activeHotOnly.value) {
540
+ return
541
+ }
542
+
543
+ const topLevelRoot = filteredRoots.value[0] ?? null
544
+
545
+ if (!topLevelRoot) {
546
+ activeSelectedId.value = null
547
+ return
548
+ }
549
+
550
+ const firstHot = findFirstHotNode(topLevelRoot)
551
+
552
+ if (!firstHot) {
553
+ activeSelectedId.value = null
554
+ return
555
+ }
556
+
557
+ activeRootId.value = topLevelRoot.id
558
+
559
+ if (activeSelectedId.value !== firstHot.id) {
560
+ activeSelectedId.value = firstHot.id
561
+ }
562
+
563
+ expandedIds.value = new Set(pathToNode(topLevelRoot, firstHot.id) ?? [topLevelRoot.id])
564
+ })
565
+
479
566
  function selectNode(node: ComponentNode) {
480
567
  activeSelectedId.value = node.id
481
568
 
@@ -522,7 +609,12 @@ function toggleFreeze() {
522
609
  }
523
610
 
524
611
  function basename(file: string) {
525
- return file.split('/').pop()?.replace(/\.vue$/i, '') ?? file
612
+ return (
613
+ file
614
+ .split('/')
615
+ .pop()
616
+ ?.replace(/\.vue$/i, '') ?? file
617
+ )
526
618
  }
527
619
 
528
620
  function pathLabel(node: ComponentNode) {
@@ -539,8 +631,15 @@ function pathLabel(node: ComponentNode) {
539
631
  </div>
540
632
  <div class="threshold-group">
541
633
  <span class="muted text-sm">threshold</span>
542
- <input v-model.number="activeThreshold" type="range" min="1" max="30" step="1" style="width: 90px" />
543
- <span class="mono text-sm">{{ activeThreshold }}+</span>
634
+ <input
635
+ v-model.number="activeThreshold"
636
+ type="range"
637
+ :min="activeMode === 'count' ? 2 : 4"
638
+ :max="activeMode === 'count' ? 20 : 100"
639
+ :step="activeMode === 'count' ? 1 : 4"
640
+ style="width: 90px"
641
+ />
642
+ <span class="mono text-sm">{{ activeThreshold }}{{ activeMode === 'count' ? '+ renders' : 'ms+' }}</span>
544
643
  </div>
545
644
  <button :class="{ active: activeHotOnly }" @click="activeHotOnly = !activeHotOnly">hot only</button>
546
645
  <button :class="{ active: frozen }" style="margin-left: auto" @click="toggleFreeze">
@@ -599,9 +698,7 @@ function pathLabel(node: ComponentNode) {
599
698
  :node="root"
600
699
  :mode="activeMode"
601
700
  :threshold="activeThreshold"
602
- :hot-only="activeHotOnly"
603
701
  :selected="activeSelected?.id"
604
- :search="search"
605
702
  :expanded-ids="expandedIds"
606
703
  @select="selectNode"
607
704
  @toggle="toggleNode"
@@ -621,9 +718,23 @@ function pathLabel(node: ComponentNode) {
621
718
  </div>
622
719
 
623
720
  <div class="detail-pill-row">
624
- <span class="detail-pill mono">{{ activeSelected.renders }} renders</span>
721
+ <span class="detail-pill mono">
722
+ {{ activeSelected.rerenders + activeSelected.mountCount }} render{{
723
+ activeSelected.rerenders + activeSelected.mountCount !== 1 ? 's' : ''
724
+ }}
725
+ </span>
726
+ <span class="detail-pill mono muted">
727
+ {{ activeSelected.mountCount }} mount{{ activeSelected.mountCount !== 1 ? 's' : '' }}
728
+ </span>
729
+ <span v-if="activeSelected.rerenders" class="detail-pill mono">
730
+ {{ activeSelected.rerenders }} re-render{{ activeSelected.rerenders !== 1 ? 's' : '' }}
731
+ </span>
732
+ <span v-if="activeSelected.isPersistent" class="detail-pill mono persistent">persistent</span>
733
+ <span v-if="activeSelected.isHydrationMount" class="detail-pill mono hydrated">hydrated</span>
625
734
  <span class="detail-pill mono">{{ activeSelected.avgMs.toFixed(1) }}ms avg</span>
626
- <span class="detail-pill mono" :class="{ hot: isHot(activeSelected) }">{{ isHot(activeSelected) ? 'hot' : 'cool' }}</span>
735
+ <span class="detail-pill mono" :class="{ hot: isHot(activeSelected) }">
736
+ {{ isHot(activeSelected) ? 'hot' : 'cool' }}
737
+ </span>
627
738
  </div>
628
739
 
629
740
  <div class="section-label">identity</div>
@@ -644,12 +755,24 @@ function pathLabel(node: ComponentNode) {
644
755
 
645
756
  <div class="section-label">rendering</div>
646
757
  <div class="meta-grid">
647
- <span class="muted text-sm">mode value</span>
648
- <span class="mono text-sm">{{ activeMode === 'count' ? activeSelected.renders : `${activeSelected.avgMs.toFixed(1)}ms` }}</span>
758
+ <span class="muted text-sm">total renders</span>
759
+ <span class="mono text-sm">{{ activeSelected.rerenders + activeSelected.mountCount }}</span>
760
+ <span class="muted text-sm">re-renders</span>
761
+ <span class="mono text-sm">{{ activeSelected.rerenders }}</span>
762
+ <span class="muted text-sm">mounts</span>
763
+ <span class="mono text-sm">{{ activeSelected.mountCount }}</span>
764
+ <span class="muted text-sm">persistent</span>
765
+ <span class="mono text-sm" :style="{ color: activeSelected.isPersistent ? 'var(--amber)' : 'inherit' }">
766
+ {{ activeSelected.isPersistent ? 'yes — survives navigation' : 'no' }}
767
+ </span>
768
+ <span class="muted text-sm">hydration mount</span>
769
+ <span class="mono text-sm">{{ activeSelected.isHydrationMount ? 'yes — SSR hydrated' : 'no' }}</span>
770
+ <span class="muted text-sm">avg render time</span>
771
+ <span class="mono text-sm">{{ activeSelected.avgMs.toFixed(1) }}ms</span>
649
772
  <span class="muted text-sm">threshold</span>
650
- <span class="mono text-sm">{{ activeThreshold }}</span>
651
- <span class="muted text-sm">selected mode</span>
652
- <span class="mono text-sm">{{ activeMode === 'count' ? 'render count' : 'render time' }}</span>
773
+ <span class="mono text-sm">{{ activeThreshold }}{{ activeMode === 'count' ? '+ renders' : 'ms+' }}</span>
774
+ <span class="muted text-sm">mode</span>
775
+ <span class="mono text-sm">{{ activeMode === 'count' ? 're-render count' : 'render time' }}</span>
653
776
  </div>
654
777
 
655
778
  <div class="section-label">triggers</div>
@@ -698,6 +821,12 @@ function pathLabel(node: ComponentNode) {
698
821
  flex-shrink: 0;
699
822
  }
700
823
 
824
+ .stat-sub {
825
+ margin-top: 4px;
826
+ font-size: 11px;
827
+ color: var(--text3);
828
+ }
829
+
701
830
  .inspector {
702
831
  display: grid;
703
832
  grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(260px, 320px);
@@ -811,7 +940,7 @@ function pathLabel(node: ComponentNode) {
811
940
 
812
941
  :deep(.tree-row) {
813
942
  display: grid;
814
- grid-template-columns: 18px 8px minmax(0, 1fr) auto;
943
+ grid-template-columns: 8px 18px minmax(0, 1fr) auto;
815
944
  align-items: center;
816
945
  gap: 6px;
817
946
  min-width: 0;
@@ -844,17 +973,20 @@ function pathLabel(node: ComponentNode) {
844
973
  background: transparent;
845
974
  color: var(--text3);
846
975
  padding: 0;
847
- font-size: 11px;
976
+ font-size: 14px;
848
977
  display: inline-flex;
849
978
  align-items: center;
850
979
  justify-content: center;
851
980
  }
852
981
 
853
982
  :deep(.tree-toggle:disabled) {
854
- opacity: 0.4;
855
983
  cursor: default;
856
984
  }
857
985
 
986
+ :deep(.tree-toggle.empty) {
987
+ opacity: 0;
988
+ }
989
+
858
990
  :deep(.tree-rail) {
859
991
  display: block;
860
992
  width: 2px;
@@ -904,6 +1036,7 @@ function pathLabel(node: ComponentNode) {
904
1036
  min-width: 92px;
905
1037
  justify-content: flex-end;
906
1038
  flex-shrink: 0;
1039
+ gap: 6px;
907
1040
  }
908
1041
 
909
1042
  :deep(.tree-metric-pill) {
@@ -919,6 +1052,28 @@ function pathLabel(node: ComponentNode) {
919
1052
  color: var(--text3);
920
1053
  }
921
1054
 
1055
+ :deep(.tree-persistent-pill) {
1056
+ display: inline-flex;
1057
+ align-items: center;
1058
+ padding: 2px 8px;
1059
+ border: 1px solid color-mix(in srgb, var(--amber) 55%, var(--border));
1060
+ border-radius: 999px;
1061
+ background: color-mix(in srgb, var(--amber) 10%, var(--bg2));
1062
+ font-size: 10px;
1063
+ color: color-mix(in srgb, var(--amber) 80%, var(--text));
1064
+ }
1065
+
1066
+ :deep(.tree-hydration-pill) {
1067
+ display: inline-flex;
1068
+ align-items: center;
1069
+ padding: 2px 8px;
1070
+ border: 1px solid color-mix(in srgb, var(--teal) 55%, var(--border));
1071
+ border-radius: 999px;
1072
+ background: color-mix(in srgb, var(--teal) 10%, var(--bg2));
1073
+ font-size: 10px;
1074
+ color: color-mix(in srgb, var(--teal) 80%, var(--text));
1075
+ }
1076
+
922
1077
  :deep(.tree-children) {
923
1078
  margin-left: 7px;
924
1079
  padding-left: 11px;
@@ -965,6 +1120,21 @@ function pathLabel(node: ComponentNode) {
965
1120
  color: var(--red);
966
1121
  }
967
1122
 
1123
+ .detail-pill.persistent {
1124
+ border-color: color-mix(in srgb, var(--amber) 55%, var(--border));
1125
+ color: color-mix(in srgb, var(--amber) 80%, var(--text));
1126
+ }
1127
+
1128
+ .detail-pill.hydrated {
1129
+ border-color: color-mix(in srgb, var(--teal) 55%, var(--border));
1130
+ color: color-mix(in srgb, var(--teal) 80%, var(--text));
1131
+ }
1132
+
1133
+ .detail-pill.muted {
1134
+ color: var(--text3);
1135
+ border-color: var(--border);
1136
+ }
1137
+
968
1138
  .section-label {
969
1139
  font-size: 10px;
970
1140
  font-weight: 500;
@@ -56,8 +56,8 @@ const timelineGeometry = computed(() => {
56
56
  return []
57
57
  }
58
58
 
59
- const minT = Math.min(...all.map((e) => e.startTime))
60
- const maxT = Math.max(...all.map((e) => e.endTime ?? e.startTime + 400))
59
+ const minT = all.reduce((min, e) => Math.min(min, e.startTime), all[0].startTime)
60
+ const maxT = all.reduce((max, e) => Math.max(max, e.endTime ?? e.startTime + 400), 0)
61
61
  const span = Math.max(maxT - minT, 1)
62
62
 
63
63
  return all.map((e) => ({
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.10",
7
+ "version": "0.1.12",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -1,3 +1,8 @@
1
+ export interface RefChangeEvent {
2
+ t: number;
3
+ key: string;
4
+ value: unknown;
5
+ }
1
6
  export interface ComposableEntry {
2
7
  id: string;
3
8
  name: string;
@@ -10,6 +15,13 @@ export interface ComposableEntry {
10
15
  type: 'ref' | 'computed' | 'reactive';
11
16
  value: unknown;
12
17
  }>;
18
+ /** Capped at MAX_HISTORY_PER_ENTRY events, newest last */
19
+ history: RefChangeEvent[];
20
+ /**
21
+ * Keys whose underlying ref/reactive object is shared across multiple
22
+ * instances of this composable — indicates module-level (global) state.
23
+ */
24
+ sharedKeys: string[];
13
25
  watcherCount: number;
14
26
  intervalCount: number;
15
27
  lifecycle: {
@@ -20,6 +32,8 @@ export interface ComposableEntry {
20
32
  };
21
33
  file: string;
22
34
  line: number;
35
+ /** Route path the composable was registered on, e.g. "/products". */
36
+ route: string;
23
37
  }
24
38
  /**
25
39
  * Registers a new composable entry, updates an existing one, or retrieves all entries.
@@ -31,6 +45,12 @@ export interface ComposableEntry {
31
45
  */
32
46
  export declare function setupComposableRegistry(): {
33
47
  register: (entry: ComposableEntry) => void;
48
+ registerLiveRefs: (id: string, refs: Record<string, import("vue").Ref<unknown>>) => void;
49
+ registerRawRefs: (id: string, refs: Record<string, unknown>) => void;
50
+ onComposableChange: (cb: () => void) => void;
51
+ clear: () => void;
52
+ setRoute: (path: string) => void;
53
+ getRoute: () => string;
34
54
  update: (id: string, patch: Partial<ComposableEntry>) => void;
35
55
  getAll: () => ComposableEntry[];
36
56
  };