@fragno-dev/core 0.1.11 → 0.2.0

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 (69) hide show
  1. package/.turbo/turbo-build.log +50 -42
  2. package/CHANGELOG.md +51 -0
  3. package/dist/api/api.d.ts +19 -1
  4. package/dist/api/api.d.ts.map +1 -1
  5. package/dist/api/api.js.map +1 -1
  6. package/dist/api/fragment-definition-builder.d.ts +17 -7
  7. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  8. package/dist/api/fragment-definition-builder.js +3 -2
  9. package/dist/api/fragment-definition-builder.js.map +1 -1
  10. package/dist/api/fragment-instantiator.d.ts +23 -16
  11. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  12. package/dist/api/fragment-instantiator.js +163 -19
  13. package/dist/api/fragment-instantiator.js.map +1 -1
  14. package/dist/api/request-input-context.d.ts +57 -1
  15. package/dist/api/request-input-context.d.ts.map +1 -1
  16. package/dist/api/request-input-context.js +67 -0
  17. package/dist/api/request-input-context.js.map +1 -1
  18. package/dist/api/request-middleware.d.ts +1 -1
  19. package/dist/api/request-middleware.d.ts.map +1 -1
  20. package/dist/api/request-middleware.js.map +1 -1
  21. package/dist/api/route.d.ts +7 -7
  22. package/dist/api/route.d.ts.map +1 -1
  23. package/dist/api/route.js.map +1 -1
  24. package/dist/client/client.d.ts +4 -3
  25. package/dist/client/client.d.ts.map +1 -1
  26. package/dist/client/client.js +103 -7
  27. package/dist/client/client.js.map +1 -1
  28. package/dist/client/vue.d.ts +7 -3
  29. package/dist/client/vue.d.ts.map +1 -1
  30. package/dist/client/vue.js +16 -1
  31. package/dist/client/vue.js.map +1 -1
  32. package/dist/internal/trace-context.d.ts +23 -0
  33. package/dist/internal/trace-context.d.ts.map +1 -0
  34. package/dist/internal/trace-context.js +14 -0
  35. package/dist/internal/trace-context.js.map +1 -0
  36. package/dist/mod-client.d.ts +3 -17
  37. package/dist/mod-client.d.ts.map +1 -1
  38. package/dist/mod-client.js +20 -10
  39. package/dist/mod-client.js.map +1 -1
  40. package/dist/mod.d.ts +3 -2
  41. package/dist/mod.js +2 -1
  42. package/dist/runtime.d.ts +15 -0
  43. package/dist/runtime.d.ts.map +1 -0
  44. package/dist/runtime.js +33 -0
  45. package/dist/runtime.js.map +1 -0
  46. package/dist/test/test.d.ts +2 -2
  47. package/dist/test/test.d.ts.map +1 -1
  48. package/dist/test/test.js.map +1 -1
  49. package/package.json +23 -17
  50. package/src/api/api.ts +22 -0
  51. package/src/api/fragment-definition-builder.ts +36 -17
  52. package/src/api/fragment-instantiator.test.ts +286 -0
  53. package/src/api/fragment-instantiator.ts +338 -31
  54. package/src/api/internal/path-runtime.test.ts +7 -0
  55. package/src/api/request-input-context.test.ts +152 -0
  56. package/src/api/request-input-context.ts +85 -0
  57. package/src/api/request-middleware.test.ts +47 -1
  58. package/src/api/request-middleware.ts +1 -1
  59. package/src/api/route.ts +7 -2
  60. package/src/client/client.test.ts +195 -0
  61. package/src/client/client.ts +185 -10
  62. package/src/client/vue.test.ts +253 -3
  63. package/src/client/vue.ts +44 -1
  64. package/src/internal/trace-context.ts +35 -0
  65. package/src/mod-client.ts +51 -7
  66. package/src/mod.ts +6 -1
  67. package/src/runtime.ts +48 -0
  68. package/src/test/test.ts +13 -4
  69. package/tsdown.config.ts +1 -0
