@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
|
@@ -1,45 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { RootModule } from "./route-types.ts";
|
|
3
|
-
|
|
4
|
-
export { createStore as createRootStore } from "../core/store.ts";
|
|
5
|
-
|
|
6
|
-
import { createStore } from "../core/store.ts";
|
|
7
|
-
|
|
8
|
-
export function attachRootData(
|
|
9
|
-
router: Router,
|
|
10
|
-
initial: unknown,
|
|
11
|
-
rootModule?: RootModule,
|
|
12
|
-
) {
|
|
13
|
-
const store = createStore(initial);
|
|
14
|
-
const loaderPath = rootModule?.loaderPath;
|
|
15
|
-
|
|
16
|
-
if (!rootModule || !loaderPath) return store;
|
|
17
|
-
|
|
18
|
-
let pending: ReturnType<Router["createFetcher"]> | undefined;
|
|
19
|
-
const unregister = router.registerRevalidator(
|
|
20
|
-
rootModule.id ?? "root",
|
|
21
|
-
async (args) => {
|
|
22
|
-
if (rootModule.shouldRevalidate?.(args) === false) return;
|
|
23
|
-
|
|
24
|
-
pending?.dispose();
|
|
25
|
-
const fetcher = router.createFetcher();
|
|
26
|
-
pending = fetcher;
|
|
27
|
-
const result = await fetcher.load(loaderPath);
|
|
28
|
-
|
|
29
|
-
if (pending === fetcher && !result.error) store.update(result.data);
|
|
30
|
-
|
|
31
|
-
fetcher.dispose();
|
|
32
|
-
|
|
33
|
-
if (pending === fetcher) pending = undefined;
|
|
34
|
-
},
|
|
35
|
-
);
|
|
36
|
-
const dispose = router.dispose;
|
|
37
|
-
router.dispose = () => {
|
|
38
|
-
pending?.dispose();
|
|
39
|
-
pending = undefined;
|
|
40
|
-
unregister();
|
|
41
|
-
dispose();
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
return store;
|
|
45
|
-
}
|
|
1
|
+
export { attachRootData, createRootStore } from "../client/root-data.ts";
|
|
@@ -15,6 +15,7 @@ import { OutletContext, useApplication } from "./root-context.ts";
|
|
|
15
15
|
import type { RouteData } from "./route-types.ts";
|
|
16
16
|
import { useContext, useMemo } from "./runtime.ts";
|
|
17
17
|
|
|
18
|
+
/** Read root loader data from the application context. */
|
|
18
19
|
export function useRootData<T = unknown>(): RouteData<T> {
|
|
19
20
|
return useApplication().root as RouteData<T>;
|
|
20
21
|
}
|
|
@@ -37,10 +38,12 @@ export function useRouteLoaderData<T = unknown>(
|
|
|
37
38
|
|
|
38
39
|
export { useSnapshot };
|
|
39
40
|
|
|
41
|
+
/** Read the current route error for an error boundary. */
|
|
40
42
|
export function useRouteError(): Error | undefined {
|
|
41
43
|
return useSnapshot("error");
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** Read the current page navigation or submission state. */
|
|
44
47
|
export function useNavigation(): {
|
|
45
48
|
state: "idle" | "loading" | "submitting";
|
|
46
49
|
location: URL | undefined;
|
|
@@ -58,24 +61,27 @@ export function useNavigation(): {
|
|
|
58
61
|
);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/** Read the active flat route match and loader data; this is not a nested layout match chain. */
|
|
61
65
|
export function useMatches(): {
|
|
62
66
|
id: string;
|
|
63
67
|
data: unknown;
|
|
64
68
|
params: import("../core/index.ts").Params;
|
|
65
69
|
}[] {
|
|
66
|
-
const
|
|
70
|
+
const router = useRouter();
|
|
67
71
|
const snapshot = useSnapshot();
|
|
68
|
-
const route = routes.find((item) => item.path === snapshot.path);
|
|
72
|
+
const route = router.routes.find((item) => item.path === snapshot.path);
|
|
69
73
|
|
|
70
74
|
return [
|
|
71
75
|
{ id: route?.id ?? "", data: snapshot.data, params: snapshot.params },
|
|
72
76
|
];
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
/** Read context supplied by the nearest Outlet. */
|
|
75
80
|
export function useOutletContext<T>(): T {
|
|
76
81
|
return useContext(OutletContext) as T;
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
/** Return a function that submits form data through the current router. */
|
|
79
85
|
export function useSubmit(): (
|
|
80
86
|
value: Submission,
|
|
81
87
|
options?: SubmissionOptions,
|
|
@@ -103,6 +109,7 @@ export function useSubmit(): (
|
|
|
103
109
|
);
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
/** Read the current router location as a URL. */
|
|
106
113
|
export function useURLLocation(): URL & { state: unknown; key: string } {
|
|
107
114
|
const href = useSnapshot("url");
|
|
108
115
|
const snapshot = useNavigationSnapshot(useRouter<Router>());
|
|
@@ -117,6 +124,7 @@ export function useURLLocation(): URL & { state: unknown; key: string } {
|
|
|
117
124
|
);
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
/** Return the application navigation function, including history offsets and relative destinations. */
|
|
120
128
|
export function useNavigate(): NavigateFunction {
|
|
121
129
|
const router = useRouter<Router>();
|
|
122
130
|
|
|
@@ -139,6 +147,7 @@ export function useNavigate(): NavigateFunction {
|
|
|
139
147
|
);
|
|
140
148
|
}
|
|
141
149
|
|
|
150
|
+
/** Read URL search parameters and update them through router navigation. */
|
|
142
151
|
export function useSearchParams(): [
|
|
143
152
|
URLSearchParams,
|
|
144
153
|
(
|
|
@@ -165,6 +174,7 @@ export function useSearchParams(): [
|
|
|
165
174
|
return [params, set];
|
|
166
175
|
}
|
|
167
176
|
|
|
177
|
+
/** Read the last browser history operation. */
|
|
168
178
|
export function useNavigationType(): NavigationType {
|
|
169
179
|
return useNavigationSnapshot(useRouter<Router>()).type;
|
|
170
180
|
}
|
|
@@ -7,30 +7,38 @@ import type {
|
|
|
7
7
|
import type { DataResponse } from "../core/responses.ts";
|
|
8
8
|
import type { ComponentType, ReactNode } from "preact/compat";
|
|
9
9
|
|
|
10
|
+
/** Request, route parameters and cancellation signal supplied to a loader. */
|
|
10
11
|
export interface LoaderFunctionArgs extends DataArgs {
|
|
11
12
|
context?: unknown;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Request, route parameters and cancellation signal supplied to an action. */
|
|
14
16
|
export type ActionFunctionArgs = LoaderFunctionArgs;
|
|
15
17
|
|
|
18
|
+
/** Loader arguments with a serverLoader callback for the server transport. */
|
|
16
19
|
export interface ClientLoaderFunctionArgs extends LoaderFunctionArgs {
|
|
17
20
|
serverLoader: <T = unknown>() => Promise<T>;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/** Action arguments with a serverAction callback for the server transport. */
|
|
20
24
|
export interface ClientActionFunctionArgs extends LoaderFunctionArgs {
|
|
21
25
|
serverAction: <T = unknown>() => Promise<T>;
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
/** Navigation or mutation context used to decide whether loader data should refresh. */
|
|
24
29
|
export type ShouldRevalidateFunctionArgs = RevalidationArgs;
|
|
25
30
|
|
|
26
31
|
type UnwrapData<T> = T extends DataResponse<infer V> ? V : T;
|
|
27
32
|
|
|
33
|
+
/** Infer successful loader data, unwrapping promises and data responses. */
|
|
28
34
|
export type RouteData<T> = T extends (...args: never[]) => infer R
|
|
29
35
|
? UnwrapData<Exclude<Awaited<R>, Response | Error>>
|
|
30
36
|
: T;
|
|
31
37
|
|
|
38
|
+
/** A document metadata descriptor, such as a title, meta tag or structured-data record. */
|
|
32
39
|
export type MetaDescriptor = Record<string, unknown>;
|
|
33
40
|
|
|
41
|
+
/** Loader data, error, parameters and location supplied to a route metadata function. */
|
|
34
42
|
export interface MetaArgs<T> {
|
|
35
43
|
data: RouteData<T> | undefined;
|
|
36
44
|
loaderData: RouteData<T> | undefined;
|
|
@@ -39,10 +47,13 @@ export interface MetaArgs<T> {
|
|
|
39
47
|
location: URL;
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
/** Derive document metadata descriptors from the current route data and location. */
|
|
42
51
|
export type MetaFunction<T = unknown> = (args: MetaArgs<T>) => MetaDescriptor[];
|
|
43
52
|
|
|
53
|
+
/** Return link descriptors rendered in the document head. */
|
|
44
54
|
export type LinksFunction = () => Record<string, string>[];
|
|
45
55
|
|
|
56
|
+
/** Data route with optional lazy component, preload and document metadata integration. */
|
|
46
57
|
export interface AppRoute extends Route {
|
|
47
58
|
resolveComponent?: () => ComponentType | undefined;
|
|
48
59
|
preload?: () => Promise<void>;
|
|
@@ -51,6 +62,7 @@ export interface AppRoute extends Route {
|
|
|
51
62
|
meta?: MetaFunction;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/** Root document components, loader, metadata and revalidation policy for a Preact application. */
|
|
54
66
|
export interface RootModule {
|
|
55
67
|
/** Route identity used by targeted revalidation. Defaults to root. */
|
|
56
68
|
id?: string;
|
|
@@ -65,12 +77,14 @@ export interface RootModule {
|
|
|
65
77
|
shouldRevalidate?: (args: RevalidationArgs) => boolean;
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
/** Serialized root data and route snapshot used to hydrate without repeating initial loaders. */
|
|
68
81
|
export interface Hydration {
|
|
69
82
|
rootError?: boolean;
|
|
70
83
|
root: unknown;
|
|
71
84
|
snapshot: Snapshot;
|
|
72
85
|
}
|
|
73
86
|
|
|
87
|
+
/** Transform a data transport response using the current request context. */
|
|
74
88
|
export type HandleDataRequestFunction = (
|
|
75
89
|
response: Response,
|
|
76
90
|
args: LoaderFunctionArgs,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
2
|
|
|
3
|
-
import type { DataFunction } from "../core/index.ts";
|
|
4
|
-
import { normalizeResult } from "../core/responses.ts";
|
|
5
3
|
import type { ReactNode } from "preact/compat";
|
|
6
4
|
import { RouterProvider } from "./context.ts";
|
|
7
5
|
import {
|
|
@@ -10,15 +8,16 @@ import {
|
|
|
10
8
|
OutletContext,
|
|
11
9
|
} from "./root-context.ts";
|
|
12
10
|
import { createRootStore } from "./root-data.ts";
|
|
13
|
-
import type { AppRoute } from "./route-types.ts";
|
|
14
11
|
import { Routes } from "./routes.ts";
|
|
15
12
|
import { useContext, useMemo, useSyncExternalStore } from "./runtime.ts";
|
|
16
13
|
|
|
14
|
+
/** Optional children and context shared with descendants through useOutletContext. */
|
|
17
15
|
export interface OutletProps {
|
|
18
16
|
context?: unknown;
|
|
19
17
|
children?: ReactNode;
|
|
20
18
|
}
|
|
21
19
|
|
|
20
|
+
/** Render supplied children or the active route, and provide outlet context to descendants. */
|
|
22
21
|
export function Outlet({
|
|
23
22
|
context,
|
|
24
23
|
children,
|
|
@@ -32,11 +31,13 @@ export function Outlet({
|
|
|
32
31
|
);
|
|
33
32
|
}
|
|
34
33
|
|
|
34
|
+
/** Root data, document components and request-scoped router for an application. */
|
|
35
35
|
export interface ApplicationProps extends ApplicationContext {
|
|
36
36
|
document?: boolean;
|
|
37
37
|
rootStore?: ReturnType<typeof createRootStore>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Render a Preact root document and active route with shared root data and application context. */
|
|
40
41
|
export function RouterApplication(
|
|
41
42
|
props: ApplicationProps,
|
|
42
43
|
): import("preact").JSX.Element {
|
|
@@ -66,48 +67,4 @@ export function RouterApplication(
|
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
export
|
|
70
|
-
routes: AppRoute[],
|
|
71
|
-
preserveDeferred = false,
|
|
72
|
-
): AppRoute[] {
|
|
73
|
-
return routes.map((route) => {
|
|
74
|
-
const wrapped: AppRoute = {
|
|
75
|
-
...route,
|
|
76
|
-
path: route.routePath ?? route.path,
|
|
77
|
-
shouldRevalidate: (args) => {
|
|
78
|
-
const { currentUrl, nextUrl } = args;
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
args.reason === "navigation" &&
|
|
82
|
-
currentUrl.pathname === nextUrl.pathname &&
|
|
83
|
-
currentUrl.search === nextUrl.search &&
|
|
84
|
-
currentUrl.hash !== nextUrl.hash
|
|
85
|
-
)
|
|
86
|
-
return false;
|
|
87
|
-
|
|
88
|
-
return route.shouldRevalidate?.(args) ?? args.defaultShouldRevalidate;
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
for (const key of [
|
|
93
|
-
"loader",
|
|
94
|
-
"action",
|
|
95
|
-
"clientLoader",
|
|
96
|
-
"clientAction",
|
|
97
|
-
] as const) {
|
|
98
|
-
const handler = route[key] as DataFunction | undefined;
|
|
99
|
-
|
|
100
|
-
if (!handler) continue;
|
|
101
|
-
|
|
102
|
-
wrapped[key] = (args) =>
|
|
103
|
-
Promise.resolve()
|
|
104
|
-
.then(() => handler(args))
|
|
105
|
-
.then(
|
|
106
|
-
(value) => normalizeResult(value, !preserveDeferred),
|
|
107
|
-
(error) => normalizeResult(error, !preserveDeferred),
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return wrapped;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
70
|
+
export { prepareRoutes } from "../core/prepare-routes.ts";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React document hydration, browser history and deployment recovery.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
/** @jsxImportSource react */
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type DeploymentOptions,
|
|
10
|
+
recoverDeployment,
|
|
11
|
+
watchDeployment,
|
|
12
|
+
} from "../client/index.ts";
|
|
13
|
+
import { connectHashHistory } from "../client/hash-history.ts";
|
|
14
|
+
import { readHydration } from "../client/hydration.ts";
|
|
15
|
+
import { attachRootData } from "../client/root-data.ts";
|
|
16
|
+
import { createDataRouter, matchRoutes, type Router } from "../core/index.ts";
|
|
17
|
+
import { connectNavigation } from "../core/navigation.ts";
|
|
18
|
+
import { prepareRoutes } from "../core/prepare-routes.ts";
|
|
19
|
+
import { retainRoute } from "../core/presentation.ts";
|
|
20
|
+
import { flushSync } from "react-dom";
|
|
21
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
22
|
+
import type { AppRoute, Hydration, RootModule } from "./route-types.ts";
|
|
23
|
+
import { RouterApplication } from "./router.ts";
|
|
24
|
+
|
|
25
|
+
export * from "../client/index.ts";
|
|
26
|
+
export { configureApplication } from "../core/environment.ts";
|
|
27
|
+
|
|
28
|
+
/** Browser hydration settings for hash history, deployment recovery and deferred-data timeout. */
|
|
29
|
+
export interface MountApplicationOptions {
|
|
30
|
+
hash?: boolean;
|
|
31
|
+
deployment?: DeploymentOptions | null;
|
|
32
|
+
/** Total initial deferred-data budget in integer milliseconds; not an inactivity timeout. Defaults to 30,000. */
|
|
33
|
+
deferredTimeout?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Create the browser application router and hydrate the document, installing recovery before initial lazy loading. */
|
|
37
|
+
export async function mountApplication(
|
|
38
|
+
routes: AppRoute[],
|
|
39
|
+
rootModule?: RootModule,
|
|
40
|
+
options: boolean | MountApplicationOptions = false,
|
|
41
|
+
): Promise<Router> {
|
|
42
|
+
const {
|
|
43
|
+
hash = false,
|
|
44
|
+
deployment = null,
|
|
45
|
+
deferredTimeout,
|
|
46
|
+
} = typeof options === "boolean" ? { hash: options } : options;
|
|
47
|
+
const initialURL = new URL(
|
|
48
|
+
hash ? location.hash.slice(1) || "/" : location.href,
|
|
49
|
+
location.origin,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const initialRoute = matchRoutes(routes, initialURL.pathname)?.route as
|
|
53
|
+
| AppRoute
|
|
54
|
+
| undefined;
|
|
55
|
+
|
|
56
|
+
await initialRoute?.preload?.().catch(async (error: unknown) => {
|
|
57
|
+
await recoverDeployment(deployment, initialURL);
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
routes = prepareRoutes(routes);
|
|
62
|
+
const element = document.getElementById("smolter-data");
|
|
63
|
+
const initial = element?.textContent
|
|
64
|
+
? readHydration(element.textContent, deferredTimeout)
|
|
65
|
+
: undefined;
|
|
66
|
+
const hydration: Hydration | undefined = initial?.hydration;
|
|
67
|
+
|
|
68
|
+
if (hydration?.snapshot.error)
|
|
69
|
+
hydration.snapshot.error = Object.assign(
|
|
70
|
+
new Error(hydration.snapshot.error.message),
|
|
71
|
+
hydration.snapshot.error,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const router = createDataRouter(routes, {
|
|
75
|
+
browser: !hash,
|
|
76
|
+
url: hash
|
|
77
|
+
? new URL(location.hash.slice(1) || "/", location.origin).href
|
|
78
|
+
: location.href,
|
|
79
|
+
...(hydration && { hydrationData: hydration.snapshot }),
|
|
80
|
+
});
|
|
81
|
+
const dispose = router.dispose;
|
|
82
|
+
router.dispose = () => {
|
|
83
|
+
initial?.dispose();
|
|
84
|
+
dispose();
|
|
85
|
+
};
|
|
86
|
+
watchDeployment(router, deployment);
|
|
87
|
+
if (hash) connectHashHistory(router);
|
|
88
|
+
|
|
89
|
+
retainRoute(router, flushSync);
|
|
90
|
+
|
|
91
|
+
connectNavigation(router);
|
|
92
|
+
const rootStore = attachRootData(router, hydration?.root, rootModule);
|
|
93
|
+
const props = {
|
|
94
|
+
routes,
|
|
95
|
+
router,
|
|
96
|
+
root: hydration?.root,
|
|
97
|
+
rootModule,
|
|
98
|
+
rootStore,
|
|
99
|
+
rootError: hydration?.rootError,
|
|
100
|
+
initialData: element?.textContent ?? undefined,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (hydration) {
|
|
104
|
+
const mounted = hydrateRoot(
|
|
105
|
+
document,
|
|
106
|
+
<RouterApplication {...props} document />,
|
|
107
|
+
);
|
|
108
|
+
const cleanup = router.dispose;
|
|
109
|
+
router.dispose = () => {
|
|
110
|
+
mounted.unmount();
|
|
111
|
+
cleanup();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return router;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const target = document.getElementById("root");
|
|
118
|
+
|
|
119
|
+
if (target) {
|
|
120
|
+
const mounted = createRoot(target);
|
|
121
|
+
mounted.render(<RouterApplication {...props} />);
|
|
122
|
+
const cleanup = router.dispose;
|
|
123
|
+
router.dispose = () => {
|
|
124
|
+
mounted.unmount();
|
|
125
|
+
cleanup();
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return router;
|
|
130
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React document metadata, resource links and hydration scripts.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
/** @jsxImportSource react */
|
|
7
|
+
|
|
8
|
+
import { applicationEnvironment } from "../core/environment.ts";
|
|
9
|
+
import { scrollPositions } from "../core/navigation.ts";
|
|
10
|
+
import { serializeData } from "../core/serialize.ts";
|
|
11
|
+
import type { ComponentProps } from "react";
|
|
12
|
+
import {
|
|
13
|
+
createElement,
|
|
14
|
+
Fragment,
|
|
15
|
+
useEffect,
|
|
16
|
+
useLayoutEffect,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from "react";
|
|
20
|
+
import { useNavigationSnapshot } from "./navigation-state.ts";
|
|
21
|
+
import { useApplication } from "./root-context.ts";
|
|
22
|
+
import { useSnapshot } from "./route-hooks.ts";
|
|
23
|
+
import type { MetaDescriptor } from "./route-types.ts";
|
|
24
|
+
|
|
25
|
+
function renderMeta(item: MetaDescriptor, index: number) {
|
|
26
|
+
if (typeof item.title === "string")
|
|
27
|
+
return <title key={index}>{item.title}</title>;
|
|
28
|
+
|
|
29
|
+
if ("script:ld+json" in item)
|
|
30
|
+
return (
|
|
31
|
+
<script
|
|
32
|
+
key={index}
|
|
33
|
+
type="application/ld+json"
|
|
34
|
+
dangerouslySetInnerHTML={{
|
|
35
|
+
__html: serializeData(item["script:ld+json"]),
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const { tagName, ...props } = item;
|
|
41
|
+
|
|
42
|
+
return createElement(tagName === "link" ? "link" : "meta", {
|
|
43
|
+
...props,
|
|
44
|
+
key: index,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Render root or active-route metadata in the document head. */
|
|
49
|
+
export function Meta(): import("react").ReactElement {
|
|
50
|
+
const { routes, rootModule } = useApplication();
|
|
51
|
+
const snapshot = useSnapshot();
|
|
52
|
+
const route = routes.find((item) => item.path === snapshot.path);
|
|
53
|
+
const args = {
|
|
54
|
+
loaderData: snapshot.data,
|
|
55
|
+
data: snapshot.data,
|
|
56
|
+
params: snapshot.params,
|
|
57
|
+
error: snapshot.error,
|
|
58
|
+
location: new URL(snapshot.url),
|
|
59
|
+
};
|
|
60
|
+
const values = route?.meta?.(args) ?? rootModule?.meta?.(args) ?? [];
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const title = values.find((item) => typeof item.title === "string")?.title;
|
|
63
|
+
|
|
64
|
+
if (typeof title === "string") document.title = title;
|
|
65
|
+
}, [values]);
|
|
66
|
+
|
|
67
|
+
return <Fragment>{values.map(renderMeta)}</Fragment>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Render stylesheet and other link descriptors from the application root. */
|
|
71
|
+
export function Links(): import("react").ReactElement {
|
|
72
|
+
const { rootModule } = useApplication();
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Fragment>
|
|
76
|
+
{rootModule?.links?.().map((props, index) => (
|
|
77
|
+
<link key={index} {...props} />
|
|
78
|
+
))}
|
|
79
|
+
</Fragment>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Nonce and browser entry options for the document hydration scripts. */
|
|
84
|
+
export interface ScriptProps {
|
|
85
|
+
nonce?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Render the hydration snapshot and browser entry script with an optional CSP nonce. */
|
|
89
|
+
export function Scripts({ nonce }: ScriptProps): import("react").ReactElement {
|
|
90
|
+
const { root, rootError, router, entry, streaming, initialData } =
|
|
91
|
+
useApplication();
|
|
92
|
+
// The bootstrap payload describes this document's initial SSR snapshot.
|
|
93
|
+
// Navigation and revalidation update the router, not the hydration script.
|
|
94
|
+
const [json] = useState(() => {
|
|
95
|
+
if (initialData !== undefined) return initialData;
|
|
96
|
+
if (streaming) return "";
|
|
97
|
+
|
|
98
|
+
const current = router.getSnapshot();
|
|
99
|
+
const snapshot = {
|
|
100
|
+
...current,
|
|
101
|
+
error: current.error && {
|
|
102
|
+
...current.error,
|
|
103
|
+
name: current.error.name,
|
|
104
|
+
message: current.error.message,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return serializeData({ root, snapshot, ...(rootError && { rootError }) });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Fragment>
|
|
113
|
+
<script
|
|
114
|
+
{...(nonce && { nonce })}
|
|
115
|
+
dangerouslySetInnerHTML={{
|
|
116
|
+
__html: applicationEnvironment?.entryScript ?? "",
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
<script
|
|
120
|
+
id="smolter-data"
|
|
121
|
+
type="application/json"
|
|
122
|
+
{...(nonce && { nonce })}
|
|
123
|
+
dangerouslySetInnerHTML={{ __html: json }}
|
|
124
|
+
/>
|
|
125
|
+
{entry && (
|
|
126
|
+
<script
|
|
127
|
+
type="module"
|
|
128
|
+
async={initialData !== undefined}
|
|
129
|
+
src={entry}
|
|
130
|
+
{...(nonce && { nonce })}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</Fragment>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Reset scroll after navigation unless the navigation requests preservation. */
|
|
138
|
+
export function ScrollRestoration(_props: ScriptProps): null {
|
|
139
|
+
const { router } = useApplication();
|
|
140
|
+
const { url, state } = useSnapshot();
|
|
141
|
+
const navigation = useNavigationSnapshot(router);
|
|
142
|
+
const positions = scrollPositions(router);
|
|
143
|
+
const restoredLocation = useRef("");
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const previous = history.scrollRestoration;
|
|
147
|
+
history.scrollRestoration = "manual";
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
history.scrollRestoration = previous;
|
|
151
|
+
};
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
useLayoutEffect(() => {
|
|
155
|
+
if (state !== "idle") return;
|
|
156
|
+
|
|
157
|
+
if ((history.state?.key ?? "initial") !== navigation.key) return;
|
|
158
|
+
|
|
159
|
+
const locationKey = `${navigation.key}:${url}`;
|
|
160
|
+
|
|
161
|
+
if (restoredLocation.current === locationKey) return;
|
|
162
|
+
|
|
163
|
+
// Lazy views subscribe independently; restore after their DOM is committed.
|
|
164
|
+
const frame = requestAnimationFrame(() => {
|
|
165
|
+
restoredLocation.current = locationKey;
|
|
166
|
+
const target = new URL(location.href);
|
|
167
|
+
const saved = navigation.type === "POP" && positions.get(navigation.key);
|
|
168
|
+
|
|
169
|
+
if (!saved && target.hash) {
|
|
170
|
+
document.getElementById(target.hash.slice(1))?.scrollIntoView();
|
|
171
|
+
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!saved && navigation.preventScrollReset) return;
|
|
176
|
+
|
|
177
|
+
const [left, top] = saved || [0, 0];
|
|
178
|
+
window.scrollTo({ left, top, behavior: "instant" });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return () => cancelAnimationFrame(frame);
|
|
182
|
+
}, [
|
|
183
|
+
url,
|
|
184
|
+
state,
|
|
185
|
+
positions,
|
|
186
|
+
navigation.key,
|
|
187
|
+
navigation.type,
|
|
188
|
+
navigation.preventScrollReset,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** HTML attributes for server markup and client document reconciliation. */
|
|
195
|
+
export interface DocumentProps extends ComponentProps<"html"> {
|
|
196
|
+
"data-debug"?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Render the server document and preserve client-managed theme classes on hydration. */
|
|
200
|
+
export function Document(props: DocumentProps): import("react").ReactElement {
|
|
201
|
+
const { children, ...attributes } = props;
|
|
202
|
+
const className = String(props.className ?? "");
|
|
203
|
+
const previousClassName = useRef(className);
|
|
204
|
+
const lang = String(props.lang ?? "en");
|
|
205
|
+
const debug = String(props["data-debug"] ?? false);
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
// Hydration must preserve classes already applied by the browser theme store.
|
|
208
|
+
// Only reconcile classes when the document's own props actually change.
|
|
209
|
+
if (previousClassName.current !== className) {
|
|
210
|
+
const previous = previousClassName.current.split(/\s+/).filter(Boolean);
|
|
211
|
+
const next = className.split(/\s+/).filter(Boolean);
|
|
212
|
+
document.documentElement.classList.remove(...previous);
|
|
213
|
+
document.documentElement.classList.add(...next);
|
|
214
|
+
previousClassName.current = className;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
document.documentElement.lang = lang;
|
|
218
|
+
document.documentElement.dataset.debug = debug;
|
|
219
|
+
}, [className, lang, debug]);
|
|
220
|
+
|
|
221
|
+
const classes =
|
|
222
|
+
typeof document !== "undefined" && previousClassName.current !== className
|
|
223
|
+
? [...document.documentElement.classList]
|
|
224
|
+
.filter(
|
|
225
|
+
(value) => !previousClassName.current.split(/\s+/).includes(value),
|
|
226
|
+
)
|
|
227
|
+
.concat(className.split(/\s+/).filter(Boolean))
|
|
228
|
+
.join(" ")
|
|
229
|
+
: props.className;
|
|
230
|
+
return (
|
|
231
|
+
<html {...attributes} className={classes} suppressHydrationWarning>
|
|
232
|
+
{children}
|
|
233
|
+
</html>
|
|
234
|
+
);
|
|
235
|
+
}
|