nuxt-devtools-observatory 0.1.32 → 0.1.34

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.
Files changed (36) hide show
  1. package/README.md +37 -1
  2. package/client/.env.example +2 -0
  3. package/client/dist/assets/index-BO7neKEi.css +1 -0
  4. package/client/dist/assets/index-fFBuk6M6.js +20 -0
  5. package/client/dist/index.html +2 -2
  6. package/client/src/App.vue +8 -0
  7. package/client/src/components/Flamegraph.vue +4 -4
  8. package/client/src/components/SpanInspector.vue +1 -1
  9. package/client/src/composables/composable-search.ts +3 -0
  10. package/client/src/composables/trace-render-aggregation.ts +11 -2
  11. package/client/src/composables/useVirtualizationConfig.ts +40 -0
  12. package/client/src/composables/useVirtualizationFlags.ts +129 -0
  13. package/client/src/stores/observatory.ts +20 -0
  14. package/client/src/views/ComposableTracker.vue +212 -71
  15. package/client/src/views/FetchDashboard.vue +181 -16
  16. package/client/src/views/PiniaStoreTracker.vue +343 -0
  17. package/client/src/views/ProvideInjectGraph.vue +66 -18
  18. package/client/src/views/RenderHeatmap.vue +329 -75
  19. package/client/src/views/TraceViewer.vue +190 -20
  20. package/client/src/views/TransitionTimeline.vue +112 -19
  21. package/dist/module.d.mts +15 -0
  22. package/dist/module.json +1 -1
  23. package/dist/module.mjs +28 -24
  24. package/dist/runtime/composables/pinia-store-registry.d.ts +44 -0
  25. package/dist/runtime/composables/pinia-store-registry.js +447 -0
  26. package/dist/runtime/composables/provide-inject-registry.js +13 -8
  27. package/dist/runtime/composables/render-registry.js +6 -4
  28. package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
  29. package/dist/runtime/instrumentation/fetch.d.ts +7 -1
  30. package/dist/runtime/instrumentation/fetch.js +22 -1
  31. package/dist/runtime/plugin.js +39 -2
  32. package/dist/runtime/test-bridge.d.ts +18 -0
  33. package/dist/runtime/test-bridge.js +100 -0
  34. package/package.json +14 -3
  35. package/client/dist/assets/index-5Wl1XYRH.js +0 -17
  36. package/client/dist/assets/index-DT_QUiIh.css +0 -1
