@fragno-dev/core 0.1.2 → 0.1.4

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 (56) hide show
  1. package/.turbo/turbo-build.log +40 -31
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE.md +16 -0
  4. package/dist/api/api.d.ts +1 -1
  5. package/dist/api/fragment-builder.d.ts +2 -2
  6. package/dist/api/fragment-instantiation.d.ts +2 -2
  7. package/dist/api/fragment-instantiation.js +3 -2
  8. package/dist/{api-jKNXmz2B.d.ts → api-BX90b4-D.d.ts} +2 -2
  9. package/dist/{api-jKNXmz2B.d.ts.map → api-BX90b4-D.d.ts.map} +1 -1
  10. package/dist/client/client.d.ts +2 -2
  11. package/dist/client/client.js +4 -3
  12. package/dist/client/client.svelte.d.ts +2 -2
  13. package/dist/client/client.svelte.js +4 -3
  14. package/dist/client/client.svelte.js.map +1 -1
  15. package/dist/client/react.d.ts +2 -2
  16. package/dist/client/react.js +4 -3
  17. package/dist/client/react.js.map +1 -1
  18. package/dist/client/solid.d.ts +2 -2
  19. package/dist/client/solid.js +4 -3
  20. package/dist/client/solid.js.map +1 -1
  21. package/dist/client/vanilla.d.ts +2 -2
  22. package/dist/client/vanilla.js +4 -3
  23. package/dist/client/vanilla.js.map +1 -1
  24. package/dist/client/vue.d.ts +2 -2
  25. package/dist/client/vue.js +4 -3
  26. package/dist/client/vue.js.map +1 -1
  27. package/dist/{client-CzWq6IlK.js → client-C6LChM0Y.js} +4 -3
  28. package/dist/{client-CzWq6IlK.js.map → client-C6LChM0Y.js.map} +1 -1
  29. package/dist/{fragment-builder-B3JXWiZB.d.ts → fragment-builder-BZr2JkuW.d.ts} +35 -35
  30. package/dist/fragment-builder-BZr2JkuW.d.ts.map +1 -0
  31. package/dist/fragment-builder-DOnCVBqc.js.map +1 -1
  32. package/dist/{fragment-instantiation-D1q7pltx.js → fragment-instantiation-D74OQjbn.js} +4 -3
  33. package/dist/fragment-instantiation-D74OQjbn.js.map +1 -0
  34. package/dist/integrations/react-ssr.js +1 -1
  35. package/dist/mod.d.ts +2 -2
  36. package/dist/mod.js +3 -2
  37. package/dist/route-CTxjMtGZ.js +10 -0
  38. package/dist/route-CTxjMtGZ.js.map +1 -0
  39. package/dist/{route-DbBZ3Ep9.js → route-D1MZR6JL.js} +2 -10
  40. package/dist/route-D1MZR6JL.js.map +1 -0
  41. package/dist/{ssr-CamRrMc0.js → ssr-BByDVfFD.js} +1 -1
  42. package/dist/{ssr-CamRrMc0.js.map → ssr-BByDVfFD.js.map} +1 -1
  43. package/dist/test/test.d.ts +112 -0
  44. package/dist/test/test.d.ts.map +1 -0
  45. package/dist/test/test.js +155 -0
  46. package/dist/test/test.js.map +1 -0
  47. package/package.json +18 -24
  48. package/src/api/fragment-builder.ts +0 -1
  49. package/src/api/request-middleware.test.ts +251 -0
  50. package/src/api/request-middleware.ts +1 -1
  51. package/src/test/test.test.ts +504 -0
  52. package/src/test/test.ts +379 -0
  53. package/tsdown.config.ts +1 -0
  54. package/dist/fragment-builder-B3JXWiZB.d.ts.map +0 -1
  55. package/dist/fragment-instantiation-D1q7pltx.js.map +0 -1
  56. package/dist/route-DbBZ3Ep9.js.map +0 -1
