@fragno-dev/core 0.0.1
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 +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import { atom, type ReadableAtom, type Store, type StoreValue } from "nanostores";
|
|
3
|
+
import type { DeepReadonly, Ref, ShallowRef, UnwrapNestedRefs } from "vue";
|
|
4
|
+
import { computed, getCurrentScope, isRef, onScopeDispose, ref, shallowRef, watch } from "vue";
|
|
5
|
+
import type { NonGetHTTPMethod } from "../api/api";
|
|
6
|
+
import {
|
|
7
|
+
isGetHook,
|
|
8
|
+
isMutatorHook,
|
|
9
|
+
type FragnoClientMutatorData,
|
|
10
|
+
type FragnoClientHookData,
|
|
11
|
+
} from "./client";
|
|
12
|
+
import type { FragnoClientError } from "./client-error";
|
|
13
|
+
import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
|
|
14
|
+
import type { InferOr } from "../util/types-util";
|
|
15
|
+
|
|
16
|
+
export type FragnoVueHook<
|
|
17
|
+
_TMethod extends "GET",
|
|
18
|
+
TPath extends string,
|
|
19
|
+
TOutputSchema extends StandardSchemaV1,
|
|
20
|
+
TErrorCode extends string,
|
|
21
|
+
TQueryParameters extends string,
|
|
22
|
+
> = (args?: {
|
|
23
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string | Ref<string> | ReadableAtom<string>>;
|
|
24
|
+
query?: QueryParamsHint<TQueryParameters, string | Ref<string> | ReadableAtom<string>>;
|
|
25
|
+
}) => {
|
|
26
|
+
data: Ref<InferOr<TOutputSchema, undefined>>;
|
|
27
|
+
loading: Ref<boolean>;
|
|
28
|
+
error: Ref<FragnoClientError<TErrorCode[number]> | undefined>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FragnoVueMutator<
|
|
32
|
+
_TMethod extends NonGetHTTPMethod,
|
|
33
|
+
TPath extends string,
|
|
34
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
35
|
+
TOutputSchema extends StandardSchemaV1 | undefined,
|
|
36
|
+
TErrorCode extends string,
|
|
37
|
+
TQueryParameters extends string,
|
|
38
|
+
> = () => {
|
|
39
|
+
mutate: (args: {
|
|
40
|
+
body?: InferOr<TInputSchema, undefined>;
|
|
41
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string | Ref<string> | ReadableAtom<string>>;
|
|
42
|
+
query?: QueryParamsHint<TQueryParameters, string | Ref<string> | ReadableAtom<string>>;
|
|
43
|
+
}) => Promise<InferOr<TOutputSchema, undefined>>;
|
|
44
|
+
loading: Ref<boolean | undefined>;
|
|
45
|
+
error: Ref<FragnoClientError<TErrorCode[number]> | undefined>;
|
|
46
|
+
data: Ref<InferOr<TOutputSchema, undefined>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Converts a Vue Ref to a NanoStore Atom.
|
|
51
|
+
*
|
|
52
|
+
* This is used to convert Vue refs to atoms, so that we can use them in the store.
|
|
53
|
+
*
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
export function refToAtom<T>(ref: Ref<T>): ReadableAtom<T> {
|
|
57
|
+
const a = atom(ref.value);
|
|
58
|
+
|
|
59
|
+
watch(ref, (newVal) => {
|
|
60
|
+
a.set(newVal);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// TODO: Do we need to unsubscribe, or is this handled by `onScopeDispose` below?
|
|
64
|
+
|
|
65
|
+
return a;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper function to create a Vue composable from a GET hook
|
|
69
|
+
// We want 1 store per hook, so on updates to params, we need to update the store instead of creating a new one.
|
|
70
|
+
// Nanostores only works with atoms (or strings), so we need to convert vue refs to atoms.
|
|
71
|
+
function createVueHook<
|
|
72
|
+
TMethod extends "GET",
|
|
73
|
+
TPath extends string,
|
|
74
|
+
TOutputSchema extends StandardSchemaV1,
|
|
75
|
+
TErrorCode extends string,
|
|
76
|
+
TQueryParameters extends string,
|
|
77
|
+
>(
|
|
78
|
+
hook: FragnoClientHookData<TMethod, TPath, TOutputSchema, TErrorCode, TQueryParameters>,
|
|
79
|
+
): FragnoVueHook<TMethod, TPath, TOutputSchema, TErrorCode, TQueryParameters> {
|
|
80
|
+
return ({ path, query } = {}) => {
|
|
81
|
+
const pathParams: Record<string, string | ReadableAtom<string>> = {};
|
|
82
|
+
const queryParams: Record<string, string | ReadableAtom<string>> = {};
|
|
83
|
+
|
|
84
|
+
for (const [key, value] of Object.entries(path ?? {})) {
|
|
85
|
+
const v = value as string | Ref<string> | ReadableAtom<string>;
|
|
86
|
+
pathParams[key] = isRef(v) ? refToAtom(v) : v;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
90
|
+
// Dunno why the cast is necessary
|
|
91
|
+
const v = value as string | Ref<string> | ReadableAtom<string>;
|
|
92
|
+
queryParams[key] = isRef(v) ? (refToAtom(v) as ReadableAtom<string>) : v;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const store = hook.store({
|
|
96
|
+
path: pathParams as MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>,
|
|
97
|
+
query: queryParams,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const data = ref();
|
|
101
|
+
const loading = ref();
|
|
102
|
+
const error = ref();
|
|
103
|
+
|
|
104
|
+
const unsubscribe = store.subscribe((updatedStoreValue) => {
|
|
105
|
+
data.value = updatedStoreValue.data;
|
|
106
|
+
loading.value = updatedStoreValue.loading;
|
|
107
|
+
error.value = updatedStoreValue.error;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (getCurrentScope()) {
|
|
111
|
+
onScopeDispose(() => {
|
|
112
|
+
unsubscribe();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
data,
|
|
118
|
+
loading,
|
|
119
|
+
error,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Helper function to create a Vue mutator from a mutator hook
|
|
125
|
+
function createVueMutator<
|
|
126
|
+
TMethod extends NonGetHTTPMethod,
|
|
127
|
+
TPath extends string,
|
|
128
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
129
|
+
TOutputSchema extends StandardSchemaV1 | undefined,
|
|
130
|
+
TErrorCode extends string,
|
|
131
|
+
TQueryParameters extends string,
|
|
132
|
+
>(
|
|
133
|
+
hook: FragnoClientMutatorData<
|
|
134
|
+
TMethod,
|
|
135
|
+
TPath,
|
|
136
|
+
TInputSchema,
|
|
137
|
+
TOutputSchema,
|
|
138
|
+
TErrorCode,
|
|
139
|
+
TQueryParameters
|
|
140
|
+
>,
|
|
141
|
+
): FragnoVueMutator<TMethod, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters> {
|
|
142
|
+
return () => {
|
|
143
|
+
const store = useStore(hook.mutatorStore);
|
|
144
|
+
|
|
145
|
+
// Create a wrapped mutate function that handles Vue refs
|
|
146
|
+
const mutate = async (args: {
|
|
147
|
+
body?: InferOr<TInputSchema, undefined>;
|
|
148
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string | Ref<string> | ReadableAtom<string>>;
|
|
149
|
+
query?: QueryParamsHint<TQueryParameters, string | Ref<string> | ReadableAtom<string>>;
|
|
150
|
+
}) => {
|
|
151
|
+
const { body, path, query } = args;
|
|
152
|
+
|
|
153
|
+
const pathParams: Record<string, string | ReadableAtom<string>> = {};
|
|
154
|
+
const queryParams: Record<string, string | ReadableAtom<string>> = {};
|
|
155
|
+
|
|
156
|
+
for (const [key, value] of Object.entries(path ?? {})) {
|
|
157
|
+
const v = value as string | Ref<string> | ReadableAtom<string>;
|
|
158
|
+
pathParams[key] = isRef(v) ? v.value : v;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
162
|
+
const v = value as string | Ref<string> | ReadableAtom<string>;
|
|
163
|
+
queryParams[key] = isRef(v) ? v.value : v;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Call the store's mutate function with normalized params
|
|
167
|
+
return store.value.mutate({
|
|
168
|
+
body,
|
|
169
|
+
path: pathParams as MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>,
|
|
170
|
+
query: queryParams,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Return the store-like object with Vue reactive refs
|
|
175
|
+
return {
|
|
176
|
+
mutate,
|
|
177
|
+
loading: computed(() => store.value.loading),
|
|
178
|
+
error: computed(() => store.value.error),
|
|
179
|
+
data: computed(() => store.value.data) as Ref<InferOr<TOutputSchema, undefined>>,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function useFragno<T extends Record<string, unknown>>(
|
|
185
|
+
clientObj: T,
|
|
186
|
+
): {
|
|
187
|
+
[K in keyof T]: T[K] extends FragnoClientHookData<
|
|
188
|
+
"GET",
|
|
189
|
+
infer TPath,
|
|
190
|
+
infer TOutputSchema,
|
|
191
|
+
infer TErrorCode,
|
|
192
|
+
infer TQueryParameters
|
|
193
|
+
>
|
|
194
|
+
? FragnoVueHook<"GET", TPath, TOutputSchema, TErrorCode, TQueryParameters>
|
|
195
|
+
: T[K] extends FragnoClientMutatorData<
|
|
196
|
+
infer M,
|
|
197
|
+
infer TPath,
|
|
198
|
+
infer TInputSchema,
|
|
199
|
+
infer TOutputSchema,
|
|
200
|
+
infer TErrorCode,
|
|
201
|
+
infer TQueryParameters
|
|
202
|
+
>
|
|
203
|
+
? FragnoVueMutator<M, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters>
|
|
204
|
+
: T[K];
|
|
205
|
+
} {
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
207
|
+
const result = {} as any;
|
|
208
|
+
|
|
209
|
+
for (const key in clientObj) {
|
|
210
|
+
if (!Object.prototype.hasOwnProperty.call(clientObj, key)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const hook = clientObj[key];
|
|
215
|
+
if (isGetHook(hook)) {
|
|
216
|
+
result[key] = createVueHook(hook);
|
|
217
|
+
} else if (isMutatorHook(hook)) {
|
|
218
|
+
result[key] = createVueMutator(hook);
|
|
219
|
+
} else {
|
|
220
|
+
// Pass through non-hook values unchanged
|
|
221
|
+
result[key] = hook;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function useStore<SomeStore extends Store, Value extends StoreValue<SomeStore>>(
|
|
229
|
+
store: SomeStore,
|
|
230
|
+
): DeepReadonly<UnwrapNestedRefs<ShallowRef<Value>>> {
|
|
231
|
+
const state = shallowRef();
|
|
232
|
+
|
|
233
|
+
const unsubscribe = store.subscribe((value) => {
|
|
234
|
+
state.value = value;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (getCurrentScope()) {
|
|
238
|
+
onScopeDispose(unsubscribe);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return state;
|
|
242
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* HTTP Status utility.
|
|
4
|
+
*
|
|
5
|
+
* Modified from honojs/hono
|
|
6
|
+
* Original source: https://github.com/honojs/hono/blob/0e3db674ad3f40be215a55a18062dd8e387ce525/src/utils/http-status.ts
|
|
7
|
+
* License: MIT
|
|
8
|
+
* Date obtained: August 28 2025
|
|
9
|
+
* Copyright (c) 2021-present Yusuke Wada and Hono contributors
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type InfoStatusCode = 100 | 101 | 102 | 103;
|
|
14
|
+
export type SuccessStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226;
|
|
15
|
+
export type DeprecatedStatusCode = 305 | 306;
|
|
16
|
+
export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | DeprecatedStatusCode | 307 | 308;
|
|
17
|
+
export type ClientErrorStatusCode =
|
|
18
|
+
| 400
|
|
19
|
+
| 401
|
|
20
|
+
| 402
|
|
21
|
+
| 403
|
|
22
|
+
| 404
|
|
23
|
+
| 405
|
|
24
|
+
| 406
|
|
25
|
+
| 407
|
|
26
|
+
| 408
|
|
27
|
+
| 409
|
|
28
|
+
| 410
|
|
29
|
+
| 411
|
|
30
|
+
| 412
|
|
31
|
+
| 413
|
|
32
|
+
| 414
|
|
33
|
+
| 415
|
|
34
|
+
| 416
|
|
35
|
+
| 417
|
|
36
|
+
| 418
|
|
37
|
+
| 421
|
|
38
|
+
| 422
|
|
39
|
+
| 423
|
|
40
|
+
| 424
|
|
41
|
+
| 425
|
|
42
|
+
| 426
|
|
43
|
+
| 428
|
|
44
|
+
| 429
|
|
45
|
+
| 431
|
|
46
|
+
| 451;
|
|
47
|
+
export type ServerErrorStatusCode = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* If you want to use an unofficial status, use `UnofficialStatusCode`.
|
|
51
|
+
*/
|
|
52
|
+
export type StatusCode =
|
|
53
|
+
| InfoStatusCode
|
|
54
|
+
| SuccessStatusCode
|
|
55
|
+
| RedirectStatusCode
|
|
56
|
+
| ClientErrorStatusCode
|
|
57
|
+
| ServerErrorStatusCode;
|
|
58
|
+
|
|
59
|
+
export type ContentlessStatusCode = 101 | 204 | 205 | 304;
|
|
60
|
+
export type ContentfulStatusCode = Exclude<StatusCode, ContentlessStatusCode>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface AstroHandlers {
|
|
2
|
+
ALL: ({ request }: { request: Request }) => Promise<Response>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts a Fragno fragment handler to an Astro API route handler
|
|
7
|
+
*
|
|
8
|
+
* @param fragment - The Fragno fragment instance
|
|
9
|
+
* @returns An Astro API route handler function
|
|
10
|
+
*/
|
|
11
|
+
export function toAstroHandler(handler: (req: Request) => Promise<Response>): AstroHandlers {
|
|
12
|
+
return {
|
|
13
|
+
ALL: async ({ request }) => {
|
|
14
|
+
return handler(request);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface NextJsHandlers {
|
|
2
|
+
GET: (request: Request) => Promise<Response>;
|
|
3
|
+
POST: (request: Request) => Promise<Response>;
|
|
4
|
+
PUT: (request: Request) => Promise<Response>;
|
|
5
|
+
PATCH: (request: Request) => Promise<Response>;
|
|
6
|
+
DELETE: (request: Request) => Promise<Response>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function toNextJsHandler<T extends { handler: (req: Request) => Promise<Response> }>(
|
|
10
|
+
fragment: T,
|
|
11
|
+
): NextJsHandlers;
|
|
12
|
+
export function toNextJsHandler(handler: (req: Request) => Promise<Response>): NextJsHandlers;
|
|
13
|
+
export function toNextJsHandler(
|
|
14
|
+
fragmentOrHandler:
|
|
15
|
+
| { handler: (req: Request) => Promise<Response> }
|
|
16
|
+
| ((req: Request) => Promise<Response>),
|
|
17
|
+
): NextJsHandlers {
|
|
18
|
+
const handler = async (request: Request) => {
|
|
19
|
+
return "handler" in fragmentOrHandler
|
|
20
|
+
? fragmentOrHandler.handler(request)
|
|
21
|
+
: fragmentOrHandler(request);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
GET: handler,
|
|
26
|
+
POST: handler,
|
|
27
|
+
PUT: handler,
|
|
28
|
+
PATCH: handler,
|
|
29
|
+
DELETE: handler,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cleanStores, getFinalStoreValues } from "../util/ssr";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advice from https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html
|
|
5
|
+
* @param obj
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
function javascriptEscaped(obj: unknown) {
|
|
9
|
+
return JSON.stringify(obj).replace(/</g, "\\u003c");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This method should be called after a first render pass is finished.
|
|
14
|
+
* It gets all values from the stores and embeds them in a script tag in the HTML body.
|
|
15
|
+
*
|
|
16
|
+
* On the client side, this script tag is used to hydrate the stores.
|
|
17
|
+
* Be sure to also call finishServerLoad when the page is rendered, to reset the stores for the next request.
|
|
18
|
+
*
|
|
19
|
+
* @returns A string to be embedded in a script tag in the HTML body
|
|
20
|
+
*/
|
|
21
|
+
export async function startServerLoad(): Promise<string> {
|
|
22
|
+
const initialStoreValues = await getFinalStoreValues();
|
|
23
|
+
|
|
24
|
+
console.log("initialStoreValues", initialStoreValues);
|
|
25
|
+
|
|
26
|
+
return `window.__FRAGNO_INITIAL_DATA__ = ${javascriptEscaped(
|
|
27
|
+
Array.from(initialStoreValues.entries()),
|
|
28
|
+
)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function initServerLoad() {
|
|
32
|
+
cleanStores();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reset the stores for the next request.
|
|
37
|
+
*/
|
|
38
|
+
export async function finishServerLoad() {
|
|
39
|
+
cleanStores();
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
2
|
+
|
|
3
|
+
type SvelteKitRequestEvent = {
|
|
4
|
+
request: Request;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type SvelteKitRequestHandler = (event: SvelteKitRequestEvent) => MaybePromise<Response>;
|
|
8
|
+
|
|
9
|
+
export interface SvelteKitHandlers {
|
|
10
|
+
GET: SvelteKitRequestHandler;
|
|
11
|
+
POST: SvelteKitRequestHandler;
|
|
12
|
+
PUT: SvelteKitRequestHandler;
|
|
13
|
+
PATCH: SvelteKitRequestHandler;
|
|
14
|
+
DELETE: SvelteKitRequestHandler;
|
|
15
|
+
OPTIONS: SvelteKitRequestHandler;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toSvelteHandler<T extends { handler: (req: Request) => Promise<Response> }>(
|
|
19
|
+
fragment: T,
|
|
20
|
+
): SvelteKitHandlers;
|
|
21
|
+
export function toSvelteHandler(handler: (req: Request) => Promise<Response>): SvelteKitHandlers;
|
|
22
|
+
export function toSvelteHandler(
|
|
23
|
+
fragmentOrHandler:
|
|
24
|
+
| { handler: (req: Request) => Promise<Response> }
|
|
25
|
+
| ((req: Request) => Promise<Response>),
|
|
26
|
+
): SvelteKitHandlers {
|
|
27
|
+
const requestHandler: SvelteKitRequestHandler = async ({ request }) => {
|
|
28
|
+
return "handler" in fragmentOrHandler
|
|
29
|
+
? fragmentOrHandler.handler(request)
|
|
30
|
+
: fragmentOrHandler(request);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
GET: requestHandler,
|
|
35
|
+
POST: requestHandler,
|
|
36
|
+
PUT: requestHandler,
|
|
37
|
+
PATCH: requestHandler,
|
|
38
|
+
DELETE: requestHandler,
|
|
39
|
+
OPTIONS: requestHandler,
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/mod.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
defineFragment,
|
|
3
|
+
createFragment,
|
|
4
|
+
FragmentBuilder,
|
|
5
|
+
type FragnoFragmentSharedConfig,
|
|
6
|
+
type FragnoPublicConfig,
|
|
7
|
+
type FragnoPublicClientConfig,
|
|
8
|
+
type FragnoInstantiatedFragment,
|
|
9
|
+
} from "./api/fragment";
|
|
10
|
+
|
|
11
|
+
export { type FragnoRouteConfig } from "./api/api";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
defineRoute,
|
|
15
|
+
defineRoutes,
|
|
16
|
+
type RouteFactory,
|
|
17
|
+
type RouteFactoryContext,
|
|
18
|
+
type AnyRouteOrFactory,
|
|
19
|
+
type FlattenRouteFactories,
|
|
20
|
+
} from "./api/route";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { createAsyncIteratorFromCallback, waitForAsyncIterator } from "./async";
|
|
3
|
+
|
|
4
|
+
describe("createAsyncIteratorFromCallback", () => {
|
|
5
|
+
test("yields values from callback", async () => {
|
|
6
|
+
let callback: ((value: string) => void) | null = null;
|
|
7
|
+
let unsubscribeCalled = false;
|
|
8
|
+
|
|
9
|
+
const subscribe = (cb: (value: string) => void) => {
|
|
10
|
+
callback = cb;
|
|
11
|
+
return () => {
|
|
12
|
+
unsubscribeCalled = true;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const iterator = createAsyncIteratorFromCallback(subscribe);
|
|
17
|
+
|
|
18
|
+
// Start consuming the iterator
|
|
19
|
+
const consumePromise = (async () => {
|
|
20
|
+
const values: string[] = [];
|
|
21
|
+
for await (const value of iterator) {
|
|
22
|
+
values.push(value);
|
|
23
|
+
if (values.length === 3) break;
|
|
24
|
+
}
|
|
25
|
+
return values;
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
// Emit values
|
|
29
|
+
callback!("first");
|
|
30
|
+
callback!("second");
|
|
31
|
+
callback!("third");
|
|
32
|
+
|
|
33
|
+
const result = await consumePromise;
|
|
34
|
+
expect(result).toEqual(["first", "second", "third"]);
|
|
35
|
+
expect(unsubscribeCalled).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("calls unsubscribe when iterator is terminated early", async () => {
|
|
39
|
+
let callback: ((value: string) => void) | null = null;
|
|
40
|
+
let unsubscribeCalled = false;
|
|
41
|
+
|
|
42
|
+
const subscribe = (cb: (value: string) => void) => {
|
|
43
|
+
callback = cb;
|
|
44
|
+
return () => {
|
|
45
|
+
unsubscribeCalled = true;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const iterator = createAsyncIteratorFromCallback(subscribe);
|
|
50
|
+
|
|
51
|
+
// Start consuming but break early
|
|
52
|
+
const consumePromise = (async () => {
|
|
53
|
+
const values: string[] = [];
|
|
54
|
+
for await (const value of iterator) {
|
|
55
|
+
values.push(value);
|
|
56
|
+
if (values.length === 2) break; // Break after 2 values
|
|
57
|
+
}
|
|
58
|
+
return values;
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
// Emit 3 values
|
|
62
|
+
callback!("first");
|
|
63
|
+
callback!("second");
|
|
64
|
+
callback!("third");
|
|
65
|
+
|
|
66
|
+
const result = await consumePromise;
|
|
67
|
+
expect(result).toEqual(["first", "second"]);
|
|
68
|
+
expect(unsubscribeCalled).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("waitForAsyncIterator", () => {
|
|
73
|
+
test("actually times out", async () => {
|
|
74
|
+
const iterable = {
|
|
75
|
+
[Symbol.asyncIterator]: async function* () {
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
77
|
+
yield "test";
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await expect(waitForAsyncIterator(iterable, () => false, { timeout: 1 })).rejects.toThrow(
|
|
82
|
+
"waitForAsyncIterator: Timeout after 1ms",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
type SubscribeFn<T> = (callback: (value: T) => void) => UnsubscribeFn | void;
|
|
2
|
+
type UnsubscribeFn = () => void;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates an async iterator from a subscribe function that follows the observable pattern.
|
|
6
|
+
*
|
|
7
|
+
* @template T The type of values produced by the store.
|
|
8
|
+
* @param subscribe A function that subscribes to store updates. It receives a callback to be
|
|
9
|
+
* called on each update, and returns an unsubscribe function.
|
|
10
|
+
* @returns An async generator that yields store values as they are produced.
|
|
11
|
+
*/
|
|
12
|
+
export function createAsyncIteratorFromCallback<T>(
|
|
13
|
+
subscribe: SubscribeFn<T>,
|
|
14
|
+
): AsyncGenerator<T, void, unknown> {
|
|
15
|
+
const queue: T[] = [];
|
|
16
|
+
let unsubscribe: UnsubscribeFn | null = null;
|
|
17
|
+
let resolveNext: ((value: IteratorResult<T>) => void) | null = null;
|
|
18
|
+
|
|
19
|
+
const unsubscribeFunc = subscribe((value) => {
|
|
20
|
+
if (resolveNext) {
|
|
21
|
+
// If there's a pending promise, resolve it immediately
|
|
22
|
+
resolveNext({ value, done: false });
|
|
23
|
+
resolveNext = null;
|
|
24
|
+
} else {
|
|
25
|
+
// Otherwise, queue the value
|
|
26
|
+
queue.push(value);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Store unsubscribe function if one was returned
|
|
31
|
+
if (typeof unsubscribeFunc === "function") {
|
|
32
|
+
unsubscribe = unsubscribeFunc;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (async function* () {
|
|
36
|
+
try {
|
|
37
|
+
while (true) {
|
|
38
|
+
if (queue.length > 0) {
|
|
39
|
+
// Yield queued values
|
|
40
|
+
yield queue.shift()!;
|
|
41
|
+
} else {
|
|
42
|
+
// Wait for the next value
|
|
43
|
+
yield await new Promise<T>((resolve) => {
|
|
44
|
+
resolveNext = (result) => {
|
|
45
|
+
if (!result.done) {
|
|
46
|
+
resolve(result.value);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
// Clean up subscription on iterator termination
|
|
54
|
+
if (unsubscribe) {
|
|
55
|
+
unsubscribe();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Waits for an async iterator to yield a value that meets a condition.
|
|
63
|
+
*
|
|
64
|
+
* @template T The type of values produced by the iterator.
|
|
65
|
+
* @param iterable The async iterable to wait for.
|
|
66
|
+
* @param condition A function that checks if a value meets the condition.
|
|
67
|
+
* @param options Optional configuration options.
|
|
68
|
+
* @returns A promise that resolves to the first value that meets the condition.
|
|
69
|
+
*/
|
|
70
|
+
export async function waitForAsyncIterator<T>(
|
|
71
|
+
iterable: AsyncIterable<T>,
|
|
72
|
+
condition: (value: T) => boolean,
|
|
73
|
+
options: { timeout?: number } = {},
|
|
74
|
+
): Promise<T> {
|
|
75
|
+
const { timeout = 1000 } = options;
|
|
76
|
+
|
|
77
|
+
// Create a timeout promise that rejects after the specified time
|
|
78
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
reject(new Error(`waitForAsyncIterator: Timeout after ${timeout}ms`));
|
|
81
|
+
}, timeout);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Create a promise that resolves when the condition is met
|
|
85
|
+
const iteratorPromise = (async () => {
|
|
86
|
+
for await (const value of iterable) {
|
|
87
|
+
if (condition(value)) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error("waitForAsyncIterator: Iterator completed without meeting condition");
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
// Race between the timeout and the iterator
|
|
95
|
+
return Promise.race([iteratorPromise, timeoutPromise]);
|
|
96
|
+
}
|