nuxt-devtools-observatory 0.1.5 → 0.1.6

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,5 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed, defineComponent, h, onUnmounted, type VNode } from 'vue'
2
+ import { ref, computed, onUnmounted } from 'vue'
3
+ import ComponentBlock from './ComponentBlock.vue'
4
+ import { useObservatoryData } from '../stores/observatory'
3
5
 
4
6
  interface ComponentNode {
5
7
  id: string
@@ -11,212 +13,16 @@ interface ComponentNode {
11
13
  children: ComponentNode[]
12
14
  }
13
15
 
14
- // ComponentBlock recursive inline component
15
- const ComponentBlock = defineComponent({
16
- name: 'ComponentBlock',
17
- props: {
18
- node: Object as () => ComponentNode,
19
- mode: String,
20
- threshold: Number,
21
- hotOnly: Boolean,
22
- selected: String,
23
- },
24
- emits: ['select'],
25
- setup(props, { emit }): () => VNode | null {
26
- function getVal(n: ComponentNode) {
27
- return props.mode === 'count' ? n.renders : n.avgMs
28
- }
29
-
30
- function getMax(): number {
31
- let max = 1
32
-
33
- function walk(ns: ComponentNode[]) {
34
- ns.forEach((n) => {
35
- const v = getVal(n)
36
-
37
- if (v > max) {
38
- max = v
39
- }
40
-
41
- walk(n.children)
42
- })
43
- }
44
-
45
- walk([props.node!])
46
-
47
- return Math.max(max, props.mode === 'count' ? 40 : 20)
48
- }
49
- function heatColor(val: number, max: number) {
50
- const r = Math.min(val / max, 1)
51
-
52
- if (r < 0.25) {
53
- return { bg: '#EAF3DE', text: '#27500A', border: '#97C459' }
54
- }
55
-
56
- if (r < 0.55) {
57
- return { bg: '#FAEEDA', text: '#633806', border: '#EF9F27' }
58
- }
59
-
60
- if (r < 0.8) {
61
- return { bg: '#FAECE7', text: '#712B13', border: '#D85A30' }
62
- }
63
-
64
- return { bg: '#FCEBEB', text: '#791F1F', border: '#E24B4A' }
65
- }
66
- function isHot(n: ComponentNode) {
67
- return (props.mode === 'count' ? n.renders : n.avgMs) >= props.threshold!
68
- }
69
-
70
- return () => {
71
- const n = props.node!
72
-
73
- if (props.hotOnly && !isHot(n) && !n.children.some((c) => (props.mode === 'count' ? c.renders : c.avgMs) >= props.threshold!)) {
74
- return null
75
- }
76
-
77
- const max = getMax()
78
- const val = getVal(n)
79
- const col = heatColor(val, max)
80
- const isSel = props.selected === n.id
81
- const unit = props.mode === 'count' ? 'renders' : 'ms avg'
82
- const valStr = props.mode === 'count' ? String(val) : val.toFixed(1) + 'ms'
83
-
84
- return h(
85
- 'div',
86
- {
87
- style: {
88
- background: col.bg,
89
- border: isSel ? `2px solid ${col.border}` : `1px solid ${col.border}`,
90
- borderRadius: '6px',
91
- padding: '6px 9px',
92
- marginBottom: '5px',
93
- cursor: 'pointer',
94
- },
95
- onClick: () => emit('select', n),
96
- },
97
- [
98
- h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [
99
- h('span', { style: { fontFamily: 'var(--mono)', fontSize: '11px', fontWeight: '500', color: col.text } }, n.label),
100
- h(
101
- 'span',
102
- { style: { fontFamily: 'var(--mono)', fontSize: '10px', color: col.text, opacity: '0.7', marginLeft: 'auto' } },
103
- `${valStr} ${unit}`
104
- ),
105
- ]),
106
- n.children.length
107
- ? h(
108
- 'div',
109
- {
110
- style: {
111
- marginLeft: '10px',
112
- borderLeft: `1.5px solid ${col.border}40`,
113
- paddingLeft: '8px',
114
- marginTop: '5px',
115
- },
116
- },
117
- n.children.map((child) =>
118
- h(ComponentBlock, {
119
- node: child,
120
- mode: props.mode,
121
- threshold: props.threshold,
122
- hotOnly: props.hotOnly,
123
- selected: props.selected,
124
- onSelect: (v: ComponentNode) => emit('select', v),
125
- })
126
- )
127
- )
128
- : null,
129
- ]
130
- )
131
- }
132
- },
133
- })
16
+ const { renders } = useObservatoryData()
17
+ const baseNodes = renders
134
18
 
