@fragno-dev/core 0.1.5 → 0.1.7
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 +49 -45
- package/CHANGELOG.md +52 -0
- package/dist/api/api.d.ts +2 -2
- package/dist/api/fragment-builder.d.ts +3 -2
- package/dist/api/fragment-instantiation.d.ts +4 -3
- package/dist/api/fragment-instantiation.js +3 -3
- package/dist/api/route.d.ts +3 -0
- package/dist/api/route.js +3 -0
- package/dist/{api-B1-h7jPC.d.ts → api-BWN97TOr.d.ts} +17 -3
- package/dist/api-BWN97TOr.d.ts.map +1 -0
- package/dist/api-DngJDcmO.js.map +1 -1
- package/dist/client/client.d.ts +4 -3
- package/dist/client/client.js +3 -3
- package/dist/client/client.svelte.d.ts +3 -3
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +3 -3
- package/dist/client/react.d.ts +3 -3
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +3 -3
- package/dist/client/solid.d.ts +3 -3
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +3 -3
- package/dist/client/vanilla.d.ts +3 -3
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +3 -3
- package/dist/client/vue.d.ts +3 -3
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +7 -7
- package/dist/client/vue.js.map +1 -1
- package/dist/{client-YUZaNg5U.js → client-C5LsYHEI.js} +92 -11
- package/dist/client-C5LsYHEI.js.map +1 -0
- package/dist/{fragment-builder-DsqUOfJ5.d.ts → fragment-builder-MGr68GNb.d.ts} +80 -44
- package/dist/fragment-builder-MGr68GNb.d.ts.map +1 -0
- package/dist/{fragment-instantiation-Cp0K8zdS.js → fragment-instantiation-C4wvwl6V.js} +108 -3
- package/dist/fragment-instantiation-C4wvwl6V.js.map +1 -0
- package/dist/mod.d.ts +3 -2
- package/dist/mod.js +3 -3
- package/dist/{route-Dk1GyqHs.js → request-output-context-CdIjwmEN.js} +13 -24
- package/dist/request-output-context-CdIjwmEN.js.map +1 -0
- package/dist/route-Bl9Zr1Yv.d.ts +26 -0
- package/dist/route-Bl9Zr1Yv.d.ts.map +1 -0
- package/dist/route-C5Uryylh.js +21 -0
- package/dist/route-C5Uryylh.js.map +1 -0
- package/dist/test/test.d.ts +24 -70
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js +27 -115
- package/dist/test/test.js.map +1 -1
- package/package.json +6 -1
- package/src/api/api.ts +1 -0
- package/src/api/fragment-instantiation.test.ts +460 -0
- package/src/api/fragment-instantiation.ts +121 -0
- package/src/api/fragno-response.ts +132 -0
- package/src/api/internal/path-type.test.ts +7 -7
- package/src/api/internal/path.ts +1 -1
- package/src/api/request-output-context.test.ts +10 -10
- package/src/api/request-output-context.ts +3 -3
- package/src/api/route-handler-input-options.ts +15 -0
- package/src/client/client-types.test.ts +4 -4
- package/src/client/client.test.ts +341 -0
- package/src/client/client.ts +96 -15
- package/src/client/internal/fetcher-merge.ts +59 -0
- package/src/test/test.test.ts +110 -165
- package/src/test/test.ts +56 -266
- package/tsdown.config.ts +1 -0
- package/dist/api-B1-h7jPC.d.ts.map +0 -1
- package/dist/client-YUZaNg5U.js.map +0 -1
- package/dist/fragment-builder-DsqUOfJ5.d.ts.map +0 -1
- package/dist/fragment-instantiation-Cp0K8zdS.js.map +0 -1
- package/dist/route-CTxjMtGZ.js +0 -10
- package/dist/route-CTxjMtGZ.js.map +0 -1
- package/dist/route-Dk1GyqHs.js.map +0 -1
package/src/client/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { nanoquery, type FetcherStore, type MutatorStore } from "@nanostores/query";
|
|
2
2
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
|
-
import { task, type ReadableAtom, type Store } from "nanostores";
|
|
3
|
+
import { computed, task, type ReadableAtom, type Store } from "nanostores";
|
|
4
4
|
import type { FragnoRouteConfig, HTTPMethod, NonGetHTTPMethod } from "../api/api";
|
|
5
5
|
import {
|
|
6
6
|
buildPath,
|
|
@@ -13,6 +13,7 @@ import { getMountRoute } from "../api/internal/route";
|
|
|
13
13
|
import { RequestInputContext } from "../api/request-input-context";
|
|
14
14
|
import { RequestOutputContext } from "../api/request-output-context";
|
|
15
15
|
import type {
|
|
16
|
+
FetcherConfig,
|
|
16
17
|
FragnoFragmentSharedConfig,
|
|
17
18
|
FragnoPublicClientConfig,
|
|
18
19
|
} from "../api/fragment-instantiation";
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
type FlattenRouteFactories,
|
|
32
33
|
resolveRouteFactories,
|
|
33
34
|
} from "../api/route";
|
|
35
|
+
import { mergeFetcherConfigs } from "./internal/fetcher-merge";
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Symbols used to identify hook types
|
|
@@ -250,11 +252,11 @@ export type FragnoClientHookData<
|
|
|
250
252
|
>;
|
|
251
253
|
query(args?: {
|
|
252
254
|
path?: MaybeExtractPathParamsOrWiden<TPath, string>;
|
|
253
|
-
query?: Record<TQueryParameters, string>;
|
|
255
|
+
query?: Record<TQueryParameters, string | undefined>;
|
|
254
256
|
}): Promise<StandardSchemaV1.InferOutput<TOutputSchema>>;
|
|
255
257
|
store(args?: {
|
|
256
258
|
path?: MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>;
|
|
257
|
-
query?: Record<TQueryParameters, string | ReadableAtom<string>>;
|
|
259
|
+
query?: Record<TQueryParameters, string | undefined | ReadableAtom<string | undefined>>;
|
|
258
260
|
}): FetcherStore<StandardSchemaV1.InferOutput<TOutputSchema>, FragnoClientError<TErrorCode>>;
|
|
259
261
|
[GET_HOOK_SYMBOL]: true;
|
|
260
262
|
} & {
|
|
@@ -285,14 +287,14 @@ export type FragnoClientMutatorData<
|
|
|
285
287
|
mutateQuery(args?: {
|
|
286
288
|
body?: InferOr<TInputSchema, undefined>;
|
|
287
289
|
path?: MaybeExtractPathParamsOrWiden<TPath, string>;
|
|
288
|
-
query?: Record<TQueryParameters, string>;
|
|
290
|
+
query?: Record<TQueryParameters, string | undefined>;
|
|
289
291
|
}): Promise<InferOr<TOutputSchema, undefined>>;
|
|
290
292
|
|
|
291
293
|
mutatorStore: MutatorStore<
|
|
292
294
|
{
|
|
293
295
|
body?: InferOr<TInputSchema, undefined>;
|
|
294
296
|
path?: MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>;
|
|
295
|
-
query?: Record<TQueryParameters, string | ReadableAtom<string>>;
|
|
297
|
+
query?: Record<TQueryParameters, string | undefined | ReadableAtom<string | undefined>>;
|
|
296
298
|
},
|
|
297
299
|
InferOr<TOutputSchema, undefined>,
|
|
298
300
|
FragnoClientError<TErrorCode>
|
|
@@ -311,7 +313,7 @@ export function buildUrl<TPath extends string>(
|
|
|
311
313
|
},
|
|
312
314
|
params: {
|
|
313
315
|
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
314
|
-
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
316
|
+
queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
|
|
315
317
|
},
|
|
316
318
|
): string {
|
|
317
319
|
const { baseUrl = "", mountRoute, path } = config;
|
|
@@ -320,7 +322,12 @@ export function buildUrl<TPath extends string>(
|
|
|
320
322
|
const normalizedPathParams = unwrapObject(pathParams) as ExtractPathParams<TPath, string>;
|
|
321
323
|
const normalizedQueryParams = unwrapObject(queryParams) ?? {};
|
|
322
324
|
|
|
323
|
-
|
|
325
|
+
// Filter out undefined values to prevent URLSearchParams from converting them to string "undefined"
|
|
326
|
+
const filteredQueryParams = Object.fromEntries(
|
|
327
|
+
Object.entries(normalizedQueryParams).filter(([_, value]) => value !== undefined),
|
|
328
|
+
) as Record<string, string>;
|
|
329
|
+
|
|
330
|
+
const searchParams = new URLSearchParams(filteredQueryParams);
|
|
324
331
|
const builtPath = buildPath(path, normalizedPathParams ?? {});
|
|
325
332
|
const search = searchParams.toString() ? `?${searchParams.toString()}` : "";
|
|
326
333
|
return `${baseUrl}${mountRoute}${builtPath}${search}`;
|
|
@@ -331,6 +338,7 @@ export function buildUrl<TPath extends string>(
|
|
|
331
338
|
*
|
|
332
339
|
* The returned array is always: path, pathParams (In order they appear in the path), queryParams (In alphabetical order)
|
|
333
340
|
* Missing pathParams are replaced with "<missing>".
|
|
341
|
+
* Atoms with undefined values are wrapped in computed atoms that map undefined to "" to avoid nanoquery treating the key as incomplete.
|
|
334
342
|
* @param path
|
|
335
343
|
* @param params
|
|
336
344
|
* @returns
|
|
@@ -340,7 +348,7 @@ export function getCacheKey<TMethod extends HTTPMethod, TPath extends string>(
|
|
|
340
348
|
path: TPath,
|
|
341
349
|
params?: {
|
|
342
350
|
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
343
|
-
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
351
|
+
queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
|
|
344
352
|
},
|
|
345
353
|
): (string | ReadableAtom<string>)[] {
|
|
346
354
|
if (!params) {
|
|
@@ -355,7 +363,15 @@ export function getCacheKey<TMethod extends HTTPMethod, TPath extends string>(
|
|
|
355
363
|
const queryParamValues = queryParams
|
|
356
364
|
? Object.keys(queryParams)
|
|
357
365
|
.sort()
|
|
358
|
-
.map((key) =>
|
|
366
|
+
.map((key) => {
|
|
367
|
+
const value = queryParams[key];
|
|
368
|
+
// If it's an atom, wrap it to convert undefined to ""
|
|
369
|
+
if (value && typeof value === "object" && "get" in value) {
|
|
370
|
+
return computed(value as ReadableAtom<string | undefined>, (v) => v ?? "");
|
|
371
|
+
}
|
|
372
|
+
// Plain string value (or undefined)
|
|
373
|
+
return value ?? "";
|
|
374
|
+
})
|
|
359
375
|
: [];
|
|
360
376
|
|
|
361
377
|
return [method, path, ...pathParamValues, ...queryParamValues];
|
|
@@ -487,6 +503,7 @@ export class ClientBuilder<
|
|
|
487
503
|
> {
|
|
488
504
|
#publicConfig: FragnoPublicClientConfig;
|
|
489
505
|
#fragmentConfig: TFragmentConfig;
|
|
506
|
+
#fetcherConfig?: FetcherConfig;
|
|
490
507
|
|
|
491
508
|
#cache = new Map<string, CacheLine>();
|
|
492
509
|
|
|
@@ -497,6 +514,7 @@ export class ClientBuilder<
|
|
|
497
514
|
constructor(publicConfig: FragnoPublicClientConfig, fragmentConfig: TFragmentConfig) {
|
|
498
515
|
this.#publicConfig = publicConfig;
|
|
499
516
|
this.#fragmentConfig = fragmentConfig;
|
|
517
|
+
this.#fetcherConfig = publicConfig.fetcherConfig;
|
|
500
518
|
|
|
501
519
|
const [createFetcherStore, createMutatorStore, { invalidateKeys }] = nanoquery({
|
|
502
520
|
cache: this.#cache,
|
|
@@ -514,6 +532,54 @@ export class ClientBuilder<
|
|
|
514
532
|
return { obj: obj, [STORE_SYMBOL]: true };
|
|
515
533
|
}
|
|
516
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Build a URL for a custom backend call using the configured baseUrl and mountRoute.
|
|
537
|
+
* Useful for fragment authors who need to make custom fetch calls.
|
|
538
|
+
*/
|
|
539
|
+
buildUrl<TPath extends string>(
|
|
540
|
+
path: TPath,
|
|
541
|
+
params?: {
|
|
542
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string>;
|
|
543
|
+
query?: Record<string, string>;
|
|
544
|
+
},
|
|
545
|
+
): string {
|
|
546
|
+
const baseUrl = this.#publicConfig.baseUrl ?? "";
|
|
547
|
+
const mountRoute = getMountRoute(this.#fragmentConfig);
|
|
548
|
+
|
|
549
|
+
return buildUrl(
|
|
550
|
+
{ baseUrl, mountRoute, path },
|
|
551
|
+
{ pathParams: params?.path, queryParams: params?.query },
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get the configured fetcher function for custom backend calls.
|
|
557
|
+
* Returns fetch with merged options applied.
|
|
558
|
+
*/
|
|
559
|
+
getFetcher(): {
|
|
560
|
+
fetcher: typeof fetch;
|
|
561
|
+
defaultOptions: RequestInit | undefined;
|
|
562
|
+
} {
|
|
563
|
+
return {
|
|
564
|
+
fetcher: this.#getFetcher(),
|
|
565
|
+
defaultOptions: this.#getFetcherOptions(),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#getFetcher(): typeof fetch {
|
|
570
|
+
if (this.#fetcherConfig?.type === "function") {
|
|
571
|
+
return this.#fetcherConfig.fetcher;
|
|
572
|
+
}
|
|
573
|
+
return fetch;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#getFetcherOptions(): RequestInit | undefined {
|
|
577
|
+
if (this.#fetcherConfig?.type === "options") {
|
|
578
|
+
return this.#fetcherConfig.options;
|
|
579
|
+
}
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
|
|
517
583
|
createHook<TPath extends ExtractGetRoutePaths<TFragmentConfig["routes"]>>(
|
|
518
584
|
path: ValidateGetRoutePath<TFragmentConfig["routes"], TPath>,
|
|
519
585
|
options?: CreateHookOptions,
|
|
@@ -611,17 +677,24 @@ export class ClientBuilder<
|
|
|
611
677
|
|
|
612
678
|
const baseUrl = this.#publicConfig.baseUrl ?? "";
|
|
613
679
|
const mountRoute = getMountRoute(this.#fragmentConfig);
|
|
680
|
+
const fetcher = this.#getFetcher();
|
|
681
|
+
const fetcherOptions = this.#getFetcherOptions();
|
|
614
682
|
|
|
615
683
|
async function callServerSideHandler(params: {
|
|
616
684
|
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
617
|
-
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
685
|
+
queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
|
|
618
686
|
}): Promise<Response> {
|
|
619
687
|
const { pathParams, queryParams } = params ?? {};
|
|
620
688
|
|
|
621
689
|
const normalizedPathParams = unwrapObject(pathParams) as ExtractPathParams<TPath, string>;
|
|
622
690
|
const normalizedQueryParams = unwrapObject(queryParams) ?? {};
|
|
623
691
|
|
|
624
|
-
|
|
692
|
+
// Filter out undefined values to prevent URLSearchParams from converting them to string "undefined"
|
|
693
|
+
const filteredQueryParams = Object.fromEntries(
|
|
694
|
+
Object.entries(normalizedQueryParams).filter(([_, value]) => value !== undefined),
|
|
695
|
+
) as Record<string, string>;
|
|
696
|
+
|
|
697
|
+
const searchParams = new URLSearchParams(filteredQueryParams);
|
|
625
698
|
|
|
626
699
|
const result = await route.handler(
|
|
627
700
|
RequestInputContext.fromSSRContext({
|
|
@@ -638,7 +711,7 @@ export class ClientBuilder<
|
|
|
638
711
|
|
|
639
712
|
async function executeQuery(params?: {
|
|
640
713
|
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
641
|
-
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
714
|
+
queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
|
|
642
715
|
}): Promise<Response> {
|
|
643
716
|
const { pathParams, queryParams } = params ?? {};
|
|
644
717
|
|
|
@@ -650,7 +723,7 @@ export class ClientBuilder<
|
|
|
650
723
|
|
|
651
724
|
let response: Response;
|
|
652
725
|
try {
|
|
653
|
-
response = await
|
|
726
|
+
response = fetcherOptions ? await fetcher(url, fetcherOptions) : await fetcher(url);
|
|
654
727
|
} catch (error) {
|
|
655
728
|
throw FragnoClientFetchError.fromUnknownFetchError(error);
|
|
656
729
|
}
|
|
@@ -795,6 +868,8 @@ export class ClientBuilder<
|
|
|
795
868
|
|
|
796
869
|
const baseUrl = this.#publicConfig.baseUrl ?? "";
|
|
797
870
|
const mountRoute = getMountRoute(this.#fragmentConfig);
|
|
871
|
+
const fetcher = this.#getFetcher();
|
|
872
|
+
const fetcherOptions = this.#getFetcherOptions();
|
|
798
873
|
|
|
799
874
|
async function executeMutateQuery({
|
|
800
875
|
body,
|
|
@@ -828,10 +903,12 @@ export class ClientBuilder<
|
|
|
828
903
|
|
|
829
904
|
let response: Response;
|
|
830
905
|
try {
|
|
831
|
-
|
|
906
|
+
const requestOptions: RequestInit = {
|
|
907
|
+
...fetcherOptions,
|
|
832
908
|
method,
|
|
833
909
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
834
|
-
}
|
|
910
|
+
};
|
|
911
|
+
response = await fetcher(url, requestOptions);
|
|
835
912
|
} catch (error) {
|
|
836
913
|
throw FragnoClientFetchError.fromUnknownFetchError(error);
|
|
837
914
|
}
|
|
@@ -1008,6 +1085,7 @@ export function createClientBuilder<
|
|
|
1008
1085
|
},
|
|
1009
1086
|
publicConfig: FragnoPublicClientConfig,
|
|
1010
1087
|
routesOrFactories: TRoutesOrFactories,
|
|
1088
|
+
authorFetcherConfig?: FetcherConfig,
|
|
1011
1089
|
): ClientBuilder<
|
|
1012
1090
|
FlattenRouteFactories<TRoutesOrFactories>,
|
|
1013
1091
|
FragnoFragmentSharedConfig<FlattenRouteFactories<TRoutesOrFactories>>
|
|
@@ -1030,12 +1108,15 @@ export function createClientBuilder<
|
|
|
1030
1108
|
};
|
|
1031
1109
|
|
|
1032
1110
|
const mountRoute = publicConfig.mountRoute ?? `/${definition.name}`;
|
|
1111
|
+
const mergedFetcherConfig = mergeFetcherConfigs(authorFetcherConfig, publicConfig.fetcherConfig);
|
|
1033
1112
|
const fullPublicConfig = {
|
|
1034
1113
|
...publicConfig,
|
|
1035
1114
|
mountRoute,
|
|
1115
|
+
fetcherConfig: mergedFetcherConfig,
|
|
1036
1116
|
};
|
|
1037
1117
|
|
|
1038
1118
|
return new ClientBuilder(fullPublicConfig, fragmentConfig);
|
|
1039
1119
|
}
|
|
1040
1120
|
|
|
1041
1121
|
export * from "./client-error";
|
|
1122
|
+
export type { FetcherConfig };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { FetcherConfig } from "../../api/fragment-instantiation";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merge two fetcher configurations, with user config taking precedence.
|
|
5
|
+
* If user provides a custom function, it takes full precedence.
|
|
6
|
+
* Otherwise, deep merge RequestInit options.
|
|
7
|
+
*/
|
|
8
|
+
export function mergeFetcherConfigs(
|
|
9
|
+
authorConfig?: FetcherConfig,
|
|
10
|
+
userConfig?: FetcherConfig,
|
|
11
|
+
): FetcherConfig | undefined {
|
|
12
|
+
// If user provides custom function, it takes full precedence
|
|
13
|
+
if (userConfig?.type === "function") {
|
|
14
|
+
return userConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!userConfig && authorConfig?.type === "function") {
|
|
18
|
+
return authorConfig;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Deep merge RequestInit options
|
|
22
|
+
const authorOpts = authorConfig?.type === "options" ? authorConfig.options : {};
|
|
23
|
+
const userOpts = userConfig?.type === "options" ? userConfig.options : {};
|
|
24
|
+
|
|
25
|
+
// If both are empty, return undefined
|
|
26
|
+
if (Object.keys(authorOpts).length === 0 && Object.keys(userOpts).length === 0) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
type: "options",
|
|
32
|
+
options: {
|
|
33
|
+
...authorOpts,
|
|
34
|
+
...userOpts,
|
|
35
|
+
headers: mergeHeaders(authorOpts.headers, userOpts.headers),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Merge headers from author and user configs.
|
|
42
|
+
* User headers override author headers.
|
|
43
|
+
*/
|
|
44
|
+
function mergeHeaders(author?: HeadersInit, user?: HeadersInit): HeadersInit | undefined {
|
|
45
|
+
if (!author && !user) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Convert to Headers objects and merge
|
|
50
|
+
const merged = new Headers(author);
|
|
51
|
+
new Headers(user).forEach((value, key) => merged.set(key, value));
|
|
52
|
+
|
|
53
|
+
// If no headers after merge, return undefined
|
|
54
|
+
if (merged.keys().next().done) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return merged;
|
|
59
|
+
}
|