@fragno-dev/core 0.1.7 → 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 (79) hide show
  1. package/.turbo/turbo-build.log +45 -53
  2. package/CHANGELOG.md +6 -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 +4 -5
  34. package/dist/client/vue.js.map +1 -1
  35. package/dist/{client-C5LsYHEI.js → client-DAFHcKqA.js} +4 -4
  36. package/dist/{client-C5LsYHEI.js.map → client-DAFHcKqA.js.map} +1 -1
  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/request-middleware.test.ts +6 -3
  60. package/src/api/route.test.ts +111 -1
  61. package/src/api/route.ts +323 -14
  62. package/src/mod.ts +11 -1
  63. package/src/test/test.test.ts +20 -15
  64. package/src/test/test.ts +48 -9
  65. package/dist/api-BWN97TOr.d.ts +0 -377
  66. package/dist/api-BWN97TOr.d.ts.map +0 -1
  67. package/dist/api-DngJDcmO.js +0 -54
  68. package/dist/api-DngJDcmO.js.map +0 -1
  69. package/dist/fragment-builder-DOnCVBqc.js +0 -47
  70. package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
  71. package/dist/fragment-builder-MGr68GNb.d.ts +0 -409
  72. package/dist/fragment-builder-MGr68GNb.d.ts.map +0 -1
  73. package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
  74. package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
  75. package/dist/request-output-context-CdIjwmEN.js +0 -320
  76. package/dist/request-output-context-CdIjwmEN.js.map +0 -1
  77. package/dist/route-Bl9Zr1Yv.d.ts +0 -26
  78. package/dist/route-Bl9Zr1Yv.d.ts.map +0 -1
  79. package/dist/route-C5Uryylh.js.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
  });
@@ -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({