@flowselections/google-maps-api-installatie-techniek 1.0.5 → 1.1.1

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.
@@ -0,0 +1,182 @@
1
+ // Tests met een nep-database en nep-Google: er worden geen echte aanroepen gedaan.
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ const google = vi.hoisted(() => ({
4
+ geocodeAddress: vi.fn(),
5
+ computeRouteMatrix: vi.fn(),
6
+ }));
7
+ vi.mock("./google-maps.server", () => google);
8
+ let state;
9
+ function makeBuilder(table) {
10
+ const ops = [];
11
+ const builder = new Proxy({}, {
12
+ get(_t, prop) {
13
+ if (prop === "then") {
14
+ return (resolve, reject) => {
15
+ state.log.push({ table, ops });
16
+ Promise.resolve(execute(table, ops)).then(resolve, reject);
17
+ };
18
+ }
19
+ return (...args) => {
20
+ ops.push([prop, args]);
21
+ return builder;
22
+ };
23
+ },
24
+ });
25
+ return builder;
26
+ }
27
+ function arg(ops, name) {
28
+ return ops.filter(([n]) => n === name).map(([, a]) => a);
29
+ }
30
+ function execute(table, ops) {
31
+ const kind = ops[0][0];
32
+ if (table === "maps_settings")
33
+ return { data: { id: "s1", ...state.settings }, error: null };
34
+ if (table === "maps_usage_log")
35
+ return { data: [], error: null };
36
+ if (table === "maps_addresses") {
37
+ if (kind === "select") {
38
+ const hash = arg(ops, "eq")[0]?.[1];
39
+ return { data: state.addresses.find((a) => a.address_hash === hash) ?? null, error: null };
40
+ }
41
+ if (kind === "insert") {
42
+ const row = { id: `a${state.addresses.length + 1}`, ...ops[0][1][0] };
43
+ state.addresses.push(row);
44
+ return { data: { id: row.id }, error: null };
45
+ }
46
+ if (kind === "update") {
47
+ const id = arg(ops, "eq")[0]?.[1];
48
+ const row = state.addresses.find((a) => a.id === id);
49
+ if (row)
50
+ Object.assign(row, ops[0][1][0]);
51
+ return { data: null, error: null };
52
+ }
53
+ }
54
+ if (table === "maps_request_queue") {
55
+ if (kind === "select") {
56
+ const max = arg(ops, "lt")[0]?.[1];
57
+ return { data: state.queue.filter((q) => q.status === "pending" && q.attempts < max).map((q) => ({ ...q })), error: null };
58
+ }
59
+ if (kind === "update") {
60
+ const patch = ops[0][1][0];
61
+ const ids = arg(ops, "in")[0]?.[1] ?? [arg(ops, "eq")[0]?.[1]];
62
+ for (const q of state.queue)
63
+ if (ids.includes(q.id))
64
+ Object.assign(q, patch);
65
+ return { data: null, error: null };
66
+ }
67
+ }
68
+ return { data: null, error: null };
69
+ }
70
+ const fakeDb = {
71
+ from: (table) => makeBuilder(table),
72
+ rpc: async (name, params) => {
73
+ state.rpcCalls.push(name);
74
+ if (name === "maps_claim_queue_items") {
75
+ // Simuleert FOR UPDATE SKIP LOCKED: atomair pending → processing.
76
+ const picked = state.queue
77
+ .filter((q) => q.status === "pending" && q.attempts < params.p_max_attempts)
78
+ .slice(0, params.p_limit);
79
+ for (const q of picked)
80
+ q.status = "processing";
81
+ return { data: picked.map((q) => ({ ...q })), error: null };
82
+ }
83
+ if (name === "maps_try_consume_call") {
84
+ const cur = state.counter[params.p_day] ?? params.p_initial;
85
+ if (cur >= params.p_limit) {
86
+ state.counter[params.p_day] = cur;
87
+ return { data: null, error: null };
88
+ }
89
+ state.counter[params.p_day] = cur + 1;
90
+ return { data: cur + 1, error: null };
91
+ }
92
+ return { data: null, error: null };
93
+ },
94
+ };
95
+ vi.mock("@supabase/supabase-js", () => ({ createClient: () => fakeDb }));
96
+ const { processMapsQueueBatch } = await import("./maps-processor.server");
97
+ function geocodeItems(n) {
98
+ return Array.from({ length: n }, (_, i) => ({
99
+ id: `q${i}`,
100
+ kind: "geocode",
101
+ raw_origin: `Straat ${i}, Plaats`,
102
+ raw_destination: null,
103
+ mode: "drive",
104
+ status: "pending",
105
+ attempts: 0,
106
+ }));
107
+ }
108
+ beforeEach(() => {
109
+ process.env.SUPABASE_URL = "http://fake";
110
+ process.env.SERVICE_ROLE_KEY = "fake";
111
+ google.geocodeAddress.mockReset();
112
+ google.computeRouteMatrix.mockReset();
113
+ google.geocodeAddress.mockImplementation(async (raw) => {
114
+ await new Promise((r) => setTimeout(r, 1));
115
+ return { formattedAddress: raw, lat: 52, lng: 5, placeId: "p" };
116
+ });
117
+ });
118
+ function reset(settings, items = geocodeItems(3)) {
119
+ state = { settings, queue: items, addresses: [], counter: {}, log: [], rpcCalls: [] };
120
+ }
121
+ describe("processMapsQueueBatch — schakelaars uit = huidig gedrag", () => {
122
+ it("oude instellingen (zonder nieuwe kolommen) en nieuwe kolommen op uit geven exact hetzelfde", async () => {
123
+ reset({ daily_call_limit: 2000, cache_ttl_days: 30 });
124
+ const oud = await processMapsQueueBatch();
125
+ const oudOps = JSON.stringify(state.log.map((l) => [l.table, l.ops.map(([n]) => n)]));
126
+ const oudQueue = state.queue.map((q) => [q.id, q.status, q.attempts]);
127
+ expect(state.rpcCalls).toEqual([]);
128
+ reset({
129
+ daily_call_limit: 2000, cache_ttl_days: 30,
130
+ claim_via_rpc_enabled: false, per_call_limit_enabled: false,
131
+ max_attempts_enabled: false, stop_bij_sleutelfout_enabled: false,
132
+ max_attempts: 9, timezone: "Europe/Amsterdam",
133
+ });
134
+ const nieuw = await processMapsQueueBatch();
135
+ expect(nieuw).toEqual(oud);
136
+ expect(nieuw).toEqual({ processed: 3, geocoded: 3, travelTimes: 0, failed: 0, skipped: null });
137
+ expect(JSON.stringify(state.log.map((l) => [l.table, l.ops.map(([n]) => n)]))).toBe(oudOps);
138
+ expect(state.queue.map((q) => [q.id, q.status, q.attempts])).toEqual(oudQueue);
139
+ expect(state.rpcCalls).toEqual([]);
140
+ });
141
+ it("zonder schakelaar loopt de batch door na een sleutelfout", async () => {
142
+ reset({ daily_call_limit: 2000 });
143
+ google.geocodeAddress.mockRejectedValue(new Error("API key not valid"));
144
+ const r = await processMapsQueueBatch();
145
+ expect(google.geocodeAddress).toHaveBeenCalledTimes(3);
146
+ expect(r.failed).toBe(3);
147
+ });
148
+ });
149
+ describe("processMapsQueueBatch — nieuwe schakelaars", () => {
150
+ it("twee gelijktijdige verwerkingen roepen Google maximaal één keer per item aan", async () => {
151
+ reset({ daily_call_limit: 2000, claim_via_rpc_enabled: true }, geocodeItems(10));
152
+ await Promise.all([processMapsQueueBatch(), processMapsQueueBatch()]);
153
+ const calls = google.geocodeAddress.mock.calls.map((c) => c[0]);
154
+ expect(calls).toHaveLength(10);
155
+ expect(new Set(calls).size).toBe(10);
156
+ });
157
+ it("de daglimiet wordt nooit overschreden (limiet 2)", async () => {
158
+ reset({ daily_call_limit: 2, per_call_limit_enabled: true, claim_via_rpc_enabled: true }, geocodeItems(5));
159
+ const r = await processMapsQueueBatch();
160
+ expect(google.geocodeAddress).toHaveBeenCalledTimes(2);
161
+ expect(Object.values(state.counter)).toEqual([2]);
162
+ expect(r.skipped).toBe("Daglimiet bereikt");
163
+ // Niet-verwerkte items staan weer op pending zonder extra poging.
164
+ expect(state.queue.filter((q) => q.status === "pending" && q.attempts === 0)).toHaveLength(3);
165
+ });
166
+ it("met stop_bij_sleutelfout_enabled stopt de batch na de eerste sleutelfout", async () => {
167
+ reset({ daily_call_limit: 2000, stop_bij_sleutelfout_enabled: true });
168
+ google.geocodeAddress.mockRejectedValue(new Error("API key not valid"));
169
+ await processMapsQueueBatch();
170
+ expect(google.geocodeAddress).toHaveBeenCalledTimes(1);
171
+ expect(state.queue.filter((q) => q.status === "pending" && q.attempts === 0)).toHaveLength(2);
172
+ });
173
+ it("max_attempts uit de instellingen wordt alleen gebruikt als de schakelaar aan staat", async () => {
174
+ const items = geocodeItems(1).map((q) => ({ ...q, attempts: 3 }));
175
+ reset({ daily_call_limit: 2000, max_attempts: 5 }, items);
176
+ await processMapsQueueBatch();
177
+ expect(google.geocodeAddress).not.toHaveBeenCalled();
178
+ reset({ daily_call_limit: 2000, max_attempts: 5, max_attempts_enabled: true }, geocodeItems(1).map((q) => ({ ...q, attempts: 3 })));
179
+ await processMapsQueueBatch();
180
+ expect(google.geocodeAddress).toHaveBeenCalledTimes(1);
181
+ });
182
+ });
@@ -7,6 +7,13 @@ export interface MapsOverview {
7
7
  dailyCallLimit: number;
8
8
  allowEstimateFallback: boolean;
9
9
  provider: "free" | "google";
10
+ claimViaRpcEnabled: boolean;
11
+ perCallLimitEnabled: boolean;
12
+ maxAttemptsEnabled: boolean;
13
+ stopBijSleutelfoutEnabled: boolean;
14
+ maxAttempts: number;
15
+ timezone: string;
16
+ updatedAt: string | null;
10
17
  lastSuccessAt: string | null;
11
18
  lastErrorAt: string | null;
12
19
  lastError: string | null;
@@ -23,6 +30,29 @@ export interface MapsOverview {
23
30
  errorsMonth: number;
24
31
  estimatedCostMonth: number | null;
25
32
  }
33
+ export interface MapsErrorPage {
34
+ total: number;
35
+ page: number;
36
+ pageSize: number;
37
+ items: Array<{
38
+ id: string;
39
+ at: string;
40
+ callType: string;
41
+ kind: "quota" | "invalid_key" | "not_found" | "token" | "other";
42
+ message: string;
43
+ }>;
44
+ }
45
+ export declare const getMapsErrors: import("@tanstack/start-client-core").RequiredFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
46
+ supabase: import("../integrations/supabase/auth-middleware.js").AuthedSupabaseClient;
47
+ userId: string;
48
+ claims: import("@supabase/auth-js").JwtPayload;
49
+ }, undefined, undefined, undefined>], (input: {
50
+ page?: number;
51
+ pageSize?: number;
52
+ }) => {
53
+ page: number;
54
+ pageSize: number;
55
+ }, Promise<MapsErrorPage>>;
26
56
  export declare const getMapsOverview: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
