@fragno-dev/core 0.1.6 → 0.1.8

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 (85) hide show
  1. package/.turbo/turbo-build.log +46 -54
  2. package/CHANGELOG.md +12 -0
  3. package/dist/api/api.d.ts +2 -2
  4. package/dist/api/api.js +3 -2
  5. package/dist/api/fragment-builder.d.ts +2 -4
  6. package/dist/api/fragment-builder.js +1 -1
  7. package/dist/api/fragment-instantiation.d.ts +2 -4
  8. package/dist/api/fragment-instantiation.js +3 -5
  9. package/dist/api/route.d.ts +2 -3
  10. package/dist/api/route.js +1 -1
  11. package/dist/api-BFrUCIsF.d.ts +963 -0
  12. package/dist/api-BFrUCIsF.d.ts.map +1 -0
  13. package/dist/client/client.d.ts +1 -3
  14. package/dist/client/client.js +4 -5
  15. package/dist/client/client.svelte.d.ts +2 -3
  16. package/dist/client/client.svelte.d.ts.map +1 -1
  17. package/dist/client/client.svelte.js +4 -5
  18. package/dist/client/client.svelte.js.map +1 -1
  19. package/dist/client/react.d.ts +2 -3
  20. package/dist/client/react.d.ts.map +1 -1
  21. package/dist/client/react.js +4 -5
  22. package/dist/client/react.js.map +1 -1
  23. package/dist/client/solid.d.ts +2 -3
  24. package/dist/client/solid.d.ts.map +1 -1
  25. package/dist/client/solid.js +4 -5
  26. package/dist/client/solid.js.map +1 -1
  27. package/dist/client/vanilla.d.ts +2 -3
  28. package/dist/client/vanilla.d.ts.map +1 -1
  29. package/dist/client/vanilla.js +4 -5
  30. package/dist/client/vanilla.js.map +1 -1
  31. package/dist/client/vue.d.ts +2 -3
  32. package/dist/client/vue.d.ts.map +1 -1
  33. package/dist/client/vue.js +8 -9
  34. package/dist/client/vue.js.map +1 -1
  35. package/dist/{client-DJfCJiHK.js → client-DAFHcKqA.js} +15 -8
  36. package/dist/client-DAFHcKqA.js.map +1 -0
  37. package/dist/fragment-builder-Boh2vNHq.js +108 -0
  38. package/dist/fragment-builder-Boh2vNHq.js.map +1 -0
  39. package/dist/fragment-instantiation-DUT-HLl1.js +898 -0
  40. package/dist/fragment-instantiation-DUT-HLl1.js.map +1 -0
  41. package/dist/integrations/react-ssr.js +1 -1
  42. package/dist/mod.d.ts +2 -4
  43. package/dist/mod.js +4 -6
  44. package/dist/{route-C5Uryylh.js → route-C4CyNHkC.js} +8 -3
  45. package/dist/route-C4CyNHkC.js.map +1 -0
  46. package/dist/{ssr-BByDVfFD.js → ssr-kyKI7pqH.js} +1 -1
  47. package/dist/{ssr-BByDVfFD.js.map → ssr-kyKI7pqH.js.map} +1 -1
  48. package/dist/test/test.d.ts +6 -7
  49. package/dist/test/test.d.ts.map +1 -1
  50. package/dist/test/test.js +9 -7
  51. package/dist/test/test.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/api/api.ts +45 -6
  54. package/src/api/fragment-builder.ts +463 -25
  55. package/src/api/fragment-instantiation.test.ts +249 -7
  56. package/src/api/fragment-instantiation.ts +283 -16
  57. package/src/api/fragment-services.test.ts +462 -0
  58. package/src/api/fragment.test.ts +65 -17
  59. package/src/api/internal/path-type.test.ts +7 -7
  60. package/src/api/internal/path.ts +1 -1
  61. package/src/api/request-middleware.test.ts +6 -3
  62. package/src/api/route.test.ts +111 -1
  63. package/src/api/route.ts +323 -14
  64. package/src/client/client-types.test.ts +4 -4
  65. package/src/client/client.test.ts +77 -0
  66. package/src/client/client.ts +31 -12
  67. package/src/mod.ts +11 -1
  68. package/src/test/test.test.ts +20 -15
  69. package/src/test/test.ts +48 -9
  70. package/dist/api-CoCkNi6h.d.ts +0 -377
  71. package/dist/api-CoCkNi6h.d.ts.map +0 -1
  72. package/dist/api-DngJDcmO.js +0 -54
  73. package/dist/api-DngJDcmO.js.map +0 -1
  74. package/dist/client-DJfCJiHK.js.map +0 -1
  75. package/dist/fragment-builder-8-tiECi5.d.ts +0 -408
  76. package/dist/fragment-builder-8-tiECi5.d.ts.map +0 -1
  77. package/dist/fragment-builder-DOnCVBqc.js +0 -47
  78. package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
  79. package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
  80. package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
  81. package/dist/request-output-context-CdIjwmEN.js +0 -320
  82. package/dist/request-output-context-CdIjwmEN.js.map +0 -1
  83. package/dist/route-C5Uryylh.js.map +0 -1
  84. package/dist/route-mGLYSUvD.d.ts +0 -26
  85. package/dist/route-mGLYSUvD.d.ts.map +0 -1
