@fragno-dev/core 0.1.8 → 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 (176) hide show
  1. package/.turbo/turbo-build.log +131 -56
  2. package/CHANGELOG.md +13 -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 -3
  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/error.js +48 -0
  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 -2
  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 -2
  71. package/dist/client/client.d.ts.map +1 -0
  72. package/dist/client/client.js +397 -5
  73. package/dist/client/client.js.map +1 -0
  74. package/dist/client/client.svelte.d.ts +5 -2
  75. package/dist/client/client.svelte.d.ts.map +1 -1
  76. package/dist/client/client.svelte.js +1 -4
  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 -2
  83. package/dist/client/react.d.ts.map +1 -1
  84. package/dist/client/react.js +3 -4
  85. package/dist/client/react.js.map +1 -1
  86. package/dist/client/solid.d.ts +5 -2
  87. package/dist/client/solid.d.ts.map +1 -1
  88. package/dist/client/solid.js +2 -4
  89. package/dist/client/solid.js.map +1 -1
  90. package/dist/client/vanilla.d.ts +5 -2
  91. package/dist/client/vanilla.d.ts.map +1 -1
  92. package/dist/client/vanilla.js +2 -42
  93. package/dist/client/vanilla.js.map +1 -1
  94. package/dist/client/vue.d.ts +5 -2
  95. package/dist/client/vue.d.ts.map +1 -1
  96. package/dist/client/vue.js +1 -4
  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 -2
  110. package/dist/mod.js +4 -4
  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 -34
  114. package/dist/test/test.d.ts.map +1 -1
  115. package/dist/test/test.js +75 -42
  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-kyKI7pqH.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 +1 -5
  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 +454 -189
  136. package/src/api/request-context-storage.ts +64 -0
  137. package/src/api/request-middleware.test.ts +301 -228
  138. package/src/api/route.test.ts +12 -36
  139. package/src/api/route.ts +167 -155
  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 +22 -15
  154. package/src/request/request.ts +8 -0
  155. package/src/test/test.test.ts +189 -375
  156. package/src/test/test.ts +186 -152
  157. package/tsdown.config.ts +8 -5
  158. package/dist/api/fragment-builder.d.ts +0 -2
  159. package/dist/api/fragment-builder.js +0 -3
  160. package/dist/api/fragment-instantiation.d.ts +0 -2
  161. package/dist/api/fragment-instantiation.js +0 -4
  162. package/dist/api-BFrUCIsF.d.ts +0 -963
  163. package/dist/api-BFrUCIsF.d.ts.map +0 -1
  164. package/dist/client-DAFHcKqA.js +0 -782
  165. package/dist/client-DAFHcKqA.js.map +0 -1
  166. package/dist/fragment-builder-Boh2vNHq.js +0 -108
  167. package/dist/fragment-builder-Boh2vNHq.js.map +0 -1
  168. package/dist/fragment-instantiation-DUT-HLl1.js +0 -898
  169. package/dist/fragment-instantiation-DUT-HLl1.js.map +0 -1
  170. package/dist/route-C4CyNHkC.js +0 -26
  171. package/dist/route-C4CyNHkC.js.map +0 -1
  172. package/dist/ssr-kyKI7pqH.js.map +0 -1
  173. package/src/api/fragment-builder.ts +0 -518
  174. package/src/api/fragment-instantiation.test.ts +0 -702
  175. package/src/api/fragment-instantiation.ts +0 -766
  176. package/src/api/fragment.test.ts +0 -585