@@ -2,15 +2,18 @@ import { isRef, isReactive, unref, getCurrentInstance, provide, inject } from "v
2
2
  export function setupProvideInjectRegistry() {
3
3
  let dirty = true;
4
4
  let cachedSnapshot = '{"provides":[],"injects":[]}';
5
+ let hasLiveProvides = false;
5
6
  function markDirty() {
6
7
  dirty = true;
7
8
  }
8
9
  const provides = /* @__PURE__ */ new Map();
9
10
  const injects = /* @__PURE__ */ new Map();
10
11
  function registerProvide(entry) {
11
- provides.set(`${entry.key}:${entry.componentUid}`, entry);
12
+ const internal = entry;
13
+ provides.set(`${entry.key}:${entry.componentUid}`, internal);
14
+ hasLiveProvides = hasLiveProvides || internal.__valueSource !== void 0;
12
15
  markDirty();
13
- emit("provide:register", entry);
16
+ emit("provide:register", sanitizeProvide(internal));
14
17
  }
15
18
  function registerInject(entry) {
16
19
  injects.set(`${entry.key}:${entry.componentUid}`, entry);
@@ -20,6 +23,7 @@ export function setupProvideInjectRegistry() {
20
23
  function clear() {
21
24
  provides.clear();
22
25
  injects.clear();
26
+ hasLiveProvides = false;
23
27
  markDirty();
24
28
  emit("provide:clear", {});
25
29
  }
@@ -32,9 +36,8 @@ export function setupProvideInjectRegistry() {
32
36
  parentUid: entry.parentUid,
33
37
  parentFile: entry.parentFile,
34
38
  isReactive: entry.isReactive,
35
- // valueSnapshot is already a plain serializable value captured at provide()
36
- // time by safeSnapshot() in the shim — no need to deep-clone it again here.
37
- valueSnapshot: entry.valueSnapshot,
39
+ // Reactive values are materialized from their live source on every read.
40
+ valueSnapshot: entry.__valueSource !== void 0 ? safeSnapshot(unref(entry.__valueSource)) : entry.valueSnapshot,
38
41
  line: entry.line,
39
42
  scope: entry.scope,
40
43
  isShadowing: entry.isShadowing
@@ -61,7 +64,7 @@ export function setupProvideInjectRegistry() {
61
64
  };
62
65
  }
63
66
  function getSnapshot() {
64
- if (!dirty) {
67
+ if (!dirty && !hasLiveProvides) {
65
68
  return cachedSnapshot;
66
69
  }
67
70
  try {
@@ -103,6 +106,7 @@ export function __devProvide(key, value, meta) {
103
106
  scope = "layout";
104
107
  }
105
108
  const isShadowing = findProvider(keyStr, instance) !== null;
109
+ const reactiveValue = isRef(value) || isReactive(value);
106
110
  registry.registerProvide({
107
111
  key: keyStr,
108
112
  componentName: instance?.type?.__name ?? "unknown",
@@ -110,11 +114,12 @@ export function __devProvide(key, value, meta) {
110
114
  componentUid: instance?.uid ?? -1,
111
115
  parentUid: instance?.parent?.uid,
112
116
  parentFile: instance?.parent?.type?.__file,
113
- isReactive: isRef(value) || isReactive(value),
117
+ isReactive: reactiveValue,
114
118
  valueSnapshot: safeSnapshot(unref(value)),
115
119
  line: meta.line,
116
120
  scope,
117
- isShadowing
121
+ isShadowing,
122
+ ...reactiveValue ? { __valueSource: value } : {}
118
123
  });
119
124
  }
120
125
  export function __devInject(key, defaultValue, meta) {
@@ -74,7 +74,7 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
74
74
  markDirty();
75
75
  }
76
76
  function aggregateFromComponentSpans() {
77
- const componentSpans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "component");
77
+ const componentSpans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "render" || span.type === "component");
78
78
  const allSpansByUid = /* @__PURE__ */ new Map();
79
79
  const postResetSpansByUid = /* @__PURE__ */ new Map();
80
80
  for (const span of componentSpans) {
@@ -96,7 +96,8 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
96
96
  const allSpans = (allSpansByUid.get(uid) ?? []).sort((a, b) => a.startTime - b.startTime);
97
97
  const postResetSpans = (postResetSpansByUid.get(uid) ?? []).sort((a, b) => a.startTime - b.startTime);
98
98
  const timeline = postResetSpans.slice(-MAX_TIMELINE).map((span) => {
99
- const lifecycle = span.metadata?.lifecycle === "mounted" ? "mount" : "update";
99
+ const isMountLifecycle = span.metadata?.lifecycle === "render:mount" || span.metadata?.lifecycle === "mounted";
100
+ const lifecycle = isMountLifecycle ? "mount" : "update";
100
101
  const routeValue = span.metadata?.route;
101
102
  const route = typeof routeValue === "string" && routeValue.length > 0 ? routeValue : entry.route;
102
103
  return {
@@ -106,8 +107,9 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
106
107
  route
107
108
  };
108
109
  });
109
- const mountCount = allSpans.filter((span) => span.metadata?.lifecycle === "mounted").length;
110
- const rerenders = postResetSpans.filter((span) => span.metadata?.lifecycle !== "mounted").length;
110
+ const isMountSpan = (span) => span.metadata?.lifecycle === "render:mount" || span.metadata?.lifecycle === "mounted";
111
+ const mountCount = allSpans.filter(isMountSpan).length;
112
+ const rerenders = postResetSpans.filter((span) => !isMountSpan(span)).length;
111
113
  const totalMs = postResetSpans.reduce((sum, span) => sum + (span.durationMs ?? 0), 0);
112
114
  const eventsCount = Math.max(postResetSpans.length, 1);
113
115
  entry.mountCount = mountCount;
@@ -4,6 +4,6 @@ interface AsyncDataMeta {
4
4
  line: number;
5
5
  originalFn?: string;
6
6
  }
7
- type AnyFn = (...args: any[]) => any;
7
+ type AnyFn = (...args: unknown[]) => unknown;
8
8
  export declare function useTracedAsyncData<TFn extends AnyFn>(originalFn: TFn, args: unknown[], handlerIndex: number, key: unknown, meta: AsyncDataMeta): ReturnType<TFn>;
9
9
  export {};
@@ -1,2 +1,8 @@
1
1
  import type { NuxtApp } from '#app';
2
- export declare function setupFetchInstrumentation(nuxtApp: NuxtApp): void;
2
+ import type { FetchEntry } from '../composables/fetch-registry.js';
3
+ type FetchRegistry = {
4
+ register: (entry: FetchEntry) => void;
5
+ update: (id: string, patch: Partial<FetchEntry>) => void;
6
+ };
7
+ export declare function setupFetchInstrumentation(nuxtApp: NuxtApp, fetchRegistry?: FetchRegistry): void;
8
+ export {};
@@ -27,7 +27,7 @@ function resolveErrorStatus(error) {
27
27
  return target?.response?.status ?? target?.statusCode ?? target?.status;
28
28
  }
29
29
  const WRAPPED_FETCH_FLAG = "__observatory_wrapped_fetch__";
30
- export function setupFetchInstrumentation(nuxtApp) {
30
+ export function setupFetchInstrumentation(nuxtApp, fetchRegistry) {
31
31
  const original = nuxtApp.$fetch;
32
32
  if (!original) {
33
33
  return;
@@ -39,6 +39,7 @@ export function setupFetchInstrumentation(nuxtApp) {
39
39
  const url = resolveUrl(request);
40
40
  const method = resolveMethod(request, options);
41
41
  const startedAt = performance.now();
42
+ const entryId = `$fetch::${Date.now()}::${Math.random().toString(36).slice(2, 7)}`;
42
43
  const span = startSpan({
43
44
  name: "$fetch",
44
45
  type: "fetch",
@@ -49,6 +50,15 @@ export function setupFetchInstrumentation(nuxtApp) {
49
50
  status: "pending"
50
51
  }
51
52
  });
53
+ fetchRegistry?.register({
54
+ id: entryId,
55
+ key: url,
56
+ url,
57
+ status: "pending",
58
+ origin: "csr",
59
+ startTime: startedAt,
60
+ cached: false
61
+ });
52
62
  return Promise.resolve(original(request, options)).then((result) => {
53
63
  const durationMs = Math.max(performance.now() - startedAt, 0);
54
64
  span.end({
@@ -61,6 +71,11 @@ export function setupFetchInstrumentation(nuxtApp) {
61
71
  durationMs: Math.round(durationMs * 10) / 10
62
72
  }
63
73
  });
74
+ fetchRegistry?.update(entryId, {
75
+ status: "ok",
76
+ endTime: performance.now(),
77
+ ms: Math.round(durationMs * 10) / 10
78
+ });
64
79
  return result;
65
80
  }).catch((error) => {
66
81
  const durationMs = Math.max(performance.now() - startedAt, 0);
@@ -76,6 +91,12 @@ export function setupFetchInstrumentation(nuxtApp) {
76
91
  durationMs: Math.round(durationMs * 10) / 10
77
92
  }
78
93
  });
94
+ fetchRegistry?.update(entryId, {
95
+ status: "error",
96
+ endTime: performance.now(),
97
+ ms: Math.round(durationMs * 10) / 10,
98
+ error
99
+ });
79
100
  throw error;
80
101
  });
81
102
  });
@@ -3,11 +3,13 @@ import { nextTick } from "vue";
3
3
  import { setupComposableRegistry } from "./composables/composable-registry.js";
4
4
  import { setupFetchRegistry } from "./composables/fetch-registry.js";
5
5
  import { setupProvideInjectRegistry } from "./composables/provide-inject-registry.js";
6
+ import { setupPiniaStoreRegistry } from "./composables/pinia-store-registry.js";
6
7
  import { setupRenderRegistry } from "./composables/render-registry.js";
7
8
  import { setupTransitionRegistry } from "./composables/transition-registry.js";
8
9
  import { setupComponentInstrumentation } from "./instrumentation/component.js";
9
10
  import { setupFetchInstrumentation } from "./instrumentation/fetch.js";
10
11
  import { setupRouteInstrumentation } from "./instrumentation/route.js";
12
+ import { injectTestBridge } from "./test-bridge.js";
11
13
  import { traceStore } from "./tracing/traceStore.js";
12
14
  export default defineNuxtPlugin(() => {
13
15
  if (!import.meta.dev) {
@@ -37,6 +39,13 @@ export default defineNuxtPlugin(() => {
37
39
  if (config.composableTracker) {
38
40
  registries.composable = setupComposableRegistry();
39
41
  }
42
+ if (config.piniaTracker) {
43
+ registries.pinia = setupPiniaStoreRegistry({
44
+ pinia: nuxtApp.$pinia,
45
+ nuxtPayload: nuxtApp.payload,
46
+ maxTimeline: config.maxPiniaTimeline
47
+ });
48
+ }
40
49
  if (config.renderHeatmap) {
41
50
  registries.render = setupRenderRegistry(nuxtApp, {
42
51
  isHydrating: () => (nuxtApp.isHydrating ?? false) && nuxtApp.payload?.serverRendered === true
@@ -91,17 +100,26 @@ export default defineNuxtPlugin(() => {
91
100
  if (import.meta.client) {
92
101
  if (config.traceViewer) {
93
102
  setupComponentInstrumentation(nuxtApp);
94
- setupFetchInstrumentation(nuxtApp);
103
+ setupFetchInstrumentation(nuxtApp, registries.fetch);
95
104
  mergeSsrSpans();
105
+ } else if (config.fetchDashboard) {
106
+ setupFetchInstrumentation(nuxtApp, registries.fetch);
96
107
  }
97
108
  delete window.__observatory__;
98
109
  window.__observatory__ = registries;
110
+ injectTestBridge();
99
111
  const composableRegistry = registries.composable;
112
+ const piniaRegistry = registries.pinia;
100
113
  if (composableRegistry && composableRegistry.onComposableChange) {
101
114
  composableRegistry.onComposableChange(() => {
102
115
  broadcastAll("composable:onChange");
103
116
  });
104
117
  }
118
+ if (piniaRegistry?.onChange) {
119
+ piniaRegistry.onChange(() => {
120
+ broadcastAll("pinia:onChange");
121
+ });
122
+ }
105
123
  import.meta.hot?.on("observatory:command", (rawPayload) => {
106
124
  if (!rawPayload || typeof rawPayload !== "object") {
107
125
  return;
@@ -135,10 +153,24 @@ export default defineNuxtPlugin(() => {
135
153
  if (payload.cmd === "edit-composable") {
136
154
  debugLog("received command: edit-composable", { id: payload.id, key: payload.key });
137
155
  composableRegistry?.editValue(payload.id, payload.key, payload.value);
156
+ return;
157
+ }
158
+ if (payload.cmd === "clear-pinia") {
159
+ debugLog("received command: clear-pinia");
160
+ piniaRegistry?.clear();
161
+ broadcastAll("command:clear-pinia");
162
+ return;
163
+ }
164
+ if (payload.cmd === "edit-pinia") {
165
+ debugLog("received command: edit-pinia", { storeId: payload.storeId, path: payload.path });
166
+ piniaRegistry?.editState(payload.storeId, payload.path, payload.value);
167
+ broadcastAll("command:edit-pinia");
138
168
  }
139
169
  });
140
170
  nuxtApp.hook("app:beforeUnmount", () => {
141
171
  import.meta.hot?.off("observatory:command");
172
+ const pinia = registries.pinia;
173
+ pinia?.teardown?.();
142
174
  if (heartbeatId !== null) {
143
175
  window.clearInterval(heartbeatId);
144
176
  heartbeatId = null;
@@ -231,6 +263,7 @@ export default defineNuxtPlugin(() => {
231
263
  reason,
232
264
  fetch: Array.isArray(snapshot.fetch) ? snapshot.fetch.length : 0,
233
265
  composables: Array.isArray(snapshot.composables) ? snapshot.composables.length : 0,
266
+ piniaStores: Array.isArray(snapshot.piniaStores) ? snapshot.piniaStores.length : 0,
234
267
  renders: Array.isArray(snapshot.renders) ? snapshot.renders.length : 0,
235
268
  transitions: Array.isArray(snapshot.transitions) ? snapshot.transitions.length : 0,
236
269
  traces: Array.isArray(snapshot.traces) ? snapshot.traces.length : 0
@@ -256,6 +289,7 @@ export default defineNuxtPlugin(() => {
256
289
  { key: "fetch", fallback: [] },
257
290
  { key: "provideInject", fallback: { provides: [], injects: [] } },
258
291
  { key: "composable", fallback: [] },
292
+ { key: "pinia", fallback: [] },
259
293
  { key: "render", fallback: {} },
260
294
  { key: "transition", fallback: {} }
261
295
  ];
@@ -263,7 +297,8 @@ export default defineNuxtPlugin(() => {
263
297
  for (const { key, fallback } of trackerDefs) {
264
298
  const reg = registries[key];
265
299
  const hasGetSnapshot = reg && typeof reg.getSnapshot === "function";
266
- snapshot[key === "composable" ? "composables" : key === "render" ? "renders" : key === "transition" ? "transitions" : key] = hasGetSnapshot ? safeParse(reg.getSnapshot(), fallback) : fallback;
300
+ const snapshotKey = key === "composable" ? "composables" : key === "pinia" ? "piniaStores" : key === "render" ? "renders" : key === "transition" ? "transitions" : key;
301
+ snapshot[snapshotKey] = hasGetSnapshot ? safeParse(reg.getSnapshot(), fallback) : fallback;
267
302
  }
268
303
  snapshot.traces = config.traceViewer ? traceStore.getAllTraces().map((trace) => ({
269
304
  id: trace.id,
@@ -290,7 +325,9 @@ export default defineNuxtPlugin(() => {
290
325
  fetchDashboard: !!registries.fetch,
291
326
  provideInjectGraph: !!registries.provideInject,
292
327
  composableTracker: !!registries.composable,
328
+ piniaTracker: !!registries.pinia,
293
329
  composableNavigationMode,
330
+ fetchPageSize: typeof config.fetchPageSize === "number" ? config.fetchPageSize : 20,
294
331
  renderHeatmap: !!registries.render,
295
332
  transitionTracker: !!registries.transition,
296
333
  traceViewer: !!config.traceViewer
@@ -0,0 +1,18 @@
1
+ import type { ObservatoryTestAPI } from '../../tests/verification/types/observatory.types.js';
2
+ interface VueApp {
3
+ _context?: {
4
+ app?: {
5
+ _component?: unknown;
6
+ };
7
+ };
8
+ }
9
+ declare global {
10
+ interface Window {
11
+ __NUXT__?: {
12
+ vueApp?: VueApp;
13
+ };
14
+ __OBSERVATORY_TEST_BRIDGE?: ObservatoryTestAPI;
15
+ }
16
+ }
17
+ export declare function injectTestBridge(): void;
18
+ export {};
@@ -0,0 +1,100 @@
1
+ function walkComponentTree(instance, counts) {
2
+ if (!instance) {
3
+ return;
4
+ }
5
+ const componentName = instance.type?.name ?? instance.type?.__name ?? "Anonymous";
6
+ if (instance.ctx?.__observatoryMountCount) {
7
+ const currentCount = counts.componentMounts[componentName] ?? 0;
8
+ counts.componentMounts[componentName] = currentCount + instance.ctx.__observatoryMountCount;
9
+ }
10
+ if (instance.subTree?.children) {
11
+ instance.subTree.children.forEach((child) => walkComponentTree(child, counts));
12
+ }
13
+ }
14
+ export function injectTestBridge() {
15
+ if (!import.meta.dev || typeof window === "undefined") {
16
+ return;
17
+ }
18
+ const bridge = {
19
+ async getTraces() {
20
+ const { traceStore } = await import("./tracing/traceStore.js");
21
+ return Array.from(traceStore.entries());
22
+ },
23
+ async getHeatmapData() {
24
+ const { renderRegistry } = await import("./composables/render-registry.js");
25
+ return renderRegistry.getData();
26
+ },
27
+ async getComposableEntries() {
28
+ const { composableRegistry } = await import("./composables/composable-registry.js");
29
+ return composableRegistry.getEntries();
30
+ },
31
+ async getFetchEntries() {
32
+ const { fetchRegistry } = await import("./composables/fetch-registry.js");
33
+ return fetchRegistry.getEntries();
34
+ },
35
+ async getProvideInjectGraph() {
36
+ const { provideInjectRegistry } = await import("./composables/provide-inject-registry.js");
37
+ return provideInjectRegistry.getGraph();
38
+ },
39
+ async getTransitionEntries() {
40
+ const { transitionRegistry } = await import("./composables/transition-registry.js");
41
+ return transitionRegistry.getEntries();
42
+ },
43
+ async getPiniaStores() {
44
+ const observatory = window.__observatory__;
45
+ const registry = observatory?.pinia;
46
+ if (!registry?.getAll) {
47
+ return [];
48
+ }
49
+ return registry.getAll();
50
+ },
51
+ async getInternalCounts() {
52
+ const counts = {
53
+ componentMounts: {},
54
+ renderOperations: {},
55
+ fetchOperations: {}
56
+ };
57
+ const vueApp = window.__NUXT__?.vueApp;
58
+ if (!vueApp) {
59
+ return counts;
60
+ }
61
+ walkComponentTree(vueApp._context?.app?._component, counts);
62
+ return counts;
63
+ },
64
+ async clearAllData() {
65
+ const { traceStore } = await import("./tracing/traceStore.js");
66
+ const { renderRegistry } = await import("./composables/render-registry.js");
67
+ const { composableRegistry } = await import("./composables/composable-registry.js");
68
+ const { fetchRegistry } = await import("./composables/fetch-registry.js");
69
+ const observatory = window.__observatory__;
70
+ const piniaRegistry = observatory?.pinia;
71
+ traceStore.clear();
72
+ renderRegistry?.clear?.();
73
+ composableRegistry?.clear?.();
74
+ fetchRegistry?.clear?.();
75
+ if (piniaRegistry?.clear) {
76
+ piniaRegistry.clear();
77
+ }
78
+ },
79
+ async startRecording() {
80
+ const { traceStore } = await import("./tracing/traceStore.js");
81
+ traceStore.startRecording();
82
+ },
83
+ async stopRecording() {
84
+ const { traceStore } = await import("./tracing/traceStore.js");
85
+ traceStore.stopRecording();
86
+ },
87
+ async exportSnapshot() {
88
+ const snapshot = {
89
+ traces: await this.getTraces(),
90
+ heatmap: await this.getHeatmapData(),
91
+ composables: await this.getComposableEntries(),
92
+ fetches: await this.getFetchEntries(),
93
+ transitions: await this.getTransitionEntries(),
94
+ piniaStores: await this.getPiniaStores()
95
+ };
96
+ return JSON.stringify(snapshot, null, 2);
97
+ }
98
+ };
99
+ window.__OBSERVATORY_TEST_BRIDGE = bridge;
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-devtools-observatory",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Nuxt DevTools: useFetch Dashboard, provide/inject Graph, Composable Tracker, Render Heatmap",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,14 +49,21 @@
49
49
  "build": "npm run build:client && nuxt-module-build build",
50
50
  "prepack": "npm run build",
51
51
  "lint": "eslint .",
52
- "format": "prettier --write '**/*.{ts,vue,js,json}' --ignore-path .prettierignore && stylelint '**/*.{css,vue}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --fix && eslint --fix '**/*.{ts,vue,js}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --ignore-pattern 'docs/dist/**'",
52
+ "format": "prettier --write '**/*.{ts,vue,js,json}' --ignore-path .prettierignore && stylelint '**/*.{css,vue}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --ignore-pattern 'docs/dist/**' --fix && eslint --fix '**/*.{ts,vue,js}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --ignore-pattern 'docs/dist/**'",
53
53
  "typecheck": "vue-tsc --noEmit",
54
54
  "test": "vitest run",
55
55
  "test:watch": "vitest",
56
56
  "test:coverage": "vitest run --coverage",
57
57
  "test:screenshots": "playwright test scripts/playwright/screenshot-trackers.spec.ts",
58
58
  "test:e2e": "playwright test scripts/playwright/playground-pages.spec.ts",
59
- "capture:screenshots": "node scripts/playwright/capture-observatory-screenshots.cjs"
59
+ "capture:screenshots": "node scripts/playwright/capture-observatory-screenshots.cjs",
60
+ "verify": "tsx scripts/run-verification.ts",
61
+ "verify:debug": "playwright test --debug",
62
+ "verify:ui": "playwright test --ui",
63
+ "verify:trace": "playwright test --trace on",
64
+ "verify:report": "playwright show-report",
65
+ "test:accuracy": "vitest run tests/verification/accuracy.test.ts",
66
+ "type-check": "tsc --noEmit --project tests/verification/tsconfig.json"
60
67
  },
61
68
  "dependencies": {
62
69
  "@babel/generator": "^7.29.1",
@@ -73,11 +80,13 @@
73
80
  "@nuxt/schema": "^3.0.0",
74
81
  "@pinia/nuxt": "^0.11.3",
75
82
  "@playwright/test": "^1.58.2",
83
+ "@tanstack/vue-virtual": "^3.13.12",
76
84
  "@types/babel__generator": "^7.27.0",
77
85
  "@types/babel__traverse": "^7.28.0",
78
86
  "@types/node": "^25.5.0",
79
87
  "@vitejs/plugin-vue": "^6.0.0",
80
88
  "@vitest/coverage-v8": "^4.1.0",
89
+ "@vitest/ui": "^4.1.4",
81
90
  "concurrently": "^9.2.1",
82
91
  "eslint": "^9.39.2",
83
92
  "eslint-config-prettier": "^10.1.8",
@@ -92,10 +101,12 @@
92
101
  "playwright": "^1.58.2",
93
102
  "postcss-html": "^1.8.1",
94
103
  "prettier": "^3.8.1",
104
+ "sinon": "^21.1.2",
95
105
  "sirv": "^3.0.2",
96
106
  "stylelint": "^17.4.0",
97
107
  "stylelint-config-standard": "^40.0.0",
98
108
  "stylelint-config-standard-vue": "^1.0.0",
109
+ "tsx": "^4.21.0",
99
110
  "typescript": "^5.9.3",
100
111
  "typescript-eslint": "^8.54.0",
101
112
  "unbuild": "^3.6.1",