nuxt-devtools-observatory 0.1.31 → 0.1.32

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,7 +1,15 @@
1
1
  import { ref } from 'vue'
2
2
  import { useDevtoolsClient, onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
3
3
  import type { ObservatorySnapshot, ObservatoryServerFunctions, ObservatoryClientFunctions } from '@observatory/types/rpc'
4
- import type { FetchEntry, ProvideEntry, InjectEntry, ComposableEntry, RenderEntry, TransitionEntry, TraceEntry } from '@observatory/types/snapshot'
4
+ import type {
5
+ FetchEntry,
6
+ ProvideEntry,
7
+ InjectEntry,
8
+ ComposableEntry,
9
+ RenderEntry,
10
+ TransitionEntry,
11
+ TraceEntry,
12
+ } from '@observatory/types/snapshot'
5
13
 
6
14
  type ProvideInjectSnapshot = { provides: ProvideEntry[]; injects: InjectEntry[] }
7
15
 
@@ -6,6 +6,7 @@ import {
6
6
  editComposableValue,
7
7
  openInEditor as openInEditorFromStore,
8
8
  } from '@observatory-client/stores/observatory'
9
+ import { matchesComposableEntryQuery } from '@observatory-client/composables/composable-search'
9
10
  import type { ComposableEntry as RuntimeComposableEntry } from '@observatory/types/snapshot'
10
11
 
11
12
  const { composables: rawEntries, connected, features, clearComposables } = useObservatoryData()
@@ -118,23 +119,8 @@ const filtered = computed(() => {
118
119
  return false
119
120
  }
120
121
 
121
- const q = search.value.toLowerCase()
122
-
123
- if (q) {
124
- const matchesName = entry.name.toLowerCase().includes(q)
125
- const matchesFile = entry.componentFile.toLowerCase().includes(q)
126
- const matchesRef = Object.keys(entry.refs).some((k) => k.toLowerCase().includes(q))
127
- const matchesVal = Object.values(entry.refs).some((v) => {
128
- try {
129
- return String(JSON.stringify(v.value)).toLowerCase().includes(q)
130
- } catch {
131
- return false
132
- }
133
- })
134
-
135
- if (!matchesName && !matchesFile && !matchesRef && !matchesVal) {
136
- return false
137
- }
122
+ if (search.value.trim() && !matchesComposableEntryQuery(entry, search.value)) {
123
+ return false
138
124
  }
139
125
 
140
126
  return true
@@ -234,22 +220,67 @@ function toggleRefExpand(entryId: string, refKey: string) {
234
220
  }
235
221
 
236
222
  // ── Reverse lookup ────────────────────────────────────────────────────────
237
- // Clicking a ref key shows every mounted instance that exposes the same key.
223
+ // Clicking a ref key prefers identity-based lookup for shared/global refs.
224
+ // For non-shared keys, fallback to legacy key-name lookup.
238
225
 
239
- const lookupKey = ref<string | null>(null)
226
+ interface LookupTarget {
227
+ key: string
228
+ composableName: string
229
+ identityGroup?: string
230
+ }
231
+
232
+ const lookupTarget = ref<LookupTarget | null>(null)
240
233
 
241
234
  const lookupResults = computed(() => {
242
- if (!lookupKey.value) {
235
+ if (!lookupTarget.value) {
243
236
  return []
244
237
  }
245
238
 
246
- const key = lookupKey.value
239
+ const target = lookupTarget.value
240
+
241
+ if (target.identityGroup) {
242
+ return entries.value.filter(
243
+ (entry) =>
244
+ entry.name === target.composableName &&
245
+ entry.sharedKeyGroups?.[target.key] === target.identityGroup &&
246
+ target.key in entry.refs
247
+ )
248
+ }
247
249
 
248
- return entries.value.filter((e) => key in e.refs)
250
+ return entries.value.filter((entry) => target.key in entry.refs)
249
251
  })
250
252
 
251
- function openLookup(key: string) {
252
- lookupKey.value = lookupKey.value === key ? null : key
253
+ const lookupTitle = computed(() => {
254
+ if (!lookupTarget.value) {
255
+ return ''
256
+ }
257
+
258
+ if (lookupTarget.value.identityGroup) {
259
+ return `${lookupTarget.value.key} (shared identity)`
260
+ }
261
+
262
+ return lookupTarget.value.key
263
+ })
264
+
265
+ function openLookup(entry: RuntimeComposableEntry, key: string) {
266
+ const identityGroup = entry.sharedKeyGroups?.[key]
267
+ const next: LookupTarget = {
268
+ key,
269
+ composableName: entry.name,
270
+ identityGroup,
271
+ }
272
+
273
+ if (
274
+ lookupTarget.value?.key === next.key &&
275
+ lookupTarget.value?.composableName === next.composableName &&
276
+ lookupTarget.value?.identityGroup === next.identityGroup
277
+ ) {
278
+ lookupTarget.value = null
279
+
280
+ return
281
+ }
282
+
283
+ lookupTarget.value = next
253
284
  }
254
285
 
255
286
  // ── Inline editing ────────────────────────────────────────────────────────
@@ -408,8 +439,12 @@ function applyEdit() {
408
439
  <div v-for="[k, v] in Object.entries(entry.refs)" :key="k" class="composable-tracker__ref-row">
409
440
  <span
410
441
  class="composable-tracker__ref-key composable-tracker__ref-key--clickable mono text-sm"
411
- :title="`click to see all instances exposing '${k}'`"
412
- @click.stop="openLookup(k)"
442
+ :title="
443
+ entry.sharedKeyGroups?.[k]
444
+ ? `click to see instances sharing this exact '${k}' state`
445
+ : `click to see all instances exposing '${k}'`
446
+ "
447
+ @click.stop="openLookup(entry, k)"
413
448
  >
414
449
  {{ k }}
415
450
  </span>
@@ -530,14 +565,14 @@ function applyEdit() {
530
565
 
531
566
  <!-- ── Reverse lookup panel ──────────────────────────────────────── -->
532
567
  <Transition name="slide">
533
- <div v-if="lookupKey" class="composable-tracker__lookup-panel">
568
+ <div v-if="lookupTarget" class="composable-tracker__lookup-panel">
534
569
  <div class="composable-tracker__lookup-header">
535
- <span class="mono text-sm">{{ lookupKey }}</span>
570
+ <span class="mono text-sm">{{ lookupTitle }}</span>
536
571
  <span class="muted text-sm">— {{ lookupResults.length }} instance{{ lookupResults.length !== 1 ? 's' : '' }}</span>
537
- <button class="composable-tracker__clear-btn composable-tracker__lookup-close" @click="lookupKey = null">✕</button>
572
+ <button class="composable-tracker__clear-btn composable-tracker__lookup-close" @click="lookupTarget = null">✕</button>
538
573
  </div>
539
574
  <div v-if="!lookupResults.length" class="composable-tracker__lookup-empty muted text-sm">
540
- No mounted instances expose this key.
575
+ No instances matched this lookup.
541
576
  </div>
542
577
  <div v-for="r in lookupResults" :key="r.id" class="composable-tracker__lookup-row">
543
578
  <span class="mono text-sm">{{ r.name }}</span>
@@ -2,6 +2,8 @@
2
2
  import { computed, defineComponent, h, ref, watch, type VNode } from 'vue'
3
3
  import { useResizablePane } from '@observatory-client/composables/useResizablePane'
4
4
  import { useObservatoryData, openInEditor as openInEditorFromStore } from '@observatory-client/stores/observatory'
5
+ import { exportJson, importJson } from '@observatory-client/composables/useExportImport'
6
+ import type { ObservatoryExportFile } from '@observatory-client/composables/useExportImport'
5
7
  import type { RenderEntry, RenderEvent } from '@observatory/types/snapshot'
6
8
 
7
9
  interface ComponentNode {
@@ -197,6 +199,7 @@ const activeThreshold = computed({
197
199
  })
198
200
  const activeHotOnly = ref(false)
199
201
  const frozen = ref(false)
202
+ const isImportedSnapshot = ref(false)
200
203
  const search = ref('')
201
204
  const activeSelectedId = ref<string | null>(null)
202
205
  const activeRootId = ref<string | null>(null)
@@ -671,6 +674,7 @@ function updateSearch(event: Event) {
671
674
  function toggleFreeze() {
672
675
  if (frozen.value) {
673
676
  frozen.value = false
677
+ isImportedSnapshot.value = false
674
678
  frozenSnapshot.value = []
675
679
 
676
680
  return
@@ -680,6 +684,45 @@ function toggleFreeze() {
680
684
  frozen.value = true
681
685
  }
682
686
 
687
+ function handleExport() {
688
+ exportJson(`observatory-renders-${Date.now()}.json`, {
689
+ type: 'observatory-renders',
690
+ version: '1',
691
+ exportedAt: Date.now(),
692
+ count: displayEntries.value.length,
693
+ data: displayEntries.value,
694
+ })
695
+ }
696
+
697
+ async function handleImport() {
698
+ let parsed: unknown
699
+
700
+ try {
701
+ parsed = await importJson()
702
+ } catch (err) {
703
+ if (err instanceof Error && err.message !== 'cancelled') {
704
+ alert(`Import failed: ${err.message}`)
705
+ }
706
+ return
707
+ }
708
+
709
+ const file = parsed as ObservatoryExportFile<RenderEntry>
710
+
711
+ if (
712
+ file?.type !== 'observatory-renders' ||
713
+ file?.version !== '1' ||
714
+ !Array.isArray(file?.data) ||
715
+ (file.data.length > 0 && (file.data[0]?.uid === undefined || !file.data[0]?.name || !file.data[0]?.file))
716
+ ) {
717
+ alert('Invalid observatory renders file.')
718
+ return
719
+ }
720
+
721
+ frozenSnapshot.value = file.data
722
+ frozen.value = true
723
+ isImportedSnapshot.value = true
724
+ }
725
+
683
726
  function basename(file: string) {
684
727
  return (
685
728
  file
@@ -732,8 +775,10 @@ function formatTimestamp(t: number): string {
732
775
  <option v-for="r in knownRoutes" :key="r" :value="r">{{ r }}</option>
733
776
  </select>
734
777
  <button :class="{ active: frozen }" class="render-heatmap__freeze tracker-toolbar__spacer" @click="toggleFreeze">
735
- {{ frozen ? 'unfreeze' : 'freeze snapshot' }}
778
+ {{ frozen && isImportedSnapshot ? 'unfreeze (imported)' : frozen ? 'unfreeze' : 'freeze snapshot' }}
736
779
  </button>
780
+ <button class="render-heatmap__action-btn" title="Export render data as JSON" @click="handleExport">↓ export</button>
781
+ <button class="render-heatmap__action-btn" title="Import render data from JSON file" @click="handleImport">↑ import</button>
737
782
  </div>
738
783
 
739
784
  <div class="render-heatmap__stats tracker-stats-row">
@@ -952,6 +997,23 @@ function formatTimestamp(t: number): string {
952
997
  width: 90px;
953
998
  }
954
999
 
1000
+ .render-heatmap__action-btn {
1001
+ padding: 3px 8px;
1002
+ background: none;
1003
+ border: 1px solid var(--border);
1004
+ color: var(--text-secondary);
1005
+ cursor: pointer;
1006
+ font-size: 11px;
1007
+ border-radius: 3px;
1008
+ transition: all 0.12s;
1009
+ font-family: var(--mono);
1010
+ }
1011
+
1012
+ .render-heatmap__action-btn:hover {
1013
+ background: var(--bg-secondary);
1014
+ color: var(--text);
1015
+ }
1016
+
955
1017
  .stat-sub {
956
1018
  margin-top: var(--tracker-space-1);
957
1019
  font-size: var(--tracker-font-size-sm);