nuxt-devtools-observatory 0.1.0
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/README.md +209 -0
- package/client/dist/assets/index-C76d764s.js +17 -0
- package/client/dist/assets/index-yIuOV1_N.css +1 -0
- package/client/dist/index.html +47 -0
- package/client/index.html +46 -0
- package/client/src/App.vue +114 -0
- package/client/src/main.ts +5 -0
- package/client/src/stores/observatory.ts +65 -0
- package/client/src/style.css +261 -0
- package/client/src/views/ComposableTracker.vue +347 -0
- package/client/src/views/FetchDashboard.vue +492 -0
- package/client/src/views/ProvideInjectGraph.vue +481 -0
- package/client/src/views/RenderHeatmap.vue +492 -0
- package/client/src/views/TransitionTimeline.vue +527 -0
- package/client/tsconfig.json +16 -0
- package/client/vite.config.ts +12 -0
- package/dist/module.d.mts +38 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +562 -0
- package/dist/runtime/composables/composable-registry.d.ts +40 -0
- package/dist/runtime/composables/composable-registry.js +135 -0
- package/dist/runtime/composables/fetch-registry.d.ts +63 -0
- package/dist/runtime/composables/fetch-registry.js +83 -0
- package/dist/runtime/composables/provide-inject-registry.d.ts +57 -0
- package/dist/runtime/composables/provide-inject-registry.js +96 -0
- package/dist/runtime/composables/render-registry.d.ts +36 -0
- package/dist/runtime/composables/render-registry.js +85 -0
- package/dist/runtime/composables/transition-registry.d.ts +21 -0
- package/dist/runtime/composables/transition-registry.js +125 -0
- package/dist/runtime/plugin.d.ts +2 -0
- package/dist/runtime/plugin.js +66 -0
- package/dist/types.d.mts +3 -0
- package/package.json +89 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ref, isRef, isReadonly, unref, getCurrentInstance, onUnmounted } from "vue";
|
|
2
|
+
export function setupComposableRegistry() {
|
|
3
|
+
const entries = ref(/* @__PURE__ */ new Map());
|
|
4
|
+
function register(entry) {
|
|
5
|
+
entries.value.set(entry.id, entry);
|
|
6
|
+
emit("composable:register", entry);
|
|
7
|
+
}
|
|
8
|
+
function update(id, patch) {
|
|
9
|
+
const existing = entries.value.get(id);
|
|
10
|
+
if (!existing) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const updated = { ...existing, ...patch };
|
|
14
|
+
entries.value.set(id, updated);
|
|
15
|
+
emit("composable:update", updated);
|
|
16
|
+
}
|
|
17
|
+
function getAll() {
|
|
18
|
+
return [...entries.value.values()];
|
|
19
|
+
}
|
|
20
|
+
function emit(event, data) {
|
|
21
|
+
if (!import.meta.client) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const channel = window.__nuxt_devtools__?.channel;
|
|
25
|
+
channel?.send(event, data);
|
|
26
|
+
}
|
|
27
|
+
return { register, update, getAll };
|
|
28
|
+
}
|
|
29
|
+
export function __trackComposable(name, callFn, meta) {
|
|
30
|
+
if (!import.meta.dev) {
|
|
31
|
+
return callFn();
|
|
32
|
+
}
|
|
33
|
+
if (!import.meta.client) {
|
|
34
|
+
return callFn();
|
|
35
|
+
}
|
|
36
|
+
const registry = window.__observatory__?.composable;
|
|
37
|
+
if (!registry) {
|
|
38
|
+
return callFn();
|
|
39
|
+
}
|
|
40
|
+
const instance = getCurrentInstance();
|
|
41
|
+
const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}`;
|
|
42
|
+
const trackedIntervals = [];
|
|
43
|
+
const originalSetInterval = window.setInterval;
|
|
44
|
+
const originalClearInterval = window.clearInterval;
|
|
45
|
+
const clearedIntervals = /* @__PURE__ */ new Set();
|
|
46
|
+
window.setInterval = ((fn, ms, ...rest) => {
|
|
47
|
+
const id2 = originalSetInterval(fn, ms, ...rest);
|
|
48
|
+
trackedIntervals.push(id2);
|
|
49
|
+
return id2;
|
|
50
|
+
});
|
|
51
|
+
window.clearInterval = ((id2) => {
|
|
52
|
+
if (id2 !== void 0) {
|
|
53
|
+
clearedIntervals.add(id2);
|
|
54
|
+
}
|
|
55
|
+
originalClearInterval(id2);
|
|
56
|
+
});
|
|
57
|
+
const trackedWatchers = [];
|
|
58
|
+
const result = callFn();
|
|
59
|
+
window.setInterval = originalSetInterval;
|
|
60
|
+
window.clearInterval = originalClearInterval;
|
|
61
|
+
const refs = {};
|
|
62
|
+
if (result && typeof result === "object") {
|
|
63
|
+
for (const [key, val] of Object.entries(result)) {
|
|
64
|
+
if (isRef(val)) {
|
|
65
|
+
refs[key] = {
|
|
66
|
+
type: isReadonly(val) ? "computed" : "ref",
|
|
67
|
+
value: safeSnapshot(unref(val))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const entry = {
|
|
73
|
+
id,
|
|
74
|
+
name,
|
|
75
|
+
componentFile: meta.file,
|
|
76
|
+
componentUid: instance?.uid ?? -1,
|
|
77
|
+
status: "mounted",
|
|
78
|
+
leak: false,
|
|
79
|
+
refs,
|
|
80
|
+
watcherCount: trackedWatchers.length,
|
|
81
|
+
intervalCount: trackedIntervals.length,
|
|
82
|
+
lifecycle: {
|
|
83
|
+
hasOnMounted: false,
|
|
84
|
+
hasOnUnmounted: false,
|
|
85
|
+
watchersCleaned: true,
|
|
86
|
+
intervalsCleaned: true
|
|
87
|
+
},
|
|
88
|
+
file: meta.file,
|
|
89
|
+
line: meta.line
|
|
90
|
+
};
|
|
91
|
+
registry.register(entry);
|
|
92
|
+
onUnmounted(() => {
|
|
93
|
+
const leakedWatchers = trackedWatchers.filter((w) => !w.stopped);
|
|
94
|
+
const leakedIntervals = trackedIntervals.filter((id2) => !clearedIntervals.has(id2));
|
|
95
|
+
const leak = leakedWatchers.length > 0 || leakedIntervals.length > 0;
|
|
96
|
+
const reasons = [];
|
|
97
|
+
if (leakedWatchers.length > 0) {
|
|
98
|
+
reasons.push(`${leakedWatchers.length} watcher${leakedWatchers.length > 1 ? "s" : ""} still active after unmount`);
|
|
99
|
+
}
|
|
100
|
+
if (leakedIntervals.length > 0) {
|
|
101
|
+
reasons.push(`setInterval #${leakedIntervals.join(", #")} never cleared`);
|
|
102
|
+
}
|
|
103
|
+
registry.update(id, {
|
|
104
|
+
status: "unmounted",
|
|
105
|
+
leak,
|
|
106
|
+
leakReason: reasons.join(" \xB7 ") || void 0,
|
|
107
|
+
lifecycle: {
|
|
108
|
+
...entry.lifecycle,
|
|
109
|
+
watchersCleaned: leakedWatchers.length === 0,
|
|
110
|
+
intervalsCleaned: leakedIntervals.length === 0
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function safeSnapshot(value) {
|
|
117
|
+
try {
|
|
118
|
+
if (value === null || value === void 0) {
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
if (typeof value === "function") {
|
|
122
|
+
return "[Function]";
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
return `Array(${value.length})`;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === "object") {
|
|
128
|
+
const str = JSON.stringify(value);
|
|
129
|
+
return str.length > 120 ? str.slice(0, 120) + "\u2026" : JSON.parse(str);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
} catch {
|
|
133
|
+
return "[unserializable]";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface FetchEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
key: string;
|
|
4
|
+
url: string;
|
|
5
|
+
status: 'pending' | 'ok' | 'error' | 'cached';
|
|
6
|
+
origin: 'ssr' | 'csr';
|
|
7
|
+
startTime: number;
|
|
8
|
+
endTime?: number;
|
|
9
|
+
ms?: number;
|
|
10
|
+
size?: number;
|
|
11
|
+
cached: boolean;
|
|
12
|
+
payload?: unknown;
|
|
13
|
+
error?: unknown;
|
|
14
|
+
file?: string;
|
|
15
|
+
line?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sets up the fetch registry, which tracks all fetch requests and their
|
|
19
|
+
* associated metadata (e.g. duration, size, origin).
|
|
20
|
+
* @returns {object} The fetch registry with `register`, `update`, `getAll`, `clear`, and `entries` members.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setupFetchRegistry(): {
|
|
23
|
+
register: (entry: FetchEntry) => void;
|
|
24
|
+
update: (id: string, patch: Partial<FetchEntry>) => void;
|
|
25
|
+
getAll: () => FetchEntry[];
|
|
26
|
+
clear: () => void;
|
|
27
|
+
entries: Readonly<import("vue").Ref<ReadonlyMap<string, {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly url: string;
|
|
31
|
+
readonly status: "pending" | "ok" | "error" | "cached";
|
|
32
|
+
readonly origin: "ssr" | "csr";
|
|
33
|
+
readonly startTime: number;
|
|
34
|
+
readonly endTime?: number | undefined;
|
|
35
|
+
readonly ms?: number | undefined;
|
|
36
|
+
readonly size?: number | undefined;
|
|
37
|
+
readonly cached: boolean;
|
|
38
|
+
readonly payload?: Readonly<unknown> | undefined;
|
|
39
|
+
readonly error?: Readonly<unknown> | undefined;
|
|
40
|
+
readonly file?: string | undefined;
|
|
41
|
+
readonly line?: number | undefined;
|
|
42
|
+
}>, ReadonlyMap<string, {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
readonly key: string;
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly status: "pending" | "ok" | "error" | "cached";
|
|
47
|
+
readonly origin: "ssr" | "csr";
|
|
48
|
+
readonly startTime: number;
|
|
49
|
+
readonly endTime?: number | undefined;
|
|
50
|
+
readonly ms?: number | undefined;
|
|
51
|
+
readonly size?: number | undefined;
|
|
52
|
+
readonly cached: boolean;
|
|
53
|
+
readonly payload?: Readonly<unknown> | undefined;
|
|
54
|
+
readonly error?: Readonly<unknown> | undefined;
|
|
55
|
+
readonly file?: string | undefined;
|
|
56
|
+
readonly line?: number | undefined;
|
|
57
|
+
}>>>;
|
|
58
|
+
};
|
|
59
|
+
export declare function __devFetch(originalFn: (url: string, opts: Record<string, unknown>) => Promise<unknown>, url: string, opts: Record<string, unknown>, meta: {
|
|
60
|
+
key: string;
|
|
61
|
+
file: string;
|
|
62
|
+
line: number;
|
|
63
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ref, readonly } from "vue";
|
|
2
|
+
export function setupFetchRegistry() {
|
|
3
|
+
const entries = ref(/* @__PURE__ */ new Map());
|
|
4
|
+
function register(entry) {
|
|
5
|
+
entries.value.set(entry.id, entry);
|
|
6
|
+
emit("fetch:start", entry);
|
|
7
|
+
}
|
|
8
|
+
function update(id, patch) {
|
|
9
|
+
const existing = entries.value.get(id);
|
|
10
|
+
if (!existing) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const updated = { ...existing, ...patch };
|
|
14
|
+
entries.value.set(id, updated);
|
|
15
|
+
emit("fetch:update", updated);
|
|
16
|
+
}
|
|
17
|
+
function getAll() {
|
|
18
|
+
return [...entries.value.values()];
|
|
19
|
+
}
|
|
20
|
+
function clear() {
|
|
21
|
+
entries.value.clear();
|
|
22
|
+
emit("fetch:clear", {});
|
|
23
|
+
}
|
|
24
|
+
function emit(event, data) {
|
|
25
|
+
if (!import.meta.client) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const channel = window.__nuxt_devtools__?.channel;
|
|
29
|
+
channel?.send(event, data);
|
|
30
|
+
}
|
|
31
|
+
return { register, update, getAll, clear, entries: readonly(entries) };
|
|
32
|
+
}
|
|
33
|
+
export function __devFetch(originalFn, url, opts, meta) {
|
|
34
|
+
if (!import.meta.dev || !import.meta.client) {
|
|
35
|
+
return originalFn(url, opts);
|
|
36
|
+
}
|
|
37
|
+
const registry = window.__observatory__?.fetch;
|
|
38
|
+
if (!registry) {
|
|
39
|
+
return originalFn(url, opts);
|
|
40
|
+
}
|
|
41
|
+
const id = `${meta.key}::${Date.now()}`;
|
|
42
|
+
const startTime = performance.now();
|
|
43
|
+
const origin = import.meta.server ? "ssr" : "csr";
|
|
44
|
+
registry.register({
|
|
45
|
+
id,
|
|
46
|
+
key: meta.key,
|
|
47
|
+
url: typeof url === "string" ? url : String(url),
|
|
48
|
+
status: "pending",
|
|
49
|
+
origin,
|
|
50
|
+
startTime,
|
|
51
|
+
cached: false,
|
|
52
|
+
file: meta.file,
|
|
53
|
+
line: meta.line
|
|
54
|
+
});
|
|
55
|
+
return originalFn(url, {
|
|
56
|
+
...opts,
|
|
57
|
+
onResponse({ response }) {
|
|
58
|
+
const ms = Math.round(performance.now() - startTime);
|
|
59
|
+
const size = Number(response.headers?.get("content-length")) || void 0;
|
|
60
|
+
const cached = response.headers?.get("x-nuxt-cache") === "HIT";
|
|
61
|
+
registry.update(id, {
|
|
62
|
+
status: response.ok ? "ok" : "error",
|
|
63
|
+
endTime: performance.now(),
|
|
64
|
+
ms,
|
|
65
|
+
size,
|
|
66
|
+
cached
|
|
67
|
+
});
|
|
68
|
+
if (typeof opts.onResponse === "function") {
|
|
69
|
+
opts.onResponse({ response });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
onResponseError({ response }) {
|
|
73
|
+
registry.update(id, {
|
|
74
|
+
status: "error",
|
|
75
|
+
endTime: performance.now(),
|
|
76
|
+
ms: Math.round(performance.now() - startTime)
|
|
77
|
+
});
|
|
78
|
+
if (typeof opts.onResponseError === "function") {
|
|
79
|
+
opts.onResponseError({ response });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface ProvideEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
componentName: string;
|
|
4
|
+
componentFile: string;
|
|
5
|
+
componentUid: number;
|
|
6
|
+
isReactive: boolean;
|
|
7
|
+
valueSnapshot: unknown;
|
|
8
|
+
line: number;
|
|
9
|
+
}
|
|
10
|
+
export interface InjectEntry {
|
|
11
|
+
key: string;
|
|
12
|
+
componentName: string;
|
|
13
|
+
componentFile: string;
|
|
14
|
+
componentUid: number;
|
|
15
|
+
resolved: boolean;
|
|
16
|
+
resolvedFromFile?: string;
|
|
17
|
+
resolvedFromUid?: number;
|
|
18
|
+
line: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sets up the provide/inject registry, which tracks all provide/inject calls
|
|
22
|
+
* and their associated metadata (e.g. component name, file, line).
|
|
23
|
+
* @returns {object} The provide/inject registry with `registerProvide`, `registerInject`, and `getAll` members.
|
|
24
|
+
*/
|
|
25
|
+
export declare function setupProvideInjectRegistry(): {
|
|
26
|
+
registerProvide: (entry: ProvideEntry) => void;
|
|
27
|
+
registerInject: (entry: InjectEntry) => void;
|
|
28
|
+
getAll: () => {
|
|
29
|
+
provides: {
|
|
30
|
+
key: string;
|
|
31
|
+
componentName: string;
|
|
32
|
+
componentFile: string;
|
|
33
|
+
componentUid: number;
|
|
34
|
+
isReactive: boolean;
|
|
35
|
+
valueSnapshot: unknown;
|
|
36
|
+
line: number;
|
|
37
|
+
}[];
|
|
38
|
+
injects: {
|
|
39
|
+
key: string;
|
|
40
|
+
componentName: string;
|
|
41
|
+
componentFile: string;
|
|
42
|
+
componentUid: number;
|
|
43
|
+
resolved: boolean;
|
|
44
|
+
resolvedFromFile?: string | undefined;
|
|
45
|
+
resolvedFromUid?: number | undefined;
|
|
46
|
+
line: number;
|
|
47
|
+
}[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export declare function __devProvide(key: string | symbol, value: unknown, meta: {
|
|
51
|
+
file: string;
|
|
52
|
+
line: number;
|
|
53
|
+
}): void;
|
|
54
|
+
export declare function __devInject<T>(key: string | symbol, defaultValue: T | undefined, meta: {
|
|
55
|
+
file: string;
|
|
56
|
+
line: number;
|
|
57
|
+
}): T | undefined;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ref, isRef, isReactive, unref, getCurrentInstance, provide, inject } from "vue";
|
|
2
|
+
export function setupProvideInjectRegistry() {
|
|
3
|
+
const provides = ref([]);
|
|
4
|
+
const injects = ref([]);
|
|
5
|
+
function registerProvide(entry) {
|
|
6
|
+
provides.value.push(entry);
|
|
7
|
+
emit("provide:register", entry);
|
|
8
|
+
}
|
|
9
|
+
function registerInject(entry) {
|
|
10
|
+
injects.value.push(entry);
|
|
11
|
+
emit("inject:register", entry);
|
|
12
|
+
}
|
|
13
|
+
function getAll() {
|
|
14
|
+
return { provides: provides.value, injects: injects.value };
|
|
15
|
+
}
|
|
16
|
+
function emit(event, data) {
|
|
17
|
+
if (!import.meta.client) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const channel = window.__nuxt_devtools__?.channel;
|
|
21
|
+
channel?.send(event, data);
|
|
22
|
+
}
|
|
23
|
+
return { registerProvide, registerInject, getAll };
|
|
24
|
+
}
|
|
25
|
+
export function __devProvide(key, value, meta) {
|
|
26
|
+
provide(key, value);
|
|
27
|
+
if (!import.meta.dev || !import.meta.client) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const registry = window.__observatory__?.provideInject;
|
|
31
|
+
if (!registry) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const instance = getCurrentInstance();
|
|
35
|
+
registry.registerProvide({
|
|
36
|
+
key: String(key),
|
|
37
|
+
componentName: instance?.type?.__name ?? "unknown",
|
|
38
|
+
componentFile: meta.file,
|
|
39
|
+
componentUid: instance?.uid ?? -1,
|
|
40
|
+
isReactive: isRef(value) || isReactive(value),
|
|
41
|
+
valueSnapshot: safeSnapshot(unref(value)),
|
|
42
|
+
line: meta.line
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function __devInject(key, defaultValue, meta) {
|
|
46
|
+
const resolved = inject(key, defaultValue);
|
|
47
|
+
if (!import.meta.dev || !import.meta.client) {
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
const registry = window.__observatory__?.provideInject;
|
|
51
|
+
if (!registry) {
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
const instance = getCurrentInstance();
|
|
55
|
+
const providerInfo = findProvider(String(key), instance);
|
|
56
|
+
registry.registerInject({
|
|
57
|
+
key: String(key),
|
|
58
|
+
componentName: instance?.type?.__name ?? "unknown",
|
|
59
|
+
componentFile: meta.file,
|
|
60
|
+
componentUid: instance?.uid ?? -1,
|
|
61
|
+
resolved: resolved !== void 0,
|
|
62
|
+
resolvedFromFile: providerInfo?.file,
|
|
63
|
+
resolvedFromUid: providerInfo?.uid,
|
|
64
|
+
line: meta.line
|
|
65
|
+
});
|
|
66
|
+
return resolved;
|
|
67
|
+
}
|
|
68
|
+
function findProvider(key, instance) {
|
|
69
|
+
let cur = instance?.parent;
|
|
70
|
+
while (cur) {
|
|
71
|
+
if (cur.appContext?.provides && key in cur.appContext.provides) {
|
|
72
|
+
return { file: cur.type?.__file ?? "unknown", uid: cur.uid };
|
|
73
|
+
}
|
|
74
|
+
if (cur.provides && key in (cur.provides ?? {})) {
|
|
75
|
+
return { file: cur.type?.__file ?? "unknown", uid: cur.uid };
|
|
76
|
+
}
|
|
77
|
+
cur = cur.parent;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function safeSnapshot(value) {
|
|
82
|
+
try {
|
|
83
|
+
if (value === null || value === void 0) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "function") {
|
|
87
|
+
return "[Function]";
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "object") {
|
|
90
|
+
return JSON.parse(JSON.stringify(value));
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
} catch {
|
|
94
|
+
return "[unserializable]";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface RenderEntry {
|
|
2
|
+
uid: number;
|
|
3
|
+
name: string;
|
|
4
|
+
file: string;
|
|
5
|
+
renders: number;
|
|
6
|
+
totalMs: number;
|
|
7
|
+
avgMs: number;
|
|
8
|
+
triggers: Array<{
|
|
9
|
+
key: string;
|
|
10
|
+
type: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}>;
|
|
13
|
+
rect?: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
top: number;
|
|
19
|
+
left: number;
|
|
20
|
+
};
|
|
21
|
+
children: number[];
|
|
22
|
+
parentUid?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sets up a render registry for the given Nuxt app.
|
|
26
|
+
* @param {{ vueApp: import('vue').App }} nuxtApp - The Nuxt app object.
|
|
27
|
+
* @param {object} nuxtApp.vueApp - The Vue app instance.
|
|
28
|
+
* @param {number} threshold - The minimum number of renders required for a component to be tracked.
|
|
29
|
+
* @returns {object} The render registry object.
|
|
30
|
+
*/
|
|
31
|
+
export declare function setupRenderRegistry(nuxtApp: {
|
|
32
|
+
vueApp: import('vue').App;
|
|
33
|
+
}, threshold: number): {
|
|
34
|
+
getAll: () => RenderEntry[];
|
|
35
|
+
snapshot: () => RenderEntry[];
|
|
36
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
export function setupRenderRegistry(nuxtApp, threshold) {
|
|
3
|
+
const entries = ref(/* @__PURE__ */ new Map());
|
|
4
|
+
nuxtApp.vueApp.mixin({
|
|
5
|
+
renderTriggered({ key, type }) {
|
|
6
|
+
const uid = this.$.uid;
|
|
7
|
+
if (!entries.value.has(uid)) {
|
|
8
|
+
entries.value.set(uid, makeEntry(uid, this));
|
|
9
|
+
}
|
|
10
|
+
const entry = entries.value.get(uid);
|
|
11
|
+
entry.triggers.push({ key: String(key), type, timestamp: performance.now() });
|
|
12
|
+
if (entry.triggers.length > 50) {
|
|
13
|
+
entry.triggers.shift();
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
updated() {
|
|
17
|
+
const uid = this.$.uid;
|
|
18
|
+
if (!entries.value.has(uid)) {
|
|
19
|
+
entries.value.set(uid, makeEntry(uid, this));
|
|
20
|
+
}
|
|
21
|
+
const entry = entries.value.get(uid);
|
|
22
|
+
entry.renders++;
|
|
23
|
+
const r = this.$el?.getBoundingClientRect?.();
|
|
24
|
+
entry.rect = r ? {
|
|
25
|
+
x: Math.round(r.x),
|
|
26
|
+
y: Math.round(r.y),
|
|
27
|
+
width: Math.round(r.width),
|
|
28
|
+
height: Math.round(r.height),
|
|
29
|
+
top: Math.round(r.top),
|
|
30
|
+
left: Math.round(r.left)
|
|
31
|
+
} : void 0;
|
|
32
|
+
emit("render:update", { uid, renders: entry.renders });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (import.meta.client && typeof PerformanceObserver !== "undefined") {
|
|
36
|
+
const observer = new PerformanceObserver((list) => {
|
|
37
|
+
for (const perf of list.getEntries()) {
|
|
38
|
+
if (!perf.name.includes("vue-component-render")) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const uidMatch = perf.name.match(/uid:(\d+)/);
|
|
42
|
+
if (!uidMatch) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const uid = Number(uidMatch[1]);
|
|
46
|
+
const entry = entries.value.get(uid);
|
|
47
|
+
if (entry) {
|
|
48
|
+
entry.totalMs += perf.duration;
|
|
49
|
+
entry.avgMs = Math.round(entry.totalMs / entry.renders * 10) / 10;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
observer.observe({ entryTypes: ["measure"] });
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getAll() {
|
|
59
|
+
return [...entries.value.values()].filter((e) => e.renders >= threshold);
|
|
60
|
+
}
|
|
61
|
+
function snapshot() {
|
|
62
|
+
return getAll();
|
|
63
|
+
}
|
|
64
|
+
function emit(event, data) {
|
|
65
|
+
if (!import.meta.client) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const channel = window.__nuxt_devtools__?.channel;
|
|
69
|
+
channel?.send(event, data);
|
|
70
|
+
}
|
|
71
|
+
return { getAll, snapshot };
|
|
72
|
+
}
|
|
73
|
+
function makeEntry(uid, instance) {
|
|
74
|
+
return {
|
|
75
|
+
uid,
|
|
76
|
+
name: instance.$.type.__name ?? instance.$.type.__file?.split("/").pop() ?? `Component#${uid}`,
|
|
77
|
+
file: instance.$.type.__file ?? "unknown",
|
|
78
|
+
renders: 0,
|
|
79
|
+
totalMs: 0,
|
|
80
|
+
avgMs: 0,
|
|
81
|
+
triggers: [],
|
|
82
|
+
children: [],
|
|
83
|
+
parentUid: instance.$parent?.$.uid
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface TransitionEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
transitionName: string;
|
|
4
|
+
parentComponent: string;
|
|
5
|
+
direction: 'enter' | 'leave';
|
|
6
|
+
phase: 'entering' | 'entered' | 'leaving' | 'left' | 'enter-cancelled' | 'leave-cancelled' | 'interrupted';
|
|
7
|
+
startTime: number;
|
|
8
|
+
endTime?: number;
|
|
9
|
+
durationMs?: number;
|
|
10
|
+
cancelled: boolean;
|
|
11
|
+
appear: boolean;
|
|
12
|
+
mode?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function setupTransitionRegistry(): {
|
|
15
|
+
register: (entry: TransitionEntry) => void;
|
|
16
|
+
update: (id: string, patch: Partial<TransitionEntry>) => void;
|
|
17
|
+
getAll: () => TransitionEntry[];
|
|
18
|
+
};
|
|
19
|
+
export declare function createTrackedTransition(registry: ReturnType<typeof setupTransitionRegistry>): import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|