@fragno-dev/core 0.1.10 → 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 +139 -131
  2. package/CHANGELOG.md +63 -0
  3. package/dist/api/api.d.ts +23 -5
  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 +129 -32
  11. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  12. package/dist/api/fragment-instantiator.js +232 -50
  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 +5 -27
  37. package/dist/mod-client.d.ts.map +1 -1
  38. package/dist/mod-client.js +50 -13
  39. package/dist/mod-client.js.map +1 -1
  40. package/dist/mod.d.ts +4 -3
  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 +31 -18
  50. package/src/api/api.ts +24 -0
  51. package/src/api/fragment-definition-builder.ts +36 -17
  52. package/src/api/fragment-instantiator.test.ts +429 -1
  53. package/src/api/fragment-instantiator.ts +572 -58
  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 +89 -9
  66. package/src/mod.ts +7 -1
  67. package/src/runtime.ts +48 -0
  68. package/src/test/test.ts +13 -4
  69. package/tsdown.config.ts +1 -0
@@ -1,5 +1,5 @@
1
- import { resolveRouteFactories } from "./route.js";
2
1
  import { instantiatedFragmentFakeSymbol } from "../internal/symbols.js";
2
+ import { resolveRouteFactories } from "./route.js";
3
3
  import { FragnoApiError } from "./error.js";
4
4
  import { getMountRoute } from "./internal/route.js";
5
5
  import { RequestInputContext } from "./request-input-context.js";
@@ -9,14 +9,66 @@ import { RequestMiddlewareInputContext, RequestMiddlewareOutputContext } from ".
9
9
  import { parseFragnoResponse } from "./fragno-response.js";
10
10
  import { RequestContextStorage } from "./request-context-storage.js";
11
11
  import { bindServicesToContext } from "./bind-services.js";
12
+ import { recordTraceEvent } from "../internal/trace-context.js";
12
13
  import { addRoute, createRouter, findRoute } from "rou3";
13
14
 
14
15
  //#region src/api/fragment-instantiator.ts
16
+ const serializeHeadersForTrace = (headers) => Array.from(headers.entries()).sort(([a], [b]) => a.localeCompare(b));
17
+ const serializeQueryForTrace = (query) => Array.from(query.entries()).sort(([a], [b]) => a.localeCompare(b));
18
+ const serializeBodyForTrace = (body) => {
19
+ if (body instanceof FormData) return {
20
+ type: "form-data",
21
+ entries: Array.from(body.entries()).map(([key, value]) => {
22
+ if (value instanceof Blob) return [key, {
23
+ type: "blob",
24
+ size: value.size,
25
+ mime: value.type
26
+ }];
27
+ return [key, value];
28
+ })
29
+ };
30
+ if (body instanceof Blob) return {
31
+ type: "blob",
32
+ size: body.size,
33
+ mime: body.type
34
+ };
35
+ if (body instanceof ReadableStream) return { type: "stream" };
36
+ return body;
37
+ };
38
+ const INTERNAL_LINKED_FRAGMENT_NAME = "_fragno_internal";
39
+ const INTERNAL_ROUTE_PREFIX = "/_internal";
40
+ function normalizeRoutePrefix(prefix) {
41
+ if (!prefix.startsWith("/")) prefix = `/${prefix}`;
42
+ return prefix.endsWith("/") && prefix.length > 1 ? prefix.slice(0, -1) : prefix;
43
+ }
44
+ function joinRoutePath(prefix, path) {
45
+ const normalizedPrefix = normalizeRoutePrefix(prefix);
46
+ if (!path || path === "/") return normalizedPrefix;
47
+ return `${normalizedPrefix}${path.startsWith("/") ? path : `/${path}`}`;
48
+ }
49
+ function collectLinkedFragmentRoutes(linkedFragments) {
50
+ const linkedRoutes = [];
51
+ for (const [name, fragment] of Object.entries(linkedFragments)) {
52
+ if (name !== INTERNAL_LINKED_FRAGMENT_NAME) continue;
53
+ const internalRoutes = fragment.routes ?? [];
54
+ if (internalRoutes.length === 0) continue;
55
+ for (const route of internalRoutes) linkedRoutes.push({
56
+ ...route,
57
+ path: joinRoutePath(INTERNAL_ROUTE_PREFIX, route.path),
58
+ __internal: {
59
+ fragment,
60
+ originalPath: route.path,
61
+ routes: internalRoutes
62
+ }
63
+ });
64
+ }
65
+ return linkedRoutes;
66
+ }
15
67
  /**
16
68
  * Instantiated fragment class with encapsulated state.
17
69
  * Provides the same public API as the old FragnoInstantiatedFragment but with better encapsulation.
18
70
  */