@@ -0,0 +1,15 @@
1
+ //#region src/runtime.d.ts
2
+ type FragnoRuntime = {
3
+ time: {
4
+ now: () => Date;
5
+ };
6
+ random: {
7
+ float: () => number;
8
+ uuid: () => string;
9
+ cuid: () => string;
10
+ };
11
+ };
12
+ declare const defaultFragnoRuntime: FragnoRuntime;
13
+ //#endregion
14
+ export { FragnoRuntime, defaultFragnoRuntime };
15
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","names":[],"sources":["../src/runtime.ts"],"sourcesContent":[],"mappings":";KAEY,aAAA;EAAA,IAAA,EAAA;IAoCC,GAAA,EAAA,GAAA,GAlCE,IAkCF;;;;;;;;cAAA,sBAAsB"}
@@ -0,0 +1,33 @@
1
+ import { createId } from "@paralleldrive/cuid2";
2
+
3
+ //#region src/runtime.ts
4
+ const fallbackUuid = () => {
5
+ const bytes = new Uint8Array(16);
6
+ for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
7
+ bytes[6] = bytes[6] & 15 | 64;
8
+ bytes[8] = bytes[8] & 63 | 128;
9
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
10
+ return [
11
+ hex.slice(0, 4).join(""),
12
+ hex.slice(4, 6).join(""),
13
+ hex.slice(6, 8).join(""),
14
+ hex.slice(8, 10).join(""),
15
+ hex.slice(10, 16).join("")
16
+ ].join("-");
17
+ };
18
+ const defaultUuid = () => {
19
+ const cryptoApi = globalThis.crypto;
20
+ return cryptoApi?.randomUUID ? cryptoApi.randomUUID() : fallbackUuid();
21
+ };
22
+ const defaultFragnoRuntime = {
23
+ time: { now: () => /* @__PURE__ */ new Date() },
24
+ random: {
25
+ float: () => Math.random(),
26
+ uuid: () => defaultUuid(),
27
+ cuid: () => createId()
28
+ }
29
+ };
30
+
31
+ //#endregion
32
+ export { defaultFragnoRuntime };
33
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","names":["defaultFragnoRuntime: FragnoRuntime"],"sources":["../src/runtime.ts"],"sourcesContent":["import { createId } from \"@paralleldrive/cuid2\";\n\nexport type FragnoRuntime = {\n time: {\n now: () => Date;\n };\n random: {\n float: () => number;\n uuid: () => string;\n cuid: () => string;\n };\n};\n\nconst fallbackUuid = () => {\n const bytes = new Uint8Array(16);\n for (let i = 0; i < bytes.length; i += 1) {\n bytes[i] = Math.floor(Math.random() * 256);\n }\n\n // RFC 4122 variant + version 4\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\"));\n return [\n hex.slice(0, 4).join(\"\"),\n hex.slice(4, 6).join(\"\"),\n hex.slice(6, 8).join(\"\"),\n hex.slice(8, 10).join(\"\"),\n hex.slice(10, 16).join(\"\"),\n ].join(\"-\");\n};\n\nconst defaultUuid = () => {\n const cryptoApi = globalThis.crypto;\n return cryptoApi?.randomUUID ? cryptoApi.randomUUID() : fallbackUuid();\n};\n\nexport const defaultFragnoRuntime: FragnoRuntime = {\n time: {\n now: () => new Date(),\n },\n random: {\n float: () => Math.random(),\n uuid: () => defaultUuid(),\n cuid: () => createId(),\n },\n};\n"],"mappings":";;;AAaA,MAAM,qBAAqB;CACzB,MAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,EACrC,OAAM,KAAK,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI;AAI5C,OAAM,KAAM,MAAM,KAAK,KAAQ;AAC/B,OAAM,KAAM,MAAM,KAAK,KAAQ;CAE/B,MAAM,MAAM,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC;AAC3E,QAAO;EACL,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG;EACxB,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG;EACxB,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG;EACxB,IAAI,MAAM,GAAG,GAAG,CAAC,KAAK,GAAG;EACzB,IAAI,MAAM,IAAI,GAAG,CAAC,KAAK,GAAG;EAC3B,CAAC,KAAK,IAAI;;AAGb,MAAM,oBAAoB;CACxB,MAAM,YAAY,WAAW;AAC7B,QAAO,WAAW,aAAa,UAAU,YAAY,GAAG,cAAc;;AAGxE,MAAaA,uBAAsC;CACjD,MAAM,EACJ,2BAAW,IAAI,MAAM,EACtB;CACD,QAAQ;EACN,aAAa,KAAK,QAAQ;EAC1B,YAAY,aAAa;EACzB,YAAY,UAAU;EACvB;CACF"}
@@ -4,7 +4,7 @@ import { RequestThisContext } from "../api/api.js";
4
4
  import { BoundServices } from "../api/bind-services.js";
5
5
  import { AnyRouteOrFactory, FlattenRouteFactories } from "../api/route.js";
6
6
  import { FragnoResponse } from "../api/fragno-response.js";
7
- import { FragnoInstantiatedFragment } from "../api/fragment-instantiator.js";
7
+ import { AnyFragnoInstantiatedFragment, FragnoInstantiatedFragment, RoutesWithInternal } from "../api/fragment-instantiator.js";
8
8
  import { FragmentDefinition, FragmentDefinitionBuilder } from "../api/fragment-definition-builder.js";
9
9
 
10
10
  //#region src/test/test.d.ts
@@ -87,7 +87,7 @@ interface CreateFragmentForTestOptions<TConfig, TOptions extends FragnoPublicCon
87
87
  * }