@@ -0,0 +1,462 @@
1
+ import { describe, test, expect, expectTypeOf } from "vitest";
2
+ import { defineFragment } from "./fragment-builder";
3
+ import { createFragment, instantiateFragment } from "./fragment-instantiation";
4
+
5
+ // Test service interface definitions
6
+ interface IEmailService {
7
+ sendEmail(to: string, subject: string, body: string): Promise<void>;
8
+ }
9
+
10
+ interface ILogger {
11
+ log(message: string): void;
12
+ }
13
+
14
+ describe("Fragment Service System", () => {
15
+ describe("usesService", () => {
16
+ test("should declare required service by default", () => {
17
+ const fragment = defineFragment<{}>("test-fragment").usesService<"email", IEmailService>(
18
+ "email",
19
+ );
20
+
21
+ expect(fragment.definition.usedServices).toBeDefined();
22
+ expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
23
+ });
24
+
25
+ test("should declare optional service with { optional: true }", () => {
26
+ const fragment = defineFragment<{}>("test-fragment").usesService<"email", IEmailService>(
27
+ "email",
28
+ { optional: true },
29
+ );
30
+
31
+ expect(fragment.definition.usedServices).toBeDefined();
32
+ expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: false });
33
+ });
34
+
35
+ test("should support multiple required services", () => {
36
+ const fragment = defineFragment<{}>("test-fragment")
37
+ .usesService<"email", IEmailService>("email")
38
+ .usesService<"logger", ILogger>("logger");
39
+
40
+ expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
41
+ expect(fragment.definition.usedServices?.logger).toEqual({ name: "logger", required: true });
42
+ });
43
+
44
+ test("should support mixing required and optional services", () => {
45
+ const fragment = defineFragment<{}>("test-fragment")
46
+ .usesService<"email", IEmailService>("email")
47
+ .usesService<"logger", ILogger>("logger", { optional: true });
48
+
49
+ expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
50
+ expect(fragment.definition.usedServices?.logger).toEqual({ name: "logger", required: false });
51
+ });
52
+
53
+ test("should preserve other fragment properties", () => {
54
+ const fragment = defineFragment<{ apiKey: string }>("test-fragment")
55
+ .withDependencies(() => ({ dep: "value" }))
56
+ .usesService<"email", IEmailService>("email");
57
+
58
+ expect(fragment.definition.name).toBe("test-fragment");
59
+ expect(fragment.definition.usedServices?.email).toBeDefined();
60
+ });
61
+
62
+ test("should have correct type inference for required service", () => {
63
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
64
+
65
+ expectTypeOf(fragment).toMatchTypeOf<{
66
+ definition: {
67
+ usedServices?: {
68
+ email: { name: string; required: boolean };
69
+ };
70
+ };
71
+ }>();
72
+ });
73
+
74
+ test("should have correct type inference for optional service", () => {
75
+ const fragment = defineFragment<{}>("test").usesService<"logger", ILogger>("logger", {
76
+ optional: true,
77
+ });
78
+
79
+ expectTypeOf(fragment).toMatchTypeOf<{
80
+ definition: {
81
+ usedServices?: {
82
+ logger: { name: string; required: boolean };
83
+ };
84
+ };
85
+ }>();
86
+ });
87
+ });
88
+
89
+ describe("providesService", () => {
90
+ test("should declare provided service implementation", () => {
91
+ const emailImpl: IEmailService = {
92
+ sendEmail: async () => {},
93
+ };
94
+
95
+ const fragment = defineFragment<{}>("test-fragment").providesService(
96
+ "email",
97
+ ({ defineService }) => defineService(emailImpl),
98
+ );
99
+
100
+ expect(fragment.definition.providedServices).toBeDefined();
101
+ });
102
+
103
+ test("should support multiple provided services", () => {
104
+ const emailImpl: IEmailService = {
105
+ sendEmail: async () => {},
106
+ };
107
+
108
+ const loggerImpl: ILogger = {
109
+ log: () => {},
110
+ };
111
+
112
+ const _fragment = defineFragment<{}>("test-fragment")
113
+ .providesService("email", ({ defineService }) => defineService(emailImpl))
114
+ .providesService("logger", ({ defineService }) => defineService(loggerImpl));
115
+ });
116
+ });
117
+
118
+ describe("Service metadata", () => {
119
+ test("should store service metadata in definition", () => {
120
+ const fragment = defineFragment<{}>("test")
121
+ .usesService<"email", IEmailService>("email")
122
+ .usesService<"logger", ILogger>("logger", { optional: true });
123
+
124
+ expect(fragment.definition.usedServices?.email?.required).toBe(true);
125
+ expect(fragment.definition.usedServices?.logger?.required).toBe(false);
126
+ });
127
+
128
+ test("should store provided services in definition", () => {
129
+ const emailImpl: IEmailService = {
130
+ sendEmail: async () => {},
131
+ };
132
+
133
+ const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
134
+ defineService(emailImpl),
135
+ );
136
+
137
+ expect(typeof fragment.definition.providedServices).toBe("object");
138
+ });
139
+
140
+ test("should allow fragments without any services", () => {
141
+ const fragment = defineFragment<{}>("test");
142
+
143
+ expect(fragment.definition.usedServices).toBeUndefined();
144
+ expect(fragment.definition.providedServices).toBeUndefined();
145
+ });
146
+ });
147
+
148
+ describe("Type safety", () => {
149
+ test("Unnamed services should have correct types (using defineService)", () => {
150
+ const fragment = defineFragment<{}>("test").providesService(({ defineService }) =>
151
+ defineService({
152
+ sendEmail: async () => {},
153
+ }),
154
+ );
155
+
156
+ const instance = createFragment(fragment, {}, [], {});
157
+ expect(instance.services.sendEmail).toBeDefined();
158
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
159
+ });
160
+
161
+ test("Named services should have correct types (using defineService)", () => {
162
+ const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
163
+ defineService({
164
+ sendEmail: async () => {},
165
+ }),
166
+ );
167
+
168
+ const instance = createFragment(fragment, {}, [], {});
169
+ expect(instance.services.email.sendEmail).toBeDefined();
170
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
171
+ });
172
+
173
+ test("Unnamed services should have correct types (using object)", () => {
174
+ const fragment = defineFragment<{}>("test").providesService({
175
+ sendEmail: async () => {},
176
+ });
177
+
178
+ const instance = createFragment(fragment, {}, [], {});
179
+ expect(instance.services.sendEmail).toBeDefined();
180
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
181
+ });
182
+
183
+ test("Unnamed services should have correct types (using callback with context)", () => {
184
+ const fragment = defineFragment<{}>("test").providesService(({ defineService }) =>
185
+ defineService({
186
+ sendEmail: async () => {},
187
+ }),
188
+ );
189
+
190
+ const instance = createFragment(fragment, {}, [], {});
191
+ expect(instance.services.sendEmail).toBeDefined();
192
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
193
+ });
194
+
195
+ test("Unnamed services should have correct types (using 0-arity factory)", () => {
196
+ const fragment = defineFragment<{}>("test").providesService(() => ({
197
+ sendEmail: async () => {},
198
+ }));
199
+
200
+ const instance = createFragment(fragment, {}, [], {});
201
+ expect(instance.services.sendEmail).toBeDefined();
202
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
203
+ });
204
+
205
+ test("Named services should have correct types (using object)", () => {
206
+ const fragment = defineFragment<{}>("test").providesService("email", {
207
+ sendEmail: async () => {},
208
+ });
209
+
210
+ const instance = createFragment(fragment, {}, [], {});
211
+ expect(instance.services.email.sendEmail).toBeDefined();
212
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
213
+ });
214
+
215
+ test("usesService (required)", () => {
216
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
217
+
218
+ const emailImpl: IEmailService = {
219
+ sendEmail: async () => {},
220
+ };
221
+
222
+ const instance = createFragment(
223
+ fragment,
224
+ {},
225
+ [],
226
+ {},
227
+ {
228
+ email: emailImpl,
229
+ },
230
+ );
231
+
232
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
233
+ (to: string, subject: string, body: string) => void
234
+ >();
235
+ });
236
+
237
+ test("usesService (required) - builder style", () => {
238
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
239
+
240
+ const emailImpl: IEmailService = {
241
+ sendEmail: async () => {},
242
+ };
243
+
244
+ const instance = instantiateFragment(fragment).withServices({ email: emailImpl }).build();
245
+
246
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
247
+ (to: string, subject: string, body: string) => void
248
+ >();
249
+ });
250
+
251
+ test("usesService (optional)", () => {
252
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email", {
253
+ optional: true,
254
+ });
255
+
256
+ const instance = createFragment(fragment, {}, [], {});
257
+ // For optional services, the service itself might be undefined
258
+ expectTypeOf<typeof instance.services.email>().toExtend<IEmailService | undefined>();
259
+
260
+ // If provided, the service should have the correct type
261
+ if (instance.services.email) {
262
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
263
+ (to: string, subject: string, body: string) => Promise<void>
264
+ >();
265
+ }
266
+ });
267
+
268
+ test("provided services should have correct types", () => {
269
+ const emailImpl: IEmailService = {
270
+ sendEmail: async () => {},
271
+ };
272
+
273
+ const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
274
+ defineService(emailImpl),
275
+ );
276
+
277
+ // providedServices stores an object with service names as keys and factory functions as values
278
+ expect(fragment.definition.providedServices).toBeDefined();
279
+ expect(typeof fragment.definition.providedServices).toBe("object");
280
+ });
281
+
282
+ test("Named services should have correct types (using callback with context)", () => {
283
+ const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
284
+ defineService({
285
+ sendEmail: async () => {},
286
+ }),
287
+ );
288
+
289
+ const instance = createFragment(fragment, {}, [], {});
290
+ expect(instance.services.email.sendEmail).toBeDefined();
291
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
292
+ });
293
+
294
+ test("Named services should have correct types (using 0-arity factory)", () => {
295
+ const fragment = defineFragment<{}>("test").providesService("email", () => ({
296
+ sendEmail: async () => {},
297
+ }));
298
+
299
+ const instance = createFragment(fragment, {}, [], {});
300
+ expect(instance.services.email.sendEmail).toBeDefined();
301
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
302
+ });
303
+ });
304
+
305
+ describe("Error handling", () => {
306
+ test("should throw error when required service is not provided", () => {
307
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
308
+
309
+ expect(() => {
310
+ createFragment(fragment, {}, [], {});
311
+ }).toThrow("Fragment 'test' requires service 'email' but it was not provided");
312
+ });
313
+
314
+ test("should not throw when optional service is not provided", () => {
315
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email", {
316
+ optional: true,
317
+ });
318
+
319
+ expect(() => {
320
+ createFragment(fragment, {}, [], {});
321
+ }).not.toThrow();
322
+ });
323
+ });
324
+
325
+ describe("Service dependencies and composition", () => {
326
+ test("provided service can access used services", () => {
327
+ const emailImpl: IEmailService = {
328
+ sendEmail: async () => {},
329
+ };
330
+
331
+ const fragment = defineFragment<{}>("test")
332
+ .usesService<"email", IEmailService>("email")
333
+ .providesService(({ deps }) => ({
334
+ sendWelcomeEmail: async (to: string) => {
335
+ await deps.email.sendEmail(to, "Welcome", "Welcome to our service!");
336
+ },
337
+ }));
338
+
339
+ const instance = createFragment(fragment, {}, [], {}, { email: emailImpl });
340
+
341
+ expect(instance.services.sendWelcomeEmail).toBeDefined();
342
+ expect(typeof instance.services.sendWelcomeEmail).toBe("function");
343
+ });
344
+
345
+ test("provided service can access used services - builder style", () => {
346
+ const emailImpl: IEmailService = {
347
+ sendEmail: async () => {},
348
+ };
349
+
350
+ const fragment = defineFragment<{}>("test")
351
+ .usesService<"email", IEmailService>("email")
352
+ .providesService(({ deps }) => ({
353
+ sendWelcomeEmail: async (to: string) => {
354
+ await deps.email.sendEmail(to, "Welcome", "Welcome to our service!");
355
+ },
356
+ }));
357
+
358
+ const instance = instantiateFragment(fragment).withServices({ email: emailImpl }).build();
359
+
360
+ expect(instance.services.sendWelcomeEmail).toBeDefined();
361
+ expect(typeof instance.services.sendWelcomeEmail).toBe("function");
362
+ });
363
+
364
+ test("provided service can access config", () => {
365
+ const fragment = defineFragment<{ apiKey: string }>("test").providesService(({ config }) => ({
366
+ getApiKey: () => config.apiKey,
367
+ }));
368
+
369
+ const instance = createFragment(fragment, { apiKey: "test-key" }, [], {});
370
+
371
+ expect(instance.services.getApiKey()).toBe("test-key");
372
+ });
373
+
374
+ test("provided service can access deps from withDependencies", () => {
375
+ const fragment = defineFragment<{ apiKey: string }>("test")
376
+ .withDependencies(({ config }) => ({
377
+ client: { key: config.apiKey },
378
+ }))
379
+ .providesService(({ deps }) => ({
380
+ getClient: () => deps.client,
381
+ }));
382
+
383
+ const instance = createFragment(fragment, { apiKey: "test-key" }, [], {});
384
+
385
+ expect(instance.services.getClient()).toEqual({ key: "test-key" });
386
+ });
387
+ });
388
+
389
+ describe("Service chaining and multiple services", () => {
390
+ test("should support chaining multiple provided services", () => {
391
+ const fragment = defineFragment<{}>("test")
392
+ .providesService("email", {
393
+ sendEmail: async () => {},
394
+ })
395
+ .providesService("logger", {
396
+ log: () => {},
397
+ });
398
+
399
+ const instance = createFragment(fragment, {}, [], {});
400
+ expect(instance.services.email.sendEmail).toBeDefined();
401
+ expect(instance.services.logger.log).toBeDefined();
402
+ });
403
+
404
+ test("should support mixing unnamed and named provided services", () => {
405
+ const fragment = defineFragment<{}>("test")
406
+ .providesService({
407
+ helper: () => "help",
408
+ })
409
+ .providesService("email", {
410
+ sendEmail: async () => {},
411
+ });
412
+
413
+ const instance = createFragment(fragment, {}, [], {});
414
+ expect(instance.services.helper).toBeDefined();
415
+ expect(instance.services.email.sendEmail).toBeDefined();
416
+ });
417
+ });
418
+
419
+ describe("Optional service runtime behavior", () => {
420
+ test("should handle optional service when not provided", () => {
421
+ const fragment = defineFragment<{}>("test")
422
+ .usesService<"email", IEmailService>("email", { optional: true })
423
+ .providesService(({ deps }) => ({
424
+ maybeSendEmail: async (to: string) => {
425
+ if (deps.email) {
426
+ await deps.email.sendEmail(to, "Subject", "Body");
427
+ return true;
428
+ }
429
+ return false;
430
+ },
431
+ }));
432
+
433
+ const instance = createFragment(fragment, {}, [], {});
434
+
435
+ expect(instance.services.maybeSendEmail).toBeDefined();
436
+ // Should not throw when optional service is not provided
437
+ });
438
+
439
+ test("should handle optional service when provided", () => {
440
+ const emailImpl: IEmailService = {
441
+ sendEmail: async () => {},
442
+ };
443
+
444
+ const fragment = defineFragment<{}>("test")
445
+ .usesService<"email", IEmailService>("email", { optional: true })
446
+ .providesService(({ deps }) => ({
447
+ maybeSendEmail: async (to: string) => {
448
+ if (deps.email) {
449
+ await deps.email.sendEmail(to, "Subject", "Body");
450
+ return true;
451
+ }
452
+ return false;
453
+ },
454
+ }));
455
+
456
+ const instance = createFragment(fragment, {}, [], {}, { email: emailImpl });
457
+
458
+ expect(instance.services.email).toBeDefined();
459
+ expect(instance.services.maybeSendEmail).toBeDefined();
460
+ });
461
+ });
462
+ });
@@ -58,7 +58,7 @@ describe("new-fragment API", () => {
58
58
  >();
59
59
  });
