@fragno-dev/core 0.1.4 → 0.1.6

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 (69) hide show
  1. package/.turbo/turbo-build.log +49 -45
  2. package/CHANGELOG.md +53 -0
  3. package/dist/api/api.d.ts +2 -2
  4. package/dist/api/fragment-builder.d.ts +3 -2
  5. package/dist/api/fragment-instantiation.d.ts +4 -3
  6. package/dist/api/fragment-instantiation.js +3 -3
  7. package/dist/api/route.d.ts +3 -0
  8. package/dist/api/route.js +3 -0
  9. package/dist/{api-BX90b4-D.d.ts → api-CoCkNi6h.d.ts} +20 -7
  10. package/dist/api-CoCkNi6h.d.ts.map +1 -0
  11. package/dist/api-DngJDcmO.js.map +1 -1
  12. package/dist/client/client.d.ts +4 -3
  13. package/dist/client/client.js +3 -3
  14. package/dist/client/client.svelte.d.ts +3 -3
  15. package/dist/client/client.svelte.d.ts.map +1 -1
  16. package/dist/client/client.svelte.js +3 -3
  17. package/dist/client/react.d.ts +3 -3
  18. package/dist/client/react.d.ts.map +1 -1
  19. package/dist/client/react.js +3 -3
  20. package/dist/client/solid.d.ts +3 -3
  21. package/dist/client/solid.d.ts.map +1 -1
  22. package/dist/client/solid.js +3 -3
  23. package/dist/client/vanilla.d.ts +3 -3
  24. package/dist/client/vanilla.d.ts.map +1 -1
  25. package/dist/client/vanilla.js +3 -3
  26. package/dist/client/vue.d.ts +3 -3
  27. package/dist/client/vue.d.ts.map +1 -1
  28. package/dist/client/vue.js +3 -3
  29. package/dist/{client-C6LChM0Y.js → client-DJfCJiHK.js} +81 -7
  30. package/dist/client-DJfCJiHK.js.map +1 -0
  31. package/dist/{fragment-builder-BZr2JkuW.d.ts → fragment-builder-8-tiECi5.d.ts} +75 -38
  32. package/dist/fragment-builder-8-tiECi5.d.ts.map +1 -0
  33. package/dist/{fragment-instantiation-D74OQjbn.js → fragment-instantiation-C4wvwl6V.js} +129 -6
  34. package/dist/fragment-instantiation-C4wvwl6V.js.map +1 -0
  35. package/dist/mod.d.ts +3 -2
  36. package/dist/mod.js +3 -3
  37. package/dist/{route-D1MZR6JL.js → request-output-context-CdIjwmEN.js} +22 -33
  38. package/dist/request-output-context-CdIjwmEN.js.map +1 -0
  39. package/dist/route-C5Uryylh.js +21 -0
  40. package/dist/route-C5Uryylh.js.map +1 -0
  41. package/dist/route-mGLYSUvD.d.ts +26 -0
  42. package/dist/route-mGLYSUvD.d.ts.map +1 -0
  43. package/dist/test/test.d.ts +24 -70
  44. package/dist/test/test.d.ts.map +1 -1
  45. package/dist/test/test.js +27 -115
  46. package/dist/test/test.js.map +1 -1
  47. package/package.json +6 -1
  48. package/src/api/api.ts +1 -0
  49. package/src/api/fragment-instantiation.test.ts +460 -0
  50. package/src/api/fragment-instantiation.ts +157 -5
  51. package/src/api/fragno-response.ts +132 -0
  52. package/src/api/request-input-context.test.ts +37 -29
  53. package/src/api/request-input-context.ts +16 -14
  54. package/src/api/request-output-context.test.ts +10 -10
  55. package/src/api/request-output-context.ts +3 -3
  56. package/src/api/route-handler-input-options.ts +15 -0
  57. package/src/client/client.test.ts +264 -0
  58. package/src/client/client.ts +65 -3
  59. package/src/client/internal/fetcher-merge.ts +59 -0
  60. package/src/test/test.test.ts +110 -165
  61. package/src/test/test.ts +56 -266
  62. package/tsdown.config.ts +1 -0
  63. package/dist/api-BX90b4-D.d.ts.map +0 -1
  64. package/dist/client-C6LChM0Y.js.map +0 -1
  65. package/dist/fragment-builder-BZr2JkuW.d.ts.map +0 -1
  66. package/dist/fragment-instantiation-D74OQjbn.js.map +0 -1
  67. package/dist/route-CTxjMtGZ.js +0 -10
  68. package/dist/route-CTxjMtGZ.js.map +0 -1
  69. package/dist/route-D1MZR6JL.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, expectTypeOf } from "vitest";
