nuxt-devtools-observatory 0.1.32 → 0.1.33

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 (31) hide show
  1. package/README.md +5 -0
  2. package/client/.env.example +1 -0
  3. package/client/dist/assets/index-BqKYgjVB.js +20 -0
  4. package/client/dist/assets/index-bs1JBJ2u.css +1 -0
  5. package/client/dist/index.html +2 -2
  6. package/client/src/App.vue +4 -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/views/ComposableTracker.vue +212 -71
  14. package/client/src/views/FetchDashboard.vue +181 -16
  15. package/client/src/views/ProvideInjectGraph.vue +41 -18
  16. package/client/src/views/RenderHeatmap.vue +329 -75
  17. package/client/src/views/TraceViewer.vue +190 -20
  18. package/client/src/views/TransitionTimeline.vue +112 -19
  19. package/dist/module.d.mts +5 -0
  20. package/dist/module.json +1 -1
  21. package/dist/module.mjs +11 -22
  22. package/dist/runtime/composables/render-registry.js +6 -4
  23. package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
  24. package/dist/runtime/instrumentation/fetch.d.ts +7 -1
  25. package/dist/runtime/instrumentation/fetch.js +22 -1
  26. package/dist/runtime/plugin.js +4 -1
  27. package/dist/runtime/test-bridge.d.ts +18 -0
  28. package/dist/runtime/test-bridge.js +86 -0
  29. package/package.json +14 -3
  30. package/client/dist/assets/index-5Wl1XYRH.js +0 -17
  31. package/client/dist/assets/index-DT_QUiIh.css +0 -1
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addVitePlugin, addPlugin, addServerPlugin } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addVitePlugin, addImports, addPlugin, addServerPlugin } from '@nuxt/kit';
2
2
  import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
3
3
  import sirv from 'sirv';
4
4
  import { parse } from '@babel/parser';
@@ -209,10 +209,6 @@ function fetchInstrumentPlugin() {
209
209
  plugins: ["typescript"]
210
210
  });
211
211
  let modified = false;
