@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.
- package/.turbo/turbo-build.log +87 -69
- package/CHANGELOG.md +79 -0
- package/dist/api/api.d.ts +21 -2
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +2 -1
- package/dist/api/api.js.map +1 -1
- package/dist/api/bind-services.d.ts +0 -1
- package/dist/api/bind-services.d.ts.map +1 -1
- package/dist/api/bind-services.js.map +1 -1
- package/dist/api/error.d.ts.map +1 -1
- package/dist/api/error.js.map +1 -1
- package/dist/api/fragment-definition-builder.d.ts +32 -40
- package/dist/api/fragment-definition-builder.d.ts.map +1 -1
- package/dist/api/fragment-definition-builder.js +15 -21
- package/dist/api/fragment-definition-builder.js.map +1 -1
- package/dist/api/fragment-instantiator.d.ts +51 -30
- package/dist/api/fragment-instantiator.d.ts.map +1 -1
- package/dist/api/fragment-instantiator.js +201 -52
- package/dist/api/fragment-instantiator.js.map +1 -1
- package/dist/api/request-context-storage.d.ts +4 -0
- package/dist/api/request-context-storage.d.ts.map +1 -1
- package/dist/api/request-context-storage.js +6 -0
- package/dist/api/request-context-storage.js.map +1 -1
- package/dist/api/request-input-context.d.ts +57 -1
- package/dist/api/request-input-context.d.ts.map +1 -1
- package/dist/api/request-input-context.js +67 -0
- package/dist/api/request-input-context.js.map +1 -1
- package/dist/api/request-middleware.d.ts +2 -2
- package/dist/api/request-middleware.d.ts.map +1 -1
- package/dist/api/request-middleware.js.map +1 -1
- package/dist/api/request-output-context.d.ts +1 -1
- package/dist/api/request-output-context.d.ts.map +1 -1
- package/dist/api/request-output-context.js.map +1 -1
- package/dist/api/route-caller.d.ts +30 -0
- package/dist/api/route-caller.d.ts.map +1 -0
- package/dist/api/route-caller.js +63 -0
- package/dist/api/route-caller.js.map +1 -0
- package/dist/api/route-handler-input-options.d.ts.map +1 -1
- package/dist/api/route.d.ts +8 -8
- package/dist/api/route.d.ts.map +1 -1
- package/dist/api/route.js.map +1 -1
- package/dist/api/shared-types.d.ts.map +1 -1
- package/dist/client/client-error.d.ts.map +1 -1
- package/dist/client/client-error.js.map +1 -1
- package/dist/client/client.d.ts +90 -50
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +128 -16
- package/dist/client/client.js.map +1 -1
- package/dist/client/client.svelte.d.ts +6 -5
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +10 -2
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/internal/ndjson-streaming.js.map +1 -1
- package/dist/client/react.d.ts +5 -4
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +104 -12
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +7 -5
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +23 -9
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +16 -4
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +21 -1
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +10 -4
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +24 -1
- package/dist/client/vue.js.map +1 -1
- package/dist/id.d.ts +2 -0
- package/dist/id.js +3 -0
- package/dist/internal/cuid.d.ts +16 -0
- package/dist/internal/cuid.d.ts.map +1 -0
- package/dist/internal/cuid.js +82 -0
- package/dist/internal/cuid.js.map +1 -0
- package/dist/internal/trace-context.d.ts +23 -0
- package/dist/internal/trace-context.d.ts.map +1 -0
- package/dist/internal/trace-context.js +14 -0
- package/dist/internal/trace-context.js.map +1 -0
- package/dist/mod-client.d.ts +7 -20
- package/dist/mod-client.d.ts.map +1 -1
- package/dist/mod-client.js +25 -13
- package/dist/mod-client.js.map +1 -1
- package/dist/mod.d.ts +8 -6
- package/dist/mod.js +3 -1
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +33 -0
- package/dist/runtime.js.map +1 -0
- package/dist/test/test.d.ts +6 -6
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js.map +1 -1
- package/dist/util/ssr.js.map +1 -1
- package/package.json +42 -52
- package/src/api/api.test.ts +3 -1
- package/src/api/api.ts +28 -0
- package/src/api/bind-services.ts +0 -5
- package/src/api/error.ts +1 -0
- package/src/api/fragment-definition-builder.extend.test.ts +2 -1
- package/src/api/fragment-definition-builder.test.ts +2 -1
- package/src/api/fragment-definition-builder.ts +56 -112
- package/src/api/fragment-instantiator.test.ts +311 -166
- package/src/api/fragment-instantiator.ts +470 -131
- package/src/api/fragment-services.test.ts +1 -0
- package/src/api/internal/path-runtime.test.ts +8 -0
- package/src/api/internal/path-type.test.ts +3 -1
- package/src/api/internal/route.test.ts +1 -0
- package/src/api/request-context-storage.ts +7 -0
- package/src/api/request-input-context.test.ts +156 -2
- package/src/api/request-input-context.ts +87 -1
- package/src/api/request-middleware.test.ts +43 -2
- package/src/api/request-middleware.ts +4 -3
- package/src/api/request-output-context.test.ts +3 -1
- package/src/api/request-output-context.ts +2 -1
- package/src/api/route-caller.test.ts +195 -0
- package/src/api/route-caller.ts +167 -0
- package/src/api/route-handler-input-options.ts +2 -1
- package/src/api/route.test.ts +4 -2
- package/src/api/route.ts +9 -3
- package/src/api/shared-types.ts +2 -1
- package/src/client/client-builder.test.ts +4 -2
- package/src/client/client-error.test.ts +2 -1
- package/src/client/client-error.ts +1 -1
- package/src/client/client-types.test.ts +19 -5
- package/src/client/client.ssr.test.ts +6 -4
- package/src/client/client.svelte.test.ts +18 -9
- package/src/client/client.svelte.ts +38 -13
- package/src/client/client.test.ts +244 -10
- package/src/client/client.ts +473 -148
- package/src/client/internal/ndjson-streaming.test.ts +6 -3
- package/src/client/internal/ndjson-streaming.ts +1 -0
- package/src/client/react.test.ts +176 -6
- package/src/client/react.ts +226 -31
- package/src/client/solid.test.ts +29 -5
- package/src/client/solid.ts +60 -22
- package/src/client/vanilla.test.ts +148 -6
- package/src/client/vanilla.ts +63 -9
- package/src/client/vue.test.ts +397 -8
- package/src/client/vue.ts +74 -4
- package/src/id.ts +1 -0
- package/src/internal/cuid.test.ts +164 -0
- package/src/internal/cuid.ts +133 -0
- package/src/internal/trace-context.ts +35 -0
- package/src/mod-client.ts +55 -9
- package/src/mod.ts +9 -3
- package/src/runtime.ts +48 -0
- package/src/test/test.test.ts +4 -2
- package/src/test/test.ts +14 -7
- package/src/util/async.test.ts +1 -0
- package/src/util/content-type.test.ts +1 -0
- package/src/util/nanostores.test.ts +3 -1
- package/src/util/ssr.ts +1 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +2 -0
- package/vitest.config.ts +2 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
|
|
4
|
+
import type { InferOrUnknown } from "../util/types-util";
|
|
5
|
+
import type { HTTPMethod } from "./api";
|
|
6
|
+
import type { FragnoResponse } from "./fragno-response";
|
|
7
|
+
import { parseFragnoResponse } from "./fragno-response";
|
|
8
|
+
import { buildPath, type ExtractPathParams } from "./internal/path";
|
|
9
|
+
import type { AnyFragnoRouteConfig } from "./route";
|
|
10
|
+
import type { RouteHandlerInputOptions } from "./route-handler-input-options";
|
|
11
|
+
|
|
12
|
+
export type RouteCallerConfig = {
|
|
13
|
+
baseUrl: string | URL;
|
|
14
|
+
mountRoute?: string;
|
|
15
|
+
baseHeaders?: HeadersInit;
|
|
16
|
+
fetch: (request: Request) => Promise<Response>;
|
|
17
|
+
redirect?: RequestRedirect;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ArrayBufferViewOfArrayBuffer = ArrayBufferView & { buffer: ArrayBuffer };
|
|
21
|
+
|
|
22
|
+
type FragmentLike = {
|
|
23
|
+
routes?: readonly AnyFragnoRouteConfig[];
|
|
24
|
+
callRoute?: (...args: never[]) => Promise<unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type RouteCallerPath<
|
|
28
|
+
TRoutes extends readonly AnyFragnoRouteConfig[],
|
|
29
|
+
TMethod extends HTTPMethod,
|
|
30
|
+
> = [ExtractRoutePath<TRoutes, TMethod>] extends [never]
|
|
31
|
+
? string
|
|
32
|
+
: ExtractRoutePath<TRoutes, TMethod>;
|
|
33
|
+
|
|
34
|
+
type RouteCallerMatch<
|
|
35
|
+
TRoutes extends readonly AnyFragnoRouteConfig[],
|
|
36
|
+
TMethod extends HTTPMethod,
|
|
37
|
+
TPath extends string,
|
|
38
|
+
> = [ExtractRouteByPath<TRoutes, TPath, TMethod>] extends [never]
|
|
39
|
+
? AnyFragnoRouteConfig
|
|
40
|
+
: ExtractRouteByPath<TRoutes, TPath, TMethod>;
|
|
41
|
+
|
|
42
|
+
export type RouteCallerForFragment<TFragment extends FragmentLike> = TFragment extends {
|
|
43
|
+
routes: infer TRoutes extends readonly AnyFragnoRouteConfig[];
|
|
44
|
+
}
|
|
45
|
+
? <TMethod extends HTTPMethod, TPath extends RouteCallerPath<TRoutes, TMethod>>(
|
|
46
|
+
method: TMethod,
|
|
47
|
+
path: TPath,
|
|
48
|
+
inputOptions?: RouteHandlerInputOptions<
|
|
49
|
+
TPath,
|
|
50
|
+
RouteCallerMatch<TRoutes, TMethod, TPath>["inputSchema"]
|
|
51
|
+
>,
|
|
52
|
+
) => Promise<
|
|
53
|
+
FragnoResponse<
|
|
54
|
+
InferOrUnknown<NonNullable<RouteCallerMatch<TRoutes, TMethod, TPath>["outputSchema"]>>
|
|
55
|
+
>
|
|
56
|
+
>
|
|
57
|
+
: TFragment extends { callRoute: (...args: never[]) => Promise<unknown> }
|
|
58
|
+
? TFragment["callRoute"]
|
|
59
|
+
: never;
|
|
60
|
+
|
|
61
|
+
function isArrayBufferView(value: unknown): value is ArrayBufferViewOfArrayBuffer {
|
|
62
|
+
return ArrayBuffer.isView(value) && value.buffer instanceof ArrayBuffer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildMountedPath(mountRoute: string, pathname: string): string {
|
|
66
|
+
const normalizedMount =
|
|
67
|
+
mountRoute === "/" ? "" : mountRoute.endsWith("/") ? mountRoute.slice(0, -1) : mountRoute;
|
|
68
|
+
const pathPart = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
69
|
+
if (!normalizedMount) {
|
|
70
|
+
return pathPart;
|
|
71
|
+
}
|
|
72
|
+
const mountPart = normalizedMount.startsWith("/") ? normalizedMount : `/${normalizedMount}`;
|
|
73
|
+
return `${mountPart}${pathPart}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createRouteCaller<TFragment extends FragmentLike>(
|
|
77
|
+
config: RouteCallerConfig,
|
|
78
|
+
): RouteCallerForFragment<TFragment> {
|
|
79
|
+
const { baseUrl, fetch } = config;
|
|
80
|
+
const mountRoute = config.mountRoute ?? "";
|
|
81
|
+
const baseHeaders = config.baseHeaders ? new Headers(config.baseHeaders) : undefined;
|
|
82
|
+
const redirect = config.redirect ?? "manual";
|
|
83
|
+
|
|
84
|
+
const callRoute = async <TPath extends string>(
|
|
85
|
+
method: HTTPMethod,
|
|
86
|
+
path: TPath,
|
|
87
|
+
inputOptions?: RouteHandlerInputOptions<TPath, StandardSchemaV1 | undefined>,
|
|
88
|
+
): Promise<FragnoResponse<unknown>> => {
|
|
89
|
+
const headers = baseHeaders ? new Headers(baseHeaders) : new Headers();
|
|
90
|
+
const explicitHeaders = inputOptions?.headers
|
|
91
|
+
? inputOptions.headers instanceof Headers
|
|
92
|
+
? inputOptions.headers
|
|
93
|
+
: new Headers(inputOptions.headers)
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
if (explicitHeaders) {
|
|
97
|
+
for (const [key, value] of explicitHeaders.entries()) {
|
|
98
|
+
headers.set(key, value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasExplicitContentType = explicitHeaders?.has("content-type") ?? false;
|
|
103
|
+
|
|
104
|
+
const searchParams =
|
|
105
|
+
inputOptions?.query instanceof URLSearchParams
|
|
106
|
+
? new URLSearchParams(inputOptions.query)
|
|
107
|
+
: new URLSearchParams();
|
|
108
|
+
|
|
109
|
+
if (inputOptions?.query && !(inputOptions.query instanceof URLSearchParams)) {
|
|
110
|
+
for (const [key, value] of Object.entries(inputOptions.query)) {
|
|
111
|
+
if (value !== undefined) {
|
|
112
|
+
searchParams.set(key, value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pathParams = (inputOptions?.pathParams ?? {}) as ExtractPathParams<TPath>;
|
|
118
|
+
const pathname = buildPath(path, pathParams);
|
|
119
|
+
const url = new URL(buildMountedPath(mountRoute, pathname), baseUrl);
|
|
120
|
+
url.search = searchParams.toString();
|
|
121
|
+
|
|
122
|
+
let body: BodyInit | undefined;
|
|
123
|
+
if (inputOptions && "body" in inputOptions) {
|
|
124
|
+
const rawBody = (inputOptions as { body?: unknown }).body;
|
|
125
|
+
|
|
126
|
+
if (rawBody instanceof FormData || rawBody instanceof Blob) {
|
|
127
|
+
body = rawBody;
|
|
128
|
+
if (!hasExplicitContentType) {
|
|
129
|
+
headers.delete("content-type");
|
|
130
|
+
}
|
|
131
|
+
} else if (rawBody instanceof ReadableStream) {
|
|
132
|
+
body = rawBody;
|
|
133
|
+
if (!hasExplicitContentType) {
|
|
134
|
+
headers.delete("content-type");
|
|
135
|
+
}
|
|
136
|
+
} else if (rawBody instanceof ArrayBuffer) {
|
|
137
|
+
body = rawBody;
|
|
138
|
+
if (!hasExplicitContentType) {
|
|
139
|
+
headers.delete("content-type");
|
|
140
|
+
}
|
|
141
|
+
} else if (isArrayBufferView(rawBody)) {
|
|
142
|
+
body = rawBody;
|
|
143
|
+
if (!hasExplicitContentType) {
|
|
144
|
+
headers.delete("content-type");
|
|
145
|
+
}
|
|
146
|
+
} else if (rawBody !== undefined) {
|
|
147
|
+
body = JSON.stringify(rawBody);
|
|
148
|
+
if (!hasExplicitContentType) {
|
|
149
|
+
headers.set("content-type", "application/json");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const response = await fetch(
|
|
155
|
+
new Request(url, {
|
|
156
|
+
method,
|
|
157
|
+
headers,
|
|
158
|
+
body,
|
|
159
|
+
redirect,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return parseFragnoResponse(response);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return callRoute as RouteCallerForFragment<TFragment>;
|
|
167
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import type { InferOr } from "../util/types-util";
|
|
4
|
+
import type { ExtractPathParams } from "./internal/path";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Options for calling a route handler
|
package/src/api/route.test.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { test, expect, expectTypeOf, describe } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { defineFragment } from "./fragment-definition-builder";
|
|
2
|
+
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
|
|
5
|
+
import { defineFragment } from "./fragment-definition-builder";
|
|
6
|
+
import { defineRoute, defineRoutes } from "./route";
|
|
7
|
+
|
|
6
8
|
describe("defineRoute", () => {
|
|
7
9
|
test("defineRoute no inputSchema", () => {
|
|
8
10
|
const route = defineRoute({
|
package/src/api/route.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// oxlint-disable no-explicit-any
|
|
2
2
|
|
|
3
3
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
|
|
4
5
|
import type { FragnoRouteConfig, HTTPMethod, RequestThisContext } from "./api";
|
|
5
|
-
import type { FragmentDefinition } from "./fragment-definition-builder";
|
|
6
6
|
import type { BoundServices } from "./bind-services";
|
|
7
|
+
import type { FragmentDefinition } from "./fragment-definition-builder";
|
|
7
8
|
|
|
8
9
|
export type AnyFragnoRouteConfig = FragnoRouteConfig<HTTPMethod, string, any, any, any, any, any>;
|
|
9
10
|
|
|
@@ -182,18 +183,19 @@ export type AnyFragmentDefinition = FragmentDefinition<
|
|
|
182
183
|
any,
|
|
183
184
|
any,
|
|
184
185
|
any,
|
|
186
|
+
any,
|
|
185
187
|
any
|
|
186
188
|
>;
|
|
187
189
|
|
|
188
190
|
// Extract config from FragmentDefinition
|
|
189
191
|
export type ExtractFragmentConfig<T> =
|
|
190
|
-
T extends FragmentDefinition<infer TConfig, any, any, any, any, any, any, any, any, any>
|
|
192
|
+
T extends FragmentDefinition<infer TConfig, any, any, any, any, any, any, any, any, any, any>
|
|
191
193
|
? TConfig
|
|
192
194
|
: never;
|
|
193
195
|
|
|
194
196
|
// Extract deps from FragmentDefinition
|
|
195
197
|
export type ExtractFragmentDeps<T> =
|
|
196
|
-
T extends FragmentDefinition<any, any, infer TDeps, any, any, any, any, any, any, any>
|
|
198
|
+
T extends FragmentDefinition<any, any, infer TDeps, any, any, any, any, any, any, any, any>
|
|
197
199
|
? TDeps
|
|
198
200
|
: never;
|
|
199
201
|
|
|
@@ -211,6 +213,7 @@ export type ExtractFragmentServices<T> =
|
|
|
211
213
|
any,
|
|
212
214
|
any,
|
|
213
215
|
any,
|
|
216
|
+
any,
|
|
214
217
|
any
|
|
215
218
|
>
|
|
216
219
|
? BoundServices<TBaseServices & TServices>
|
|
@@ -228,6 +231,7 @@ export type ExtractFragmentServiceDeps<T> =
|
|
|
228
231
|
any,
|
|
229
232
|
any,
|
|
230
233
|
any,
|
|
234
|
+
any,
|
|
231
235
|
any
|
|
232
236
|
>
|
|
233
237
|
? TServiceDependencies
|
|
@@ -245,6 +249,7 @@ export type ExtractFragmentServiceThisContext<T> =
|
|
|
245
249
|
any,
|
|
246
250
|
infer TServiceThisContext,
|
|
247
251
|
any,
|
|
252
|
+
any,
|
|
248
253
|
any
|
|
249
254
|
>
|
|
250
255
|
? TServiceThisContext
|
|
@@ -262,6 +267,7 @@ export type ExtractFragmentHandlerThisContext<T> =
|
|
|
262
267
|
any,
|
|
263
268
|
any,
|
|
264
269
|
infer THandlerThisContext,
|
|
270
|
+
any,
|
|
265
271
|
any
|
|
266
272
|
>
|
|
267
273
|
? THandlerThisContext
|
package/src/api/shared-types.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { test, expect, expectTypeOf, describe } from "vitest";
|
|
2
|
+
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
import { defineRoute } from "../api/route";
|
|
4
|
+
|
|
5
5
|
import { defineFragment } from "../api/fragment-definition-builder";
|
|
6
|
+
import { defineRoute } from "../api/route";
|
|
6
7
|
import type { FragnoPublicClientConfig } from "../api/shared-types";
|
|
8
|
+
import { createClientBuilder } from "./client";
|
|
7
9
|
|
|
8
10
|
// Test route configurations
|
|
9
11
|
const testFragment = defineFragment("test-fragment").build();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { test, expect, describe } from "vitest";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { FragnoApiError } from "../api/error";
|
|
4
4
|
import { RequestOutputContext } from "../api/request-output-context";
|
|
5
|
+
import { FragnoClientApiError, FragnoClientUnknownApiError } from "./client-error";
|
|
5
6
|
|
|
6
7
|
describe("Error Conversion", () => {
|
|
7
8
|
test("should convert API error to client error", async () => {
|
|
@@ -112,7 +112,7 @@ export class FragnoClientApiError<
|
|
|
112
112
|
* The type is `TErrorCode` (the set of known error codes for this route), but may also be a string
|
|
113
113
|
* for forward compatibility with future error codes.
|
|
114
114
|
*/
|
|
115
|
-
get code(): TErrorCode | (string & {}) {
|
|
115
|
+
override get code(): TErrorCode | (string & {}) {
|
|
116
116
|
return super.code as TErrorCode | (string & {});
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { test, expectTypeOf, describe } from "vitest";
|
|
2
|
+
|
|
2
3
|
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
6
|
+
|
|
3
7
|
import { type FragnoRouteConfig, type HTTPMethod } from "../api/api";
|
|
4
|
-
import { defineRoute } from "../api/route";
|
|
8
|
+
import { type AnyFragnoRouteConfig, defineRoute } from "../api/route";
|
|
5
9
|
import type {
|
|
6
10
|
ExtractGetRoutes,
|
|
7
11
|
ExtractGetRoutePaths,
|
|
8
12
|
ExtractOutputSchemaForPath,
|
|
9
13
|
ExtractRouteByPath,
|
|
14
|
+
ExtractRoutePath,
|
|
10
15
|
IsValidGetRoutePath,
|
|
11
16
|
ValidateGetRoutePath,
|
|
12
17
|
HasGetRoutes,
|
|
13
18
|
FragnoClientMutatorData,
|
|
14
19
|
} from "./client";
|
|
15
|
-
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
16
20
|
|
|
17
21
|
// Test route configurations for type testing
|
|
18
22
|
const _testRoutes = [
|
|
@@ -150,12 +154,12 @@ test("ExtractOutputSchemaForPath type tests", () => {
|
|
|
150
154
|
|
|
151
155
|
// Note: Routes without output schema have complex type inference, skipping direct test
|
|
152
156
|
|
|
153
|
-
//
|
|
157
|
+
// Non-existent paths currently widen to unknown, while routes without an output schema stay optional.
|
|
154
158
|
type NonExistentSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/nonexistent">;
|
|
155
|
-
expectTypeOf<NonExistentSchema>().toEqualTypeOf<
|
|
159
|
+
expectTypeOf<NonExistentSchema>().toEqualTypeOf<unknown>();
|
|
156
160
|
|
|
157
161
|
type PathWithNoSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/home">;
|
|
158
|
-
expectTypeOf<PathWithNoSchema>().toEqualTypeOf<StandardSchemaV1<unknown, unknown
|
|
162
|
+
expectTypeOf<PathWithNoSchema>().toEqualTypeOf<StandardSchemaV1<unknown, unknown>>();
|
|
159
163
|
});
|
|
160
164
|
|
|
161
165
|
test("IsValidGetRoutePath type tests", () => {
|
|
@@ -343,6 +347,16 @@ test("GET route with outputSchema", () => {
|
|
|
343
347
|
expectTypeOf<ConstPaths>().toEqualTypeOf<"/test">();
|
|
344
348
|
});
|
|
345
349
|
|
|
350
|
+
test("route extraction falls back gracefully for widened route types", () => {
|
|
351
|
+
type BroadRoutes = readonly AnyFragnoRouteConfig[];
|
|
352
|
+
|
|
353
|
+
expectTypeOf<ExtractRoutePath<BroadRoutes, "POST">>().toEqualTypeOf<string>();
|
|
354
|
+
expectTypeOf<ExtractGetRoutePaths<BroadRoutes>>().toEqualTypeOf<string>();
|
|
355
|
+
expectTypeOf<
|
|
356
|
+
ExtractRouteByPath<BroadRoutes, "/users/:id", "POST">
|
|
357
|
+
>().toExtend<AnyFragnoRouteConfig>();
|
|
358
|
+
});
|
|
359
|
+
|
|
346
360
|
describe("ExtractRouteByPath", () => {
|
|
347
361
|
const _fragmentConfig = {
|
|
348
362
|
name: "test-fragment",
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from "vitest";
|
|
10
|
-
|
|
11
|
-
import { createClientBuilder } from "./client";
|
|
12
|
-
import { defineRoute } from "../api/route";
|
|
13
|
-
import { defineFragment } from "../api/fragment-definition-builder";
|
|
10
|
+
|
|
14
11
|
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import { defineFragment } from "../api/fragment-definition-builder";
|
|
14
|
+
import { defineRoute } from "../api/route";
|
|
15
15
|
import { createAsyncIteratorFromCallback, waitForAsyncIterator } from "../util/async";
|
|
16
|
+
import { type FragnoPublicClientConfig } from "./client";
|
|
17
|
+
import { createClientBuilder } from "./client";
|
|
16
18
|
|
|
17
19
|
describe("server side rendering", () => {
|
|
18
20
|
const testFragmentDefinition = defineFragment("test-fragment").build();
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { test, expect, describe, vi, beforeEach, afterEach, assert } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
|
|
3
|
+
import { atom, computed } from "nanostores";
|
|
4
|
+
import { writable, readable, get, derived } from "svelte/store";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
4
7
|
import { render } from "@testing-library/svelte";
|
|
5
|
-
|
|
8
|
+
|
|
6
9
|
import { defineFragment } from "../api/fragment-definition-builder";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
+
import { RequestOutputContext } from "../api/request-output-context";
|
|
11
|
+
import { defineRoute } from "../api/route";
|
|
12
|
+
import { type FragnoPublicClientConfig } from "./client";
|
|
13
|
+
import { createClientBuilder } from "./client";
|
|
10
14
|
import { FragnoClientUnknownApiError } from "./client-error";
|
|
15
|
+
import { readableToAtom, useFragno } from "./client.svelte";
|
|
11
16
|
import TestComponent from "./component.test.svelte";
|
|
12
|
-
import { atom, computed } from "nanostores";
|
|
13
|
-
import { RequestOutputContext } from "../api/request-output-context";
|
|
14
17
|
|
|
15
18
|
function renderHook(
|
|
16
19
|
clientObj: Record<string, unknown>,
|
|
@@ -766,7 +769,13 @@ describe("useFragno", () => {
|
|
|
766
769
|
expect((get(hook.data) as TestData | undefined)?.id).toBe(456);
|
|
767
770
|
});
|
|
768
771
|
|
|
769
|
-
|
|
772
|
+
const requestedIds = vi
|
|
773
|
+
.mocked(global.fetch)
|
|
774
|
+
.mock.calls.map(([input]) => String(input).match(/\/users\/([^/]+)/)?.[1])
|
|
775
|
+
.filter((id): id is string => id !== undefined);
|
|
776
|
+
|
|
777
|
+
expect(requestedIds).toContain("123");
|
|
778
|
+
expect(requestedIds).toContain("456");
|
|
770
779
|
});
|
|
771
780
|
});
|
|
772
781
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
1
|
import { atom, type ReadableAtom } from "nanostores";
|
|
2
|
+
import { onDestroy } from "svelte";
|
|
3
|
+
import { writable, type Readable, get } from "svelte/store";
|
|
4
|
+
|
|
5
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
6
|
+
|
|
3
7
|
import type { NonGetHTTPMethod } from "../api/api";
|
|
8
|
+
import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
|
|
9
|
+
import type { InferOr } from "../util/types-util";
|
|
4
10
|
import {
|
|
5
11
|
isGetHook,
|
|
6
12
|
isMutatorHook,
|
|
@@ -8,13 +14,10 @@ import {
|
|
|
8
14
|
type FragnoClientHookData,
|
|
9
15
|
type FragnoClientMutatorData,
|
|
10
16
|
type FragnoStoreData,
|
|
17
|
+
type FragnoStoreFactoryData,
|
|
18
|
+
type FragnoStoreObjectData,
|
|
11
19
|
} from "./client";
|
|
12
20
|
import type { FragnoClientError } from "./client-error";
|
|
13
|
-
import type { InferOr } from "../util/types-util";
|
|
14
|
-
import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
|
|
15
|
-
|
|
16
|
-
import { writable, type Readable, get } from "svelte/store";
|
|
17
|
-
import { onDestroy } from "svelte";
|
|
18
21
|
|
|
19
22
|
export type FragnoSvelteHook<
|
|
20
23
|
_TMethod extends "GET",
|
|
@@ -223,10 +226,30 @@ function createSvelteMutator<
|
|
|
223
226
|
};
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
export
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
export type FragnoSvelteStore<T extends object, TArgs extends unknown[] = []> = TArgs extends []
|
|
230
|
+
? T
|
|
231
|
+
: (...args: TArgs) => T;
|
|
232
|
+
|
|
233
|
+
export function createSvelteStore<T extends object, TArgs extends unknown[]>(
|
|
234
|
+
hook: FragnoStoreData<T, TArgs>,
|
|
235
|
+
): FragnoSvelteStore<T, TArgs> {
|
|
236
|
+
if ("obj" in hook) {
|
|
237
|
+
// Since nanostores already implement Svelte's store contract,
|
|
238
|
+
// we can return the store object directly for use with $ syntax
|
|
239
|
+
return hook.obj as FragnoSvelteStore<T, TArgs>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return ((...args: TArgs) => {
|
|
243
|
+
const value = hook.factory(...args);
|
|
244
|
+
const disposer = value[Symbol.dispose as keyof typeof value];
|
|
245
|
+
if (typeof disposer === "function") {
|
|
246
|
+
onDestroy(() => {
|
|
247
|
+
disposer.call(value);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return value;
|
|
252
|
+
}) as FragnoSvelteStore<T, TArgs>;
|
|
230
253
|
}
|
|
231
254
|
|
|
232
255
|
export function useFragno<T extends Record<string, unknown>>(
|
|
@@ -249,9 +272,11 @@ export function useFragno<T extends Record<string, unknown>>(
|
|
|
249
272
|
infer TQueryParameters
|
|
250
273
|
>
|
|
251
274
|
? FragnoSvelteMutator<M, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters>
|
|
252
|
-
: T[K] extends
|
|
253
|
-
? TStoreObj
|
|
254
|
-
: T[K]
|
|
275
|
+
: T[K] extends FragnoStoreObjectData<infer TStoreObj>
|
|
276
|
+
? FragnoSvelteStore<TStoreObj, []>
|
|
277
|
+
: T[K] extends FragnoStoreFactoryData<infer TStoreObj, infer TStoreArgs>
|
|
278
|
+
? FragnoSvelteStore<TStoreObj, TStoreArgs>
|
|
279
|
+
: T[K];
|
|
255
280
|
} {
|
|
256
281
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
257
282
|
const result = {} as any;
|