@@ -0,0 +1,810 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { defineFragment, FragmentDefinitionBuilder } from "./fragment-definition-builder";
3
+ import type { FragnoPublicConfig } from "./shared-types";
4
+ import type { RequestThisContext } from "./api";
5
+
6
+ describe("FragmentDefinitionBuilder.extend()", () => {
7
+ describe("basic functionality", () => {
8
+ it("should allow extending with a simple transformer", () => {
9
+ const addTimestamp = () => {
10
+ return <
11
+ TConfig,
12
+ TOptions extends FragnoPublicConfig,
13
+ TDeps,
14
+ TBaseServices,
15
+ TServices,
16
+ TServiceDeps,
17
+ TPrivateServices,
18
+ TServiceThisContext extends RequestThisContext,
19
+ THandlerThisContext extends RequestThisContext,
20
+ TRequestStorage,
21
+ >(
22
+ builder: FragmentDefinitionBuilder<
23
+ TConfig,
24
+ TOptions,
25
+ TDeps,
26
+ TBaseServices,
27
+ TServices,
28
+ TServiceDeps,
29
+ TPrivateServices,
30
+ TServiceThisContext,
31
+ THandlerThisContext,
32
+ TRequestStorage
33
+ >,
34
+ ): FragmentDefinitionBuilder<
35
+ TConfig,
36
+ TOptions,
37
+ TDeps,
38
+ TBaseServices & { timestamp: () => number },
39
+ TServices,
40
+ TServiceDeps,
41
+ TPrivateServices,
42
+ TServiceThisContext,
43
+ THandlerThisContext,
44
+ TRequestStorage
45
+ > => {
46
+ const currentDef = builder.build();
47
+ const currentBaseServices = currentDef.baseServices;
48
+
49
+ return new FragmentDefinitionBuilder(builder.name, {
50
+ ...currentDef,
51
+ baseServices: (context) => {
52
+ const existing = currentBaseServices
53
+ ? currentBaseServices(context)
54
+ : ({} as TBaseServices);
55
+ return {
56
+ ...existing,
57
+ timestamp: () => Date.now(),
58
+ } as TBaseServices & { timestamp: () => number };
59
+ },
60
+ });
61
+ };
62
+ };
63
+
64
+ const definition = defineFragment<{ enabled: boolean }>("test")
65
+ .withDependencies(({ config }) => ({ enabled: config.enabled }))
66
+ .extend(addTimestamp())
67
+ .build();
68
+
69
+ expect(definition.name).toBe("test");
70
+ expect(definition.dependencies).toBeDefined();
71
+ expect(definition.baseServices).toBeDefined();
72
+ });
73
+
74
+ it("should allow chaining multiple extend() calls", () => {
75
+ const addCounter = () => {
76
+ return <
77
+ TConfig,
78
+ TOptions extends FragnoPublicConfig,
79
+ TDeps,
80
+ TBaseServices,
81
+ TServices,
82
+ TServiceDeps,
83
+ TPrivateServices,
84
+ TServiceThisContext extends RequestThisContext,
85
+ THandlerThisContext extends RequestThisContext,
86
+ TRequestStorage,
87
+ >(
88
+ builder: FragmentDefinitionBuilder<
89
+ TConfig,
90
+ TOptions,
91
+ TDeps,
92
+ TBaseServices,
93
+ TServices,
94
+ TServiceDeps,
95
+ TPrivateServices,
96
+ TServiceThisContext,
97
+ THandlerThisContext,
98
+ TRequestStorage
99
+ >,
100
+ ): FragmentDefinitionBuilder<
101
+ TConfig,
102
+ TOptions,
103
+ TDeps,
104
+ TBaseServices & { counter: { value: number; increment: () => void } },
105
+ TServices,
106
+ TServiceDeps,
107
+ TPrivateServices,
108
+ TServiceThisContext,
109
+ THandlerThisContext,
110
+ TRequestStorage
111
+ > => {
112
+ const currentDef = builder.build();
113
+ const currentBaseServices = currentDef.baseServices;
114
+
115
+ return new FragmentDefinitionBuilder(builder.name, {
116
+ ...currentDef,
117
+ baseServices: (context) => {
118
+ const existing = currentBaseServices
119
+ ? currentBaseServices(context)
120
+ : ({} as TBaseServices);
121
+ let count = 0;
122
+ return {
123
+ ...existing,
124
+ counter: {
125
+ value: count,
126
+ increment: () => {
127
+ count++;
128
+ },
129
+ },
130
+ } as TBaseServices & { counter: { value: number; increment: () => void } };
131
+ },
132
+ });
133
+ };
134
+ };
135
+
136
+ const addLogger = () => {
137
+ return <
138
+ TConfig,
139
+ TOptions extends FragnoPublicConfig,
140
+ TDeps,
141
+ TBaseServices,
142
+ TServices,
143
+ TServiceDeps,
144
+ TPrivateServices,
145
+ TServiceThisContext extends RequestThisContext,
146
+ THandlerThisContext extends RequestThisContext,
147
+ TRequestStorage,
148
+ >(
149
+ builder: FragmentDefinitionBuilder<
150
+ TConfig,
151
+ TOptions,
152
+ TDeps,
153
+ TBaseServices,
154
+ TServices,
155
+ TServiceDeps,
156
+ TPrivateServices,
157
+ TServiceThisContext,
158
+ THandlerThisContext,
159
+ TRequestStorage
160
+ >,
161
+ ): FragmentDefinitionBuilder<
162
+ TConfig,
163
+ TOptions,
164
+ TDeps,
165
+ TBaseServices & { logger: { log: (msg: string) => void } },
166
+ TServices,
167
+ TServiceDeps,
168
+ TPrivateServices,
169
+ TServiceThisContext,
170
+ THandlerThisContext,
171
+ TRequestStorage
172
+ > => {
173
+ const currentDef = builder.build();
174
+ const currentBaseServices = currentDef.baseServices;
175
+
176
+ return new FragmentDefinitionBuilder(builder.name, {
177
+ ...currentDef,
178
+ baseServices: (context) => {
179
+ const existing = currentBaseServices
180
+ ? currentBaseServices(context)
181
+ : ({} as TBaseServices);
182
+ return {
183
+ ...existing,
184
+ logger: {
185
+ log: (_msg: string) => {
186
+ // In tests, we won't actually log
187
+ },
188
+ },
189
+ } as TBaseServices & { logger: { log: (msg: string) => void } };
190
+ },
191
+ });
192
+ };
193
+ };
194
+
195
+ const definition = defineFragment<{ name: string }>("chained")
196
+ .withDependencies(({ config }) => ({ name: config.name }))
197
+ .extend(addCounter())
198
+ .extend(addLogger())
199
+ .build();
200
+
201
+ expect(definition.name).toBe("chained");
202
+ expect(definition.baseServices).toBeDefined();
203
+
204
+ // Verify both extensions are present in the type
205
+ const services = definition.baseServices!({
206
+ config: { name: "test" },
207
+ options: {},
208
+ deps: { name: "test" },
209
+ serviceDeps: {},
210
+ privateServices: {},
211
+ defineService: (svc) => svc,
212
+ });
213
+
214
+ expect(services).toHaveProperty("counter");
215
+ expect(services).toHaveProperty("logger");
216
+ expect(services.counter).toHaveProperty("value");
217
+ expect(services.counter).toHaveProperty("increment");
218
+ expect(services.logger).toHaveProperty("log");
219
+ });
220
+ });
221
+
222
+ describe("ordering", () => {
223
+ it("should apply extends in order - first extend's services available to second", () => {
224
+ // First extension adds a base value
225
+ const addBase = () => {
226
+ return <
227
+ TConfig,
228
+ TOptions extends FragnoPublicConfig,
229
+ TDeps,
230
+ TBaseServices,
231
+ TServices,
232
+ TServiceDeps,
233
+ TPrivateServices,
234
+ TServiceThisContext extends RequestThisContext,
235
+ THandlerThisContext extends RequestThisContext,
236
+ TRequestStorage,
237
+ >(
238
+ builder: FragmentDefinitionBuilder<
239
+ TConfig,
240
+ TOptions,
241
+ TDeps,
242
+ TBaseServices,
243
+ TServices,
244
+ TServiceDeps,
245
+ TPrivateServices,
246
+ TServiceThisContext,
247
+ THandlerThisContext,
248
+ TRequestStorage
249
+ >,
250
+ ): FragmentDefinitionBuilder<
251
+ TConfig,
252
+ TOptions,
253
+ TDeps,
254
+ TBaseServices & { baseValue: number },
255
+ TServices,
256
+ TServiceDeps,
257
+ TPrivateServices,
258
+ TServiceThisContext,
259
+ THandlerThisContext,
260
+ TRequestStorage
261
+ > => {
262
+ const currentDef = builder.build();
263
+ const currentBaseServices = currentDef.baseServices;
264
+
265
+ return new FragmentDefinitionBuilder(builder.name, {
266
+ ...currentDef,
267
+ baseServices: (context) => {
268
+ const existing = currentBaseServices
269
+ ? currentBaseServices(context)
270
+ : ({} as TBaseServices);
271
+ return {
272
+ ...existing,
273
+ baseValue: 42,
274
+ } as TBaseServices & { baseValue: number };
275
+ },
276
+ });
277
+ };
278
+ };
279
+
280
+ // Second extension uses the base value to compute a derived value
281
+ const addDerived = () => {
282
+ return <
283
+ TConfig,
284
+ TOptions extends FragnoPublicConfig,
285
+ TDeps,
286
+ TBaseServices extends { baseValue: number },
287
+ TServices,
288
+ TServiceDeps,
289
+ TPrivateServices,
290
+ TServiceThisContext extends RequestThisContext,
291
+ THandlerThisContext extends RequestThisContext,
292
+ TRequestStorage,
293
+ >(
294
+ builder: FragmentDefinitionBuilder<
295
+ TConfig,
296
+ TOptions,
297
+ TDeps,
298
+ TBaseServices,
299
+ TServices,
300
+ TServiceDeps,
301
+ TPrivateServices,
302
+ TServiceThisContext,
303
+ THandlerThisContext,
304
+ TRequestStorage
305
+ >,
306
+ ): FragmentDefinitionBuilder<
307
+ TConfig,
308
+ TOptions,
309
+ TDeps,
310
+ TBaseServices & { derivedValue: number },
311
+ TServices,
312
+ TServiceDeps,
313
+ TPrivateServices,
314
+ TServiceThisContext,
315
+ THandlerThisContext,
316
+ TRequestStorage
317
+ > => {
318
+ const currentDef = builder.build();
319
+ const currentBaseServices = currentDef.baseServices;
320
+
321
+ return new FragmentDefinitionBuilder(builder.name, {
322
+ ...currentDef,
323
+ baseServices: (context) => {
324
+ const existing = currentBaseServices!(context);
325
+ return {
326
+ ...existing,
327
+ derivedValue: existing.baseValue * 2,
328
+ } as TBaseServices & { derivedValue: number };
329
+ },
330
+ });
331
+ };
332
+ };
333
+
334
+ const definition = defineFragment("ordered")
335
+ .extend(addBase())
336
+ .extend(addDerived()) // This depends on addBase being called first
337
+ .build();
338
+
339
+ const services = definition.baseServices!({
340
+ config: {},
341
+ options: {},
342
+ deps: {},
343
+ serviceDeps: {},
344
+ privateServices: {},
345
+ defineService: (svc) => svc,
346
+ });
347
+
348
+ expect(services.baseValue).toBe(42);
349
+ expect(services.derivedValue).toBe(84); // 42 * 2
350
+ });
351
+
352
+ it("withDependencies resets base services - extend before withDependencies is lost", () => {
353
+ const addUtility = () => {
354
+ return <
355
+ TConfig,
356
+ TOptions extends FragnoPublicConfig,
357
+ TDeps,
358
+ TBaseServices,
359
+ TServices,
360
+ TServiceDeps,
361
+ TPrivateServices,
362
+ TServiceThisContext extends RequestThisContext,
363
+ THandlerThisContext extends RequestThisContext,
364
+ TRequestStorage,
365
+ >(
366
+ builder: FragmentDefinitionBuilder<
367
+ TConfig,
368
+ TOptions,
369
+ TDeps,
370
+ TBaseServices,
371
+ TServices,
372
+ TServiceDeps,
373
+ TPrivateServices,
374
+ TServiceThisContext,
375
+ THandlerThisContext,
376
+ TRequestStorage
377
+ >,
378
+ ): FragmentDefinitionBuilder<
379
+ TConfig,
380
+ TOptions,
381
+ TDeps,
382
+ TBaseServices & { utility: { helper: () => string } },
383
+ TServices,
384
+ TServiceDeps,
385
+ TPrivateServices,
386
+ TServiceThisContext,
387
+ THandlerThisContext,
388
+ TRequestStorage
389
+ > => {
390
+ const currentDef = builder.build();
391
+ const currentBaseServices = currentDef.baseServices;
392
+
393
+ return new FragmentDefinitionBuilder(builder.name, {
394
+ ...currentDef,
395
+ baseServices: (context) => {
396
+ const existing = currentBaseServices
397
+ ? currentBaseServices(context)
398
+ : ({} as TBaseServices);
399
+ return {
400
+ ...existing,
401
+ utility: {
402
+ helper: () => "helped",
403
+ },
404
+ } as TBaseServices & { utility: { helper: () => string } };
405
+ },
406
+ });
407
+ };
408
+ };
409
+
410
+ // This demonstrates that withDependencies resets baseServices
411
+ // So extending BEFORE withDependencies means the extension is lost
412
+ const definition = defineFragment<{ key: string }>("early-extend")
413
+ .extend(addUtility())
414
+ .withDependencies(({ config }) => ({ key: config.key })) // This resets base services!
415
+ .build();
416
+
417
+ // Base services are undefined because withDependencies reset them
418
+ expect(definition.baseServices).toBeUndefined();
419
+ });
420
+
421
+ it("should preserve types when extending after other builder methods", () => {
422
+ const addFormatter = () => {
423
+ return <
424
+ TConfig,
425
+ TOptions extends FragnoPublicConfig,
426
+ TDeps,
427
+ TBaseServices,
428
+ TServices,
429
+ TServiceDeps,
430
+ TPrivateServices,
431
+ TServiceThisContext extends RequestThisContext,
432
+ THandlerThisContext extends RequestThisContext,
433
+ TRequestStorage,
434
+ >(
435
+ builder: FragmentDefinitionBuilder<
436
+ TConfig,
437
+ TOptions,
438
+ TDeps,
439
+ TBaseServices,
440
+ TServices,
441
+ TServiceDeps,
442
+ TPrivateServices,
443
+ TServiceThisContext,
444
+ THandlerThisContext,
445
+ TRequestStorage
446
+ >,
447
+ ): FragmentDefinitionBuilder<
448
+ TConfig,
449
+ TOptions,
450
+ TDeps,
451
+ TBaseServices & { formatter: { format: (s: string) => string } },
452
+ TServices,
453
+ TServiceDeps,
454
+ TPrivateServices,
455
+ TServiceThisContext,
456
+ THandlerThisContext,
457
+ TRequestStorage
458
+ > => {
459
+ const currentDef = builder.build();
460
+ const currentBaseServices = currentDef.baseServices;
461
+
462
+ return new FragmentDefinitionBuilder(builder.name, {
463
+ ...currentDef,
464
+ baseServices: (context) => {
465
+ const existing = currentBaseServices
466
+ ? currentBaseServices(context)
467
+ : ({} as TBaseServices);
468
+ return {
469
+ ...existing,
470
+ formatter: {
471
+ format: (s: string) => s.toUpperCase(),
472
+ },
473
+ } as TBaseServices & { formatter: { format: (s: string) => string } };
474
+ },
475
+ });
476
+ };
477
+ };
478
+
479
+ // Add other things first, then extend
480
+ const definition = defineFragment<{ prefix: string }>("late-extend")
481
+ .withDependencies(({ config }) => ({ prefix: config.prefix }))
482
+ .providesService("prefixer", ({ deps, defineService }) =>
483
+ defineService({
484
+ addPrefix: (s: string) => `${deps.prefix}-${s}`,
485
+ }),
486
+ )
487
+ .extend(addFormatter())
488
+ .build();
489
+
490
+ // Dependencies should still work
491
+ expect(definition.dependencies).toBeDefined();
492
+ const deps = definition.dependencies!({ config: { prefix: "TEST" }, options: {} });
493
+ expect(deps.prefix).toBe("TEST");
494
+
495
+ // Named services should still work
496
+ expect(definition.namedServices).toBeDefined();
497
+ const namedService = definition.namedServices!.prefixer({
498
+ config: { prefix: "TEST" },
499
+ options: {},
500
+ deps: { prefix: "TEST" },
501
+ serviceDeps: {},
502
+ privateServices: {},
503
+ defineService: (svc) => svc,
504
+ });
505
+ expect(namedService.addPrefix("value")).toBe("TEST-value");
506
+
507
+ // Extended base service should be present
508
+ const services = definition.baseServices!({
509
+ config: { prefix: "TEST" },
510
+ options: {},
511
+ deps: { prefix: "TEST" },
512
+ serviceDeps: {},
513
+ privateServices: {},
514
+ defineService: (svc) => svc,
515
+ });
516
+ expect(services.formatter.format("hello")).toBe("HELLO");
517
+ });
518
+ });
519
+
520
+ describe("type preservation", () => {
521
+ it("should preserve all type parameters through extend", () => {
522
+ type MyConfig = { value: number };
523
+ type MyDeps = { computed: number };
524
+
525
+ const addMath = () => {
526
+ return <
527
+ TConfig,
528
+ TOptions extends FragnoPublicConfig,
529
+ TDeps,
530
+ TBaseServices,
531
+ TServices,
532
+ TServiceDeps,
533
+ TPrivateServices,
534
+ TServiceThisContext extends RequestThisContext,
535
+ THandlerThisContext extends RequestThisContext,
536
+ TRequestStorage,
537
+ >(
538
+ builder: FragmentDefinitionBuilder<
539
+ TConfig,
540
+ TOptions,
541
+ TDeps,
542
+ TBaseServices,
543
+ TServices,
544
+ TServiceDeps,
545
+ TPrivateServices,
546
+ TServiceThisContext,
547
+ THandlerThisContext,
548
+ TRequestStorage
549
+ >,
550
+ ): FragmentDefinitionBuilder<
551
+ TConfig,
552
+ TOptions,
553
+ TDeps,
554
+ TBaseServices & { math: { add: (a: number, b: number) => number } },
555
+ TServices,
556
+ TServiceDeps,
557
+ TPrivateServices,
558
+ TServiceThisContext,
559
+ THandlerThisContext,
560
+ TRequestStorage
561
+ > => {
562
+ const currentDef = builder.build();
563
+ const currentBaseServices = currentDef.baseServices;
564
+
565
+ return new FragmentDefinitionBuilder(builder.name, {
566
+ ...currentDef,
567
+ baseServices: (context) => {
568
+ const existing = currentBaseServices
569
+ ? currentBaseServices(context)
570
+ : ({} as TBaseServices);
571
+ return {
572
+ ...existing,
573
+ math: {
574
+ add: (a: number, b: number) => a + b,
575
+ },
576
+ } as TBaseServices & { math: { add: (a: number, b: number) => number } };
577
+ },
578
+ });
579
+ };
580
+ };
581
+
582
+ const definition = defineFragment<MyConfig>("type-preserve")
583
+ .withDependencies(({ config }) => ({ computed: config.value * 2 }) as MyDeps)
584
+ .providesService("calculator", ({ deps, defineService }) =>
585
+ defineService({
586
+ getComputed: () => deps.computed,
587
+ }),
588
+ )
589
+ .usesService<"logger", { log: (msg: string) => void }>("logger")
590
+ .extend(addMath())
591
+ .build();
592
+
593
+ // Config type is preserved
594
+ expect(definition.dependencies).toBeDefined();
595
+ const deps = definition.dependencies!({ config: { value: 10 }, options: {} });
596
+ expect(deps.computed).toBe(20);
597
+
598
+ // Named services are preserved
599
+ expect(definition.namedServices).toBeDefined();
600
+ const calculator = definition.namedServices!.calculator({
601
+ config: { value: 10 },
602
+ options: {},
603
+ deps: { computed: 20 },
604
+ serviceDeps: { logger: { log: () => {} } },
605
+ privateServices: {},
606
+ defineService: (svc) => svc,
607
+ });
608
+ expect(calculator.getComputed()).toBe(20);
609
+
610
+ // Service dependencies are preserved
611
+ expect(definition.serviceDependencies).toBeDefined();
612
+ expect(definition.serviceDependencies!.logger).toEqual({ name: "logger", required: true });
613
+
614
+ // Extended base service is present
615
+ const services = definition.baseServices!({
616
+ config: { value: 10 },
617
+ options: {},
618
+ deps: { computed: 20 },
619
+ serviceDeps: { logger: { log: () => {} } },
620
+ privateServices: {},
621
+ defineService: (svc) => svc,
622
+ });
623
+ expect(services.math.add(5, 3)).toBe(8);
624
+ });
625
+
626
+ it("should allow extending to add request storage configuration", () => {
627
+ // This test shows a practical extend() pattern: adding storage after deps are set
628
+ const withRequestId = () => {
629
+ return <
630
+ TConfig,
631
+ TOptions extends FragnoPublicConfig,
632
+ TDeps,
633
+ TBaseServices,
634
+ TServices,
635
+ TServiceDeps,
636
+ TPrivateServices,
637
+ TServiceThisContext extends RequestThisContext,
638
+ THandlerThisContext extends RequestThisContext,
639
+ TRequestStorage,
640
+ >(
641
+ builder: FragmentDefinitionBuilder<
642
+ TConfig,
643
+ TOptions,
644
+ TDeps,
645
+ TBaseServices,
646
+ TServices,
647
+ TServiceDeps,
648
+ TPrivateServices,
649
+ TServiceThisContext,
650
+ THandlerThisContext,
651
+ TRequestStorage
652
+ >,
653
+ ) => {
654
+ // Simple approach: use the builder's own methods which handle types correctly
655
+ return builder.withRequestStorage(() => ({
656
+ requestId: Math.random().toString(36),
657
+ timestamp: Date.now(),
658
+ }));
659
+ };
660
+ };
661
+
662
+ const definition = defineFragment("request-extend")
663
+ .withDependencies(() => ({ value: 42 }))
664
+ .extend(withRequestId())
665
+ .build();
666
+
667
+ expect(definition.createRequestStorage).toBeDefined();
668
+ const storage = definition.createRequestStorage!({
669
+ config: {},
670
+ options: {},
671
+ deps: { value: 42 },
672
+ });
673
+ expect(storage).toHaveProperty("requestId");
674
+ expect(storage).toHaveProperty("timestamp");
675
+ expect(typeof storage.requestId).toBe("string");
676
+ expect(typeof storage.timestamp).toBe("number");
677
+ });
678
+ });
679
+
680
+ describe("edge cases", () => {
681
+ it("should handle extend on minimal fragment definition", () => {
682
+ const addSimple = () => {
683
+ return <
684
+ TConfig,
685
+ TOptions extends FragnoPublicConfig,
686
+ TDeps,
687
+ TBaseServices,
688
+ TServices,
689
+ TServiceDeps,
690
+ TPrivateServices,
691
+ TServiceThisContext extends RequestThisContext,
692
+ THandlerThisContext extends RequestThisContext,
693
+ TRequestStorage,
694
+ >(
695
+ builder: FragmentDefinitionBuilder<
696
+ TConfig,
697
+ TOptions,
698
+ TDeps,
699
+ TBaseServices,
700
+ TServices,
701
+ TServiceDeps,
702
+ TPrivateServices,
703
+ TServiceThisContext,
704
+ THandlerThisContext,
705
+ TRequestStorage
706
+ >,
707
+ ): FragmentDefinitionBuilder<
708
+ TConfig,
709
+ TOptions,
710
+ TDeps,
711
+ TBaseServices & { simple: string },
712
+ TServices,
713
+ TServiceDeps,
714
+ TPrivateServices,
715
+ TServiceThisContext,
716
+ THandlerThisContext,
717
+ TRequestStorage
718
+ > => {
719
+ const currentDef = builder.build();
720
+ const currentBaseServices = currentDef.baseServices;
721
+
722
+ return new FragmentDefinitionBuilder(builder.name, {
723
+ ...currentDef,
724
+ baseServices: (context) => {
725
+ const existing = currentBaseServices
726
+ ? currentBaseServices(context)
727
+ : ({} as TBaseServices);
728
+ return {
729
+ ...existing,
730
+ simple: "value",
731
+ } as TBaseServices & { simple: string };
732
+ },
733
+ });
734
+ };
735
+ };
736
+
737
+ // Minimal fragment - just a name, then extend
738
+ const definition = defineFragment("minimal").extend(addSimple()).build();
739
+
740
+ expect(definition.name).toBe("minimal");
741
+ expect(definition.baseServices).toBeDefined();
742
+
743
+ const services = definition.baseServices!({
744
+ config: {},
745
+ options: {},
746
+ deps: {},
747
+ serviceDeps: {},
748
+ privateServices: {},
749
+ defineService: (svc) => svc,
750
+ });
751
+
752
+ expect(services.simple).toBe("value");
753
+ });
754
+
755
+ it("should allow extend with identity transformation", () => {
756
+ const identity = () => {
757
+ return <
758
+ TConfig,
759
+ TOptions extends FragnoPublicConfig,
760
+ TDeps,
761
+ TBaseServices,
762
+ TServices,
763
+ TServiceDeps,
764
+ TPrivateServices,
765
+ TServiceThisContext extends RequestThisContext,
766
+ THandlerThisContext extends RequestThisContext,
767
+ TRequestStorage,
768
+ >(
769
+ builder: FragmentDefinitionBuilder<
770
+ TConfig,
771
+ TOptions,
772
+ TDeps,
773
+ TBaseServices,
774
+ TServices,
775
+ TServiceDeps,
776
+ TPrivateServices,
777
+ TServiceThisContext,
778
+ THandlerThisContext,
779
+ TRequestStorage
780
+ >,
781
+ ): FragmentDefinitionBuilder<
782
+ TConfig,
783
+ TOptions,
784
+ TDeps,
785
+ TBaseServices,
786
+ TServices,
787
+ TServiceDeps,
788
+ TPrivateServices,
789
+ TServiceThisContext,
790
+ THandlerThisContext,
791
+ TRequestStorage
792
+ > => {
793
+ // Just return the builder unchanged
794
+ return builder;
795
+ };
796
+ };
797
+
798
+ const definition = defineFragment("identity")
799
+ .withDependencies(() => ({ x: 1 }))
800
+ .extend(identity())
801
+ .build();
802
+
803
+ expect(definition.name).toBe("identity");
804
+ expect(definition.dependencies).toBeDefined();
805
+
806
+ const deps = definition.dependencies!({ config: {}, options: {} });
807
+ expect(deps.x).toBe(1);
808
+ });
809
+ });
810
+ });