212
- let needsFetchCallHelper = false;
213
- let needsTracedAsyncDataHelper = false;
214
- const hasFetchCallImport = scriptCode.includes("__devFetchCall");
215
- const hasTracedAsyncDataImport = scriptCode.includes("useTracedAsyncData");
216
212
  traverse$1(ast, {
217
213
  CallExpression(path) {
218
214
  if (path.node.__observatoryTransformed) {
@@ -284,7 +280,6 @@ function fetchInstrumentPlugin() {
284
280
  ]);
285
281
  newCall.__observatoryTransformed = true;
286
282
  path.replaceWith(newCall);
287
- needsTracedAsyncDataHelper = true;
288
283
  modified = true;
289
284
  } else {
290
285
  const newCall = t.callExpression(t.identifier("__devFetchCall"), [
@@ -294,7 +289,6 @@ function fetchInstrumentPlugin() {
294
289
  meta
295
290
  ]);
296
291
  newCall.__observatoryTransformed = true;
297
- needsFetchCallHelper = true;
298
292
  path.replaceWith(newCall);
299
293
  modified = true;
300
294
  }
@@ -303,18 +297,12 @@ function fetchInstrumentPlugin() {
303
297
  if (!modified) {
304
298
  return null;
305
299
  }
306
- const fetchImportNames = [needsFetchCallHelper && !hasFetchCallImport ? "__devFetchCall" : ""].filter(Boolean);
307
- const fetchImportStatement = fetchImportNames.length ? `import { ${fetchImportNames.join(", ")} } from 'nuxt-devtools-observatory/runtime/fetch-registry';
308
- ` : "";
309
- const asyncDataImportStatement = needsTracedAsyncDataHelper && !hasTracedAsyncDataImport ? `import { useTracedAsyncData } from 'nuxt-devtools-observatory/runtime/async-data-instrumentation';
310
- ` : "";
311
- const importStatement = fetchImportStatement + asyncDataImportStatement;
312
300
  const output = generate$1(ast, { retainLines: true }, scriptCode);
313
301
  let finalCode;
314
302
  if (isVue) {
315
- finalCode = code.slice(0, scriptStart) + importStatement + output.code + code.slice(scriptStart + scriptCode.length);
303
+ finalCode = code.slice(0, scriptStart) + output.code + code.slice(scriptStart + scriptCode.length);
316
304
  } else {
317
- finalCode = importStatement + output.code;
305
+ finalCode = output.code;
318
306
  }
319
307
  return {
320
308
  code: finalCode,
@@ -537,6 +525,7 @@ const defaults = {
537
525
  heatmapThresholdTime: process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600,
538
526
  maxFetchEntries: process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200,
539
527
  maxPayloadBytes: process.env.OBSERVATORY_MAX_PAYLOAD_BYTES ? Number(process.env.OBSERVATORY_MAX_PAYLOAD_BYTES) : 1e4,
528
+ fetchPageSize: process.env.OBSERVATORY_FETCH_PAGE_SIZE ? Number(process.env.OBSERVATORY_FETCH_PAGE_SIZE) : 20,
540
529
  maxTransitions: process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500,
541
530
  maxComposableHistory: process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50,
542
531
  maxComposableEntries: process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300,
@@ -576,6 +565,7 @@ const module$1 = defineNuxtModule({
576
565
  heatmapThresholdTime: options.heatmapThresholdTime ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600),
577
566
  maxFetchEntries: options.maxFetchEntries ?? (process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200),
578
567
  maxPayloadBytes: options.maxPayloadBytes ?? (process.env.OBSERVATORY_MAX_PAYLOAD_BYTES ? Number(process.env.OBSERVATORY_MAX_PAYLOAD_BYTES) : 1e4),
568
+ fetchPageSize: options.fetchPageSize ?? (process.env.OBSERVATORY_FETCH_PAGE_SIZE ? Number(process.env.OBSERVATORY_FETCH_PAGE_SIZE) : 20),
579
569
  maxTransitions: options.maxTransitions ?? (process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500),
580
570
  maxComposableHistory: options.maxComposableHistory ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50),
581
571
  maxComposableEntries: options.maxComposableEntries ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300),
@@ -599,6 +589,10 @@ const module$1 = defineNuxtModule({
599
589
  const vitePluginScope = resolved.instrumentServer ? { server: true, client: true } : { server: false, client: true };
600
590
  if (resolved.fetchDashboard) {
601
591
  addVitePlugin(fetchInstrumentPlugin(), vitePluginScope);
592
+ addImports([
593
+ { name: "__devFetchCall", from: resolver.resolve("./runtime/composables/fetch-registry") },
594
+ { name: "useTracedAsyncData", from: resolver.resolve("./runtime/instrumentation/asyncData") }
595
+ ]);
602
596
  }
603
597
  if (resolved.provideInjectGraph) {
604
598
  addVitePlugin(provideInjectPlugin(), vitePluginScope);
@@ -634,6 +628,7 @@ const module$1 = defineNuxtModule({
634
628
  provideInjectGraph: !!resolved.provideInjectGraph,
635
629
  composableTracker: !!resolved.composableTracker,
636
630
  composableNavigationMode: resolved.composableNavigationMode,
631
+ fetchPageSize: resolved.fetchPageSize,
637
632
  renderHeatmap: !!resolved.renderHeatmap,
638
633
  transitionTracker: !!resolved.transitionTracker,
639
634
  traceViewer: !!resolved.traceViewer
@@ -691,13 +686,6 @@ const module$1 = defineNuxtModule({
691
686
  );
692
687
  rpc.broadcast.onSnapshot.asEvent(latestSnapshot);
693
688
  }, nuxt);
694
- nuxt.hook("render:response", (response, { url }) => {
695
- if (url.startsWith("/trackers") || url === "/" || url.startsWith("/index.html")) {
696
- const configScript = `<script>window.__observatoryConfig = ${JSON.stringify(nuxt.options.runtimeConfig.public.observatory)};<\/script>`;
697
- response.body = response.body.replace("<head>", `<head>
698
- ${configScript}`);
699
- }
700
- });
701
689
  nuxt.hook("devtools:customTabs", (tabs) => {
702
690
  if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
703
691
  tabs.push({
@@ -718,6 +706,7 @@ ${configScript}`);
718
706
  traceViewer: resolved.traceViewer,
719
707
  maxFetchEntries: resolved.maxFetchEntries,
720
708
  maxPayloadBytes: resolved.maxPayloadBytes,
709
+ fetchPageSize: resolved.fetchPageSize,
721
710
  maxTransitions: resolved.maxTransitions,
722
711
  maxComposableHistory: resolved.maxComposableHistory,
723
712
  maxComposableEntries: resolved.maxComposableEntries,
@@ -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
  });
@@ -91,8 +91,10 @@ export default defineNuxtPlugin(() => {
91
91
  if (import.meta.client) {
92
92
  if (config.traceViewer) {
93
93
  setupComponentInstrumentation(nuxtApp);
94
- setupFetchInstrumentation(nuxtApp);
94
+ setupFetchInstrumentation(nuxtApp, registries.fetch);
95
95
  mergeSsrSpans();
96
+ } else if (config.fetchDashboard) {
97
+ setupFetchInstrumentation(nuxtApp, registries.fetch);
96
98
  }
97
99
  delete window.__observatory__;
98
100
  window.__observatory__ = registries;
@@ -291,6 +293,7 @@ export default defineNuxtPlugin(() => {
291
293
  provideInjectGraph: !!registries.provideInject,
292
294
  composableTracker: !!registries.composable,
293
295
  composableNavigationMode,
296
+ fetchPageSize: typeof config.fetchPageSize === "number" ? config.fetchPageSize : 20,
294
297
  renderHeatmap: !!registries.render,
295
298
  transitionTracker: !!registries.transition,
296
299
  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,86 @@
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 getInternalCounts() {
44
+ const counts = {
45
+ componentMounts: {},
46
+ renderOperations: {},
47
+ fetchOperations: {}
48
+ };
49
+ const vueApp = window.__NUXT__?.vueApp;
50
+ if (!vueApp) {
51
+ return counts;
52
+ }
53
+ walkComponentTree(vueApp._context?.app?._component, counts);
54
+ return counts;
55
+ },
56
+ async clearAllData() {
57
+ const { traceStore } = await import("./tracing/traceStore.js");
58
+ const { renderRegistry } = await import("./composables/render-registry.js");
59
+ const { composableRegistry } = await import("./composables/composable-registry.js");
60
+ const { fetchRegistry } = await import("./composables/fetch-registry.js");
61
+ traceStore.clear();
62
+ renderRegistry.clear();
63
+ composableRegistry.clear();
64
+ fetchRegistry.clear();
65
+ },
66
+ async startRecording() {
67
+ const { traceStore } = await import("./tracing/traceStore.js");
68
+ traceStore.startRecording();
69
+ },
70
+ async stopRecording() {
71
+ const { traceStore } = await import("./tracing/traceStore.js");
72
+ traceStore.stopRecording();
73
+ },
74
+ async exportSnapshot() {
75
+ const snapshot = {
76
+ traces: await this.getTraces(),
77
+ heatmap: await this.getHeatmapData(),
78
+ composables: await this.getComposableEntries(),
79
+ fetches: await this.getFetchEntries(),
80
+ transitions: await this.getTransitionEntries()
81
+ };
82
+ return JSON.stringify(snapshot, null, 2);
83
+ }
84
+ };
85
+ window.__OBSERVATORY_TEST_BRIDGE = bridge;
86
+ }
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.33",
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",