135
- // Mock data
136
- const baseNodes = ref<ComponentNode[]>([
137
- {
138
- id: 'NavBar',
139
- label: 'NavBar.vue',
140
- file: 'components/NavBar.vue',
141
- renders: 3,
142
- avgMs: 1.2,
143
- triggers: ['props.user changed'],
144
- children: [],
145
- },
146
- {
147
- id: 'Sidebar',
148
- label: 'Sidebar.vue',
149
- file: 'components/Sidebar.vue',
150
- renders: 2,
151
- avgMs: 0.8,
152
- triggers: ['parent re-render'],
153
- children: [],
154
- },
155
- {
156
- id: 'ProductGrid',
157
- label: 'ProductGrid.vue',
158
- file: 'components/ProductGrid.vue',
159
- renders: 18,
160
- avgMs: 14.3,
161
- triggers: ['store: products updated', 'props.filter changed', 'parent re-render (×16)'],
162
- children: [
163
- {
164
- id: 'ProductCard',
165
- label: 'ProductCard.vue ×12',
166
- file: 'components/ProductCard.vue',
167
- renders: 24,
168
- avgMs: 3.1,
169
- triggers: ['parent re-render (×24)'],
170
- children: [],
171
- },
172
- {
173
- id: 'PriceTag',
174
- label: 'PriceTag.vue ×12',
175
- file: 'components/PriceTag.vue',
176
- renders: 36,
177
- avgMs: 0.4,
178
- triggers: ['props.price changed (×36)'],
179
- children: [],
180
- },
181
- ],
182
- },
183
- {
184
- id: 'CartSummary',
185
- label: 'CartSummary.vue',
186
- file: 'components/CartSummary.vue',
187
- renders: 9,
188
- avgMs: 5.7,
189
- triggers: ['store: cart updated (×9)'],
190
- children: [
191
- {
192
- id: 'CartItem',
193
- label: 'CartItem.vue ×3',
194
- file: 'components/CartItem.vue',
195
- renders: 12,
196
- avgMs: 1.8,
197
- triggers: ['parent re-render (×12)'],
198
- children: [],
199
- },
200
- ],
201
- },
202
- {
203
- id: 'FilterBar',
204
- label: 'FilterBar.vue',
205
- file: 'components/FilterBar.vue',
206
- renders: 7,
207
- avgMs: 2.1,
208
- triggers: ['store: filters changed (×7)'],
209
- children: [],
210
- },
211
- { id: 'Footer', label: 'Footer.vue', file: 'components/Footer.vue', renders: 1, avgMs: 0.3, triggers: ['initial mount'], children: [] },
212
- ])
19
+ let liveInterval: ReturnType<typeof setInterval> | undefined
213
20
 
214
21
  const activeMode = ref<'count' | 'time'>('count')
215
22
  const activeThreshold = ref(5)
216
23
  const activeHotOnly = ref(false)
217
24
  const frozen = ref(false)
218
25
  const activeSelected = ref<ComponentNode | null>(null)
219
- let liveInterval: ReturnType<typeof setInterval> | null = null
220
26
 
221
27
  const rootNodes = computed(() => baseNodes.value)
222
28
 
@@ -251,6 +57,10 @@ function isHot(n: ComponentNode) {
251
57
  return (activeMode.value === 'count' ? n.renders : n.avgMs) >= activeThreshold.value
252
58
  }
