@fragno-dev/core 0.2.0 → 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 (146) hide show
  1. package/.turbo/turbo-build.log +72 -62
  2. package/CHANGELOG.md +28 -0
  3. package/dist/api/api.d.ts +3 -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 +26 -44
  13. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  14. package/dist/api/fragment-definition-builder.js +15 -22
  15. package/dist/api/fragment-definition-builder.js.map +1 -1
  16. package/dist/api/fragment-instantiator.d.ts +51 -37
  17. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  18. package/dist/api/fragment-instantiator.js +74 -69
  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.map +1 -1
  25. package/dist/api/request-input-context.js.map +1 -1
  26. package/dist/api/request-middleware.d.ts +1 -1
  27. package/dist/api/request-middleware.d.ts.map +1 -1
  28. package/dist/api/request-middleware.js.map +1 -1
  29. package/dist/api/request-output-context.d.ts +1 -1
  30. package/dist/api/request-output-context.d.ts.map +1 -1
  31. package/dist/api/request-output-context.js.map +1 -1
  32. package/dist/api/route-caller.d.ts +30 -0
  33. package/dist/api/route-caller.d.ts.map +1 -0
  34. package/dist/api/route-caller.js +63 -0
  35. package/dist/api/route-caller.js.map +1 -0
  36. package/dist/api/route-handler-input-options.d.ts.map +1 -1
  37. package/dist/api/route.d.ts +1 -1
  38. package/dist/api/route.d.ts.map +1 -1
  39. package/dist/api/route.js.map +1 -1
  40. package/dist/api/shared-types.d.ts.map +1 -1
  41. package/dist/client/client-error.d.ts.map +1 -1
  42. package/dist/client/client-error.js.map +1 -1
  43. package/dist/client/client.d.ts +91 -52
  44. package/dist/client/client.d.ts.map +1 -1
  45. package/dist/client/client.js +25 -9
  46. package/dist/client/client.js.map +1 -1
  47. package/dist/client/client.svelte.d.ts +6 -5
  48. package/dist/client/client.svelte.d.ts.map +1 -1
  49. package/dist/client/client.svelte.js +10 -2
  50. package/dist/client/client.svelte.js.map +1 -1
  51. package/dist/client/internal/ndjson-streaming.js.map +1 -1
  52. package/dist/client/react.d.ts +5 -4
  53. package/dist/client/react.d.ts.map +1 -1
  54. package/dist/client/react.js +104 -12
  55. package/dist/client/react.js.map +1 -1
  56. package/dist/client/solid.d.ts +7 -5
  57. package/dist/client/solid.d.ts.map +1 -1
  58. package/dist/client/solid.js +23 -9
  59. package/dist/client/solid.js.map +1 -1
  60. package/dist/client/vanilla.d.ts +16 -4
  61. package/dist/client/vanilla.d.ts.map +1 -1
  62. package/dist/client/vanilla.js +21 -1
  63. package/dist/client/vanilla.js.map +1 -1
  64. package/dist/client/vue.d.ts +7 -5
  65. package/dist/client/vue.d.ts.map +1 -1
  66. package/dist/client/vue.js +18 -10
  67. package/dist/client/vue.js.map +1 -1
  68. package/dist/id.d.ts +2 -0
  69. package/dist/id.js +3 -0
  70. package/dist/internal/cuid.d.ts +16 -0
  71. package/dist/internal/cuid.d.ts.map +1 -0
  72. package/dist/internal/cuid.js +82 -0
  73. package/dist/internal/cuid.js.map +1 -0
  74. package/dist/mod-client.d.ts +5 -4
  75. package/dist/mod-client.d.ts.map +1 -1
  76. package/dist/mod-client.js +7 -5
  77. package/dist/mod-client.js.map +1 -1
  78. package/dist/mod.d.ts +6 -5
  79. package/dist/mod.js +2 -1
  80. package/dist/runtime.js +1 -1
  81. package/dist/runtime.js.map +1 -1
  82. package/dist/test/test.d.ts +6 -6
  83. package/dist/test/test.d.ts.map +1 -1
  84. package/dist/test/test.js.map +1 -1
  85. package/dist/util/ssr.js.map +1 -1
  86. package/package.json +24 -40
  87. package/src/api/api.test.ts +3 -1
  88. package/src/api/api.ts +6 -0
  89. package/src/api/bind-services.ts +0 -5
  90. package/src/api/error.ts +1 -0
  91. package/src/api/fragment-definition-builder.extend.test.ts +2 -1
  92. package/src/api/fragment-definition-builder.test.ts +2 -1
  93. package/src/api/fragment-definition-builder.ts +49 -124
  94. package/src/api/fragment-instantiator.test.ts +92 -233
  95. package/src/api/fragment-instantiator.ts +228 -196
  96. package/src/api/fragment-services.test.ts +1 -0
  97. package/src/api/internal/path-runtime.test.ts +1 -0
  98. package/src/api/internal/path-type.test.ts +3 -1
  99. package/src/api/internal/route.test.ts +1 -0
  100. package/src/api/request-context-storage.ts +7 -0
  101. package/src/api/request-input-context.test.ts +4 -2
  102. package/src/api/request-input-context.ts +2 -1
  103. package/src/api/request-middleware.test.ts +9 -14
  104. package/src/api/request-middleware.ts +3 -2
  105. package/src/api/request-output-context.test.ts +3 -1
  106. package/src/api/request-output-context.ts +2 -1
  107. package/src/api/route-caller.test.ts +195 -0
  108. package/src/api/route-caller.ts +167 -0
  109. package/src/api/route-handler-input-options.ts +2 -1
  110. package/src/api/route.test.ts +4 -2
  111. package/src/api/route.ts +2 -1
  112. package/src/api/shared-types.ts +2 -1
  113. package/src/client/client-builder.test.ts +4 -2
  114. package/src/client/client-error.test.ts +2 -1
  115. package/src/client/client-error.ts +1 -1
  116. package/src/client/client-types.test.ts +19 -5
  117. package/src/client/client.ssr.test.ts +6 -4
  118. package/src/client/client.svelte.test.ts +18 -9
  119. package/src/client/client.svelte.ts +38 -13
  120. package/src/client/client.test.ts +49 -10
  121. package/src/client/client.ts +291 -141
  122. package/src/client/internal/ndjson-streaming.test.ts +6 -3
  123. package/src/client/internal/ndjson-streaming.ts +1 -0
  124. package/src/client/react.test.ts +176 -6
  125. package/src/client/react.ts +226 -31
  126. package/src/client/solid.test.ts +29 -5
  127. package/src/client/solid.ts +60 -22
  128. package/src/client/vanilla.test.ts +148 -6
  129. package/src/client/vanilla.ts +63 -9
  130. package/src/client/vue.test.ts +223 -84
  131. package/src/client/vue.ts +57 -30
  132. package/src/id.ts +1 -0
  133. package/src/internal/cuid.test.ts +164 -0
  134. package/src/internal/cuid.ts +133 -0
  135. package/src/mod-client.ts +4 -2
  136. package/src/mod.ts +3 -2
  137. package/src/runtime.ts +1 -1
  138. package/src/test/test.test.ts +4 -2
  139. package/src/test/test.ts +7 -9
  140. package/src/util/async.test.ts +1 -0
  141. package/src/util/content-type.test.ts +1 -0
  142. package/src/util/nanostores.test.ts +3 -1
  143. package/src/util/ssr.ts +1 -0
  144. package/tsconfig.json +1 -1
  145. package/tsdown.config.ts +1 -0
  146. 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", () => {
