@voidhash/mimic-effect 0.0.1-alpha.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.
- package/README.md +0 -0
- package/package.json +40 -0
- package/src/DocumentManager.ts +252 -0
- package/src/DocumentProtocol.ts +112 -0
- package/src/MimicAuthService.ts +103 -0
- package/src/MimicConfig.ts +131 -0
- package/src/MimicDataStorage.ts +157 -0
- package/src/MimicServer.ts +363 -0
- package/src/PresenceManager.ts +297 -0
- package/src/WebSocketHandler.ts +735 -0
- package/src/auth/NoAuth.ts +46 -0
- package/src/errors.ts +113 -0
- package/src/index.ts +48 -0
- package/src/storage/InMemoryDataStorage.ts +66 -0
- package/tests/DocumentManager.test.ts +340 -0
- package/tests/DocumentProtocol.test.ts +113 -0
- package/tests/InMemoryDataStorage.test.ts +190 -0
- package/tests/MimicAuthService.test.ts +185 -0
- package/tests/MimicConfig.test.ts +175 -0
- package/tests/MimicDataStorage.test.ts +190 -0
- package/tests/MimicServer.test.ts +385 -0
- package/tests/NoAuth.test.ts +94 -0
- package/tests/PresenceManager.test.ts +421 -0
- package/tests/WebSocketHandler.test.ts +321 -0
- package/tests/errors.test.ts +77 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- package/vitest.mts +11 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import * as Chunk from "effect/Chunk";
|
|
6
|
+
import * as Fiber from "effect/Fiber";
|
|
7
|
+
import * as Schema from "effect/Schema";
|
|
8
|
+
import { Primitive, Presence } from "@voidhash/mimic";
|
|
9
|
+
import * as WebSocketHandler from "../src/WebSocketHandler";
|
|
10
|
+
import * as MimicConfig from "../src/MimicConfig";
|
|
11
|
+
import * as MimicAuthService from "../src/MimicAuthService";
|
|
12
|
+
import * as DocumentManager from "../src/DocumentManager";
|
|
13
|
+
import * as PresenceManager from "../src/PresenceManager";
|
|
14
|
+
import * as InMemoryDataStorage from "../src/storage/InMemoryDataStorage";
|
|
15
|
+
import { MissingDocumentIdError } from "../src/errors";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Test Schema
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
const TestSchema = Primitive.Struct({
|
|
22
|
+
title: Primitive.String().default(""),
|
|
23
|
+
count: Primitive.Number().default(0),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const CursorPresence = Presence.make({
|
|
27
|
+
schema: Schema.Struct({
|
|
28
|
+
x: Schema.Number,
|
|
29
|
+
y: Schema.Number,
|
|
30
|
+
name: Schema.optional(Schema.String),
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Test Layer Factory
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
const makeTestLayer = (options?: { withPresence?: boolean }) => {
|
|
39
|
+
const configLayer = MimicConfig.layer({
|
|
40
|
+
schema: TestSchema,
|
|
41
|
+
presence: options?.withPresence ? CursorPresence : undefined,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const authLayer = MimicAuthService.layer({
|
|
45
|
+
authHandler: (token) => ({ success: true, userId: token || "anonymous" }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return Layer.mergeAll(
|
|
49
|
+
configLayer,
|
|
50
|
+
authLayer,
|
|
51
|
+
DocumentManager.layer.pipe(
|
|
52
|
+
Layer.provide(configLayer),
|
|
53
|
+
Layer.provide(InMemoryDataStorage.layer)
|
|
54
|
+
),
|
|
55
|
+
PresenceManager.layer
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// extractDocumentId Tests
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
describe("WebSocketHandler", () => {
|
|
64
|
+
describe("extractDocumentId", () => {
|
|
65
|
+
it("should extract document ID from /doc/{id} path", () => {
|
|
66
|
+
const result = Effect.runSync(
|
|
67
|
+
WebSocketHandler.extractDocumentId("/doc/my-document-id")
|
|
68
|
+
);
|
|
69
|
+
expect(result).toBe("my-document-id");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should extract document ID from /doc/{id} with leading slashes", () => {
|
|
73
|
+
const result = Effect.runSync(
|
|
74
|
+
WebSocketHandler.extractDocumentId("///doc/my-document-id")
|
|
75
|
+
);
|
|
76
|
+
expect(result).toBe("my-document-id");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should extract document ID from nested paths like /mimic/todo/doc/{id}", () => {
|
|
80
|
+
const result = Effect.runSync(
|
|
81
|
+
WebSocketHandler.extractDocumentId("/mimic/todo/doc/my-document-id")
|
|
82
|
+
);
|
|
83
|
+
expect(result).toBe("my-document-id");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should handle URL-encoded document IDs", () => {
|
|
87
|
+
const result = Effect.runSync(
|
|
88
|
+
WebSocketHandler.extractDocumentId("/doc/my%20document%3Aid")
|
|
89
|
+
);
|
|
90
|
+
expect(result).toBe("my document:id");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle document IDs with colons (type:id format)", () => {
|
|
94
|
+
const result = Effect.runSync(
|
|
95
|
+
WebSocketHandler.extractDocumentId("/doc/todo:abc-123")
|
|
96
|
+
);
|
|
97
|
+
expect(result).toBe("todo:abc-123");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should fail for empty path", () => {
|
|
101
|
+
const result = Effect.runSyncExit(
|
|
102
|
+
WebSocketHandler.extractDocumentId("/")
|
|
103
|
+
);
|
|
104
|
+
expect(result._tag).toBe("Failure");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should fail for /doc without document ID", () => {
|
|
108
|
+
const result = Effect.runSyncExit(
|
|
109
|
+
WebSocketHandler.extractDocumentId("/doc")
|
|
110
|
+
);
|
|
111
|
+
expect(result._tag).toBe("Failure");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should fail for /doc/ without document ID", () => {
|
|
115
|
+
const result = Effect.runSyncExit(
|
|
116
|
+
WebSocketHandler.extractDocumentId("/doc/")
|
|
117
|
+
);
|
|
118
|
+
// This will fail because after split, parts[1] will be empty string
|
|
119
|
+
expect(result._tag).toBe("Failure");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("makeHandler", () => {
|
|
124
|
+
it("should create a handler function", async () => {
|
|
125
|
+
const result = await Effect.runPromise(
|
|
126
|
+
Effect.gen(function* () {
|
|
127
|
+
const handler = yield* WebSocketHandler.makeHandler;
|
|
128
|
+
return typeof handler === "function";
|
|
129
|
+
}).pipe(Effect.provide(makeTestLayer()))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should create a handler with presence enabled", async () => {
|
|
136
|
+
const result = await Effect.runPromise(
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
const handler = yield* WebSocketHandler.makeHandler;
|
|
139
|
+
return typeof handler === "function";
|
|
140
|
+
}).pipe(Effect.provide(makeTestLayer({ withPresence: true })))
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("presence integration with PresenceManager", () => {
|
|
148
|
+
it("should store presence data through PresenceManager", async () => {
|
|
149
|
+
// This tests that the PresenceManager is properly integrated
|
|
150
|
+
// with the WebSocketHandler layer composition
|
|
151
|
+
const result = await Effect.runPromise(
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
const pm = yield* PresenceManager.PresenceManagerTag;
|
|
154
|
+
|
|
155
|
+
// Simulate what the WebSocketHandler would do when receiving presence_set
|
|
156
|
+
yield* pm.set("doc-1", "conn-1", {
|
|
157
|
+
data: { x: 100, y: 200 },
|
|
158
|
+
userId: "user-1",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const snapshot = yield* pm.getSnapshot("doc-1");
|
|
162
|
+
return snapshot;
|
|
163
|
+
}).pipe(Effect.provide(makeTestLayer({ withPresence: true })))
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(result.presences["conn-1"]).toEqual({
|
|
167
|
+
data: { x: 100, y: 200 },
|
|
168
|
+
userId: "user-1",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should remove presence data through PresenceManager", async () => {
|
|
173
|
+
const result = await Effect.runPromise(
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const pm = yield* PresenceManager.PresenceManagerTag;
|
|
176
|
+
|
|
177
|
+
// Set presence
|
|
178
|
+
yield* pm.set("doc-1", "conn-1", {
|
|
179
|
+
data: { x: 100, y: 200 },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Simulate disconnect - remove presence
|
|
183
|
+
yield* pm.remove("doc-1", "conn-1");
|
|
184
|
+
|
|
185
|
+
const snapshot = yield* pm.getSnapshot("doc-1");
|
|
186
|
+
return snapshot;
|
|
187
|
+
}).pipe(Effect.provide(makeTestLayer({ withPresence: true })))
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(result.presences).toEqual({});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should broadcast presence events to subscribers", async () => {
|
|
194
|
+
const result = await Effect.runPromise(
|
|
195
|
+
Effect.scoped(
|
|
196
|
+
Effect.gen(function* () {
|
|
197
|
+
const pm = yield* PresenceManager.PresenceManagerTag;
|
|
198
|
+
|
|
199
|
+
// Subscribe to presence events
|
|
200
|
+
const eventStream = yield* pm.subscribe("doc-1");
|
|
201
|
+
|
|
202
|
+
// Collect events in background
|
|
203
|
+
const eventsFiber = yield* Effect.fork(
|
|
204
|
+
Stream.runCollect(Stream.take(eventStream, 2))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
yield* Effect.sleep("10 millis");
|
|
208
|
+
|
|
209
|
+
// Simulate presence set and remove
|
|
210
|
+
yield* pm.set("doc-1", "conn-1", { data: { x: 10, y: 20 } });
|
|
211
|
+
yield* pm.remove("doc-1", "conn-1");
|
|
212
|
+
|
|
213
|
+
const events = yield* Fiber.join(eventsFiber);
|
|
214
|
+
return Chunk.toArray(events);
|
|
215
|
+
})
|
|
216
|
+
).pipe(Effect.provide(makeTestLayer({ withPresence: true })))
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(result.length).toBe(2);
|
|
220
|
+
expect(result[0]!.type).toBe("presence_update");
|
|
221
|
+
expect(result[1]!.type).toBe("presence_remove");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("presence validation", () => {
|
|
226
|
+
it("should validate presence data against schema using Presence module", () => {
|
|
227
|
+
// This tests the validation logic that WebSocketHandler uses
|
|
228
|
+
const validData = { x: 100, y: 200 };
|
|
229
|
+
const invalidData = { x: "invalid", y: 200 };
|
|
230
|
+
|
|
231
|
+
// Valid data should pass validation
|
|
232
|
+
const validated = Presence.validateSafe(CursorPresence, validData);
|
|
233
|
+
expect(validated).toEqual({ x: 100, y: 200 });
|
|
234
|
+
|
|
235
|
+
// Invalid data should return undefined
|
|
236
|
+
const invalidResult = Presence.validateSafe(CursorPresence, invalidData);
|
|
237
|
+
expect(invalidResult).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should handle optional fields in presence schema", () => {
|
|
241
|
+
// Without optional field
|
|
242
|
+
const withoutName = Presence.validateSafe(CursorPresence, {
|
|
243
|
+
x: 10,
|
|
244
|
+
y: 20,
|
|
245
|
+
});
|
|
246
|
+
expect(withoutName).toEqual({ x: 10, y: 20 });
|
|
247
|
+
|
|
248
|
+
// With optional field
|
|
249
|
+
const withName = Presence.validateSafe(CursorPresence, {
|
|
250
|
+
x: 10,
|
|
251
|
+
y: 20,
|
|
252
|
+
name: "Alice",
|
|
253
|
+
});
|
|
254
|
+
expect(withName).toEqual({ x: 10, y: 20, name: "Alice" });
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("presence message types", () => {
|
|
259
|
+
// These tests document the expected presence message types
|
|
260
|
+
// that the WebSocketHandler should handle
|
|
261
|
+
|
|
262
|
+
it("should define presence_set client message format", () => {
|
|
263
|
+
const message = {
|
|
264
|
+
type: "presence_set" as const,
|
|
265
|
+
data: { x: 100, y: 200 },
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
expect(message.type).toBe("presence_set");
|
|
269
|
+
expect(message.data).toEqual({ x: 100, y: 200 });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should define presence_clear client message format", () => {
|
|
273
|
+
const message = {
|
|
274
|
+
type: "presence_clear" as const,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
expect(message.type).toBe("presence_clear");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should define presence_snapshot server message format", () => {
|
|
281
|
+
const message = {
|
|
282
|
+
type: "presence_snapshot" as const,
|
|
283
|
+
selfId: "conn-123",
|
|
284
|
+
presences: {
|
|
285
|
+
"conn-456": { data: { x: 10, y: 20 }, userId: "user-1" },
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
expect(message.type).toBe("presence_snapshot");
|
|
290
|
+
expect(message.selfId).toBe("conn-123");
|
|
291
|
+
expect(message.presences["conn-456"]).toEqual({
|
|
292
|
+
data: { x: 10, y: 20 },
|
|
293
|
+
userId: "user-1",
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should define presence_update server message format", () => {
|
|
298
|
+
const message = {
|
|
299
|
+
type: "presence_update" as const,
|
|
300
|
+
id: "conn-789",
|
|
301
|
+
data: { x: 50, y: 75 },
|
|
302
|
+
userId: "user-2",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
expect(message.type).toBe("presence_update");
|
|
306
|
+
expect(message.id).toBe("conn-789");
|
|
307
|
+
expect(message.data).toEqual({ x: 50, y: 75 });
|
|
308
|
+
expect(message.userId).toBe("user-2");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should define presence_remove server message format", () => {
|
|
312
|
+
const message = {
|
|
313
|
+
type: "presence_remove" as const,
|
|
314
|
+
id: "conn-disconnected",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
expect(message.type).toBe("presence_remove");
|
|
318
|
+
expect(message.id).toBe("conn-disconnected");
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as errors from "../src/errors";
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Error Tests
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
describe("errors", () => {
|
|
9
|
+
describe("DocumentTypeNotFoundError", () => {
|
|
10
|
+
it("should have correct message", () => {
|
|
11
|
+
const error = new errors.DocumentTypeNotFoundError({
|
|
12
|
+
documentType: "unknown-type",
|
|
13
|
+
});
|
|
14
|
+
expect(error.message).toBe("Document type not found: unknown-type");
|
|
15
|
+
expect(error._tag).toBe("DocumentTypeNotFoundError");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("DocumentNotFoundError", () => {
|
|
20
|
+
it("should have correct message", () => {
|
|
21
|
+
const error = new errors.DocumentNotFoundError({
|
|
22
|
+
documentId: "doc-123",
|
|
23
|
+
});
|
|
24
|
+
expect(error.message).toBe("Document not found: doc-123");
|
|
25
|
+
expect(error._tag).toBe("DocumentNotFoundError");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("AuthenticationError", () => {
|
|
30
|
+
it("should have correct message", () => {
|
|
31
|
+
const error = new errors.AuthenticationError({
|
|
32
|
+
reason: "Invalid token",
|
|
33
|
+
});
|
|
34
|
+
expect(error.message).toBe("Authentication failed: Invalid token");
|
|
35
|
+
expect(error._tag).toBe("AuthenticationError");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("TransactionRejectedError", () => {
|
|
40
|
+
it("should have correct message", () => {
|
|
41
|
+
const error = new errors.TransactionRejectedError({
|
|
42
|
+
transactionId: "tx-456",
|
|
43
|
+
reason: "Transaction is empty",
|
|
44
|
+
});
|
|
45
|
+
expect(error.message).toBe("Transaction tx-456 rejected: Transaction is empty");
|
|
46
|
+
expect(error._tag).toBe("TransactionRejectedError");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("MessageParseError", () => {
|
|
51
|
+
it("should have correct message", () => {
|
|
52
|
+
const error = new errors.MessageParseError({
|
|
53
|
+
cause: new SyntaxError("Unexpected token"),
|
|
54
|
+
});
|
|
55
|
+
expect(error.message).toContain("Failed to parse message");
|
|
56
|
+
expect(error._tag).toBe("MessageParseError");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("InvalidConnectionError", () => {
|
|
61
|
+
it("should have correct message", () => {
|
|
62
|
+
const error = new errors.InvalidConnectionError({
|
|
63
|
+
reason: "Connection closed",
|
|
64
|
+
});
|
|
65
|
+
expect(error.message).toBe("Invalid connection: Connection closed");
|
|
66
|
+
expect(error._tag).toBe("InvalidConnectionError");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("MissingDocumentIdError", () => {
|
|
71
|
+
it("should have correct message", () => {
|
|
72
|
+
const error = new errors.MissingDocumentIdError({});
|
|
73
|
+
expect(error.message).toBe("Document ID is required in the URL path");
|
|
74
|
+
expect(error._tag).toBe("MissingDocumentIdError");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "Preserve",
|
|
4
|
+
"lib": ["es2022", "dom", "dom.iterable"],
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"declarationDir": "dist",
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"strictNullChecks": true,
|
|
12
|
+
"noUnusedLocals": false,
|
|
13
|
+
"noUnusedParameters": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
20
|
+
"noImplicitOverride": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"],
|
|
23
|
+
"exclude": ["test", "**/*.test.ts", "**/*.test.tsx", "__tests__"]
|
|
24
|
+
}
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from "tsdown";
|
|
2
|
+
|
|
3
|
+
export const input = ["./src/index.ts"];
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
target: ["es2017"],
|
|
7
|
+
entry: input,
|
|
8
|
+
dts: {
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
tsconfig: "./tsconfig.build.json",
|
|
11
|
+
},
|
|
12
|
+
// unbundle: true,
|
|
13
|
+
format: ["cjs", "esm"],
|
|
14
|
+
outExtensions: (ctx) => ({
|
|
15
|
+
dts: ctx.format === "cjs" ? ".d.cts" : ".d.mts",
|
|
16
|
+
js: ctx.format === "cjs" ? ".cjs" : ".mjs",
|
|
17
|
+
}),
|
|
18
|
+
});
|
package/vitest.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [tsconfigPaths()],
|
|
6
|
+
test: {
|
|
7
|
+
include: ["./**/*.test.ts"],
|
|
8
|
+
exclude: ["./node_modules/**"],
|
|
9
|
+
reporters: ["verbose"],
|
|
10
|
+
},
|
|
11
|
+
});
|