@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,14 +1,16 @@
1
1
  import { afterEach, assert, beforeEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import { atom, computed, effect } from "nanostores";
2
4
  import { z } from "zod";
5
+
6
+ import { defineFragment } from "../api/fragment-definition-builder";
7
+ import { RequestOutputContext } from "../api/request-output-context";
3
8
  import { defineRoute } from "../api/route";
4
- import { buildUrl, createClientBuilder, getCacheKey, isGetHook, isMutatorHook } from "./client";
5
- import { useFragno } from "./vanilla";
6
9
  import { createAsyncIteratorFromCallback, waitForAsyncIterator } from "../util/async";
10
+ import { buildUrl, createClientBuilder, getCacheKey, isGetHook, isMutatorHook } from "./client";
7
11
  import type { FragnoPublicClientConfig } from "./client";
8
- import { atom, computed, effect } from "nanostores";
9
- import { defineFragment } from "../api/fragment-definition-builder";
10
- import { RequestOutputContext } from "../api/request-output-context";
11
12
  import { FragnoClientUnknownApiError } from "./client-error";
13
+ import { useFragno } from "./vanilla";
12
14
 
13
15
  // Mock fetch globally
14
16
  global.fetch = vi.fn();
@@ -111,6 +113,59 @@ describe("getCacheKey", () => {
111
113
  });
112
114
  });
113
115
 
