@fragno-dev/core 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.turbo/turbo-build.log +45 -53
  2. package/CHANGELOG.md +6 -0
  3. package/dist/api/api.d.ts +2 -2
  4. package/dist/api/api.js +3 -2
  5. package/dist/api/fragment-builder.d.ts +2 -4
  6. package/dist/api/fragment-builder.js +1 -1
  7. package/dist/api/fragment-instantiation.d.ts +2 -4
  8. package/dist/api/fragment-instantiation.js +3 -5
  9. package/dist/api/route.d.ts +2 -3
  10. package/dist/api/route.js +1 -1
  11. package/dist/api-BFrUCIsF.d.ts +963 -0
  12. package/dist/api-BFrUCIsF.d.ts.map +1 -0
  13. package/dist/client/client.d.ts +1 -3
  14. package/dist/client/client.js +4 -5
  15. package/dist/client/client.svelte.d.ts +2 -3
  16. package/dist/client/client.svelte.d.ts.map +1 -1
  17. package/dist/client/client.svelte.js +4 -5
  18. package/dist/client/client.svelte.js.map +1 -1
  19. package/dist/client/react.d.ts +2 -3
  20. package/dist/client/react.d.ts.map +1 -1
  21. package/dist/client/react.js +4 -5
  22. package/dist/client/react.js.map +1 -1
  23. package/dist/client/solid.d.ts +2 -3
  24. package/dist/client/solid.d.ts.map +1 -1
  25. package/dist/client/solid.js +4 -5
  26. package/dist/client/solid.js.map +1 -1
  27. package/dist/client/vanilla.d.ts +2 -3
  28. package/dist/client/vanilla.d.ts.map +1 -1
  29. package/dist/client/vanilla.js +4 -5
  30. package/dist/client/vanilla.js.map +1 -1
  31. package/dist/client/vue.d.ts +2 -3
  32. package/dist/client/vue.d.ts.map +1 -1
  33. package/dist/client/vue.js +4 -5
  34. package/dist/client/vue.js.map +1 -1
  35. package/dist/{client-C5LsYHEI.js → client-DAFHcKqA.js} +4 -4
  36. package/dist/{client-C5LsYHEI.js.map → client-DAFHcKqA.js.map} +1 -1
  37. package/dist/fragment-builder-Boh2vNHq.js +108 -0
  38. package/dist/fragment-builder-Boh2vNHq.js.map +1 -0
  39. package/dist/fragment-instantiation-DUT-HLl1.js +898 -0
  40. package/dist/fragment-instantiation-DUT-HLl1.js.map +1 -0
  41. package/dist/integrations/react-ssr.js +1 -1
  42. package/dist/mod.d.ts +2 -4
  43. package/dist/mod.js +4 -6
  44. package/dist/{route-C5Uryylh.js → route-C4CyNHkC.js} +8 -3
  45. package/dist/route-C4CyNHkC.js.map +1 -0
  46. package/dist/{ssr-BByDVfFD.js → ssr-kyKI7pqH.js} +1 -1
  47. package/dist/{ssr-BByDVfFD.js.map → ssr-kyKI7pqH.js.map} +1 -1
  48. package/dist/test/test.d.ts +6 -7
  49. package/dist/test/test.d.ts.map +1 -1
  50. package/dist/test/test.js +9 -7
  51. package/dist/test/test.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/api/api.ts +45 -6
  54. package/src/api/fragment-builder.ts +463 -25
  55. package/src/api/fragment-instantiation.test.ts +249 -7
  56. package/src/api/fragment-instantiation.ts +283 -16
  57. package/src/api/fragment-services.test.ts +462 -0
  58. package/src/api/fragment.test.ts +65 -17
  59. package/src/api/request-middleware.test.ts +6 -3
  60. package/src/api/route.test.ts +111 -1
  61. package/src/api/route.ts +323 -14
  62. package/src/mod.ts +11 -1
  63. package/src/test/test.test.ts +20 -15
  64. package/src/test/test.ts +48 -9
  65. package/dist/api-BWN97TOr.d.ts +0 -377
  66. package/dist/api-BWN97TOr.d.ts.map +0 -1
  67. package/dist/api-DngJDcmO.js +0 -54
  68. package/dist/api-DngJDcmO.js.map +0 -1
  69. package/dist/fragment-builder-DOnCVBqc.js +0 -47
  70. package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
  71. package/dist/fragment-builder-MGr68GNb.d.ts +0 -409
  72. package/dist/fragment-builder-MGr68GNb.d.ts.map +0 -1
  73. package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
  74. package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
  75. package/dist/request-output-context-CdIjwmEN.js +0 -320
  76. package/dist/request-output-context-CdIjwmEN.js.map +0 -1
  77. package/dist/route-Bl9Zr1Yv.d.ts +0 -26
  78. package/dist/route-Bl9Zr1Yv.d.ts.map +0 -1
  79. package/dist/route-C5Uryylh.js.map +0 -1