2
2
  import { createFragmentForTest } from "./test";
3
3
  import { defineFragment } from "../api/fragment-builder";
4
4
  import { defineRoute, defineRoutes } from "../api/route";
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  describe("createFragmentForTest", () => {
8
8
  it("should create a test fragment with config only", () => {
9
9
  const fragment = defineFragment<{ apiKey: string }>("test");
10
- const testFragment = createFragmentForTest(fragment, {
10
+ const testFragment = createFragmentForTest(fragment, [], {
11
11
  config: { apiKey: "test-key" },
12
12
  });
13
13
 
@@ -21,7 +21,7 @@ describe("createFragmentForTest", () => {
21
21
  client: { apiKey: config.apiKey },
22
22
  }));
23
23
 
24
- const testFragment = createFragmentForTest(fragment, {
24
+ const testFragment = createFragmentForTest(fragment, [], {
25
25
  config: { apiKey: "test-key" },
26
26
  });
27
27
 
@@ -33,7 +33,7 @@ describe("createFragmentForTest", () => {
33
33
  client: { apiKey: config.apiKey },
34
34
  }));
35
35
 
36
- const testFragment = createFragmentForTest(fragment, {
36
+ const testFragment = createFragmentForTest(fragment, [], {
37
37
  config: { apiKey: "test-key" },
38
38
  deps: { client: { apiKey: "override-key" } },
39
39
  });
@@ -50,7 +50,7 @@ describe("createFragmentForTest", () => {
50
50
  getApiKey: () => deps.client.apiKey,
51
51
  }));
52
52
 
53
- const testFragment = createFragmentForTest(fragment, {
53
+ const testFragment = createFragmentForTest(fragment, [], {
54
54
  config: { apiKey: "test-key" },
55
55
  });
56
56
 
@@ -66,7 +66,7 @@ describe("createFragmentForTest", () => {
66
66
  getApiKey: () => deps.client.apiKey,
67
67
  }));
68
68
 
69
- const testFragment = createFragmentForTest(fragment, {
69
+ const testFragment = createFragmentForTest(fragment, [], {
70
70
  config: { apiKey: "test-key" },
71
71
  services: { getApiKey: () => "override-key" },
72
72
  });
@@ -74,145 +74,51 @@ describe("createFragmentForTest", () => {
74
74
  expect(testFragment.services.getApiKey()).toBe("override-key");
75
75
  });
76
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
77
  it("should initialize route factories with fragment context", async () => {
106
- const fragment = defineFragment<{ multiplier: number }>("test")
78
+ type Config = { multiplier: number };
79
+ type Deps = { dep: string };
80
+ type Services = { multiply: (x: number) => number };
81
+
82
+ const fragment = defineFragment<Config>("test")
107
83
  .withDependencies(() => ({ dep: "value" }))
108
84
  .withServices(({ config }) => ({
109
85
  multiply: (x: number) => x * config.multiplier,
110
86
  }));
111
87
 
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
- };
88
+ const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
89
+ defineRoute({
90
+ method: "GET",
91
+ path: "/multiply/:num",
92
+ outputSchema: z.object({ result: z.number() }),
93
+ handler: async ({ pathParams }, { json }) => {
94
+ const { num } = pathParams;
95
+ return json({ result: services.multiply(Number(num)) });
96
+ },
97
+ }),
98
+ ]);
128
99
 
129
100
  const routes = [routeFactory] as const;
130
- const [multiplyRoute] = testFragment.initRoutes(routes);
131
101
 
132
- expect(multiplyRoute.method).toBe("GET");
133
- expect(multiplyRoute.path).toBe("/multiply");
102
+ const testFragment = createFragmentForTest(fragment, routes, {
103
+ config: { multiplier: 3 },
104
+ });
134
105
 
135
106
  // 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
- },
107
+ const response = await testFragment.callRoute("GET", "/multiply/:num", {
108
+ // ^?
109
+ pathParams: { num: "5" },
191
110
  });
192
-
193
- const response = await testFragment.handler(overriddenRoute);
194
111
  expect(response.type).toBe("json");
195
112
  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
- });
113
+ expect(response.data).toEqual({ result: 15 }); // 5 * 3
114
+ expectTypeOf(response.data).toMatchObjectType<{ result: number }>();
201
115
  }
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
116
  });
208
117
  });
209
118
 
210
- describe("fragment.handler", () => {
119
+ describe("fragment.callRoute", () => {
211
120
  it("should handle JSON response", async () => {
212
121
  const fragment = defineFragment<{ apiKey: string }>("test");
213
- const testFragment = createFragmentForTest(fragment, {
214
- config: { apiKey: "test-key" },
215
- });
216
122
 
217
123
  const route = defineRoute({
218
124
  method: "GET",
@@ -223,21 +129,23 @@ describe("fragment.handler", () => {
223
129
  },
224
130
  });
225
131
 
226
- const response = await testFragment.handler(route);
132
+ const testFragment = createFragmentForTest(fragment, [route], {
133
+ config: { apiKey: "test-key" },
134
+ });
135
+
136
+ const response = await testFragment.callRoute("GET", "/test");
227
137
 
228
138
  expect(response.type).toBe("json");
229
139
  if (response.type === "json") {
230
140
  expect(response.status).toBe(200);
231
141
  expect(response.data).toEqual({ message: "hello" });
232
142
  expect(response.headers).toBeInstanceOf(Headers);
143
+ expectTypeOf(response.data).toMatchObjectType<{ message: string }>();
233
144
  }
234
145
  });
235
146
 
236
147
  it("should handle empty response", async () => {
237
148
  const fragment = defineFragment<{ apiKey: string }>("test");
238
- const testFragment = createFragmentForTest(fragment, {
239
- config: { apiKey: "test-key" },
240
- });
241
149
 
242
150
  const route = defineRoute({
243
151
  method: "DELETE",
@@ -247,7 +155,11 @@ describe("fragment.handler", () => {
247
155
  },
248
156
  });
249
157
 
250
- const response = await testFragment.handler(route);
158
+ const testFragment = createFragmentForTest(fragment, [route], {
159
+ config: { apiKey: "test-key" },
160
+ });
161
+
162
+ const response = await testFragment.callRoute("DELETE", "/test");
251
163
 
252
164
  expect(response.type).toBe("empty");
253
165
  if (response.type === "empty") {
@@ -258,9 +170,6 @@ describe("fragment.handler", () => {
258
170
 
259
171
  it("should handle error response", async () => {
260
172
  const fragment = defineFragment<{ apiKey: string }>("test");
261
- const testFragment = createFragmentForTest(fragment, {
262
- config: { apiKey: "test-key" },
263
- });
264
173
 
265
174
  const route = defineRoute({
266
175
  method: "GET",
@@ -271,7 +180,11 @@ describe("fragment.handler", () => {
271
180
  },
272
181
  });
273
182
 
274
- const response = await testFragment.handler(route);
183
+ const testFragment = createFragmentForTest(fragment, [route], {
184
+ config: { apiKey: "test-key" },
185
+ });
186
+
187
+ const response = await testFragment.callRoute("GET", "/test");
275
188
 
276
189
  expect(response.type).toBe("error");
277
190
  if (response.type === "error") {
@@ -283,9 +196,6 @@ describe("fragment.handler", () => {
283
196
 
284
197
  it("should handle JSON stream response", async () => {
285
198
  const fragment = defineFragment<{ apiKey: string }>("test");
286
- const testFragment = createFragmentForTest(fragment, {
287
- config: { apiKey: "test-key" },
288
- });
289
199
 
290
200
  const route = defineRoute({
291
201
  method: "GET",
@@ -300,7 +210,11 @@ describe("fragment.handler", () => {
300
210
  },
301
211
  });
302
212
 
303
- const response = await testFragment.handler(route);
213
+ const testFragment = createFragmentForTest(fragment, [route], {
214
+ config: { apiKey: "test-key" },
215
+ });
216
+
217
+ const response = await testFragment.callRoute("GET", "/test/stream");
304
218
 
305
219
  expect(response.type).toBe("jsonStream");
306
220
  if (response.type === "jsonStream") {
@@ -314,6 +228,7 @@ describe("fragment.handler", () => {
314
228
  }
315
229
 
316
230
  expect(items).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }]);
231
+ expectTypeOf(items[0]).toMatchObjectType<{ value: number }>();
317
232
  }
318
233
  });
319
234
 
@@ -323,10 +238,6 @@ describe("fragment.handler", () => {
323
238
  getCount: () => 42,
324
239
  }));
325
240
 
326
- const testFragment = createFragmentForTest(fragment, {
327
- config: { apiKey: "test-key" },
328
- });
329
-
330
241
  type Config = { apiKey: string };
331
242
  type Deps = {};
332
243
  type Services = { getGreeting: (name: string) => string; getCount: () => number };
@@ -350,11 +261,12 @@ describe("fragment.handler", () => {
350
261
  }),
351
262
  ]);
352
263
 
353
- const routes = [routeFactory] as const;
354
- const [greetingRoute, countRoute] = testFragment.initRoutes(routes);
264
+ const testFragment = createFragmentForTest(fragment, [routeFactory], {
265
+ config: { apiKey: "test-key" },
266
+ });
355
267
 
356
268
  // Test first route
357
- const greetingResponse = await testFragment.handler(greetingRoute, {
269
+ const greetingResponse = await testFragment.callRoute("GET", "/greeting/:name", {
358
270
  pathParams: { name: "World" },
359
271
  });
360
272
 
@@ -364,7 +276,7 @@ describe("fragment.handler", () => {
364
276
  }
365
277
 
366
278
  // Test second route
367
- const countResponse = await testFragment.handler(countRoute);
279
+ const countResponse = await testFragment.callRoute("GET", "/count");
368
280
 
369
281
  expect(countResponse.type).toBe("json");
370
282
  if (countResponse.type === "json") {
@@ -374,9 +286,6 @@ describe("fragment.handler", () => {
374
286
 
375
287
  it("should handle path parameters", async () => {
376
288
  const fragment = defineFragment<{}>("test");
377
- const testFragment = createFragmentForTest(fragment, {
378
- config: {},
379
- });
380
289
 
381
290
  const route = defineRoute({
382
291
  method: "GET",
@@ -387,7 +296,11 @@ describe("fragment.handler", () => {
387
296
  },
388
297
  });
389
298
 
390
- const response = await testFragment.handler(route, {
299
+ const testFragment = createFragmentForTest(fragment, [route], {
300
+ config: {},
301
+ });
302
+
303
+ const response = await testFragment.callRoute("GET", "/users/:id", {
391
304
  pathParams: { id: "123" },
392
305
  });
393
306
 
@@ -399,9 +312,6 @@ describe("fragment.handler", () => {
399
312
 
400
313
  it("should handle query parameters", async () => {
401
314
  const fragment = defineFragment<{}>("test");
402
- const testFragment = createFragmentForTest(fragment, {
403
- config: {},
404
- });
405
315
 
406
316
  const route = defineRoute({
407
317
  method: "GET",
@@ -412,7 +322,11 @@ describe("fragment.handler", () => {
412
322
  },
413
323
  });
414
324
 
415
- const response = await testFragment.handler(route, {
325
+ const testFragment = createFragmentForTest(fragment, [route], {
326
+ config: {},
327
+ });
328
+
329
+ const response = await testFragment.callRoute("GET", "/search", {
416
330
  query: { q: "test" },
417
331
  });
418
332
 
@@ -424,9 +338,6 @@ describe("fragment.handler", () => {
424
338
 
425
339
  it("should handle request body", async () => {
426
340
  const fragment = defineFragment<{}>("test");
427
- const testFragment = createFragmentForTest(fragment, {
428
- config: {},
429
- });
430
341
 
431
342
  const route = defineRoute({
432
343
  method: "POST",
@@ -442,7 +353,11 @@ describe("fragment.handler", () => {
442
353
  },
443
354
  });
444
355
 
445
- const response = await testFragment.handler(route, {
356
+ const testFragment = createFragmentForTest(fragment, [route], {
357
+ config: {},
358
+ });
359
+
360
+ const response = await testFragment.callRoute("POST", "/users", {
446
361
  body: { name: "John", email: "john@example.com" },
447
362
  });
448
363
 
@@ -452,12 +367,37 @@ describe("fragment.handler", () => {
452
367
  }
453
368
  });
454
369
 
455
- it("should handle custom headers", async () => {
370
+ it("should have the right types", () => {
456
371
  const fragment = defineFragment<{}>("test");
457
- const testFragment = createFragmentForTest(fragment, {
372
+
373
+ const route = defineRoute({
374
+ method: "POST",
375
+ path: "/users",
376
+ inputSchema: z.object({ name: z.string(), email: z.string() }),
377
+ outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
378
+ handler: async ({ input }, { json }) => {
379
+ if (input) {
380
+ const data = await input.valid();
381
+ return json({ id: 1, name: data.name, email: data.email });
382
+ }
383
+ return json({ id: 1, name: "", email: "" });
384
+ },
385
+ });
386
+
387
+ const testFragment = createFragmentForTest(fragment, [route], {
458
388
  config: {},
459
389
  });
460
390
 
391
+ // Check what type body is expected to have
392
+ type InputOptions = Parameters<typeof testFragment.callRoute<"POST", "/users">>[2];
393
+ type BodyType = NonNullable<NonNullable<InputOptions>["body"]>;
394
+
395
+ expectTypeOf<BodyType>().toMatchObjectType<{ name: string; email: string }>();
396
+ });
397
+
398
+ it("should handle custom headers", async () => {
399
+ const fragment = defineFragment<{}>("test");
400
+
461
401
  const route = defineRoute({
462
402
  method: "GET",
463
403
  path: "/test",
@@ -467,7 +407,11 @@ describe("fragment.handler", () => {
467
407
  },
468
408
  });
469
409
 
470
- const response = await testFragment.handler(route, {
410
+ const testFragment = createFragmentForTest(fragment, [route], {
411
+ config: {},
412
+ });
413
+
414
+ const response = await testFragment.callRoute("GET", "/test", {
471
415
  headers: { authorization: "Bearer token" },
472
416
  });
473
417
 
@@ -479,9 +423,6 @@ describe("fragment.handler", () => {
479
423
 
480
424
  it("should properly type path params", async () => {
481
425
  const fragment = defineFragment<{}>("test");
482
- const testFragment = createFragmentForTest(fragment, {
483
- config: {},
484
- });
485
426
 
486
427
  const route = defineRoute({
487
428
  method: "GET",
@@ -492,7 +433,11 @@ describe("fragment.handler", () => {
492
433
  },
493
434
  });
494
435
 
495
- const response = await testFragment.handler(route, {
436
+ const testFragment = createFragmentForTest(fragment, [route], {
437
+ config: {},
438
+ });
439
+
440
+ const response = await testFragment.callRoute("GET", "/orgs/:orgId/users/:userId", {
496
441
  pathParams: { orgId: "123", userId: "456" },
497
442
  });
498
443