253
59
 
60
+ function toggleFreeze() {
61
+ frozen.value = !frozen.value
62
+ }
63
+
254
64
  function startLive() {
255
65
  liveInterval = setInterval(() => {
256
66
  if (frozen.value) {
@@ -263,13 +73,11 @@ function startLive() {
263
73
  }, 1800)
264
74
  }
265
75
 
266
- function toggleFreeze() {
267
- frozen.value = !frozen.value
268
- }
269
-
270
76
  startLive()
271
77
  onUnmounted(() => {
272
- if (liveInterval) clearInterval(liveInterval)
78
+ if (liveInterval) {
79
+ clearInterval(liveInterval)
80
+ }
273
81
  })
274
82
  </script>
275
83
 
@@ -61,7 +61,7 @@ const timelineGeometry = computed(() => {
61
61
  }))
62
62
  })
63
63
 
64
- function phaseColor(phase: TransitionEntry['phase'], direction: TransitionEntry['direction']): string {
64
+ function phaseColor(phase: TransitionEntry['phase']): string {
65
65
  if (phase === 'entering' || phase === 'leaving') {
66
66
  return '#7f77dd'
67
67
  }
@@ -204,7 +204,7 @@ function directionColor(e: TransitionEntry): string {
204
204
  :style="{
205
205
  left: timelineGeometry[i]?.left + '%',
206
206
  width: Math.max(timelineGeometry[i]?.width ?? 1, 1) + '%',
207
- background: phaseColor(entry.phase, entry.direction),
207
+ background: phaseColor(entry.phase),
208
208
  opacity: entry.phase === 'entering' || entry.phase === 'leaving' ? '0.55' : '1',
209
209
  }"
210
210
  />
@@ -267,7 +267,13 @@ function directionColor(e: TransitionEntry): string {
267
267
  <div class="panel-row">
268
268
  <span class="panel-key">Duration</span>
269
269
  <span class="panel-val mono" style="font-weight: 500">
270
- {{ selected.durationMs !== undefined ? selected.durationMs + 'ms' : selected.phase === 'interrupted' ? 'interrupted' : 'in progress' }}
270
+ {{
271
+ selected.durationMs !== undefined
272
+ ? selected.durationMs + 'ms'
273
+ : selected.phase === 'interrupted'
274
+ ? 'interrupted'
275
+ : 'in progress'
276
+ }}
271
277
  </span>
272
278
  </div>
273
279
  </div>
package/dist/module.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import * as nuxt_schema from 'nuxt/schema';
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  interface ModuleOptions {
4
4
  /**
@@ -32,7 +32,7 @@ interface ModuleOptions {
32
32
  */
33
33
  heatmapThreshold?: number;
34
34
  }
35
- declare const _default: nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
35
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
36
36
 
37
37
  export { _default as default };
38
38
  export type { ModuleOptions };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "0.1.5",
7
+ "version": "0.1.6",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -29,7 +29,7 @@ function fetchInstrumentPlugin() {
29
29
  if (!isVue && !id.endsWith(".ts") && !id.endsWith(".js")) {
30
30
  return;
31
31
  }
32
- if (id.includes("node_modules") || id.includes("composable-registry") || id.includes("provide-inject-registry") || id.includes("fetch-registry")) {
32
+ if (id.includes("node_modules")) {
33
33
  return;
34
34
  }
35
35
  let scriptCode = code;
@@ -66,20 +66,29 @@ function fetchInstrumentPlugin() {
66
66
  }
67
67
  const originalName = callee.name;
68
68
  const args = path.node.arguments;
69
- const urlArg = args[0] ?? t.stringLiteral("");
70
- const optsArg = args[1] ?? t.objectExpression([]);
71
- let key = originalName;
72
- if (t.isObjectExpression(optsArg)) {
73
- const keyProp = optsArg.properties.find(
74
- (p) => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === "key"
75
- );
76
- if (keyProp && t.isStringLiteral(keyProp.value)) {
77
- key = keyProp.value.value;
69
+ let keyArg = void 0;
70
+ let handlerArg = void 0;
71
+ let optsArg = void 0;
72
+ function getExpr(node) {
73
+ return t.isExpression(node) ? node : void 0;
74
+ }
75
+ if (originalName === "useAsyncData" || originalName === "useLazyAsyncData") {
76
+ if (args.length === 1 && getExpr(args[0])) {
77
+ handlerArg = getExpr(args[0]);
78
+ } else if (args.length >= 2 && getExpr(args[0]) && getExpr(args[1])) {
79
+ keyArg = getExpr(args[0]);
80
+ handlerArg = getExpr(args[1]);
81
+ optsArg = getExpr(args[2]) ?? t.objectExpression([]);
78
82
  } else {
79
- if (t.isStringLiteral(urlArg)) {
80
- key = urlArg.value.replace(/[^a-z0-9]/gi, "-").replace(/^-+|-+$/g, "");
81
- }
83
+ return;
82
84
  }
85
+ } else {
86
+ keyArg = getExpr(args[0]) ?? t.stringLiteral("");
87
+ optsArg = getExpr(args[1]) ?? t.objectExpression([]);
88
+ }
89
+ let key = originalName;
90
+ if (keyArg && t.isStringLiteral(keyArg)) {
91
+ key = keyArg.value.replace(/[^a-z0-9]/gi, "-").replace(/^-+|-+$/g, "");
83
92
  }
84
93
  const loc = path.node.loc;
85
94
  const meta = t.objectExpression([
@@ -88,8 +97,37 @@ function fetchInstrumentPlugin() {
88
97
  t.objectProperty(t.identifier("line"), t.numericLiteral(loc?.start.line ?? 0)),
89
98
  t.objectProperty(t.identifier("originalFn"), t.stringLiteral(originalName))
90
99
  ]);
91
- path.replaceWith(t.callExpression(t.identifier("__devFetch"), [t.identifier(originalName), urlArg, optsArg, meta]));
92
- modified = true;
100
+ if (originalName === "useAsyncData" || originalName === "useLazyAsyncData") {
101
+ if (handlerArg) {
102
+ const wrappedHandler = t.callExpression(t.identifier("__devFetch"), [
103
+ handlerArg,
104
+ keyArg ?? t.stringLiteral(key),
105
+ meta
106
+ ]);
107
+ if (keyArg) {
108
+ path.replaceWith(
109
+ t.callExpression(t.identifier(originalName), [
110
+ keyArg,
111
+ wrappedHandler,
112
+ optsArg ?? t.objectExpression([])
113
+ ])
114
+ );
115
+ } else {
116
+ path.replaceWith(t.callExpression(t.identifier(originalName), [wrappedHandler]));
117
+ }
118
+ modified = true;
119
+ }
120
+ } else {
121
+ path.replaceWith(
122
+ t.callExpression(t.identifier("__devFetch"), [
123
+ t.identifier(originalName),
124
+ keyArg ?? t.stringLiteral(""),
125
+ optsArg ?? t.objectExpression([]),
126
+ meta
127
+ ])
128
+ );
129
+ modified = true;
130
+ }
93
131
  }
94
132
  });
95
133
  if (!modified) {
@@ -98,12 +136,14 @@ function fetchInstrumentPlugin() {
98
136
  const importStatement = hasImport ? "" : `import { __devFetch } from 'nuxt-devtools-observatory/runtime/fetch-registry';
99
137
  `;
100
138
  const output = generate$2(ast, { retainLines: true }, scriptCode);
139
+ let finalCode;
101
140
  if (isVue) {
102
- const newCode = code.slice(0, scriptStart) + importStatement + output.code + code.slice(scriptStart + scriptCode.length);
103
- return { code: newCode };
141
+ finalCode = code.slice(0, scriptStart) + importStatement + output.code + code.slice(scriptStart + scriptCode.length);
142
+ } else {
143
+ finalCode = importStatement + output.code;
104
144
  }
105
145
  return {
106
- code: importStatement + output.code,
146
+ code: finalCode,
107
147
  map: output.map
108
148
  };
109
149
  } catch (err) {
@@ -194,11 +234,13 @@ function provideInjectPlugin() {
194
234
  const importLine = `import { __devProvide, __devInject } from 'nuxt-devtools-observatory/runtime/provide-inject-registry';
195
235
  `;
196
236
  const output = generate$1(ast, { retainLines: true }, scriptCode);
237
+ let finalCode;
197
238
  if (isVue) {
198
- const newCode = code.slice(0, scriptStart) + importLine + output.code + code.slice(scriptStart + scriptCode.length);
199
- return { code: newCode };
239
+ finalCode = code.slice(0, scriptStart) + (scriptCode.includes("__devProvide") ? "" : importLine) + output.code + code.slice(scriptStart + scriptCode.length);
240
+ } else {
241
+ finalCode = (scriptCode.includes("__devProvide") ? "" : importLine) + output.code;
200
242
  }
201
- return { code: importLine + output.code, map: output.map };
243
+ return { code: finalCode, map: output.map };
202
244
  } catch (err) {
203
245
  console.warn("[observatory] provide/inject transform error:", err);
204
246
  return null;
@@ -323,11 +365,13 @@ function composableTrackerPlugin() {
323
365
  const importLine = `import { __trackComposable } from 'nuxt-devtools-observatory/runtime/composable-registry';
324
366
  `;
325
367
  const output = generate(ast, { retainLines: true }, scriptCode);
368
+ let finalCode;
326
369
  if (isVue) {
327
- const newCode = code.slice(0, scriptStart) + importLine + output.code + code.slice(scriptStart + scriptCode.length);
328
- return { code: newCode };
370
+ finalCode = code.slice(0, scriptStart) + (scriptCode.includes("__trackComposable") ? "" : importLine) + output.code + code.slice(scriptStart + scriptCode.length);
371
+ } else {
372
+ finalCode = (scriptCode.includes("__trackComposable") ? "" : importLine) + output.code;
329
373
  }
330
- return { code: importLine + output.code, map: output.map };
374
+ return { code: finalCode, map: output.map };
331
375
  } catch (err) {
332
376
  console.warn("[observatory] composable transform error:", err);
333
377
  return null;
@@ -14,8 +14,49 @@ export function setupComposableRegistry() {
14
14
  entries.value.set(id, updated);
15
15
  emit("composable: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
+ name: entry.name,
37
+ componentFile: entry.componentFile,
38
+ componentUid: entry.componentUid,
39
+ status: entry.status,
40
+ leak: entry.leak,
41
+ 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
+ ),
51
+ watcherCount: entry.watcherCount,
52
+ intervalCount: entry.intervalCount,
53
+ lifecycle: entry.lifecycle,
54
+ file: entry.file,
55
+ line: entry.line
56
+ };
57
+ }
17
58
  function getAll() {
18
- return [...entries.value.values()];
59
+ return [...entries.value.values()].map(sanitize);
19
60
  }
20
61
  function emit(event, data) {
21
62
  if (!import.meta.client) {
@@ -56,8 +56,8 @@ export declare function setupFetchRegistry(): {
56
56
  readonly line?: number | undefined;
57
57
  }>>>;
58
58
  };
59
- export declare function __devFetch(originalFn: (url: string, opts: Record<string, unknown>) => Promise<unknown>, url: string, opts: Record<string, unknown>, meta: {
59
+ export declare function __devFetch(originalFn: (...args: unknown[]) => unknown, arg1: ((...args: unknown[]) => unknown) | string, arg2: Record<string, unknown>, meta: {
60
60
  key: string;
61
61
  file: string;
62
62
  line: number;
63
- }): Promise<unknown>;
63
+ }): unknown;