@fragno-dev/core 0.1.8 → 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 (176) hide show
  1. package/.turbo/turbo-build.log +131 -56
  2. package/CHANGELOG.md +13 -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 -3
  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/error.js +48 -0
  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 -2
  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 -2
  71. package/dist/client/client.d.ts.map +1 -0
  72. package/dist/client/client.js +397 -5
  73. package/dist/client/client.js.map +1 -0
  74. package/dist/client/client.svelte.d.ts +5 -2
  75. package/dist/client/client.svelte.d.ts.map +1 -1
  76. package/dist/client/client.svelte.js +1 -4
  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 -2
  83. package/dist/client/react.d.ts.map +1 -1
  84. package/dist/client/react.js +3 -4
  85. package/dist/client/react.js.map +1 -1
  86. package/dist/client/solid.d.ts +5 -2
  87. package/dist/client/solid.d.ts.map +1 -1
  88. package/dist/client/solid.js +2 -4
  89. package/dist/client/solid.js.map +1 -1
  90. package/dist/client/vanilla.d.ts +5 -2
  91. package/dist/client/vanilla.d.ts.map +1 -1
  92. package/dist/client/vanilla.js +2 -42
  93. package/dist/client/vanilla.js.map +1 -1
  94. package/dist/client/vue.d.ts +5 -2
  95. package/dist/client/vue.d.ts.map +1 -1
  96. package/dist/client/vue.js +1 -4
  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 -2
  110. package/dist/mod.js +4 -4
  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 -34
  114. package/dist/test/test.d.ts.map +1 -1
  115. package/dist/test/test.js +75 -42
  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-kyKI7pqH.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 +1 -5
  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 +454 -189
  136. package/src/api/request-context-storage.ts +64 -0
  137. package/src/api/request-middleware.test.ts +301 -228
  138. package/src/api/route.test.ts +12 -36
  139. package/src/api/route.ts +167 -155
  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 +22 -15
  154. package/src/request/request.ts +8 -0
  155. package/src/test/test.test.ts +189 -375
  156. package/src/test/test.ts +186 -152
  157. package/tsdown.config.ts +8 -5
  158. package/dist/api/fragment-builder.d.ts +0 -2
  159. package/dist/api/fragment-builder.js +0 -3
  160. package/dist/api/fragment-instantiation.d.ts +0 -2
  161. package/dist/api/fragment-instantiation.js +0 -4
  162. package/dist/api-BFrUCIsF.d.ts +0 -963
  163. package/dist/api-BFrUCIsF.d.ts.map +0 -1
  164. package/dist/client-DAFHcKqA.js +0 -782
  165. package/dist/client-DAFHcKqA.js.map +0 -1
  166. package/dist/fragment-builder-Boh2vNHq.js +0 -108
  167. package/dist/fragment-builder-Boh2vNHq.js.map +0 -1
  168. package/dist/fragment-instantiation-DUT-HLl1.js +0 -898
  169. package/dist/fragment-instantiation-DUT-HLl1.js.map +0 -1
  170. package/dist/route-C4CyNHkC.js +0 -26
  171. package/dist/route-C4CyNHkC.js.map +0 -1
  172. package/dist/ssr-kyKI7pqH.js.map +0 -1
  173. package/src/api/fragment-builder.ts +0 -518
  174. package/src/api/fragment-instantiation.test.ts +0 -702
  175. package/src/api/fragment-instantiation.ts +0 -766
  176. package/src/api/fragment.test.ts +0 -585