60
60
 
61
- test("withServices has access to dependencies and config", () => {
61
+ test("providesService has access to dependencies and config", () => {
62
62
  const _config = {
63
63
  apiKey: "test-key",
64
64
  baseUrl: "https://api.example.com",
@@ -73,11 +73,11 @@ describe("new-fragment API", () => {
73
73
 
74
74
  return { httpClient: { baseUrl: config.baseUrl } };
75
75
  })
76
- .withServices(({ config, deps }) => {
76
+ .providesService(({ config, deps, defineService }) => {
77
77
  expectTypeOf(config).toEqualTypeOf<typeof _config>();
78
78
  expectTypeOf(deps).toEqualTypeOf<{ httpClient: { baseUrl: string } }>();
79
79
 
80
- return {
80
+ return defineService({
81
81
  userService: {
82
82
  getUser: async (id: string) => ({ id, name: "Test User" }),
83
83
  },
@@ -85,7 +85,7 @@ describe("new-fragment API", () => {
85
85
  get: (_key: string): string => crypto.randomUUID(),
86
86
  set: (_key: string, _value: string) => {},
87
87
  },
88
- };
88
+ });
89
89
  });
90
90
 
91
91
  expectTypeOf<typeof _fragment>().toEqualTypeOf<
@@ -165,7 +165,9 @@ describe("new-fragment API", () => {
165
165
  expectTypeOf(lib2).toEqualTypeOf<
166
166
  FragmentBuilder<typeof _config, { dep1: string }, Empty, Empty>
167
167
  >();
168
- const lib3 = lib2.withServices(() => ({ service1: "value1" }));
168
+ const lib3 = lib2.providesService(({ defineService }) =>
169
+ defineService({ service1: "value1" }),
170
+ );
169
171
  expectTypeOf(lib3).toEqualTypeOf<
170
172
  FragmentBuilder<typeof _config, { dep1: string }, { service1: string }, Empty>
171
173
  >();
@@ -182,9 +184,11 @@ describe("new-fragment API", () => {
182
184
  .withDependencies(({ config }) => ({
183
185
  client: `Client for ${config.apiKey}`,
184
186
  }))
185
- .withServices(({ deps }) => ({
186
- service: `Service using ${deps.client}`,
187
- }));
187
+ .providesService(({ deps, defineService }) =>
188
+ defineService({
189
+ service: `Service using ${deps.client}`,
190
+ }),
191
+ );
188
192
 
189
193
  expect(fragment.definition.name).toBe("my-lib");
190
194
  expect(fragment.definition.dependencies).toBeDefined();
@@ -220,9 +224,11 @@ describe("new-fragment API", () => {
220
224
  .withDependencies(() => ({
221
225
  formatter: (s: string) => s.toUpperCase(),
222
226
  }))
223
- .withServices(() => ({
224
- logger: { log: (s: string) => console.log(s) },
225
- }));
227
+ .providesService(({ defineService }) =>
228
+ defineService({
229
+ logger: { log: (s: string) => console.log(s) },
230
+ }),
231
+ );
226
232
 
227
233
  const fragment = createFragment(fragmentDef, { prefix: "Hello" }, [routeFactory], {});
228
234
 
@@ -297,7 +303,7 @@ describe("new-fragment API", () => {
297
303
 
298
304
  const fragmentDef = defineFragment("test")
299
305
  .withDependencies(() => ({ tool: "hammer" }))
300
- .withServices(() => ({ storage: "memory" }));
306
+ .providesService(({ defineService }) => defineService({ storage: "memory" }));
301
307
 
302
308
  createFragment(fragmentDef, { setting: "value" }, [routeFactory], {});
303
309
 
@@ -309,11 +315,13 @@ describe("new-fragment API", () => {
309
315
 
310
316
  describe("Type constraints", () => {
311
317
  test("Services must extend Record<string, unknown>", () => {
312
- const fragmentDef = defineFragment("test").withServices(() => ({
313
- validService: { method: () => {} },
314
- anotherService: "string value",
315
- numberService: 123,
316
- }));
318
+ const fragmentDef = defineFragment("test").providesService(({ defineService }) =>
319
+ defineService({
320
+ validService: { method: () => {} },
321
+ anotherService: "string value",
322
+ numberService: 123,
323
+ }),
324
+ );
317
325
 
318
326
  const _fragment = createFragment(fragmentDef, {}, [], {});
319
327
 
@@ -534,4 +542,44 @@ describe("new-fragment API", () => {
534
542
  expect(fragment.mountRoute).toBe("/custom");
535
543
  });
536
544
  });
545
+
546
+ describe("Route handler this context", () => {
547
+ test("this context type is RequestThisContext for standard fragments", () => {
548
+ const fragmentDef = defineFragment("test");
549
+
550
+ const routesFactory = defineRoutes().create(() => {
551
+ return [
552
+ defineRoute({
553
+ method: "GET",
554
+ path: "/test",
555
+ handler: async function (_, { json }) {
556
+ // this should be RequestThisContext
557
+ // (we can't easily test the exact type due to how TypeScript handles 'this')
558
+ expect(this).toBeDefined();
559
+ expect(typeof this).toBe("object");
560
+ return json({ ok: true });
561
+ },
562
+ }),
563
+ ];
564
+ });
565
+
566
+ const _fragment = createFragment(fragmentDef, {}, [routesFactory], {});
567
+ expect(_fragment).toBeDefined();
568
+ });
569
+
570
+ test("defineRoute without defineRoutes defaults to RequestThisContext", () => {
571
+ const route = defineRoute({
572
+ method: "GET",
573
+ path: "/test",
574
+ handler: async function (_, { json }) {
575
+ // this defaults to RequestThisContext
576
+ expect(this).toBeDefined();
577
+ expect(typeof this).toBe("object");
578
+ return json({ ok: true });
579
+ },
580
+ });
581
+
582
+ expect(route).toBeDefined();
583
+ });
584
+ });
537
585
  });
@@ -446,39 +446,39 @@ test("MaybeExtractPathParamsOrWiden type tests", () => {
446
446
  test("QueryParamsHint type tests", () => {
447
447
  // Basic usage with string union
448
448
  expectTypeOf<QueryParamsHint<"page" | "limit">>().toEqualTypeOf<
449
- Partial<Record<"page" | "limit", string>> & Record<string, string>
449
+ Partial<Record<"page" | "limit", string>> & Record<string, string | undefined>
450
450
  >();
451
451
 
452
452
  // Single parameter hint
453
453
  expectTypeOf<QueryParamsHint<"search">>().toEqualTypeOf<
454
- Partial<Record<"search", string>> & Record<string, string>
454
+ Partial<Record<"search", string>> & Record<string, string | undefined>
455
455
  >();
456
456
 
457
457
  // Empty hint (never) - should still allow any string keys
458
458
  expectTypeOf<QueryParamsHint<never>>().toEqualTypeOf<
459
- Partial<Record<never, string>> & Record<string, string>
459
+ Partial<Record<never, string>> & Record<string, string | undefined>
460
460
  >();
461
461
 
462
462
  // With custom value type
463
463
  expectTypeOf<QueryParamsHint<"page" | "limit", number>>().toEqualTypeOf<
464
- Partial<Record<"page" | "limit", number>> & Record<string, number>
464
+ Partial<Record<"page" | "limit", number>> & Record<string, number | undefined>
465
465
  >();
466
466
 
467
467
  // With boolean value type
468
468
  expectTypeOf<QueryParamsHint<"enabled" | "debug", boolean>>().toEqualTypeOf<
469
- Partial<Record<"enabled" | "debug", boolean>> & Record<string, boolean>
469
+ Partial<Record<"enabled" | "debug", boolean>> & Record<string, boolean | undefined>
470
470
  >();
471
471
 
472
472
  // With union value type
473
473
  type StringOrNumber = string | number;
474
474
  expectTypeOf<QueryParamsHint<"value", StringOrNumber>>().toEqualTypeOf<
475
- Partial<Record<"value", StringOrNumber>> & Record<string, StringOrNumber>
475
+ Partial<Record<"value", StringOrNumber>> & Record<string, StringOrNumber | undefined>
476
476
  >();
477
477
 
478
478
  // With custom object type
479
479
  type CustomType = { raw: string; parsed: boolean };
480
480
  expectTypeOf<QueryParamsHint<"data", CustomType>>().toEqualTypeOf<
481
- Partial<Record<"data", CustomType>> & Record<string, CustomType>
481
+ Partial<Record<"data", CustomType>> & Record<string, CustomType | undefined>
482
482
  >();
483
483
  });
484
484
 
@@ -124,7 +124,7 @@ export type HasPathParams<T extends string> = ExtractPathParamNames<T> extends n
124
124
  export type QueryParamsHint<TQueryParameters extends string, ValueType = string> = Partial<
125
125
  Record<TQueryParameters, ValueType>
126
126
  > &
127
- Record<string, ValueType>;
127
+ Record<string, ValueType | undefined>;
128
128
 
129
129
  // Runtime utilities
130
130
 
@@ -9,9 +9,12 @@ describe("Request Middleware", () => {
9
9
  test("middleware can intercept and return early", async () => {
10
10
  const config = { apiKey: "test" };
11
11
 
12
- const fragment = defineFragment<typeof config>("test-lib").withServices(() => ({
13
- auth: { isAuthorized: (token?: string) => token === "valid-token" },
14
- }));
12
+ const fragment = defineFragment<typeof config>("test-lib").providesService(
13
+ ({ defineService }) =>
14
+ defineService({
15
+ auth: { isAuthorized: (token?: string) => token === "valid-token" },
16
+ }),
17
+ );
15
18
 
16
19
  const routes = [
17
20
  defineRoute({