116
+ describe("FormData utilities", () => {
117
+ // These tests verify the internal FormData handling behavior
118
+
119
+ test("prepareRequestBody should JSON-stringify regular objects", () => {
120
+ // We can't test internal functions directly, but we can test through the mutator behavior
121
+ // This is a placeholder to document expected behavior
122
+ const body = { name: "test", value: 123 };
123
+ expect(JSON.stringify(body)).toBe('{"name":"test","value":123}');
124
+ });
125
+
126
+ test("FormData should be detected correctly", () => {
127
+ const formData = new FormData();
128
+ formData.append("file", new Blob(["test"]), "test.txt");
129
+
130
+ expect(formData instanceof FormData).toBe(true);
131
+ expect({} instanceof FormData).toBe(false);
132
+ // Note: null instanceof X is a TS error, so we test with a nullable variable
133
+ const nullValue: unknown = null;
134
+ expect(nullValue instanceof FormData).toBe(false);
135
+ });
136
+
137
+ test("File and Blob should be detected correctly", () => {
138
+ const file = new File(["content"], "test.txt", { type: "text/plain" });
139
+ const blob = new Blob(["content"], { type: "text/plain" });
140
+
141
+ expect(file instanceof File).toBe(true);
142
+ expect(file instanceof Blob).toBe(true);
143
+ expect(blob instanceof Blob).toBe(true);
144
+ expect(blob instanceof File).toBe(false);
145
+ });
146
+
147
+ test("toFormData should convert object with files to FormData", () => {
148
+ const file = new File(["content"], "test.txt", { type: "text/plain" });
149
+ const formData = new FormData();
150
+ formData.append("file", file, file.name);
151
+ formData.append("description", "A test file");
152
+
153
+ expect(formData.get("file")).toBeInstanceOf(File);
154
+ expect(formData.get("description")).toBe("A test file");
155
+ });
156
+
157
+ test("FormData can contain multiple files", () => {
158
+ const file1 = new File(["content1"], "test1.txt", { type: "text/plain" });
159
+ const file2 = new File(["content2"], "test2.txt", { type: "text/plain" });
160
+ const formData = new FormData();
161
+ formData.append("files", file1, file1.name);
162
+ formData.append("files", file2, file2.name);
163
+
164
+ const files = formData.getAll("files");
165
+ expect(files).toHaveLength(2);
166
+ });
167
+ });
168
+
114
169
  describe("invalidation", () => {
115
170
  const testFragment = defineFragment("test-fragment").build();
116
171
  const testRoutes = [
@@ -1271,6 +1326,125 @@ describe("createMutator", () => {
1271
1326
 
1272
1327
  expect(result).toBeUndefined();
1273
1328
  });
1329
+
1330
+ test("body is optional when inputSchema allows undefined", async () => {
1331
+ const testFragment = defineFragment("test-fragment").build();
1332
+ const testRoutes = [
1333
+ defineRoute({
1334
+ method: "POST",
1335
+ path: "/sign-out",
1336
+ inputSchema: z.object({ sessionId: z.string().optional() }).optional(),
1337
+ outputSchema: z.object({ success: z.boolean() }),
1338
+ handler: async (_ctx, { empty }) => empty(),
1339
+ }),
1340
+ ] as const;
1341
+
1342
+ vi.mocked(global.fetch).mockImplementation(async () => {
1343
+ return new Response(null, { status: 204 });
1344
+ });
1345
+
1346
+ const cb = createClientBuilder(testFragment, clientConfig, testRoutes);
1347
+ const signOut = cb.createMutator("POST", "/sign-out");
1348
+
1349
+ const result = await signOut.mutateQuery({});
1350
+ expect(result).toBeUndefined();
1351
+
1352
+ const storeResult = await signOut.mutatorStore.mutate({});
1353
+ expect(storeResult).toBeUndefined();
1354
+ });
1355
+
1356
+ test("should send octet-stream body without wrapping", async () => {
1357
+ const testFragment = defineFragment("test-fragment").build();
1358
+ const testRoutes = [
1359
+ defineRoute({
1360
+ method: "PUT",
1361
+ path: "/upload",
1362
+ contentType: "application/octet-stream",
1363
+ inputSchema: z.unknown(),
1364
+ handler: async (_ctx, { empty }) => empty(),
1365
+ }),
1366
+ ] as const;
1367
+
1368
+ vi.mocked(global.fetch).mockImplementation(async () => {
1369
+ return new Response(null, { status: 204 });
1370
+ });
1371
+
1372
+ const cb = createClientBuilder(testFragment, clientConfig, testRoutes);
1373
+ const upload = cb.createMutator("PUT", "/upload");
1374
+ const body = new Uint8Array([1, 2, 3, 4]);
1375
+
1376
+ await upload.mutateQuery({ body });
1377
+
1378
+ const [_url, options] = vi.mocked(global.fetch).mock.calls[0];
1379
+ const headers = options?.headers as Record<string, string> | undefined;
1380
+
1381
+ expect(headers?.["Content-Type"]).toBe("application/octet-stream");
1382
+ expect(options?.body).toBe(body);
1383
+ });
1384
+
1385
+ test("should send ReadableStream body with duplex for octet-stream", async () => {
1386
+ const testFragment = defineFragment("test-fragment").build();
1387
+ const testRoutes = [
1388
+ defineRoute({
1389
+ method: "PUT",
1390
+ path: "/upload",
1391
+ contentType: "application/octet-stream",
1392
+ inputSchema: z.unknown(),
1393
+ handler: async (_ctx, { empty }) => empty(),
1394
+ }),
1395
+ ] as const;
1396
+
1397
+ let capturedOptions: (RequestInit & { duplex?: "half" }) | undefined;
1398
+ let capturedBodyText = "";
1399
+
1400
+ vi.mocked(global.fetch).mockImplementation(async (_url, options) => {
1401
+ capturedOptions = options as RequestInit & { duplex?: "half" };
1402
+ const body = capturedOptions?.body;
1403
+
1404
+ if (!(body instanceof ReadableStream)) {
1405
+ throw new Error("Expected ReadableStream body");
1406
+ }
1407
+
1408
+ const reader = body.getReader();
1409
+ const chunks: Uint8Array[] = [];
1410
+ while (true) {
1411
+ const { done, value } = await reader.read();
1412
+ if (done) {
1413
+ break;
1414
+ }
1415
+ if (value) {
1416
+ chunks.push(value);
1417
+ }
1418
+ }
1419
+
1420
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1421
+ const combined = new Uint8Array(totalLength);
1422
+ let offset = 0;
1423
+ for (const chunk of chunks) {
1424
+ combined.set(chunk, offset);
1425
+ offset += chunk.length;
1426
+ }
1427
+ capturedBodyText = new TextDecoder().decode(combined);
1428
+
1429
+ return new Response(null, { status: 204 });
1430
+ });
1431
+
1432
+ const cb = createClientBuilder(testFragment, clientConfig, testRoutes);
1433
+ const upload = cb.createMutator("PUT", "/upload");
1434
+ const body = new ReadableStream<Uint8Array>({
1435
+ start(controller) {
1436
+ controller.enqueue(new TextEncoder().encode("streamed body"));
1437
+ controller.close();
1438
+ },
1439
+ });
1440
+
1441
+ await upload.mutateQuery({ body });
1442
+
1443
+ const headers = capturedOptions?.headers as Record<string, string> | undefined;
1444
+ expect(headers?.["Content-Type"]).toBe("application/octet-stream");
1445
+ expect(capturedOptions?.duplex).toBe("half");
1446
+ expect(capturedBodyText).toBe("streamed body");
1447
+ });
1274
1448
  });
1275
1449
 
1276
1450
  describe("createMutator - streaming", () => {
@@ -1997,6 +2171,55 @@ describe("Custom Fetcher Configuration", () => {
1997
2171
  expect(url4).toBe("http://localhost:3000/api/test-fragment/users/456?include=posts");
1998
2172
  });
1999
2173
 
2174
+ test("public mountRoute is used for hooks", async () => {
2175
+ let capturedUrl: string | undefined;
2176
+
2177
+ (global.fetch as ReturnType<typeof vi.fn>).mockImplementation(async (url) => {
2178
+ capturedUrl = String(url);
2179
+ return {
2180
+ headers: new Headers(),
2181
+ ok: true,
2182
+ json: async () => [{ id: 1, name: "John" }],
2183
+ } as Response;
2184
+ });
2185
+
2186
+ const client = createClientBuilder(
2187
+ testFragment,
2188
+ { ...clientConfig, mountRoute: "/api/uploads-direct" },
2189
+ testRoutes,
2190
+ );
2191
+
2192
+ const useUsers = client.createHook("/users");
2193
+ await useUsers.query();
2194
+
2195
+ expect(capturedUrl).toBe("http://localhost:3000/api/uploads-direct/users");
2196
+ });
2197
+
2198
+ test("public mountRoute is used for mutators", async () => {
2199
+ let capturedUrl: string | undefined;
2200
+
2201
+ (global.fetch as ReturnType<typeof vi.fn>).mockImplementation(async (url) => {
2202
+ capturedUrl = String(url);
2203
+ return {
2204
+ headers: new Headers(),
2205
+ ok: true,
2206
+ status: 200,
2207
+ json: async () => ({ id: 2, name: "Jane" }),
2208
+ } as Response;
2209
+ });
2210
+
2211
+ const client = createClientBuilder(
2212
+ testFragment,
2213
+ { ...clientConfig, mountRoute: "/api/uploads-proxy" },
2214
+ testRoutes,
2215
+ );
2216
+
2217
+ const mutator = client.createMutator("POST", "/users");
2218
+ await mutator.mutateQuery({ body: { name: "Jane" } });
2219
+
2220
+ expect(capturedUrl).toBe("http://localhost:3000/api/uploads-proxy/users");
2221
+ });
2222
+
2000
2223
  test("getFetcher returns correct fetcher and options", () => {
2001
2224
  const customFetch = vi.fn() as unknown as typeof fetch;
2002
2225
  const client = createClientBuilder(
@@ -2013,7 +2236,7 @@ describe("Custom Fetcher Configuration", () => {
2013
2236
  expect(defaultOptions).toBeUndefined();
2014
2237
  });
2015
2238
 
2016
- test("getFetcher returns default fetch and options", () => {
2239
+ test("getFetcher returns a bound default fetch and options", async () => {
2017
2240
  const client = createClientBuilder(
2018
2241
  testFragment,
2019
2242
  {
@@ -2023,9 +2246,20 @@ describe("Custom Fetcher Configuration", () => {
2023
2246
  testRoutes,
2024
2247
  );
2025
2248
 
2026
- const { fetcher, defaultOptions } = client.getFetcher();
2027
- expect(fetcher).toBe(fetch);
2028
- expect(defaultOptions).toBeDefined();
2029
- expect(defaultOptions?.credentials).toBe("include");
2249
+ const originalFetch = globalThis.fetch;
2250
+ const fetchSpy = vi.fn<typeof fetch>().mockResolvedValue(new Response(null, { status: 204 }));
2251
+ globalThis.fetch = fetchSpy;
2252
+
2253
+ try {
2254
+ const { fetcher, defaultOptions } = client.getFetcher();
2255
+ expect(fetcher).not.toBe(globalThis.fetch);
2256
+ expect(defaultOptions).toBeDefined();
2257
+ expect(defaultOptions?.credentials).toBe("include");
2258
+
2259
+ await fetcher("https://example.com");
2260
+ expect(fetchSpy).toHaveBeenCalledWith("https://example.com");
2261
+ } finally {
2262
+ globalThis.fetch = originalFetch;
2263
+ }
2030
2264
  });
2031
2265
  });