@kasperrt/smolter 0.0.1 → 0.3.0
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/{src/internal → .generated}/client/index.ts +1 -0
- package/.generated/core/environment.ts +43 -0
- package/{src/internal → .generated}/core/errors.ts +1 -1
- package/{src/internal → .generated}/core/index.ts +2 -0
- package/.generated/core/navigation-router.ts +107 -0
- package/.generated/core/navigation.ts +147 -0
- package/.generated/core/presentation.ts +108 -0
- package/.generated/core/resource-types.ts +44 -0
- package/.generated/core/resources.ts +444 -0
- package/.generated/core/responses.ts +86 -0
- package/{src/internal → .generated}/core/router.ts +136 -63
- package/.generated/core/serialize.ts +20 -0
- package/.generated/core/store.ts +25 -0
- package/.generated/core/submission.ts +116 -0
- package/{src/internal → .generated}/core/types.ts +18 -1
- package/.generated/core/url.ts +29 -0
- package/.generated/core/utils.ts +5 -0
- package/.generated/preact/await.tsx +41 -0
- package/.generated/preact/client.tsx +93 -0
- package/.generated/preact/context.ts +59 -0
- package/.generated/preact/document.tsx +198 -0
- package/{src/internal → .generated}/preact/fetcher.ts +43 -23
- package/.generated/preact/form.ts +58 -0
- package/{src/internal → .generated}/preact/hooks.ts +25 -28
- package/.generated/preact/index.ts +34 -0
- package/.generated/preact/lazy-route.ts +100 -0
- package/.generated/preact/navigation-state.ts +9 -0
- package/.generated/preact/navigation.tsx +147 -0
- package/.generated/preact/root-context.ts +21 -0
- package/.generated/preact/root-data.ts +45 -0
- package/.generated/preact/route-hooks.tsx +170 -0
- package/.generated/preact/route-types.ts +76 -0
- package/.generated/preact/router.tsx +107 -0
- package/{src/internal → .generated}/preact/routes.ts +17 -9
- package/.generated/react/context.ts +59 -0
- package/.generated/react/data.ts +25 -0
- package/{src/internal → .generated}/react/fetcher.ts +13 -11
- package/.generated/react/form.ts +58 -0
- package/{src/internal → .generated}/react/hooks.ts +9 -14
- package/{src/internal/preact → .generated/react}/index.ts +5 -1
- package/{src/internal → .generated}/react/link.ts +13 -21
- package/.generated/react/resources/components.tsx +404 -0
- package/{src/internal → .generated}/react/routes.ts +17 -9
- package/.generated/server/application.tsx +185 -0
- package/.generated/server/assets.ts +19 -0
- package/.generated/server/handler.ts +69 -0
- package/.generated/server/http.ts +55 -0
- package/{src/internal → .generated}/server/index.ts +4 -17
- package/.generated/server/node.ts +4 -0
- package/.generated/server/preact.ts +7 -0
- package/.generated/vite/application/browser-boundary.ts +42 -0
- package/.generated/vite/application/lazy-files.ts +136 -0
- package/.generated/vite/application/vite.ts +144 -0
- package/{src/internal → .generated}/vite/index.ts +4 -2
- package/.generated/vite/preact.ts +24 -0
- package/.generated/vite/react.ts +18 -0
- package/{src/internal → .generated}/vite/transport.ts +2 -1
- package/CHANGELOG.md +54 -0
- package/README.md +269 -55
- package/dist/client/deserialize.js +10 -0
- package/dist/{internal/client → client}/index.d.ts +1 -0
- package/dist/client/index.js +46 -0
- package/dist/core/environment.d.ts +12 -0
- package/dist/core/environment.js +23 -0
- package/dist/core/errors.js +7 -0
- package/dist/{internal/core → core}/index.d.ts +2 -0
- package/dist/core/match.js +33 -0
- package/dist/core/navigation-router.d.ts +15 -0
- package/dist/core/navigation-router.js +64 -0
- package/dist/core/navigation.d.ts +15 -0
- package/dist/core/navigation.js +92 -0
- package/dist/core/presentation.d.ts +5 -0
- package/dist/core/presentation.js +69 -0
- package/dist/core/resource-types.d.ts +37 -0
- package/dist/core/resources.d.ts +19 -0
- package/dist/core/resources.js +345 -0
- package/dist/core/responses.d.ts +15 -0
- package/dist/core/responses.js +48 -0
- package/dist/{internal/core → core}/router.d.ts +1 -1
- package/dist/core/router.js +204 -0
- package/dist/core/serialize.d.ts +5 -0
- package/dist/core/serialize.js +17 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +21 -0
- package/dist/core/submission.d.ts +30 -0
- package/dist/core/submission.js +41 -0
- package/dist/{internal/core → core}/types.d.ts +16 -1
- package/dist/core/url.d.ts +7 -0
- package/dist/core/url.js +12 -0
- package/dist/core/utils.d.ts +5 -0
- package/dist/core/utils.js +5 -0
- package/dist/files/index.js +64 -0
- package/dist/preact/await.d.ts +7 -0
- package/dist/preact/await.js +19 -0
- package/dist/preact/client.d.ts +7 -0
- package/dist/preact/client.js +67 -0
- package/dist/preact/context.d.ts +18 -0
- package/dist/preact/context.js +23 -0
- package/dist/preact/document.d.ts +15 -0
- package/dist/preact/document.js +155 -0
- package/dist/{internal/preact → preact}/fetcher.d.ts +9 -5
- package/dist/preact/fetcher.js +45 -0
- package/dist/{internal/preact → preact}/form.d.ts +5 -6
- package/dist/preact/form.js +21 -0
- package/dist/{internal/preact → preact}/hooks.d.ts +7 -6
- package/dist/preact/hooks.js +25 -0
- package/dist/preact/index.d.ts +18 -0
- package/dist/preact/index.js +15 -0
- package/dist/preact/lazy-route.d.ts +19 -0
- package/dist/preact/lazy-route.js +51 -0
- package/dist/preact/navigation-state.d.ts +2 -0
- package/dist/preact/navigation-state.js +9 -0
- package/dist/preact/navigation.d.ts +28 -0
- package/dist/preact/navigation.js +60 -0
- package/dist/preact/root-context.d.ts +13 -0
- package/dist/preact/root-context.js +9 -0
- package/dist/preact/root-data.d.ts +8 -0
- package/dist/preact/root-data.js +28 -0
- package/dist/preact/route-hooks.d.ts +32 -0
- package/dist/preact/route-hooks.js +94 -0
- package/dist/preact/route-types.d.ts +54 -0
- package/dist/preact/router.d.ts +16 -0
- package/dist/preact/router.js +63 -0
- package/dist/{internal/preact → preact}/routes.d.ts +1 -1
- package/dist/preact/routes.js +12 -0
- package/dist/{internal/preact → preact}/runtime.js +1 -1
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +23 -0
- package/dist/react/data.d.ts +5 -0
- package/dist/react/data.js +3 -0
- package/dist/{internal/react → react}/fetcher.d.ts +2 -2
- package/dist/react/fetcher.js +34 -0
- package/dist/{internal/react → react}/form.d.ts +5 -6
- package/dist/react/form.js +21 -0
- package/dist/react/hooks.js +25 -0
- package/dist/{internal/react → react}/index.d.ts +1 -1
- package/dist/react/index.js +10 -0
- package/dist/{internal/react → react}/link.d.ts +2 -2
- package/dist/react/link.js +27 -0
- package/dist/react/resources/components.d.ts +64 -0
- package/dist/react/resources/components.js +239 -0
- package/dist/{internal/react → react}/routes.d.ts +1 -1
- package/dist/react/routes.js +12 -0
- package/dist/react/runtime.js +1 -0
- package/dist/server/application.d.ts +4 -0
- package/dist/server/application.js +119 -0
- package/dist/server/assets.d.ts +5 -0
- package/dist/server/assets.js +7 -0
- package/dist/server/handler.d.ts +19 -0
- package/dist/server/handler.js +22 -0
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +27 -0
- package/dist/{internal/server → server}/index.d.ts +2 -3
- package/dist/server/index.js +76 -0
- package/dist/server/node.d.ts +3 -0
- package/dist/server/node.js +3 -0
- package/dist/server/preact.d.ts +3 -0
- package/dist/server/preact.js +4 -0
- package/dist/vite/application/browser-boundary.d.ts +3 -0
- package/dist/vite/application/browser-boundary.js +35 -0
- package/dist/vite/application/lazy-files.d.ts +2 -0
- package/dist/vite/application/lazy-files.js +87 -0
- package/dist/vite/application/vite.d.ts +9 -0
- package/dist/vite/application/vite.js +101 -0
- package/dist/{internal/vite → vite}/index.d.ts +2 -1
- package/dist/vite/index.js +104 -0
- package/dist/vite/preact.d.ts +11 -0
- package/dist/vite/preact.js +15 -0
- package/dist/vite/react.d.ts +9 -0
- package/dist/vite/react.js +14 -0
- package/dist/{internal/vite → vite}/transport.d.ts +1 -1
- package/dist/vite/transport.js +35 -0
- package/jsr.json +28 -11
- package/package.json +119 -38
- package/dist/client.d.ts +0 -2
- package/dist/client.js +0 -2
- package/dist/core.d.ts +0 -2
- package/dist/core.js +0 -2
- package/dist/files.d.ts +0 -2
- package/dist/files.js +0 -2
- package/dist/internal/client/deserialize.js +0 -11
- package/dist/internal/client/index.js +0 -44
- package/dist/internal/core/errors.js +0 -6
- package/dist/internal/core/index.js +0 -6
- package/dist/internal/core/match.js +0 -27
- package/dist/internal/core/router.js +0 -189
- package/dist/internal/core/types.js +0 -5
- package/dist/internal/core/url.d.ts +0 -2
- package/dist/internal/core/url.js +0 -6
- package/dist/internal/files/index.js +0 -77
- package/dist/internal/preact/context.d.ts +0 -18
- package/dist/internal/preact/context.js +0 -20
- package/dist/internal/preact/fetcher.js +0 -28
- package/dist/internal/preact/form.js +0 -41
- package/dist/internal/preact/hooks.js +0 -35
- package/dist/internal/preact/index.d.ts +0 -9
- package/dist/internal/preact/index.js +0 -7
- package/dist/internal/preact/link.d.ts +0 -12
- package/dist/internal/preact/link.js +0 -28
- package/dist/internal/preact/routes.js +0 -22
- package/dist/internal/react/context.d.ts +0 -18
- package/dist/internal/react/context.js +0 -20
- package/dist/internal/react/fetcher.js +0 -28
- package/dist/internal/react/form.js +0 -41
- package/dist/internal/react/hooks.js +0 -35
- package/dist/internal/react/index.js +0 -7
- package/dist/internal/react/link.js +0 -28
- package/dist/internal/react/routes.js +0 -22
- package/dist/internal/react/runtime.js +0 -2
- package/dist/internal/server/index.js +0 -81
- package/dist/internal/vite/index.js +0 -132
- package/dist/internal/vite/transport.js +0 -42
- package/dist/preact.d.ts +0 -2
- package/dist/preact.js +0 -2
- package/dist/react.d.ts +0 -2
- package/dist/react.js +0 -2
- package/dist/server.d.ts +0 -2
- package/dist/server.js +0 -2
- package/dist/vite.d.ts +0 -2
- package/dist/vite.js +0 -2
- package/src/client.ts +0 -2
- package/src/core.ts +0 -2
- package/src/files.ts +0 -2
- package/src/internal/core/url.ts +0 -6
- package/src/internal/preact/context.ts +0 -46
- package/src/internal/preact/form.ts +0 -79
- package/src/internal/preact/link.ts +0 -46
- package/src/internal/react/context.ts +0 -46
- package/src/internal/react/form.ts +0 -79
- package/src/internal/react/index.ts +0 -13
- package/src/preact.ts +0 -2
- package/src/react.ts +0 -2
- package/src/server.ts +0 -2
- package/src/vite.ts +0 -2
- /package/{src/internal → .generated}/client/deserialize.ts +0 -0
- /package/{src/internal → .generated}/core/match.ts +0 -0
- /package/{src/internal → .generated}/files/index.ts +0 -0
- /package/{src/internal → .generated}/preact/runtime.ts +0 -0
- /package/{src/internal → .generated}/react/runtime.ts +0 -0
- /package/dist/{internal/client → client}/deserialize.d.ts +0 -0
- /package/dist/{internal/core → core}/errors.d.ts +0 -0
- /package/dist/{internal/core → core}/match.d.ts +0 -0
- /package/dist/{internal/files → files}/index.d.ts +0 -0
- /package/dist/{internal/preact → preact}/runtime.d.ts +0 -0
- /package/dist/{internal/react → react}/hooks.d.ts +0 -0
- /package/dist/{internal/react → react}/runtime.d.ts +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import type {
|
|
3
|
+
DataResult,
|
|
4
|
+
NavigationState,
|
|
5
|
+
Params,
|
|
6
|
+
ResourceModule,
|
|
7
|
+
RouteState,
|
|
8
|
+
} from "../../core/resource-types.ts";
|
|
9
|
+
import type { ResourceRouter } from "../../core/resources.ts";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
Component,
|
|
13
|
+
type ComponentType,
|
|
14
|
+
createContext,
|
|
15
|
+
type PropsWithChildren,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
useCallback,
|
|
18
|
+
useContext,
|
|
19
|
+
useEffect,
|
|
20
|
+
useMemo,
|
|
21
|
+
useRef,
|
|
22
|
+
useState,
|
|
23
|
+
useSyncExternalStore,
|
|
24
|
+
} from "react";
|
|
25
|
+
|
|
26
|
+
export interface ResourceRouteModule extends ResourceModule {
|
|
27
|
+
default?: ComponentType;
|
|
28
|
+
ErrorBoundary?: ComponentType<{ retry: () => void }>;
|
|
29
|
+
HydrateFallback?: ComponentType;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const RouterContext = createContext<ResourceRouter | null>(null);
|
|
33
|
+
const RouteContext = createContext<{ params: Params; routeId: string } | null>(
|
|
34
|
+
null,
|
|
35
|
+
);
|
|
36
|
+
const FormContext = createContext<
|
|
37
|
+
((input?: unknown) => Promise<DataResult<unknown>>) | null
|
|
38
|
+
>(null);
|
|
39
|
+
|
|
40
|
+
interface RouterProviderProps extends PropsWithChildren {
|
|
41
|
+
initialMatches?: readonly { params?: Params; routeId: string }[];
|
|
42
|
+
router: ResourceRouter;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function RouterProvider({
|
|
46
|
+
children,
|
|
47
|
+
initialMatches = EMPTY_MATCHES,
|
|
48
|
+
router,
|
|
49
|
+
}: RouterProviderProps): import("react").JSX.Element {
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
for (const match of initialMatches) {
|
|
52
|
+
void router.load(match.routeId, match.params ?? {});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return () => router.dispose();
|
|
56
|
+
}, [initialMatches, router]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<RouterContext.Provider value={router}>{children}</RouterContext.Provider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RouteProps extends PropsWithChildren {
|
|
64
|
+
params?: Params;
|
|
65
|
+
persistent?: boolean;
|
|
66
|
+
routeId: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function Route({
|
|
70
|
+
children,
|
|
71
|
+
params = {},
|
|
72
|
+
persistent = false,
|
|
73
|
+
routeId,
|
|
74
|
+
}: RouteProps): import("react").JSX.Element {
|
|
75
|
+
const router = useRouter();
|
|
76
|
+
const stableParams = useStableParams(params);
|
|
77
|
+
const state = useRouteState(routeId, stableParams, true);
|
|
78
|
+
const route = router.getRoute(routeId) as ResourceRouteModule | null;
|
|
79
|
+
const retry = useCallback(
|
|
80
|
+
() => void router.retry(routeId, stableParams),
|
|
81
|
+
[routeId, router, stableParams],
|
|
82
|
+
);
|
|
83
|
+
const contextValue = useMemo(
|
|
84
|
+
() => ({ params: stableParams, routeId }),
|
|
85
|
+
[routeId, stableParams],
|
|
86
|
+
);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (persistent) router.persist(routeId, stableParams);
|
|
89
|
+
}, [persistent, routeId, router, stableParams]);
|
|
90
|
+
let content: ReactNode = children;
|
|
91
|
+
|
|
92
|
+
if (children === undefined && route?.default) {
|
|
93
|
+
const RouteComponent = route.default;
|
|
94
|
+
content = <RouteComponent />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (state.unexpectedError && route?.ErrorBoundary) {
|
|
98
|
+
const ErrorBoundary = route.ErrorBoundary;
|
|
99
|
+
content = <ErrorBoundary retry={retry} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
!state.unexpectedError &&
|
|
104
|
+
!state.loaderInitialized &&
|
|
105
|
+
route?.HydrateFallback
|
|
106
|
+
) {
|
|
107
|
+
const HydrateFallback = route.HydrateFallback;
|
|
108
|
+
content = <HydrateFallback />;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<RouteContext.Provider value={contextValue}>
|
|
113
|
+
<RenderErrorBoundary
|
|
114
|
+
onRetry={retry}
|
|
115
|
+
resetKey={`${routeId}:${JSON.stringify(stableParams)}`}
|
|
116
|
+
{...(route?.ErrorBoundary ? { fallback: route.ErrorBoundary } : {})}
|
|
117
|
+
>
|
|
118
|
+
{content}
|
|
119
|
+
</RenderErrorBoundary>
|
|
120
|
+
</RouteContext.Provider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function useLoaderData<Data>(): Data | null {
|
|
125
|
+
const route = useCurrentRoute();
|
|
126
|
+
|
|
127
|
+
return useRouteLoaderData<Data>(route.routeId, route.params);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function useRouteLoaderData<Data>(
|
|
131
|
+
routeId: string,
|
|
132
|
+
params: Params = {},
|
|
133
|
+
): Data | null {
|
|
134
|
+
const state = useRouteState(routeId, useStableParams(params), true);
|
|
135
|
+
|
|
136
|
+
return state.loaderData as Data | null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function useActionData<Data>(): Data | null {
|
|
140
|
+
const route = useCurrentRoute();
|
|
141
|
+
const state = useRouteState(route.routeId, route.params, false);
|
|
142
|
+
|
|
143
|
+
return state.actionData as Data | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function useNavigation(): { state: NavigationState } {
|
|
147
|
+
const route = useCurrentRoute();
|
|
148
|
+
const state = useRouteState(route.routeId, route.params, false);
|
|
149
|
+
|
|
150
|
+
return { state: state.navigationState };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function useRevalidator(): {
|
|
154
|
+
revalidate: () => Promise<DataResult<unknown>>;
|
|
155
|
+
state: string;
|
|
156
|
+
} {
|
|
157
|
+
const route = useCurrentRoute();
|
|
158
|
+
const router = useRouter();
|
|
159
|
+
const state = useRouteState(route.routeId, route.params, false);
|
|
160
|
+
|
|
161
|
+
return useMemo(
|
|
162
|
+
() => ({
|
|
163
|
+
revalidate: () => router.load(route.routeId, route.params, true),
|
|
164
|
+
state: state.navigationState === "loading" ? "loading" : "idle",
|
|
165
|
+
}),
|
|
166
|
+
[route, router, state.navigationState],
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface FetcherOptions {
|
|
171
|
+
params?: Params;
|
|
172
|
+
routeId?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface FetcherTarget {
|
|
176
|
+
params?: Params;
|
|
177
|
+
routeId?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface FormProps extends PropsWithChildren {
|
|
181
|
+
action?: string;
|
|
182
|
+
input?: unknown;
|
|
183
|
+
params?: Params;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function Form({
|
|
187
|
+
action,
|
|
188
|
+
children,
|
|
189
|
+
input,
|
|
190
|
+
params,
|
|
191
|
+
}: FormProps): import("react").JSX.Element {
|
|
192
|
+
const currentRoute = useContext(RouteContext);
|
|
193
|
+
const router = useRouter();
|
|
194
|
+
const routeId = action ?? currentRoute?.routeId ?? "";
|
|
195
|
+
const routeParams = useStableParams(params ?? currentRoute?.params ?? {});
|
|
196
|
+
const submit = useCallback(
|
|
197
|
+
(override?: unknown) =>
|
|
198
|
+
router.submit(
|
|
199
|
+
routeId,
|
|
200
|
+
override === undefined ? input : override,
|
|
201
|
+
routeParams,
|
|
202
|
+
),
|
|
203
|
+
[input, routeId, routeParams, router],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!routeId) throw new Error("Form requires an action or a parent Route.");
|
|
207
|
+
|
|
208
|
+
return <FormContext.Provider value={submit}>{children}</FormContext.Provider>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function useSubmit(): (input?: unknown) => Promise<DataResult<unknown>> {
|
|
212
|
+
const submit = useContext(FormContext);
|
|
213
|
+
|
|
214
|
+
if (!submit) throw new Error("useSubmit must be used inside Form.");
|
|
215
|
+
|
|
216
|
+
return submit;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function useFetcher<Data = unknown>(
|
|
220
|
+
options: FetcherOptions = {},
|
|
221
|
+
): readonly [
|
|
222
|
+
{ data: Data | null; error: string; state: NavigationState },
|
|
223
|
+
{
|
|
224
|
+
Form: ({
|
|
225
|
+
children,
|
|
226
|
+
input,
|
|
227
|
+
}: Pick<FormProps, "children" | "input">) => import("react").JSX.Element;
|
|
228
|
+
load: (target?: FetcherTarget) => Promise<DataResult<Data>>;
|
|
229
|
+
submit: (
|
|
230
|
+
input: unknown,
|
|
231
|
+
target?: FetcherTarget,
|
|
232
|
+
) => Promise<DataResult<Data>>;
|
|
233
|
+
},
|
|
234
|
+
] {
|
|
235
|
+
const currentRoute = useContext(RouteContext);
|
|
236
|
+
const router = useRouter();
|
|
237
|
+
const routeId = options.routeId ?? currentRoute?.routeId ?? "";
|
|
238
|
+
const params = useStableParams(options.params ?? currentRoute?.params ?? {});
|
|
239
|
+
const [data, setData] = useState<Data | null>(null);
|
|
240
|
+
const [error, setError] = useState("");
|
|
241
|
+
const [state, setState] = useState<NavigationState>("idle");
|
|
242
|
+
const controller = useRef<AbortController | null>(null);
|
|
243
|
+
useEffect(
|
|
244
|
+
() => () => {
|
|
245
|
+
controller.current?.abort();
|
|
246
|
+
},
|
|
247
|
+
[],
|
|
248
|
+
);
|
|
249
|
+
const run = useCallback(
|
|
250
|
+
async (mutating: boolean, input: unknown, target: FetcherTarget) => {
|
|
251
|
+
controller.current?.abort();
|
|
252
|
+
|
|
253
|
+
const nextController = new AbortController();
|
|
254
|
+
const { signal } = nextController;
|
|
255
|
+
const targetId = target.routeId ?? routeId;
|
|
256
|
+
const targetParams = target.params ?? params;
|
|
257
|
+
|
|
258
|
+
controller.current = nextController;
|
|
259
|
+
setError("");
|
|
260
|
+
setState(mutating ? "submitting" : "loading");
|
|
261
|
+
|
|
262
|
+
const result = await (mutating
|
|
263
|
+
? router.fetchAction<Data>(targetId, input, targetParams, signal)
|
|
264
|
+
: router.fetchLoader<Data>(targetId, targetParams, signal));
|
|
265
|
+
|
|
266
|
+
if (signal.aborted) return result;
|
|
267
|
+
|
|
268
|
+
controller.current = null;
|
|
269
|
+
setData(result.data);
|
|
270
|
+
setError(result.error);
|
|
271
|
+
setState("idle");
|
|
272
|
+
|
|
273
|
+
if (result.unexpected && currentRoute) {
|
|
274
|
+
router.reportUnexpected(currentRoute.routeId, currentRoute.params);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
},
|
|
279
|
+
[currentRoute, params, routeId, router],
|
|
280
|
+
);
|
|
281
|
+
const submit = useCallback(
|
|
282
|
+
(input: unknown, target: FetcherTarget = {}) => run(true, input, target),
|
|
283
|
+
[run],
|
|
284
|
+
);
|
|
285
|
+
const load = useCallback(
|
|
286
|
+
(target: FetcherTarget = {}) => run(false, undefined, target),
|
|
287
|
+
[run],
|
|
288
|
+
);
|
|
289
|
+
const BoundForm = useCallback(
|
|
290
|
+
({ children, input }: Pick<FormProps, "children" | "input">) => (
|
|
291
|
+
<FormContext.Provider
|
|
292
|
+
value={(override) => submit(override === undefined ? input : override)}
|
|
293
|
+
>
|
|
294
|
+
{children}
|
|
295
|
+
</FormContext.Provider>
|
|
296
|
+
),
|
|
297
|
+
[submit],
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (!routeId)
|
|
301
|
+
throw new Error("useFetcher requires a routeId or a parent Route.");
|
|
302
|
+
|
|
303
|
+
const fetcherState = useMemo(
|
|
304
|
+
() => ({ data, error, state }),
|
|
305
|
+
[data, error, state],
|
|
306
|
+
);
|
|
307
|
+
const fetcherOperations = useMemo(
|
|
308
|
+
() => ({ Form: BoundForm, load, submit }),
|
|
309
|
+
[BoundForm, load, submit],
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return [fetcherState, fetcherOperations] as const;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function useRouter(): ResourceRouter {
|
|
316
|
+
const router = useContext(RouterContext);
|
|
317
|
+
|
|
318
|
+
if (!router) throw new Error("RouterProvider is missing.");
|
|
319
|
+
|
|
320
|
+
return router;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function useCurrentRoute() {
|
|
324
|
+
const route = useContext(RouteContext);
|
|
325
|
+
|
|
326
|
+
if (!route) throw new Error("This hook requires a parent Route.");
|
|
327
|
+
|
|
328
|
+
return route;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function useRouteState(routeId: string, params: Params, loadOnMount: boolean) {
|
|
332
|
+
const router = useRouter();
|
|
333
|
+
const subscribe = useCallback(
|
|
334
|
+
(listener: () => void) => router.subscribe(routeId, params, listener),
|
|
335
|
+
[params, routeId, router],
|
|
336
|
+
);
|
|
337
|
+
const getSnapshot = useCallback(
|
|
338
|
+
() => router.getSnapshot(routeId, params),
|
|
339
|
+
[params, routeId, router],
|
|
340
|
+
);
|
|
341
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
if (loadOnMount) void router.load(routeId, params);
|
|
344
|
+
}, [loadOnMount, params, routeId, router]);
|
|
345
|
+
|
|
346
|
+
return state;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function useStableParams(params: Params) {
|
|
350
|
+
const key = JSON.stringify(
|
|
351
|
+
Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
|
|
352
|
+
);
|
|
353
|
+
const ref = useRef({ key, value: params });
|
|
354
|
+
|
|
355
|
+
if (ref.current.key !== key) ref.current = { key, value: params };
|
|
356
|
+
|
|
357
|
+
return ref.current.value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
interface RenderErrorBoundaryProps extends PropsWithChildren {
|
|
361
|
+
fallback?: ComponentType<{ retry: () => void }>;
|
|
362
|
+
onRetry: () => void;
|
|
363
|
+
resetKey: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
class RenderErrorBoundary extends Component<
|
|
367
|
+
RenderErrorBoundaryProps,
|
|
368
|
+
{ error: Error | null }
|
|
369
|
+
> {
|
|
370
|
+
override state = { error: null };
|
|
371
|
+
|
|
372
|
+
static getDerivedStateFromError(error: Error) {
|
|
373
|
+
return { error };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
override componentDidUpdate(previousProps: RenderErrorBoundaryProps) {
|
|
377
|
+
if (this.state.error && previousProps.resetKey !== this.props.resetKey) {
|
|
378
|
+
this.setState({ error: null });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
override render() {
|
|
383
|
+
if (this.state.error && this.props.fallback) {
|
|
384
|
+
const ErrorBoundary = this.props.fallback;
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<ErrorBoundary
|
|
388
|
+
retry={() => {
|
|
389
|
+
this.setState({ error: null });
|
|
390
|
+
this.props.onRetry();
|
|
391
|
+
}}
|
|
392
|
+
/>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (this.state.error) throw this.state.error;
|
|
397
|
+
|
|
398
|
+
return this.props.children;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export type { NavigationState, RouteState };
|
|
403
|
+
|
|
404
|
+
const EMPTY_MATCHES: readonly { params?: Params; routeId: string }[] = [];
|
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Route as RouteDefinition, RouteModule } from "../core/index.ts";
|
|
7
7
|
import { useRouter, useSnapshot } from "./context.ts";
|
|
8
|
-
|
|
9
|
-
import {
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type ComponentType,
|
|
11
|
+
createElement,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
useMemo,
|
|
14
|
+
} from "./runtime.ts";
|
|
10
15
|
|
|
11
16
|
/** Renderable route module with optional loading and error components. */
|
|
12
17
|
export interface ViewModule extends RouteModule {
|
|
@@ -25,17 +30,20 @@ export interface RoutesProps {
|
|
|
25
30
|
fallback?: ReactNode;
|
|
26
31
|
}
|
|
27
32
|
/** Renders the first matching registered module. */
|
|
28
|
-
export function Routes({ fallback
|
|
33
|
+
export function Routes({ fallback }: RoutesProps): ReactNode {
|
|
29
34
|
const router = useRouter();
|
|
30
|
-
const state = useSnapshot(
|
|
35
|
+
const state = useSnapshot();
|
|
31
36
|
const module = router.routes.find((route) => route.path === state.path) as
|
|
32
37
|
| RouteDefinition<ViewModule>
|
|
33
38
|
| undefined;
|
|
34
|
-
const component =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const component =
|
|
40
|
+
module?.[
|
|
41
|
+
state.error
|
|
42
|
+
? "ErrorBoundary"
|
|
43
|
+
: state.state === "loading" && !state.loaded
|
|
44
|
+
? "HydrateFallback"
|
|
45
|
+
: "default"
|
|
46
|
+
];
|
|
39
47
|
|
|
40
48
|
const element = useMemo(
|
|
41
49
|
() =>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
|
|
3
|
+
import { createDataRouter, matchRoutes } from "../core/index.ts";
|
|
4
|
+
import {
|
|
5
|
+
type DataResponse,
|
|
6
|
+
normalizeResult,
|
|
7
|
+
resolveData,
|
|
8
|
+
} from "../core/responses.ts";
|
|
9
|
+
import type { AppRoute, RootModule } from "../preact/route-types.ts";
|
|
10
|
+
import { prepareRoutes, RouterApplication } from "../preact/router.tsx";
|
|
11
|
+
import { loadRouter } from "./index.ts";
|
|
12
|
+
import { renderToStringAsync } from "preact-render-to-string";
|
|
13
|
+
|
|
14
|
+
export async function renderApplication(
|
|
15
|
+
request: Request,
|
|
16
|
+
routes: AppRoute[],
|
|
17
|
+
rootModule: RootModule,
|
|
18
|
+
entry: string,
|
|
19
|
+
): Promise<Response> {
|
|
20
|
+
routes = routes.map((route) => ({
|
|
21
|
+
...route,
|
|
22
|
+
path: route.routePath ?? route.path,
|
|
23
|
+
}));
|
|
24
|
+
const match = matchRoutes(routes, new URL(request.url).pathname);
|
|
25
|
+
|
|
26
|
+
if (match && !match.route.default)
|
|
27
|
+
return dispatchRoute(request, routes, true);
|
|
28
|
+
|
|
29
|
+
let actionResponse: Response | undefined;
|
|
30
|
+
let actionData: unknown;
|
|
31
|
+
|
|
32
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
33
|
+
actionResponse = await dispatchRoute(request, routes, true);
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
actionResponse.headers.has("Location") ||
|
|
37
|
+
!actionResponse.headers.get("Content-Type")?.includes("application/json")
|
|
38
|
+
)
|
|
39
|
+
return actionResponse;
|
|
40
|
+
|
|
41
|
+
actionData = await normalizeResult(await actionResponse.json());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const root = await resolveData(
|
|
45
|
+
await rootModule.loader?.({
|
|
46
|
+
request,
|
|
47
|
+
params: {},
|
|
48
|
+
signal: request.signal,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
let redirectResponse: Response | undefined;
|
|
52
|
+
const prepared = prepareRoutes(
|
|
53
|
+
routes.map((route) => ({
|
|
54
|
+
...route,
|
|
55
|
+
ErrorBoundary: route.ErrorBoundary ?? rootModule.ErrorBoundary,
|
|
56
|
+
})),
|
|
57
|
+
).map((route) => {
|
|
58
|
+
if (!route.loader) return route;
|
|
59
|
+
|
|
60
|
+
const loader = route.loader;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...route,
|
|
64
|
+
loader: async (args: Parameters<typeof loader>[0]) => {
|
|
65
|
+
const value = await loader(args);
|
|
66
|
+
|
|
67
|
+
if (value instanceof Response && value.headers.has("Location"))
|
|
68
|
+
redirectResponse = value;
|
|
69
|
+
|
|
70
|
+
return value;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
let router = await loadRouter(prepared, request);
|
|
75
|
+
|
|
76
|
+
if (redirectResponse) {
|
|
77
|
+
router.dispose();
|
|
78
|
+
|
|
79
|
+
return redirectResponse;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (actionResponse) {
|
|
83
|
+
const hydrationData = {
|
|
84
|
+
...router.getSnapshot(),
|
|
85
|
+
actionData: actionData instanceof Error ? undefined : actionData,
|
|
86
|
+
...(actionData instanceof Error && { error: actionData }),
|
|
87
|
+
};
|
|
88
|
+
router.dispose();
|
|
89
|
+
router = createDataRouter(router.routes, { hydrationData });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const snapshot = router.getSnapshot();
|
|
93
|
+
const html = await renderToStringAsync(
|
|
94
|
+
<RouterApplication
|
|
95
|
+
router={router}
|
|
96
|
+
routes={routes}
|
|
97
|
+
root={root}
|
|
98
|
+
rootModule={rootModule}
|
|
99
|
+
entry={entry}
|
|
100
|
+
document
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
router.dispose();
|
|
104
|
+
const headers = new Headers(actionResponse?.headers);
|
|
105
|
+
headers.delete("Content-Length");
|
|
106
|
+
headers.delete("Content-Encoding");
|
|
107
|
+
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
108
|
+
headers.set("Cache-Control", "no-store");
|
|
109
|
+
|
|
110
|
+
return new Response(`<!doctype html>${html}`, {
|
|
111
|
+
status: snapshot.error
|
|
112
|
+
? Number("status" in snapshot.error ? snapshot.error.status : 500)
|
|
113
|
+
: (actionResponse?.status ?? 200),
|
|
114
|
+
headers,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function dispatchRoute(
|
|
119
|
+
request: Request,
|
|
120
|
+
routes: AppRoute[],
|
|
121
|
+
document = false,
|
|
122
|
+
): Promise<Response> {
|
|
123
|
+
const match = matchRoutes(
|
|
124
|
+
routes.map((route) => ({ ...route, path: route.routePath ?? route.path })),
|
|
125
|
+
new URL(request.url).pathname,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!match) return Response.json({ error: "No route" }, { status: 404 });
|
|
129
|
+
|
|
130
|
+
const handler =
|
|
131
|
+
request.method === "GET" || request.method === "HEAD"
|
|
132
|
+
? match.route.loader
|
|
133
|
+
: match.route.action;
|
|
134
|
+
|
|
135
|
+
if (!handler) return Response.json({ error: "No handler" }, { status: 405 });
|
|
136
|
+
|
|
137
|
+
const value = await Promise.resolve()
|
|
138
|
+
.then(() =>
|
|
139
|
+
handler({ request, params: match.params, signal: request.signal }),
|
|
140
|
+
)
|
|
141
|
+
.then(
|
|
142
|
+
(result) => result,
|
|
143
|
+
(error: unknown) => error,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (value instanceof Response) {
|
|
147
|
+
const location = value.headers.get("Location");
|
|
148
|
+
|
|
149
|
+
if (document) return value;
|
|
150
|
+
|
|
151
|
+
if (!location) {
|
|
152
|
+
if (value.ok) return value;
|
|
153
|
+
|
|
154
|
+
return Response.json({
|
|
155
|
+
__routeError: {
|
|
156
|
+
message: await value.text(),
|
|
157
|
+
status: value.status,
|
|
158
|
+
statusText: value.statusText,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const headers = new Headers(value.headers);
|
|
164
|
+
headers.delete("Location");
|
|
165
|
+
headers.set("X-Smolter-Redirect", location);
|
|
166
|
+
|
|
167
|
+
return new Response(null, { headers });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (value instanceof Error)
|
|
171
|
+
return Response.json({
|
|
172
|
+
__routeError: { ...value, message: value.message },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (value && typeof value === "object" && "__routeData" in value) {
|
|
176
|
+
const result = value as DataResponse<unknown>;
|
|
177
|
+
|
|
178
|
+
return Response.json(
|
|
179
|
+
await resolveData(result.value),
|
|
180
|
+
document ? result.init : { headers: result.init.headers },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Response.json((await normalizeResult(value)) ?? null);
|
|
185
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { matchRoutes } from "../core/index.ts";
|
|
2
|
+
|
|
3
|
+
export interface RouteAssets {
|
|
4
|
+
path: string;
|
|
5
|
+
files: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function routePreloads(
|
|
9
|
+
routes: RouteAssets[],
|
|
10
|
+
pathname: string,
|
|
11
|
+
base: string,
|
|
12
|
+
): string {
|
|
13
|
+
const match = matchRoutes(routes, pathname);
|
|
14
|
+
const route = match?.route as RouteAssets | undefined;
|
|
15
|
+
|
|
16
|
+
return (route?.files ?? [])
|
|
17
|
+
.map((file) => `<${base.replace(/\/$/, "")}/${file}>; rel=modulepreload`)
|
|
18
|
+
.join(", ");
|
|
19
|
+
}
|