@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,6 +1,6 @@
1
1
  import { test, expect, describe } from "vitest";
2
2
  import { defineFragment } from "./fragment-builder";
3
- import { createFragment } from "./fragment-instantiation";
3
+ import { createFragment, instantiateFragment } from "./fragment-instantiation";
4
4
  import { defineRoute, defineRoutes } from "./route";
5
5
  import { z } from "zod";
6
6
 
@@ -31,7 +31,7 @@ describe("callRoute", () => {
31
31
  body: { name: "World" },
32
32
  });
33
33
 
34
- expect(response.type).toBe("json");
34
+ // expect(response.type).toBe("json");
35
35
  if (response.type === "json") {
36
36
  expect(response.status).toBe(200);
37
37
  expect(response.data).toEqual({ message: "Hello, World!" });
@@ -391,11 +391,13 @@ describe("callRoute", () => {
391
391
  getUserName: () => string;
392
392
  };
393
393
 
394
- const fragment = defineFragment<typeof config>("test-fragment").withServices(() => {
395
- return {
396
- getUserName: () => "Test User",
397
- };
398
- });
394
+ const fragment = defineFragment<typeof config>("test-fragment").providesService(
395
+ ({ defineService }) => {
396
+ return defineService({
397
+ getUserName: () => "Test User",
398
+ });
399
+ },
400
+ );
399
401
 
400
402
  const routesFactory = defineRoutes<typeof config, {}, Services>().create(({ services }) => {
401
403
  return [
@@ -457,4 +459,244 @@ describe("callRoute", () => {
457
459
  expect(response.data).toEqual({ result: "database-result" });
458
460
  }
459
461
  });
462
+
463
+ test("this context is RequestThisContext for standard fragments", async () => {
464
+ const config = {};
465
+
466
+ const fragment = defineFragment<typeof config>("test-fragment");
467
+
468
+ const routesFactory = defineRoutes<typeof config>().create(() => {
469
+ return [
470
+ defineRoute({
471
+ method: "GET",
472
+ path: "/context-test",
473
+ outputSchema: z.object({ contextType: z.string() }),
474
+ handler: async function (_, { json }) {
475
+ // this should be RequestThisContext (empty object by default)
476
+ expect(this).toBeDefined();
477
+ expect(typeof this).toBe("object");
478
+ return json({ contextType: "standard" });
479
+ },
480
+ }),
481
+ ];
482
+ });
483
+
484
+ const instance = createFragment(fragment, config, [routesFactory], {});
485
+
486
+ const response = await instance.callRoute("GET", "/context-test");
487
+
488
+ expect(response.type).toBe("json");
489
+ if (response.type === "json") {
490
+ expect(response.status).toBe(200);
491
+ expect(response.data).toEqual({ contextType: "standard" });
492
+ }
493
+ });
494
+
495
+ test("route handlers receive correct this context at runtime", async () => {
496
+ const fragment = defineFragment("this-test");
497
+
498
+ const routesFactory = defineRoutes(fragment).create(({ defineRoute }) => {
499
+ return [
500
+ defineRoute({
501
+ method: "GET",
502
+ path: "/this-test",
503
+ outputSchema: z.object({ hasThis: z.boolean() }),
504
+ handler: async function (_, { json }) {
505
+ // Verify that 'this' is defined and is an object
506
+ const hasThis = this !== undefined && typeof this === "object";
507
+ return json({ hasThis });
508
+ },
509
+ }),
510
+ ];
511
+ });
512
+
513
+ const instance = createFragment(fragment, {}, [routesFactory], {});
514
+
515
+ const response = await instance.callRoute("GET", "/this-test");
516
+
517
+ expect(response.type).toBe("json");
518
+ if (response.type === "json") {
519
+ expect(response.status).toBe(200);
520
+ expect(response.data).toEqual({ hasThis: true });
521
+ }
522
+ });
523
+ });
524
+
525
+ describe("FragmentInstantiationBuilder", () => {
526
+ describe("instantiate", () => {
527
+ test("basic instantiation with defaults", () => {
528
+ const fragment = defineFragment<{ apiKey: string }>("test");
529
+
530
+ const instance = instantiateFragment(fragment).build();
531
+
532
+ expect(instance.config.name).toBe("test");
533
+ expect(instance.deps).toEqual({});
534
+ expect(instance.services).toEqual({});
535
+ });
536
+
537
+ test("with config", () => {
538
+ const fragment = defineFragment<{ apiKey: string }>("test");
539
+
540
+ const instance = instantiateFragment(fragment).withConfig({ apiKey: "test-key" }).build();
541
+
542
+ expect(instance.config.name).toBe("test");
543
+ });
544
+
545
+ test("with routes", async () => {
546
+ const fragment = defineFragment<{}>("test");
547
+
548
+ const route = defineRoute({
549
+ method: "GET",
550
+ path: "/hello",
551
+ outputSchema: z.object({ message: z.string() }),
552
+ handler: async (_ctx, { json }) => json({ message: "hello" }),
553
+ });
554
+
555
+ const instance = instantiateFragment(fragment).withConfig({}).withRoutes([route]).build();
556
+
557
+ const response = await instance.callRoute("GET", "/hello");
558
+
559
+ expect(response.type).toBe("json");
560
+ if (response.type === "json") {
561
+ expect(response.data).toEqual({ message: "hello" });
562
+ }
563
+ });
564
+
565
+ test("with options", () => {
566
+ const fragment = defineFragment<{}>("test");
567
+
568
+ const instance = instantiateFragment(fragment)
569
+ .withConfig({})
570
+ .withOptions({ mountRoute: "/custom" })
571
+ .build();
572
+
573
+ expect(instance.mountRoute).toBe("/custom");
574
+ });
575
+
576
+ test("with services (used services)", () => {
577
+ interface IEmailService {
578
+ sendEmail(to: string, subject: string): Promise<void>;
579
+ }
580
+
581
+ const emailImpl: IEmailService = {
582
+ sendEmail: async () => {},
583
+ };
584
+
585
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
586
+
587
+ const instance = instantiateFragment(fragment)
588
+ .withConfig({})
589
+ .withServices({ email: emailImpl })
590
+ .build();
591
+
592
+ expect(instance.services.email).toBeDefined();
593
+ expect(instance.services.email.sendEmail).toBeDefined();
594
+ });
595
+
596
+ test("all options together", async () => {
597
+ interface ILogger {
598
+ log(message: string): void;
599
+ }
600
+
601
+ const loggerImpl: ILogger = {
602
+ log: () => {},
603
+ };
604
+
605
+ const fragment = defineFragment<{ apiKey: string }>("test")
606
+ .withDependencies(({ config }) => ({
607
+ client: { key: config.apiKey },
608
+ }))
609
+ .usesService<"logger", ILogger>("logger");
610
+
611
+ const route = defineRoute({
612
+ method: "GET",
613
+ path: "/test",
614
+ outputSchema: z.object({ success: z.boolean() }),
615
+ handler: async (_ctx, { json }) => json({ success: true }),
616
+ });
617
+
618
+ const instance = instantiateFragment(fragment)
619
+ .withConfig({ apiKey: "my-key" })
620
+ .withRoutes([route])
621
+ .withOptions({ mountRoute: "/api" })
622
+ .withServices({ logger: loggerImpl })
623
+ .build();
624
+
625
+ expect(instance.config.name).toBe("test");
626
+ expect(instance.mountRoute).toBe("/api");
627
+ expect(instance.deps.client.key).toBe("my-key");
628
+ expect(instance.services.logger).toBeDefined();
629
+
630
+ const response = await instance.callRoute("GET", "/test");
631
+ expect(response.type).toBe("json");
632
+ if (response.type === "json") {
633
+ expect(response.data).toEqual({ success: true });
634
+ }
635
+ });
636
+
637
+ test("method chaining is fluent", () => {
638
+ const fragment = defineFragment<{ apiKey: string }>("test");
639
+
640
+ // Should be chainable
641
+ const builder = instantiateFragment(fragment)
642
+ .withConfig({ apiKey: "key" })
643
+ .withRoutes([])
644
+ .withOptions({});
645
+
646
+ expect(builder).toBeDefined();
647
+ expect(typeof builder.build).toBe("function");
648
+ });
649
+
650
+ test("config defaults to empty object", () => {
651
+ const fragment = defineFragment<{}>("test");
652
+
653
+ const instance = instantiateFragment(fragment).build();
654
+
655
+ expect(instance).toBeDefined();
656
+ });
657
+
658
+ test("routes defaults to empty array", () => {
659
+ const fragment = defineFragment<{}>("test");
660
+
661
+ const instance = instantiateFragment(fragment).withConfig({}).build();
662
+
663
+ expect(instance.config.routes).toEqual([]);
664
+ });
665
+
666
+ test("options defaults to empty object", () => {
667
+ const fragment = defineFragment<{}>("test");
668
+
669
+ const instance = instantiateFragment(fragment).withConfig({}).build();
670
+
671
+ // Default mountRoute is generated
672
+ expect(instance.mountRoute).toContain("/test");
673
+ });
674
+
675
+ test("services are optional if not required", () => {
676
+ interface ILogger {
677
+ log(message: string): void;
678
+ }
679
+
680
+ const fragment = defineFragment<{}>("test").usesService<"logger", ILogger>("logger", {
681
+ optional: true,
682
+ });
683
+
684
+ // Should not throw even without providing the service
685
+ const instance = instantiateFragment(fragment).withConfig({}).build();
686
+
687
+ expect(instance).toBeDefined();
688
+ });
689
+
690
+ test("throws when required service not provided", () => {
691
+ interface IEmailService {
692
+ sendEmail(to: string): Promise<void>;
693
+ }
694
+
695
+ const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
696
+
697
+ expect(() => {
698
+ instantiateFragment(fragment).withConfig({}).build();
699
+ }).toThrow("Fragment 'test' requires service 'email' but it was not provided");
700
+ });
701
+ });
460
702
  });
@@ -1,5 +1,5 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
- import { type FragnoRouteConfig, type HTTPMethod } from "./api";
2
+ import { type FragnoRouteConfig, type HTTPMethod, type RequestThisContext } from "./api";
3
3
  import { FragnoApiError } from "./error";
4
4
  import { getMountRoute } from "./internal/route";
5
5
  import { addRoute, createRouter, findRoute } from "rou3";
@@ -17,7 +17,7 @@ import {
17
17
  RequestMiddlewareOutputContext,
18
18
  type FragnoMiddlewareCallback,
19
19
  } from "./request-middleware";
20
- import type { FragmentDefinition } from "./fragment-builder";
20
+ import type { FragmentDefinition, RouteHandler } from "./fragment-builder";
21
21
  import { MutableRequestState } from "./mutable-request-state";
22
22
  import type { RouteHandlerInputOptions } from "./route-handler-input-options";
23
23
  import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
@@ -147,29 +147,94 @@ export function createFragment<
147
147
  const TServices extends Record<string, unknown>,
148
148
  const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
149
149
  const TAdditionalContext extends Record<string, unknown>,
150
+ const TRequiredInterfaces extends Record<string, unknown>,
151
+ const TProvidedInterfaces extends Record<string, unknown>,
150
152
  const TOptions extends FragnoPublicConfig,
153
+ const TThisContext extends RequestThisContext = RequestThisContext,
151
154
  >(
152
155
  fragmentBuilder: {
153
- definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;
156
+ definition: FragmentDefinition<
157
+ TConfig,
158
+ TDeps,
159
+ TServices,
160
+ TAdditionalContext,
161
+ TRequiredInterfaces,
162
+ TProvidedInterfaces,
163
+ TThisContext
164
+ >;
154
165
  $requiredOptions: TOptions;
155
166
  },
156
167
  config: TConfig,
157
168
  routesOrFactories: TRoutesOrFactories,
158
169
  options: TOptions,
170
+ interfaceImplementations?: TRequiredInterfaces,
159
171
  ): FragnoInstantiatedFragment<
160
172
  FlattenRouteFactories<TRoutesOrFactories>,
161
- TDeps,
162
- TServices,
173
+ TDeps & TRequiredInterfaces,
174
+ TServices & TProvidedInterfaces & TRequiredInterfaces,
163
175
  TAdditionalContext
164
176
  > {
165
177
  type TRoutes = FlattenRouteFactories<TRoutesOrFactories>;
166
178
 
167
179
  const definition = fragmentBuilder.definition;
168
180
 
181
+ // Validate required services are satisfied
182
+ if (definition.usedServices) {
183
+ for (const [serviceName, serviceMeta] of Object.entries(definition.usedServices)) {
184
+ const implementation = interfaceImplementations?.[serviceName];
185
+ if (serviceMeta.required && !implementation) {
186
+ throw new Error(
187
+ `Fragment '${definition.name}' requires service '${serviceMeta.name}' but it was not provided`,
188
+ );
189
+ }
190
+ }
191
+ }
192
+
169
193
  const dependencies = definition.dependencies?.(config, options) ?? ({} as TDeps);
170
- const services = definition.services?.(config, options, dependencies) ?? ({} as TServices);
171
194
 
172
- const context = { config, deps: dependencies, services };
195
+ // Merge interface implementations into dependencies
196
+ const depsWithInterfaces = {
197
+ ...dependencies,
198
+ ...interfaceImplementations,
199
+ } as TDeps & TRequiredInterfaces;
200
+
201
+ const servicesFromWithServices =
202
+ definition.services?.(config, options, depsWithInterfaces) ?? ({} as TServices);
203
+
204
+ // Handle providedServices - can be:
205
+ // 1. A function that returns all services
206
+ // 2. An object where each value is a factory function
207
+ // 3. undefined
208
+ let providedServicesResolved: TProvidedInterfaces | undefined;
209
+
210
+ if (typeof definition.providedServices === "function") {
211
+ // Case 1: It's a function, call it to get the services
212
+ providedServicesResolved = definition.providedServices(config, options, depsWithInterfaces);
213
+ } else if (definition.providedServices && typeof definition.providedServices === "object") {
214
+ // Case 2: It's an object where each value might be a factory function
215
+ providedServicesResolved = {} as TProvidedInterfaces;
216
+ for (const [serviceName, serviceOrFactory] of Object.entries(definition.providedServices)) {
217
+ if (typeof serviceOrFactory === "function") {
218
+ // Call the factory function
219
+ (providedServicesResolved as Record<string, unknown>)[serviceName] = serviceOrFactory(
220
+ config,
221
+ options,
222
+ depsWithInterfaces,
223
+ );
224
+ } else {
225
+ // It's already a resolved service
226
+ (providedServicesResolved as Record<string, unknown>)[serviceName] = serviceOrFactory;
227
+ }
228
+ }
229
+ }
230
+
231
+ const services = {
232
+ ...servicesFromWithServices,
233
+ ...providedServicesResolved,
234
+ ...interfaceImplementations,
235
+ } as TServices & TProvidedInterfaces & TRequiredInterfaces;
236
+
237
+ const context = { config, deps: depsWithInterfaces, services };
173
238
  const routes = resolveRouteFactories(context, routesOrFactories);
174
239
 
175
240
  const mountRoute = getMountRoute({
@@ -185,22 +250,30 @@ export function createFragment<
185
250
  StandardSchemaV1 | undefined,
186
251
  StandardSchemaV1 | undefined,
187
252
  string,
188
- string
253
+ string,
254
+ RequestThisContext
189
255
  >
190
256
  >();
191
257
 
192
258
  let middlewareHandler:
193
- | FragnoMiddlewareCallback<FlattenRouteFactories<TRoutesOrFactories>, TDeps, TServices>
259
+ | FragnoMiddlewareCallback<
260
+ FlattenRouteFactories<TRoutesOrFactories>,
261
+ TDeps & TRequiredInterfaces,
262
+ TServices & TProvidedInterfaces & TRequiredInterfaces
263
+ >
194
264
  | undefined;
195
265
 
266
+ // Store the handler wrapper if provided (e.g., for database support)
267
+ const handlerWrapper = definition.createHandlerWrapper?.(options);
268
+
196
269
  for (const routeConfig of routes) {
197
270
  addRoute(router, routeConfig.method.toUpperCase(), routeConfig.path, routeConfig);
198
271
  }
199
272
 
200
273
  const fragment: FragnoInstantiatedFragment<
201
274
  FlattenRouteFactories<TRoutesOrFactories>,
202
- TDeps,
203
- TServices,
275
+ TDeps & TRequiredInterfaces,
276
+ TServices & TProvidedInterfaces & TRequiredInterfaces,
204
277
  TAdditionalContext & TOptions
205
278
  > = {
206
279
  [instantiatedFragmentFakeSymbol]: instantiatedFragmentFakeSymbol,
@@ -210,7 +283,7 @@ export function createFragment<
210
283
  routes,
211
284
  },
212
285
  services,
213
- deps: dependencies,
286
+ deps: depsWithInterfaces,
214
287
  additionalContext: {
215
288
  ...definition.additionalContext,
216
289
  ...options,
@@ -297,9 +370,25 @@ export function createFragment<
297
370
  // Construct RequestOutputContext
298
371
  const outputContext = new RequestOutputContext(route.outputSchema);
299
372
 
300
- // Call the route handler
373
+ // Call the route handler (wrap with handlerWrapper if provided)
301
374
  try {
302
- const response = await route.handler(inputContext, outputContext);
375
+ let response: Response;
376
+ const thisContext: RequestThisContext = {};
377
+
378
+ if (handlerWrapper) {
379
+ // Wrapper handles binding the this context internally for database fragments
380
+ // Safe: wrapper knows how to handle the specific this type (DatabaseRequestThisContext)
381
+ const wrappedHandler = handlerWrapper(route.handler as unknown as RouteHandler);
382
+ response = await wrappedHandler.call(thisContext, inputContext, outputContext);
383
+ } else {
384
+ // For standard fragments, bind to an empty RequestThisContext
385
+ // Safe: we know route.handler expects RequestThisContext for standard fragments
386
+ response = await (route.handler as RouteHandler).call(
387
+ thisContext,
388
+ inputContext,
389
+ outputContext,
390
+ );
391
+ }
303
392
  return response;
304
393
  } catch (error) {
305
394
  console.error("Error in callRoute handler", error);
@@ -440,7 +529,10 @@ export function createFragment<
440
529
  state: requestState,
441
530
  });
442
531
 
443
- const middlewareOutputContext = new RequestMiddlewareOutputContext(dependencies, services);
532
+ const middlewareOutputContext = new RequestMiddlewareOutputContext(
533
+ depsWithInterfaces,
534
+ services,
535
+ );
444
536
 
445
537
  try {
446
538
  const middlewareResult = await middlewareHandler(
@@ -478,7 +570,16 @@ export function createFragment<
478
570
  });
479
571
 
480
572
  try {
481
- const result = await handler(inputContext, outputContext);
573
+ // Apply handler wrapper if provided (e.g., for database support)
574
+ // Safe cast: handler wrapper preserves handler signature
575
+ const actualHandler = handlerWrapper
576
+ ? (handlerWrapper(handler as RouteHandler) as typeof handler)
577
+ : handler;
578
+
579
+ // Create base this context (empty object for standard fragments)
580
+ // Database fragments will provide their own context via handler wrapper
581
+ const thisContext = {} as RequestThisContext;
582
+ const result = await actualHandler.call(thisContext, inputContext, outputContext);
482
583
  return result;
483
584
  } catch (error) {
484
585
  console.error("Error in handler", error);
@@ -497,3 +598,169 @@ export function createFragment<
497
598
 
498
599
  return fragment;
499
600
  }
601
+
602
+ /**
603
+ * Builder class for fluent fragment instantiation API
604
+ */
605
+ export class FragmentInstantiationBuilder<
606
+ TConfig,
607
+ TDeps,
608
+ TServices extends Record<string, unknown>,
609
+ TRoutesOrFactories extends readonly AnyRouteOrFactory[],
610
+ TAdditionalContext extends Record<string, unknown>,
611
+ TRequiredInterfaces extends Record<string, unknown>,
612
+ TProvidedInterfaces extends Record<string, unknown>,
613
+ TOptions extends FragnoPublicConfig,
614
+ TThisContext extends RequestThisContext,
615
+ > {
616
+ #fragmentBuilder: {
617
+ definition: FragmentDefinition<
618
+ TConfig,
619
+ TDeps,
620
+ TServices,
621
+ TAdditionalContext,
622
+ TRequiredInterfaces,
623
+ TProvidedInterfaces,
624
+ TThisContext
625
+ >;
626
+ $requiredOptions: TOptions;
627
+ };
628
+ #config?: TConfig;
629
+ #routes?: TRoutesOrFactories;
630
+ #options?: TOptions;
631
+ #services?: TRequiredInterfaces;
632
+
633
+ constructor(fragmentBuilder: {
634
+ definition: FragmentDefinition<
635
+ TConfig,
636
+ TDeps,
637
+ TServices,
638
+ TAdditionalContext,
639
+ TRequiredInterfaces,
640
+ TProvidedInterfaces,
641
+ TThisContext
642
+ >;
643
+ $requiredOptions: TOptions;
644
+ }) {
645
+ this.#fragmentBuilder = fragmentBuilder;
646
+ }
647
+
648
+ /**
649
+ * Set the configuration for the fragment
650
+ */
651
+ withConfig(config: TConfig): this {
652
+ this.#config = config;
653
+ return this;
654
+ }
655
+
656
+ /**
657
+ * Set the routes for the fragment
658
+ */
659
+ withRoutes<const TNewRoutes extends readonly AnyRouteOrFactory[]>(
660
+ routes: TNewRoutes,
661
+ ): FragmentInstantiationBuilder<
662
+ TConfig,
663
+ TDeps,
664
+ TServices,
665
+ TNewRoutes,
666
+ TAdditionalContext,
667
+ TRequiredInterfaces,
668
+ TProvidedInterfaces,
669
+ TOptions,
670
+ TThisContext
671
+ > {
672
+ this.#routes = routes as unknown as TRoutesOrFactories;
673
+ // Safe cast: We're changing the route type parameter
674
+ return this as unknown as FragmentInstantiationBuilder<
675
+ TConfig,
676
+ TDeps,
677
+ TServices,
678
+ TNewRoutes,
679
+ TAdditionalContext,
680
+ TRequiredInterfaces,
681
+ TProvidedInterfaces,
682
+ TOptions,
683
+ TThisContext
684
+ >;
685
+ }
686
+
687
+ /**
688
+ * Set the options for the fragment (e.g., mountRoute, databaseAdapter)
689
+ */
690
+ withOptions(options: TOptions): this {
691
+ this.#options = options;
692
+ return this;
693
+ }
694
+
695
+ /**
696
+ * Provide implementations for services that this fragment uses
697
+ */
698
+ withServices(services: TRequiredInterfaces): this {
699
+ this.#services = services;
700
+ return this;
701
+ }
702
+
703
+ /**
704
+ * Build and return the instantiated fragment
705
+ */
706
+ build(): FragnoInstantiatedFragment<
707
+ FlattenRouteFactories<TRoutesOrFactories>,
708
+ TDeps & TRequiredInterfaces,
709
+ TServices & TProvidedInterfaces & TRequiredInterfaces,
710
+ TAdditionalContext
711
+ > {
712
+ return createFragment(
713
+ this.#fragmentBuilder,
714
+ this.#config ?? ({} as TConfig),
715
+ this.#routes ?? ([] as const as unknown as TRoutesOrFactories),
716
+ this.#options ?? ({} as TOptions),
717
+ this.#services,
718
+ );
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Create a fluent builder for instantiating a fragment
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * const fragment = instantiateFragment(myFragmentBuilder)
728
+ * .withConfig({ apiKey: "key" })
729
+ * .withRoutes([route1, route2])
730
+ * .withOptions({ mountRoute: "/api" })
731
+ * .build();
732
+ * ```
733
+ */
734
+ export function instantiateFragment<
735
+ TConfig,
736
+ TDeps,
737
+ TServices extends Record<string, unknown>,
738
+ TAdditionalContext extends Record<string, unknown>,
739
+ TRequiredInterfaces extends Record<string, unknown>,
740
+ TProvidedInterfaces extends Record<string, unknown>,
741
+ TOptions extends FragnoPublicConfig,
742
+ TThisContext extends RequestThisContext = RequestThisContext,
743
+ >(fragmentBuilder: {
744
+ definition: FragmentDefinition<
745
+ TConfig,
746
+ TDeps,
747
+ TServices,
748
+ TAdditionalContext,
749
+ TRequiredInterfaces,
750
+ TProvidedInterfaces,
751
+ TThisContext
752
+ >;
753
+ $requiredOptions: TOptions;
754
+ }): FragmentInstantiationBuilder<
755
+ TConfig,
756
+ TDeps,
757
+ TServices,
758
+ readonly [],
759
+ TAdditionalContext,
760
+ TRequiredInterfaces,
761
+ TProvidedInterfaces,
762
+ TOptions,
763
+ TThisContext
764
+ > {
765
+ return new FragmentInstantiationBuilder(fragmentBuilder);
766
+ }