88
88
  * ```
89
89
  */
90
- declare function createFragmentForTest<TConfig, TOptions extends FragnoPublicConfig, TDeps, TBaseServices extends Record<string, unknown>, TServices extends Record<string, unknown>, TServiceDependencies, TPrivateServices extends Record<string, unknown>, TServiceThisContext extends RequestThisContext, THandlerThisContext extends RequestThisContext, TRequestStorage, const TRoutesOrFactories extends readonly AnyRouteOrFactory[]>(definition: FragmentDefinition<TConfig, TOptions, TDeps, TBaseServices, TServices, TServiceDependencies, TPrivateServices, TServiceThisContext, THandlerThisContext, TRequestStorage>, routesOrFactories: TRoutesOrFactories, options: CreateFragmentForTestOptions<TConfig, TOptions, TServiceDependencies>): FragnoInstantiatedFragment<FlattenRouteFactories<TRoutesOrFactories>, TDeps, BoundServices<TBaseServices & TServices>, TServiceThisContext, THandlerThisContext, TRequestStorage>;
90
+ declare function createFragmentForTest<TConfig, TOptions extends FragnoPublicConfig, TDeps, TBaseServices extends Record<string, unknown>, TServices extends Record<string, unknown>, TServiceDependencies, TPrivateServices extends Record<string, unknown>, TServiceThisContext extends RequestThisContext, THandlerThisContext extends RequestThisContext, TRequestStorage, const TRoutesOrFactories extends readonly AnyRouteOrFactory[], TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment> = {}>(definition: FragmentDefinition<TConfig, TOptions, TDeps, TBaseServices, TServices, TServiceDependencies, TPrivateServices, TServiceThisContext, THandlerThisContext, TRequestStorage, TLinkedFragments>, routesOrFactories: TRoutesOrFactories, options: CreateFragmentForTestOptions<TConfig, TOptions, TServiceDependencies>): FragnoInstantiatedFragment<RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TLinkedFragments>, TDeps, BoundServices<TBaseServices & TServices>, TServiceThisContext, THandlerThisContext, TRequestStorage, TOptions, TLinkedFragments>;
91
91
  //#endregion
92
92
  export { CreateFragmentForTestOptions, type FragnoResponse, type RouteHandlerInputOptions, TestBaseServices, createFragmentForTest, withTestUtils };
93
93
  //# sourceMappingURL=test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","names":[],"sources":["../../src/test/test.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;KAeY;;AAAZ;AAsBA;EAGqB,IAAA,EArBb,KAqBa;CAMW;;;;;;;;;;;;;;;;AAoB5B,iBA7BY,aAAA,CAAA,CA6BZ,EAAA,CAAA,OAAA,EAAA,iBA1BiB,kBA0BjB,EAAA,KAAA,EAAA,aAAA,EAAA,SAAA,EAAA,YAAA,EAAA,gBAAA,EAAA,4BApB4B,kBAoB5B,EAAA,4BAnB4B,kBAmB5B,EAAA,eAAA,CAAA,CAAA,OAAA,EAhBS,yBAgBT,CAfE,OAeF,EAdE,QAcF,EAbE,KAaF,EAZE,aAYF,EAXE,SAWF,EAVE,YAUF,EATE,gBASF,EARE,mBAQF,EAPE,mBAOF,EANE,eAMF,CAAA,EAAA,GAJC,yBAID,CAHA,OAGA,EAFA,QAEA,EADA,KACA,EAAA,aAAA,GAAgB,gBAAhB,CAAiC,KAAjC,CAAA,EACA,SADA,EAEA,YAFA,EAGA,gBAHA,EAIA,mBAJA,EAKA,mBALA,EAMA,eANA,CAAA;;;;AAEA,UA+Da,4BA/Db,CAAA,OAAA,EAAA,iBAiEe,kBAjEf,EAAA,oBAAA,CAAA,CAAA;EACA,MAAA,EAmEM,OAnEN;EACA,OAAA,CAAA,EAmEQ,OAnER,CAmEgB,QAnEhB,CAAA;EACA,sBAAA,CAAA,EAmEuB,oBAnEvB;;;;AA4DJ;;;;;;;AA2DA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,gDAEG,iDAEK,2CACJ,wEAEO,qDACG,gDACA,+EAEc,iCAE9B,mBACV,SACA,UACA,OACA,eACA,WACA,sBACA,kBACA,qBACA,qBACA,qCAEiB,6BACV,6BAA6B,SAAS,UAAU,wBACxD,2BACD,sBAAsB,qBACtB,OACA,cAAc,gBAAgB,YAC9B,qBACA,qBACA"}
1
+ {"version":3,"file":"test.d.ts","names":[],"sources":["../../src/test/test.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;KAoBY;;AAAZ;AAsBA;EAGqB,IAAA,EArBb,KAqBa;CAMW;;;;;;;;;;;;;;;;AAoB5B,iBA7BY,aAAA,CAAA,CA6BZ,EAAA,CAAA,OAAA,EAAA,iBA1BiB,kBA0BjB,EAAA,KAAA,EAAA,aAAA,EAAA,SAAA,EAAA,YAAA,EAAA,gBAAA,EAAA,4BApB4B,kBAoB5B,EAAA,4BAnB4B,kBAmB5B,EAAA,eAAA,CAAA,CAAA,OAAA,EAhBS,yBAgBT,CAfE,OAeF,EAdE,QAcF,EAbE,KAaF,EAZE,aAYF,EAXE,SAWF,EAVE,YAUF,EATE,gBASF,EARE,mBAQF,EAPE,mBAOF,EANE,eAMF,CAAA,EAAA,GAJC,yBAID,CAHA,OAGA,EAFA,QAEA,EADA,KACA,EAAA,aAAA,GAAgB,gBAAhB,CAAiC,KAAjC,CAAA,EACA,SADA,EAEA,YAFA,EAGA,gBAHA,EAIA,mBAJA,EAKA,mBALA,EAMA,eANA,CAAA;;;;AAEA,UA+Da,4BA/Db,CAAA,OAAA,EAAA,iBAiEe,kBAjEf,EAAA,oBAAA,CAAA,CAAA;EACA,MAAA,EAmEM,OAnEN;EACA,OAAA,CAAA,EAmEQ,OAnER,CAmEgB,QAnEhB,CAAA;EACA,sBAAA,CAAA,EAmEuB,oBAnEvB;;;;AA4DJ;;;;;;;AA2DA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BG,iBA7Ba,qBA6Bb,CAAA,OAAA,EAAA,iBA3BgB,kBA2BhB,EAAA,KAAA,EAAA,sBAzBqB,MAyBrB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,kBAxBiB,MAwBjB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,oBAAA,EAAA,yBAtBwB,MAsBxB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,4BArB2B,kBAqB3B,EAAA,4BApB2B,kBAoB3B,EAAA,eAAA,EAAA,iCAAA,SAlByC,iBAkBzC,EAAA,EAAA,yBAjBwB,MAiBxB,CAAA,MAAA,EAjBuC,6BAiBvC,CAAA,GAAA,CAAA,CAAA,CAAA,CAAA,UAAA,EAfW,kBAeX,CAdC,OAcD,EAbC,QAaD,EAZC,KAYD,EAXC,aAWD,EAVC,SAUD,EATC,oBASD,EARC,gBAQD,EAPC,mBAOD,EANC,mBAMD,EALC,eAKD,EAJC,gBAID,CAAA,EAAA,iBAAA,EAFkB,kBAElB,EAAA,OAAA,EADQ,4BACR,CADqC,OACrC,EAD8C,QAC9C,EADwD,oBACxD,CAAA,CAAA,EAAA,0BAAA,CACD,kBADC,CACkB,qBADlB,CACwC,kBADxC,CAAA,EAC6D,gBAD7D,CAAA,EAED,KAFC,EAGD,aAHC,CAGa,aAHb,GAG6B,SAH7B,CAAA,EAID,mBAJC,EAKD,mBALC,EAMD,eANC,EAOD,QAPC,EAQD,gBARC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"test.js","names":["newBaseServices: ServiceConstructorFn<\n TConfig,\n TOptions,\n TDeps,\n TServiceDeps,\n TPrivateServices,\n TBaseServices & TestBaseServices<TDeps>,\n TServiceThisContext\n >","testUtils: TestBaseServices<TDeps>"],"sources":["../../src/test/test.ts"],"sourcesContent":["import type { RequestThisContext } from \"../api/api\";\nimport type { AnyRouteOrFactory, FlattenRouteFactories } from \"../api/route\";\nimport type { FragnoPublicConfig } from \"../api/shared-types\";\nimport {\n FragmentDefinitionBuilder,\n type FragmentDefinition,\n type ServiceConstructorFn,\n} from \"../api/fragment-definition-builder\";\nimport { instantiateFragment, type FragnoInstantiatedFragment } from \"../api/fragment-instantiator\";\nimport type { BoundServices } from \"../api/bind-services\";\n\n// Re-export for convenience\nexport type { RouteHandlerInputOptions } from \"../api/route-handler-input-options\";\nexport type { FragnoResponse } from \"../api/fragno-response\";\n\nexport type TestBaseServices<TDeps> = {\n /**\n * Access to fragment dependencies for testing purposes.\n */\n deps: TDeps;\n};\n\n/**\n * Extension function that adds test utilities to a fragment definition.\n * This adds a `test` service to base services with access to deps.\n *\n * @example\n * ```typescript\n * const definition = defineFragment(\"my-fragment\")\n * .withDependencies(({ config }) => ({ client: createClient(config) }))\n * .extend(withTestUtils())\n * .build();\n *\n * const fragment = createFragmentForTest(definition, [], { config: {...} });\n * expect(fragment.services.test.deps.client).toBeDefined();\n * ```\n */\nexport function withTestUtils() {\n return <\n TConfig,\n TOptions extends FragnoPublicConfig,\n TDeps,\n TBaseServices,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext extends RequestThisContext,\n THandlerThisContext extends RequestThisContext,\n TRequestStorage,\n >(\n builder: FragmentDefinitionBuilder<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n >,\n ): FragmentDefinitionBuilder<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices & TestBaseServices<TDeps>,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n > => {\n // Get the current base services factory\n const currentBaseDef = builder.build();\n const currentBaseServices = currentBaseDef.baseServices;\n\n // Create new base services factory that merges test utilities\n const newBaseServices: ServiceConstructorFn<\n TConfig,\n TOptions,\n TDeps,\n TServiceDeps,\n TPrivateServices,\n TBaseServices & TestBaseServices<TDeps>,\n TServiceThisContext\n > = (context) => {\n // Call existing base services if they exist\n const existingServices = currentBaseServices\n ? currentBaseServices(context)\n : ({} as TBaseServices);\n\n // Add test utilities\n const testUtils: TestBaseServices<TDeps> = {\n deps: context.deps,\n };\n\n return {\n ...existingServices,\n ...testUtils,\n } as TBaseServices & TestBaseServices<TDeps>;\n };\n\n // Create new builder with updated base services\n return new FragmentDefinitionBuilder<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices & TestBaseServices<TDeps>,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n >(builder.name, {\n dependencies: currentBaseDef.dependencies,\n baseServices: newBaseServices,\n namedServices: currentBaseDef.namedServices,\n privateServices: currentBaseDef.privateServices,\n serviceDependencies: currentBaseDef.serviceDependencies,\n createRequestStorage: currentBaseDef.createRequestStorage,\n createThisContext: currentBaseDef.createThisContext,\n });\n };\n}\n\n/**\n * Options for creating a test fragment with the new architecture\n */\nexport interface CreateFragmentForTestOptions<\n TConfig,\n TOptions extends FragnoPublicConfig,\n TServiceDependencies,\n> {\n config: TConfig;\n options?: Partial<TOptions>;\n serviceImplementations?: TServiceDependencies;\n}\n\n/**\n * Create a fragment instance for testing with optional service dependency overrides.\n * This uses the new fragment definition and instantiation architecture.\n *\n * **Important:** Use `.extend(withTestUtils())` before `.build()` to expose deps via `services.test.deps`.\n *\n * Returns an instantiated fragment with:\n * - `services` - Services (base + named) from the fragment definition\n * - `services.test.deps` - Dependencies (only if you used .extend(withTestUtils()))\n * - `callRoute()` - Type-safe method to call routes directly\n * - `handler()` - Request handler for integration\n * - `inContext()` - Run code within request context\n *\n * @param definition - The fragment definition from builder.build()\n * @param routesOrFactories - Route configurations or route factories\n * @param options - Configuration and optional overrides\n * @returns An instantiated fragment ready for testing\n *\n * @example\n * ```typescript\n * const definition = defineFragment<{ apiKey: string }>(\"test\")\n * .withDependencies(({ config }) => ({ client: createClient(config.apiKey) }))\n * .providesService(\"api\", ({ deps }) => ({ call: () => deps.client.call() }))\n * .extend(withTestUtils()) // <- Add test utilities\n * .build();\n *\n * const fragment = createFragmentForTest(\n * definition,\n * [route1, route2],\n * {\n * config: { apiKey: \"test-key\" },\n * options: { mountRoute: \"/api/test\" }\n * }\n * );\n *\n * // Access deps via test utilities\n * expect(fragment.services.test.deps.client).toBeDefined();\n * expect(fragment.services.api.call()).toBeDefined();\n *\n * // Call routes directly by method and path\n * const response = await fragment.callRoute(\"POST\", \"/login\", {\n * body: { username: \"test\", password: \"test123\" }\n * });\n *\n * if (response.type === 'json') {\n * expect(response.data).toEqual({...});\n * }\n * ```\n */\nexport function createFragmentForTest<\n TConfig,\n TOptions extends FragnoPublicConfig,\n TDeps,\n TBaseServices extends Record<string, unknown>,\n TServices extends Record<string, unknown>,\n TServiceDependencies,\n TPrivateServices extends Record<string, unknown>,\n TServiceThisContext extends RequestThisContext,\n THandlerThisContext extends RequestThisContext,\n TRequestStorage,\n const TRoutesOrFactories extends readonly AnyRouteOrFactory[],\n>(\n definition: FragmentDefinition<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices,\n TServices,\n TServiceDependencies,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n >,\n routesOrFactories: TRoutesOrFactories,\n options: CreateFragmentForTestOptions<TConfig, TOptions, TServiceDependencies>,\n): FragnoInstantiatedFragment<\n FlattenRouteFactories<TRoutesOrFactories>,\n TDeps,\n BoundServices<TBaseServices & TServices>,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n> {\n const { config, options: fragmentOptions = {} as TOptions, serviceImplementations } = options;\n\n // Use default mountRoute for testing if not provided\n const fullOptions = {\n mountRoute: \"/api/test\",\n ...fragmentOptions,\n } as TOptions;\n\n // Instantiate and return the fragment directly\n return instantiateFragment(\n definition,\n config,\n routesOrFactories,\n fullOptions,\n serviceImplementations,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,gBAAgB;AAC9B,SAYE,YAuBG;EAEH,MAAM,iBAAiB,QAAQ,OAAO;EACtC,MAAM,sBAAsB,eAAe;EAG3C,MAAMA,mBAQD,YAAY;GAEf,MAAM,mBAAmB,sBACrB,oBAAoB,QAAQ,GAC3B,EAAE;GAGP,MAAMC,YAAqC,EACzC,MAAM,QAAQ,MACf;AAED,UAAO;IACL,GAAG;IACH,GAAG;IACJ;;AAIH,SAAO,IAAI,0BAWT,QAAQ,MAAM;GACd,cAAc,eAAe;GAC7B,cAAc;GACd,eAAe,eAAe;GAC9B,iBAAiB,eAAe;GAChC,qBAAqB,eAAe;GACpC,sBAAsB,eAAe;GACrC,mBAAmB,eAAe;GACnC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEN,SAAgB,sBAad,YAYA,mBACA,SAQA;CACA,MAAM,EAAE,QAAQ,SAAS,kBAAkB,EAAE,EAAc,2BAA2B;AAStF,QAAO,oBACL,YACA,QACA,mBATkB;EAClB,YAAY;EACZ,GAAG;EACJ,EAQC,uBACD"}
1
+ {"version":3,"file":"test.js","names":["newBaseServices: ServiceConstructorFn<\n TConfig,\n TOptions,\n TDeps,\n TServiceDeps,\n TPrivateServices,\n TBaseServices & TestBaseServices<TDeps>,\n TServiceThisContext\n >","testUtils: TestBaseServices<TDeps>"],"sources":["../../src/test/test.ts"],"sourcesContent":["import type { RequestThisContext } from \"../api/api\";\nimport type { AnyRouteOrFactory, FlattenRouteFactories } from \"../api/route\";\nimport type { FragnoPublicConfig } from \"../api/shared-types\";\nimport {\n FragmentDefinitionBuilder,\n type FragmentDefinition,\n type ServiceConstructorFn,\n} from \"../api/fragment-definition-builder\";\nimport {\n instantiateFragment,\n type AnyFragnoInstantiatedFragment,\n type FragnoInstantiatedFragment,\n type RoutesWithInternal,\n} from \"../api/fragment-instantiator\";\nimport type { BoundServices } from \"../api/bind-services\";\n\n// Re-export for convenience\nexport type { RouteHandlerInputOptions } from \"../api/route-handler-input-options\";\nexport type { FragnoResponse } from \"../api/fragno-response\";\n\nexport type TestBaseServices<TDeps> = {\n /**\n * Access to fragment dependencies for testing purposes.\n */\n deps: TDeps;\n};\n\n/**\n * Extension function that adds test utilities to a fragment definition.\n * This adds a `test` service to base services with access to deps.\n *\n * @example\n * ```typescript\n * const definition = defineFragment(\"my-fragment\")\n * .withDependencies(({ config }) => ({ client: createClient(config) }))\n * .extend(withTestUtils())\n * .build();\n *\n * const fragment = createFragmentForTest(definition, [], { config: {...} });\n * expect(fragment.services.test.deps.client).toBeDefined();\n * ```\n */\nexport function withTestUtils() {\n return <\n TConfig,\n TOptions extends FragnoPublicConfig,\n TDeps,\n TBaseServices,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext extends RequestThisContext,\n THandlerThisContext extends RequestThisContext,\n TRequestStorage,\n >(\n builder: FragmentDefinitionBuilder<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n >,\n ): FragmentDefinitionBuilder<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices & TestBaseServices<TDeps>,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n > => {\n // Get the current base services factory\n const currentBaseDef = builder.build();\n const currentBaseServices = currentBaseDef.baseServices;\n\n // Create new base services factory that merges test utilities\n const newBaseServices: ServiceConstructorFn<\n TConfig,\n TOptions,\n TDeps,\n TServiceDeps,\n TPrivateServices,\n TBaseServices & TestBaseServices<TDeps>,\n TServiceThisContext\n > = (context) => {\n // Call existing base services if they exist\n const existingServices = currentBaseServices\n ? currentBaseServices(context)\n : ({} as TBaseServices);\n\n // Add test utilities\n const testUtils: TestBaseServices<TDeps> = {\n deps: context.deps,\n };\n\n return {\n ...existingServices,\n ...testUtils,\n } as TBaseServices & TestBaseServices<TDeps>;\n };\n\n // Create new builder with updated base services\n return new FragmentDefinitionBuilder<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices & TestBaseServices<TDeps>,\n TServices,\n TServiceDeps,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage\n >(builder.name, {\n dependencies: currentBaseDef.dependencies,\n baseServices: newBaseServices,\n namedServices: currentBaseDef.namedServices,\n privateServices: currentBaseDef.privateServices,\n serviceDependencies: currentBaseDef.serviceDependencies,\n createRequestStorage: currentBaseDef.createRequestStorage,\n createThisContext: currentBaseDef.createThisContext,\n });\n };\n}\n\n/**\n * Options for creating a test fragment with the new architecture\n */\nexport interface CreateFragmentForTestOptions<\n TConfig,\n TOptions extends FragnoPublicConfig,\n TServiceDependencies,\n> {\n config: TConfig;\n options?: Partial<TOptions>;\n serviceImplementations?: TServiceDependencies;\n}\n\n/**\n * Create a fragment instance for testing with optional service dependency overrides.\n * This uses the new fragment definition and instantiation architecture.\n *\n * **Important:** Use `.extend(withTestUtils())` before `.build()` to expose deps via `services.test.deps`.\n *\n * Returns an instantiated fragment with:\n * - `services` - Services (base + named) from the fragment definition\n * - `services.test.deps` - Dependencies (only if you used .extend(withTestUtils()))\n * - `callRoute()` - Type-safe method to call routes directly\n * - `handler()` - Request handler for integration\n * - `inContext()` - Run code within request context\n *\n * @param definition - The fragment definition from builder.build()\n * @param routesOrFactories - Route configurations or route factories\n * @param options - Configuration and optional overrides\n * @returns An instantiated fragment ready for testing\n *\n * @example\n * ```typescript\n * const definition = defineFragment<{ apiKey: string }>(\"test\")\n * .withDependencies(({ config }) => ({ client: createClient(config.apiKey) }))\n * .providesService(\"api\", ({ deps }) => ({ call: () => deps.client.call() }))\n * .extend(withTestUtils()) // <- Add test utilities\n * .build();\n *\n * const fragment = createFragmentForTest(\n * definition,\n * [route1, route2],\n * {\n * config: { apiKey: \"test-key\" },\n * options: { mountRoute: \"/api/test\" }\n * }\n * );\n *\n * // Access deps via test utilities\n * expect(fragment.services.test.deps.client).toBeDefined();\n * expect(fragment.services.api.call()).toBeDefined();\n *\n * // Call routes directly by method and path\n * const response = await fragment.callRoute(\"POST\", \"/login\", {\n * body: { username: \"test\", password: \"test123\" }\n * });\n *\n * if (response.type === 'json') {\n * expect(response.data).toEqual({...});\n * }\n * ```\n */\nexport function createFragmentForTest<\n TConfig,\n TOptions extends FragnoPublicConfig,\n TDeps,\n TBaseServices extends Record<string, unknown>,\n TServices extends Record<string, unknown>,\n TServiceDependencies,\n TPrivateServices extends Record<string, unknown>,\n TServiceThisContext extends RequestThisContext,\n THandlerThisContext extends RequestThisContext,\n TRequestStorage,\n const TRoutesOrFactories extends readonly AnyRouteOrFactory[],\n TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment> = {},\n>(\n definition: FragmentDefinition<\n TConfig,\n TOptions,\n TDeps,\n TBaseServices,\n TServices,\n TServiceDependencies,\n TPrivateServices,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage,\n TLinkedFragments\n >,\n routesOrFactories: TRoutesOrFactories,\n options: CreateFragmentForTestOptions<TConfig, TOptions, TServiceDependencies>,\n): FragnoInstantiatedFragment<\n RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TLinkedFragments>,\n TDeps,\n BoundServices<TBaseServices & TServices>,\n TServiceThisContext,\n THandlerThisContext,\n TRequestStorage,\n TOptions,\n TLinkedFragments\n> {\n const { config, options: fragmentOptions = {} as TOptions, serviceImplementations } = options;\n\n // Use default mountRoute for testing if not provided\n const fullOptions = {\n mountRoute: \"/api/test\",\n ...fragmentOptions,\n } as TOptions;\n\n // Instantiate and return the fragment directly\n return instantiateFragment(\n definition,\n config,\n routesOrFactories,\n fullOptions,\n serviceImplementations,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,gBAAgB;AAC9B,SAYE,YAuBG;EAEH,MAAM,iBAAiB,QAAQ,OAAO;EACtC,MAAM,sBAAsB,eAAe;EAG3C,MAAMA,mBAQD,YAAY;GAEf,MAAM,mBAAmB,sBACrB,oBAAoB,QAAQ,GAC3B,EAAE;GAGP,MAAMC,YAAqC,EACzC,MAAM,QAAQ,MACf;AAED,UAAO;IACL,GAAG;IACH,GAAG;IACJ;;AAIH,SAAO,IAAI,0BAWT,QAAQ,MAAM;GACd,cAAc,eAAe;GAC7B,cAAc;GACd,eAAe,eAAe;GAC9B,iBAAiB,eAAe;GAChC,qBAAqB,eAAe;GACpC,sBAAsB,eAAe;GACrC,mBAAmB,eAAe;GACnC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEN,SAAgB,sBAcd,YAaA,mBACA,SAUA;CACA,MAAM,EAAE,QAAQ,SAAS,kBAAkB,EAAE,EAAc,2BAA2B;AAStF,QAAO,oBACL,YACA,QACA,mBATkB;EAClB,YAAY;EACZ,GAAG;EACJ,EAQC,uBACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragno-dev/core",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
4
4
  "exports": {
5
5
  ".": {
6
6
  "workerd": {
@@ -40,6 +40,11 @@
40
40
  "types": "./dist/api/request-context-storage.d.ts",
41
41
  "default": "./dist/api/request-context-storage.js"
42
42
  },
43
+ "./internal/trace-context": {
44
+ "development": "./src/internal/trace-context.ts",
45
+ "types": "./dist/internal/trace-context.d.ts",
46
+ "default": "./dist/internal/trace-context.js"
47
+ },
43
48
  "./internal/symbols": {
44
49
  "development": "./src/internal/symbols.ts",
45
50
  "types": "./dist/internal/symbols.d.ts",
@@ -88,29 +93,30 @@
88
93
  "type": "module",
89
94
  "sideEffects": false,
90
95
  "dependencies": {
96
+ "@paralleldrive/cuid2": "^2.3.1",
91
97
  "@nanostores/query": "^0.3.4",
92
98
  "@nanostores/solid": "^1.1.1",
93
- "@standard-schema/spec": "^1.0.0",
94
- "nanostores": "^1.0.1",
95
- "rou3": "^0.7.3"
99
+ "@standard-schema/spec": "^1.1.0",
100
+ "nanostores": "^1.1.0",
101
+ "rou3": "^0.7.12"
96
102
  },
97
103
  "devDependencies": {
98
- "@sveltejs/vite-plugin-svelte": "^6.2.0",
99
- "@testing-library/react": "^16.3.0",
100
- "@testing-library/svelte": "^5.2.8",
104
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
105
+ "@testing-library/react": "^16.3.2",
106
+ "@testing-library/svelte": "^5.3.1",
101
107
  "@testing-library/vue": "^8.1.0",
102
- "@types/node": "^22",
103
- "@types/react": "^19.0.0",
104
- "@types/react-dom": "^19.0.0",
108
+ "@types/node": "^22.19.7",
109
+ "@types/react": "^19.2.8",
110
+ "@types/react-dom": "^19.2.3",
105
111
  "@vitest/coverage-istanbul": "^3.2.4",
106
- "happy-dom": "^20.0.0",
107
- "react": "^19.0.0",
108
- "react-dom": "^19.0.0",
109
- "svelte": "^5.1.0",
110
- "solid-js": "^1.9.3",
112
+ "happy-dom": "^20.3.3",
113
+ "react": "^19.0.3",
114
+ "react-dom": "^19.0.3",
115
+ "solid-js": "^1.9.10",
116
+ "svelte": "^5.47.0",
111
117
  "vitest": "^3.2.4",
112
- "vue": "^3",
113
- "zod": "^4.0.5",
118
+ "vue": "^3.5.27",
119
+ "zod": "^4.3.5",
114
120
  "@fragno-private/typescript-config": "0.0.1",
115
121
  "@fragno-private/vitest-config": "0.0.0"
116
122
  },
package/src/api/api.ts CHANGED
@@ -27,6 +27,17 @@ export type ValidPath<T extends string = string> = T extends `/${infer Rest}`
27
27
 
28
28
  export interface RequestThisContext {}
29
29
 
30
+ /**
31
+ * Content types that can be accepted by a route.
32
+ *
33
+ * - `"application/json"` (default): JSON request body, validated against inputSchema
34
+ * - `"multipart/form-data"`: FormData request body (file uploads), no schema validation
35
+ */
36
+ export type RouteContentType =
37
+ | "application/json"
38
+ | "multipart/form-data"
39
+ | "application/octet-stream";
40
+
30
41
  export interface FragnoRouteConfig<
31
42
  TMethod extends HTTPMethod,
32
43
  TPath extends string,
@@ -38,6 +49,17 @@ export interface FragnoRouteConfig<
38
49
  > {
39
50
  method: TMethod;
40
51
  path: TPath;
52
+ /**
53
+ * The expected content type for this route's request body.
54
+ *
55
+ * - `"application/json"` (default): Expects JSON body, will be validated against inputSchema
56
+ * - `"multipart/form-data"`: Expects FormData body (for file uploads), use `ctx.formData()` in handler
57
+ *
58
+ * The server will reject requests with mismatched Content-Type headers.
59
+ *
60
+ * @default "application/json"
61
+ */
62
+ contentType?: RouteContentType;
41
63
  inputSchema?: TInputSchema;
42
64
  outputSchema?: TOutputSchema;
43
65
  errorCodes?: readonly TErrorCode[];
@@ -24,12 +24,12 @@ export type LinkedFragmentCallback<
24
24
  TConfig,
25
25
  TOptions extends FragnoPublicConfig,
26
26
  TServiceDependencies,
27
+ TFragment extends AnyFragnoInstantiatedFragment = AnyFragnoInstantiatedFragment,
27
28
  > = (context: {
28
29
  config: TConfig;
29
30
  options: TOptions;
30
31
  serviceDependencies?: TServiceDependencies;
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- }) => FragnoInstantiatedFragment<any, any, any, any, any, any, any>;
32
+ }) => TFragment;
33
33
 
34
34
  /**
35
35
  * Extract the services type from a FragnoInstantiatedFragment
@@ -223,12 +223,27 @@ export interface FragmentDefinition<
223
223
  deps: TDeps;
224
224
  }) => RequestContextStorage<TRequestStorage>;
225
225
 
226
+ /**
227
+ * Optional factory for internal data attached to fragment.$internal.
228
+ */
229
+ internalDataFactory?: (context: {
230
+ config: TConfig;
231
+ options: TOptions;
232
+ deps: TDeps;
233
+ linkedFragments: TLinkedFragments;
234
+ }) => Record<string, unknown> | void;
235
+
226
236
  /**
227
237
  * Optional linked fragments that will be automatically instantiated with this fragment.
228
238
  * Linked fragments are service-only and share the same config/options as the parent.
229
239
  */
230
240
  linkedFragments?: {
231
- [K in keyof TLinkedFragments]: LinkedFragmentCallback<TConfig, TOptions, TServiceDependencies>;
241
+ [K in keyof TLinkedFragments]: LinkedFragmentCallback<
242
+ TConfig,
243
+ TOptions,
244
+ TServiceDependencies,
245
+ TLinkedFragments[K]
246
+ >;
232
247
  };
233
248
 
234
249
  $serviceThisContext?: TServiceThisContext;
@@ -307,7 +322,12 @@ export class FragmentDefinitionBuilder<
307
322
  deps: TDeps;
308
323
  }) => RequestContextStorage<TRequestStorage>;
309
324
  #linkedFragments?: {
310
- [K in keyof TLinkedFragments]: LinkedFragmentCallback<TConfig, TOptions, TServiceDependencies>;
325
+ [K in keyof TLinkedFragments]: LinkedFragmentCallback<
326
+ TConfig,
327
+ TOptions,
328
+ TServiceDependencies,
329
+ TLinkedFragments[K]
330
+ >;
311
331
  };
312
332
 
313
333
  constructor(
@@ -368,7 +388,8 @@ export class FragmentDefinitionBuilder<
368
388
  [K in keyof TLinkedFragments]: LinkedFragmentCallback<
369
389
  TConfig,
370
390
  TOptions,
371
- TServiceDependencies
391
+ TServiceDependencies,
392
+ TLinkedFragments[K]
372
393
  >;
373
394
  };
374
395
  },
