@fragno-dev/core 0.1.11 → 0.2.2

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 (155) hide show
  1. package/.turbo/turbo-build.log +87 -69
  2. package/CHANGELOG.md +79 -0
  3. package/dist/api/api.d.ts +21 -2
  4. package/dist/api/api.d.ts.map +1 -1
  5. package/dist/api/api.js +2 -1
  6. package/dist/api/api.js.map +1 -1
  7. package/dist/api/bind-services.d.ts +0 -1
  8. package/dist/api/bind-services.d.ts.map +1 -1
  9. package/dist/api/bind-services.js.map +1 -1
  10. package/dist/api/error.d.ts.map +1 -1
  11. package/dist/api/error.js.map +1 -1
  12. package/dist/api/fragment-definition-builder.d.ts +32 -40
  13. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  14. package/dist/api/fragment-definition-builder.js +15 -21
  15. package/dist/api/fragment-definition-builder.js.map +1 -1
  16. package/dist/api/fragment-instantiator.d.ts +51 -30
  17. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  18. package/dist/api/fragment-instantiator.js +201 -52
  19. package/dist/api/fragment-instantiator.js.map +1 -1
  20. package/dist/api/request-context-storage.d.ts +4 -0
  21. package/dist/api/request-context-storage.d.ts.map +1 -1
  22. package/dist/api/request-context-storage.js +6 -0
  23. package/dist/api/request-context-storage.js.map +1 -1
  24. package/dist/api/request-input-context.d.ts +57 -1
  25. package/dist/api/request-input-context.d.ts.map +1 -1
  26. package/dist/api/request-input-context.js +67 -0
  27. package/dist/api/request-input-context.js.map +1 -1
  28. package/dist/api/request-middleware.d.ts +2 -2
  29. package/dist/api/request-middleware.d.ts.map +1 -1
  30. package/dist/api/request-middleware.js.map +1 -1
  31. package/dist/api/request-output-context.d.ts +1 -1
  32. package/dist/api/request-output-context.d.ts.map +1 -1
  33. package/dist/api/request-output-context.js.map +1 -1
  34. package/dist/api/route-caller.d.ts +30 -0
  35. package/dist/api/route-caller.d.ts.map +1 -0
  36. package/dist/api/route-caller.js +63 -0
  37. package/dist/api/route-caller.js.map +1 -0
  38. package/dist/api/route-handler-input-options.d.ts.map +1 -1
  39. package/dist/api/route.d.ts +8 -8
  40. package/dist/api/route.d.ts.map +1 -1
  41. package/dist/api/route.js.map +1 -1
  42. package/dist/api/shared-types.d.ts.map +1 -1
  43. package/dist/client/client-error.d.ts.map +1 -1
  44. package/dist/client/client-error.js.map +1 -1
  45. package/dist/client/client.d.ts +90 -50
  46. package/dist/client/client.d.ts.map +1 -1
  47. package/dist/client/client.js +128 -16
  48. package/dist/client/client.js.map +1 -1
  49. package/dist/client/client.svelte.d.ts +6 -5
  50. package/dist/client/client.svelte.d.ts.map +1 -1
  51. package/dist/client/client.svelte.js +10 -2
  52. package/dist/client/client.svelte.js.map +1 -1
  53. package/dist/client/internal/ndjson-streaming.js.map +1 -1
  54. package/dist/client/react.d.ts +5 -4
  55. package/dist/client/react.d.ts.map +1 -1
  56. package/dist/client/react.js +104 -12
  57. package/dist/client/react.js.map +1 -1
  58. package/dist/client/solid.d.ts +7 -5
  59. package/dist/client/solid.d.ts.map +1 -1
  60. package/dist/client/solid.js +23 -9
  61. package/dist/client/solid.js.map +1 -1
  62. package/dist/client/vanilla.d.ts +16 -4
  63. package/dist/client/vanilla.d.ts.map +1 -1
  64. package/dist/client/vanilla.js +21 -1
  65. package/dist/client/vanilla.js.map +1 -1
  66. package/dist/client/vue.d.ts +10 -4
  67. package/dist/client/vue.d.ts.map +1 -1
  68. package/dist/client/vue.js +24 -1
  69. package/dist/client/vue.js.map +1 -1
  70. package/dist/id.d.ts +2 -0
  71. package/dist/id.js +3 -0
  72. package/dist/internal/cuid.d.ts +16 -0
  73. package/dist/internal/cuid.d.ts.map +1 -0
  74. package/dist/internal/cuid.js +82 -0
  75. package/dist/internal/cuid.js.map +1 -0
  76. package/dist/internal/trace-context.d.ts +23 -0
  77. package/dist/internal/trace-context.d.ts.map +1 -0
  78. package/dist/internal/trace-context.js +14 -0
  79. package/dist/internal/trace-context.js.map +1 -0
  80. package/dist/mod-client.d.ts +7 -20
  81. package/dist/mod-client.d.ts.map +1 -1
  82. package/dist/mod-client.js +25 -13
  83. package/dist/mod-client.js.map +1 -1
  84. package/dist/mod.d.ts +8 -6
  85. package/dist/mod.js +3 -1
  86. package/dist/runtime.d.ts +15 -0
  87. package/dist/runtime.d.ts.map +1 -0
  88. package/dist/runtime.js +33 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/test/test.d.ts +6 -6
  91. package/dist/test/test.d.ts.map +1 -1
  92. package/dist/test/test.js.map +1 -1
  93. package/dist/util/ssr.js.map +1 -1
  94. package/package.json +42 -52
  95. package/src/api/api.test.ts +3 -1
  96. package/src/api/api.ts +28 -0
  97. package/src/api/bind-services.ts +0 -5
  98. package/src/api/error.ts +1 -0
  99. package/src/api/fragment-definition-builder.extend.test.ts +2 -1
  100. package/src/api/fragment-definition-builder.test.ts +2 -1
  101. package/src/api/fragment-definition-builder.ts +56 -112
  102. package/src/api/fragment-instantiator.test.ts +311 -166
  103. package/src/api/fragment-instantiator.ts +470 -131
  104. package/src/api/fragment-services.test.ts +1 -0
  105. package/src/api/internal/path-runtime.test.ts +8 -0
  106. package/src/api/internal/path-type.test.ts +3 -1
  107. package/src/api/internal/route.test.ts +1 -0
  108. package/src/api/request-context-storage.ts +7 -0
  109. package/src/api/request-input-context.test.ts +156 -2
  110. package/src/api/request-input-context.ts +87 -1
  111. package/src/api/request-middleware.test.ts +43 -2
  112. package/src/api/request-middleware.ts +4 -3
  113. package/src/api/request-output-context.test.ts +3 -1
  114. package/src/api/request-output-context.ts +2 -1
  115. package/src/api/route-caller.test.ts +195 -0
  116. package/src/api/route-caller.ts +167 -0
  117. package/src/api/route-handler-input-options.ts +2 -1
  118. package/src/api/route.test.ts +4 -2
  119. package/src/api/route.ts +9 -3
  120. package/src/api/shared-types.ts +2 -1
  121. package/src/client/client-builder.test.ts +4 -2
  122. package/src/client/client-error.test.ts +2 -1
  123. package/src/client/client-error.ts +1 -1
  124. package/src/client/client-types.test.ts +19 -5
  125. package/src/client/client.ssr.test.ts +6 -4
  126. package/src/client/client.svelte.test.ts +18 -9
  127. package/src/client/client.svelte.ts +38 -13
  128. package/src/client/client.test.ts +244 -10
  129. package/src/client/client.ts +473 -148
  130. package/src/client/internal/ndjson-streaming.test.ts +6 -3
  131. package/src/client/internal/ndjson-streaming.ts +1 -0
  132. package/src/client/react.test.ts +176 -6
  133. package/src/client/react.ts +226 -31
  134. package/src/client/solid.test.ts +29 -5
  135. package/src/client/solid.ts +60 -22
  136. package/src/client/vanilla.test.ts +148 -6
  137. package/src/client/vanilla.ts +63 -9
  138. package/src/client/vue.test.ts +397 -8
  139. package/src/client/vue.ts +74 -4
  140. package/src/id.ts +1 -0
  141. package/src/internal/cuid.test.ts +164 -0
  142. package/src/internal/cuid.ts +133 -0
  143. package/src/internal/trace-context.ts +35 -0
  144. package/src/mod-client.ts +55 -9
  145. package/src/mod.ts +9 -3
  146. package/src/runtime.ts +48 -0
  147. package/src/test/test.test.ts +4 -2
  148. package/src/test/test.ts +14 -7
  149. package/src/util/async.test.ts +1 -0
  150. package/src/util/content-type.test.ts +1 -0
  151. package/src/util/nanostores.test.ts +3 -1
  152. package/src/util/ssr.ts +1 -0
  153. package/tsconfig.json +1 -1
  154. package/tsdown.config.ts +2 -0
  155. package/vitest.config.ts +2 -1