@@ -1,766 +0,0 @@
1
- import type { StandardSchemaV1 } from "@standard-schema/spec";
2
- import { type FragnoRouteConfig, type HTTPMethod, type RequestThisContext } from "./api";
3
- import { FragnoApiError } from "./error";
4
- import { getMountRoute } from "./internal/route";
5
- import { addRoute, createRouter, findRoute } from "rou3";
6
- import { RequestInputContext, type RequestBodyType } from "./request-input-context";
7
- import type { ExtractPathParams } from "./internal/path";
8
- import { RequestOutputContext } from "./request-output-context";
9
- import {
10
- type AnyFragnoRouteConfig,
11
- type AnyRouteOrFactory,
12
- type FlattenRouteFactories,
13
- resolveRouteFactories,
14
- } from "./route";
15
- import {
16
- RequestMiddlewareInputContext,
17
- RequestMiddlewareOutputContext,
18
- type FragnoMiddlewareCallback,
19
- } from "./request-middleware";
20
- import type { FragmentDefinition, RouteHandler } from "./fragment-builder";
21
- import { MutableRequestState } from "./mutable-request-state";
22
- import type { RouteHandlerInputOptions } from "./route-handler-input-options";
23
- import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
24
- import { type FragnoResponse, parseFragnoResponse } from "./fragno-response";
25
- import type { InferOrUnknown } from "../util/types-util";
26
-
27
- export interface FragnoPublicConfig {
28
- mountRoute?: string;
29
- }
30
-
31
- export type FetcherConfig =
32
- | { type: "options"; options: RequestInit }
33
- | { type: "function"; fetcher: typeof fetch };
34
-
35
- export interface FragnoPublicClientConfig {
36
- mountRoute?: string;
37
- baseUrl?: string;
38
- fetcherConfig?: FetcherConfig;
39
- }
40
-
41
- type AstroHandlers = {
42
- ALL: (req: Request) => Promise<Response>;
43
- };
44
-
45
- type ReactRouterHandlers = {
46
- loader: (args: { request: Request }) => Promise<Response>;
47
- action: (args: { request: Request }) => Promise<Response>;
48
- };
49
-
50
- type SolidStartHandlers = {
51
- GET: (args: { request: Request }) => Promise<Response>;
52
- POST: (args: { request: Request }) => Promise<Response>;
53
- PUT: (args: { request: Request }) => Promise<Response>;
54
- DELETE: (args: { request: Request }) => Promise<Response>;
55
- PATCH: (args: { request: Request }) => Promise<Response>;
56
- HEAD: (args: { request: Request }) => Promise<Response>;
57
- OPTIONS: (args: { request: Request }) => Promise<Response>;
58
- };
59
-
60
- type TanStackStartHandlers = SolidStartHandlers;
61
-
62
- type StandardHandlers = {
63
- GET: (req: Request) => Promise<Response>;
64
- POST: (req: Request) => Promise<Response>;
65
- PUT: (req: Request) => Promise<Response>;
66
- DELETE: (req: Request) => Promise<Response>;
67
- PATCH: (req: Request) => Promise<Response>;
68
- HEAD: (req: Request) => Promise<Response>;
69
- OPTIONS: (req: Request) => Promise<Response>;
70
- };
71
-
72
- type HandlersByFramework = {
73
- astro: AstroHandlers;
74
- "react-router": ReactRouterHandlers;
75
- "next-js": StandardHandlers;
76
- "svelte-kit": StandardHandlers;
77
- "solid-start": SolidStartHandlers;
78
- "tanstack-start": TanStackStartHandlers;
79
- };
80
-
81
- // Not actually a symbol, since we might be dealing with multiple instances of this code.
82
- export const instantiatedFragmentFakeSymbol = "$fragno-instantiated-fragment" as const;
83
-
84
- type FullstackFrameworks = keyof HandlersByFramework;
85
-
86
- export interface FragnoInstantiatedFragment<
87
- TRoutes extends readonly AnyFragnoRouteConfig[] = [],
88
- TDeps = {},
89
- TServices extends Record<string, unknown> = Record<string, unknown>,
90
- TAdditionalContext extends Record<string, unknown> = {},
91
- > {
92
- [instantiatedFragmentFakeSymbol]: typeof instantiatedFragmentFakeSymbol;
93
-
94
- config: FragnoFragmentSharedConfig<TRoutes>;
95
- deps: TDeps;
96
- services: TServices;
97
- additionalContext?: TAdditionalContext;
98
- handlersFor: <T extends FullstackFrameworks>(framework: T) => HandlersByFramework[T];
99
- handler: (req: Request) => Promise<Response>;
100
- mountRoute: string;
101
- callRoute: <TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
102
- method: TMethod,
103
- path: TPath,
104
- inputOptions?: RouteHandlerInputOptions<
105
- TPath,
106
- ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
107
- >,
108
- ) => Promise<
109
- FragnoResponse<
110
- InferOrUnknown<NonNullable<ExtractRouteByPath<TRoutes, TPath, TMethod>["outputSchema"]>>
111
- >
112
- >;
113
- callRouteRaw: <TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
114
- method: TMethod,
115
- path: TPath,
116
- inputOptions?: RouteHandlerInputOptions<
117
- TPath,
118
- ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
119
- >,
120
- ) => Promise<Response>;
121
- withMiddleware: (
122
- handler: FragnoMiddlewareCallback<TRoutes, TDeps, TServices>,
123
- ) => FragnoInstantiatedFragment<TRoutes, TDeps, TServices, TAdditionalContext>;
124
- }
125
-
126
- export interface FragnoFragmentSharedConfig<
127
- TRoutes extends readonly FragnoRouteConfig<
128
- HTTPMethod,
129
- string,
130
- StandardSchemaV1 | undefined,
131
- StandardSchemaV1 | undefined,
132
- string,
133
- string
134
- >[],
135
- > {
136
- name: string;
137
- routes: TRoutes;
138
- }
139
-
140
- export type AnyFragnoFragmentSharedConfig = FragnoFragmentSharedConfig<
141
- readonly AnyFragnoRouteConfig[]
142
- >;
143
-
144
- export function createFragment<
145
- const TConfig,
146
- const TDeps,
147
- const TServices extends Record<string, unknown>,
148
- const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
149
- const TAdditionalContext extends Record<string, unknown>,
150
- const TRequiredInterfaces extends Record<string, unknown>,
151
- const TProvidedInterfaces extends Record<string, unknown>,
152
- const TOptions extends FragnoPublicConfig,
153
- const TThisContext extends RequestThisContext = RequestThisContext,
154
- >(
155
- fragmentBuilder: {
156
- definition: FragmentDefinition<
157
- TConfig,
158
- TDeps,
159
- TServices,
160
- TAdditionalContext,
161
- TRequiredInterfaces,
162
- TProvidedInterfaces,
163
- TThisContext
164
- >;
165
- $requiredOptions: TOptions;
166
- },
167
- config: TConfig,
168
- routesOrFactories: TRoutesOrFactories,
169
- options: TOptions,
170
- interfaceImplementations?: TRequiredInterfaces,
171
- ): FragnoInstantiatedFragment<
172
- FlattenRouteFactories<TRoutesOrFactories>,
173
- TDeps & TRequiredInterfaces,
174
- TServices & TProvidedInterfaces & TRequiredInterfaces,
175
- TAdditionalContext
176
- > {
177
- type TRoutes = FlattenRouteFactories<TRoutesOrFactories>;
178
-
179
- const definition = fragmentBuilder.definition;
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
-
193
- const dependencies = definition.dependencies?.(config, options) ?? ({} as TDeps);
194
-
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 };
238
- const routes = resolveRouteFactories(context, routesOrFactories);
239
-
240
- const mountRoute = getMountRoute({
241
- name: definition.name,
242
- mountRoute: options.mountRoute,
243
- });
244
-
245
- const router =
246
- createRouter<
247
- FragnoRouteConfig<
248
- HTTPMethod,
249
- string,
250
- StandardSchemaV1 | undefined,
251
- StandardSchemaV1 | undefined,
252
- string,
253
- string,
254
- RequestThisContext
255
- >
256
- >();
257
-
258
- let middlewareHandler:
259
- | FragnoMiddlewareCallback<
260
- FlattenRouteFactories<TRoutesOrFactories>,
261
- TDeps & TRequiredInterfaces,
262
- TServices & TProvidedInterfaces & TRequiredInterfaces
263
- >
264
- | undefined;
265
-
266
- // Store the handler wrapper if provided (e.g., for database support)
267
- const handlerWrapper = definition.createHandlerWrapper?.(options);
268
-
269
- for (const routeConfig of routes) {
270
- addRoute(router, routeConfig.method.toUpperCase(), routeConfig.path, routeConfig);
271
- }
272
-
273
- const fragment: FragnoInstantiatedFragment<
274
- FlattenRouteFactories<TRoutesOrFactories>,
275
- TDeps & TRequiredInterfaces,
276
- TServices & TProvidedInterfaces & TRequiredInterfaces,
277
- TAdditionalContext & TOptions
278
- > = {
279
- [instantiatedFragmentFakeSymbol]: instantiatedFragmentFakeSymbol,
280
- mountRoute,
281
- config: {
282
- name: definition.name,
283
- routes,
284
- },
285
- services,
286
- deps: depsWithInterfaces,
287
- additionalContext: {
288
- ...definition.additionalContext,
289
- ...options,
290
- } as TAdditionalContext & TOptions,
291
- withMiddleware: (handler) => {
292
- if (middlewareHandler) {
293
- throw new Error("Middleware already set");
294
- }
295
-
296
- middlewareHandler = handler;
297
-
298
- return fragment;
299
- },
300
- callRoute: async <TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
301
- method: TMethod,
302
- path: TPath,
303
- inputOptions?: RouteHandlerInputOptions<
304
- TPath,
305
- ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
306
- >,
307
- ): Promise<
308
- FragnoResponse<
309
- InferOrUnknown<NonNullable<ExtractRouteByPath<TRoutes, TPath, TMethod>["outputSchema"]>>
310
- >
311
- > => {
312
- const response = await fragment.callRouteRaw(method, path, inputOptions);
313
- return parseFragnoResponse(response);
314
- },
315
- callRouteRaw: async <
316
- TMethod extends HTTPMethod,
317
- TPath extends ExtractRoutePath<TRoutes, TMethod>,
318
- >(
319
- method: TMethod,
320
- path: TPath,
321
- inputOptions?: RouteHandlerInputOptions<
322
- TPath,
323
- ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
324
- >,
325
- ): Promise<Response> => {
326
- // Find the route configuration
327
- const route = routes.find((r) => r.method === method && r.path === path);
328
-
329
- if (!route) {
330
- return Response.json(
331
- {
332
- error: `Route ${method} ${path} not found`,
333
- code: "ROUTE_NOT_FOUND",
334
- },
335
- { status: 404 },
336
- );
337
- }
338
-
339
- const {
340
- pathParams = {} as ExtractPathParams<TPath>,
341
- body,
342
- query,
343
- headers,
344
- } = inputOptions || {};
345
-
346
- // Convert query to URLSearchParams if needed
347
- const searchParams =
348
- query instanceof URLSearchParams
349
- ? query
350
- : query
351
- ? new URLSearchParams(query)
352
- : new URLSearchParams();
353
-
354
- // Convert headers to Headers if needed
355
- const requestHeaders =
356
- headers instanceof Headers ? headers : headers ? new Headers(headers) : new Headers();
357
-
358
- // Construct RequestInputContext
359
- const inputContext = new RequestInputContext({
360
- path: route.path,
361
- method: route.method,
362
- pathParams,
363
- searchParams,
364
- headers: requestHeaders,
365
- parsedBody: body,
366
- inputSchema: route.inputSchema,
367
- shouldValidateInput: true, // Enable validation for production use
368
- });
369
-
370
- // Construct RequestOutputContext
371
- const outputContext = new RequestOutputContext(route.outputSchema);
372
-
373
- // Call the route handler (wrap with handlerWrapper if provided)
374
- try {
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
- }
392
- return response;
393
- } catch (error) {
394
- console.error("Error in callRoute handler", error);
395
-
396
- if (error instanceof FragnoApiError) {
397
- return error.toResponse();
398
- }
399
-
400
- return Response.json(
401
- { error: "Internal server error", code: "INTERNAL_SERVER_ERROR" },
402
- { status: 500 },
403
- );
404
- }
405
- },
406
- handlersFor: <T extends FullstackFrameworks>(framework: T): HandlersByFramework[T] => {
407
- const handler = fragment.handler;
408
-
409
- // LLMs hallucinate these values sometimes, solution isn't obvious so we throw this error
410
- // @ts-expect-error TS2367
411
- if (framework === "h3" || framework === "nuxt") {
412
- throw new Error(`To get handlers for h3, use the 'fromWebHandler' utility function:
413
- import { fromWebHandler } from "h3";
414
- export default fromWebHandler(myFragment().handler);`);
415
- }
416
- const allHandlers = {
417
- astro: { ALL: handler },
418
- "react-router": {
419
- loader: ({ request }: { request: Request }) => handler(request),
420
- action: ({ request }: { request: Request }) => handler(request),
421
- },
422
- "next-js": {
423
- GET: handler,
424
- POST: handler,
425
- PUT: handler,
426
- DELETE: handler,
427
- PATCH: handler,
428
- HEAD: handler,
429
- OPTIONS: handler,
430
- },
431
- "svelte-kit": {
432
- GET: handler,
433
- POST: handler,
434
- PUT: handler,
435
- DELETE: handler,
436
- PATCH: handler,
437
- HEAD: handler,
438
- OPTIONS: handler,
439
- },
440
- "solid-start": {
441
- GET: ({ request }: { request: Request }) => handler(request),
442
- POST: ({ request }: { request: Request }) => handler(request),
443
- PUT: ({ request }: { request: Request }) => handler(request),
444
- DELETE: ({ request }: { request: Request }) => handler(request),
445
- PATCH: ({ request }: { request: Request }) => handler(request),
446
- HEAD: ({ request }: { request: Request }) => handler(request),
447
- OPTIONS: ({ request }: { request: Request }) => handler(request),
448
- },
449
- "tanstack-start": {
450
- GET: ({ request }: { request: Request }) => handler(request),
451
- POST: ({ request }: { request: Request }) => handler(request),
452
- PUT: ({ request }: { request: Request }) => handler(request),
453
- DELETE: ({ request }: { request: Request }) => handler(request),
454
- PATCH: ({ request }: { request: Request }) => handler(request),
455
- HEAD: ({ request }: { request: Request }) => handler(request),
456
- OPTIONS: ({ request }: { request: Request }) => handler(request),
457
- },
458
- } satisfies HandlersByFramework;
459
-
460
- return allHandlers[framework];
461
- },
462
- handler: async (req: Request) => {
463
- const url = new URL(req.url);
464
- const pathname = url.pathname;
465
-
466
- const matchRoute = pathname.startsWith(mountRoute) ? pathname.slice(mountRoute.length) : null;
467
-
468
- if (matchRoute === null) {
469
- return Response.json(
470
- {
471
- error:
472
- `Fragno: Route for '${definition.name}' not found. Is the fragment mounted on the right route? ` +
473
- `Expecting: '${mountRoute}'.`,
474
- code: "ROUTE_NOT_FOUND",
475
- },
476
- { status: 404 },
477
- );
478
- }
479
-
480
- const route = findRoute(router, req.method, matchRoute);
481
-
482
- if (!route) {
483
- return Response.json(
484
- { error: `Fragno: Route for '${definition.name}' not found`, code: "ROUTE_NOT_FOUND" },
485
- { status: 404 },
486
- );
487
- }
488
-
489
- const { handler, inputSchema, outputSchema, path } = route.data;
490
-
491
- const outputContext = new RequestOutputContext(outputSchema);
492
-
493
- // Create mutable request state that can be modified by middleware
494
- // Clone the request to read body as both text and JSON without consuming original stream
495
- let requestBody: RequestBodyType = undefined;
496
- let rawBody: string | undefined = undefined;
497
-
498
- if (req.body instanceof ReadableStream) {
499
- // Clone request to make sure we don't consume body stream
500
- const clonedReq = req.clone();
501
-
502
- // Get raw text
503
- rawBody = await clonedReq.text();
504
-
505
- // Parse JSON if body is not empty
506
- if (rawBody) {
507
- try {
508
- requestBody = JSON.parse(rawBody);
509
- } catch {
510
- // If JSON parsing fails, keep body as undefined
511
- // This handles cases where body is not JSON
512
- requestBody = undefined;
513
- }
514
- }
515
- }
516
-
517
- const requestState = new MutableRequestState({
518
- pathParams: route.params ?? {},
519
- searchParams: url.searchParams,
520
- body: requestBody,
521
- headers: new Headers(req.headers),
522
- });
523
-
524
- if (middlewareHandler) {
525
- const middlewareInputContext = new RequestMiddlewareInputContext(routes, {
526
- method: req.method as HTTPMethod,
527
- path,
528
- request: req,
529
- state: requestState,
530
- });
531
-
532
- const middlewareOutputContext = new RequestMiddlewareOutputContext(
533
- depsWithInterfaces,
534
- services,
535
- );
536
-
537
- try {
538
- const middlewareResult = await middlewareHandler(
539
- middlewareInputContext,
540
- middlewareOutputContext,
541
- );
542
- if (middlewareResult !== undefined) {
543
- return middlewareResult;
544
- }
545
- } catch (error) {
546
- console.error("Error in middleware", error);
547
-
548
- if (error instanceof FragnoApiError) {
549
- // TODO: If a validation error occurs in middleware (when calling `await input.valid()`)
550
- // the processing is short-circuited and a potential `catch` block around the call
551
- // to `input.valid()` in the actual handler will not be executed.
552
- return error.toResponse();
553
- }
554
-
555
- return Response.json(
556
- { error: "Internal server error", code: "INTERNAL_SERVER_ERROR" },
557
- { status: 500 },
558
- );
559
- }
560
- }
561
-
562
- const inputContext = await RequestInputContext.fromRequest({
563
- request: req,
564
- method: req.method,
565
- path,
566
- pathParams: (route.params ?? {}) as ExtractPathParams<typeof path>,
567
- inputSchema,
568
- state: requestState,
569
- rawBody,
570
- });
571
-
572
- try {
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);
583
- return result;
584
- } catch (error) {
585
- console.error("Error in handler", error);
586
-
587
- if (error instanceof FragnoApiError) {
588
- return error.toResponse();
589
- }
590
-
591
- return Response.json(
592
- { error: "Internal server error", code: "INTERNAL_SERVER_ERROR" },
593
- { status: 500 },
594
- );
595
- }
596
- },
597
- };
598
-
599
- return fragment;
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
- }