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.
@@ -1,10 +1,89 @@
1
- import { ref, isRef, isReadonly, unref, getCurrentInstance, onUnmounted } from "vue";
1
+ import { ref, isRef, isReactive, isReadonly, unref, computed, watchEffect, getCurrentInstance, onUnmounted } from "vue";
2
2
  export function setupComposableRegistry() {
3
3
  const entries = ref(/* @__PURE__ */ new Map());
4
+ const liveRefs = /* @__PURE__ */ new Map();
5
+ const liveRefWatchers = /* @__PURE__ */ new Map();
6
+ const MAX_HISTORY = 50;
7
+ const entryHistory = /* @__PURE__ */ new Map();
8
+ const prevValues = /* @__PURE__ */ new Map();
9
+ const rawRefs = /* @__PURE__ */ new Map();
10
+ function computeSharedKeys(id, name) {
11
+ const ownRaw = rawRefs.get(id);
12
+ if (!ownRaw) return [];
13
+ const shared = /* @__PURE__ */ new Set();
14
+ for (const [otherId, entry] of entries.value.entries()) {
15
+ if (otherId === id || entry.name !== name) continue;
16
+ const otherRaw = rawRefs.get(otherId);
17
+ if (!otherRaw) continue;
18
+ for (const [key, obj] of Object.entries(ownRaw)) {
19
+ if (key in otherRaw && otherRaw[key] === obj) {
20
+ shared.add(key);
21
+ }
22
+ }
23
+ }
24
+ return [...shared];
25
+ }
26
+ let currentRoute = "/";
27
+ function setRoute(path) {
28
+ currentRoute = path;
29
+ }
30
+ function getRoute() {
31
+ return currentRoute;
32
+ }
4
33
  function register(entry) {
5
34
  entries.value.set(entry.id, entry);
6
35
  emit("composable:register", entry);
7
36
  }
37
+ function registerLiveRefs(id, refs) {
38
+ const prevStop = liveRefWatchers.get(id);
39
+ if (prevStop) prevStop();
40
+ liveRefWatchers.delete(id);
41
+ if (Object.keys(refs).length === 0) {
42
+ liveRefs.delete(id);
43
+ rawRefs.delete(id);
44
+ prevValues.delete(id);
45
+ return;
46
+ }
47
+ liveRefs.set(id, refs);
48
+ prevValues.set(
49
+ id,
50
+ Object.fromEntries(
51
+ Object.entries(refs).map(([k, r]) => {
52
+ try {
53
+ return [k, JSON.stringify(unref(r)) ?? ""];
54
+ } catch {
55
+ return [k, ""];
56
+ }
57
+ })
58
+ )
59
+ );
60
+ const stop = watchEffect(() => {
61
+ const prev = prevValues.get(id) ?? {};
62
+ const now = {};
63
+ const t = typeof performance !== "undefined" ? performance.now() : Date.now();
64
+ for (const [k, r] of Object.entries(refs)) {
65
+ const val = unref(r);
66
+ const serialised = JSON.stringify(val) ?? "";
67
+ now[k] = serialised;
68
+ if (serialised !== prev[k]) {
69
+ const history = entryHistory.get(id) ?? [];
70
+ history.push({ t, key: k, value: safeValue(val) });
71
+ if (history.length > MAX_HISTORY) history.shift();
72
+ entryHistory.set(id, history);
73
+ }
74
+ }
75
+ prevValues.set(id, now);
76
+ _onChange?.();
77
+ });
78
+ liveRefWatchers.set(id, stop);
79
+ }
80
+ function registerRawRefs(id, refs) {
81
+ rawRefs.set(id, refs);
82
+ }
83
+ let _onChange = null;
84
+ function onComposableChange(cb) {
85
+ _onChange = cb;
86
+ }
8
87
  function update(id, patch) {
9
88
  const existing = entries.value.get(id);
10
89
  if (!existing) {
@@ -31,6 +110,25 @@ export function setupComposableRegistry() {
31
110
  return val;
32
111
  }
33
112
  function sanitize(entry) {
113
+ const live = liveRefs.get(entry.id);
114
+ const hasLive = live != null && Object.keys(live).length > 0;
115
+ const freshRefs = hasLive ? Object.fromEntries(
116
+ Object.entries(live).map(([k, r]) => [
117
+ k,
118
+ {
119
+ type: entry.refs[k]?.type ?? "ref",
120
+ value: safeValue(unref(r))
121
+ }
122
+ ])
123
+ ) : Object.fromEntries(
124
+ Object.entries(entry.refs).map(([k, v]) => [
125
+ k,
126
+ {
127
+ type: v.type,
128
+ value: safeValue(typeof v.value === "object" && v.value !== null && "value" in v.value ? v.value.value : v.value)
129
+ }
130
+ ])
131
+ );
34
132
  return {
35
133
  id: entry.id,
36
134
  name: entry.name,
@@ -39,20 +137,15 @@ export function setupComposableRegistry() {
39
137
  status: entry.status,
40
138
  leak: entry.leak,
41
139
  leakReason: entry.leakReason,
42
- refs: Object.fromEntries(
43
- Object.entries(entry.refs).map(([k, v]) => [
44
- k,
45
- {
46
- type: v.type,
47
- value: safeValue(typeof v.value === "object" && v.value !== null && "value" in v.value ? v.value.value : v.value)
48
- }
49
- ])
50
- ),
140
+ refs: freshRefs,
141
+ history: entryHistory.get(entry.id) ?? [],
142
+ sharedKeys: computeSharedKeys(entry.id, entry.name),
51
143
  watcherCount: entry.watcherCount,
52
144
  intervalCount: entry.intervalCount,
53
145
  lifecycle: entry.lifecycle,
54
146
  file: entry.file,
55
- line: entry.line
147
+ line: entry.line,
148
+ route: entry.route
56
149
  };
57
150
  }
58
151
  function getAll() {
@@ -65,7 +158,17 @@ export function setupComposableRegistry() {
65
158
  const channel = window.__nuxt_devtools__?.channel;
66
159
  channel?.send(event, data);
67
160
  }
68
- return { register, update, getAll };
161
+ function clear() {
162
+ for (const stop of liveRefWatchers.values()) stop();
163
+ liveRefWatchers.clear();
164
+ liveRefs.clear();
165
+ rawRefs.clear();
166
+ prevValues.clear();
167
+ entryHistory.clear();
168
+ entries.value.clear();
169
+ emit("composable:clear", {});
170
+ }
171
+ return { register, registerLiveRefs, registerRawRefs, onComposableChange, clear, setRoute, getRoute, update, getAll };
69
172
  }
70
173
  export function __trackComposable(name, callFn, meta) {
71
174
  if (!import.meta.dev) {
@@ -79,40 +182,53 @@ export function __trackComposable(name, callFn, meta) {
79
182
  return callFn();
80
183
  }
81
184
  const instance = getCurrentInstance();
82
- const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}`;
185
+ const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}::${Math.random().toString(36).slice(2, 7)}`;
83
186
  const trackedIntervals = [];
84
- const originalSetInterval = window.setInterval;
85
- const originalClearInterval = window.clearInterval;
86
187
  const clearedIntervals = /* @__PURE__ */ new Set();
87
- window.setInterval = ((fn, ms, ...rest) => {
88
- const id2 = originalSetInterval(fn, ms, ...rest);
89
- trackedIntervals.push(id2);
90
- return id2;
91
- });
92
- window.clearInterval = ((id2) => {
93
- if (id2 !== void 0) {
94
- clearedIntervals.add(id2);
95
- }
96
- originalClearInterval(id2);
97
- });
188
+ const alreadyPatched = !!window.setInterval.__obs;
189
+ let originalSetInterval = null;
190
+ let originalClearInterval = null;
191
+ if (!alreadyPatched) {
192
+ originalSetInterval = window.setInterval;
193
+ originalClearInterval = window.clearInterval;
194
+ const patchedSetInterval = ((fn, ms, ...rest) => {
195
+ const timerId = originalSetInterval(fn, ms, ...rest);
196
+ trackedIntervals.push(timerId);
197
+ return timerId;
198
+ });
199
+ patchedSetInterval.__obs = true;
200
+ window.setInterval = patchedSetInterval;
201
+ window.clearInterval = ((timerId) => {
202
+ if (timerId !== void 0) clearedIntervals.add(timerId);
203
+ originalClearInterval(timerId);
204
+ });
205
+ }
98
206
  const effectsBefore = new Set(instance?.scope?.effects ?? []);
99
207
  const mountedHooksBefore = instance?.bm?.length ?? 0;
100
208
  const unmountedHooksBefore = instance?.um?.length ?? 0;
101
209
  const result = callFn();
102
- window.setInterval = originalSetInterval;
103
- window.clearInterval = originalClearInterval;
210
+ if (!alreadyPatched && originalSetInterval) {
211
+ window.setInterval = originalSetInterval;
212
+ window.clearInterval = originalClearInterval;
213
+ }
104
214
  const trackedWatchers = (instance?.scope?.effects ?? []).filter((effect) => !effectsBefore.has(effect)).map((effect) => ({
105
215
  effect,
106
216
  stop: () => effect.stop?.()
107
217
  }));
108
218
  const refs = {};
219
+ const liveRefMap = {};
220
+ const rawRefMap = {};
109
221
  if (result && typeof result === "object") {
110
222
  for (const [key, val] of Object.entries(result)) {
111
223
  if (isRef(val)) {
112
- refs[key] = {
113
- type: isReadonly(val) ? "computed" : "ref",
114
- value: safeSnapshot(unref(val))
115
- };
224
+ const type = isReadonly(val) ? "computed" : "ref";
225
+ refs[key] = { type, value: safeSnapshot(unref(val)) };
226
+ liveRefMap[key] = val;
227
+ rawRefMap[key] = val;
228
+ } else if (isReactive(val)) {
229
+ refs[key] = { type: "reactive", value: safeSnapshot(val) };
230
+ liveRefMap[key] = computed(() => val);
231
+ rawRefMap[key] = val;
116
232
  }
117
233
  }
118
234
  }
@@ -124,6 +240,9 @@ export function __trackComposable(name, callFn, meta) {
124
240
  status: "mounted",
125
241
  leak: false,
126
242
  refs,
243
+ history: [],
244
+ sharedKeys: [],
245
+ // computed lazily in sanitize() after all instances are registered
127
246
  watcherCount: trackedWatchers.length,
128
247
  intervalCount: trackedIntervals.length,
129
248
  lifecycle: {
@@ -133,31 +252,37 @@ export function __trackComposable(name, callFn, meta) {
133
252
  intervalsCleaned: true
134
253
  },
135
254
  file: meta.file,
136
- line: meta.line
255
+ line: meta.line,
256
+ route: registry.getRoute()
137
257
  };
138
258
  registry.register(entry);
139
- onUnmounted(() => {
140
- const leakedWatchers = trackedWatchers.filter((w) => w.effect.active);
141
- const leakedIntervals = trackedIntervals.filter((id2) => !clearedIntervals.has(id2));
142
- const leak = leakedWatchers.length > 0 || leakedIntervals.length > 0;
143
- const reasons = [];
144
- if (leakedWatchers.length > 0) {
145
- reasons.push(`${leakedWatchers.length} watcher${leakedWatchers.length > 1 ? "s" : ""} still active after unmount`);
146
- }
147
- if (leakedIntervals.length > 0) {
148
- reasons.push(`setInterval #${leakedIntervals.join(", #")} never cleared`);
149
- }
150
- registry.update(id, {
151
- status: "unmounted",
152
- leak,
153
- leakReason: reasons.join(" \xB7 ") || void 0,
154
- lifecycle: {
155
- ...entry.lifecycle,
156
- watchersCleaned: leakedWatchers.length === 0,
157
- intervalsCleaned: leakedIntervals.length === 0
259
+ registry.registerLiveRefs(id, liveRefMap);
260
+ registry.registerRawRefs(id, rawRefMap);
261
+ if (instance) {
262
+ onUnmounted(() => {
263
+ const leakedWatchers = trackedWatchers.filter((w) => w.effect.active);
264
+ const leakedIntervals = trackedIntervals.filter((timerId) => !clearedIntervals.has(timerId));
265
+ const leak = leakedWatchers.length > 0 || leakedIntervals.length > 0;
266
+ const reasons = [];
267
+ if (leakedWatchers.length > 0) {
268
+ reasons.push(`${leakedWatchers.length} watcher${leakedWatchers.length > 1 ? "s" : ""} still active after unmount`);
269
+ }
270
+ if (leakedIntervals.length > 0) {
271
+ reasons.push(`setInterval #${leakedIntervals.join(", #")} never cleared`);
158
272
  }
273
+ registry.update(id, {
274
+ status: "unmounted",
275
+ leak,
276
+ leakReason: reasons.join(" \xB7 ") || void 0,
277
+ lifecycle: {
278
+ ...entry.lifecycle,
279
+ watchersCleaned: leakedWatchers.length === 0,
280
+ intervalsCleaned: leakedIntervals.length === 0
281
+ }
282
+ });
283
+ registry.registerLiveRefs(id, {});
159
284
  });
160
- });
285
+ }
161
286
  return result;
162
287
  }
163
288
  function safeSnapshot(value) {
@@ -8,6 +8,17 @@ export interface ProvideEntry {
8
8
  isReactive: boolean;
9
9
  valueSnapshot: unknown;
10
10
  line: number;
11
+ /**
12
+ * Scope of the provide:
13
+ * - 'global' — provided via app.provide() or at the app root (no parent component)
14
+ * - 'layout' — provided by a layout component (file path contains 'layout')
15
+ * - 'component' — provided by a regular component
16
+ */
17
+ scope: 'global' | 'layout' | 'component';
18
+ /**
19
+ * True when a parent component already provides the same key — this provide shadows it.
20
+ */
21
+ isShadowing: boolean;
11
22
  }
12
23
  export interface InjectEntry {
13
24
  key: string;
@@ -24,7 +35,7 @@ export interface InjectEntry {
24
35
  /**
25
36
  * Sets up the provide/inject registry, which tracks all provide/inject calls
26
37
  * and their associated metadata (e.g. component name, file, line).
27
- * @returns {object} The provide/inject registry with `registerProvide`, `registerInject`, and `getAll` members.
38
+ * @returns {object} The provide/inject registry with `registerProvide`, `registerInject`, `getAll`, and `clear` members.
28
39
  */
29
40
  export declare function setupProvideInjectRegistry(): {
30
41
  registerProvide: (entry: ProvideEntry) => void;
@@ -33,6 +44,7 @@ export declare function setupProvideInjectRegistry(): {
33
44
  provides: ProvideEntry[];
34
45
  injects: InjectEntry[];
35
46
  };
47
+ clear: () => void;
36
48
  };
37
49
  export declare function __devProvide(key: string | symbol, value: unknown, meta: {
38
50
  file: string;
@@ -3,13 +3,28 @@ export function setupProvideInjectRegistry() {
3
3
  const provides = ref([]);
4
4
  const injects = ref([]);
5
5
  function registerProvide(entry) {
6
- provides.value.push(entry);
6
+ const idx = provides.value.findIndex((e) => e.key === entry.key && e.componentUid === entry.componentUid);
7
+ if (idx !== -1) {
8
+ provides.value[idx] = entry;
9
+ } else {
10
+ provides.value.push(entry);
11
+ }
7
12
  emit("provide:register", entry);
8
13
  }
9
14
  function registerInject(entry) {
10
- injects.value.push(entry);
15
+ const idx = injects.value.findIndex((e) => e.key === entry.key && e.componentUid === entry.componentUid);
16
+ if (idx !== -1) {
17
+ injects.value[idx] = entry;
18
+ } else {
19
+ injects.value.push(entry);
20
+ }
11
21
  emit("inject:register", entry);
12
22
  }
23
+ function clear() {
24
+ provides.value = [];
25
+ injects.value = [];
26
+ emit("provide:clear", {});
27
+ }
13
28
  function safeValue(val) {
14
29
  if (val === void 0 || val === null) {
15
30
  return val;
@@ -36,7 +51,9 @@ export function setupProvideInjectRegistry() {
36
51
  parentFile: entry.parentFile,
37
52
  isReactive: entry.isReactive,
38
53
  valueSnapshot: safeValue(entry.valueSnapshot),
39
- line: entry.line
54
+ line: entry.line,
55
+ scope: entry.scope,
56
+ isShadowing: entry.isShadowing
40
57
  };
41
58
  }
42
59
  function sanitizeInject(entry) {
@@ -66,7 +83,7 @@ export function setupProvideInjectRegistry() {
66
83
  const channel = window.__nuxt_devtools__?.channel;
67
84
  channel?.send(event, data);
68
85
  }
69
- return { registerProvide, registerInject, getAll };
86
+ return { registerProvide, registerInject, getAll, clear };
70
87
  }
71
88
  export function __devProvide(key, value, meta) {
72
89
  provide(key, value);
@@ -78,8 +95,17 @@ export function __devProvide(key, value, meta) {
78
95
  return;
79
96
  }
80
97
  const instance = getCurrentInstance();
98
+ const keyStr = String(key);
99
+ const file = meta.file.toLowerCase();
100
+ let scope = "component";
101
+ if (!instance?.parent || instance?.parent?.type === instance?.appContext?.app?._component) {
102
+ scope = "global";
103
+ } else if (file.includes("layout") || file.includes("layouts")) {
104
+ scope = "layout";
105
+ }
106
+ const isShadowing = findProvider(keyStr, instance) !== null;
81
107
  registry.registerProvide({
82
- key: String(key),
108
+ key: keyStr,
83
109
  componentName: instance?.type?.__name ?? "unknown",
84
110
  componentFile: meta.file,
85
111
  componentUid: instance?.uid ?? -1,
@@ -87,7 +113,9 @@ export function __devProvide(key, value, meta) {
87
113
  parentFile: instance?.parent?.type?.__file,
88
114
  isReactive: isRef(value) || isReactive(value),
89
115
  valueSnapshot: safeSnapshot(unref(value)),
90
- line: meta.line
116
+ line: meta.line,
117
+ scope,
118
+ isShadowing
91
119
  });
92
120
  }
93
121
  export function __devInject(key, defaultValue, meta) {
@@ -3,7 +3,11 @@ export interface RenderEntry {
3
3
  name: string;
4
4
  file: string;
5
5
  element?: string;
6
- renders: number;
6
+ /** Total times this instance mounted (usually 1, >1 means it was unmounted+remounted) */
7
+ mountCount: number;
8
+ /** Re-renders triggered by reactive state changes (excludes initial mount) */
9
+ rerenders: number;
10
+ /** Subset of rerenders that happened within 800ms of a navigation */
7
11
  totalMs: number;
8
12
  avgMs: number;
9
13
  triggers: Array<{
@@ -19,18 +23,27 @@ export interface RenderEntry {
19
23
  top: number;
20
24
  left: number;
21
25
  };
22
- children: number[];
23
26
  parentUid?: number;
27
+ /** True if this component survived at least one reset() — indicates a layout/persistent component */
28
+ isPersistent: boolean;
29
+ /** True if the first mount of this component happened during SSR hydration */
30
+ isHydrationMount: boolean;
24
31
  }
25
32
  /**
26
- * Sets up a render registry for the given Nuxt app.
27
- * @param {{ vueApp: import('vue').App }} nuxtApp - The Nuxt app object.
28
- * @param {object} nuxtApp.vueApp - The Vue app instance.
29
- * @returns {object} The render registry object.
33
+ * Sets up a render registry for tracking render-related metrics (e.g. rerenders, render time, etc.)
34
+ * The registry is exposed over the WebSocket channel, and can be accessed from the browser's devtools.
35
+ * @param {object} nuxtApp - The Nuxt app instance.
36
+ * @param {import('vue').App} nuxtApp.vueApp - The Vue app instance used to register lifecycle hooks.
37
+ * @param {object} [options] - Optional configuration object.
38
+ * @param {function(): boolean} [options.isHydrating] - Function to determine if the current render is during SSR hydration.
39
+ * @returns {object} An object containing the render registry's API methods: `getAll()`, `snapshot()`, and `reset()`.
30
40
  */
31
41
  export declare function setupRenderRegistry(nuxtApp: {
32
42
  vueApp: import('vue').App;
43
+ }, options?: {
44
+ isHydrating?: () => boolean;
33
45
  }): {
34
46
  getAll: () => RenderEntry[];
35
47
  snapshot: () => RenderEntry[];
48
+ reset: () => void;
36
49
  };
@@ -1,6 +1,9 @@
1
1
  import { ref } from "vue";
2
- export function setupRenderRegistry(nuxtApp) {
2
+ export function setupRenderRegistry(nuxtApp, options = {}) {
3
3
  const entries = ref(/* @__PURE__ */ new Map());
4
+ const pendingTriggeredRenders = /* @__PURE__ */ new Set();
5
+ const renderStartTimes = /* @__PURE__ */ new Map();
6
+ let preResetUids = /* @__PURE__ */ new Set();
4
7
  function ensureEntry(instance) {
5
8
  const uid = instance.$.uid;
6
9
  if (!entries.value.has(uid)) {
@@ -23,61 +26,90 @@ export function setupRenderRegistry(nuxtApp) {
23
26
  const uid = instance.$.uid;
24
27
  entries.value.delete(uid);
25
28
  }
29
+ function startRenderTimer(uid) {
30
+ if (typeof performance === "undefined") {
31
+ return;
32
+ }
33
+ renderStartTimes.set(uid, performance.now());
34
+ }
35
+ function recordRenderDuration(entry) {
36
+ if (typeof performance === "undefined") {
37
+ return;
38
+ }
39
+ const startedAt = renderStartTimes.get(entry.uid);
40
+ if (startedAt === void 0) {
41
+ return;
42
+ }
43
+ renderStartTimes.delete(entry.uid);
44
+ entry.totalMs += Math.max(performance.now() - startedAt, 0);
45
+ const totalEvents = (entry.rerenders ?? 0) + Math.max(entry.mountCount, 1);
46
+ entry.avgMs = Math.round(entry.totalMs / totalEvents * 10) / 10;
47
+ }
48
+ function reset() {
49
+ preResetUids = new Set(entries.value.keys());
50
+ pendingTriggeredRenders.clear();
51
+ renderStartTimes.clear();
52
+ for (const entry of entries.value.values()) {
53
+ entry.rerenders = 0;
54
+ entry.totalMs = 0;
55
+ entry.avgMs = 0;
56
+ entry.triggers = [];
57
+ }
58
+ }
26
59
  nuxtApp.vueApp.mixin({
60
+ beforeMount() {
61
+ startRenderTimer(this.$.uid);
62
+ },
27
63
  mounted() {
28
64
  const entry = ensureEntry(this);
29
- entry.renders++;
65
+ entry.mountCount++;
66
+ if (preResetUids.has(entry.uid)) {
67
+ entry.isPersistent = true;
68
+ }
69
+ const isHydration = options.isHydrating?.() ?? false;
70
+ if (isHydration && entry.mountCount === 1) {
71
+ entry.isHydrationMount = true;
72
+ } else if (entry.mountCount > 1) {
73
+ entry.rerenders++;
74
+ }
30
75
  syncRect(entry, this);
31
- emit("render:update", { uid: entry.uid, renders: entry.renders });
76
+ recordRenderDuration(entry);
77
+ emit("render:update", { uid: entry.uid, renders: entry.rerenders });
78
+ },
79
+ beforeUpdate() {
80
+ startRenderTimer(this.$.uid);
32
81
  },
33
82
  renderTriggered({ key, type }) {
34
83
  const entry = ensureEntry(this);
35
84
  entry.triggers.push({ key: String(key), type, timestamp: performance.now() });
85
+ pendingTriggeredRenders.add(entry.uid);
36
86
  if (entry.triggers.length > 50) {
37
87
  entry.triggers.shift();
38
88
  }
39
89
  },
40
90
  updated() {
41
91
  const entry = ensureEntry(this);
42
- entry.renders++;
92
+ pendingTriggeredRenders.delete(entry.uid);
93
+ entry.rerenders++;
43
94
  syncRect(entry, this);
44
- emit("render:update", { uid: entry.uid, renders: entry.renders });
95
+ recordRenderDuration(entry);
96
+ emit("render:update", { uid: entry.uid, renders: entry.rerenders });
45
97
  },
46
98
  unmounted() {
99
+ pendingTriggeredRenders.delete(this.$.uid);
100
+ renderStartTimes.delete(this.$.uid);
47
101
  removeEntry(this);
48
102
  emit("render:remove", { uid: this.$.uid });
49
103
  }
50
104
  });
51
- if (import.meta.client && typeof PerformanceObserver !== "undefined") {
52
- const observer = new PerformanceObserver((list) => {
53
- for (const perf of list.getEntries()) {
54
- if (!perf.name.includes("vue-component-render")) {
55
- continue;
56
- }
57
- const uidMatch = perf.name.match(/uid:(\d+)/);
58
- if (!uidMatch) {
59
- continue;
60
- }
61
- const uid = Number(uidMatch[1]);
62
- const entry = entries.value.get(uid);
63
- if (entry) {
64
- entry.totalMs += perf.duration;
65
- entry.avgMs = Math.round(entry.totalMs / entry.renders * 10) / 10;
66
- }
67
- }
68
- });
69
- try {
70
- observer.observe({ entryTypes: ["measure"] });
71
- } catch {
72
- }
73
- }
74
105
  function sanitize(entry) {
75
106
  return {
76
107
  uid: entry.uid,
77
108
  name: entry.name,
78
109
  file: entry.file,
79
110
  element: entry.element,
80
- renders: entry.renders,
111
+ mountCount: entry.mountCount,
112
+ rerenders: entry.rerenders,
81
113
  totalMs: entry.totalMs,
82
114
  avgMs: entry.avgMs,
83
115
  triggers: entry.triggers,
@@ -89,8 +121,9 @@ export function setupRenderRegistry(nuxtApp) {
89
121
  top: entry.rect.top,
90
122
  left: entry.rect.left
91
123
  } : void 0,
92
- children: entry.children,
93
- parentUid: entry.parentUid
124
+ parentUid: entry.parentUid,
125
+ isPersistent: entry.isPersistent,
126
+ isHydrationMount: entry.isHydrationMount
94
127
  };
95
128
  }
96
129
  function getAll() {
@@ -106,7 +139,7 @@ export function setupRenderRegistry(nuxtApp) {
106
139
  const channel = window.__nuxt_devtools__?.channel;
107
140
  channel?.send(event, data);
108
141
  }
109
- return { getAll, snapshot };
142
+ return { getAll, snapshot, reset };
110
143
  }
111
144
  function makeEntry(uid, instance) {
112
145
  const type = instance.$.type;
@@ -120,12 +153,14 @@ function makeEntry(uid, instance) {
120
153
  name: ownLabel ?? inferAnonymousLabel(parentLabel, element) ?? `Component#${uid}`,
121
154
  file,
122
155
  element,
123
- renders: 0,
156
+ mountCount: 0,
157
+ rerenders: 0,
124
158
  totalMs: 0,
125
159
  avgMs: 0,
126
160
  triggers: [],
127
- children: [],
128
- parentUid: instance.$parent?.$.uid
161
+ parentUid: instance.$parent?.$.uid,
162
+ isPersistent: false,
163
+ isHydrationMount: false
129
164
  };
130
165
  }
131
166
  function describeElement(el) {
@@ -18,13 +18,19 @@ export function setupTransitionRegistry() {
18
18
  emit("transition:update", updated);
19
19
  }
20
20
  function sanitize(entry) {
21
- const clone = { ...entry };
22
- for (const key in clone) {
23
- if (typeof clone[key] === "function") {
24
- delete clone[key];
25
- }
26
- }
27
- return clone;
21
+ return {
22
+ id: entry.id,
23
+ transitionName: entry.transitionName,
24
+ parentComponent: entry.parentComponent,
25
+ direction: entry.direction,
26
+ phase: entry.phase,
27
+ startTime: entry.startTime,
28
+ endTime: entry.endTime,
29
+ durationMs: entry.durationMs,
30
+ cancelled: entry.cancelled,
31
+ appear: entry.appear,
32
+ mode: entry.mode
33
+ };
28
34
  }
29
35
  function getAll() {
30
36
  return [...entries.value.values()].map(sanitize);