@@ -1017,6 +1019,48 @@ describe("fragment-instantiator", () => {
1017
1019
 
1018
1020
  expect(contextCreationSpy).toHaveBeenCalledTimes(2);
1019
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
+ });
1020
1064
  });
1021
1065
 
1022
1066
  describe("defineService with custom this context", () => {
@@ -1465,60 +1509,18 @@ describe("fragment-instantiator", () => {
1465
1509
  });
1466
1510
  });
1467
1511
 
1468
- describe("linked fragments", () => {
1469
- it("should instantiate linked fragments with the parent fragment", () => {
1470
- interface Config {
1471
- apiKey: string;
1472
- }
1473
-
1474
- // Create a linked fragment definition
1475
- const linkedFragmentDef = defineFragment<Config>("linked-fragment")
1476
- .providesService("linkedService", () => ({
1477
- getValue: () => "from-linked",
1478
- }))
1479
- .build();
1480
-
1481
- // Create main fragment with linked fragment
1482
- const definition = defineFragment<Config>("main-fragment")
1483
- .withLinkedFragment("internal", ({ config, options }) => {
1484
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1485
- })
1486
- .build();
1487
-
1488
- const fragment = instantiate(definition)
1489
- .withConfig({ apiKey: "test-key" })
1490
- .withOptions({ mountRoute: "/api" })
1491
- .build();
1492
-
1493
- // Verify linked fragment exists
1494
- expect(Object.keys(fragment.$internal.linkedFragments).length).toBe(1);
1495
- expect("internal" in fragment.$internal.linkedFragments).toBe(true);
1496
-
1497
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1498
- expect(linkedFragment).toBeDefined();
1499
- expect(linkedFragment?.name).toBe("linked-fragment");
1500
- });
1501
-
1502
- it("should mount internal linked fragment routes under /_internal", async () => {
1503
- const linkedFragmentDef = defineFragment("linked-fragment").build();
1504
- const linkedRoutes = defineRoutes(linkedFragmentDef).create(({ defineRoute }) => [
1505
- defineRoute({
1506
- method: "GET",
1507
- path: "/status",
1508
- handler: async (_input, { json }) => {
1509
- return json({ ok: true });
1510
- },
1511
- }),
1512
- ]);
1513
-
1512
+ describe("internal routes", () => {
1513
+ it("should mount internal routes under /_internal", async () => {
1514
1514
  const definition = defineFragment("main-fragment")
1515
- .withLinkedFragment("_fragno_internal", ({ config, options }) => {
1516
- return instantiate(linkedFragmentDef)
1517
- .withConfig(config)
1518
- .withOptions(options)
1519
- .withRoutes([linkedRoutes])
1520
- .build();
1521
- })
1515
+ .withInternalRoutes([
1516
+ defineRoute({
1517
+ method: "GET",
1518
+ path: "/status",
1519
+ handler: async (_input, { json }) => {
1520
+ return json({ ok: true });
1521
+ },
1522
+ }),
1523
+ ])
1522
1524
  .build();
1523
1525
 
1524
1526
  const fragment = instantiate(definition).withOptions({}).build();
@@ -1528,197 +1530,54 @@ describe("fragment-instantiator", () => {
1528
1530
  await expect(response.json()).resolves.toEqual({ ok: true });
1529
1531
  });
1530
1532
 
1531
- it("should run middleware set on the internal fragment", async () => {
1532
- const linkedFragmentDef = defineFragment("linked-fragment").build();
1533
- const linkedRoutes = defineRoutes(linkedFragmentDef).create(({ defineRoute }) => [
1534
- defineRoute({
1535
- method: "GET",
1536
- path: "/status",
1537
- handler: async (_input, { json }) => {
1538
- return json({ ok: true });
1539
- },
1540
- }),
1541
- ]);
1542
-
1533
+ it("should mount internal routes under the fragment mount route", async () => {
1543
1534
  const definition = defineFragment("main-fragment")
1544
- .withLinkedFragment("_fragno_internal", ({ config, options }) => {
1545
- return instantiate(linkedFragmentDef)
1546
- .withConfig(config)
1547
- .withOptions(options)
1548
- .withRoutes([linkedRoutes])
1549
- .build();
1550
- })
1535
+ .withInternalRoutes([
1536
+ defineRoute({
1537
+ method: "GET",
1538
+ path: "/status",
1539
+ handler: async (_input, { json }) => {
1540
+ return json({ ok: true });
1541
+ },
1542
+ }),
1543
+ ])
1551
1544
  .build();
1552
1545
 
1553
1546
  const fragment = instantiate(definition).withOptions({ mountRoute: "/api" }).build();
1554
1547
 
1555
- const internalFragment = fragment.$internal.linkedFragments._fragno_internal;
1556
- internalFragment.withMiddleware(async ({ ifMatchesRoute }) => {
1557
- const result = await ifMatchesRoute("GET", "/status", async (_input, { json }) => {
1558
- return json({ ok: false, source: "internal-middleware" }, 418);
1559
- });
1560
-
1561
- return result;
1562
- });
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 });
1552
+ });
1563
1553
 
1564
- const response = await fragment.handler(
1565
- new Request("http://localhost/api/_internal/status", {
1566
- method: "GET",
1554
+ it("should resolve internal route factories with services", async () => {
1555
+ const definitionBuilder = defineFragment("main-fragment").providesService(
1556
+ "statusService",
1557
+ () => ({
1558
+ getStatus: () => ({ ok: true }),
1567
1559
  }),
1568
1560
  );
1569
1561
 
1570
- expect(response.status).toBe(418);
1571
- expect(await response.json()).toEqual({ ok: false, source: "internal-middleware" });
1572
- });
1573
-
1574
- it("should pass config and options to linked fragments", () => {
1575
- interface Config {
1576
- value: string;
1577
- }
1578
-
1579
- interface Options extends FragnoPublicConfig {
1580
- customOption: string;
1581
- }
1582
-
1583
- const linkedFragmentDef = defineFragment<Config, Options>("linked-fragment")
1584
- .withDependencies(({ config, options }) => ({
1585
- combined: `${config.value}-${options.customOption}`,
1586
- }))
1587
- .build();
1588
-
1589
- const definition = defineFragment<Config, Options>("main-fragment")
1590
- .withLinkedFragment("internal", ({ config, options }) => {
1591
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1592
- })
1593
- .build();
1594
-
1595
- const fragment = instantiate(definition)
1596
- .withConfig({ value: "config" })
1597
- .withOptions({ customOption: "option", mountRoute: "/api" } as Options)
1598
- .build();
1599
-
1600
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1601
- expect(linkedFragment?.$internal.deps).toEqual({
1602
- combined: "config-option",
1603
- });
1604
- });
1605
-
1606
- it("should allow linked fragments to provide services", () => {
1607
- const linkedFragmentDef = defineFragment("linked-fragment")
1608
- .providesService("settingsService", () => ({
1609
- get: (key: string) => `value-for-${key}`,
1610
- set: (key: string, value: string) => {
1611
- console.log(`Setting ${key} = ${value}`);
1612
- },
1613
- }))
1614
- .build();
1615
-
1616
- const definition = defineFragment("main-fragment")
1617
- .withLinkedFragment("internal", ({ config, options }) => {
1618
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1619
- })
1620
- .build();
1621
-
1622
- const fragment = instantiate(definition).withOptions({}).build();
1623
-
1624
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1625
- expect(linkedFragment?.services.settingsService).toBeDefined();
1626
- expect(linkedFragment?.services.settingsService.get("test")).toBe("value-for-test");
1627
- });
1628
-
1629
- it("should support multiple linked fragments", () => {
1630
- const linkedFragmentDef1 = defineFragment("linked-fragment-1")
1631
- .providesService("service1", () => ({ method: () => "service1" }))
1632
- .build();
1633
-
1634
- const linkedFragmentDef2 = defineFragment("linked-fragment-2")
1635
- .providesService("service2", () => ({ method: () => "service2" }))
1636
- .build();
1637
-
1638
- const definition = defineFragment("main-fragment")
1639
- .withLinkedFragment("internal1", ({ config, options }) => {
1640
- return instantiate(linkedFragmentDef1).withConfig(config).withOptions(options).build();
1641
- })
1642
- .withLinkedFragment("internal2", ({ config, options }) => {
1643
- return instantiate(linkedFragmentDef2).withConfig(config).withOptions(options).build();
1644
- })
1645
- .build();
1646
-
1647
- const fragment = instantiate(definition).withOptions({}).build();
1648
-
1649
- expect(Object.keys(fragment.$internal.linkedFragments).length).toBe(2);
1650
- expect("internal1" in fragment.$internal.linkedFragments).toBe(true);
1651
- expect("internal2" in fragment.$internal.linkedFragments).toBe(true);
1652
-
1653
- const linked1 = fragment.$internal.linkedFragments.internal1;
1654
- const linked2 = fragment.$internal.linkedFragments.internal2;
1655
-
1656
- expect(linked1?.services.service1.method()).toBe("service1");
1657
- expect(linked2?.services.service2.method()).toBe("service2");
1658
- });
1659
-
1660
- it("should pass service dependencies to linked fragments", () => {
1661
- interface ExternalService {
1662
- getValue: () => string;
1663
- }
1664
-
1665
- const externalService: ExternalService = {
1666
- getValue: () => "external-value",
1667
- };
1668
-
1669
- const linkedFragmentDef = defineFragment("linked-fragment")
1670
- .usesService<"externalService", ExternalService>("externalService")
1671
- .providesService("linkedService", ({ serviceDeps }) => ({
1672
- getFromExternal: () => serviceDeps.externalService.getValue(),
1673
- }))
1674
- .build();
1675
-
1676
- const definition = defineFragment("main-fragment")
1677
- .usesService<"externalService", ExternalService>("externalService")
1678
- .withLinkedFragment("internal", ({ config, options, serviceDependencies }) => {
1679
- return instantiate(linkedFragmentDef)
1680
- .withConfig(config)
1681
- .withOptions(options)
1682
- .withServices(serviceDependencies!)
1683
- .build();
1684
- })
1685
- .build();
1686
-
1687
- const fragment = instantiate(definition)
1688
- .withOptions({})
1689
- .withServices({ externalService })
1690
- .build();
1691
-
1692
- const linkedFragment = fragment.$internal.linkedFragments.internal;
1693
- expect(linkedFragment?.services.linkedService.getFromExternal()).toBe("external-value");
1694
- });
1695
-
1696
- it("should expose linked fragment services as private services", () => {
1697
- const linkedFragmentDef = defineFragment("linked-fragment")
1698
- .providesService("linkedService", () => ({
1699
- getValue: () => "from-linked",
1700
- }))
1701
- .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
+ );
1702
1573
 
1703
- const definition = defineFragment("main-fragment")
1704
- .withLinkedFragment("internal", ({ config, options }) => {
1705
- return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
1706
- })
1707
- .providesService("mainService", ({ privateServices }) => ({
1708
- getLinkedValue: () => {
1709
- return privateServices.linkedService.getValue();
1710
- },
1711
- }))
1712
- .build();
1574
+ const definition = definitionBuilder.withInternalRoutes([internalRoutes]).build();
1713
1575
 
1714
1576
  const fragment = instantiate(definition).withOptions({}).build();
1715
1577
 
1716
- // The main service can access linked fragment services via privateServices
1717
- expect(fragment.services.mainService.getLinkedValue()).toBe("from-linked");
1718
-
1719
- // Linked fragment services are NOT directly exposed on the main fragment
1720
- // @ts-expect-error - Linked fragment service should not be accessible
1721
- 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 });
1722
1581
  });
1723
1582
  });
1724
1583