@fragno-dev/core 0.1.7 → 0.1.9

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 (183) hide show
  1. package/.turbo/turbo-build.log +131 -64
  2. package/CHANGELOG.md +19 -0
  3. package/dist/api/api.d.ts +38 -2
  4. package/dist/api/api.d.ts.map +1 -0
  5. package/dist/api/api.js +9 -2
  6. package/dist/api/api.js.map +1 -0
  7. package/dist/api/bind-services.d.ts +6 -0
  8. package/dist/api/bind-services.d.ts.map +1 -0
  9. package/dist/api/bind-services.js +20 -0
  10. package/dist/api/bind-services.js.map +1 -0
  11. package/dist/api/error.d.ts +26 -0
  12. package/dist/api/error.d.ts.map +1 -0
  13. package/dist/{api-DngJDcmO.js → api/error.js} +2 -8
  14. package/dist/api/error.js.map +1 -0
  15. package/dist/api/fragment-definition-builder.d.ts +313 -0
  16. package/dist/api/fragment-definition-builder.d.ts.map +1 -0
  17. package/dist/api/fragment-definition-builder.js +326 -0
  18. package/dist/api/fragment-definition-builder.js.map +1 -0
  19. package/dist/api/fragment-instantiator.d.ts +216 -0
  20. package/dist/api/fragment-instantiator.d.ts.map +1 -0
  21. package/dist/api/fragment-instantiator.js +487 -0
  22. package/dist/api/fragment-instantiator.js.map +1 -0
  23. package/dist/api/fragno-response.d.ts +30 -0
  24. package/dist/api/fragno-response.d.ts.map +1 -0
  25. package/dist/api/fragno-response.js +73 -0
  26. package/dist/api/fragno-response.js.map +1 -0
  27. package/dist/api/internal/path.d.ts +50 -0
  28. package/dist/api/internal/path.d.ts.map +1 -0
  29. package/dist/api/internal/path.js +76 -0
  30. package/dist/api/internal/path.js.map +1 -0
  31. package/dist/api/internal/response-stream.d.ts +43 -0
  32. package/dist/api/internal/response-stream.d.ts.map +1 -0
  33. package/dist/api/internal/response-stream.js +81 -0
  34. package/dist/api/internal/response-stream.js.map +1 -0
  35. package/dist/api/internal/route.js +10 -0
  36. package/dist/api/internal/route.js.map +1 -0
  37. package/dist/api/mutable-request-state.d.ts +82 -0
  38. package/dist/api/mutable-request-state.d.ts.map +1 -0
  39. package/dist/api/mutable-request-state.js +97 -0
  40. package/dist/api/mutable-request-state.js.map +1 -0
  41. package/dist/api/request-context-storage.d.ts +42 -0
  42. package/dist/api/request-context-storage.d.ts.map +1 -0
  43. package/dist/api/request-context-storage.js +43 -0
  44. package/dist/api/request-context-storage.js.map +1 -0
  45. package/dist/api/request-input-context.d.ts +89 -0
  46. package/dist/api/request-input-context.d.ts.map +1 -0
  47. package/dist/api/request-input-context.js +118 -0
  48. package/dist/api/request-input-context.js.map +1 -0
  49. package/dist/api/request-middleware.d.ts +50 -0
  50. package/dist/api/request-middleware.d.ts.map +1 -0
  51. package/dist/api/request-middleware.js +83 -0
  52. package/dist/api/request-middleware.js.map +1 -0
  53. package/dist/api/request-output-context.d.ts +41 -0
  54. package/dist/api/request-output-context.d.ts.map +1 -0
  55. package/dist/api/request-output-context.js +119 -0
  56. package/dist/api/request-output-context.js.map +1 -0
  57. package/dist/api/route-handler-input-options.d.ts +21 -0
  58. package/dist/api/route-handler-input-options.d.ts.map +1 -0
  59. package/dist/api/route.d.ts +54 -3
  60. package/dist/api/route.d.ts.map +1 -0
  61. package/dist/api/route.js +29 -2
  62. package/dist/api/route.js.map +1 -0
  63. package/dist/api/shared-types.d.ts +47 -0
  64. package/dist/api/shared-types.d.ts.map +1 -0
  65. package/dist/api/shared-types.js +1 -0
  66. package/dist/client/client-error.d.ts +60 -0
  67. package/dist/client/client-error.d.ts.map +1 -0
  68. package/dist/client/client-error.js +92 -0
  69. package/dist/client/client-error.js.map +1 -0
  70. package/dist/client/client.d.ts +210 -4
  71. package/dist/client/client.d.ts.map +1 -0
  72. package/dist/client/client.js +397 -6
  73. package/dist/client/client.js.map +1 -0
  74. package/dist/client/client.svelte.d.ts +5 -3
  75. package/dist/client/client.svelte.d.ts.map +1 -1
  76. package/dist/client/client.svelte.js +1 -5
  77. package/dist/client/client.svelte.js.map +1 -1
  78. package/dist/client/internal/fetcher-merge.js +36 -0
  79. package/dist/client/internal/fetcher-merge.js.map +1 -0
  80. package/dist/client/internal/ndjson-streaming.js +139 -0
  81. package/dist/client/internal/ndjson-streaming.js.map +1 -0
  82. package/dist/client/react.d.ts +5 -3
  83. package/dist/client/react.d.ts.map +1 -1
  84. package/dist/client/react.js +3 -5
  85. package/dist/client/react.js.map +1 -1
  86. package/dist/client/solid.d.ts +5 -3
  87. package/dist/client/solid.d.ts.map +1 -1
  88. package/dist/client/solid.js +2 -5
  89. package/dist/client/solid.js.map +1 -1
  90. package/dist/client/vanilla.d.ts +5 -3
  91. package/dist/client/vanilla.d.ts.map +1 -1
  92. package/dist/client/vanilla.js +2 -43
  93. package/dist/client/vanilla.js.map +1 -1
  94. package/dist/client/vue.d.ts +5 -3
  95. package/dist/client/vue.d.ts.map +1 -1
  96. package/dist/client/vue.js +1 -5
  97. package/dist/client/vue.js.map +1 -1
  98. package/dist/http/http-status.d.ts +26 -0
  99. package/dist/http/http-status.d.ts.map +1 -0
  100. package/dist/integrations/react-ssr.js +1 -1
  101. package/dist/internal/symbols.d.ts +9 -0
  102. package/dist/internal/symbols.d.ts.map +1 -0
  103. package/dist/internal/symbols.js +10 -0
  104. package/dist/internal/symbols.js.map +1 -0
  105. package/dist/mod-client.d.ts +36 -0
  106. package/dist/mod-client.d.ts.map +1 -0
  107. package/dist/mod-client.js +21 -0
  108. package/dist/mod-client.js.map +1 -0
  109. package/dist/mod.d.ts +7 -4
  110. package/dist/mod.js +4 -6
  111. package/dist/request/request.d.ts +4 -0
  112. package/dist/request/request.js +5 -0
  113. package/dist/test/test.d.ts +62 -35
  114. package/dist/test/test.d.ts.map +1 -1
  115. package/dist/test/test.js +75 -40
  116. package/dist/test/test.js.map +1 -1
  117. package/dist/util/async.js +40 -0
  118. package/dist/util/async.js.map +1 -0
  119. package/dist/util/content-type.js +49 -0
  120. package/dist/util/content-type.js.map +1 -0
  121. package/dist/util/nanostores.js +31 -0
  122. package/dist/util/nanostores.js.map +1 -0
  123. package/dist/{ssr-BByDVfFD.js → util/ssr.js} +2 -2
  124. package/dist/util/ssr.js.map +1 -0
  125. package/dist/util/types-util.d.ts +8 -0
  126. package/dist/util/types-util.d.ts.map +1 -0
  127. package/package.json +19 -12
  128. package/src/api/api.ts +41 -6
  129. package/src/api/bind-services.ts +42 -0
  130. package/src/api/fragment-definition-builder.extend.test.ts +810 -0
  131. package/src/api/fragment-definition-builder.test.ts +499 -0
  132. package/src/api/fragment-definition-builder.ts +1088 -0
  133. package/src/api/fragment-instantiator.test.ts +1488 -0
  134. package/src/api/fragment-instantiator.ts +1053 -0
  135. package/src/api/fragment-services.test.ts +727 -0
  136. package/src/api/request-context-storage.ts +64 -0
  137. package/src/api/request-middleware.test.ts +301 -225
  138. package/src/api/route.test.ts +87 -1
  139. package/src/api/route.ts +345 -24
  140. package/src/api/shared-types.ts +43 -0
  141. package/src/client/client-builder.test.ts +23 -23
  142. package/src/client/client.ssr.test.ts +3 -3
  143. package/src/client/client.svelte.test.ts +15 -15
  144. package/src/client/client.test.ts +22 -22
  145. package/src/client/client.ts +72 -12
  146. package/src/client/internal/fetcher-merge.ts +1 -1
  147. package/src/client/react.test.ts +2 -2
  148. package/src/client/solid.test.ts +2 -2
  149. package/src/client/vanilla.test.ts +2 -2
  150. package/src/client/vue.test.ts +2 -2
  151. package/src/internal/symbols.ts +5 -0
  152. package/src/mod-client.ts +59 -0
  153. package/src/mod.ts +26 -9
  154. package/src/request/request.ts +8 -0
  155. package/src/test/test.test.ts +200 -381
  156. package/src/test/test.ts +190 -117
  157. package/tsdown.config.ts +8 -5
  158. package/dist/api/fragment-builder.d.ts +0 -4
  159. package/dist/api/fragment-builder.js +0 -3
  160. package/dist/api/fragment-instantiation.d.ts +0 -4
  161. package/dist/api/fragment-instantiation.js +0 -6
  162. package/dist/api-BWN97TOr.d.ts +0 -377
  163. package/dist/api-BWN97TOr.d.ts.map +0 -1
  164. package/dist/api-DngJDcmO.js.map +0 -1
  165. package/dist/client-C5LsYHEI.js +0 -782
  166. package/dist/client-C5LsYHEI.js.map +0 -1
  167. package/dist/fragment-builder-DOnCVBqc.js +0 -47
  168. package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
  169. package/dist/fragment-builder-MGr68GNb.d.ts +0 -409
  170. package/dist/fragment-builder-MGr68GNb.d.ts.map +0 -1
  171. package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
  172. package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
  173. package/dist/request-output-context-CdIjwmEN.js +0 -320
  174. package/dist/request-output-context-CdIjwmEN.js.map +0 -1
  175. package/dist/route-Bl9Zr1Yv.d.ts +0 -26
  176. package/dist/route-Bl9Zr1Yv.d.ts.map +0 -1
  177. package/dist/route-C5Uryylh.js +0 -21
  178. package/dist/route-C5Uryylh.js.map +0 -1
  179. package/dist/ssr-BByDVfFD.js.map +0 -1
  180. package/src/api/fragment-builder.ts +0 -80
  181. package/src/api/fragment-instantiation.test.ts +0 -460
  182. package/src/api/fragment-instantiation.ts +0 -499
  183. package/src/api/fragment.test.ts +0 -537