@@ -1,26 +1,92 @@
1
+ import type { RequestThisContext } from "./api";
1
2
  import type { FragnoPublicConfig } from "./fragment-instantiation";
3
+ import type { RequestInputContext } from "./request-input-context";
4
+ import type { RequestOutputContext } from "./request-output-context";
5
+
6
+ /**
7
+ * Metadata for a service dependency
8
+ */
9
+ interface ServiceMetadata {
10
+ /** Name of the service */
11
+ name: string;
12
+ /** Whether this service is required (false means optional) */
13
+ required: boolean;
14
+ }
15
+
16
+ export type RouteHandler = (
17
+ this: RequestThisContext,
18
+ inputContext: RequestInputContext,
19
+ outputContext: RequestOutputContext,
20
+ ) => Promise<Response>;
2
21
 
3
22
  export interface FragmentDefinition<
4
23
  TConfig,
5
24
  TDeps = {},
6
- TServices extends Record<string, unknown> = {},
25
+ TServices = {},
7
26
  TAdditionalContext extends Record<string, unknown> = {},
27
+ TUsedServices = {},
28
+ TProvidedServices = {},
29
+ TThisContext extends RequestThisContext = RequestThisContext,
8
30
  > {
9
31
  name: string;
10
32
  dependencies?: (config: TConfig, options: FragnoPublicConfig) => TDeps;
11
- services?: (config: TConfig, options: FragnoPublicConfig, deps: TDeps) => TServices;
33
+ services?: (
34
+ config: TConfig,
35
+ options: FragnoPublicConfig,
36
+ deps: TDeps & TUsedServices,
37
+ ) => TServices;
12
38
  additionalContext?: TAdditionalContext;
39
+ createHandlerWrapper?: (
40
+ options: FragnoPublicConfig,
41
+ ) => (
42
+ handler: (this: TThisContext, ...args: Parameters<RouteHandler>) => ReturnType<RouteHandler>,
43
+ ) => RouteHandler;
44
+ /** Services that this fragment uses (can be required or optional) */
45
+ usedServices?: {
46
+ [K in keyof TUsedServices]: ServiceMetadata;
47
+ };
48
+ /** Services that this fragment provides to other fragments (can be a factory function or direct object) */
49
+ providedServices?:
50
+ | {
51
+ [K in keyof TProvidedServices]: TProvidedServices[K];
52
+ }
53
+ | ((
54
+ config: TConfig,
55
+ options: FragnoPublicConfig,
56
+ deps: TDeps & TUsedServices,
57
+ ) => TProvidedServices);
13
58
  }
14
59
 
15
60
  export class FragmentBuilder<
16
61
  const TConfig,
17
62
  const TDeps = {},
18
- const TServices extends Record<string, unknown> = {},
63
+ const TServices = {},
19
64
  const TAdditionalContext extends Record<string, unknown> = {},
