nuxt-devtools-observatory 0.1.7 → 0.1.8

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.
@@ -95,10 +95,16 @@ export function __trackComposable(name, callFn, meta) {
95
95
  }
96
96
  originalClearInterval(id2);
97
97
  });
98
- const trackedWatchers = [];
98
+ const effectsBefore = new Set(instance?.scope?.effects ?? []);
99
+ const mountedHooksBefore = instance?.bm?.length ?? 0;
100
+ const unmountedHooksBefore = instance?.um?.length ?? 0;
99
101
  const result = callFn();
100
102
  window.setInterval = originalSetInterval;
101
103
  window.clearInterval = originalClearInterval;
104
+ const trackedWatchers = (instance?.scope?.effects ?? []).filter((effect) => !effectsBefore.has(effect)).map((effect) => ({
105
+ effect,
106
+ stop: () => effect.stop?.()
107
+ }));
102
108
  const refs = {};
103
109
  if (result && typeof result === "object") {
104
110
  for (const [key, val] of Object.entries(result)) {
@@ -121,8 +127,8 @@ export function __trackComposable(name, callFn, meta) {
121
127
  watcherCount: trackedWatchers.length,
122
128
  intervalCount: trackedIntervals.length,
123
129
  lifecycle: {
124
- hasOnMounted: false,
125
- hasOnUnmounted: false,
130
+ hasOnMounted: (instance?.bm?.length ?? 0) > mountedHooksBefore,
131
+ hasOnUnmounted: (instance?.um?.length ?? 0) > unmountedHooksBefore,
126
132
  watchersCleaned: true,
127
133
  intervalsCleaned: true
128
134
  },
@@ -131,7 +137,7 @@ export function __trackComposable(name, callFn, meta) {
131
137
  };
132
138
  registry.register(entry);
133
139
  onUnmounted(() => {
134
- const leakedWatchers = trackedWatchers.filter((w) => !w.stopped);
140
+ const leakedWatchers = trackedWatchers.filter((w) => w.effect.active);
135
141
  const leakedIntervals = trackedIntervals.filter((id2) => !clearedIntervals.has(id2));
136
142
  const leak = leakedWatchers.length > 0 || leakedIntervals.length > 0;
137
143
  const reasons = [];
@@ -14,6 +14,19 @@ export interface FetchEntry {
14
14
  file?: string;
15
15
  line?: number;
16
16
  }
17
+ interface FetchResponse extends Response {
18
+ _data?: unknown;
19
+ }
20
+ interface FetchOptions {
21
+ server?: boolean;
22
+ onResponse?: (ctx: {
23
+ response: FetchResponse;
24
+ }) => void;
25
+ onResponseError?: (ctx: {
26
+ response: FetchResponse;
27
+ }) => void;
28
+ [key: string]: unknown;
29
+ }
17
30
  /**
18
31
  * Sets up the fetch registry, which tracks all fetch requests and their
19
32
  * associated metadata (e.g. duration, size, origin).
@@ -56,8 +69,9 @@ export declare function setupFetchRegistry(): {
56
69
  readonly line?: number | undefined;
57
70
  }>>>;
58
71
  };
59
- export declare function __devFetch(originalFn: (...args: unknown[]) => unknown, arg1: ((...args: unknown[]) => unknown) | string, arg2: Record<string, unknown>, meta: {
72
+ export declare function __devFetch(originalFn: (url: string, opts: FetchOptions) => Promise<unknown>, url: string, opts: FetchOptions, meta: {
60
73
  key: string;
61
74
  file: string;
62
75
  line: number;
63
- }): unknown;
76
+ }): Promise<unknown>;
77
+ export {};
@@ -14,42 +14,8 @@ export function setupFetchRegistry() {
14
14
  entries.value.set(id, updated);
15
15
  emit("fetch:update", updated);
16
16
  }
17
- function safeValue(val) {
18
- if (val === void 0 || val === null) {
19
- return val;
20
- }
21
- if (typeof val === "function") {
22
- return void 0;
23
- }
24
- if (typeof val === "object") {
25
- try {
26
- return JSON.parse(JSON.stringify(val));
27
- } catch {
28
- return String(val);
29
- }
30
- }
31
- return val;
32
- }
33
- function sanitize(entry) {
34
- return {
35
- id: entry.id,
36
- key: entry.key,
37
- url: entry.url,
38
- status: entry.status,
39
- origin: entry.origin,
40
- startTime: entry.startTime,
41
- endTime: entry.endTime,
42
- ms: entry.ms,
43
- size: entry.size,
44
- cached: entry.cached,
45
- payload: safeValue(entry.payload),
46
- error: safeValue(entry.error),
47
- file: entry.file,
48
- line: entry.line
49
- };
50
- }
51
17
  function getAll() {
52
- return [...entries.value.values()].map(sanitize);
18
+ return [...entries.value.values()];
53
19
  }
54
20
  function clear() {
55
21
  entries.value.clear();
@@ -64,97 +30,61 @@ export function setupFetchRegistry() {
64
30
  }
65
31
  return { register, update, getAll, clear, entries: readonly(entries) };
66
32
  }
67
- export function __devFetch(originalFn, arg1, arg2, meta) {
33
+ export function __devFetch(originalFn, url, opts, meta) {
68
34
  if (!import.meta.dev || !import.meta.client) {
69
- if (typeof arg1 === "function") {
70
- return (...args) => arg1(...args);
71
- }
72
- return originalFn(arg1, arg2);
35
+ return originalFn(url, opts);
73
36
  }
74
37
  const registry = window.__observatory__?.fetch;
75
38
  if (!registry) {
76
- return typeof arg1 === "function" ? originalFn(arg1, arg2) : originalFn(arg1, arg2);
39
+ return originalFn(url, opts);
40
+ }
41
+ const id = `${meta.key}::${Date.now()}`;
42
+ const startTime = performance.now();
43
+ const payload = window.__NUXT__?.data ?? {};
44
+ const fromPayload = Object.prototype.hasOwnProperty.call(payload, meta.key);
45
+ const origin = fromPayload ? "ssr" : "csr";
46
+ registry.register({
47
+ id,
48
+ key: meta.key,
49
+ url: typeof url === "string" ? url : String(url),
50
+ status: fromPayload ? "cached" : "pending",
51
+ origin,
52
+ startTime,
53
+ cached: fromPayload,
54
+ payload: fromPayload ? payload[meta.key] : void 0,
55
+ file: meta.file,
56
+ line: meta.line
57
+ });
58
+ if (fromPayload) {
59
+ return originalFn(url, opts);
77
60
  }
78
- if (typeof arg1 === "function") {
79
- const handler = arg1;
80
- return function wrappedHandler(...handlerArgs) {
81
- const id = `${meta.key}::${Date.now()}`;
82
- const startTime = performance.now();
83
- const origin = import.meta.server ? "ssr" : "csr";
84
- registry.register({
85
- id,
86
- key: meta.key,
87
- url: meta.key,
88
- // No URL in useAsyncData, use key as identifier
89
- status: "pending",
90
- origin,
91
- startTime,
92
- cached: false,
93
- file: meta.file,
94
- line: meta.line
61
+ return originalFn(url, {
62
+ ...opts,
63
+ onResponse({ response }) {
64
+ const ms = Math.round(performance.now() - startTime);
65
+ const size = Number(response.headers?.get("content-length")) || void 0;
66
+ const cached = response.headers?.get("x-nuxt-cache") === "HIT";
67
+ registry.update(id, {
68
+ status: cached ? "cached" : response.ok ? "ok" : "error",
69
+ endTime: performance.now(),
70
+ ms,
71
+ size,
72
+ cached,
73
+ payload: response._data
95
74
  });
96
- return Promise.resolve(handler(...handlerArgs)).then((result) => {
97
- registry.update(id, {
98
- status: "ok",
99
- endTime: performance.now(),
100
- ms: Math.round(performance.now() - startTime),
101
- payload: result
102
- });
103
- return result;
104
- }).catch((error) => {
105
- registry.update(id, {
106
- status: "error",
107
- endTime: performance.now(),
108
- ms: Math.round(performance.now() - startTime),
109
- error
110
- });
111
- throw error;
75
+ if (typeof opts.onResponse === "function") {
76
+ opts.onResponse({ response });
77
+ }
78
+ },
79
+ onResponseError({ response }) {
80
+ registry.update(id, {
81
+ status: "error",
82
+ endTime: performance.now(),
83
+ ms: Math.round(performance.now() - startTime)
112
84
  });
113
- };
114
- } else {
115
- const url = arg1;
116
- const opts = arg2 || {};
117
- const id = `${meta.key}::${Date.now()}`;
118
- const startTime = performance.now();
119
- const origin = import.meta.server ? "ssr" : "csr";
120
- registry.register({
121
- id,
122
- key: meta.key,
123
- url: typeof url === "string" ? url : String(url),
124
- status: "pending",
125
- origin,
126
- startTime,
127
- cached: false,
128
- file: meta.file,
129
- line: meta.line
130
- });
131
- return originalFn(url, {
132
- ...opts,
133
- onResponse({ response }) {
134
- const ms = Math.round(performance.now() - startTime);
135
- const size = Number(response.headers?.get("content-length")) || void 0;
136
- const cached = response.headers?.get("x-nuxt-cache") === "HIT";
137
- registry.update(id, {
138
- status: response.ok ? "ok" : "error",
139
- endTime: performance.now(),
140
- ms,
141
- size,
142
- cached
143
- });
144
- if (typeof opts.onResponse === "function") {
145
- opts.onResponse({ response });
146
- }
147
- },
148
- onResponseError({ response }) {
149
- registry.update(id, {
150
- status: "error",
151
- endTime: performance.now(),
152
- ms: Math.round(performance.now() - startTime)
153
- });
154
- if (typeof opts.onResponseError === "function") {
155
- opts.onResponseError({ response });
156
- }
85
+ if (typeof opts.onResponseError === "function") {
86
+ opts.onResponseError({ response });
157
87
  }
158
- });
159
- }
88
+ }
89
+ });
160
90
  }
@@ -3,6 +3,8 @@ export interface ProvideEntry {
3
3
  componentName: string;
4
4
  componentFile: string;
5
5
  componentUid: number;
6
+ parentUid?: number;
7
+ parentFile?: string;
6
8
  isReactive: boolean;
7
9
  valueSnapshot: unknown;
8
10
  line: number;
@@ -12,6 +14,8 @@ export interface InjectEntry {
12
14
  componentName: string;
13
15
  componentFile: string;
14
16
  componentUid: number;
17
+ parentUid?: number;
18
+ parentFile?: string;
15
19
  resolved: boolean;
16
20
  resolvedFromFile?: string;
17
21
  resolvedFromUid?: number;
@@ -79,6 +79,8 @@ export function __devProvide(key, value, meta) {
79
79
  componentName: instance?.type?.__name ?? "unknown",
80
80
  componentFile: meta.file,
81
81
  componentUid: instance?.uid ?? -1,
82
+ parentUid: instance?.parent?.uid,
83
+ parentFile: instance?.parent?.type?.__file,
82
84
  isReactive: isRef(value) || isReactive(value),
83
85
  valueSnapshot: safeSnapshot(unref(value)),
84
86
  line: meta.line
@@ -100,6 +102,8 @@ export function __devInject(key, defaultValue, meta) {
100
102
  componentName: instance?.type?.__name ?? "unknown",
101
103
  componentFile: meta.file,
102
104
  componentUid: instance?.uid ?? -1,
105
+ parentUid: instance?.parent?.uid,
106
+ parentFile: instance?.parent?.type?.__file,
103
107
  resolved: resolved !== void 0,
104
108
  resolvedFromFile: providerInfo?.file,
105
109
  resolvedFromUid: providerInfo?.uid,
@@ -25,12 +25,11 @@ export interface RenderEntry {
25
25
  * Sets up a render registry for the given Nuxt app.
26
26
  * @param {{ vueApp: import('vue').App }} nuxtApp - The Nuxt app object.
27
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
28
  * @returns {object} The render registry object.
30
29
  */
31
30
  export declare function setupRenderRegistry(nuxtApp: {
32
31
  vueApp: import('vue').App;
33
- }, threshold: number): {
32
+ }): {
34
33
  getAll: () => RenderEntry[];
35
34
  snapshot: () => RenderEntry[];
36
35
  };
@@ -1,62 +1,42 @@
1
1
  import { ref } from "vue";
2
- export function setupRenderRegistry(nuxtApp, threshold) {
2
+ export function setupRenderRegistry(nuxtApp) {
3
3
  const entries = ref(/* @__PURE__ */ new Map());
4
- function ensureEntryAndParent() {
5
- const uid = this.$.uid;
6
- let isFirstRender = false;
4
+ function ensureEntry(instance) {
5
+ const uid = instance.$.uid;
7
6
  if (!entries.value.has(uid)) {
8
- entries.value.set(uid, makeEntry(uid, this));
9
- isFirstRender = true;
10
- }
11
- if (isFirstRender) {
12
- const parent = this.$parent?.$;
13
- if (parent) {
14
- const parentUid = parent.uid;
15
- if (!entries.value.has(parentUid)) {
16
- entries.value.set(parentUid, makeEntry(parentUid, this.$parent));
17
- }
18
- const parentEntry = entries.value.get(parentUid);
19
- if (!parentEntry.children.includes(uid)) {
20
- parentEntry.children.push(uid);
21
- }
22
- }
7
+ entries.value.set(uid, makeEntry(uid, instance));
23
8
  }
24
9
  return entries.value.get(uid);
25
10
  }
11
+ function syncRect(entry, instance) {
12
+ const rect = instance.$el?.getBoundingClientRect?.();
13
+ entry.rect = rect ? {
14
+ x: Math.round(rect.x),
15
+ y: Math.round(rect.y),
16
+ width: Math.round(rect.width),
17
+ height: Math.round(rect.height),
18
+ top: Math.round(rect.top),
19
+ left: Math.round(rect.left)
20
+ } : void 0;
21
+ }
26
22
  nuxtApp.vueApp.mixin({
27
23
  mounted() {
28
- ensureEntryAndParent.call(this);
24
+ const entry = ensureEntry(this);
25
+ entry.renders++;
26
+ syncRect(entry, this);
27
+ emit("render:update", { uid: entry.uid, renders: entry.renders });
29
28
  },
30
29
  renderTriggered({ key, type }) {
31
- const uid = this.$.uid;
32
- if (!entries.value.has(uid)) {
33
- entries.value.set(uid, makeEntry(uid, this));
34
- }
35
- const entry = entries.value.get(uid);
30
+ const entry = ensureEntry(this);
36
31
  entry.triggers.push({ key: String(key), type, timestamp: performance.now() });
37
32
  if (entry.triggers.length > 50) {
38
33
  entry.triggers.shift();
39
34
  }
40
35
  },
41
36
  updated() {
42
- const entry = ensureEntryAndParent.call(this);
37
+ const entry = ensureEntry(this);
43
38
  entry.renders++;
44
- console.log("[Observatory] render:", {
45
- name: entry.name,
46
- uid: entry.uid,
47
- file: entry.file,
48
- renders: entry.renders,
49
- instance: this
50
- });
51
- const r = this.$el?.getBoundingClientRect?.();
52
- entry.rect = r ? {
53
- x: Math.round(r.x),
54
- y: Math.round(r.y),
55
- width: Math.round(r.width),
56
- height: Math.round(r.height),
57
- top: Math.round(r.top),
58
- left: Math.round(r.left)
59
- } : void 0;
39
+ syncRect(entry, this);
60
40
  emit("render:update", { uid: entry.uid, renders: entry.renders });
61
41
  }
62
42
  });
@@ -120,23 +100,10 @@ export function setupRenderRegistry(nuxtApp, threshold) {
120
100
  return { getAll, snapshot };
121
101
  }
122
102
  function makeEntry(uid, instance) {
123
- const type = instance.$.type;
124
- let name = "";
125
- if (typeof type.__name === "string" && type.__name.trim()) {
126
- name = type.__name;
127
- } else if (typeof type.name === "string" && type.name.trim()) {
128
- name = type.name;
129
- } else if (typeof type.__file === "string" && type.__file.trim()) {
130
- name = type.__file.split("/").pop()?.replace(/\.vue$/, "") || "";
131
- }
132
- if (!name) {
133
- name = `Component#${uid}`;
134
- }
135
- console.log("[Observatory] makeEntry:", { uid, name, file: type.__file, type });
136
103
  return {
137
104
  uid,
138
- name,
139
- file: type.__file ?? "unknown",
105
+ name: instance.$.type.__name ?? instance.$.type.__file?.split("/").pop() ?? `Component#${uid}`,
106
+ file: instance.$.type.__file ?? "unknown",
140
107
  renders: 0,
141
108
  totalMs: 0,
142
109
  avgMs: 0,
@@ -4,6 +4,39 @@ import { setupProvideInjectRegistry } from "./composables/provide-inject-registr
4
4
  import { setupComposableRegistry } from "./composables/composable-registry.js";
5
5
  import { setupRenderRegistry } from "./composables/render-registry.js";
6
6
  import { setupTransitionRegistry } from "./composables/transition-registry.js";
7
+ function toSerializable(value, seen = /* @__PURE__ */ new WeakSet()) {
8
+ if (value === null || value === void 0) {
9
+ return value;
10
+ }
11
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
12
+ return value;
13
+ }
14
+ if (typeof value === "bigint") {
15
+ return value.toString();
16
+ }
17
+ if (typeof value === "function" || typeof value === "symbol") {
18
+ return `[${typeof value}]`;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return value.map((entry) => toSerializable(entry, seen));
22
+ }
23
+ if (typeof value === "object") {
24
+ if (seen.has(value)) {
25
+ return "[circular]";
26
+ }
27
+ seen.add(value);
28
+ if (value instanceof Date) {
29
+ return value.toISOString();
30
+ }
31
+ const plain = {};
32
+ for (const [key, entry] of Object.entries(value)) {
33
+ plain[key] = toSerializable(entry, seen);
34
+ }
35
+ seen.delete(value);
36
+ return plain;
37
+ }
38
+ return String(value);
39
+ }
7
40
  export default defineNuxtPlugin(() => {
8
41
  if (!import.meta.dev) {
9
42
  return;
@@ -14,7 +47,7 @@ export default defineNuxtPlugin(() => {
14
47
  const fetchRegistry = setupFetchRegistry();
15
48
  const provideInjectRegistry = setupProvideInjectRegistry();
16
49
  const composableRegistry = setupComposableRegistry();
17
- const renderRegistry = setupRenderRegistry(nuxtApp, config.heatmapThreshold);
50
+ const renderRegistry = setupRenderRegistry(nuxtApp);
18
51
  const transitionRegistry = setupTransitionRegistry();
19
52
  if (import.meta.client) {
20
53
  delete window.__observatory__;
@@ -29,26 +62,18 @@ export default defineNuxtPlugin(() => {
29
62
  if (event.data?.type !== "observatory:request") {
30
63
  return;
31
64
  }
32
- const source = event.source;
33
- try {
34
- const snapshot = {
35
- fetch: fetchRegistry.getAll(),
36
- provideInject: provideInjectRegistry.getAll(),
37
- composables: composableRegistry.getAll(),
38
- renders: renderRegistry.getAll(),
39
- transitions: transitionRegistry.getAll()
40
- };
41
- const payload = JSON.stringify(snapshot);
42
- source?.postMessage(
43
- {
44
- type: "observatory:snapshot",
45
- data: payload
46
- },
47
- "*"
48
- );
49
- } catch (err) {
50
- console.warn("Observatory snapshot serialization failed:", err);
65
+ if (config.clientOrigin && event.origin !== config.clientOrigin) {
66
+ return;
51
67
  }
68
+ const source = event.source;
69
+ const snapshot = buildSnapshot();
70
+ source?.postMessage(
71
+ {
72
+ type: "observatory:snapshot",
73
+ data: snapshot
74
+ },
75
+ event.origin
76
+ );
52
77
  });
53
78
  }
54
79
  nuxtApp.hook("app:mounted", () => {
@@ -67,7 +92,10 @@ export default defineNuxtPlugin(() => {
67
92
  if (!channel) {
68
93
  return;
69
94
  }
70
- channel.send("observatory:snapshot", {
95
+ channel.send("observatory:snapshot", buildSnapshot());
96
+ }
97
+ function buildSnapshot() {
98
+ return toSerializable({
71
99
  fetch: fetchRegistry.getAll(),
72
100
  provideInject: provideInjectRegistry.getAll(),
73
101
  composables: composableRegistry.getAll(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-devtools-observatory",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Nuxt DevTools: useFetch Dashboard, provide/inject Graph, Composable Tracker, Render Heatmap",
5
5
  "type": "module",
6
6
  "main": "./dist/module.mjs",