@@ -966,15 +987,13 @@ export class FragmentDefinitionBuilder<
966
987
 
967
988
  /**
968
989
  * Register a linked fragment that will be automatically instantiated.
969
- * Linked fragments are service-only (no routes) and share the same config/options as the parent.
970
- * All services from the linked fragment will be available as private services.
990
+ * Linked fragments share the same config/options as the parent and their services
991
+ * are exposed as private services. Routes are not exposed by default, but the
992
+ * instantiator may mount internal linked fragment routes under an internal prefix.
971
993
  */
972
- withLinkedFragment<
973
- const TName extends string,
974
- TCallback extends LinkedFragmentCallback<TConfig, TOptions, TServiceDependencies>,
975
- >(
994
+ withLinkedFragment<const TName extends string, TFragment extends AnyFragnoInstantiatedFragment>(
976
995
  name: TName,
977
- callback: TCallback,
996
+ callback: LinkedFragmentCallback<TConfig, TOptions, TServiceDependencies, TFragment>,
978
997
  ): FragmentDefinitionBuilder<
979
998
  TConfig,
980
999
  TOptions,
@@ -982,18 +1001,18 @@ export class FragmentDefinitionBuilder<
982
1001
  TBaseServices,
983
1002
  TServices,
984
1003
  TServiceDependencies,
985
- TPrivateServices & ExtractLinkedServices<TCallback>,
1004
+ TPrivateServices & ExtractLinkedServices<() => TFragment>,
986
1005
  TServiceThisContext,
987
1006
  THandlerThisContext,
988
1007
  TRequestStorage,
989
- TLinkedFragments & { [K in TName]: ReturnType<TCallback> }
1008
+ TLinkedFragments & { [K in TName]: TFragment }
990
1009
  > {
991
1010
  const newLinkedFragments = {
992
1011
  ...this.#linkedFragments,
993
1012
  [name]: callback,
994
1013
  };
995
1014
 
996
- // Cast is safe: We're declaring that the returned builder has TPrivateServices & ExtractLinkedServices<TCallback>,
1015
+ // Cast is safe: We're declaring that the returned builder has TPrivateServices & ExtractLinkedServices<TFragment>,
997
1016
  // even though the runtime privateServices hasn't changed yet. The linked fragment services will be
998
1017
  // merged into privateServices at instantiation time by the instantiator.
999
1018
  return new FragmentDefinitionBuilder(this.#name, {
@@ -1013,11 +1032,11 @@ export class FragmentDefinitionBuilder<
1013
1032
  TBaseServices,
1014
1033
  TServices,
1015
1034
  TServiceDependencies,
1016
- TPrivateServices & ExtractLinkedServices<TCallback>,
1035
+ TPrivateServices & ExtractLinkedServices<() => TFragment>,
1017
1036
  TServiceThisContext,
1018
1037
  THandlerThisContext,
1019
1038
  TRequestStorage,
1020
- TLinkedFragments & { [K in TName]: ReturnType<TCallback> }
1039
+ TLinkedFragments & { [K in TName]: TFragment }
1021
1040
  >;
1022
1041
  }
1023
1042