@@ -1,4 +1,8 @@
1
1
  import { describe, it, expect, vi, expectTypeOf } from "vitest";
2
+
3
+ import { z } from "zod";
4
+
5
+ import type { RequestThisContext } from "./api";
2
6
  import { defineFragment } from "./fragment-definition-builder";
3
7
  import {
4
8
  instantiate,
@@ -7,8 +11,6 @@ import {
7
11
  } from "./fragment-instantiator";
8
12
  import { defineRoute, defineRoutes, type AnyFragmentDefinition } from "./route";
9
13
  import type { FragnoPublicConfig } from "./shared-types";
10
- import type { RequestThisContext } from "./api";
11
- import { z } from "zod";
12
14
 
13
15
  describe("fragment-instantiator", () => {
14
16
  describe("basic instantiation", () => {
@@ -416,6 +418,56 @@ describe("fragment-instantiator", () => {
416
418
  expect(data).toEqual({ userId: "123" });
417
419
  });
418
420
 
421
+ it("should URL decode path params", async () => {
422
+ const definition = defineFragment("test-fragment").build();
423
+
424
+ const route = defineRoute({
425
+ method: "GET",
426
+ path: "/users/:name",
427
+ handler: async (input, { json }) => {
428
+ return json({ userName: input.pathParams.name });
429
+ },
430
+ });
431
+
432
+ const fragment = instantiate(definition)
433
+ .withRoutes([route])
434
+ .withOptions({ mountRoute: "/api" })
435
+ .build();
436
+
437
+ // URL with encoded space: "a%20b" should be decoded to "a b"
438
+ const request = new Request("http://localhost/api/users/a%20b");
439
+ const response = await fragment.handler(request);
440
+
441
+ expect(response.status).toBe(200);
442
+ const data = await response.json();
443
+ expect(data).toEqual({ userName: "a b" });
444
+ });
445
+
446
+ it("should URL decode path params with special characters", async () => {
447
+ const definition = defineFragment("test-fragment").build();
448
+
449
+ const route = defineRoute({
450
+ method: "GET",
451
+ path: "/files/:path",
452
+ handler: async (input, { json }) => {
453
+ return json({ filePath: input.pathParams.path });
454
+ },
455
+ });
456
+
457
+ const fragment = instantiate(definition)
458
+ .withRoutes([route])
459
+ .withOptions({ mountRoute: "/api" })
460
+ .build();
461
+
462
+ // URL with encoded slash: "folder%2Fsubfolder" should be decoded to "folder/subfolder"
463
+ const request = new Request("http://localhost/api/files/folder%2Fsubfolder");
464
+ const response = await fragment.handler(request);
465
+
466
+ expect(response.status).toBe(200);
467
+ const data = await response.json();
468
+ expect(data).toEqual({ filePath: "folder/subfolder" });
469
+ });
470
+
419
471
  it("should handle POST requests with body", async () => {
420
472
  const definition = defineFragment("test-fragment").build();
421
473
 
@@ -450,6 +502,170 @@ describe("fragment-instantiator", () => {
450
502
  const data = await response.json();
451
503
  expect(data).toEqual({ received: { test: "data" } });
452
504
  });
505
+
506
+ it("should accept FormData for routes with contentType: multipart/form-data", async () => {
507
+ const definition = defineFragment("test-fragment").build();
508
+
509
+ const route = defineRoute({
510
+ method: "POST",
511
+ path: "/upload",
512
+ contentType: "multipart/form-data",
513
+ handler: async (ctx, { json }) => {
514
+ const formData = ctx.formData();
515
+ const description = formData.get("description") as string;
516
+ return json({ description });
517
+ },
518
+ });
519
+
520
+ const fragment = instantiate(definition)
521
+ .withRoutes([route])
522
+ .withOptions({ mountRoute: "/api" })
523
+ .build();
524
+
525
+ const formData = new FormData();
526
+ formData.append("description", "Test file upload");
527
+
528
+ const request = new Request("http://localhost/api/upload", {
529
+ method: "POST",
530
+ body: formData,
531
+ });
532
+
533
+ const response = await fragment.handler(request);
534
+
535
+ expect(response.status).toBe(200);
536
+ const data = await response.json();
537
+ expect(data).toEqual({ description: "Test file upload" });
538
+ });
539
+
540
+ it("should reject FormData for JSON routes (default contentType)", async () => {
541
+ const definition = defineFragment("test-fragment").build();
542
+
543
+ const route = defineRoute({
544
+ method: "POST",
545
+ path: "/json-only",
546
+ inputSchema: z.object({ name: z.string() }),
547
+ handler: async ({ input }, { json }) => {
548
+ const body = await input.valid();
549
+ return json(body);
550
+ },
551
+ });
552
+
553
+ const fragment = instantiate(definition)
554
+ .withRoutes([route])
555
+ .withOptions({ mountRoute: "/api" })
556
+ .build();
557
+
558
+ const formData = new FormData();
559
+ formData.append("name", "test");
560
+
561
+ const request = new Request("http://localhost/api/json-only", {
562
+ method: "POST",
563
+ body: formData,
564
+ });
565
+
566
+ const response = await fragment.handler(request);
567
+
568
+ expect(response.status).toBe(415);
569
+ const data = await response.json();
570
+ expect(data.code).toBe("UNSUPPORTED_MEDIA_TYPE");
571
+ });
572
+
573
+ it("should reject JSON for FormData routes", async () => {
574
+ const definition = defineFragment("test-fragment").build();
575
+
576
+ const route = defineRoute({
577
+ method: "POST",
578
+ path: "/upload",
579
+ contentType: "multipart/form-data",
580
+ handler: async (ctx, { json }) => {
581
+ // Verify formData() works (would throw if not FormData)
582
+ ctx.formData();
583
+ return json({ success: true });
584
+ },
585
+ });
586
+
587
+ const fragment = instantiate(definition)
588
+ .withRoutes([route])
589
+ .withOptions({ mountRoute: "/api" })
590
+ .build();
591
+
592
+ const request = new Request("http://localhost/api/upload", {
593
+ method: "POST",
594
+ headers: { "Content-Type": "application/json" },
595
+ body: JSON.stringify({ name: "test" }),
596
+ });
597
+
598
+ const response = await fragment.handler(request);
599
+
600
+ expect(response.status).toBe(415);
601
+ const data = await response.json();
602
+ expect(data.code).toBe("UNSUPPORTED_MEDIA_TYPE");
603
+ });
604
+
605
+ it("should accept octet-stream for routes with contentType: application/octet-stream", async () => {
606
+ const definition = defineFragment("test-fragment").build();
607
+
608
+ const route = defineRoute({
609
+ method: "PUT",
610
+ path: "/stream",
611
+ contentType: "application/octet-stream",
612
+ handler: async (ctx, { json }) => {
613
+ const stream = ctx.bodyStream();
614
+ const text = await new Response(stream).text();
615
+ return json({ received: text });
616
+ },
617
+ });
618
+
619
+ const fragment = instantiate(definition)
620
+ .withRoutes([route])
621
+ .withOptions({ mountRoute: "/api" })
622
+ .build();
623
+
624
+ const request = new Request("http://localhost/api/stream", {
625
+ method: "PUT",
626
+ headers: { "Content-Type": "application/octet-stream" },
627
+ body: new TextEncoder().encode("hello"),
628
+ });
629
+
630
+ const response = await fragment.handler(request);
631
+
632
+ expect(response.status).toBe(200);
633
+ const data = await response.json();
634
+ expect(data).toEqual({ received: "hello" });
635
+ });
636
+
637
+ it("should reject JSON for octet-stream routes", async () => {
638
+ const definition = defineFragment("test-fragment").build();
639
+
640
+ const route = defineRoute({
641
+ method: "PUT",
642
+ path: "/stream",
643
+ contentType: "application/octet-stream",
644
+ handler: async (ctx, { json }) => {
645
+ if (!ctx.isBodyStream()) {
646
+ throw new Error("Expected stream body");
647
+ }
648
+ return json({ ok: true });
649
+ },
650
+ });
651
+
652
+ const fragment = instantiate(definition)
653
+ .withRoutes([route])
654
+ .withOptions({ mountRoute: "/api" })
655
+ .build();
656
+
657
+ const request = new Request("http://localhost/api/stream", {
658
+ method: "PUT",
659
+ headers: { "Content-Type": "application/json" },
660
+ body: JSON.stringify({ hello: "world" }),
661
+ });
662
+
663
+ const response = await fragment.handler(request);
664
+
665
+ expect(response.status).toBe(415);
666
+ const data = await response.json();
667
+ expect(data.code).toBe("UNSUPPORTED_MEDIA_TYPE");
668
+ });
453
669
  });
454
670
 
455
671
  describe("callRoute", () => {
@@ -803,6 +1019,48 @@ describe("fragment-instantiator", () => {
803
1019
 
804
1020
  expect(contextCreationSpy).toHaveBeenCalledTimes(2);
805
1021
  });
1022
+
1023
+ it("should store lifecycle waitUntil in request storage", async () => {
1024
+ const requestWaitUntilSymbol = Symbol.for("fragno-request-wait-until");
1025
+ const waitUntil = vi.fn();
1026
+
1027
+ const definition = defineFragment("test-fragment")
1028
+ .withRequestStorage(() => ({}))
1029
+ .withThisContext(({ storage }) => {
1030
+ const ctx = {
1031
+ get waitUntil() {
1032
+ return (storage.getStore() as Record<symbol, unknown>)[requestWaitUntilSymbol];
1033
+ },
1034
+ };
1035
+ return { serviceContext: ctx, handlerContext: ctx };
1036
+ })
1037
+ .build();
1038
+
1039
+ const routes = defineRoutes(definition).create(({ defineRoute }) => [
1040
+ defineRoute({
1041
+ method: "GET",
1042
+ path: "/test",
1043
+ handler: async function (_input, { json }) {
1044
+ return json({
1045
+ hasWaitUntil: typeof this.waitUntil === "function",
1046
+ sameWaitUntil: this.waitUntil === waitUntil,
1047
+ });
1048
+ },
1049
+ }),
1050
+ ]);
1051
+
1052
+ const fragment = instantiate(definition)
1053
+ .withRoutes([routes])
1054
+ .withOptions({ mountRoute: "/api" })
1055
+ .build();
1056
+
1057
+ const response = await fragment.handler(new Request("http://localhost/api/test"), {
1058
+ waitUntil,
1059
+ });
1060
+ const data = await response.json();
1061
+
1062
+ expect(data).toEqual({ hasWaitUntil: true, sameWaitUntil: true });
1063
+ });
806
1064
  });
807
1065
 
808
1066
  describe("defineService with custom this context", () => {
@@ -1251,188 +1509,75 @@ describe("fragment-instantiator", () => {
1251
1509
  });
1252
1510
  });
1253
1511
 
1254
- describe("linked fragments", () => {
1255
- it("should instantiate linked fragments with the parent fragment", () => {
1256
- interface Config {
1257
- apiKey: string;
1258
- }
1259
-
1260
- // Create a linked fragment definition
1261
- const linkedFragmentDef = defineFragment<Config>("linked-fragment")
1262
- .providesService("linkedService", () => ({
1263
- getValue: () => "from-linked",
1264
- }))
1265
- .build();
1266
-
1267
- // Create main fragment with linked fragment
1268
- const definition = defineFragment<Config>("main-fragment")
1269
- .withLinkedFragment("internal", ({ config, options }) => {
1270
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1271
- })
1272
- .build();
1273
-
1274
- const fragment = instantiate(definition)
1275
- .withConfig({ apiKey: "test-key" })
1276
- .withOptions({ mountRoute: "/api" })
1277
- .build();
1278
-
1279
- // Verify linked fragment exists
1280
- expect(Object.keys(fragment.$internal.linkedFragments).length).toBe(1);
1281
- expect("internal" in fragment.$internal.linkedFragments).toBe(true);
1282
-
1283
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1284
- expect(linkedFragment).toBeDefined();
1285
- expect(linkedFragment?.name).toBe("linked-fragment");
1286
- });
1287
-
1288
- it("should pass config and options to linked fragments", () => {
1289
- interface Config {
1290
- value: string;
1291
- }
1292
-
1293
- interface Options extends FragnoPublicConfig {
1294
- customOption: string;
1295
- }
1296
-
1297
- const linkedFragmentDef = defineFragment<Config, Options>("linked-fragment")
1298
- .withDependencies(({ config, options }) => ({
1299
- combined: `${config.value}-${options.customOption}`,
1300
- }))
1301
- .build();
1302
-
1303
- const definition = defineFragment<Config, Options>("main-fragment")
1304
- .withLinkedFragment("internal", ({ config, options }) => {
1305
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1306
- })
1307
- .build();
1308
-
1309
- const fragment = instantiate(definition)
1310
- .withConfig({ value: "config" })
1311
- .withOptions({ customOption: "option", mountRoute: "/api" } as Options)
1312
- .build();
1313
-
1314
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1315
- expect(linkedFragment?.$internal.deps).toEqual({
1316
- combined: "config-option",
1317
- });
1318
- });
1319
-
1320
- it("should allow linked fragments to provide services", () => {
1321
- const linkedFragmentDef = defineFragment("linked-fragment")
1322
- .providesService("settingsService", () => ({
1323
- get: (key: string) => `value-for-${key}`,
1324
- set: (key: string, value: string) => {
1325
- console.log(`Setting ${key} = ${value}`);
1326
- },
1327
- }))
1328
- .build();
1329
-
1512
+ describe("internal routes", () => {
1513
+ it("should mount internal routes under /_internal", async () => {
1330
1514
  const definition = defineFragment("main-fragment")
1331
- .withLinkedFragment("internal", ({ config, options }) => {
1332
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1333
- })
1515
+ .withInternalRoutes([
1516
+ defineRoute({
1517
+ method: "GET",
1518
+ path: "/status",
1519
+ handler: async (_input, { json }) => {
1520
+ return json({ ok: true });
1521
+ },
1522
+ }),
1523
+ ])
1334
1524
  .build();
1335
1525
 
1336
1526
  const fragment = instantiate(definition).withOptions({}).build();
1337
1527
 
1338
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1339
- expect(linkedFragment?.services.settingsService).toBeDefined();
1340
- expect(linkedFragment?.services.settingsService.get("test")).toBe("value-for-test");
1528
+ const response = await fragment.callRouteRaw("GET", "/_internal/status" as never);
1529
+ expect(response.status).toBe(200);
1530
+ await expect(response.json()).resolves.toEqual({ ok: true });
1341
1531
  });
1342
1532
 
1343
- it("should support multiple linked fragments", () => {
1344
- const linkedFragmentDef1 = defineFragment("linked-fragment-1")
1345
- .providesService("service1", () => ({ method: () => "service1" }))
1346
- .build();
1347
-
1348
- const linkedFragmentDef2 = defineFragment("linked-fragment-2")
1349
- .providesService("service2", () => ({ method: () => "service2" }))
1350
- .build();
1351
-
1533
+ it("should mount internal routes under the fragment mount route", async () => {
1352
1534
  const definition = defineFragment("main-fragment")
1353
- .withLinkedFragment("internal1", ({ config, options }) => {
1354
- return instantiate(linkedFragmentDef1).withConfig(config).withOptions(options).build();
1355
- })
1356
- .withLinkedFragment("internal2", ({ config, options }) => {
1357
- return instantiate(linkedFragmentDef2).withConfig(config).withOptions(options).build();
1358
- })
1535
+ .withInternalRoutes([
1536
+ defineRoute({
1537
+ method: "GET",
1538
+ path: "/status",
1539
+ handler: async (_input, { json }) => {
1540
+ return json({ ok: true });
1541
+ },
1542
+ }),
1543
+ ])
1359
1544
  .build();
1360
1545
 
1361
- const fragment = instantiate(definition).withOptions({}).build();
1362
-
1363
- expect(Object.keys(fragment.$internal.linkedFragments).length).toBe(2);
1364
- expect("internal1" in fragment.$internal.linkedFragments).toBe(true);
1365
- expect("internal2" in fragment.$internal.linkedFragments).toBe(true);
1366
-
1367
- const linked1 = fragment.$internal.linkedFragments.internal1;
1368
- const linked2 = fragment.$internal.linkedFragments.internal2;
1546
+ const fragment = instantiate(definition).withOptions({ mountRoute: "/api" }).build();
1369
1547
 
1370
- expect(linked1?.services.service1.method()).toBe("service1");
1371
- expect(linked2?.services.service2.method()).toBe("service2");
1548
+ const request = new Request("http://localhost/api/_internal/status");
1549
+ const response = await fragment.handler(request);
1550
+ expect(response.status).toBe(200);
1551
+ await expect(response.json()).resolves.toEqual({ ok: true });
1372
1552
  });
1373
1553
 
1374
- it("should pass service dependencies to linked fragments", () => {
1375
- interface ExternalService {
1376
- getValue: () => string;
1377
- }
1378
-
1379
- const externalService: ExternalService = {
1380
- getValue: () => "external-value",
1381
- };
1382
-
1383
- const linkedFragmentDef = defineFragment("linked-fragment")
1384
- .usesService<"externalService", ExternalService>("externalService")
1385
- .providesService("linkedService", ({ serviceDeps }) => ({
1386
- getFromExternal: () => serviceDeps.externalService.getValue(),
1387
- }))
1388
- .build();
1389
-
1390
- const definition = defineFragment("main-fragment")
1391
- .usesService<"externalService", ExternalService>("externalService")
1392
- .withLinkedFragment("internal", ({ config, options, serviceDependencies }) => {
1393
- return instantiate(linkedFragmentDef)
1394
- .withConfig(config)
1395
- .withOptions(options)
1396
- .withServices(serviceDependencies!)
1397
- .build();
1398
- })
1399
- .build();
1400
-
1401
- const fragment = instantiate(definition)
1402
- .withOptions({})
1403
- .withServices({ externalService })
1404
- .build();
1405
-
1406
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1407
- expect(linkedFragment?.services.linkedService.getFromExternal()).toBe("external-value");
1408
- });
1554
+ it("should resolve internal route factories with services", async () => {
1555
+ const definitionBuilder = defineFragment("main-fragment").providesService(
1556
+ "statusService",
1557
+ () => ({
1558
+ getStatus: () => ({ ok: true }),
1559
+ }),
1560
+ );
1409
1561
 
1410
- it("should expose linked fragment services as private services", () => {
1411
- const linkedFragmentDef = defineFragment("linked-fragment")
1412
- .providesService("linkedService", () => ({
1413
- getValue: () => "from-linked",
1414
- }))
1415
- .build();
1562
+ const internalRoutes = defineRoutes(definitionBuilder.build()).create(
1563
+ ({ services, defineRoute }) => [
1564
+ defineRoute({
1565
+ method: "GET",
1566
+ path: "/status",
1567
+ handler: async (_input, { json }) => {
1568
+ return json(services.statusService.getStatus());
1569
+ },
1570
+ }),
1571
+ ],
1572
+ );
1416
1573
 
1417
- const definition = defineFragment("main-fragment")
1418
- .withLinkedFragment("internal", ({ config, options }) => {
1419
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1420
- })
1421
- .providesService("mainService", ({ privateServices }) => ({
1422
- getLinkedValue: () => {
1423
- return privateServices.linkedService.getValue();
1424
- },
1425
- }))
1426
- .build();
1574
+ const definition = definitionBuilder.withInternalRoutes([internalRoutes]).build();
1427
1575
 
1428
1576
  const fragment = instantiate(definition).withOptions({}).build();
1429
1577
 
1430
- // The main service can access linked fragment services via privateServices
1431
- expect(fragment.services.mainService.getLinkedValue()).toBe("from-linked");
1432
-
1433
- // Linked fragment services are NOT directly exposed on the main fragment
1434
- // @ts-expect-error - Linked fragment service should not be accessible
1435
- expect(fragment.services.linkedService).toBeUndefined();
1578
+ const response = await fragment.callRouteRaw("GET", "/_internal/status" as never);
1579
+ expect(response.status).toBe(200);
1580
+ await expect(response.json()).resolves.toEqual({ ok: true });
1436
1581
  });
1437
1582
  });
1438
1583