nuxt-devtools-observatory 0.1.11 → 0.1.13
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.
- package/LICENSE +9 -0
- package/README.md +114 -31
- package/client/dist/assets/index-BFrWlkvI.js +17 -0
- package/client/dist/assets/index-BUQNNbrq.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +59 -15
- package/client/src/views/ComposableTracker.vue +685 -101
- package/client/src/views/ProvideInjectGraph.vue +232 -61
- package/client/src/views/RenderHeatmap.vue +138 -37
- package/client/src/views/TransitionTimeline.vue +2 -2
- package/client/src/views/ValueInspector.vue +124 -0
- package/dist/module.json +1 -1
- package/dist/runtime/composables/composable-registry.d.ts +21 -0
- package/dist/runtime/composables/composable-registry.js +208 -53
- package/dist/runtime/composables/provide-inject-registry.d.ts +13 -1
- package/dist/runtime/composables/provide-inject-registry.js +34 -6
- package/dist/runtime/composables/render-registry.d.ts +18 -8
- package/dist/runtime/composables/render-registry.js +29 -35
- package/dist/runtime/composables/transition-registry.js +13 -7
- package/dist/runtime/plugin.js +49 -17
- package/package.json +7 -3
- package/client/dist/assets/index--Igqz_EM.js +0 -17
- package/client/dist/assets/index-BoC4M4Nb.css +0 -1
|
@@ -1,10 +1,99 @@
|
|
|
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) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const shared = /* @__PURE__ */ new Set();
|
|
16
|
+
for (const [otherId, entry] of entries.value.entries()) {
|
|
17
|
+
if (otherId === id || entry.name !== name) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const otherRaw = rawRefs.get(otherId);
|
|
21
|
+
if (!otherRaw) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
for (const [key, obj] of Object.entries(ownRaw)) {
|
|
25
|
+
if (key in otherRaw && otherRaw[key] === obj) {
|
|
26
|
+
shared.add(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [...shared];
|
|
31
|
+
}
|
|
32
|
+
let currentRoute = "/";
|
|
33
|
+
function setRoute(path) {
|
|
34
|
+
currentRoute = path;
|
|
35
|
+
}
|
|
36
|
+
function getRoute() {
|
|
37
|
+
return currentRoute;
|
|
38
|
+
}
|
|
4
39
|
function register(entry) {
|
|
5
40
|
entries.value.set(entry.id, entry);
|
|
6
41
|
emit("composable:register", entry);
|
|
7
42
|
}
|
|
43
|
+
function registerLiveRefs(id, refs) {
|
|
44
|
+
const prevStop = liveRefWatchers.get(id);
|
|
45
|
+
if (prevStop) {
|
|
46
|
+
prevStop();
|
|
47
|
+
}
|
|
48
|
+
liveRefWatchers.delete(id);
|
|
49
|
+
if (Object.keys(refs).length === 0) {
|
|
50
|
+
liveRefs.delete(id);
|
|
51
|
+
rawRefs.delete(id);
|
|
52
|
+
prevValues.delete(id);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
liveRefs.set(id, refs);
|
|
56
|
+
prevValues.set(
|
|
57
|
+
id,
|
|
58
|
+
Object.fromEntries(
|
|
59
|
+
Object.entries(refs).map(([k, r]) => {
|
|
60
|
+
try {
|
|
61
|
+
return [k, JSON.stringify(unref(r)) ?? ""];
|
|
62
|
+
} catch {
|
|
63
|
+
return [k, ""];
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
const stop = watchEffect(() => {
|
|
69
|
+
const prev = prevValues.get(id) ?? {};
|
|
70
|
+
const now = {};
|
|
71
|
+
const t = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
72
|
+
for (const [k, r] of Object.entries(refs)) {
|
|
73
|
+
const val = unref(r);
|
|
74
|
+
const serialised = JSON.stringify(val) ?? "";
|
|
75
|
+
now[k] = serialised;
|
|
76
|
+
if (serialised !== prev[k]) {
|
|
77
|
+
const history = entryHistory.get(id) ?? [];
|
|
78
|
+
history.push({ t, key: k, value: safeValue(val) });
|
|
79
|
+
if (history.length > MAX_HISTORY) {
|
|
80
|
+
history.shift();
|
|
81
|
+
}
|
|
82
|
+
entryHistory.set(id, history);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
prevValues.set(id, now);
|
|
86
|
+
_onChange?.();
|
|
87
|
+
});
|
|
88
|
+
liveRefWatchers.set(id, stop);
|
|
89
|
+
}
|
|
90
|
+
function registerRawRefs(id, refs) {
|
|
91
|
+
rawRefs.set(id, refs);
|
|
92
|
+
}
|
|
93
|
+
let _onChange = null;
|
|
94
|
+
function onComposableChange(cb) {
|
|
95
|
+
_onChange = cb;
|
|
96
|
+
}
|
|
8
97
|
function update(id, patch) {
|
|
9
98
|
const existing = entries.value.get(id);
|
|
10
99
|
if (!existing) {
|
|
@@ -31,6 +120,25 @@ export function setupComposableRegistry() {
|
|
|
31
120
|
return val;
|
|
32
121
|
}
|
|
33
122
|
function sanitize(entry) {
|
|
123
|
+
const live = liveRefs.get(entry.id);
|
|
124
|
+
const hasLive = live != null && Object.keys(live).length > 0;
|
|
125
|
+
const freshRefs = hasLive ? Object.fromEntries(
|
|
126
|
+
Object.entries(live).map(([k, r]) => [
|
|
127
|
+
k,
|
|
128
|
+
{
|
|
129
|
+
type: entry.refs[k]?.type ?? "ref",
|
|
130
|
+
value: safeValue(unref(r))
|
|
131
|
+
}
|
|
132
|
+
])
|
|
133
|
+
) : Object.fromEntries(
|
|
134
|
+
Object.entries(entry.refs).map(([k, v]) => [
|
|
135
|
+
k,
|
|
136
|
+
{
|
|
137
|
+
type: v.type,
|
|
138
|
+
value: safeValue(typeof v.value === "object" && v.value !== null && "value" in v.value ? v.value.value : v.value)
|
|
139
|
+
}
|
|
140
|
+
])
|
|
141
|
+
);
|
|
34
142
|
return {
|
|
35
143
|
id: entry.id,
|
|
36
144
|
name: entry.name,
|
|
@@ -39,20 +147,15 @@ export function setupComposableRegistry() {
|
|
|
39
147
|
status: entry.status,
|
|
40
148
|
leak: entry.leak,
|
|
41
149
|
leakReason: entry.leakReason,
|
|
42
|
-
refs:
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
),
|
|
150
|
+
refs: freshRefs,
|
|
151
|
+
history: entryHistory.get(entry.id) ?? [],
|
|
152
|
+
sharedKeys: computeSharedKeys(entry.id, entry.name),
|
|
51
153
|
watcherCount: entry.watcherCount,
|
|
52
154
|
intervalCount: entry.intervalCount,
|
|
53
155
|
lifecycle: entry.lifecycle,
|
|
54
156
|
file: entry.file,
|
|
55
|
-
line: entry.line
|
|
157
|
+
line: entry.line,
|
|
158
|
+
route: entry.route
|
|
56
159
|
};
|
|
57
160
|
}
|
|
58
161
|
function getAll() {
|
|
@@ -65,7 +168,35 @@ export function setupComposableRegistry() {
|
|
|
65
168
|
const channel = window.__nuxt_devtools__?.channel;
|
|
66
169
|
channel?.send(event, data);
|
|
67
170
|
}
|
|
68
|
-
|
|
171
|
+
function clear() {
|
|
172
|
+
for (const stop of liveRefWatchers.values()) stop();
|
|
173
|
+
liveRefWatchers.clear();
|
|
174
|
+
liveRefs.clear();
|
|
175
|
+
rawRefs.clear();
|
|
176
|
+
prevValues.clear();
|
|
177
|
+
entryHistory.clear();
|
|
178
|
+
entries.value.clear();
|
|
179
|
+
emit("composable:clear", {});
|
|
180
|
+
}
|
|
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 };
|
|
69
200
|
}
|
|
70
201
|
export function __trackComposable(name, callFn, meta) {
|
|
71
202
|
if (!import.meta.dev) {
|
|
@@ -79,40 +210,55 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
79
210
|
return callFn();
|
|
80
211
|
}
|
|
81
212
|
const instance = getCurrentInstance();
|
|
82
|
-
const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}`;
|
|
213
|
+
const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}::${Math.random().toString(36).slice(2, 7)}`;
|
|
83
214
|
const trackedIntervals = [];
|
|
84
|
-
const originalSetInterval = window.setInterval;
|
|
85
|
-
const originalClearInterval = window.clearInterval;
|
|
86
215
|
const clearedIntervals = /* @__PURE__ */ new Set();
|
|
87
|
-
window.setInterval
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
216
|
+
const alreadyPatched = !!window.setInterval.__obs;
|
|
217
|
+
let originalSetInterval = null;
|
|
218
|
+
let originalClearInterval = null;
|
|
219
|
+
if (!alreadyPatched) {
|
|
220
|
+
originalSetInterval = window.setInterval;
|
|
221
|
+
originalClearInterval = window.clearInterval;
|
|
222
|
+
const patchedSetInterval = ((fn, ms, ...rest) => {
|
|
223
|
+
const timerId = originalSetInterval(fn, ms, ...rest);
|
|
224
|
+
trackedIntervals.push(timerId);
|
|
225
|
+
return timerId;
|
|
226
|
+
});
|
|
227
|
+
patchedSetInterval.__obs = true;
|
|
228
|
+
window.setInterval = patchedSetInterval;
|
|
229
|
+
window.clearInterval = ((timerId) => {
|
|
230
|
+
if (timerId !== void 0) {
|
|
231
|
+
clearedIntervals.add(timerId);
|
|
232
|
+
}
|
|
233
|
+
originalClearInterval(timerId);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
98
236
|
const effectsBefore = new Set(instance?.scope?.effects ?? []);
|
|
99
237
|
const mountedHooksBefore = instance?.bm?.length ?? 0;
|
|
100
238
|
const unmountedHooksBefore = instance?.um?.length ?? 0;
|
|
101
239
|
const result = callFn();
|
|
102
|
-
|
|
103
|
-
|
|
240
|
+
if (!alreadyPatched && originalSetInterval) {
|
|
241
|
+
window.setInterval = originalSetInterval;
|
|
242
|
+
window.clearInterval = originalClearInterval;
|
|
243
|
+
}
|
|
104
244
|
const trackedWatchers = (instance?.scope?.effects ?? []).filter((effect) => !effectsBefore.has(effect)).map((effect) => ({
|
|
105
245
|
effect,
|
|
106
246
|
stop: () => effect.stop?.()
|
|
107
247
|
}));
|
|
108
248
|
const refs = {};
|
|
249
|
+
const liveRefMap = {};
|
|
250
|
+
const rawRefMap = {};
|
|
109
251
|
if (result && typeof result === "object") {
|
|
110
252
|
for (const [key, val] of Object.entries(result)) {
|
|
111
253
|
if (isRef(val)) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
254
|
+
const type = isReadonly(val) ? "computed" : "ref";
|
|
255
|
+
refs[key] = { type, value: safeSnapshot(unref(val)) };
|
|
256
|
+
liveRefMap[key] = val;
|
|
257
|
+
rawRefMap[key] = val;
|
|
258
|
+
} else if (isReactive(val)) {
|
|
259
|
+
refs[key] = { type: "reactive", value: safeSnapshot(val) };
|
|
260
|
+
liveRefMap[key] = computed(() => val);
|
|
261
|
+
rawRefMap[key] = val;
|
|
116
262
|
}
|
|
117
263
|
}
|
|
118
264
|
}
|
|
@@ -124,6 +270,9 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
124
270
|
status: "mounted",
|
|
125
271
|
leak: false,
|
|
126
272
|
refs,
|
|
273
|
+
history: [],
|
|
274
|
+
sharedKeys: [],
|
|
275
|
+
// computed lazily in sanitize() after all instances are registered
|
|
127
276
|
watcherCount: trackedWatchers.length,
|
|
128
277
|
intervalCount: trackedIntervals.length,
|
|
129
278
|
lifecycle: {
|
|
@@ -133,31 +282,37 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
133
282
|
intervalsCleaned: true
|
|
134
283
|
},
|
|
135
284
|
file: meta.file,
|
|
136
|
-
line: meta.line
|
|
285
|
+
line: meta.line,
|
|
286
|
+
route: registry.getRoute()
|
|
137
287
|
};
|
|
138
288
|
registry.register(entry);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
289
|
+
registry.registerLiveRefs(id, liveRefMap);
|
|
290
|
+
registry.registerRawRefs(id, rawRefMap);
|
|
291
|
+
if (instance) {
|
|
292
|
+
onUnmounted(() => {
|
|
293
|
+
const leakedWatchers = trackedWatchers.filter((w) => w.effect.active);
|
|
294
|
+
const leakedIntervals = trackedIntervals.filter((timerId) => !clearedIntervals.has(timerId));
|
|
295
|
+
const leak = leakedWatchers.length > 0 || leakedIntervals.length > 0;
|
|
296
|
+
const reasons = [];
|
|
297
|
+
if (leakedWatchers.length > 0) {
|
|
298
|
+
reasons.push(`${leakedWatchers.length} watcher${leakedWatchers.length > 1 ? "s" : ""} still active after unmount`);
|
|
158
299
|
}
|
|
300
|
+
if (leakedIntervals.length > 0) {
|
|
301
|
+
reasons.push(`setInterval #${leakedIntervals.join(", #")} never cleared`);
|
|
302
|
+
}
|
|
303
|
+
registry.update(id, {
|
|
304
|
+
status: "unmounted",
|
|
305
|
+
leak,
|
|
306
|
+
leakReason: reasons.join(" \xB7 ") || void 0,
|
|
307
|
+
lifecycle: {
|
|
308
|
+
...entry.lifecycle,
|
|
309
|
+
watchersCleaned: leakedWatchers.length === 0,
|
|
310
|
+
intervalsCleaned: leakedIntervals.length === 0
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
registry.registerLiveRefs(id, {});
|
|
159
314
|
});
|
|
160
|
-
}
|
|
315
|
+
}
|
|
161
316
|
return result;
|
|
162
317
|
}
|
|
163
318
|
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 `
|
|
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.
|
|
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.
|
|
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:
|
|
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,8 +3,11 @@ export interface RenderEntry {
|
|
|
3
3
|
name: string;
|
|
4
4
|
file: string;
|
|
5
5
|
element?: string;
|
|
6
|
-
|
|
7
|
-
|
|
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 */
|
|
8
11
|
totalMs: number;
|
|
9
12
|
avgMs: number;
|
|
10
13
|
triggers: Array<{
|
|
@@ -20,20 +23,27 @@ export interface RenderEntry {
|
|
|
20
23
|
top: number;
|
|
21
24
|
left: number;
|
|
22
25
|
};
|
|
23
|
-
children: number[];
|
|
24
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;
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
|
-
* Sets up a render registry for
|
|
28
|
-
*
|
|
29
|
-
* @param {object} nuxtApp
|
|
30
|
-
* @
|
|
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()`.
|
|
31
40
|
*/
|
|
32
41
|
export declare function setupRenderRegistry(nuxtApp: {
|
|
33
42
|
vueApp: import('vue').App;
|
|
43
|
+
}, options?: {
|
|
44
|
+
isHydrating?: () => boolean;
|
|
34
45
|
}): {
|
|
35
46
|
getAll: () => RenderEntry[];
|
|
36
47
|
snapshot: () => RenderEntry[];
|
|
37
|
-
markNavigation: () => void;
|
|
38
48
|
reset: () => void;
|
|
39
49
|
};
|
|
@@ -1,9 +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
4
|
const pendingTriggeredRenders = /* @__PURE__ */ new Set();
|
|
5
5
|
const renderStartTimes = /* @__PURE__ */ new Map();
|
|
6
|
-
let
|
|
6
|
+
let preResetUids = /* @__PURE__ */ new Set();
|
|
7
7
|
function ensureEntry(instance) {
|
|
8
8
|
const uid = instance.$.uid;
|
|
9
9
|
if (!entries.value.has(uid)) {
|
|
@@ -26,9 +26,6 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
26
26
|
const uid = instance.$.uid;
|
|
27
27
|
entries.value.delete(uid);
|
|
28
28
|
}
|
|
29
|
-
function isNavigationRender() {
|
|
30
|
-
return typeof performance !== "undefined" && performance.now() <= navigationWindowUntil;
|
|
31
|
-
}
|
|
32
29
|
function startRenderTimer(uid) {
|
|
33
30
|
if (typeof performance === "undefined") {
|
|
34
31
|
return;
|
|
@@ -45,20 +42,15 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
45
42
|
}
|
|
46
43
|
renderStartTimes.delete(entry.uid);
|
|
47
44
|
entry.totalMs += Math.max(performance.now() - startedAt, 0);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function markNavigation() {
|
|
51
|
-
if (typeof performance === "undefined") {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
navigationWindowUntil = performance.now() + 800;
|
|
45
|
+
const totalEvents = (entry.rerenders ?? 0) + Math.max(entry.mountCount, 1);
|
|
46
|
+
entry.avgMs = Math.round(entry.totalMs / totalEvents * 10) / 10;
|
|
55
47
|
}
|
|
56
48
|
function reset() {
|
|
49
|
+
preResetUids = new Set(entries.value.keys());
|
|
57
50
|
pendingTriggeredRenders.clear();
|
|
58
51
|
renderStartTimes.clear();
|
|
59
52
|
for (const entry of entries.value.values()) {
|
|
60
|
-
entry.
|
|
61
|
-
entry.navigationRenders = 0;
|
|
53
|
+
entry.rerenders = 0;
|
|
62
54
|
entry.totalMs = 0;
|
|
63
55
|
entry.avgMs = 0;
|
|
64
56
|
entry.triggers = [];
|
|
@@ -70,13 +62,19 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
70
62
|
},
|
|
71
63
|
mounted() {
|
|
72
64
|
const entry = ensureEntry(this);
|
|
73
|
-
entry.
|
|
74
|
-
if (
|
|
75
|
-
entry.
|
|
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++;
|
|
76
74
|
}
|
|
77
75
|
syncRect(entry, this);
|
|
78
76
|
recordRenderDuration(entry);
|
|
79
|
-
emit("render:update", { uid: entry.uid, renders: entry.
|
|
77
|
+
emit("render:update", { uid: entry.uid, renders: entry.rerenders });
|
|
80
78
|
},
|
|
81
79
|
beforeUpdate() {
|
|
82
80
|
startRenderTimer(this.$.uid);
|
|
@@ -91,17 +89,11 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
91
89
|
},
|
|
92
90
|
updated() {
|
|
93
91
|
const entry = ensureEntry(this);
|
|
94
|
-
if (!pendingTriggeredRenders.has(entry.uid)) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
92
|
pendingTriggeredRenders.delete(entry.uid);
|
|
98
|
-
entry.
|
|
99
|
-
if (isNavigationRender()) {
|
|
100
|
-
entry.navigationRenders++;
|
|
101
|
-
}
|
|
93
|
+
entry.rerenders++;
|
|
102
94
|
syncRect(entry, this);
|
|
103
95
|
recordRenderDuration(entry);
|
|
104
|
-
emit("render:update", { uid: entry.uid, renders: entry.
|
|
96
|
+
emit("render:update", { uid: entry.uid, renders: entry.rerenders });
|
|
105
97
|
},
|
|
106
98
|
unmounted() {
|
|
107
99
|
pendingTriggeredRenders.delete(this.$.uid);
|
|
@@ -116,8 +108,8 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
116
108
|
name: entry.name,
|
|
117
109
|
file: entry.file,
|
|
118
110
|
element: entry.element,
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
mountCount: entry.mountCount,
|
|
112
|
+
rerenders: entry.rerenders,
|
|
121
113
|
totalMs: entry.totalMs,
|
|
122
114
|
avgMs: entry.avgMs,
|
|
123
115
|
triggers: entry.triggers,
|
|
@@ -129,8 +121,9 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
129
121
|
top: entry.rect.top,
|
|
130
122
|
left: entry.rect.left
|
|
131
123
|
} : void 0,
|
|
132
|
-
|
|
133
|
-
|
|
124
|
+
parentUid: entry.parentUid,
|
|
125
|
+
isPersistent: entry.isPersistent,
|
|
126
|
+
isHydrationMount: entry.isHydrationMount
|
|
134
127
|
};
|
|
135
128
|
}
|
|
136
129
|
function getAll() {
|
|
@@ -146,7 +139,7 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
146
139
|
const channel = window.__nuxt_devtools__?.channel;
|
|
147
140
|
channel?.send(event, data);
|
|
148
141
|
}
|
|
149
|
-
return { getAll, snapshot,
|
|
142
|
+
return { getAll, snapshot, reset };
|
|
150
143
|
}
|
|
151
144
|
function makeEntry(uid, instance) {
|
|
152
145
|
const type = instance.$.type;
|
|
@@ -160,13 +153,14 @@ function makeEntry(uid, instance) {
|
|
|
160
153
|
name: ownLabel ?? inferAnonymousLabel(parentLabel, element) ?? `Component#${uid}`,
|
|
161
154
|
file,
|
|
162
155
|
element,
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
mountCount: 0,
|
|
157
|
+
rerenders: 0,
|
|
165
158
|
totalMs: 0,
|
|
166
159
|
avgMs: 0,
|
|
167
160
|
triggers: [],
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
parentUid: instance.$parent?.$.uid,
|
|
162
|
+
isPersistent: false,
|
|
163
|
+
isHydrationMount: false
|
|
170
164
|
};
|
|
171
165
|
}
|
|
172
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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);
|