@@ -0,0 +1,727 @@
1
+ import { describe, test, expect, expectTypeOf } from "vitest";
2
+ import { defineFragment } from "./fragment-definition-builder";
3
+ import { instantiate } from "./fragment-instantiator";
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 definition = defineFragment("test-fragment")
18
+ .usesService<"email", IEmailService>("email")
19
+ .build();
20
+
21
+ expect(definition.serviceDependencies).toBeDefined();
22
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
23
+ });
24
+
25
+ test("should declare optional service with usesOptionalService", () => {
26
+ const definition = defineFragment("test-fragment")
27
+ .usesOptionalService<"email", IEmailService>("email")
28
+ .build();
29
+
30
+ expect(definition.serviceDependencies).toBeDefined();
31
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: false });
32
+ });
33
+
34
+ test("should support multiple required services", () => {
35
+ const definition = defineFragment("test-fragment")
36
+ .usesService<"email", IEmailService>("email")
37
+ .usesService<"logger", ILogger>("logger")
38
+ .build();
39
+
40
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
41
+ expect(definition.serviceDependencies?.logger).toEqual({ name: "logger", required: true });
42
+ });
43
+
44
+ test("should support mixing required and optional services", () => {
45
+ const definition = defineFragment("test-fragment")
46
+ .usesService<"email", IEmailService>("email")
47
+ .usesOptionalService<"logger", ILogger>("logger")
48
+ .build();
49
+
50
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
51
+ expect(definition.serviceDependencies?.logger).toEqual({ name: "logger", required: false });
52
+ });
53
+
54
+ test("should preserve other fragment properties", () => {
55
+ const definition = defineFragment<{ apiKey: string }>("test-fragment")
56
+ .withDependencies(() => ({ dep: "value" }))
57
+ .usesService<"email", IEmailService>("email")
58
+ .build();
59
+
60
+ expect(definition.name).toBe("test-fragment");
61
+ expect(definition.serviceDependencies?.email).toBeDefined();
62
+ });
63
+
64
+ test("should have correct type inference for required service", () => {
65
+ const definition = defineFragment("test")
66
+ .usesService<"email", IEmailService>("email")
67
+ .build();
68
+
69
+ expect(definition.serviceDependencies?.email).toBeDefined();
70
+ expect(definition.serviceDependencies?.email?.required).toBe(true);
71
+ });
72
+
73
+ test("should have correct type inference for optional service", () => {
74
+ const definition = defineFragment("test")
75
+ .usesOptionalService<"logger", ILogger>("logger")
76
+ .build();
77
+
78
+ expect(definition.serviceDependencies?.logger).toBeDefined();
79
+ expect(definition.serviceDependencies?.logger?.required).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe("providesService", () => {
84
+ test("should declare provided service implementation", () => {
85
+ const emailImpl: IEmailService = {
86
+ sendEmail: async () => {},
87
+ };
88
+
89
+ const definition = defineFragment("test-fragment")
90
+ .providesService("email", () => emailImpl)
91
+ .build();
92
+
93
+ expect(definition.namedServices).toBeDefined();
94
+ });
95
+
96
+ test("should support multiple provided services", () => {
97
+ const emailImpl: IEmailService = {
98
+ sendEmail: async () => {},
99
+ };
100
+
101
+ const loggerImpl: ILogger = {
102
+ log: () => {},
103
+ };
104
+
105
+ const _definition = defineFragment("test-fragment")
106
+ .providesService("email", () => emailImpl)
107
+ .providesService("logger", () => loggerImpl)
108
+ .build();
109
+ });
110
+ });
111
+
112
+ describe("Service metadata", () => {
113
+ test("should store service metadata in definition", () => {
114
+ const definition = defineFragment("test")
115
+ .usesService<"email", IEmailService>("email")
116
+ .usesOptionalService<"logger", ILogger>("logger")
117
+ .build();
118
+
119
+ expect(definition.serviceDependencies?.email?.required).toBe(true);
120
+ expect(definition.serviceDependencies?.logger?.required).toBe(false);
121
+ });
122
+
123
+ test("should store provided services in definition", () => {
124
+ const emailImpl: IEmailService = {
125
+ sendEmail: async () => {},
126
+ };
127
+
128
+ const definition = defineFragment("test")
129
+ .providesService("email", () => emailImpl)
130
+ .build();
131
+
132
+ expect(typeof definition.namedServices).toBe("object");
133
+ });
134
+
135
+ test("should allow fragments without any services", () => {
136
+ const definition = defineFragment("test").build();
137
+
138
+ expect(definition.serviceDependencies).toBeUndefined();
139
+ expect(definition.namedServices).toBeUndefined();
140
+ });
141
+ });
142
+
143
+ describe("Type safety", () => {
144
+ test("Unnamed services should have correct types (using providesBaseService)", () => {
145
+ const definition = defineFragment("test")
146
+ .providesBaseService(() => ({
147
+ sendEmail: async () => {},
148
+ }))
149
+ .build();
150
+
151
+ const instance = instantiate(definition).withOptions({}).build();
152
+ expect(instance.services.sendEmail).toBeDefined();
153
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
154
+ });
155
+
156
+ test("Named services should have correct types", () => {
157
+ const definition = defineFragment("test")
158
+ .providesService("email", () => ({
159
+ sendEmail: async () => {},
160
+ }))
161
+ .build();
162
+
163
+ const instance = instantiate(definition).withOptions({}).build();
164
+ expect(instance.services.email.sendEmail).toBeDefined();
165
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
166
+ });
167
+
168
+ test("Unnamed services should have correct types (using factory)", () => {
169
+ const definition = defineFragment("test")
170
+ .providesBaseService(() => ({
171
+ sendEmail: async () => {},
172
+ }))
173
+ .build();
174
+
175
+ const instance = instantiate(definition).withOptions({}).build();
176
+ expect(instance.services.sendEmail).toBeDefined();
177
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
178
+ });
179
+
180
+ test("Unnamed services should have correct types (using callback with context)", () => {
181
+ const definition = defineFragment("test")
182
+ .providesBaseService(() => ({
183
+ sendEmail: async () => {},
184
+ }))
185
+ .build();
186
+
187
+ const instance = instantiate(definition).withOptions({}).build();
188
+ expect(instance.services.sendEmail).toBeDefined();
189
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
190
+ });
191
+
192
+ test("Unnamed services should have correct types (using 0-arity factory)", () => {
193
+ const definition = defineFragment("test")
194
+ .providesBaseService(() => ({
195
+ sendEmail: async () => {},
196
+ }))
197
+ .build();
198
+
199
+ const instance = instantiate(definition).withOptions({}).build();
200
+ expect(instance.services.sendEmail).toBeDefined();
201
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
202
+ });
203
+
204
+ test("Named services should have correct types (using factory)", () => {
205
+ const definition = defineFragment("test")
206
+ .providesService("email", () => ({
207
+ sendEmail: async () => {},
208
+ }))
209
+ .build();
210
+
211
+ const instance = instantiate(definition).withOptions({}).build();
212
+ expect(instance.services.email.sendEmail).toBeDefined();
213
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
214
+ });
215
+
216
+ test("usesService (required)", () => {
217
+ const definition = defineFragment("test")
218
+ .usesService<"email", IEmailService>("email")
219
+ .providesBaseService(({ serviceDeps }) => ({
220
+ sendEmail: (to: string, subject: string, body: string) => {
221
+ return serviceDeps.email.sendEmail(to, subject, body);
222
+ },
223
+ }))
224
+ .build();
225
+
226
+ const emailImpl: IEmailService = {
227
+ sendEmail: async () => {},
228
+ };
229
+
230
+ const instance = instantiate(definition)
231
+ .withOptions({})
232
+ .withServices({ email: emailImpl })
233
+ .build();
234
+
235
+ expect(instance.services.sendEmail).toBeDefined();
236
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<
237
+ (to: string, subject: string, body: string) => void
238
+ >();
239
+ });
240
+
241
+ test("usesService (required) - builder style", () => {
242
+ const definition = defineFragment("test")
243
+ .usesService<"email", IEmailService>("email")
244
+ .providesBaseService(({ serviceDeps }) => ({
245
+ sendEmail: (to: string, subject: string, body: string) => {
246
+ return serviceDeps.email.sendEmail(to, subject, body);
247
+ },
248
+ }))
249
+ .build();
250
+
251
+ const emailImpl: IEmailService = {
252
+ sendEmail: async () => {},
253
+ };
254
+
255
+ const instance = instantiate(definition)
256
+ .withServices({ email: emailImpl })
257
+ .withOptions({})
258
+ .build();
259
+
260
+ expect(instance.services.sendEmail).toBeDefined();
261
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<
262
+ (to: string, subject: string, body: string) => void
263
+ >();
264
+ });
265
+
266
+ test("usesOptionalService", () => {
267
+ const definition = defineFragment("test")
268
+ .usesOptionalService<"email", IEmailService>("email")
269
+ .providesBaseService(({ serviceDeps }) => ({
270
+ sendEmail: serviceDeps.email
271
+ ? (to: string, subject: string, body: string) =>
272
+ serviceDeps.email!.sendEmail(to, subject, body)
273
+ : undefined,
274
+ }))
275
+ .build();
276
+
277
+ const instance = instantiate(definition).withOptions({}).build();
278
+
279
+ // For optional services, the wrapped service method might be undefined
280
+ expect(instance.services.sendEmail).toBeUndefined();
281
+ });
282
+
283
+ test("provided services should have correct types", () => {
284
+ const emailImpl: IEmailService = {
285
+ sendEmail: async () => {},
286
+ };
287
+
288
+ const definition = defineFragment("test")
289
+ .providesService("email", () => emailImpl)
290
+ .build();
291
+
292
+ // namedServices stores an object with service names as keys and factory functions as values
293
+ expect(definition.namedServices).toBeDefined();
294
+ expect(typeof definition.namedServices).toBe("object");
295
+ });
296
+
297
+ test("Named services should have correct types (using callback with context)", () => {
298
+ const definition = defineFragment("test")
299
+ .providesService("email", () => ({
300
+ sendEmail: async () => {},
301
+ }))
302
+ .build();
303
+
304
+ const instance = instantiate(definition).withOptions({}).build();
305
+ expect(instance.services.email.sendEmail).toBeDefined();
306
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
307
+ });
308
+
309
+ test("Named services should have correct types (using 0-arity factory)", () => {
310
+ const definition = defineFragment("test")
311
+ .providesService("email", () => ({
312
+ sendEmail: async () => {},
313
+ }))
314
+ .build();
315
+
316
+ const instance = instantiate(definition).withOptions({}).build();
317
+ expect(instance.services.email.sendEmail).toBeDefined();
318
+ expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
319
+ });
320
+
321
+ test("Type mismatch when using a service", () => {
322
+ interface ExpectedService {
323
+ throwDice: () => 1 | 2 | 3 | 4 | 5 | 6;
324
+ }
325
+
326
+ const definition = defineFragment("test")
327
+ .usesService<"expected", ExpectedService>("expected")
328
+ .providesBaseService(({ serviceDeps }) => ({
329
+ throwDice: () => serviceDeps.expected.throwDice(),
330
+ }))
331
+ .build();
332
+
333
+ interface ActualService {
334
+ throwDice: () => number;
335
+ }
336
+
337
+ const actualService: ActualService = {
338
+ throwDice: () => 1,
339
+ };
340
+
341
+ const instance = instantiate(definition)
342
+ // @ts-expect-error - Type mismatch
343
+ .withServices({ expected: actualService })
344
+ .withOptions({})
345
+ .build();
346
+
347
+ // The wrapped service on the instance has the correct type based on the declared service
348
+ expect(instance.services.throwDice).toBeDefined();
349
+ expectTypeOf<typeof instance.services.throwDice>().toExtend<() => 1 | 2 | 3 | 4 | 5 | 6>();
350
+ });
351
+ });
352
+
353
+ describe("Error handling", () => {
354
+ test("should throw error when required service is not provided", () => {
355
+ const definition = defineFragment("test")
356
+ .usesService<"email", IEmailService>("email")
357
+ .build();
358
+
359
+ expect(() => {
360
+ instantiate(definition).withOptions({}).build();
361
+ }).toThrow("Fragment 'test' requires service 'email' but it was not provided");
362
+ });
363
+
364
+ test("should not throw when optional service is not provided", () => {
365
+ const definition = defineFragment("test")
366
+ .usesOptionalService<"email", IEmailService>("email")
367
+ .build();
368
+
369
+ expect(() => {
370
+ instantiate(definition).withOptions({}).build();
371
+ }).not.toThrow();
372
+ });
373
+ });
374
+
375
+ describe("Service dependencies and composition", () => {
376
+ test("provided service can access used services", () => {
377
+ const emailImpl: IEmailService = {
378
+ sendEmail: async () => {},
379
+ };
380
+
381
+ const definition = defineFragment("test")
382
+ .usesService<"email", IEmailService>("email")
383
+ .providesBaseService(({ serviceDeps }) => ({
384
+ sendWelcomeEmail: async (to: string) => {
385
+ await serviceDeps.email.sendEmail(to, "Welcome", "Welcome to our service!");
386
+ },
387
+ }))
388
+ .build();
389
+
390
+ const instance = instantiate(definition)
391
+ .withOptions({})
392
+ .withServices({ email: emailImpl })
393
+ .build();
394
+
395
+ expect(instance.services.sendWelcomeEmail).toBeDefined();
396
+ expect(typeof instance.services.sendWelcomeEmail).toBe("function");
397
+ });
398
+
399
+ test("provided service can access used services - builder style", () => {
400
+ const emailImpl: IEmailService = {
401
+ sendEmail: async () => {},
402
+ };
403
+
404
+ const definition = defineFragment("test")
405
+ .usesService<"email", IEmailService>("email")
406
+ .providesBaseService(({ serviceDeps }) => ({
407
+ sendWelcomeEmail: async (to: string) => {
408
+ await serviceDeps.email.sendEmail(to, "Welcome", "Welcome to our service!");
409
+ },
410
+ }))
411
+ .build();
412
+
413
+ const instance = instantiate(definition)
414
+ .withServices({ email: emailImpl })
415
+ .withOptions({})
416
+ .build();
417
+
418
+ expect(instance.services.sendWelcomeEmail).toBeDefined();
419
+ expect(typeof instance.services.sendWelcomeEmail).toBe("function");
420
+ });
421
+
422
+ test("provided service can access config", () => {
423
+ const definition = defineFragment<{ apiKey: string }>("test")
424
+ .providesBaseService(({ config }) => ({
425
+ getApiKey: () => config.apiKey,
426
+ }))
427
+ .build();
428
+
429
+ const instance = instantiate(definition)
430
+ .withConfig({ apiKey: "test-key" })
431
+ .withOptions({})
432
+ .build();
433
+
434
+ expect(instance.services.getApiKey()).toBe("test-key");
435
+ });
436
+
437
+ test("provided service can access deps from withDependencies", () => {
438
+ const definition = defineFragment<{ apiKey: string }>("test")
439
+ .withDependencies(({ config }) => ({
440
+ client: { key: config.apiKey },
441
+ }))
442
+ .providesBaseService(({ deps }) => ({
443
+ getClient: () => deps.client,
444
+ }))
445
+ .build();
446
+
447
+ const instance = instantiate(definition)
448
+ .withConfig({ apiKey: "test-key" })
449
+ .withOptions({})
450
+ .build();
451
+
452
+ expect(instance.services.getClient()).toEqual({ key: "test-key" });
453
+ });
454
+ });
455
+
456
+ describe("Service chaining and multiple services", () => {
457
+ test("should support chaining multiple provided services", () => {
458
+ const definition = defineFragment("test")
459
+ .providesService("email", () => ({
460
+ sendEmail: async () => {},
461
+ }))
462
+ .providesService("logger", () => ({
463
+ log: () => {},
464
+ }))
465
+ .build();
466
+
467
+ const instance = instantiate(definition).withOptions({}).build();
468
+ expect(instance.services.email.sendEmail).toBeDefined();
469
+ expect(instance.services.logger.log).toBeDefined();
470
+ });
471
+
472
+ test("should support mixing unnamed and named provided services", () => {
473
+ const definition = defineFragment("test")
474
+ .providesBaseService(() => ({
475
+ helper: () => "help",
476
+ }))
477
+ .providesService("email", () => ({
478
+ sendEmail: async () => {},
479
+ }))
480
+ .build();
481
+
482
+ const instance = instantiate(definition).withOptions({}).build();
483
+ expect(instance.services.helper).toBeDefined();
484
+ expect(instance.services.email.sendEmail).toBeDefined();
485
+ });
486
+ });
487
+
488
+ describe("Optional service runtime behavior", () => {
489
+ test("should handle optional service when not provided", () => {
490
+ const definition = defineFragment("test")
491
+ .usesOptionalService<"email", IEmailService>("email")
492
+ .providesBaseService(({ serviceDeps }) => ({
493
+ maybeSendEmail: async (to: string) => {
494
+ if (serviceDeps.email) {
495
+ await serviceDeps.email.sendEmail(to, "Subject", "Body");
496
+ return true;
497
+ }
498
+ return false;
499
+ },
500
+ }))
501
+ .build();
502
+
503
+ const instance = instantiate(definition).withOptions({}).build();
504
+
505
+ expect(instance.services.maybeSendEmail).toBeDefined();
506
+ // Should not throw when optional service is not provided
507
+ });
508
+
509
+ test("should handle optional service when provided", () => {
510
+ const emailImpl: IEmailService = {
511
+ sendEmail: async () => {},
512
+ };
513
+
514
+ const definition = defineFragment("test")
515
+ .usesOptionalService<"email", IEmailService>("email")
516
+ .providesBaseService(({ serviceDeps }) => ({
517
+ maybeSendEmail: async (to: string) => {
518
+ if (serviceDeps.email) {
519
+ await serviceDeps.email.sendEmail(to, "Subject", "Body");
520
+ return true;
521
+ }
522
+ return false;
523
+ },
524
+ }))
525
+ .build();
526
+
527
+ const instance = instantiate(definition)
528
+ .withOptions({})
529
+ .withServices({ email: emailImpl })
530
+ .build();
531
+
532
+ expect(instance.services.maybeSendEmail).toBeDefined();
533
+ // When the optional service is provided, the wrapped method should work
534
+ expect(typeof instance.services.maybeSendEmail).toBe("function");
535
+ });
536
+ });
537
+
538
+ describe("Private Services", () => {
539
+ test("private service should be accessible when defining other services", () => {
540
+ interface IDataStore {
541
+ get(key: string): string | undefined;
542
+ set(key: string, value: string): void;
543
+ }
544
+
545
+ const dataStoreImpl: IDataStore = {
546
+ get: () => "cached-value",
547
+ set: () => {},
548
+ };
549
+
550
+ const definition = defineFragment("test")
551
+ .providesPrivateService("dataStore", () => dataStoreImpl)
552
+ .providesBaseService(({ privateServices }) => ({
553
+ getValue: (key: string) => {
554
+ // Private service is accessible here
555
+ return privateServices.dataStore.get(key);
556
+ },
557
+ }))
558
+ .build();
559
+
560
+ const instance = instantiate(definition).withOptions({}).build();
561
+
562
+ // Private service should NOT be accessible on the instance
563
+ expectTypeOf<typeof instance.services>().not.toMatchTypeOf<{ dataStore: IDataStore }>();
564
+
565
+ // But the public service that uses it should work
566
+ expect(instance.services.getValue).toBeDefined();
567
+ expect(instance.services.getValue("test")).toBe("cached-value");
568
+ });
569
+
570
+ test("private service should NOT be exposed on fragment instance", () => {
571
+ interface IInternalCache {
572
+ cache: Map<string, unknown>;
573
+ }
574
+
575
+ const definition = defineFragment("test")
576
+ .providesPrivateService<"cache", IInternalCache>("cache", () => ({
577
+ cache: new Map(),
578
+ }))
579
+ .providesBaseService(({ privateServices }) => ({
580
+ getCacheSize: () => privateServices.cache.cache.size,
581
+ }))
582
+ .build();
583
+
584
+ const instance = instantiate(definition).withOptions({}).build();
585
+
586
+ // @ts-expect-error - Private service should not be accessible
587
+ expect(instance.services.cache).toBeUndefined();
588
+
589
+ // Only the public service should be accessible
590
+ expect(instance.services.getCacheSize).toBeDefined();
591
+ });
592
+
593
+ test("multiple private services should work together", () => {
594
+ interface ILogger {
595
+ log(msg: string): void;
596
+ }
597
+
598
+ interface ICache {
599
+ get(key: string): unknown;
600
+ }
601
+
602
+ const logger: ILogger = {
603
+ log: () => {},
604
+ };
605
+
606
+ const cache: ICache = {
607
+ get: () => "cached",
608
+ };
609
+
610
+ const definition = defineFragment("test")
611
+ .providesPrivateService("logger", () => logger)
612
+ .providesPrivateService("cache", () => cache)
613
+ .providesBaseService(({ privateServices }) => ({
614
+ getCachedValue: (key: string) => {
615
+ privateServices.logger.log(`Getting ${key}`);
616
+ return privateServices.cache.get(key);
617
+ },
618
+ }))
619
+ .build();
620
+
621
+ const instance = instantiate(definition).withOptions({}).build();
622
+
623
+ expect(instance.services.getCachedValue).toBeDefined();
624
+ expect(instance.services.getCachedValue("test")).toBe("cached");
625
+ });
626
+
627
+ test("private services can access config and deps", () => {
628
+ const definition = defineFragment<{ apiKey: string }>("test")
629
+ .withDependencies(({ config }) => ({
630
+ endpoint: `https://api.example.com/${config.apiKey}`,
631
+ }))
632
+ .providesPrivateService("internalApi", ({ deps }) => ({
633
+ makeRequest: () => `${deps.endpoint}/request`,
634
+ }))
635
+ .providesBaseService(({ privateServices }) => ({
636
+ doRequest: () => privateServices.internalApi.makeRequest(),
637
+ }))
638
+ .build();
639
+
640
+ const instance = instantiate(definition)
641
+ .withConfig({ apiKey: "test-key" })
642
+ .withOptions({})
643
+ .build();
644
+
645
+ expect(instance.services.doRequest()).toBe("https://api.example.com/test-key/request");
646
+ });
647
+
648
+ test("private services can access serviceDeps", () => {
649
+ interface IEmailService {
650
+ sendEmail(to: string): Promise<void>;
651
+ }
652
+
653
+ const emailImpl: IEmailService = {
654
+ sendEmail: async () => {},
655
+ };
656
+
657
+ const definition = defineFragment("test")
658
+ .usesService<"email", IEmailService>("email")
659
+ .providesPrivateService("emailHelper", ({ serviceDeps }) => ({
660
+ sendWelcomeEmail: (to: string) => serviceDeps.email.sendEmail(to),
661
+ }))
662
+ .providesBaseService(({ privateServices }) => ({
663
+ welcomeUser: (email: string) => privateServices.emailHelper.sendWelcomeEmail(email),
664
+ }))
665
+ .build();
666
+
667
+ const instance = instantiate(definition)
668
+ .withServices({ email: emailImpl })
669
+ .withOptions({})
670
+ .build();
671
+
672
+ expect(instance.services.welcomeUser).toBeDefined();
673
+ expectTypeOf<typeof instance.services.welcomeUser>().toExtend<
674
+ (email: string) => Promise<void>
675
+ >();
676
+ });
677
+
678
+ test("named services can also access private services", () => {
679
+ const definition = defineFragment("test")
680
+ .providesPrivateService("helper", () => ({
681
+ multiply: (a: number, b: number) => a * b,
682
+ }))
683
+ .providesService("calculator", ({ privateServices }) => ({
684
+ square: (n: number) => privateServices.helper.multiply(n, n),
685
+ }))
686
+ .build();
687
+
688
+ const instance = instantiate(definition).withOptions({}).build();
689
+
690
+ expect(instance.services.calculator.square(5)).toBe(25);
691
+ // @ts-expect-error - Private service should not be accessible
692
+ expect(instance.services.helper).toBeUndefined();
693
+ });
694
+
695
+ test("private services can access other private services (in order)", () => {
696
+ const definition = defineFragment("test")
697
+ .providesPrivateService("math", () => ({
698
+ add: (a: number, b: number) => a + b,
699
+ multiply: (a: number, b: number) => a * b,
700
+ }))
701
+ .providesPrivateService("calculator", ({ privateServices }) => ({
702
+ // This private service can access the earlier private service
703
+ square: (n: number) => privateServices.math.multiply(n, n),
704
+ addTen: (n: number) => privateServices.math.add(n, 10),
705
+ }))
706
+ .providesBaseService(({ privateServices }) => ({
707
+ compute: (n: number) => {
708
+ // Public service can access both private services
709
+ const squared = privateServices.calculator.square(n);
710
+ return privateServices.calculator.addTen(squared);
711
+ },
712
+ }))
713
+ .build();
714
+
715
+ const instance = instantiate(definition).withOptions({}).build();
716
+
717
+ // 5^2 = 25, 25 + 10 = 35
718
+ expect(instance.services.compute(5)).toBe(35);
719
+
720
+ // Private services should not be accessible on the instance
721
+ // @ts-expect-error - Private service should not be accessible
722
+ expect(instance.services.math).toBeUndefined();
723
+ // @ts-expect-error - Private service should not be accessible
724
+ expect(instance.services.calculator).toBeUndefined();
725
+ });
726
+ });
727
+ });