@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.
Files changed (151) hide show
  1. package/.generated/client/hash-history.ts +27 -0
  2. package/.generated/client/hydration.ts +98 -0
  3. package/.generated/client/root-data.ts +52 -0
  4. package/.generated/core/environment.ts +1 -0
  5. package/.generated/core/navigation.ts +1 -0
  6. package/.generated/core/prepare-routes.ts +50 -0
  7. package/.generated/core/presentation.ts +1 -1
  8. package/.generated/core/responses.ts +12 -0
  9. package/.generated/core/utils.ts +5 -2
  10. package/.generated/preact/await.tsx +1 -0
  11. package/.generated/preact/client.tsx +8 -27
  12. package/.generated/preact/deferred-hydration.ts +1 -98
  13. package/.generated/preact/document.tsx +10 -0
  14. package/.generated/preact/index.ts +11 -2
  15. package/.generated/preact/lazy-route.ts +7 -0
  16. package/.generated/preact/navigation.tsx +19 -5
  17. package/.generated/preact/root-data.ts +1 -45
  18. package/.generated/preact/route-hooks.tsx +12 -2
  19. package/.generated/preact/route-types.ts +14 -0
  20. package/.generated/preact/router.tsx +5 -48
  21. package/.generated/react/client.tsx +130 -0
  22. package/.generated/react/document.tsx +235 -0
  23. package/.generated/react/fetcher.ts +29 -7
  24. package/.generated/react/hooks.ts +20 -18
  25. package/.generated/react/index.ts +28 -2
  26. package/.generated/react/lazy-route.ts +116 -0
  27. package/.generated/react/link.ts +1 -38
  28. package/.generated/react/navigation-state.ts +13 -0
  29. package/.generated/react/navigation.ts +145 -0
  30. package/.generated/react/outlet.ts +48 -0
  31. package/.generated/react/root-context.ts +22 -0
  32. package/.generated/react/route-hooks.ts +159 -0
  33. package/.generated/react/route-types.ts +91 -0
  34. package/.generated/react/router.ts +50 -0
  35. package/.generated/react/runtime.ts +1 -0
  36. package/.generated/server/application.tsx +18 -250
  37. package/.generated/server/assets.ts +7 -0
  38. package/.generated/server/dispatch.ts +87 -0
  39. package/.generated/server/handler.ts +3 -0
  40. package/.generated/server/http.ts +2 -0
  41. package/.generated/server/index.ts +2 -0
  42. package/.generated/server/node.ts +5 -2
  43. package/.generated/server/preact.ts +5 -2
  44. package/.generated/server/react.ts +34 -0
  45. package/.generated/server/render-application.ts +203 -0
  46. package/.generated/vite/application/lazy-files.ts +2 -1
  47. package/.generated/vite/application/vite.ts +12 -4
  48. package/.generated/vite/react.ts +11 -0
  49. package/CHANGELOG.md +20 -0
  50. package/README.md +48 -48
  51. package/dist/client/hash-history.d.ts +2 -0
  52. package/dist/client/hash-history.js +26 -0
  53. package/dist/client/hydration.d.ts +10 -0
  54. package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
  55. package/dist/client/root-data.d.ts +14 -0
  56. package/dist/{preact → client}/root-data.js +1 -1
  57. package/dist/core/environment.d.ts +1 -0
  58. package/dist/core/navigation.d.ts +1 -0
  59. package/dist/core/prepare-routes.d.ts +7 -0
  60. package/dist/core/prepare-routes.js +28 -0
  61. package/dist/core/presentation.js +1 -1
  62. package/dist/core/responses.d.ts +12 -0
  63. package/dist/core/responses.js +5 -0
  64. package/dist/core/utils.d.ts +5 -2
  65. package/dist/core/utils.js +1 -2
  66. package/dist/preact/await.d.ts +1 -0
  67. package/dist/preact/await.js +1 -0
  68. package/dist/preact/client.d.ts +7 -1
  69. package/dist/preact/client.js +11 -27
  70. package/dist/preact/deferred-hydration.d.ts +1 -10
  71. package/dist/preact/document.d.ts +10 -0
  72. package/dist/preact/document.js +11 -2
  73. package/dist/preact/index.d.ts +6 -2
  74. package/dist/preact/index.js +1 -1
  75. package/dist/preact/lazy-route.d.ts +2 -0
  76. package/dist/preact/lazy-route.js +6 -0
  77. package/dist/preact/navigation-state.js +1 -1
  78. package/dist/preact/navigation.d.ts +6 -0
  79. package/dist/preact/navigation.js +9 -5
  80. package/dist/preact/root-data.d.ts +1 -8
  81. package/dist/preact/route-hooks.d.ts +10 -0
  82. package/dist/preact/route-hooks.js +13 -3
  83. package/dist/preact/route-types.d.ts +14 -0
  84. package/dist/preact/router.d.ts +5 -2
  85. package/dist/preact/router.js +4 -26
  86. package/dist/react/client.d.ts +20 -0
  87. package/dist/react/client.js +86 -0
  88. package/dist/react/document.d.ts +25 -0
  89. package/dist/react/document.js +169 -0
  90. package/dist/react/fetcher.d.ts +8 -4
  91. package/dist/react/fetcher.js +12 -1
  92. package/dist/react/form.js +1 -1
  93. package/dist/react/hooks.d.ts +7 -6
  94. package/dist/react/index.d.ts +13 -2
  95. package/dist/react/index.js +7 -2
  96. package/dist/react/lazy-route.d.ts +22 -0
  97. package/dist/react/lazy-route.js +57 -0
  98. package/dist/react/link.d.ts +1 -12
  99. package/dist/react/navigation-state.d.ts +2 -0
  100. package/dist/react/navigation-state.js +9 -0
  101. package/dist/react/navigation.d.ts +33 -0
  102. package/dist/react/navigation.js +64 -0
  103. package/dist/react/outlet.d.ts +16 -0
  104. package/dist/react/outlet.js +26 -0
  105. package/dist/react/root-context.d.ts +14 -0
  106. package/dist/react/root-context.js +10 -0
  107. package/dist/react/route-hooks.d.ts +34 -0
  108. package/dist/react/route-hooks.js +89 -0
  109. package/dist/react/route-types.d.ts +69 -0
  110. package/dist/react/router.d.ts +10 -0
  111. package/dist/react/router.js +22 -0
  112. package/dist/react/runtime.d.ts +1 -1
  113. package/dist/react/runtime.js +1 -1
  114. package/dist/server/application.d.ts +4 -5
  115. package/dist/server/application.js +9 -154
  116. package/dist/server/assets.d.ts +2 -0
  117. package/dist/server/assets.js +6 -0
  118. package/dist/server/dispatch.d.ts +5 -0
  119. package/dist/server/dispatch.js +50 -0
  120. package/dist/server/handler.d.ts +3 -0
  121. package/dist/server/http.d.ts +2 -0
  122. package/dist/server/http.js +2 -0
  123. package/dist/server/index.d.ts +1 -0
  124. package/dist/server/index.js +2 -1
  125. package/dist/server/node.d.ts +5 -1
  126. package/dist/server/preact.d.ts +5 -1
  127. package/dist/server/preact.js +2 -1
  128. package/dist/server/react.d.ts +10 -0
  129. package/dist/server/react.js +22 -0
  130. package/dist/server/render-application.d.ts +31 -0
  131. package/dist/server/render-application.js +118 -0
  132. package/dist/vite/application/lazy-files.d.ts +1 -1
  133. package/dist/vite/application/lazy-files.js +2 -2
  134. package/dist/vite/application/vite.d.ts +2 -1
  135. package/dist/vite/application/vite.js +12 -5
  136. package/dist/vite/react.d.ts +4 -0
  137. package/dist/vite/react.js +9 -1
  138. package/jsr.json +10 -6
  139. package/package.json +21 -12
  140. package/.generated/core/resource-types.ts +0 -44
  141. package/.generated/core/resources.ts +0 -444
  142. package/.generated/react/data.ts +0 -25
  143. package/.generated/react/resources/components.tsx +0 -404
  144. package/dist/core/resource-types.d.ts +0 -37
  145. package/dist/core/resources.d.ts +0 -19
  146. package/dist/core/resources.js +0 -345
  147. package/dist/react/data.d.ts +0 -5
  148. package/dist/react/data.js +0 -3
  149. package/dist/react/link.js +0 -27
  150. package/dist/react/resources/components.d.ts +0 -64
  151. package/dist/react/resources/components.js +0 -239
@@ -1,45 +1 @@
1
- import type { Router } from "../core/index.ts";
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 { routes } = useApplication();
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 function prepareRoutes(
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
+ }