@@ -0,0 +1,504 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createFragmentForTest } from "./test";
3
+ import { defineFragment } from "../api/fragment-builder";
4
+ import { defineRoute, defineRoutes } from "../api/route";
5
+ import { z } from "zod";
6
+
7
+ describe("createFragmentForTest", () => {
8
+ it("should create a test fragment with config only", () => {
9
+ const fragment = defineFragment<{ apiKey: string }>("test");
10
+ const testFragment = createFragmentForTest(fragment, {
11
+ config: { apiKey: "test-key" },
12
+ });
13
+
14
+ expect(testFragment.config).toEqual({ apiKey: "test-key" });
15
+ expect(testFragment.deps).toEqual({});
16
+ expect(testFragment.services).toEqual({});
17
+ });
18
+
19
+ it("should create deps from fragment definition", () => {
20
+ const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
21
+ client: { apiKey: config.apiKey },
22
+ }));
23
+
24
+ const testFragment = createFragmentForTest(fragment, {
25
+ config: { apiKey: "test-key" },
26
+ });
27
+
28
+ expect(testFragment.deps).toEqual({ client: { apiKey: "test-key" } });
29
+ });
30
+
31
+ it("should override deps when provided", () => {
32
+ const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
33
+ client: { apiKey: config.apiKey },
34
+ }));
35
+
36
+ const testFragment = createFragmentForTest(fragment, {
37
+ config: { apiKey: "test-key" },
38
+ deps: { client: { apiKey: "override-key" } },
39
+ });
40
+
41
+ expect(testFragment.deps).toEqual({ client: { apiKey: "override-key" } });
42
+ });
43
+
44
+ it("should create services from fragment definition", () => {
45
+ const fragment = defineFragment<{ apiKey: string }>("test")
46
+ .withDependencies(({ config }) => ({
47
+ client: { apiKey: config.apiKey },
48
+ }))
49
+ .withServices(({ deps }) => ({
50
+ getApiKey: () => deps.client.apiKey,
51
+ }));
52
+
53
+ const testFragment = createFragmentForTest(fragment, {
54
+ config: { apiKey: "test-key" },
55
+ });
56
+
57
+ expect(testFragment.services.getApiKey()).toBe("test-key");
58
+ });
59
+
60
+ it("should override services when provided", () => {
61
+ const fragment = defineFragment<{ apiKey: string }>("test")
62
+ .withDependencies(({ config }) => ({
63
+ client: { apiKey: config.apiKey },
64
+ }))
65
+ .withServices(({ deps }) => ({
66
+ getApiKey: () => deps.client.apiKey,
67
+ }));
68
+
69
+ const testFragment = createFragmentForTest(fragment, {
70
+ config: { apiKey: "test-key" },
71
+ services: { getApiKey: () => "override-key" },
72
+ });
73
+
74
+ expect(testFragment.services.getApiKey()).toBe("override-key");
75
+ });
76
+
77
+ it("should initialize routes with fragment context", () => {
78
+ const fragment = defineFragment<{ multiplier: number }>("test")
79
+ .withDependencies(() => ({ dep: "value" }))
80
+ .withServices(({ config }) => ({
81
+ multiply: (x: number) => x * config.multiplier,
82
+ }));
83
+
84
+ const testFragment = createFragmentForTest(fragment, {
85
+ config: { multiplier: 2 },
86
+ });
87
+
88
+ const route = defineRoute({
89
+ method: "GET",
90
+ path: "/test",
91
+ outputSchema: z.object({ result: z.number() }),
92
+ handler: async (_ctx, { json }) => {
93
+ return json({ result: 42 });
94
+ },
95
+ });
96
+
97
+ const routes = [route] as const;
98
+ const [initializedRoute] = testFragment.initRoutes(routes);
99
+
100
+ expect(initializedRoute).toBe(route);
101
+ expect(initializedRoute.method).toBe("GET");
102
+ expect(initializedRoute.path).toBe("/test");
103
+ });
104
+
105
+ it("should initialize route factories with fragment context", async () => {
106
+ const fragment = defineFragment<{ multiplier: number }>("test")
107
+ .withDependencies(() => ({ dep: "value" }))
108
+ .withServices(({ config }) => ({
109
+ multiply: (x: number) => x * config.multiplier,
110
+ }));
111
+
112
+ const testFragment = createFragmentForTest(fragment, {
113
+ config: { multiplier: 3 },
114
+ });
115
+
116
+ const routeFactory = ({ services }: { services: { multiply: (x: number) => number } }) => {
117
+ return [
118
+ defineRoute({
119
+ method: "GET",
120
+ path: "/multiply",
121
+ outputSchema: z.object({ result: z.number() }),
122
+ handler: async (_ctx, { json }) => {
123
+ return json({ result: services.multiply(5) });
124
+ },
125
+ }),
126
+ ];
127
+ };
128
+
129
+ const routes = [routeFactory] as const;
130
+ const [multiplyRoute] = testFragment.initRoutes(routes);
131
+
132
+ expect(multiplyRoute.method).toBe("GET");
133
+ expect(multiplyRoute.path).toBe("/multiply");
134
+
135
+ // Test that the route was initialized with the correct services
136
+ const response = await testFragment.handler(multiplyRoute);
137
+ expect(response.type).toBe("json");
138
+ if (response.type === "json") {
139
+ expect(response.data).toEqual({ result: 15 }); // 5 * 3
140
+ }
141
+ });
142
+
143
+ it("should allow overriding config/deps/services for specific route initialization", async () => {
144
+ const fragment = defineFragment<{ multiplier: number }>("test")
145
+ .withDependencies(() => ({ baseUrl: "https://api.example.com" }))
146
+ .withServices(({ config }) => ({
147
+ multiply: (x: number) => x * config.multiplier,
148
+ getMessage: (): string => "original message",
149
+ }));
150
+
151
+ const testFragment = createFragmentForTest(fragment, {
152
+ config: { multiplier: 2 },
153
+ });
154
+
155
+ const routeFactory = ({
156
+ config,
157
+ services,
158
+ }: {
159
+ config: { multiplier: number };
160
+ services: { multiply: (x: number) => number; getMessage: () => string };
161
+ }) => {
162
+ return [
163
+ defineRoute({
164
+ method: "GET",
165
+ path: "/test",
166
+ outputSchema: z.object({
167
+ result: z.number(),
168
+ message: z.string(),
169
+ multiplier: z.number(),
170
+ }),
171
+ handler: async (_ctx, { json }) => {
172
+ return json({
173
+ result: services.multiply(10),
174
+ message: services.getMessage(),
175
+ multiplier: config.multiplier,
176
+ });
177
+ },
178
+ }),
179
+ ];
180
+ };
181
+
182
+ const routes = [routeFactory] as const;
183
+
184
+ // Initialize with overrides - completely replace the multiply service
185
+ const [overriddenRoute] = testFragment.initRoutes(routes, {
186
+ config: { multiplier: 5 },
187
+ services: {
188
+ multiply: (x: number) => x * 5, // Mock implementation uses hardcoded multiplier
189
+ getMessage: (): string => "mocked message",
190
+ },
191
+ });
192
+
193
+ const response = await testFragment.handler(overriddenRoute);
194
+ expect(response.type).toBe("json");
195
+ if (response.type === "json") {
196
+ expect(response.data).toEqual({
197
+ result: 50, // 10 * 5 (mocked multiply service)
198
+ message: "mocked message", // overridden service
199
+ multiplier: 5, // overridden config
200
+ });
201
+ }
202
+
203
+ // Verify original fragment config/services are unchanged
204
+ expect(testFragment.config.multiplier).toBe(2);
205
+ expect(testFragment.services.multiply(10)).toBe(20); // Original multiplier is 2
206
+ expect(testFragment.services.getMessage()).toBe("original message");
207
+ });
208
+ });
209
+
210
+ describe("fragment.handler", () => {
211
+ it("should handle JSON response", async () => {
212
+ const fragment = defineFragment<{ apiKey: string }>("test");
213
+ const testFragment = createFragmentForTest(fragment, {
214
+ config: { apiKey: "test-key" },
215
+ });
216
+
217
+ const route = defineRoute({
218
+ method: "GET",
219
+ path: "/test",
220
+ outputSchema: z.object({ message: z.string() }),
221
+ handler: async (_ctx, { json }) => {
222
+ return json({ message: "hello" });
223
+ },
224
+ });
225
+
226
+ const response = await testFragment.handler(route);
227
+
228
+ expect(response.type).toBe("json");
229
+ if (response.type === "json") {
230
+ expect(response.status).toBe(200);
231
+ expect(response.data).toEqual({ message: "hello" });
232
+ expect(response.headers).toBeInstanceOf(Headers);
233
+ }
234
+ });
235
+
236
+ it("should handle empty response", async () => {
237
+ const fragment = defineFragment<{ apiKey: string }>("test");
238
+ const testFragment = createFragmentForTest(fragment, {
239
+ config: { apiKey: "test-key" },
240
+ });
241
+
242
+ const route = defineRoute({
243
+ method: "DELETE",
244
+ path: "/test",
245
+ handler: async (_ctx, { empty }) => {
246
+ return empty(204);
247
+ },
248
+ });
249
+
250
+ const response = await testFragment.handler(route);
251
+
252
+ expect(response.type).toBe("empty");
253
+ if (response.type === "empty") {
254
+ expect(response.status).toBe(204);
255
+ expect(response.headers).toBeInstanceOf(Headers);
256
+ }
257
+ });
258
+
259
+ it("should handle error response", async () => {
260
+ const fragment = defineFragment<{ apiKey: string }>("test");
261
+ const testFragment = createFragmentForTest(fragment, {
262
+ config: { apiKey: "test-key" },
263
+ });
264
+
265
+ const route = defineRoute({
266
+ method: "GET",
267
+ path: "/test",
268
+ errorCodes: ["NOT_FOUND"] as const,
269
+ handler: async (_ctx, { error }) => {
270
+ return error({ message: "Not found", code: "NOT_FOUND" }, 404);
271
+ },
272
+ });
273
+
274
+ const response = await testFragment.handler(route);
275
+
276
+ expect(response.type).toBe("error");
277
+ if (response.type === "error") {
278
+ expect(response.status).toBe(404);
279
+ expect(response.error).toEqual({ message: "Not found", code: "NOT_FOUND" });
280
+ expect(response.headers).toBeInstanceOf(Headers);
281
+ }
282
+ });
283
+
284
+ it("should handle JSON stream response", async () => {
285
+ const fragment = defineFragment<{ apiKey: string }>("test");
286
+ const testFragment = createFragmentForTest(fragment, {
287
+ config: { apiKey: "test-key" },
288
+ });
289
+
290
+ const route = defineRoute({
291
+ method: "GET",
292
+ path: "/test/stream",
293
+ outputSchema: z.array(z.object({ value: z.number() })),
294
+ handler: async (_ctx, { jsonStream }) => {
295
+ return jsonStream(async (stream) => {
296
+ for (let i = 1; i <= 5; i++) {
297
+ await stream.write({ value: i });
298
+ }
299
+ });
300
+ },
301
+ });
302
+
303
+ const response = await testFragment.handler(route);
304
+
305
+ expect(response.type).toBe("jsonStream");
306
+ if (response.type === "jsonStream") {
307
+ expect(response.status).toBe(200);
308
+ expect(response.headers).toBeInstanceOf(Headers);
309
+ expect(response.headers.get("content-type")).toContain("application/x-ndjson");
310
+
311
+ const items = [];
312
+ for await (const item of response.stream) {
313
+ items.push(item);
314
+ }
315
+
316
+ expect(items).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }]);
317
+ }
318
+ });
319
+
320
+ it("should handle route factory created with defineRoutes", async () => {
321
+ const fragment = defineFragment<{ apiKey: string }>("test").withServices(() => ({
322
+ getGreeting: (name: string) => `Hello, ${name}!`,
323
+ getCount: () => 42,
324
+ }));
325
+
326
+ const testFragment = createFragmentForTest(fragment, {
327
+ config: { apiKey: "test-key" },
328
+ });
329
+
330
+ type Config = { apiKey: string };
331
+ type Deps = {};
332
+ type Services = { getGreeting: (name: string) => string; getCount: () => number };
333
+
334
+ const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
335
+ defineRoute({
336
+ method: "GET",
337
+ path: "/greeting/:name",
338
+ outputSchema: z.object({ message: z.string() }),
339
+ handler: async ({ pathParams }, { json }) => {
340
+ return json({ message: services.getGreeting(pathParams.name) });
341
+ },
342
+ }),
343
+ defineRoute({
344
+ method: "GET",
345
+ path: "/count",
346
+ outputSchema: z.object({ count: z.number() }),
347
+ handler: async (_ctx, { json }) => {
348
+ return json({ count: services.getCount() });
349
+ },
350
+ }),
351
+ ]);
352
+
353
+ const routes = [routeFactory] as const;
354
+ const [greetingRoute, countRoute] = testFragment.initRoutes(routes);
355
+
356
+ // Test first route
357
+ const greetingResponse = await testFragment.handler(greetingRoute, {
358
+ pathParams: { name: "World" },
359
+ });
360
+
361
+ expect(greetingResponse.type).toBe("json");
362
+ if (greetingResponse.type === "json") {
363
+ expect(greetingResponse.data).toEqual({ message: "Hello, World!" });
364
+ }
365
+
366
+ // Test second route
367
+ const countResponse = await testFragment.handler(countRoute);
368
+
369
+ expect(countResponse.type).toBe("json");
370
+ if (countResponse.type === "json") {
371
+ expect(countResponse.data).toEqual({ count: 42 });
372
+ }
373
+ });
374
+
375
+ it("should handle path parameters", async () => {
376
+ const fragment = defineFragment<{}>("test");
377
+ const testFragment = createFragmentForTest(fragment, {
378
+ config: {},
379
+ });
380
+
381
+ const route = defineRoute({
382
+ method: "GET",
383
+ path: "/users/:id",
384
+ outputSchema: z.object({ userId: z.string() }),
385
+ handler: async ({ pathParams }, { json }) => {
386
+ return json({ userId: pathParams.id });
387
+ },
388
+ });
389
+
390
+ const response = await testFragment.handler(route, {
391
+ pathParams: { id: "123" },
392
+ });
393
+
394
+ expect(response.type).toBe("json");
395
+ if (response.type === "json") {
396
+ expect(response.data).toEqual({ userId: "123" });
397
+ }
398
+ });
399
+
400
+ it("should handle query parameters", async () => {
401
+ const fragment = defineFragment<{}>("test");
402
+ const testFragment = createFragmentForTest(fragment, {
403
+ config: {},
404
+ });
405
+
406
+ const route = defineRoute({
407
+ method: "GET",
408
+ path: "/search",
409
+ outputSchema: z.object({ query: z.string() }),
410
+ handler: async ({ query }, { json }) => {
411
+ return json({ query: query.get("q") || "" });
412
+ },
413
+ });
414
+
415
+ const response = await testFragment.handler(route, {
416
+ query: { q: "test" },
417
+ });
418
+
419
+ expect(response.type).toBe("json");
420
+ if (response.type === "json") {
421
+ expect(response.data).toEqual({ query: "test" });
422
+ }
423
+ });
424
+
425
+ it("should handle request body", async () => {
426
+ const fragment = defineFragment<{}>("test");
427
+ const testFragment = createFragmentForTest(fragment, {
428
+ config: {},
429
+ });
430
+
431
+ const route = defineRoute({
432
+ method: "POST",
433
+ path: "/users",
434
+ inputSchema: z.object({ name: z.string(), email: z.string() }),
435
+ outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
436
+ handler: async ({ input }, { json }) => {
437
+ if (input) {
438
+ const data = await input.valid();
439
+ return json({ id: 1, name: data.name, email: data.email });
440
+ }
441
+ return json({ id: 1, name: "", email: "" });
442
+ },
443
+ });
444
+
445
+ const response = await testFragment.handler(route, {
446
+ body: { name: "John", email: "john@example.com" },
447
+ });
448
+
449
+ expect(response.type).toBe("json");
450
+ if (response.type === "json") {
451
+ expect(response.data).toEqual({ id: 1, name: "John", email: "john@example.com" });
452
+ }
453
+ });
454
+
455
+ it("should handle custom headers", async () => {
456
+ const fragment = defineFragment<{}>("test");
457
+ const testFragment = createFragmentForTest(fragment, {
458
+ config: {},
459
+ });
460
+
461
+ const route = defineRoute({
462
+ method: "GET",
463
+ path: "/test",
464
+ outputSchema: z.object({ authHeader: z.string() }),
465
+ handler: async ({ headers }, { json }) => {
466
+ return json({ authHeader: headers.get("authorization") || "" });
467
+ },
468
+ });
469
+
470
+ const response = await testFragment.handler(route, {
471
+ headers: { authorization: "Bearer token" },
472
+ });
473
+
474
+ expect(response.type).toBe("json");
475
+ if (response.type === "json") {
476
+ expect(response.data).toEqual({ authHeader: "Bearer token" });
477
+ }
478
+ });
479
+
480
+ it("should properly type path params", async () => {
481
+ const fragment = defineFragment<{}>("test");
482
+ const testFragment = createFragmentForTest(fragment, {
483
+ config: {},
484
+ });
485
+
486
+ const route = defineRoute({
487
+ method: "GET",
488
+ path: "/orgs/:orgId/users/:userId",
489
+ outputSchema: z.object({ orgId: z.string(), userId: z.string() }),
490
+ handler: async ({ pathParams }, { json }) => {
491
+ return json({ orgId: pathParams.orgId, userId: pathParams.userId });
492
+ },
493
+ });
494
+
495
+ const response = await testFragment.handler(route, {
496
+ pathParams: { orgId: "123", userId: "456" },
497
+ });
498
+
499
+ expect(response.type).toBe("json");
500
+ if (response.type === "json") {
501
+ expect(response.data).toEqual({ orgId: "123", userId: "456" });
502
+ }
503
+ });
504
+ });