65
+ const TUsedServices = {},
66
+ const TProvidedServices = {},
67
+ const TThisContext extends RequestThisContext = RequestThisContext,
20
68
  > {
21
- #definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;
69
+ #definition: FragmentDefinition<
70
+ TConfig,
71
+ TDeps,
72
+ TServices,
73
+ TAdditionalContext,
74
+ TUsedServices,
75
+ TProvidedServices,
76
+ TThisContext
77
+ >;
22
78
 
23
- constructor(definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>) {
79
+ constructor(
80
+ definition: FragmentDefinition<
81
+ TConfig,
82
+ TDeps,
83
+ TServices,
84
+ TAdditionalContext,
85
+ TUsedServices,
86
+ TProvidedServices,
87
+ TThisContext
88
+ >,
89
+ ) {
24
90
  this.#definition = definition;
25
91
  }
26
92
 
@@ -36,8 +102,36 @@ export class FragmentBuilder<
36
102
  fn: (
37
103
  context: { config: TConfig; fragnoConfig: FragnoPublicConfig } & TAdditionalContext,
38
104
  ) => TNewDeps,
39
- ): FragmentBuilder<TConfig, TNewDeps, {}, TAdditionalContext> {
40
- return new FragmentBuilder<TConfig, TNewDeps, {}, TAdditionalContext>({
105
+ ): FragmentBuilder<
106
+ TConfig,
107
+ TNewDeps,
108
+ {},
109
+ TAdditionalContext,
110
+ TUsedServices,
111
+ TProvidedServices,
112
+ TThisContext
113
+ > {
114
+ // Safe cast: If providedServices is a function, we need to update its signature to use TNewDeps.
115
+ // This is safe because the function will be called with the new deps at runtime.
116
+ const providedServices = this.#definition.providedServices;
117
+ const recastProvidedServices =
118
+ typeof providedServices === "function"
119
+ ? (providedServices as unknown as (
120
+ config: TConfig,
121
+ options: FragnoPublicConfig,
122
+ deps: TNewDeps & TUsedServices,
123
+ ) => TProvidedServices)
124
+ : providedServices;
125
+
126
+ return new FragmentBuilder<
127
+ TConfig,
128
+ TNewDeps,
129
+ {},
130
+ TAdditionalContext,
131
+ TUsedServices,
132
+ TProvidedServices,
133
+ TThisContext
134
+ >({
41
135
  name: this.#definition.name,
42
136
  dependencies: (config: TConfig, options: FragnoPublicConfig) => {
43
137
  return fn({ config, fragnoConfig: options } as {
@@ -47,33 +141,377 @@ export class FragmentBuilder<
47
141
  },
48
142
  services: undefined,
49
143
  additionalContext: this.#definition.additionalContext,
144
+ usedServices: this.#definition.usedServices,
145
+ providedServices: recastProvidedServices,
50
146
  });
51
147
  }
52
148
 
53
- withServices<TNewServices extends Record<string, unknown>>(
54
- fn: (
55
- context: {
56
- config: TConfig;
57
- fragnoConfig: FragnoPublicConfig;
58
- deps: TDeps;
59
- } & TAdditionalContext,
60
- ) => TNewServices,
61
- ): FragmentBuilder<TConfig, TDeps, TNewServices, TAdditionalContext> {
62
- return new FragmentBuilder<TConfig, TDeps, TNewServices, TAdditionalContext>({
149
+ /**
150
+ * Declare that this fragment uses a service.
151
+ * By default, the service is required. Pass { optional: true } to make it optional.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * // Required service
156
+ * defineFragment("my-fragment")
157
+ * .usesService<"email", IEmailService>("email")
158
+ * .providesService(({ deps, define }) => define({
159
+ * sendWelcome: () => deps.email.send(...)
160
+ * }))
161
+ *
162
+ * // Optional service
163
+ * defineFragment("my-fragment")
164
+ * .usesService<"email", IEmailService>("email", { optional: true })
165
+ * .providesService(({ deps, define }) => define({
166
+ * sendWelcome: () => {
167
+ * if (deps.email) {
168
+ * deps.email.send(...)
169
+ * }
170
+ * }
171
+ * }))
172
+ * ```
173
+ */
174
+ usesService<TServiceName extends string, TService>(
175
+ serviceName: TServiceName,
176
+ options?: { optional?: false },
177
+ ): FragmentBuilder<
178
+ TConfig,
179
+ TDeps,
180
+ TServices,
181
+ TAdditionalContext,
182
+ TUsedServices & { [K in TServiceName]: TService },
183
+ TProvidedServices,
184
+ TThisContext
185
+ >;
186
+ usesService<TServiceName extends string, TService>(
187
+ serviceName: TServiceName,
188
+ options: { optional: true },
189
+ ): FragmentBuilder<
190
+ TConfig,
191
+ TDeps,
192
+ TServices,
193
+ TAdditionalContext,
194
+ TUsedServices & { [K in TServiceName]: TService | undefined },
195
+ TProvidedServices,
196
+ TThisContext
197
+ >;
198
+ usesService<TServiceName extends string, TService>(
199
+ serviceName: TServiceName,
200
+ options?: { optional?: boolean },
201
+ ): FragmentBuilder<
202
+ TConfig,
203
+ TDeps,
204
+ TServices,
205
+ TAdditionalContext,
206
+ TUsedServices & { [K in TServiceName]: TService | TService | undefined },
207
+ TProvidedServices,
208
+ TThisContext
209
+ > {
210
+ const isOptional = options?.optional ?? false;
211
+ return new FragmentBuilder<
212
+ TConfig,
213
+ TDeps,
214
+ TServices,
215
+ TAdditionalContext,
216
+ TUsedServices & { [K in TServiceName]: TService | (TService | undefined) },
217
+ TProvidedServices,
218
+ TThisContext
219
+ >({
63
220
  name: this.#definition.name,
64
221
  dependencies: this.#definition.dependencies,
65
- services: (config: TConfig, options: FragnoPublicConfig, deps: TDeps) => {
66
- return fn({ config, fragnoConfig: options, deps } as {
67
- config: TConfig;
68
- fragnoConfig: FragnoPublicConfig;
69
- deps: TDeps;
70
- } & TAdditionalContext);
71
- },
222
+ services: this.#definition.services as
223
+ | ((
224
+ config: TConfig,
225
+ options: FragnoPublicConfig,
226
+ deps: TDeps &
227
+ (TUsedServices & { [K in TServiceName]: TService | (TService | undefined) }),
228
+ ) => TServices)
229
+ | undefined,
72
230
  additionalContext: this.#definition.additionalContext,
231
+ usedServices: {
232
+ ...this.#definition.usedServices,
233
+ [serviceName]: { name: serviceName, required: !isOptional },
234
+ } as {
235
+ [K in keyof (TUsedServices & {
236
+ [K in TServiceName]: TService | (TService | undefined);
237
+ })]: ServiceMetadata;
238
+ },
239
+ providedServices: this.#definition.providedServices,
73
240
  });
74
241
  }
242
+
243
+ /**
244
+ * Define services for this fragment (unnamed) using a callback.
245
+ * Use the `defineService` function from the callback context for proper typing (optional).
246
+ */
247
+ providesService<TNewServices>(
248
+ fn: (context: {
249
+ config: TConfig;
250
+ fragnoConfig: FragnoPublicConfig;
251
+ deps: TDeps & TUsedServices;
252
+ defineService: <T>(services: T) => T;
253
+ }) => TNewServices,
254
+ ): FragmentBuilder<
255
+ TConfig,
256
+ TDeps,
257
+ TNewServices,
258
+ TAdditionalContext,
259
+ TUsedServices,
260
+ TProvidedServices,
261
+ TThisContext
262
+ >;
263
+
264
+ /**
265
+ * Define services for this fragment (unnamed) using a direct object.
266
+ */
267
+ providesService<TNewServices>(
268
+ services: TNewServices,
269
+ ): FragmentBuilder<
270
+ TConfig,
271
+ TDeps,
272
+ TNewServices,
273
+ TAdditionalContext,
274
+ TUsedServices,
275
+ TProvidedServices,
276
+ TThisContext
277
+ >;
278
+
279
+ /**
280
+ * Provide a named service using a callback.
281
+ * Use the `defineService` function from the callback context for proper typing (optional).
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * interface IEmailService {
286
+ * send(to: string, subject: string, body: string): Promise<void>;
287
+ * }
288
+ *
289
+ * defineFragment("email-fragment")
290
+ * .providesService<"email", IEmailService>("email", ({ defineService }) => defineService({
291
+ * send: async (to, subject, body) => {
292
+ * // implementation
293
+ * }
294
+ * }))
295
+ * ```
296
+ */
297
+ providesService<TServiceName extends string, TService>(
298
+ serviceName: TServiceName,
299
+ fn: (context: {
300
+ config: TConfig;
301
+ fragnoConfig: FragnoPublicConfig;
302
+ deps: TDeps & TUsedServices;
303
+ defineService: <T>(services: T) => T;
304
+ }) => TService,
305
+ ): FragmentBuilder<
306
+ TConfig,
307
+ TDeps,
308
+ TServices,
309
+ TAdditionalContext,
310
+ TUsedServices,
311
+ TProvidedServices & { [K in TServiceName]: TService },
312
+ TThisContext
313
+ >;
314
+
315
+ /**
316
+ * Provide a named service using a direct object.
317
+ */
318
+ providesService<TServiceName extends string, TService>(
319
+ serviceName: TServiceName,
320
+ service: TService,
321
+ ): FragmentBuilder<
322
+ TConfig,
323
+ TDeps,
324
+ TServices,
325
+ TAdditionalContext,
326
+ TUsedServices,
327
+ TProvidedServices & { [K in TServiceName]: TService },
328
+ TThisContext
329
+ >;
330
+
331
+ providesService<TServiceName extends string, TService>(
332
+ ...args:
333
+ | [
334
+ fnOrServices:
335
+ | ((context: {
336
+ config: TConfig;
337
+ fragnoConfig: FragnoPublicConfig;
338
+ deps: TDeps & TUsedServices;
339
+ defineService: <T>(services: T) => T;
340
+ }) => TService)
341
+ | TService,
342
+ ]
343
+ | [
344
+ serviceName: TServiceName,
345
+ fnOrService:
346
+ | ((context: {
347
+ config: TConfig;
348
+ fragnoConfig: FragnoPublicConfig;
349
+ deps: TDeps & TUsedServices;
350
+ defineService: <T>(services: T) => T;
351
+ }) => TService)
352
+ | TService,
353
+ ]
354
+ ):
355
+ | FragmentBuilder<
356
+ TConfig,
357
+ TDeps,
358
+ TService,
359
+ TAdditionalContext,
360
+ TUsedServices,
361
+ TProvidedServices,
362
+ TThisContext
363
+ >
364
+ | FragmentBuilder<
365
+ TConfig,
366
+ TDeps,
367
+ TServices,
368
+ TAdditionalContext,
369
+ TUsedServices,
370
+ TProvidedServices & { [K in TServiceName]: TService },
371
+ TThisContext
372
+ > {
373
+ // The defineService function is just an identity function at runtime
374
+ const defineService = <T>(services: T) => services;
375
+
376
+ if (args.length === 1) {
377
+ // Unnamed service
378
+ const [fnOrServices] = args;
379
+
380
+ // Create a callback that provides the full context including defineService
381
+ const servicesCallback = (
382
+ config: TConfig,
383
+ options: FragnoPublicConfig,
384
+ deps: TDeps & TUsedServices,
385
+ ): TService => {
386
+ if (typeof fnOrServices === "function") {
387
+ // It's a factory function - call it with context
388
+ const fn = fnOrServices as (context: {
389
+ config: TConfig;
390
+ fragnoConfig: FragnoPublicConfig;
391
+ deps: TDeps & TUsedServices;
392
+ defineService: <T>(services: T) => T;
393
+ }) => TService;
394
+ return fn({
395
+ config,
396
+ fragnoConfig: options,
397
+ deps,
398
+ defineService,
399
+ });
400
+ } else {
401
+ // It's a direct service object
402
+ return fnOrServices;
403
+ }
404
+ };
405
+
406
+ return new FragmentBuilder<
407
+ TConfig,
408
+ TDeps,
409
+ TService,
410
+ TAdditionalContext,
411
+ TUsedServices,
412
+ TProvidedServices,
413
+ TThisContext
414
+ >({
415
+ name: this.#definition.name,
416
+ dependencies: this.#definition.dependencies,
417
+ services: servicesCallback as (
418
+ config: TConfig,
419
+ options: FragnoPublicConfig,
420
+ deps: TDeps & TUsedServices,
421
+ ) => TService,
422
+ additionalContext: this.#definition.additionalContext,
423
+ usedServices: this.#definition.usedServices,
424
+ providedServices: this.#definition.providedServices,
425
+ });
426
+ } else {
427
+ // Named service
428
+ const [serviceName, fnOrService] = args;
429
+
430
+ // Create a callback that provides the full context including defineService
431
+ const createService = (
432
+ config: TConfig,
433
+ options: FragnoPublicConfig,
434
+ deps: TDeps & TUsedServices,
435
+ ): TService => {
436
+ if (typeof fnOrService === "function") {
437
+ // It's a factory function - call it with context
438
+ const fn = fnOrService as (context: {
439
+ config: TConfig;
440
+ fragnoConfig: FragnoPublicConfig;
441
+ deps: TDeps & TUsedServices;
442
+ defineService: <T>(services: T) => T;
443
+ }) => TService;
444
+ return fn({
445
+ config,
446
+ fragnoConfig: options,
447
+ deps,
448
+ defineService,
449
+ });
450
+ } else {
451
+ // It's a direct service object
452
+ return fnOrService;
453
+ }
454
+ };
455
+
456
+ // We need to handle both function and object forms of providedServices
457
+ const existingProvidedServices = this.#definition.providedServices;
458
+ const newProvidedServices:
459
+ | {
460
+ [K in keyof (TProvidedServices & {
461
+ [K in TServiceName]: TService;
462
+ })]: (TProvidedServices & {
463
+ [K in TServiceName]: TService;
464
+ })[K];
465
+ }
466
+ | ((
467
+ config: TConfig,
468
+ options: FragnoPublicConfig,
469
+ deps: TDeps & TUsedServices,
470
+ ) => TProvidedServices & { [K in TServiceName]: TService }) =
471
+ typeof existingProvidedServices === "function"
472
+ ? // If existing is a function, create a new function that calls both
473
+ (config: TConfig, options: FragnoPublicConfig, deps: TDeps & TUsedServices) => {
474
+ const existing = existingProvidedServices(config, options, deps);
475
+ const newService = createService(config, options, deps);
476
+ return {
477
+ ...existing,
478
+ [serviceName]: newService,
479
+ } as TProvidedServices & { [K in TServiceName]: TService };
480
+ }
481
+ : // If existing is an object or undefined, spread it
482
+ ({
483
+ ...existingProvidedServices,
484
+ [serviceName]: createService,
485
+ } as {
486
+ [K in keyof (TProvidedServices & {
487
+ [K in TServiceName]: TService;
488
+ })]: (TProvidedServices & {
489
+ [K in TServiceName]: TService;
490
+ })[K];
491
+ });
492
+
493
+ return new FragmentBuilder<
494
+ TConfig,
495
+ TDeps,
496
+ TServices,
497
+ TAdditionalContext,
498
+ TUsedServices,
499
+ TProvidedServices & { [K in TServiceName]: TService },
500
+ TThisContext
501
+ >({
502
+ name: this.#definition.name,
503
+ dependencies: this.#definition.dependencies,
504
+ services: this.#definition.services,
505
+ additionalContext: this.#definition.additionalContext,
506
+ usedServices: this.#definition.usedServices,
507
+ providedServices: newProvidedServices,
508
+ });
509
+ }
510
+ }
75
511
  }
76
- export function defineFragment<TConfig = {}>(name: string): FragmentBuilder<TConfig, {}, {}, {}> {
512
+ export function defineFragment<TConfig = {}>(
513
+ name: string,
514
+ ): FragmentBuilder<TConfig, {}, {}, {}, {}, {}, RequestThisContext> {
77
515
  return new FragmentBuilder({
78
516
  name,
79
517
  });