27
57
  supabase: import("../integrations/supabase/auth-middleware.js").AuthedSupabaseClient;
28
58
  userId: string;
@@ -42,7 +72,21 @@ export declare const updateMapsSettings: import("@tanstack/start-client-core").R
42
72
  dailyCallLimit: number;
43
73
  allowEstimateFallback: boolean;
44
74
  provider: "free" | "google";
75
+ claimViaRpcEnabled?: boolean;
76
+ perCallLimitEnabled?: boolean;
77
+ maxAttemptsEnabled?: boolean;
78
+ stopBijSleutelfoutEnabled?: boolean;
79
+ maxAttempts?: number;
80
+ timezone?: string;
81
+ expectedUpdatedAt?: string | null;
45
82
  }) => {
83
+ expectedUpdatedAt: string | null;
84
+ timezone?: string | undefined;
85
+ maxAttempts?: number | undefined;
86
+ stopBijSleutelfoutEnabled?: boolean | undefined;
87
+ maxAttemptsEnabled?: boolean | undefined;
88
+ perCallLimitEnabled?: boolean | undefined;
89
+ claimViaRpcEnabled?: boolean | undefined;
46
90
  cacheTtlDays: number;
47
91
  dailyCallLimit: number;
48
92
  allowEstimateFallback: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"maps.functions.d.ts","sourceRoot":"","sources":["../../src/lib/maps.functions.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,qBAAqB,EAAE,OAAO,CAAC;QAC/B,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;QAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,GAAG,IAAI,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,eAAO,MAAM,eAAe;;;;uEAKxB,CAAC;AAEL,eAAO,MAAM,gBAAgB;;;;0GAKzB,CAAC;AAEL,eAAO,MAAM,kBAAkB;;;;8CAEL;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC7B;;;;;;;GAgBC,CAAC;AAEL,eAAO,MAAM,cAAc;;;;;;GAKvB,CAAC;AAEL,eAAO,MAAM,uBAAuB;;;;;;GAKhC,CAAC;AAEL,eAAO,MAAM,aAAa;;;;+GAKtB,CAAC;AAEL,eAAO,MAAM,kBAAkB;;;;;;;;GAK3B,CAAC;AAEL,eAAO,MAAM,kBAAkB;;;;gHAK3B,CAAC"}
1
+ {"version":3,"file":"maps.functions.d.ts","sourceRoot":"","sources":["../../src/lib/maps.functions.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,qBAAqB,EAAE,OAAO,CAAC;QAC/B,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;QAC5B,kBAAkB,EAAE,OAAO,CAAC;QAC5B,mBAAmB,EAAE,OAAO,CAAC;QAC7B,kBAAkB,EAAE,OAAO,CAAC;QAC5B,yBAAyB,EAAE,OAAO,CAAC;QACnC,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,GAAG,IAAI,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC;QAChE,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;CACJ;AAED,eAAO,MAAM,aAAa;;;;8CAEA;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE;;;0BAO1D,CAAC;AAEL,eAAO,MAAM,eAAe;;;;uEAKxB,CAAC;AAEL,eAAO,MAAM,gBAAgB;;;;0GAKzB,CAAC;AAEL,eAAO,MAAM,kBAAkB;;;;8CAEL;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;;;;;;;;;;;;;;GA6BC,CAAC;AAEL,eAAO,MAAM,cAAc;;;;;;GAKvB,CAAC;AAEL,eAAO,MAAM,uBAAuB;;;;;;GAKhC,CAAC;AAEL,eAAO,MAAM,aAAa;;;;+GAKtB,CAAC;AAEL,eAAO,MAAM,kBAAkB;;;;;;;;GAK3B,CAAC;AAEL,eAAO,MAAM,kBAAkB;;;;gHAK3B,CAAC"}
@@ -1,5 +1,15 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
2
  import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware.js";
3
+ export const getMapsErrors = createServerFn({ method: "GET" })
4
+ .middleware([requireSupabaseAuth])
5
+ .inputValidator((input) => ({
6
+ page: Math.max(0, Math.floor(Number(input?.page ?? 0)) || 0),
7
+ pageSize: Math.min(100, Math.max(1, Math.floor(Number(input?.pageSize ?? 10)) || 10)),
8
+ }))
9
+ .handler(async ({ data }) => {
10
+ const { listMapsErrors } = await import("./maps-admin.server");
11
+ return listMapsErrors(data.page, data.pageSize);
12
+ });
3
13
  export const getMapsOverview = createServerFn({ method: "GET" })
4
14
  .middleware([requireSupabaseAuth])
5
15
  .handler(async () => {
@@ -25,6 +35,19 @@ export const updateMapsSettings = createServerFn({ method: "POST" })
25
35
  dailyCallLimit: limit,
26
36
  allowEstimateFallback: Boolean(input.allowEstimateFallback),
27
37
  provider: input.provider === "google" ? "google" : "free",
38
+ ...(input.claimViaRpcEnabled !== undefined && { claimViaRpcEnabled: Boolean(input.claimViaRpcEnabled) }),
39
+ ...(input.perCallLimitEnabled !== undefined && { perCallLimitEnabled: Boolean(input.perCallLimitEnabled) }),
40
+ ...(input.maxAttemptsEnabled !== undefined && { maxAttemptsEnabled: Boolean(input.maxAttemptsEnabled) }),
41
+ ...(input.stopBijSleutelfoutEnabled !== undefined && {
42
+ stopBijSleutelfoutEnabled: Boolean(input.stopBijSleutelfoutEnabled),
43
+ }),
44
+ ...(input.maxAttempts !== undefined && {
45
+ maxAttempts: Math.min(100, Math.max(1, Math.round(Number(input.maxAttempts)) || 3)),
46
+ }),
47
+ ...(typeof input.timezone === "string" && input.timezone.trim() && {
48
+ timezone: input.timezone.trim().slice(0, 64),
49
+ }),
50
+ expectedUpdatedAt: typeof input.expectedUpdatedAt === "string" ? input.expectedUpdatedAt : null,
28
51
  };
29
52
  })
30
53
  .handler(async ({ data }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/google-maps-api-installatie-techniek",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "flowselections": {
5
5
  "moduleId": "maps-installatie",
6
6
  "pages": [
@@ -37,7 +37,8 @@
37
37
  "build:lib:watch": "tsc -p tsconfig.build.json --watch",
38
38
  "build:dev": "vite build --mode development",
39
39
  "preview": "vite preview",
40
- "lint": "eslint ."
40
+ "lint": "eslint .",
41
+ "test": "vitest run --dir src"
41
42
  },
42
43
  "peerDependencies": {
43
44
  "@flowselections/core": "*",
@@ -55,7 +56,7 @@
55
56
  "devDependencies": {
56
57
  "@cloudflare/vite-plugin": "^1.25.5",
57
58
  "@eslint/js": "^9.32.0",
58
- "@flowselections/core": "1.0.30",
59
+ "@flowselections/core": "1.0.37",
59
60
  "@lovable.dev/vite-tanstack-config": "2.26.5",
60
61
  "@rsbuild/core": "^1.0.0",
61
62
  "@tailwindcss/vite": "^4.2.1",
@@ -72,16 +73,17 @@
72
73
  "eslint-plugin-react-refresh": "^0.4.20",
73
74
  "globals": "^15.15.0",
74
75
  "lucide-react": "^0.575.0",
76
+ "nitro": "3.0.260603-beta",
75
77
  "react": "^19.2.0",
76
78
  "react-dom": "^19.2.0",
77
79
  "sonner": "^2.0.7",
78
80
  "tailwindcss": "^4.2.1",
81
+ "tsc-alias": "^1.8.10",
79
82
  "tw-animate-css": "^1.3.4",
80
83
  "typescript": "^5.8.3",
81
- "tsc-alias": "^1.8.10",
82
84
  "typescript-eslint": "^8.56.1",
83
85
  "vite": "^7.3.1",
84
86
  "vite-tsconfig-paths": "^6.0.2",
85
- "nitro": "3.0.260603-beta"
87
+ "vitest": "^3"
86
88
  }
87
89
  }