@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,499 @@
1
+ import { describe, it, expect, expectTypeOf, vi } from "vitest";
2
+ import { defineFragment, type FragmentDefinition } from "./fragment-definition-builder";
3
+ import type { FragnoPublicConfig } from "./shared-types";
4
+ import type { RequestThisContext } from "./api";
5
+
6
+ describe("FragmentDefinitionBuilder", () => {
7
+ describe("defineFragment", () => {
8
+ it("should create a basic fragment builder", () => {
9
+ const builder = defineFragment("test-fragment");
10
+ expect(builder.name).toBe("test-fragment");
11
+ });
12
+
13
+ it("should build a minimal definition", () => {
14
+ const definition = defineFragment("test-fragment").build();
15
+
16
+ expect(definition.name).toBe("test-fragment");
17
+ expect(definition.dependencies).toBeUndefined();
18
+ expect(definition.baseServices).toBeUndefined();
19
+ expect(definition.namedServices).toBeUndefined();
20
+ expect(definition.serviceDependencies).toBeUndefined();
21
+ });
22
+ });
23
+
24
+ describe("withDependencies", () => {
25
+ it("should define dependencies", () => {
26
+ interface Config {
27
+ apiKey: string;
28
+ }
29
+
30
+ const definition = defineFragment<Config>("test-fragment")
31
+ .withDependencies(({ config, options }) => ({
32
+ apiKey: config.apiKey,
33
+ mountRoute: options.mountRoute,
34
+ }))
35
+ .build();
36
+
37
+ expect(definition.dependencies).toBeDefined();
38
+
39
+ // Test the dependencies function
40
+ const deps = definition.dependencies!({
41
+ config: { apiKey: "test-key" },
42
+ options: { mountRoute: "/api" },
43
+ });
44
+
45
+ expect(deps.apiKey).toBe("test-key");
46
+ expect(deps.mountRoute).toBe("/api");
47
+ });
48
+
49
+ it("should reset services when setting dependencies", () => {
50
+ const definition = defineFragment("test-fragment")
51
+ .providesBaseService(() => ({
52
+ method1: () => "test",
53
+ }))
54
+ .withDependencies(() => ({
55
+ dep1: "value",
56
+ }))
57
+ .build();
58
+
59
+ // Base services should be reset
60
+ expect(definition.baseServices).toBeUndefined();
61
+ expect(definition.dependencies).toBeDefined();
62
+ });
63
+
64
+ it("should reset request storage and context when called late in chain", () => {
65
+ // This demonstrates that calling withDependencies late erases earlier storage/context setup
66
+ const definition = defineFragment("test-fragment")
67
+ .withRequestStorage(() => ({
68
+ counter: 0,
69
+ userId: "user-123",
70
+ }))
71
+ .withThisContext(({ storage }) => {
72
+ const ctx = {
73
+ get counter() {
74
+ return storage.getStore()?.counter ?? 0;
75
+ },
76
+ get userId() {
77
+ return storage.getStore()?.userId;
78
+ },
79
+ };
80
+ return { serviceContext: ctx, handlerContext: ctx };
81
+ })
82
+ // Calling withDependencies here will erase the storage and context configuration!
83
+ .withDependencies(() => ({
84
+ apiKey: "secret",
85
+ }))
86
+ .build();
87
+
88
+ // Storage and context should be reset (undefined)
89
+ expect(definition.createRequestStorage).toBeUndefined();
90
+ expect(definition.createThisContext).toBeUndefined();
91
+ expect(definition.getExternalStorage).toBeUndefined();
92
+ expect(definition.dependencies).toBeDefined();
93
+ });
94
+
95
+ it("should preserve storage and context when dependencies set early", () => {
96
+ // This is the recommended pattern: set dependencies first
97
+ const definition = defineFragment("test-fragment")
98
+ .withDependencies(() => ({
99
+ apiKey: "secret",
100
+ }))
101
+ .withRequestStorage(({ deps }) => ({
102
+ counter: 0,
103
+ apiKey: deps.apiKey,
104
+ }))
105
+ .withThisContext(({ storage }) => {
106
+ const ctx = {
107
+ get counter() {
108
+ return storage.getStore()?.counter ?? 0;
109
+ },
110
+ };
111
+ return { serviceContext: ctx, handlerContext: ctx };
112
+ })
113
+ .build();
114
+
115
+ // Everything should be preserved
116
+ expect(definition.createRequestStorage).toBeDefined();
117
+ expect(definition.createThisContext).toBeDefined();
118
+ expect(definition.dependencies).toBeDefined();
119
+ });
120
+
121
+ it("should warn when withDependencies is called after storage/services are configured", () => {
122
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
123
+
124
+ defineFragment("test-fragment")
125
+ .withRequestStorage(() => ({ counter: 0 }))
126
+ .providesService("myService", () => ({ test: () => "hi" }))
127
+ .withDependencies(() => ({ apiKey: "secret" }));
128
+
129
+ expect(warnSpy).toHaveBeenCalledWith(
130
+ expect.stringContaining(
131
+ '[Fragno] Warning: withDependencies() on fragment "test-fragment" is resetting',
132
+ ),
133
+ );
134
+
135
+ warnSpy.mockRestore();
136
+ });
137
+
138
+ it("should not warn when withDependencies is called early", () => {
139
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
140
+
141
+ defineFragment("test-fragment")
142
+ .withDependencies(() => ({ apiKey: "secret" }))
143
+ .withRequestStorage(() => ({ counter: 0 }))
144
+ .providesService("myService", () => ({ test: () => "hi" }));
145
+
146
+ expect(warnSpy).not.toHaveBeenCalled();
147
+
148
+ warnSpy.mockRestore();
149
+ });
150
+ });
151
+
152
+ describe("providesBaseService", () => {
153
+ it("should define unnamed services", () => {
154
+ const definition = defineFragment("test-fragment")
155
+ .withDependencies(() => ({ apiKey: "key" }))
156
+ .providesBaseService(({ deps }) => ({
157
+ method1: () => `${deps.apiKey}-method1`,
158
+ method2: () => "method2",
159
+ }))
160
+ .build();
161
+
162
+ expect(definition.baseServices).toBeDefined();
163
+
164
+ // Test the services function
165
+ const services = definition.baseServices!({
166
+ config: {},
167
+ options: {},
168
+ deps: { apiKey: "test-key" },
169
+ serviceDeps: {},
170
+ privateServices: {},
171
+ defineService: (svc) => svc,
172
+ });
173
+
174
+ expect(services.method1()).toBe("test-key-method1");
175
+ expect(services.method2()).toBe("method2");
176
+ });
177
+
178
+ it("should define unnamed services with defineService", () => {
179
+ interface Bla extends RequestThisContext {
180
+ myThisNumber: number;
181
+ myThisString: string;
182
+ }
183
+
184
+ const definition = defineFragment<{}, FragnoPublicConfig, Bla>("test-fragment")
185
+ .withDependencies(() => ({ apiKey: "key" }))
186
+ .providesBaseService(({ deps, defineService }) =>
187
+ defineService({
188
+ method1: function () {
189
+ this.myThisNumber++;
190
+ expectTypeOf(this).toMatchObjectType<{
191
+ myThisNumber: number;
192
+ myThisString: string;
193
+ }>();
194
+ return `${deps.apiKey}-method1`;
195
+ },
196
+ }),
197
+ )
198
+ .build();
199
+
200
+ expect(definition.baseServices).toBeDefined();
201
+ });
202
+ });
203
+
204
+ describe("providesService", () => {
205
+ it("should define named services", () => {
206
+ const definition = defineFragment("test-fragment")
207
+ .withDependencies(() => ({ apiKey: "key" }))
208
+ .providesService("email", ({ deps }) => ({
209
+ send: (to: string) => `Sending to ${to} with ${deps.apiKey}`,
210
+ }))
211
+ .build();
212
+
213
+ expect(definition.namedServices).toBeDefined();
214
+ expect(definition.namedServices!.email).toBeDefined();
215
+
216
+ // Test the service function
217
+ const emailService = definition.namedServices!.email({
218
+ config: {},
219
+ options: {},
220
+ deps: { apiKey: "test-key" },
221
+ serviceDeps: {},
222
+ privateServices: {},
223
+ defineService: (svc) => svc,
224
+ });
225
+
226
+ expect(emailService.send("user@example.com")).toBe(
227
+ "Sending to user@example.com with test-key",
228
+ );
229
+ });
230
+
231
+ it("should support multiple named services", () => {
232
+ const definition = defineFragment("test-fragment")
233
+ .providesService("email", () => ({
234
+ send: () => "email sent",
235
+ }))
236
+ .providesService("sms", () => ({
237
+ send: () => "sms sent",
238
+ }))
239
+ .build();
240
+
241
+ expect(definition.namedServices!.email).toBeDefined();
242
+ expect(definition.namedServices!.sms).toBeDefined();
243
+
244
+ const emailService = definition.namedServices!.email({
245
+ config: {},
246
+ options: {},
247
+ deps: {},
248
+ serviceDeps: {},
249
+ privateServices: {},
250
+ defineService: (svc) => svc,
251
+ });
252
+ const smsService = definition.namedServices!.sms({
253
+ config: {},
254
+ options: {},
255
+ deps: {},
256
+ serviceDeps: {},
257
+ privateServices: {},
258
+ defineService: (svc) => svc,
259
+ });
260
+
261
+ expect(emailService.send()).toBe("email sent");
262
+ expect(smsService.send()).toBe("sms sent");
263
+ });
264
+ });
265
+
266
+ describe("usesService", () => {
267
+ it("should declare required service dependency", () => {
268
+ interface EmailService {
269
+ send: (to: string) => void;
270
+ }
271
+
272
+ const definition = defineFragment("test-fragment")
273
+ .usesService<"email", EmailService>("email")
274
+ .build();
275
+
276
+ expect(definition.serviceDependencies).toBeDefined();
277
+ expect(definition.serviceDependencies!.email).toEqual({
278
+ name: "email",
279
+ required: true,
280
+ });
281
+ });
282
+
283
+ it("should declare optional service dependency", () => {
284
+ interface LogService {
285
+ log: (msg: string) => void;
286
+ }
287
+
288
+ const definition = defineFragment("test-fragment")
289
+ .usesOptionalService<"logger", LogService>("logger")
290
+ .build();
291
+
292
+ expect(definition.serviceDependencies!.logger).toEqual({
293
+ name: "logger",
294
+ required: false,
295
+ });
296
+ });
297
+
298
+ it("should support multiple service dependencies", () => {
299
+ interface EmailService {
300
+ send: (to: string) => void;
301
+ }
302
+ interface LogService {
303
+ log: (msg: string) => void;
304
+ }
305
+
306
+ const definition = defineFragment("test-fragment")
307
+ .usesService<"email", EmailService>("email")
308
+ .usesOptionalService<"logger", LogService>("logger")
309
+ .build();
310
+
311
+ expect(definition.serviceDependencies!.email.required).toBe(true);
312
+ expect(definition.serviceDependencies!.logger.required).toBe(false);
313
+ });
314
+
315
+ it("should allow services to use service dependencies", () => {
316
+ interface EmailService {
317
+ send: (to: string) => string;
318
+ }
319
+
320
+ const definition = defineFragment("test-fragment")
321
+ .withDependencies(() => ({ apiKey: "key" }))
322
+ .usesService<"email", EmailService>("email")
323
+ .providesBaseService(({ deps, serviceDeps }) => ({
324
+ sendWelcome: () => {
325
+ // serviceDeps should have email
326
+ return serviceDeps.email.send("welcome@example.com");
327
+ },
328
+ getKey: () => deps.apiKey,
329
+ }))
330
+ .build();
331
+
332
+ // Test with mock email service
333
+ const services = definition.baseServices!({
334
+ config: {},
335
+ options: {},
336
+ deps: { apiKey: "test-key" },
337
+ serviceDeps: {
338
+ email: {
339
+ send: (to: string) => `Email sent to ${to}`,
340
+ },
341
+ },
342
+ privateServices: {},
343
+ defineService: (svc) => svc,
344
+ });
345
+
346
+ expect(services.sendWelcome()).toBe("Email sent to welcome@example.com");
347
+ expect(services.getKey()).toBe("test-key");
348
+ });
349
+ });
350
+
351
+ describe("complex scenarios", () => {
352
+ it("should support full fragment definition", () => {
353
+ interface Config {
354
+ apiKey: string;
355
+ debug: boolean;
356
+ }
357
+
358
+ interface LogService {
359
+ log: (msg: string) => void;
360
+ }
361
+
362
+ const definition = defineFragment<Config>("complex-fragment")
363
+ .withDependencies(({ config }) => ({
364
+ apiKey: config.apiKey,
365
+ debug: config.debug,
366
+ }))
367
+ .usesOptionalService<"logger", LogService>("logger")
368
+ .providesBaseService(({ deps, serviceDeps }) => ({
369
+ getData: () => {
370
+ if (serviceDeps.logger) {
371
+ serviceDeps.logger.log("Getting data");
372
+ }
373
+ return `data-${deps.apiKey}`;
374
+ },
375
+ }))
376
+ .providesService("analytics", ({ deps }) => ({
377
+ track: (event: string) => `Tracking ${event} with ${deps.apiKey}`,
378
+ }))
379
+ .build();
380
+
381
+ expect(definition.name).toBe("complex-fragment");
382
+ expect(definition.dependencies).toBeDefined();
383
+ expect(definition.baseServices).toBeDefined();
384
+ expect(definition.namedServices).toBeDefined();
385
+ expect(definition.serviceDependencies).toBeDefined();
386
+
387
+ // Test execution
388
+ const logs: string[] = [];
389
+ const deps = definition.dependencies!({
390
+ config: { apiKey: "my-key", debug: true },
391
+ options: {},
392
+ });
393
+
394
+ const services = definition.baseServices!({
395
+ config: { apiKey: "my-key", debug: true },
396
+ options: {},
397
+ deps,
398
+ serviceDeps: {
399
+ logger: {
400
+ log: (msg) => logs.push(msg),
401
+ },
402
+ },
403
+ privateServices: {},
404
+ defineService: (svc) => svc,
405
+ });
406
+
407
+ const analyticsService = definition.namedServices!.analytics({
408
+ config: { apiKey: "my-key", debug: true },
409
+ options: {},
410
+ deps,
411
+ serviceDeps: {
412
+ logger: {
413
+ log: (msg) => logs.push(msg),
414
+ },
415
+ },
416
+ privateServices: {},
417
+ defineService: (svc) => svc,
418
+ });
419
+
420
+ expect(services.getData()).toBe("data-my-key");
421
+ expect(logs).toContain("Getting data");
422
+ expect(analyticsService.track("click")).toBe("Tracking click with my-key");
423
+ });
424
+ });
425
+
426
+ describe("type safety", () => {
427
+ it("should infer correct types", () => {
428
+ interface Config {
429
+ port: number;
430
+ }
431
+
432
+ const builder = defineFragment<Config>("typed-fragment")
433
+ .withDependencies(({ config }) => ({
434
+ port: config.port,
435
+ }))
436
+ .providesService("server", ({ deps }) => ({
437
+ start: () => `Server starting on port ${deps.port}`,
438
+ }));
439
+
440
+ const definition = builder.build();
441
+
442
+ // Type check: definition should have correct structure
443
+ type DefType = typeof definition;
444
+ const _typeCheck: DefType extends FragmentDefinition<
445
+ Config,
446
+ FragnoPublicConfig,
447
+ { port: number },
448
+ {},
449
+ { server: { start: () => string } },
450
+ {},
451
+ {},
452
+ RequestThisContext,
453
+ RequestThisContext,
454
+ {}
455
+ >
456
+ ? true
457
+ : false = true;
458
+
459
+ expect(_typeCheck).toBe(true);
460
+ });
461
+ });
462
+
463
+ describe("extend", () => {
464
+ it("should allow extending builder with transformation function", () => {
465
+ const builder = defineFragment("test");
466
+
467
+ // Simple transformation that wraps the builder
468
+ const extended = builder.extend((b) => ({
469
+ builder: b,
470
+ additionalMethod: () => "extended",
471
+ }));
472
+
473
+ expect(extended.builder).toBe(builder);
474
+ expect(extended.additionalMethod()).toBe("extended");
475
+ });
476
+
477
+ it("should pass correct type through transformer", () => {
478
+ interface Config {
479
+ apiKey: string;
480
+ }
481
+
482
+ const builder = defineFragment<Config>("test").withDependencies(({ config }) => ({
483
+ key: config.apiKey,
484
+ }));
485
+
486
+ // Transformer that returns a new builder type
487
+ const extended = builder.extend((b) => {
488
+ const def = b.build();
489
+ return {
490
+ definition: def,
491
+ name: def.name,
492
+ };
493
+ });
494
+
495
+ expect(extended.name).toBe("test");
496
+ expect(extended.definition.name).toBe("test");
497
+ });
498
+ });
499
+ });