@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,454 +1,268 @@
1
- import { describe, it, expect, expectTypeOf, assert } from "vitest";
2
- import { createFragmentForTest } from "./test";
3
- import { defineFragment } from "../api/fragment-builder";
4
- import { defineRoute, defineRoutes } from "../api/route";
1
+ import { describe, it, expect } from "vitest";
2
+ import { createFragmentForTest, withTestUtils } from "./test";
3
+ import { defineFragment } from "../api/fragment-definition-builder";
4
+ import { defineRoutes } from "../api/route";
5
5
  import { z } from "zod";
6
6
 
7
- describe("createFragmentForTest", () => {
8
- it("should create a test fragment with config only", () => {
9
- const fragment = defineFragment<{ apiKey: string }>("test");
10
- const testFragment = createFragmentForTest(fragment, [], {
7
+ describe("withTestUtils extension", () => {
8
+ it("should expose deps via services.deps", () => {
9
+ const definition = defineFragment<{ apiKey: string }>("test")
10
+ .withDependencies(({ config }) => ({
11
+ client: { apiKey: config.apiKey },
12
+ }))
13
+ .extend(withTestUtils())
14
+ .build();
15
+
16
+ const fragment = createFragmentForTest(definition, [], {
11
17
  config: { apiKey: "test-key" },
12
18
  });
13
19
 
14
- expect(testFragment.config).toEqual({ apiKey: "test-key" });
15
- expect(testFragment.deps).toEqual({});
16
- expect(testFragment.services).toEqual({});
20
+ expect(fragment.services.deps).toEqual({ client: { apiKey: "test-key" } });
17
21
  });
18
22
 
19
- it("should create deps from fragment definition", () => {
20
- const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
21
- client: { apiKey: config.apiKey },
22
- }));
23
+ it("should work with empty deps", () => {
24
+ const definition = defineFragment<{ value: number }>("test").extend(withTestUtils()).build();
23
25
 
24
- const testFragment = createFragmentForTest(fragment, [], {
25
- config: { apiKey: "test-key" },
26
+ const fragment = createFragmentForTest(definition, [], {
27
+ config: { value: 5 },
26
28
  });
27
29
 
28
- expect(testFragment.deps).toEqual({ client: { apiKey: "test-key" } });
30
+ expect(fragment.services.deps).toEqual({});
29
31
  });
30
32
 
31
- it("should override deps when provided", () => {
32
- const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
33
- client: { apiKey: config.apiKey },
34
- }));
33
+ it("should preserve existing base services", () => {
34
+ const definition = defineFragment<{ x: number; y: number }>("test")
35
+ .withDependencies(({ config }) => ({
36
+ x: config.x,
37
+ y: config.y,
38
+ }))
39
+ .providesBaseService(({ deps, defineService }) =>
40
+ defineService({
41
+ add: () => deps.x + deps.y,
42
+ multiply: () => deps.x * deps.y,
43
+ }),
44
+ )
45
+ .extend(withTestUtils())
46
+ .build();
35
47
 
36
- const testFragment = createFragmentForTest(fragment, [], {
37
- config: { apiKey: "test-key" },
38
- deps: { client: { apiKey: "override-key" } },
48
+ const fragment = createFragmentForTest(definition, [], {
49
+ config: { x: 3, y: 4 },
39
50
  });
40
51
 
41
- expect(testFragment.deps).toEqual({ client: { apiKey: "override-key" } });
52
+ // Both existing services and deps should be available
53
+ expect(fragment.services.add()).toBe(7);
54
+ expect(fragment.services.multiply()).toBe(12);
55
+ expect(fragment.services.deps).toEqual({ x: 3, y: 4 });
42
56
  });
43
57
 
44
- it("should create services from fragment definition", () => {
45
- const fragment = defineFragment<{ apiKey: string }>("test")
58
+ it("should work with named services", () => {
59
+ const definition = defineFragment<{ value: number }>("test")
46
60
  .withDependencies(({ config }) => ({
47
- client: { apiKey: config.apiKey },
61
+ value: config.value,
48
62
  }))
49
- .providesService(({ deps, defineService }) =>
63
+ .providesService("math", ({ deps, defineService }) =>
50
64
  defineService({
51
- getApiKey: () => deps.client.apiKey,
65
+ double: () => deps.value * 2,
52
66
  }),
53
- );
67
+ )
68
+ .extend(withTestUtils())
69
+ .build();
54
70
 
55
- const testFragment = createFragmentForTest(fragment, [], {
56
- config: { apiKey: "test-key" },
71
+ const fragment = createFragmentForTest(definition, [], {
72
+ config: { value: 21 },
57
73
  });
58
74
 
59
- expect(testFragment.services.getApiKey()).toBe("test-key");
75
+ expect(fragment.services.deps).toEqual({ value: 21 });
76
+ expect(fragment.services.math.double()).toBe(42);
60
77
  });
61
78
 
62
- it("should override services when provided", () => {
63
- const fragment = defineFragment<{ apiKey: string }>("test")
79
+ it("should work with service dependencies", () => {
80
+ type Logger = { log: (msg: string) => void };
81
+
82
+ const logs: string[] = [];
83
+ const mockLogger: Logger = {
84
+ log: (msg: string) => logs.push(msg),
85
+ };
86
+
87
+ const definition = defineFragment<{ name: string }>("test")
64
88
  .withDependencies(({ config }) => ({
65
- client: { apiKey: config.apiKey },
89
+ name: config.name,
66
90
  }))
67
- .providesService(({ deps, defineService }) =>
91
+ .usesService<"logger", Logger>("logger")
92
+ .providesBaseService(({ deps, serviceDeps, defineService }) =>
68
93
  defineService({
69
- getApiKey: () => deps.client.apiKey,
94
+ greet: () => {
95
+ serviceDeps.logger.log(`Hello, ${deps.name}!`);
96
+ },
70
97
  }),
71
- );
98
+ )
99
+ .extend(withTestUtils())
100
+ .build();
101
+
102
+ const fragment = createFragmentForTest(definition, [], {
103
+ config: { name: "World" },
104
+ serviceImplementations: {
105
+ logger: mockLogger,
106
+ },
107
+ });
72
108
 
73
- const testFragment = createFragmentForTest(fragment, [], {
74
- config: { apiKey: "test-key" },
75
- services: { getApiKey: () => "override-key" },
109
+ expect(fragment.services.deps).toEqual({ name: "World" });
110
+ fragment.services.greet();
111
+ expect(logs).toEqual(["Hello, World!"]);
112
+ });
113
+
114
+ it("should work with request storage and context", () => {
115
+ type RequestStorage = { counter: number };
116
+
117
+ const definition = defineFragment<{ initialValue: number }>("test")
118
+ .withDependencies(({ config }) => ({
119
+ initialValue: config.initialValue,
120
+ }))
121
+ .withRequestStorage(
122
+ ({ deps }): RequestStorage => ({
123
+ counter: deps.initialValue,
124
+ }),
125
+ )
126
+ .withThisContext(({ storage }) => {
127
+ const ctx = {
128
+ getCounter: () => storage.getStore()?.counter ?? 0,
129
+ };
130
+ return { serviceContext: ctx, handlerContext: ctx };
131
+ })
132
+ .extend(withTestUtils())
133
+ .build();
134
+
135
+ const fragment = createFragmentForTest(definition, [], {
136
+ config: { initialValue: 10 },
76
137
  });
77
138
 
78
- expect(testFragment.services.getApiKey()).toBe("override-key");
139
+ expect(fragment.services.deps).toEqual({ initialValue: 10 });
79
140
  });
141
+ });
80
142
 
81
- it("should initialize route factories with fragment context", async () => {
143
+ describe("createFragmentForTest", () => {
144
+ it("should instantiate fragments with routes", async () => {
82
145
  type Config = { multiplier: number };
83
- type Deps = { dep: string };
84
- type Services = { multiply: (x: number) => number };
85
146
 
86
- const fragment = defineFragment<Config>("test")
87
- .withDependencies(() => ({ dep: "value" }))
88
- .providesService(({ config, defineService }) =>
147
+ const definition = defineFragment<Config>("test")
148
+ .withDependencies(({ config }) => ({ multiplier: config.multiplier }))
149
+ .providesService("math", ({ deps, defineService }) =>
89
150
  defineService({
90
- multiply: (x: number) => x * config.multiplier,
151
+ multiply: (x: number) => x * deps.multiplier,
91
152
  }),
92
- );
153
+ )
154
+ .extend(withTestUtils())
155
+ .build();
93
156
 
94
- const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
157
+ const routeFactory = defineRoutes(definition).create(({ services, defineRoute }) => [
95
158
  defineRoute({
96
159
  method: "GET",
97
160
  path: "/multiply/:num",
98
- outputSchema: z.object({ result: z.number() }),
161
+ outputSchema: z.object({ result: z.number(), deps: z.object({ multiplier: z.number() }) }),
99
162
  handler: async ({ pathParams }, { json }) => {
100
163
  const { num } = pathParams;
101
- return json({ result: services.multiply(Number(num)) });
164
+ return json({
165
+ result: services.math.multiply(Number(num)),
166
+ deps: services.deps,
167
+ });
102
168
  },
103
169
  }),
104
170
  ]);
105
171
 
106
- const routes = [routeFactory] as const;
107
-
108
- const testFragment = createFragmentForTest(fragment, routes, {
172
+ const fragment = createFragmentForTest(definition, [routeFactory], {
109
173
  config: { multiplier: 3 },
110
174
  });
111
175
 
112
- // Test that the route was initialized with the correct services
113
- const response = await testFragment.callRoute("GET", "/multiply/:num", {
114
- // ^?
176
+ const response = await fragment.callRoute("GET", "/multiply/:num", {
115
177
  pathParams: { num: "5" },
116
178
  });
117
- expect(response.type).toBe("json");
118
- if (response.type === "json") {
119
- expect(response.data).toEqual({ result: 15 }); // 5 * 3
120
- expectTypeOf(response.data).toMatchObjectType<{ result: number }>();
121
- }
122
- });
123
- });
124
-
125
- describe("fragment.callRoute", () => {
126
- it("should handle JSON response", async () => {
127
- const fragment = defineFragment<{ apiKey: string }>("test");
128
-
129
- const route = defineRoute({
130
- method: "GET",
131
- path: "/test",
132
- outputSchema: z.object({ message: z.string() }),
133
- handler: async (_ctx, { json }) => {
134
- return json({ message: "hello" });
135
- },
136
- });
137
-
138
- const testFragment = createFragmentForTest(fragment, [route], {
139
- config: { apiKey: "test-key" },
140
- });
141
-
142
- const response = await testFragment.callRoute("GET", "/test");
143
179
 
144
180
  expect(response.type).toBe("json");
145
181
  if (response.type === "json") {
146
- expect(response.status).toBe(200);
147
- expect(response.data).toEqual({ message: "hello" });
148
- expect(response.headers).toBeInstanceOf(Headers);
149
- expectTypeOf(response.data).toMatchObjectType<{ message: string }>();
150
- }
151
- });
152
-
153
- it("should handle empty response", async () => {
154
- const fragment = defineFragment<{ apiKey: string }>("test");
155
-
156
- const route = defineRoute({
157
- method: "DELETE",
158
- path: "/test",
159
- handler: async (_ctx, { empty }) => {
160
- return empty(204);
161
- },
162
- });
163
-
164
- const testFragment = createFragmentForTest(fragment, [route], {
165
- config: { apiKey: "test-key" },
166
- });
167
-
168
- const response = await testFragment.callRoute("DELETE", "/test");
169
-
170
- expect(response.type).toBe("empty");
171
- if (response.type === "empty") {
172
- expect(response.status).toBe(204);
173
- expect(response.headers).toBeInstanceOf(Headers);
174
- }
175
- });
176
-
177
- it("should handle error response", async () => {
178
- const fragment = defineFragment<{ apiKey: string }>("test");
179
-
180
- const route = defineRoute({
181
- method: "GET",
182
- path: "/test",
183
- errorCodes: ["NOT_FOUND"] as const,
184
- handler: async (_ctx, { error }) => {
185
- return error({ message: "Not found", code: "NOT_FOUND" }, 404);
186
- },
187
- });
188
-
189
- const testFragment = createFragmentForTest(fragment, [route], {
190
- config: { apiKey: "test-key" },
191
- });
192
-
193
- const response = await testFragment.callRoute("GET", "/test");
194
-
195
- expect(response.type).toBe("error");
196
- if (response.type === "error") {
197
- expect(response.status).toBe(404);
198
- expect(response.error).toEqual({ message: "Not found", code: "NOT_FOUND" });
199
- expect(response.headers).toBeInstanceOf(Headers);
200
- }
201
- });
202
-
203
- it("should handle JSON stream response", async () => {
204
- const fragment = defineFragment<{ apiKey: string }>("test");
205
-
206
- const route = defineRoute({
207
- method: "GET",
208
- path: "/test/stream",
209
- outputSchema: z.array(z.object({ value: z.number() })),
210
- handler: async (_ctx, { jsonStream }) => {
211
- return jsonStream(async (stream) => {
212
- for (let i = 1; i <= 5; i++) {
213
- await stream.write({ value: i });
214
- }
215
- });
216
- },
217
- });
218
-
219
- const testFragment = createFragmentForTest(fragment, [route], {
220
- config: { apiKey: "test-key" },
221
- });
222
-
223
- const response = await testFragment.callRoute("GET", "/test/stream");
224
-
225
- expect(response.type).toBe("jsonStream");
226
- if (response.type === "jsonStream") {
227
- expect(response.status).toBe(200);
228
- expect(response.headers).toBeInstanceOf(Headers);
229
- expect(response.headers.get("content-type")).toContain("application/x-ndjson");
230
-
231
- const items = [];
232
- for await (const item of response.stream) {
233
- items.push(item);
234
- }
235
-
236
- expect(items).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }]);
237
- expectTypeOf(items[0]).toMatchObjectType<{ value: number }>();
182
+ expect(response.data).toEqual({
183
+ result: 15,
184
+ deps: { multiplier: 3 },
185
+ });
238
186
  }
239
187
  });
240
188
 
241
- it("should handle route factory created with defineRoutes", async () => {
242
- const fragment = defineFragment<{ apiKey: string }>("test").providesService(() => ({
243
- getGreeting: (name: string) => `Hello, ${name}!`,
244
- getCount: () => 42,
245
- }));
246
-
247
- type Config = { apiKey: string };
248
- type Deps = {};
249
- type Services = { getGreeting: (name: string) => string; getCount: () => number };
189
+ it("should support request context in routes", async () => {
190
+ type RequestStorage = { counter: number };
250
191
 
251
- const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
252
- defineRoute({
253
- method: "GET",
254
- path: "/greeting/:name",
255
- outputSchema: z.object({ message: z.string() }),
256
- handler: async ({ pathParams }, { json }) => {
257
- return json({ message: services.getGreeting(pathParams.name) });
258
- },
259
- }),
192
+ const definition = defineFragment<{ initialValue: number }>("test")
193
+ .withDependencies(({ config }) => ({
194
+ initialValue: config.initialValue,
195
+ }))
196
+ .extend(withTestUtils())
197
+ .withRequestStorage(
198
+ ({ deps }): RequestStorage => ({
199
+ counter: deps.initialValue,
200
+ }),
201
+ )
202
+ .withThisContext(({ storage }) => {
203
+ const ctx = {
204
+ getCounter: () => storage.getStore()?.counter ?? 0,
205
+ incrementCounter: () => {
206
+ const store = storage.getStore();
207
+ if (store) {
208
+ store.counter++;
209
+ }
210
+ },
211
+ };
212
+ return { serviceContext: ctx, handlerContext: ctx };
213
+ })
214
+ .build();
215
+
216
+ const routeFactory = defineRoutes(definition).create(({ defineRoute }) => [
260
217
  defineRoute({
261
- method: "GET",
262
- path: "/count",
218
+ method: "POST",
219
+ path: "/increment",
263
220
  outputSchema: z.object({ count: z.number() }),
264
- handler: async (_ctx, { json }) => {
265
- return json({ count: services.getCount() });
221
+ handler: async function (_ctx, { json }) {
222
+ this.incrementCounter();
223
+ return json({ count: this.getCounter() });
266
224
  },
267
225
  }),
268
226
  ]);
269
227
 
270
- const testFragment = createFragmentForTest(fragment, [routeFactory], {
271
- config: { apiKey: "test-key" },
272
- });
273
-
274
- // Test first route
275
- const greetingResponse = await testFragment.callRoute("GET", "/greeting/:name", {
276
- pathParams: { name: "World" },
277
- });
278
-
279
- console.log(greetingResponse);
280
- assert(greetingResponse.type === "json");
281
- expect(greetingResponse.data).toEqual({ message: "Hello, World!" });
282
-
283
- // Test second route
284
- const countResponse = await testFragment.callRoute("GET", "/count");
285
-
286
- expect(countResponse.type).toBe("json");
287
- if (countResponse.type === "json") {
288
- expect(countResponse.data).toEqual({ count: 42 });
289
- }
290
- });
291
-
292
- it("should handle path parameters", async () => {
293
- const fragment = defineFragment<{}>("test");
294
-
295
- const route = defineRoute({
296
- method: "GET",
297
- path: "/users/:id",
298
- outputSchema: z.object({ userId: z.string() }),
299
- handler: async ({ pathParams }, { json }) => {
300
- return json({ userId: pathParams.id });
301
- },
302
- });
303
-
304
- const testFragment = createFragmentForTest(fragment, [route], {
305
- config: {},
306
- });
307
-
308
- const response = await testFragment.callRoute("GET", "/users/:id", {
309
- pathParams: { id: "123" },
310
- });
311
-
312
- expect(response.type).toBe("json");
313
- if (response.type === "json") {
314
- expect(response.data).toEqual({ userId: "123" });
315
- }
316
- });
317
-
318
- it("should handle query parameters", async () => {
319
- const fragment = defineFragment<{}>("test");
320
-
321
- const route = defineRoute({
322
- method: "GET",
323
- path: "/search",
324
- outputSchema: z.object({ query: z.string() }),
325
- handler: async ({ query }, { json }) => {
326
- return json({ query: query.get("q") || "" });
327
- },
328
- });
329
-
330
- const testFragment = createFragmentForTest(fragment, [route], {
331
- config: {},
332
- });
333
-
334
- const response = await testFragment.callRoute("GET", "/search", {
335
- query: { q: "test" },
336
- });
337
-
338
- expect(response.type).toBe("json");
339
- if (response.type === "json") {
340
- expect(response.data).toEqual({ query: "test" });
341
- }
342
- });
343
-
344
- it("should handle request body", async () => {
345
- const fragment = defineFragment<{}>("test");
346
-
347
- const route = defineRoute({
348
- method: "POST",
349
- path: "/users",
350
- inputSchema: z.object({ name: z.string(), email: z.string() }),
351
- outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
352
- handler: async ({ input }, { json }) => {
353
- if (input) {
354
- const data = await input.valid();
355
- return json({ id: 1, name: data.name, email: data.email });
356
- }
357
- return json({ id: 1, name: "", email: "" });
358
- },
359
- });
360
-
361
- const testFragment = createFragmentForTest(fragment, [route], {
362
- config: {},
228
+ const fragment = createFragmentForTest(definition, [routeFactory], {
229
+ config: { initialValue: 10 },
363
230
  });
364
231
 
365
- const response = await testFragment.callRoute("POST", "/users", {
366
- body: { name: "John", email: "john@example.com" },
367
- });
368
-
369
- expect(response.type).toBe("json");
370
- if (response.type === "json") {
371
- expect(response.data).toEqual({ id: 1, name: "John", email: "john@example.com" });
232
+ // Each request should have its own isolated storage
233
+ const response1 = await fragment.callRoute("POST", "/increment");
234
+ expect(response1.type).toBe("json");
235
+ if (response1.type === "json") {
236
+ expect(response1.data).toEqual({ count: 11 });
372
237
  }
373
- });
374
-
375
- it("should have the right types", () => {
376
- const fragment = defineFragment<{}>("test");
377
-
378
- const route = defineRoute({
379
- method: "POST",
380
- path: "/users",
381
- inputSchema: z.object({ name: z.string(), email: z.string() }),
382
- outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
383
- handler: async ({ input }, { json }) => {
384
- if (input) {
385
- const data = await input.valid();
386
- return json({ id: 1, name: data.name, email: data.email });
387
- }
388
- return json({ id: 1, name: "", email: "" });
389
- },
390
- });
391
-
392
- const testFragment = createFragmentForTest(fragment, [route], {
393
- config: {},
394
- });
395
-
396
- // Check what type body is expected to have
397
- type InputOptions = Parameters<typeof testFragment.callRoute<"POST", "/users">>[2];
398
- type BodyType = NonNullable<NonNullable<InputOptions>["body"]>;
399
-
400
- expectTypeOf<BodyType>().toMatchObjectType<{ name: string; email: string }>();
401
- });
402
-
403
- it("should handle custom headers", async () => {
404
- const fragment = defineFragment<{}>("test");
405
-
406
- const route = defineRoute({
407
- method: "GET",
408
- path: "/test",
409
- outputSchema: z.object({ authHeader: z.string() }),
410
- handler: async ({ headers }, { json }) => {
411
- return json({ authHeader: headers.get("authorization") || "" });
412
- },
413
- });
414
-
415
- const testFragment = createFragmentForTest(fragment, [route], {
416
- config: {},
417
- });
418
-
419
- const response = await testFragment.callRoute("GET", "/test", {
420
- headers: { authorization: "Bearer token" },
421
- });
422
238
 
423
- expect(response.type).toBe("json");
424
- if (response.type === "json") {
425
- expect(response.data).toEqual({ authHeader: "Bearer token" });
239
+ // New request should start fresh
240
+ const response2 = await fragment.callRoute("POST", "/increment");
241
+ expect(response2.type).toBe("json");
242
+ if (response2.type === "json") {
243
+ expect(response2.data).toEqual({ count: 11 }); // Not 12!
426
244
  }
427
245
  });
428
246
 
429
- it("should properly type path params", async () => {
430
- const fragment = defineFragment<{}>("test");
431
-
432
- const route = defineRoute({
433
- method: "GET",
434
- path: "/orgs/:orgId/users/:userId",
435
- outputSchema: z.object({ orgId: z.string(), userId: z.string() }),
436
- handler: async ({ pathParams }, { json }) => {
437
- return json({ orgId: pathParams.orgId, userId: pathParams.userId });
438
- },
439
- });
440
-
441
- const testFragment = createFragmentForTest(fragment, [route], {
442
- config: {},
443
- });
247
+ it("should work without withTestUtils (no deps exposed)", () => {
248
+ const definition = defineFragment<{ value: number }>("test")
249
+ .withDependencies(({ config }) => ({
250
+ value: config.value,
251
+ }))
252
+ .providesBaseService(({ deps, defineService }) =>
253
+ defineService({
254
+ getValue: () => deps.value,
255
+ }),
256
+ )
257
+ .build();
444
258
 
445
- const response = await testFragment.callRoute("GET", "/orgs/:orgId/users/:userId", {
446
- pathParams: { orgId: "123", userId: "456" },
259
+ const fragment = createFragmentForTest(definition, [], {
260
+ config: { value: 42 },
447
261
  });
448
262
 
449
- expect(response.type).toBe("json");
450
- if (response.type === "json") {
451
- expect(response.data).toEqual({ orgId: "123", userId: "456" });
452
- }
263
+ // Services should work
264
+ expect(fragment.services.getValue()).toBe(42);
265
+ // But deps should not be exposed
266
+ expect(fragment.services).not.toHaveProperty("deps");
453
267
  });
454
268
  });