@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,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
+ });