19
- var FragnoInstantiatedFragment = class {
71
+ var FragnoInstantiatedFragment = class FragnoInstantiatedFragment {
20
72
  [instantiatedFragmentFakeSymbol] = instantiatedFragmentFakeSymbol;
21
73
  #name;
22
74
  #routes;
@@ -31,6 +83,7 @@ var FragnoInstantiatedFragment = class {
31
83
  #createRequestStorage;
32
84
  #options;
33
85
  #linkedFragments;
86
+ #internalData;
34
87
  constructor(params) {
35
88
  this.#name = params.name;
36
89
  this.#routes = params.routes;
@@ -43,6 +96,7 @@ var FragnoInstantiatedFragment = class {
43
96
  this.#createRequestStorage = params.createRequestStorage;
44
97
  this.#options = params.options;
45
98
  this.#linkedFragments = params.linkedFragments ?? {};
99
+ this.#internalData = params.internalData ?? {};
46
100
  this.#router = createRouter();
47
101
  for (const routeConfig of this.#routes) addRoute(this.#router, routeConfig.method.toUpperCase(), routeConfig.path, routeConfig);
48
102
  this.handler = this.handler.bind(this);
@@ -66,7 +120,8 @@ var FragnoInstantiatedFragment = class {
66
120
  return {
67
121
  deps: this.#deps,
68
122
  options: this.#options,
69
- linkedFragments: this.#linkedFragments
123
+ linkedFragments: this.#linkedFragments,
124
+ ...this.#internalData
70
125
  };
71
126
  }
72
127
  /**
@@ -160,26 +215,65 @@ var FragnoInstantiatedFragment = class {
160
215
  error: `Fragno: Route for '${this.#name}' not found`,
161
216
  code: "ROUTE_NOT_FOUND"
162
217
  }, { status: 404 });
218
+ const routeConfig = route.data;
219
+ const expectedContentType = routeConfig.contentType ?? "application/json";
163
220
  let requestBody = void 0;
164
221
  let rawBody = void 0;
165
222
  if (req.body instanceof ReadableStream) {
166
- rawBody = await req.clone().text();
167
- if (rawBody) try {
168
- requestBody = JSON.parse(rawBody);
169
- } catch {
170
- requestBody = void 0;
223
+ const requestContentType = (req.headers.get("content-type") ?? "").toLowerCase();
224
+ if (expectedContentType === "multipart/form-data") {
225
+ if (!requestContentType.includes("multipart/form-data")) return Response.json({
226
+ error: `This endpoint expects multipart/form-data, but received: ${requestContentType || "no content-type"}`,
227
+ code: "UNSUPPORTED_MEDIA_TYPE"
228
+ }, { status: 415 });
229
+ try {
230
+ requestBody = await req.formData();
231
+ } catch {
232
+ return Response.json({
233
+ error: "Failed to parse multipart form data",
234
+ code: "INVALID_REQUEST_BODY"
235
+ }, { status: 400 });
236
+ }
237
+ } else if (expectedContentType === "application/octet-stream") {
238
+ if (!requestContentType.includes("application/octet-stream")) return Response.json({
239
+ error: `This endpoint expects application/octet-stream, but received: ${requestContentType || "no content-type"}`,
240
+ code: "UNSUPPORTED_MEDIA_TYPE"
241
+ }, { status: 415 });
242
+ requestBody = req.body ?? new ReadableStream();
243
+ } else {
244
+ if (requestContentType.includes("multipart/form-data")) return Response.json({
245
+ error: `This endpoint expects JSON, but received multipart/form-data. Use a route with contentType: "multipart/form-data" for file uploads.`,
246
+ code: "UNSUPPORTED_MEDIA_TYPE"
247
+ }, { status: 415 });
248
+ rawBody = await req.clone().text();
249
+ if (rawBody) try {
250
+ requestBody = JSON.parse(rawBody);
251
+ } catch {
252
+ requestBody = void 0;
253
+ }
171
254
  }
172
255
  }
256
+ const decodedRouteParams = {};
257
+ for (const [key, value] of Object.entries(route.params ?? {})) decodedRouteParams[key] = decodeURIComponent(value);
173
258
  const requestState = new MutableRequestState({
174
- pathParams: route.params ?? {},
259
+ pathParams: decodedRouteParams,
175
260
  searchParams: url.searchParams,
176
261
  body: requestBody,
177
262
  headers: new Headers(req.headers)
178
263
  });
179
264
  const executeRequest = async () => {
180
- if (this.#middlewareHandler) {
181
- const middlewareResult = await this.#executeMiddleware(req, route, requestState);
182
- if (middlewareResult !== void 0) return middlewareResult;
265
+ const middlewareResult = await this.#executeMiddleware(req, route, requestState);
266
+ if (middlewareResult !== void 0) return middlewareResult;
267
+ const internalMeta = routeConfig.__internal;
268
+ if (internalMeta) {
269
+ const internalResult = await FragnoInstantiatedFragment.#runMiddlewareForFragment(internalMeta.fragment, {
270
+ req,
271
+ method: routeConfig.method,
272
+ path: internalMeta.originalPath,
273
+ requestState,
274
+ routes: internalMeta.routes
275
+ });
276
+ if (internalResult !== void 0) return internalResult;
183
277
  }
184
278
  return this.#executeHandler(req, route, requestState, rawBody);
185
279
  };
@@ -215,6 +309,15 @@ var FragnoInstantiatedFragment = class {
215
309
  inputSchema: route.inputSchema,
216
310
  shouldValidateInput: true
217
311
  });
312
+ recordTraceEvent({
313
+ type: "route-input",
314
+ method: route.method,
315
+ path: route.path,
316
+ pathParams: pathParams ?? {},
317
+ queryParams: serializeQueryForTrace(searchParams),
318
+ headers: serializeHeadersForTrace(requestHeaders),
319
+ body: serializeBodyForTrace(body)
320
+ });
218
321
  const outputContext = new RequestOutputContext(route.outputSchema);
219
322
  const executeHandler = async () => {
220
323
  try {
@@ -236,20 +339,43 @@ var FragnoInstantiatedFragment = class {
236
339
  * Returns undefined if middleware allows the request to continue to the handler.
237
340
  */
238
341
  async #executeMiddleware(req, route, requestState) {
239
- if (!this.#middlewareHandler || !route) return;
342
+ if (!route) return;
240
343
  const { path } = route.data;
241
- const middlewareInputContext = new RequestMiddlewareInputContext(this.#routes, {
344
+ return FragnoInstantiatedFragment.#runMiddlewareForFragment(this, {
345
+ req,
242
346
  method: req.method,
243
347
  path,
244
- request: req,
245
- state: requestState
348
+ requestState
246
349
  });
247
- const middlewareOutputContext = new RequestMiddlewareOutputContext(this.#deps, this.#services);
350
+ }
351
+ static async #runMiddlewareForFragment(fragment, options) {
352
+ if (!fragment.#middlewareHandler) return;
353
+ const middlewareInputContext = new RequestMiddlewareInputContext(options.routes ?? fragment.#routes, {
354
+ method: options.method,
355
+ path: options.path,
356
+ request: options.req,
357
+ state: options.requestState
358
+ });
359
+ const middlewareOutputContext = new RequestMiddlewareOutputContext(fragment.#deps, fragment.#services);
248
360
  try {
249
- const middlewareResult = await this.#middlewareHandler(middlewareInputContext, middlewareOutputContext);
361
+ const middlewareResult = await fragment.#middlewareHandler(middlewareInputContext, middlewareOutputContext);
362
+ recordTraceEvent({
363
+ type: "middleware-decision",
364
+ method: options.method,
365
+ path: options.path,
366
+ outcome: middlewareResult ? "deny" : "allow",
367
+ status: middlewareResult?.status
368
+ });
250
369
  if (middlewareResult !== void 0) return middlewareResult;
251
370
  } catch (error) {
252
371
  console.error("Error in middleware", error);
372
+ recordTraceEvent({
373
+ type: "middleware-decision",
374
+ method: options.method,
375
+ path: options.path,
376
+ outcome: "deny",
377
+ status: error instanceof FragnoApiError ? error.status : 500
378
+ });
253
379
  if (error instanceof FragnoApiError) return error.toResponse();
254
380
  return Response.json({
255
381
  error: "Internal server error",
@@ -275,6 +401,15 @@ var FragnoInstantiatedFragment = class {
275
401
  state: requestState,
276
402
  rawBody
277
403
  });
404
+ recordTraceEvent({
405
+ type: "route-input",
406
+ method: req.method,
407
+ path,
408
+ pathParams: inputContext.pathParams,
409
+ queryParams: serializeQueryForTrace(requestState.searchParams),
410
+ headers: serializeHeadersForTrace(requestState.headers),
411
+ body: serializeBodyForTrace(requestState.body)
412
+ });
278
413
  const outputContext = new RequestOutputContext(outputSchema);
279
414
  try {
280
415
  const contextForHandler = this.#handlerThisContext ?? {};
@@ -293,17 +428,26 @@ var FragnoInstantiatedFragment = class {
293
428
  * Core instantiation function that creates a fragment instance from a definition.
294
429
  * This function validates dependencies, calls all callbacks, and wires everything together.
295
430
  */
296
- function instantiateFragment(definition, config, routesOrFactories, options, serviceImplementations) {
431
+ function instantiateFragment(definition, config, routesOrFactories, options, serviceImplementations, instantiationOptions) {
432
+ const { dryRun = false } = instantiationOptions ?? {};
297
433
  const serviceDependencies = definition.serviceDependencies;
298
434
  if (serviceDependencies) for (const [serviceName, meta] of Object.entries(serviceDependencies)) {
299
435
  const metadata = meta;
300
436
  const implementation = serviceImplementations?.[serviceName];
301
437
  if (metadata.required && !implementation) throw new Error(`Fragment '${definition.name}' requires service '${metadata.name}' but it was not provided`);
302
438
  }
303
- const deps = definition.dependencies?.({
304
- config,
305
- options
306
- }) ?? {};
439
+ let deps;
440
+ try {
441
+ deps = definition.dependencies?.({
442
+ config,
443
+ options
444
+ }) ?? {};
445
+ } catch (error) {
446
+ if (dryRun) {
447
+ console.warn("Warning: Failed to initialize dependencies in dry run mode:", error instanceof Error ? error.message : String(error));
448
+ deps = {};
449
+ } else throw error;
450
+ }
307
451
  const linkedFragmentInstances = {};
308
452
  const linkedFragmentServices = {};
309
453
  if (definition.linkedFragments) for (const [name, callback] of Object.entries(definition.linkedFragments)) {
@@ -318,31 +462,59 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
318
462
  }
319
463
  const defineService = (services$1) => services$1;
320
464
  const privateServices = { ...linkedFragmentServices };
321
- if (definition.privateServices) for (const [serviceName, factory] of Object.entries(definition.privateServices)) privateServices[serviceName] = factory({
322
- config,
323
- options,
324
- deps,
325
- serviceDeps: serviceImplementations ?? {},
326
- privateServices,
327
- defineService
328
- });
329
- const baseServices = definition.baseServices?.({
330
- config,
331
- options,
332
- deps,
333
- serviceDeps: serviceImplementations ?? {},
334
- privateServices,
335
- defineService
336
- }) ?? {};
465
+ if (definition.privateServices) for (const [serviceName, factory] of Object.entries(definition.privateServices)) {
466
+ const serviceFactory = factory;
467
+ try {
468
+ privateServices[serviceName] = serviceFactory({
469
+ config,
470
+ options,
471
+ deps,
472
+ serviceDeps: serviceImplementations ?? {},
473
+ privateServices,
474
+ defineService
475
+ });
476
+ } catch (error) {
477
+ if (dryRun) {
478
+ console.warn(`Warning: Failed to initialize private service '${serviceName}' in dry run mode:`, error instanceof Error ? error.message : String(error));
479
+ privateServices[serviceName] = {};
480
+ } else throw error;
481
+ }
482
+ }
483
+ let baseServices;
484
+ try {
485
+ baseServices = definition.baseServices?.({
486
+ config,
487
+ options,
488
+ deps,
489
+ serviceDeps: serviceImplementations ?? {},
490
+ privateServices,
491
+ defineService
492
+ }) ?? {};
493
+ } catch (error) {
494
+ if (dryRun) {
495
+ console.warn("Warning: Failed to initialize base services in dry run mode:", error instanceof Error ? error.message : String(error));
496
+ baseServices = {};
497
+ } else throw error;
498
+ }
337
499
  const namedServices = {};
338
- if (definition.namedServices) for (const [serviceName, factory] of Object.entries(definition.namedServices)) namedServices[serviceName] = factory({
339
- config,
340
- options,
341
- deps,
342
- serviceDeps: serviceImplementations ?? {},
343
- privateServices,
344
- defineService
345
- });
500
+ if (definition.namedServices) for (const [serviceName, factory] of Object.entries(definition.namedServices)) {
501
+ const serviceFactory = factory;
502
+ try {
503
+ namedServices[serviceName] = serviceFactory({
504
+ config,
505
+ options,
506
+ deps,
507
+ serviceDeps: serviceImplementations ?? {},
508
+ privateServices,
509
+ defineService
510
+ });
511
+ } catch (error) {
512
+ if (dryRun) {
513
+ console.warn(`Warning: Failed to initialize service '${serviceName}' in dry run mode:`, error instanceof Error ? error.message : String(error));
514
+ namedServices[serviceName] = {};
515
+ } else throw error;
516
+ }
517
+ }
346
518
  const services = {
347
519
  ...baseServices,
348
520
  ...namedServices
@@ -360,6 +532,12 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
360
532
  });
361
533
  const serviceContext = contexts?.serviceContext;
362
534
  const handlerContext = contexts?.handlerContext;
535
+ const internalData = definition.internalDataFactory?.({
536
+ config,
537
+ options,
538
+ deps,
539
+ linkedFragments: linkedFragmentInstances
540
+ }) ?? {};
363
541
  const boundServices = serviceContext ? bindServicesToContext(services, serviceContext) : services;
364
542
  const routes = resolveRouteFactories({
365
543
  config,
@@ -367,6 +545,8 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
367
545
  services: boundServices,
368
546
  serviceDeps: serviceImplementations ?? {}
369
547
  }, routesOrFactories);
548
+ const linkedRoutes = collectLinkedFragmentRoutes(linkedFragmentInstances);
549
+ const finalRoutes = linkedRoutes.length > 0 ? [...routes, ...linkedRoutes] : routes;
370
550
  const mountRoute = getMountRoute({
371
551
  name: definition.name,
372
552
  mountRoute: options.mountRoute
@@ -378,7 +558,7 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
378
558
  }) : void 0;
379
559
  return new FragnoInstantiatedFragment({
380
560
  name: definition.name,
381
- routes,
561
+ routes: finalRoutes,
382
562
  deps,
383
563
  services: boundServices,
384
564
  mountRoute,
@@ -387,7 +567,8 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
387
567
  storage,
388
568
  createRequestStorage: createRequestStorageWithContext,
389
569
  options,
390
- linkedFragments: linkedFragmentInstances
570
+ linkedFragments: linkedFragmentInstances,
571
+ internalData
391
572
  });
392
573
  }
393
574
  /**
@@ -463,7 +644,8 @@ var FragmentInstantiationBuilder = class FragmentInstantiationBuilder {
463
644
  * Build and return the instantiated fragment
464
645
  */
465
646
  build() {
466
- return instantiateFragment(this.#definition, this.#config ?? {}, this.#routes ?? [], this.#options ?? {}, this.#services);
647
+ const dryRun = process.env["FRAGNO_INIT_DRY_RUN"] === "true";
648
+ return instantiateFragment(this.#definition, this.#config ?? {}, this.#routes ?? [], this.#options ?? {}, this.#services, { dryRun });
467
649
  }
468
650
  };
469
651
  /**