nuxt-devtools-observatory 0.1.24 → 0.1.25

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.
@@ -2,7 +2,20 @@
2
2
  import { ref, computed } from 'vue'
3
3
  import { useObservatoryData, getObservatoryOrigin, type ComposableEntry as RuntimeComposableEntry } from '../stores/observatory'
4
4
 
5
- const { composables: rawEntries, connected, clearComposables } = useObservatoryData()
5
+ const { composables: rawEntries, connected, features, clearComposables } = useObservatoryData()
6
+
7
+ const composableMode = computed<'route' | 'session'>(() => (features.value?.composableNavigationMode === 'session' ? 'session' : 'route'))
8
+
9
+ function toggleComposableMode() {
10
+ const origin = getObservatoryOrigin()
11
+
12
+ if (!origin) {
13
+ return
14
+ }
15
+
16
+ const nextMode = composableMode.value === 'route' ? 'session' : 'route'
17
+ window.top?.postMessage({ type: 'observatory:set-composable-mode', mode: nextMode }, origin)
18
+ }
6
19
 
7
20
  function clearSession() {
8
21
  const origin = getObservatoryOrigin()
@@ -17,17 +30,28 @@ function clearSession() {
17
30
  // same composable in different components are two separate rows.
18
31
 
19
32
  function formatVal(v: unknown): string {
20
- if (v === null) return 'null'
21
- if (v === undefined) return 'undefined'
22
- if (typeof v === 'string') return `"${v}"`
33
+ if (v === null) {
34
+ return 'null'
35
+ }
36
+
37
+ if (v === undefined) {
38
+ return 'undefined'
39
+ }
40
+
41
+ if (typeof v === 'string') {
42
+ return `"${v}"`
43
+ }
44
+
23
45
  if (typeof v === 'object') {
24
46
  try {
25
47
  const s = JSON.stringify(v)
48
+
26
49
  return s.length > 80 ? s.slice(0, 80) + '…' : s
27
50
  } catch {
28
51
  return String(v)
29
52
  }
30
53
  }
54
+
31
55
  return String(v)
32
56
  }
33
57
 
@@ -90,7 +114,11 @@ const counts = computed(() => ({
90
114
  }))
91
115
 
92
116
  const filtered = computed(() => {
93
- return entries.value.filter((entry) => {
117
+ // Newest entries are appended by the runtime registry, so reverse for recency-first UI.
118
+ const reversed = [...entries.value].reverse()
119
+
120
+ // Apply all filters first
121
+ const filtered = reversed.filter((entry) => {
94
122
  if (filter.value === 'leak' && !entry.leak) {
95
123
  return false
96
124
  }
@@ -124,6 +152,21 @@ const filtered = computed(() => {
124
152
 
125
153
  return true
126
154
  })
155
+
156
+ // Partition into layout-level (pinned to top) and regular entries
157
+ const layoutEntries: RuntimeComposableEntry[] = []
158
+ const regularEntries: RuntimeComposableEntry[] = []
159
+
160
+ for (const entry of filtered) {
161
+ if (entry.isLayoutComposable) {
162
+ layoutEntries.push(entry)
163
+ } else {
164
+ regularEntries.push(entry)
165
+ }
166
+ }
167
+
168
+ // Combine: layout entries first (already sorted by recency), then regular entries
169
+ return [...layoutEntries, ...regularEntries]
127
170
  })
128
171
 
129
172
  function lifecycleRows(entry: RuntimeComposableEntry) {
@@ -175,7 +218,10 @@ function refExpandKey(entryId: string, refKey: string) {
175
218
  }
176
219
 
177
220
  function isLongValue(v: unknown): boolean {
178
- if (v === null || v === undefined || typeof v !== 'object') return false
221
+ if (v === null || v === undefined || typeof v !== 'object') {
222
+ return false
223
+ }
224
+
179
225
  try {
180
226
  return JSON.stringify(v).length > 60
181
227
  } catch {
@@ -190,8 +236,13 @@ function isRefExpanded(entryId: string, refKey: string): boolean {
190
236
  function toggleRefExpand(entryId: string, refKey: string) {
191
237
  const key = refExpandKey(entryId, refKey)
192
238
  const next = new Set(expandedRefs.value)
193
- if (next.has(key)) next.delete(key)
194
- else next.add(key)
239
+
240
+ if (next.has(key)) {
241
+ next.delete(key)
242
+ } else {
243
+ next.add(key)
244
+ }
245
+
195
246
  expandedRefs.value = next
196
247
  }
197
248
 
@@ -247,6 +298,7 @@ function applyEdit() {
247
298
  editError.value = ''
248
299
  } catch (err) {
249
300
  editError.value = `Invalid JSON: ${(err as Error).message}`
301
+
250
302
  return
251
303
  }
252
304
 
@@ -296,8 +348,17 @@ function applyEdit() {
296
348
  <button :class="{ active: filter === 'mounted' }" @click="filter = 'mounted'">mounted</button>
297
349
  <button :class="{ 'danger-active': filter === 'leak' }" @click="filter = 'leak'">leaks only</button>
298
350
  <button :class="{ active: filter === 'unmounted' }" @click="filter = 'unmounted'">unmounted</button>
351
+ <button
352
+ class="mode-btn"
353
+ :title="`switch to ${composableMode === 'route' ? 'session' : 'route'} mode`"
354
+ @click="toggleComposableMode"
355
+ >
356
+ mode: {{ composableMode }}
357
+ </button>
299
358
  <input v-model="search" type="search" placeholder="search name, file, or ref…" style="max-width: 220px; margin-left: auto" />
300
- <button class="clear-btn" title="Clear session history" @click="clearSession">clear</button>
359
+ <button v-if="composableMode === 'session'" class="clear-btn" title="Clear session history" @click="clearSession">
360
+ clear session
361
+ </button>
301
362
  </div>
302
363
 
303
364
  <div class="list">
@@ -543,6 +604,16 @@ function applyEdit() {
543
604
  background: transparent;
544
605
  }
545
606
 
607
+ .mode-btn {
608
+ border-color: color-mix(in srgb, var(--blue) 40%, var(--border));
609
+ color: var(--blue);
610
+ }
611
+
612
+ .mode-btn:hover {
613
+ border-color: var(--blue);
614
+ background: color-mix(in srgb, var(--blue) 12%, transparent);
615
+ }
616
+
546
617
  .list {
547
618
  flex: 1;
548
619
  overflow: auto;
package/dist/module.d.mts CHANGED
@@ -39,6 +39,13 @@ interface ModuleOptions {
39
39
  * @default 100
40
40
  */
41
41
  maxRenderTimeline?: number;
42
+ /**
43
+ * Composable tracker navigation mode.
44
+ * - `route`: clear composable entries on every page navigation
45
+ * - `session`: keep entries across navigations until manually cleared
46
+ * @default 'route'
47
+ */
48
+ composableNavigationMode?: 'route' | 'session';
42
49
  /**
43
50
  * Enable the useFetch / useAsyncData dashboard tab
44
51
  * @default true
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.24",
7
+ "version": "0.1.25",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -558,6 +558,7 @@ const defaults = {
558
558
  maxComposableHistory: process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50,
559
559
  maxComposableEntries: process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300,
560
560
  maxRenderTimeline: process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100,
561
+ composableNavigationMode: process.env.OBSERVATORY_COMPOSABLE_NAVIGATION_MODE === "session" ? "session" : "route",
561
562
  heatmapHideInternals: process.env.OBSERVATORY_HEATMAP_HIDE_INTERNALS === "true"
562
563
  };
563
564
  const module$1 = defineNuxtModule({
@@ -593,7 +594,8 @@ const module$1 = defineNuxtModule({
593
594
  maxTransitions: options.maxTransitions ?? (process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500),
594
595
  maxComposableHistory: options.maxComposableHistory ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50),
595
596
  maxComposableEntries: options.maxComposableEntries ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300),
596
- maxRenderTimeline: options.maxRenderTimeline ?? (process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100)
597
+ maxRenderTimeline: options.maxRenderTimeline ?? (process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100),
598
+ composableNavigationMode: options.composableNavigationMode ?? (process.env.OBSERVATORY_COMPOSABLE_NAVIGATION_MODE === "session" ? "session" : "route")
597
599
  };
598
600
  nuxt.hook("vite:extendConfig", (config) => {
599
601
  const alias = config.resolve?.alias;
@@ -683,6 +685,7 @@ ${configScript}`);
683
685
  maxComposableHistory: resolved.maxComposableHistory,
684
686
  maxComposableEntries: resolved.maxComposableEntries,
685
687
  maxRenderTimeline: resolved.maxRenderTimeline,
688
+ composableNavigationMode: resolved.composableNavigationMode,
686
689
  heatmapHideInternals: resolved.heatmapHideInternals,
687
690
  heatmapThresholdCount: resolved.heatmapThresholdCount,
688
691
  heatmapThresholdTime: resolved.heatmapThresholdTime
@@ -34,6 +34,10 @@ export interface ComposableEntry {
34
34
  line: number;
35
35
  /** Route path the composable was registered on, e.g. "/products". */
36
36
  route: string;
37
+ /** File path of the component that called this composable. */
38
+ callerComponentFile?: string;
39
+ /** Whether this composable is called from a layout component (persists across pages). */
40
+ isLayoutComposable?: boolean;
37
41
  }
38
42
  /**
39
43
  * Registers a new composable entry, updates an existing one, or retrieves all entries.
@@ -62,6 +66,7 @@ export declare function setupComposableRegistry(): {
62
66
  registerRawRefs: (id: string, refs: Record<string, unknown>) => void;
63
67
  onComposableChange: (cb: () => void) => void;
64
68
  clear: () => void;
69
+ clearNonLayout: () => void;
65
70
  setRoute: (path: string) => void;
66
71
  getRoute: () => string;
67
72
  update: (id: string, patch: Partial<ComposableEntry>) => void;
@@ -213,7 +213,9 @@ export function setupComposableRegistry() {
213
213
  lifecycle: entry.lifecycle,
214
214
  file: entry.file,
215
215
  line: entry.line,
216
- route: entry.route
216
+ route: entry.route,
217
+ callerComponentFile: entry.callerComponentFile,
218
+ isLayoutComposable: entry.isLayoutComposable
217
219
  };
218
220
  }
219
221
  function getAll() {
@@ -254,6 +256,30 @@ export function setupComposableRegistry() {
254
256
  markDirty();
255
257
  emit("composable:clear", {});
256
258
  }
259
+ function clearNonLayout() {
260
+ if (_pendingFrame !== null) {
261
+ cancelAnimationFrame(_pendingFrame);
262
+ _pendingFrame = null;
263
+ }
264
+ const layoutIds = [...entries.entries()].filter(([, entry]) => entry.isLayoutComposable).map(([id]) => id);
265
+ for (const [id] of entries.entries()) {
266
+ if (!layoutIds.includes(id)) {
267
+ const stop = liveRefWatchers.get(id);
268
+ if (stop) {
269
+ stop();
270
+ liveRefWatchers.delete(id);
271
+ }
272
+ liveRefs.delete(id);
273
+ rawRefs.delete(id);
274
+ prevValues.delete(id);
275
+ entryHistory.delete(id);
276
+ entries.delete(id);
277
+ }
278
+ }
279
+ sharedKeysCache.clear();
280
+ markDirty();
281
+ emit("composable:clear", {});
282
+ }
257
283
  function editValue(id, key, value) {
258
284
  const live = liveRefs.get(id);
259
285
  if (!live) {
@@ -278,6 +304,7 @@ export function setupComposableRegistry() {
278
304
  registerRawRefs,
279
305
  onComposableChange,
280
306
  clear,
307
+ clearNonLayout,
281
308
  setRoute,
282
309
  getRoute,
283
310
  update,
@@ -350,6 +377,21 @@ export function __trackComposable(name, callFn, meta) {
350
377
  }
351
378
  }
352
379
  }
380
+ const inst = instance;
381
+ let callerComponentFile;
382
+ if (inst?.type?.__file) {
383
+ callerComponentFile = inst.type.__file;
384
+ } else if (inst?.__vueParentComponent?.type?.__file) {
385
+ callerComponentFile = inst.__vueParentComponent.type.__file;
386
+ } else if (inst?.parent?.type?.__file) {
387
+ callerComponentFile = inst.parent.type.__file;
388
+ } else if (inst?.__vueParentComponent?.fileName) {
389
+ callerComponentFile = inst.__vueParentComponent.fileName;
390
+ } else if (inst?.type?.name && inst?.type?.name.includes("default")) {
391
+ callerComponentFile = `layouts/${inst.type.name}.vue`;
392
+ }
393
+ const normalizedFile = callerComponentFile?.replace(/\\/g, "/") ?? "";
394
+ const isLayoutComponent = normalizedFile.includes("/layouts/");
353
395
  const entry = {
354
396
  id,
355
397
  name,
@@ -371,7 +413,9 @@ export function __trackComposable(name, callFn, meta) {
371
413
  },
372
414
  file: meta.file,
373
415
  line: meta.line,
374
- route: registry.getRoute()
416
+ route: registry.getRoute(),
417
+ callerComponentFile,
418
+ isLayoutComposable: isLayoutComponent
375
419
  };
376
420
  if (!instance && registry.getAll().some((e) => e.id === id)) {
377
421
  registry.update(id, {
@@ -11,6 +11,7 @@ export default defineNuxtPlugin(() => {
11
11
  }
12
12
  const nuxtApp = useNuxtApp();
13
13
  const config = useRuntimeConfig().public.observatory;
14
+ let composableNavigationMode = config.composableNavigationMode === "session" ? "session" : "route";
14
15
  if (config.renderHeatmap) {
15
16
  nuxtApp.vueApp.config.performance = true;
16
17
  }
@@ -55,6 +56,15 @@ export default defineNuxtPlugin(() => {
55
56
  }
56
57
  const source = event.source;
57
58
  source?.postMessage({ type: "observatory:snapshot", data: buildSnapshot() }, event.origin);
59
+ return;
60
+ }
61
+ if (type === "observatory:set-composable-mode") {
62
+ const mode = event.data?.mode;
63
+ if (mode === "route" || mode === "session") {
64
+ composableNavigationMode = mode;
65
+ }
66
+ const source = event.source;
67
+ source?.postMessage({ type: "observatory:snapshot", data: buildSnapshot() }, event.origin);
58
68
  }
59
69
  if (type === "observatory:edit-composable") {
60
70
  const { id, key, value } = event.data;
@@ -109,8 +119,8 @@ export default defineNuxtPlugin(() => {
109
119
  if (provideInject && typeof provideInject.clear === "function")
110
120
  provideInject.clear();
111
121
  const composable = registries.composable;
112
- if (composable && typeof composable.clear === "function")
113
- composable.clear();
122
+ if (composableNavigationMode === "route" && composable && typeof composable.clearNonLayout === "function")
123
+ composable.clearNonLayout();
114
124
  const transition = registries.transition;
115
125
  if (transition && typeof transition.clear === "function")
116
126
  transition.clear();
@@ -171,6 +181,7 @@ export default defineNuxtPlugin(() => {
171
181
  fetchDashboard: !!registries.fetch,
172
182
  provideInjectGraph: !!registries.provideInject,
173
183
  composableTracker: !!registries.composable,
184
+ composableNavigationMode,
174
185
  renderHeatmap: !!registries.render,
175
186
  transitionTracker: !!registries.transition
176
187
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-devtools-observatory",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "Nuxt DevTools: useFetch Dashboard, provide/inject Graph, Composable Tracker, Render Heatmap",
5
5
  "license": "MIT",
6
6
  "repository": {