@kasperrt/smolter 0.4.2 → 0.4.4
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/.generated/client/hash-history.ts +27 -0
- package/.generated/client/hydration.ts +98 -0
- package/.generated/client/root-data.ts +52 -0
- package/.generated/core/environment.ts +1 -0
- package/.generated/core/navigation.ts +1 -0
- package/.generated/core/prepare-routes.ts +50 -0
- package/.generated/core/presentation.ts +1 -1
- package/.generated/core/responses.ts +12 -0
- package/.generated/core/utils.ts +5 -2
- package/.generated/preact/await.tsx +1 -0
- package/.generated/preact/client.tsx +8 -27
- package/.generated/preact/deferred-hydration.ts +1 -98
- package/.generated/preact/document.tsx +10 -0
- package/.generated/preact/index.ts +11 -2
- package/.generated/preact/lazy-route.ts +7 -0
- package/.generated/preact/navigation.tsx +19 -5
- package/.generated/preact/root-data.ts +1 -45
- package/.generated/preact/route-hooks.tsx +12 -2
- package/.generated/preact/route-types.ts +14 -0
- package/.generated/preact/router.tsx +5 -48
- package/.generated/react/client.tsx +130 -0
- package/.generated/react/document.tsx +235 -0
- package/.generated/react/fetcher.ts +29 -7
- package/.generated/react/hooks.ts +20 -18
- package/.generated/react/index.ts +28 -2
- package/.generated/react/lazy-route.ts +116 -0
- package/.generated/react/link.ts +1 -38
- package/.generated/react/navigation-state.ts +13 -0
- package/.generated/react/navigation.ts +145 -0
- package/.generated/react/outlet.ts +48 -0
- package/.generated/react/root-context.ts +22 -0
- package/.generated/react/route-hooks.ts +159 -0
- package/.generated/react/route-types.ts +91 -0
- package/.generated/react/router.ts +50 -0
- package/.generated/react/runtime.ts +1 -0
- package/.generated/server/application.tsx +18 -250
- package/.generated/server/assets.ts +7 -0
- package/.generated/server/dispatch.ts +87 -0
- package/.generated/server/handler.ts +3 -0
- package/.generated/server/http.ts +2 -0
- package/.generated/server/index.ts +2 -0
- package/.generated/server/node.ts +5 -2
- package/.generated/server/preact.ts +5 -2
- package/.generated/server/react.ts +34 -0
- package/.generated/server/render-application.ts +203 -0
- package/.generated/vite/application/lazy-files.ts +2 -1
- package/.generated/vite/application/vite.ts +12 -4
- package/.generated/vite/react.ts +11 -0
- package/CHANGELOG.md +20 -0
- package/README.md +48 -48
- package/dist/client/hash-history.d.ts +2 -0
- package/dist/client/hash-history.js +26 -0
- package/dist/client/hydration.d.ts +10 -0
- package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
- package/dist/client/root-data.d.ts +14 -0
- package/dist/{preact → client}/root-data.js +1 -1
- package/dist/core/environment.d.ts +1 -0
- package/dist/core/navigation.d.ts +1 -0
- package/dist/core/prepare-routes.d.ts +7 -0
- package/dist/core/prepare-routes.js +28 -0
- package/dist/core/presentation.js +1 -1
- package/dist/core/responses.d.ts +12 -0
- package/dist/core/responses.js +5 -0
- package/dist/core/utils.d.ts +5 -2
- package/dist/core/utils.js +1 -2
- package/dist/preact/await.d.ts +1 -0
- package/dist/preact/await.js +1 -0
- package/dist/preact/client.d.ts +7 -1
- package/dist/preact/client.js +11 -27
- package/dist/preact/deferred-hydration.d.ts +1 -10
- package/dist/preact/document.d.ts +10 -0
- package/dist/preact/document.js +11 -2
- package/dist/preact/index.d.ts +6 -2
- package/dist/preact/index.js +1 -1
- package/dist/preact/lazy-route.d.ts +2 -0
- package/dist/preact/lazy-route.js +6 -0
- package/dist/preact/navigation-state.js +1 -1
- package/dist/preact/navigation.d.ts +6 -0
- package/dist/preact/navigation.js +9 -5
- package/dist/preact/root-data.d.ts +1 -8
- package/dist/preact/route-hooks.d.ts +10 -0
- package/dist/preact/route-hooks.js +13 -3
- package/dist/preact/route-types.d.ts +14 -0
- package/dist/preact/router.d.ts +5 -2
- package/dist/preact/router.js +4 -26
- package/dist/react/client.d.ts +20 -0
- package/dist/react/client.js +86 -0
- package/dist/react/document.d.ts +25 -0
- package/dist/react/document.js +169 -0
- package/dist/react/fetcher.d.ts +8 -4
- package/dist/react/fetcher.js +12 -1
- package/dist/react/form.js +1 -1
- package/dist/react/hooks.d.ts +7 -6
- package/dist/react/index.d.ts +13 -2
- package/dist/react/index.js +7 -2
- package/dist/react/lazy-route.d.ts +22 -0
- package/dist/react/lazy-route.js +57 -0
- package/dist/react/link.d.ts +1 -12
- package/dist/react/navigation-state.d.ts +2 -0
- package/dist/react/navigation-state.js +9 -0
- package/dist/react/navigation.d.ts +33 -0
- package/dist/react/navigation.js +64 -0
- package/dist/react/outlet.d.ts +16 -0
- package/dist/react/outlet.js +26 -0
- package/dist/react/root-context.d.ts +14 -0
- package/dist/react/root-context.js +10 -0
- package/dist/react/route-hooks.d.ts +34 -0
- package/dist/react/route-hooks.js +89 -0
- package/dist/react/route-types.d.ts +69 -0
- package/dist/react/router.d.ts +10 -0
- package/dist/react/router.js +22 -0
- package/dist/react/runtime.d.ts +1 -1
- package/dist/react/runtime.js +1 -1
- package/dist/server/application.d.ts +4 -5
- package/dist/server/application.js +9 -154
- package/dist/server/assets.d.ts +2 -0
- package/dist/server/assets.js +6 -0
- package/dist/server/dispatch.d.ts +5 -0
- package/dist/server/dispatch.js +50 -0
- package/dist/server/handler.d.ts +3 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -1
- package/dist/server/node.d.ts +5 -1
- package/dist/server/preact.d.ts +5 -1
- package/dist/server/preact.js +2 -1
- package/dist/server/react.d.ts +10 -0
- package/dist/server/react.js +22 -0
- package/dist/server/render-application.d.ts +31 -0
- package/dist/server/render-application.js +118 -0
- package/dist/vite/application/lazy-files.d.ts +1 -1
- package/dist/vite/application/lazy-files.js +2 -2
- package/dist/vite/application/vite.d.ts +2 -1
- package/dist/vite/application/vite.js +12 -5
- package/dist/vite/react.d.ts +4 -0
- package/dist/vite/react.js +9 -1
- package/jsr.json +10 -6
- package/package.json +21 -12
- package/.generated/core/resource-types.ts +0 -44
- package/.generated/core/resources.ts +0 -444
- package/.generated/react/data.ts +0 -25
- package/.generated/react/resources/components.tsx +0 -404
- package/dist/core/resource-types.d.ts +0 -37
- package/dist/core/resources.d.ts +0 -19
- package/dist/core/resources.js +0 -345
- package/dist/react/data.d.ts +0 -5
- package/dist/react/data.js +0 -3
- package/dist/react/link.js +0 -27
- package/dist/react/resources/components.d.ts +0 -64
- package/dist/react/resources/components.js +0 -239
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { NavigateOptions, Router, SubmitOptions } from "../core/index.ts";
|
|
2
|
+
import type { NavigationType } from "../core/navigation.ts";
|
|
3
|
+
import { pendingLocation } from "../core/presentation.ts";
|
|
4
|
+
import {
|
|
5
|
+
type Submission,
|
|
6
|
+
type SubmissionOptions,
|
|
7
|
+
submissionBody,
|
|
8
|
+
submissionSearch,
|
|
9
|
+
} from "../core/submission.ts";
|
|
10
|
+
import { useRouter, useSnapshot } from "./context.ts";
|
|
11
|
+
import type { NavigateFunction } from "./navigation.ts";
|
|
12
|
+
import { useNavigationSnapshot } from "./navigation-state.ts";
|
|
13
|
+
import { useApplication } from "./root-context.ts";
|
|
14
|
+
import type { RouteData } from "./route-types.ts";
|
|
15
|
+
import { useCallback, useMemo } from "./runtime.ts";
|
|
16
|
+
|
|
17
|
+
/** Read root loader data from the application context. */
|
|
18
|
+
export function useRootData<T = unknown>(): RouteData<T> {
|
|
19
|
+
return useApplication().root as RouteData<T>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Read root data or the active route's data by its manifest identifier. */
|
|
23
|
+
export function useRouteLoaderData<T = unknown>(
|
|
24
|
+
id: string,
|
|
25
|
+
): RouteData<T> | undefined {
|
|
26
|
+
const app = useApplication();
|
|
27
|
+
const snapshot = useSnapshot();
|
|
28
|
+
|
|
29
|
+
if (id === "root") return app.root as RouteData<T>;
|
|
30
|
+
|
|
31
|
+
const route = app.routes.find((item) => item.id === id);
|
|
32
|
+
|
|
33
|
+
if (!route || route.path !== snapshot.path) return undefined;
|
|
34
|
+
|
|
35
|
+
return snapshot.data as RouteData<T>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { useSnapshot };
|
|
39
|
+
|
|
40
|
+
/** Read the current route error for an error boundary. */
|
|
41
|
+
export function useRouteError(): Error | undefined {
|
|
42
|
+
return useSnapshot("error");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read the current page navigation or submission state. */
|
|
46
|
+
export function useNavigation(): {
|
|
47
|
+
state: "idle" | "loading" | "submitting";
|
|
48
|
+
location: URL | undefined;
|
|
49
|
+
} {
|
|
50
|
+
const router = useRouter<Router>();
|
|
51
|
+
const snapshot = useSnapshot();
|
|
52
|
+
const location = pendingLocation(router);
|
|
53
|
+
|
|
54
|
+
return useMemo(
|
|
55
|
+
() => ({
|
|
56
|
+
state: snapshot.state,
|
|
57
|
+
location,
|
|
58
|
+
}),
|
|
59
|
+
[snapshot.state, location?.href],
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return a function that submits form data through the current router. */
|
|
64
|
+
export function useSubmit(): (
|
|
65
|
+
value: Submission,
|
|
66
|
+
options?: SubmissionOptions,
|
|
67
|
+
) => Promise<void> | Promise<import("../core/index.ts").Result<unknown>> {
|
|
68
|
+
const router = useRouter<Router>();
|
|
69
|
+
|
|
70
|
+
return useCallback(
|
|
71
|
+
(value: Submission, options: SubmissionOptions = {}) => {
|
|
72
|
+
const body = submissionBody(value, options.encType);
|
|
73
|
+
const method = options.method?.toUpperCase();
|
|
74
|
+
const target = options.action ?? router.getSnapshot().url;
|
|
75
|
+
|
|
76
|
+
if (method === "GET") {
|
|
77
|
+
const url = new URL(target, router.getSnapshot().url);
|
|
78
|
+
url.search = submissionSearch(body);
|
|
79
|
+
|
|
80
|
+
return router.navigate(url.href, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return router.submit(target, body, {
|
|
84
|
+
method: method as SubmitOptions["method"],
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
[router],
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Read the current router location as a URL. */
|
|
92
|
+
export function useURLLocation(): URL & { state: unknown; key: string } {
|
|
93
|
+
const href = useSnapshot("url");
|
|
94
|
+
const snapshot = useNavigationSnapshot(useRouter<Router>());
|
|
95
|
+
|
|
96
|
+
return useMemo(
|
|
97
|
+
() =>
|
|
98
|
+
Object.assign(new URL(href), {
|
|
99
|
+
state: snapshot.state,
|
|
100
|
+
key: snapshot.key,
|
|
101
|
+
}),
|
|
102
|
+
[href, snapshot.state, snapshot.key],
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Return the application navigation function, including history offsets and relative destinations. */
|
|
107
|
+
export function useNavigate(): NavigateFunction {
|
|
108
|
+
const router = useRouter<Router>();
|
|
109
|
+
|
|
110
|
+
return useCallback(
|
|
111
|
+
(to, options) => {
|
|
112
|
+
if (typeof to === "number") {
|
|
113
|
+
window.history.go(to);
|
|
114
|
+
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const current = new URL(router.getSnapshot().url);
|
|
119
|
+
const target =
|
|
120
|
+
typeof to === "string"
|
|
121
|
+
? to
|
|
122
|
+
: `${to.pathname ?? current.pathname}${to.search ?? current.search}${to.hash ?? current.hash}`;
|
|
123
|
+
void router.navigate(target, options);
|
|
124
|
+
},
|
|
125
|
+
[router],
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Read URL search parameters and update them through router navigation. */
|
|
130
|
+
export function useSearchParams(): [
|
|
131
|
+
URLSearchParams,
|
|
132
|
+
(
|
|
133
|
+
value: URLSearchParams | Record<string, string>,
|
|
134
|
+
options?: NavigateOptions,
|
|
135
|
+
) => void,
|
|
136
|
+
] {
|
|
137
|
+
const location = useURLLocation();
|
|
138
|
+
const navigate = useNavigate();
|
|
139
|
+
const params = useMemo(
|
|
140
|
+
() => new URLSearchParams(location.search),
|
|
141
|
+
[location.search],
|
|
142
|
+
);
|
|
143
|
+
const set = useCallback(
|
|
144
|
+
(
|
|
145
|
+
value: URLSearchParams | Record<string, string>,
|
|
146
|
+
options?: NavigateOptions,
|
|
147
|
+
) => {
|
|
148
|
+
navigate(`${location.pathname}?${new URLSearchParams(value)}`, options);
|
|
149
|
+
},
|
|
150
|
+
[location.pathname, navigate],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return [params, set];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Read the last browser history operation. */
|
|
157
|
+
export function useNavigationType(): NavigationType {
|
|
158
|
+
return useNavigationSnapshot(useRouter<Router>()).type;
|
|
159
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataArgs,
|
|
3
|
+
RevalidationArgs,
|
|
4
|
+
Route,
|
|
5
|
+
Snapshot,
|
|
6
|
+
} from "../core/index.ts";
|
|
7
|
+
import type { DataResponse } from "../core/responses.ts";
|
|
8
|
+
import type { ComponentType, ReactNode } from "./runtime.ts";
|
|
9
|
+
|
|
10
|
+
/** Request, route parameters and cancellation signal supplied to a loader. */
|
|
11
|
+
export interface LoaderFunctionArgs extends DataArgs {
|
|
12
|
+
context?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Request, route parameters and cancellation signal supplied to an action. */
|
|
16
|
+
export type ActionFunctionArgs = LoaderFunctionArgs;
|
|
17
|
+
|
|
18
|
+
/** Loader arguments with a serverLoader callback for the server transport. */
|
|
19
|
+
export interface ClientLoaderFunctionArgs extends LoaderFunctionArgs {
|
|
20
|
+
serverLoader: <T = unknown>() => Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Action arguments with a serverAction callback for the server transport. */
|
|
24
|
+
export interface ClientActionFunctionArgs extends LoaderFunctionArgs {
|
|
25
|
+
serverAction: <T = unknown>() => Promise<T>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Navigation or mutation context used to decide whether loader data should refresh. */
|
|
29
|
+
export type ShouldRevalidateFunctionArgs = RevalidationArgs;
|
|
30
|
+
|
|
31
|
+
type UnwrapData<T> = T extends DataResponse<infer V> ? V : T;
|
|
32
|
+
|
|
33
|
+
/** Infer successful loader data, unwrapping promises and data responses. */
|
|
34
|
+
export type RouteData<T> = T extends (...args: never[]) => infer R
|
|
35
|
+
? UnwrapData<Exclude<Awaited<R>, Response | Error>>
|
|
36
|
+
: T;
|
|
37
|
+
|
|
38
|
+
/** A document metadata descriptor, such as a title, meta tag or structured-data record. */
|
|
39
|
+
export type MetaDescriptor = Record<string, unknown>;
|
|
40
|
+
|
|
41
|
+
/** Loader data, error, parameters and location supplied to a route metadata function. */
|
|
42
|
+
export interface MetaArgs<T> {
|
|
43
|
+
data: RouteData<T> | undefined;
|
|
44
|
+
loaderData: RouteData<T> | undefined;
|
|
45
|
+
error?: unknown;
|
|
46
|
+
params: Record<string, string>;
|
|
47
|
+
location: URL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Derive document metadata descriptors from the current route data and location. */
|
|
51
|
+
export type MetaFunction<T = unknown> = (args: MetaArgs<T>) => MetaDescriptor[];
|
|
52
|
+
|
|
53
|
+
/** Return link descriptors rendered in the document head. */
|
|
54
|
+
export type LinksFunction = () => Record<string, string>[];
|
|
55
|
+
|
|
56
|
+
/** Data route with optional lazy component, preload and document metadata integration. */
|
|
57
|
+
export interface AppRoute extends Route {
|
|
58
|
+
resolveComponent?: () => ComponentType | undefined;
|
|
59
|
+
preload?: () => Promise<void>;
|
|
60
|
+
id?: string;
|
|
61
|
+
routePath?: string;
|
|
62
|
+
meta?: MetaFunction;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Root document components, loader, metadata and revalidation policy for a React application. */
|
|
66
|
+
export interface RootModule {
|
|
67
|
+
/** Route identity used by targeted revalidation. Defaults to root. */
|
|
68
|
+
id?: string;
|
|
69
|
+
/** Optional registered route URL used to revalidate root data after mutations. */
|
|
70
|
+
loaderPath?: string;
|
|
71
|
+
ErrorBoundary?: ComponentType;
|
|
72
|
+
default: ComponentType;
|
|
73
|
+
Layout: ComponentType<{ children: ReactNode }>;
|
|
74
|
+
loader?: (args: LoaderFunctionArgs) => unknown;
|
|
75
|
+
links?: LinksFunction;
|
|
76
|
+
meta?: MetaFunction;
|
|
77
|
+
shouldRevalidate?: (args: RevalidationArgs) => boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Serialized root data and route snapshot used to hydrate without repeating initial loaders. */
|
|
81
|
+
export interface Hydration {
|
|
82
|
+
rootError?: boolean;
|
|
83
|
+
root: unknown;
|
|
84
|
+
snapshot: Snapshot;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Transform a data transport response using the current request context. */
|
|
88
|
+
export type HandleDataRequestFunction = (
|
|
89
|
+
response: Response,
|
|
90
|
+
args: LoaderFunctionArgs,
|
|
91
|
+
) => Response | Promise<Response>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createStore } from "../core/store.ts";
|
|
2
|
+
import { RouterProvider } from "./context.ts";
|
|
3
|
+
import { Outlet } from "./outlet.ts";
|
|
4
|
+
import { Application, type ApplicationContext } from "./root-context.ts";
|
|
5
|
+
import {
|
|
6
|
+
createElement,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
useMemo,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
} from "./runtime.ts";
|
|
11
|
+
|
|
12
|
+
/** Root data, document components and request-scoped router for an application. */
|
|
13
|
+
export interface ApplicationProps extends ApplicationContext {
|
|
14
|
+
document?: boolean;
|
|
15
|
+
rootStore?: ReturnType<typeof createStore>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Compose a persistent root layout with route views and observable root loader data. */
|
|
19
|
+
export function RouterApplication(props: ApplicationProps): ReactNode {
|
|
20
|
+
const store = useMemo(
|
|
21
|
+
() => props.rootStore ?? createStore(props.root),
|
|
22
|
+
[props.rootStore, props.root],
|
|
23
|
+
);
|
|
24
|
+
const root = useSyncExternalStore(
|
|
25
|
+
store.subscribe,
|
|
26
|
+
store.getSnapshot,
|
|
27
|
+
store.getSnapshot,
|
|
28
|
+
);
|
|
29
|
+
const snapshot = useSyncExternalStore(
|
|
30
|
+
props.router.subscribe,
|
|
31
|
+
props.router.getSnapshot,
|
|
32
|
+
props.router.getSnapshot,
|
|
33
|
+
);
|
|
34
|
+
const route = props.routes.find((item) => item.path === snapshot.path);
|
|
35
|
+
const Root =
|
|
36
|
+
snapshot.error && (props.rootError || !route?.ErrorBoundary)
|
|
37
|
+
? (props.rootModule?.ErrorBoundary ?? Outlet)
|
|
38
|
+
: (props.rootModule?.default ?? Outlet);
|
|
39
|
+
const Layout = props.rootModule?.Layout;
|
|
40
|
+
const children = createElement(Root);
|
|
41
|
+
return createElement(
|
|
42
|
+
RouterProvider,
|
|
43
|
+
{ router: props.router },
|
|
44
|
+
createElement(
|
|
45
|
+
Application.Provider,
|
|
46
|
+
{ value: { ...props, root } },
|
|
47
|
+
props.document && Layout ? createElement(Layout, { children }) : children,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,271 +1,39 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
|
-
|
|
3
|
-
import { createDataRouter, matchRoutes, safeWrapAsync } from "../core/index.ts";
|
|
4
|
-
import {
|
|
5
|
-
type DataResponse,
|
|
6
|
-
normalizeResult,
|
|
7
|
-
resolveData,
|
|
8
|
-
} from "../core/responses.ts";
|
|
9
2
|
import type { AppRoute, RootModule } from "../preact/route-types.ts";
|
|
10
|
-
import {
|
|
11
|
-
import { loadRouter } from "./index.ts";
|
|
3
|
+
import { RouterApplication } from "../preact/router.tsx";
|
|
12
4
|
import type { ComponentChildren } from "preact";
|
|
13
5
|
import { Suspense } from "preact/compat";
|
|
14
6
|
import { renderToReadableStream } from "preact-render-to-string/stream";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
import {
|
|
8
|
+
type RenderApplicationOptions,
|
|
9
|
+
renderDocument,
|
|
10
|
+
} from "./render-application.ts";
|
|
19
11
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
earlyHydration?: boolean;
|
|
23
|
-
}
|
|
12
|
+
export { dispatchRoute } from "./dispatch.ts";
|
|
13
|
+
export type { RenderApplicationOptions } from "./render-application.ts";
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
/** Render a Preact application response with root data and optional early deferred hydration. */
|
|
16
|
+
export function renderApplication(
|
|
26
17
|
request: Request,
|
|
27
18
|
routes: AppRoute[],
|
|
28
19
|
rootModule: RootModule,
|
|
29
20
|
entry: string,
|
|
30
21
|
options: RenderApplicationOptions = {},
|
|
31
22
|
): Promise<Response> {
|
|
32
|
-
routes
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let actionResponse: Response | undefined;
|
|
42
|
-
let actionData: unknown;
|
|
43
|
-
|
|
44
|
-
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
45
|
-
actionResponse = await dispatchRoute(request, routes, true);
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
actionResponse.headers.has("Location") ||
|
|
49
|
-
!actionResponse.headers.get("Content-Type")?.includes("application/json")
|
|
50
|
-
)
|
|
51
|
-
return actionResponse;
|
|
52
|
-
|
|
53
|
-
actionData = await normalizeResult(await actionResponse.json());
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const rootResult = safeWrapAsync(() =>
|
|
57
|
-
Promise.resolve()
|
|
58
|
-
.then(() =>
|
|
59
|
-
rootModule.loader?.({ request, params: {}, signal: request.signal }),
|
|
60
|
-
)
|
|
61
|
-
.then(
|
|
62
|
-
(value) => normalizeResult(value, false),
|
|
63
|
-
(error: unknown) =>
|
|
64
|
-
error instanceof Response
|
|
65
|
-
? normalizeResult(error, false)
|
|
66
|
-
: error instanceof Error
|
|
67
|
-
? error
|
|
68
|
-
: new Error(String(error), { cause: error }),
|
|
69
|
-
),
|
|
70
|
-
);
|
|
71
|
-
let redirectResponse: Response | undefined;
|
|
72
|
-
const prepared = prepareRoutes(
|
|
73
|
-
routes.map((route) => ({
|
|
74
|
-
...route,
|
|
75
|
-
ErrorBoundary: route.ErrorBoundary ?? rootModule.ErrorBoundary,
|
|
76
|
-
})),
|
|
77
|
-
true,
|
|
78
|
-
).map((route) => {
|
|
79
|
-
if (!route.loader) return route;
|
|
80
|
-
|
|
81
|
-
const loader = route.loader;
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
...route,
|
|
85
|
-
loader: async (args: Parameters<typeof loader>[0]) => {
|
|
86
|
-
const value = await loader(args);
|
|
87
|
-
|
|
88
|
-
if (value instanceof Response && value.headers.has("Location"))
|
|
89
|
-
redirectResponse = value;
|
|
90
|
-
|
|
91
|
-
return value;
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
const [rootState, loadedRouter] = await Promise.all([
|
|
96
|
-
rootResult,
|
|
97
|
-
loadRouter(prepared, request),
|
|
98
|
-
]);
|
|
99
|
-
let router = loadedRouter;
|
|
100
|
-
const rootError =
|
|
101
|
-
rootState.error ??
|
|
102
|
-
(rootState.data instanceof Error ? rootState.data : undefined);
|
|
103
|
-
const root = rootError ? undefined : rootState.data;
|
|
104
|
-
|
|
105
|
-
if (root instanceof Response && root.headers.has("Location")) {
|
|
106
|
-
router.dispose();
|
|
107
|
-
return root;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (redirectResponse) {
|
|
111
|
-
router.dispose();
|
|
112
|
-
|
|
113
|
-
return redirectResponse;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (actionResponse || rootError) {
|
|
117
|
-
const hydrationData = {
|
|
118
|
-
...router.getSnapshot(),
|
|
119
|
-
actionData: actionData instanceof Error ? undefined : actionData,
|
|
120
|
-
...(actionData instanceof Error && { error: actionData }),
|
|
121
|
-
...(rootError && { error: rootError }),
|
|
122
|
-
};
|
|
123
|
-
router.dispose();
|
|
124
|
-
router = createDataRouter(router.routes, { hydrationData });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const snapshot = router.getSnapshot();
|
|
128
|
-
const hydrationData = {
|
|
129
|
-
root,
|
|
130
|
-
snapshot: {
|
|
131
|
-
...snapshot,
|
|
132
|
-
error: snapshot.error && {
|
|
133
|
-
...snapshot.error,
|
|
134
|
-
name: snapshot.error.name,
|
|
135
|
-
message: snapshot.error.message,
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
...(rootError && { rootError: true }),
|
|
139
|
-
};
|
|
140
|
-
const deferred = options.earlyHydration
|
|
141
|
-
? deferredHydration(hydrationData, options.nonce)
|
|
142
|
-
: undefined;
|
|
143
|
-
const stream = renderToReadableStream(
|
|
144
|
-
<RouterApplication
|
|
145
|
-
router={router}
|
|
146
|
-
routes={routes}
|
|
147
|
-
root={root}
|
|
148
|
-
rootModule={rootModule}
|
|
149
|
-
entry={entry}
|
|
150
|
-
rootError={Boolean(rootError)}
|
|
151
|
-
streaming
|
|
152
|
-
deferredBoundary={options.earlyHydration ? undefined : deferredBoundary}
|
|
153
|
-
initialData={deferred?.initial}
|
|
154
|
-
document
|
|
155
|
-
/>,
|
|
156
|
-
options,
|
|
157
|
-
);
|
|
158
|
-
// The renderer reports the same rejection through its stream reader.
|
|
159
|
-
void stream.allReady.catch(() => {});
|
|
160
|
-
const headers = new Headers(actionResponse?.headers);
|
|
161
|
-
headers.delete("Content-Length");
|
|
162
|
-
headers.delete("Content-Encoding");
|
|
163
|
-
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
164
|
-
headers.set("Cache-Control", "no-store");
|
|
165
|
-
|
|
166
|
-
return new Response(
|
|
167
|
-
deferred
|
|
168
|
-
? progressiveDocument(stream, deferred.stream, {
|
|
169
|
-
signal: request.signal,
|
|
170
|
-
dispose: () => router.dispose(),
|
|
171
|
-
})
|
|
172
|
-
: streamDocument(stream, {
|
|
173
|
-
hydration: resolveHydration(hydrationData),
|
|
174
|
-
signal: request.signal,
|
|
175
|
-
nonce: options.nonce,
|
|
176
|
-
dispose: () => router.dispose(),
|
|
177
|
-
}),
|
|
178
|
-
{
|
|
179
|
-
status: snapshot.error
|
|
180
|
-
? Number("status" in snapshot.error ? snapshot.error.status : 500)
|
|
181
|
-
: (actionResponse?.status ?? 200),
|
|
182
|
-
headers,
|
|
183
|
-
},
|
|
23
|
+
return renderDocument(request, routes, rootModule, entry, options, (props) =>
|
|
24
|
+
renderToReadableStream(
|
|
25
|
+
<RouterApplication
|
|
26
|
+
{...props}
|
|
27
|
+
rootModule={rootModule}
|
|
28
|
+
deferredBoundary={options.earlyHydration ? undefined : deferredBoundary}
|
|
29
|
+
/>,
|
|
30
|
+
options,
|
|
31
|
+
),
|
|
184
32
|
);
|
|
185
33
|
}
|
|
186
|
-
|
|
187
34
|
function deferredBoundary(
|
|
188
35
|
children: ComponentChildren,
|
|
189
36
|
fallback: ComponentChildren,
|
|
190
37
|
) {
|
|
191
38
|
return <Suspense fallback={fallback}>{children}</Suspense>;
|
|
192
39
|
}
|
|
193
|
-
|
|
194
|
-
async function dispatch(
|
|
195
|
-
request: Request,
|
|
196
|
-
routes: AppRoute[],
|
|
197
|
-
document = false,
|
|
198
|
-
): Promise<Response> {
|
|
199
|
-
const match = matchRoutes(
|
|
200
|
-
routes.map((route) => ({ ...route, path: route.routePath ?? route.path })),
|
|
201
|
-
new URL(request.url).pathname,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
if (!match) return Response.json({ error: "No route" }, { status: 404 });
|
|
205
|
-
|
|
206
|
-
const handler =
|
|
207
|
-
request.method === "GET" || request.method === "HEAD"
|
|
208
|
-
? match.route.loader
|
|
209
|
-
: match.route.action;
|
|
210
|
-
|
|
211
|
-
if (!handler) return Response.json({ error: "No handler" }, { status: 405 });
|
|
212
|
-
|
|
213
|
-
const value = await Promise.resolve()
|
|
214
|
-
.then(() =>
|
|
215
|
-
handler({ request, params: match.params, signal: request.signal }),
|
|
216
|
-
)
|
|
217
|
-
.then(
|
|
218
|
-
(result) => result,
|
|
219
|
-
(error: unknown) => error,
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
if (value instanceof Response) {
|
|
223
|
-
const location = value.headers.get("Location");
|
|
224
|
-
|
|
225
|
-
if (document) return value;
|
|
226
|
-
|
|
227
|
-
if (!location) {
|
|
228
|
-
if (value.ok) return value;
|
|
229
|
-
|
|
230
|
-
return Response.json({
|
|
231
|
-
__routeError: {
|
|
232
|
-
message: await value.text(),
|
|
233
|
-
status: value.status,
|
|
234
|
-
statusText: value.statusText,
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const headers = new Headers(value.headers);
|
|
240
|
-
headers.delete("Location");
|
|
241
|
-
headers.set("X-Smolter-Redirect", location);
|
|
242
|
-
|
|
243
|
-
return new Response(null, { headers });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (value instanceof Error)
|
|
247
|
-
return Response.json({
|
|
248
|
-
__routeError: { ...value, message: value.message },
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
if (value && typeof value === "object" && "__routeData" in value) {
|
|
252
|
-
const result = value as DataResponse<unknown>;
|
|
253
|
-
|
|
254
|
-
return Response.json(
|
|
255
|
-
await resolveData(result.value),
|
|
256
|
-
document ? result.init : { headers: result.init.headers },
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return Response.json((await normalizeResult(value)) ?? null);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export async function dispatchRoute(
|
|
264
|
-
request: Request,
|
|
265
|
-
routes: AppRoute[],
|
|
266
|
-
document = false,
|
|
267
|
-
): Promise<Response> {
|
|
268
|
-
const response = await dispatch(request, routes, document);
|
|
269
|
-
|
|
270
|
-
return document ? response : privateResponse(response);
|
|
271
|
-
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production route asset discovery for SSR preload hints.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
1
6
|
import { matchRoutes } from "../core/index.ts";
|
|
2
7
|
|
|
8
|
+
/** Emitted JavaScript and stylesheet URLs associated with a route. */
|
|
3
9
|
export interface RouteAssets {
|
|
4
10
|
path: string;
|
|
5
11
|
files: string[];
|
|
6
12
|
styles?: string[];
|
|
7
13
|
}
|
|
8
14
|
|
|
15
|
+
/** Collect matched-route JavaScript and stylesheet assets from the production route manifest. */
|
|
9
16
|
export function routePreloads(
|
|
10
17
|
routes: RouteAssets[],
|
|
11
18
|
pathname: string,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { matchRoutes, type Route } from "../core/index.ts";
|
|
2
|
+
import {
|
|
3
|
+
type DataResponse,
|
|
4
|
+
normalizeResult,
|
|
5
|
+
resolveData,
|
|
6
|
+
} from "../core/responses.ts";
|
|
7
|
+
import { privateResponse } from "./private-response.ts";
|
|
8
|
+
|
|
9
|
+
async function dispatch(
|
|
10
|
+
request: Request,
|
|
11
|
+
routes: readonly (Route & { routePath?: string })[],
|
|
12
|
+
document = false,
|
|
13
|
+
): Promise<Response> {
|
|
14
|
+
const match = matchRoutes(
|
|
15
|
+
routes.map((route) => ({ ...route, path: route.routePath ?? route.path })),
|
|
16
|
+
new URL(request.url).pathname,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (!match) return Response.json({ error: "No route" }, { status: 404 });
|
|
20
|
+
|
|
21
|
+
const handler =
|
|
22
|
+
request.method === "GET" || request.method === "HEAD"
|
|
23
|
+
? match.route.loader
|
|
24
|
+
: match.route.action;
|
|
25
|
+
|
|
26
|
+
if (!handler) return Response.json({ error: "No handler" }, { status: 405 });
|
|
27
|
+
|
|
28
|
+
const value = await Promise.resolve()
|
|
29
|
+
.then(() =>
|
|
30
|
+
handler({ request, params: match.params, signal: request.signal }),
|
|
31
|
+
)
|
|
32
|
+
.then(
|
|
33
|
+
(result) => result,
|
|
34
|
+
(error: unknown) => error,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (value instanceof Response) {
|
|
38
|
+
const location = value.headers.get("Location");
|
|
39
|
+
|
|
40
|
+
if (document) return value;
|
|
41
|
+
|
|
42
|
+
if (!location) {
|
|
43
|
+
if (value.ok) return value;
|
|
44
|
+
|
|
45
|
+
return Response.json({
|
|
46
|
+
__routeError: {
|
|
47
|
+
message: await value.text(),
|
|
48
|
+
status: value.status,
|
|
49
|
+
statusText: value.statusText,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const headers = new Headers(value.headers);
|
|
55
|
+
headers.delete("Location");
|
|
56
|
+
headers.set("X-Smolter-Redirect", location);
|
|
57
|
+
|
|
58
|
+
return new Response(null, { headers });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (value instanceof Error)
|
|
62
|
+
return Response.json({
|
|
63
|
+
__routeError: { ...value, message: value.message },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (value && typeof value === "object" && "__routeData" in value) {
|
|
67
|
+
const result = value as DataResponse<unknown>;
|
|
68
|
+
|
|
69
|
+
return Response.json(
|
|
70
|
+
await resolveData(result.value),
|
|
71
|
+
document ? result.init : { headers: result.init.headers },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Response.json((await normalizeResult(value)) ?? null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Dispatch a normalized application URL to its route loader or action and return an HTTP response. */
|
|
79
|
+
export async function dispatchRoute(
|
|
80
|
+
request: Request,
|
|
81
|
+
routes: readonly (Route & { routePath?: string })[],
|
|
82
|
+
document = false,
|
|
83
|
+
): Promise<Response> {
|
|
84
|
+
const response = await dispatch(request, routes, document);
|
|
85
|
+
|
|
86
|
+
return document ? response : privateResponse(response);
|
|
87
|
+
}
|