@kasperrt/smolter 0.4.3 → 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 (112) 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/prepare-routes.ts +50 -0
  5. package/.generated/core/presentation.ts +1 -1
  6. package/.generated/preact/client.tsx +1 -26
  7. package/.generated/preact/deferred-hydration.ts +1 -98
  8. package/.generated/preact/index.ts +6 -1
  9. package/.generated/preact/navigation.tsx +13 -5
  10. package/.generated/preact/root-data.ts +1 -45
  11. package/.generated/preact/router.tsx +2 -48
  12. package/.generated/react/client.tsx +130 -0
  13. package/.generated/react/document.tsx +235 -0
  14. package/.generated/react/fetcher.ts +29 -7
  15. package/.generated/react/hooks.ts +20 -18
  16. package/.generated/react/index.ts +17 -1
  17. package/.generated/react/lazy-route.ts +116 -0
  18. package/.generated/react/link.ts +1 -38
  19. package/.generated/react/navigation-state.ts +13 -0
  20. package/.generated/react/navigation.ts +145 -0
  21. package/.generated/react/root-context.ts +22 -0
  22. package/.generated/react/route-hooks.ts +159 -0
  23. package/.generated/react/route-types.ts +91 -0
  24. package/.generated/react/router.ts +50 -0
  25. package/.generated/react/runtime.ts +1 -0
  26. package/.generated/server/application.tsx +18 -253
  27. package/.generated/server/dispatch.ts +87 -0
  28. package/.generated/server/index.ts +2 -0
  29. package/.generated/server/react.ts +34 -0
  30. package/.generated/server/render-application.ts +203 -0
  31. package/.generated/vite/application/lazy-files.ts +2 -1
  32. package/.generated/vite/application/vite.ts +12 -4
  33. package/.generated/vite/react.ts +11 -0
  34. package/CHANGELOG.md +10 -0
  35. package/README.md +41 -41
  36. package/dist/client/hash-history.d.ts +2 -0
  37. package/dist/client/hash-history.js +26 -0
  38. package/dist/client/hydration.d.ts +10 -0
  39. package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
  40. package/dist/client/root-data.d.ts +14 -0
  41. package/dist/{preact → client}/root-data.js +1 -1
  42. package/dist/core/prepare-routes.d.ts +7 -0
  43. package/dist/core/prepare-routes.js +28 -0
  44. package/dist/core/presentation.js +1 -1
  45. package/dist/preact/client.js +5 -26
  46. package/dist/preact/deferred-hydration.d.ts +1 -10
  47. package/dist/preact/document.js +2 -2
  48. package/dist/preact/index.d.ts +1 -1
  49. package/dist/preact/index.js +1 -1
  50. package/dist/preact/navigation-state.js +1 -1
  51. package/dist/preact/navigation.js +7 -5
  52. package/dist/preact/root-data.d.ts +1 -8
  53. package/dist/preact/route-hooks.js +1 -1
  54. package/dist/preact/router.d.ts +2 -2
  55. package/dist/preact/router.js +2 -26
  56. package/dist/react/client.d.ts +20 -0
  57. package/dist/react/client.js +86 -0
  58. package/dist/react/document.d.ts +25 -0
  59. package/dist/react/document.js +169 -0
  60. package/dist/react/fetcher.d.ts +8 -4
  61. package/dist/react/fetcher.js +12 -1
  62. package/dist/react/form.js +1 -1
  63. package/dist/react/hooks.d.ts +7 -6
  64. package/dist/react/index.d.ts +7 -1
  65. package/dist/react/index.js +6 -2
  66. package/dist/react/lazy-route.d.ts +22 -0
  67. package/dist/react/lazy-route.js +57 -0
  68. package/dist/react/link.d.ts +1 -12
  69. package/dist/react/navigation-state.d.ts +2 -0
  70. package/dist/react/navigation-state.js +9 -0
  71. package/dist/react/navigation.d.ts +33 -0
  72. package/dist/react/navigation.js +64 -0
  73. package/dist/react/root-context.d.ts +14 -0
  74. package/dist/react/root-context.js +10 -0
  75. package/dist/react/route-hooks.d.ts +34 -0
  76. package/dist/react/route-hooks.js +89 -0
  77. package/dist/react/route-types.d.ts +69 -0
  78. package/dist/react/router.d.ts +10 -0
  79. package/dist/react/router.js +22 -0
  80. package/dist/react/runtime.d.ts +1 -1
  81. package/dist/react/runtime.js +1 -1
  82. package/dist/server/application.d.ts +4 -8
  83. package/dist/server/application.js +9 -156
  84. package/dist/server/dispatch.d.ts +5 -0
  85. package/dist/server/dispatch.js +50 -0
  86. package/dist/server/index.d.ts +1 -0
  87. package/dist/server/index.js +2 -1
  88. package/dist/server/preact.js +2 -1
  89. package/dist/server/react.d.ts +10 -0
  90. package/dist/server/react.js +22 -0
  91. package/dist/server/render-application.d.ts +31 -0
  92. package/dist/server/render-application.js +118 -0
  93. package/dist/vite/application/lazy-files.d.ts +1 -1
  94. package/dist/vite/application/lazy-files.js +2 -2
  95. package/dist/vite/application/vite.d.ts +2 -1
  96. package/dist/vite/application/vite.js +12 -5
  97. package/dist/vite/react.d.ts +4 -0
  98. package/dist/vite/react.js +9 -1
  99. package/jsr.json +10 -6
  100. package/package.json +18 -10
  101. package/.generated/core/resource-types.ts +0 -48
  102. package/.generated/core/resources.ts +0 -446
  103. package/.generated/react/data.ts +0 -29
  104. package/.generated/react/resources/components.tsx +0 -416
  105. package/dist/core/resource-types.d.ts +0 -41
  106. package/dist/core/resources.d.ts +0 -21
  107. package/dist/core/resources.js +0 -347
  108. package/dist/react/data.d.ts +0 -9
  109. package/dist/react/data.js +0 -3
  110. package/dist/react/link.js +0 -27
  111. package/dist/react/resources/components.d.ts +0 -76
  112. package/dist/react/resources/components.js +0 -250
@@ -0,0 +1,27 @@
1
+ import type { Router } from "../core/index.ts";
2
+
3
+ export function connectHashHistory(router: Router) {
4
+ const read = router.getSnapshot;
5
+ const navigate = router.navigate;
6
+ router.navigate = async (url, options) => {
7
+ const target = new URL(url, router.getSnapshot().url);
8
+ const hash = `#${target.pathname}${target.search}${target.hash}`;
9
+ history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
10
+ await navigate(url, options);
11
+ };
12
+ const unsubscribe = router.subscribe(() => {
13
+ const target = new URL(read().url);
14
+ const hash = `#${target.pathname}${target.search}${target.hash}`;
15
+
16
+ // Redirects commit inside the router rather than through navigate().
17
+ if (location.hash !== hash) history.replaceState(history.state, "", hash);
18
+ });
19
+ const onHashChange = () => void navigate(location.hash.slice(1) || "/");
20
+ window.addEventListener("hashchange", onHashChange);
21
+ const dispose = router.dispose;
22
+ router.dispose = () => {
23
+ unsubscribe();
24
+ window.removeEventListener("hashchange", onHashChange);
25
+ dispose();
26
+ };
27
+ }
@@ -0,0 +1,98 @@
1
+ type Settlement = [number, unknown, string | null];
2
+
3
+ interface PendingValue {
4
+ promise: Promise<unknown>;
5
+ resolve: (value: unknown) => void;
6
+ reject: (reason: Error) => void;
7
+ }
8
+
9
+ export interface DeferredWindow extends Window {
10
+ __smolterDeferred?: Settlement[];
11
+ }
12
+
13
+ /** Restore promise identity before hydrating Suspense boundaries. */
14
+ export function readHydration(json: string, timeout = 30_000) {
15
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 2_147_483_647)
16
+ throw new RangeError(
17
+ "deferredTimeout must be an integer between 1 and 2147483647 ms",
18
+ );
19
+
20
+ const pending = new Map<number, PendingValue>();
21
+ const queue = ((window as DeferredWindow).__smolterDeferred ??= []);
22
+
23
+ function revive(_key: string, value: unknown): unknown {
24
+ if (!value || typeof value !== "object") return value;
25
+ if ("__smolterUndefined" in value) return undefined;
26
+
27
+ if (
28
+ "__smolterPending" in value &&
29
+ typeof value.__smolterPending === "number"
30
+ ) {
31
+ const id = value.__smolterPending;
32
+ const existing = pending.get(id);
33
+ if (existing) return existing.promise;
34
+
35
+ let resolve!: PendingValue["resolve"];
36
+ let reject!: PendingValue["reject"];
37
+ const promise = new Promise((accept, fail) => {
38
+ resolve = accept;
39
+ reject = fail;
40
+ });
41
+ void promise.catch(() => {});
42
+ pending.set(id, { promise, resolve, reject });
43
+
44
+ return promise;
45
+ }
46
+
47
+ if ("__smolterRejected" in value) {
48
+ const failure = value.__smolterRejected as { message: string };
49
+ const rejected = Promise.reject(
50
+ Object.assign(new Error(failure.message), failure),
51
+ );
52
+ void rejected.catch(() => {});
53
+
54
+ return rejected;
55
+ }
56
+
57
+ return value;
58
+ }
59
+
60
+ const hydration = JSON.parse(json, revive);
61
+ const dispose = () => {
62
+ clearTimeout(deadline);
63
+ window.removeEventListener("DOMContentLoaded", dispose);
64
+ for (const target of pending.values())
65
+ target.reject(new Error("Hydration stream closed"));
66
+ pending.clear();
67
+ queue.push = () => 0;
68
+ };
69
+ const deadline = pending.size
70
+ ? setTimeout(() => {
71
+ for (const target of pending.values())
72
+ target.reject(new Error("Hydration stream timed out"));
73
+ dispose();
74
+ }, timeout)
75
+ : undefined;
76
+ const receive = (packet: Settlement) => {
77
+ const [id, value, error] = packet;
78
+ if (id === -1) return dispose();
79
+
80
+ const target = pending.get(id);
81
+ if (!target) return;
82
+
83
+ if (error !== null) target.reject(new Error(error));
84
+ if (error === null)
85
+ target.resolve(JSON.parse(JSON.stringify(value), revive));
86
+ };
87
+ const buffered = queue.splice(0);
88
+ queue.push = (...packets) => {
89
+ packets.forEach(receive);
90
+ return 0;
91
+ };
92
+
93
+ window.addEventListener("DOMContentLoaded", dispose, { once: true });
94
+ buffered.forEach(receive);
95
+ if (document.readyState !== "loading") dispose();
96
+
97
+ return { hydration, dispose };
98
+ }
@@ -0,0 +1,52 @@
1
+ import type { RevalidationArgs, Router } from "../core/index.ts";
2
+
3
+ interface RootModule {
4
+ default?: unknown;
5
+ Layout?: unknown;
6
+ id?: string;
7
+ loaderPath?: string;
8
+ shouldRevalidate?: (args: RevalidationArgs) => boolean;
9
+ }
10
+
11
+ export { createStore as createRootStore } from "../core/store.ts";
12
+
13
+ import { createStore } from "../core/store.ts";
14
+
15
+ export function attachRootData(
16
+ router: Router,
17
+ initial: unknown,
18
+ rootModule?: RootModule,
19
+ ) {
20
+ const store = createStore(initial);
21
+ const loaderPath = rootModule?.loaderPath;
22
+
23
+ if (!rootModule || !loaderPath) return store;
24
+
25
+ let pending: ReturnType<Router["createFetcher"]> | undefined;
26
+ const unregister = router.registerRevalidator(
27
+ rootModule.id ?? "root",
28
+ async (args) => {
29
+ if (rootModule.shouldRevalidate?.(args) === false) return;
30
+
31
+ pending?.dispose();
32
+ const fetcher = router.createFetcher();
33
+ pending = fetcher;
34
+ const result = await fetcher.load(loaderPath);
35
+
36
+ if (pending === fetcher && !result.error) store.update(result.data);
37
+
38
+ fetcher.dispose();
39
+
40
+ if (pending === fetcher) pending = undefined;
41
+ },
42
+ );
43
+ const dispose = router.dispose;
44
+ router.dispose = () => {
45
+ pending?.dispose();
46
+ pending = undefined;
47
+ unregister();
48
+ dispose();
49
+ };
50
+
51
+ return store;
52
+ }
@@ -0,0 +1,50 @@
1
+ import { normalizeResult } from "./responses.ts";
2
+ import type { DataFunction, Route } from "./types.ts";
3
+
4
+ type AppRoute = Route & { routePath?: string; preload?: () => Promise<void> };
5
+
6
+ export function prepareRoutes(
7
+ routes: AppRoute[],
8
+ preserveDeferred = false,
9
+ ): AppRoute[] {
10
+ return routes.map((route) => {
11
+ const wrapped: AppRoute = {
12
+ ...route,
13
+ path: route.routePath ?? route.path,
14
+ shouldRevalidate: (args) => {
15
+ const { currentUrl, nextUrl } = args;
16
+
17
+ if (
18
+ args.reason === "navigation" &&
19
+ currentUrl.pathname === nextUrl.pathname &&
20
+ currentUrl.search === nextUrl.search &&
21
+ currentUrl.hash !== nextUrl.hash
22
+ )
23
+ return false;
24
+
25
+ return route.shouldRevalidate?.(args) ?? args.defaultShouldRevalidate;
26
+ },
27
+ };
28
+
29
+ for (const key of [
30
+ "loader",
31
+ "action",
32
+ "clientLoader",
33
+ "clientAction",
34
+ ] as const) {
35
+ const handler = route[key] as DataFunction | undefined;
36
+
37
+ if (!handler) continue;
38
+
39
+ wrapped[key] = (args) =>
40
+ Promise.resolve()
41
+ .then(() => handler(args))
42
+ .then(
43
+ (value) => normalizeResult(value, !preserveDeferred),
44
+ (error) => normalizeResult(error, !preserveDeferred),
45
+ );
46
+ }
47
+
48
+ return wrapped;
49
+ });
50
+ }
@@ -9,7 +9,7 @@ type CommitView = (update: () => void) => void;
9
9
  const pending = new WeakMap<Router, () => Snapshot>();
10
10
 
11
11
  export function pendingLocation(router: Router) {
12
- const snapshot = pending.get(router)?.();
12
+ const snapshot = pending.get(router)?.() ?? router.getSnapshot();
13
13
 
14
14
  return snapshot?.state !== "idle" && snapshot?.url
15
15
  ? new URL(snapshot.url)
@@ -10,6 +10,7 @@ import {
10
10
  recoverDeployment,
11
11
  watchDeployment,
12
12
  } from "../client/index.ts";
13
+ import { connectHashHistory } from "../client/hash-history.ts";
13
14
  import { createDataRouter, matchRoutes, type Router } from "../core/index.ts";
14
15
  import { connectNavigation } from "../core/navigation.ts";
15
16
  import { retainRoute } from "../core/presentation.ts";
@@ -123,29 +124,3 @@ export async function mountApplication(
123
124
 
124
125
  return router;
125
126
  }
126
-
127
- function connectHashHistory(router: Router) {
128
- const read = router.getSnapshot;
129
- const navigate = router.navigate;
130
- router.navigate = async (url, options) => {
131
- const target = new URL(url, router.getSnapshot().url);
132
- const hash = `#${target.pathname}${target.search}${target.hash}`;
133
- history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
134
- await navigate(url, options);
135
- };
136
- const unsubscribe = router.subscribe(() => {
137
- const target = new URL(read().url);
138
- const hash = `#${target.pathname}${target.search}${target.hash}`;
139
-
140
- // Redirects commit inside the router rather than through navigate().
141
- if (location.hash !== hash) history.replaceState(history.state, "", hash);
142
- });
143
- const onHashChange = () => void navigate(location.hash.slice(1) || "/");
144
- window.addEventListener("hashchange", onHashChange);
145
- const dispose = router.dispose;
146
- router.dispose = () => {
147
- unsubscribe();
148
- window.removeEventListener("hashchange", onHashChange);
149
- dispose();
150
- };
151
- }
@@ -1,98 +1 @@
1
- type Settlement = [number, unknown, string | null];
2
-
3
- interface PendingValue {
4
- promise: Promise<unknown>;
5
- resolve: (value: unknown) => void;
6
- reject: (reason: Error) => void;
7
- }
8
-
9
- export interface DeferredWindow extends Window {
10
- __smolterDeferred?: Settlement[];
11
- }
12
-
13
- /** Restore promise identity before hydrating Suspense boundaries. */
14
- export function readHydration(json: string, timeout = 30_000) {
15
- if (!Number.isInteger(timeout) || timeout < 1 || timeout > 2_147_483_647)
16
- throw new RangeError(
17
- "deferredTimeout must be an integer between 1 and 2147483647 ms",
18
- );
19
-
20
- const pending = new Map<number, PendingValue>();
21
- const queue = ((window as DeferredWindow).__smolterDeferred ??= []);
22
-
23
- function revive(_key: string, value: unknown): unknown {
24
- if (!value || typeof value !== "object") return value;
25
- if ("__smolterUndefined" in value) return undefined;
26
-
27
- if (
28
- "__smolterPending" in value &&
29
- typeof value.__smolterPending === "number"
30
- ) {
31
- const id = value.__smolterPending;
32
- const existing = pending.get(id);
33
- if (existing) return existing.promise;
34
-
35
- let resolve!: PendingValue["resolve"];
36
- let reject!: PendingValue["reject"];
37
- const promise = new Promise((accept, fail) => {
38
- resolve = accept;
39
- reject = fail;
40
- });
41
- void promise.catch(() => {});
42
- pending.set(id, { promise, resolve, reject });
43
-
44
- return promise;
45
- }
46
-
47
- if ("__smolterRejected" in value) {
48
- const failure = value.__smolterRejected as { message: string };
49
- const rejected = Promise.reject(
50
- Object.assign(new Error(failure.message), failure),
51
- );
52
- void rejected.catch(() => {});
53
-
54
- return rejected;
55
- }
56
-
57
- return value;
58
- }
59
-
60
- const hydration = JSON.parse(json, revive);
61
- const dispose = () => {
62
- clearTimeout(deadline);
63
- window.removeEventListener("DOMContentLoaded", dispose);
64
- for (const target of pending.values())
65
- target.reject(new Error("Hydration stream closed"));
66
- pending.clear();
67
- queue.push = () => 0;
68
- };
69
- const deadline = pending.size
70
- ? setTimeout(() => {
71
- for (const target of pending.values())
72
- target.reject(new Error("Hydration stream timed out"));
73
- dispose();
74
- }, timeout)
75
- : undefined;
76
- const receive = (packet: Settlement) => {
77
- const [id, value, error] = packet;
78
- if (id === -1) return dispose();
79
-
80
- const target = pending.get(id);
81
- if (!target) return;
82
-
83
- if (error !== null) target.reject(new Error(error));
84
- if (error === null)
85
- target.resolve(JSON.parse(JSON.stringify(value), revive));
86
- };
87
- const buffered = queue.splice(0);
88
- queue.push = (...packets) => {
89
- packets.forEach(receive);
90
- return 0;
91
- };
92
-
93
- window.addEventListener("DOMContentLoaded", dispose, { once: true });
94
- buffered.forEach(receive);
95
- if (document.readyState !== "loading") dispose();
96
-
97
- return { hydration, dispose };
98
- }
1
+ export { type DeferredWindow, readHydration } from "../client/hydration.ts";
@@ -34,5 +34,10 @@ export {
34
34
  useSubmit,
35
35
  } from "./route-hooks.tsx";
36
36
  export * from "./route-types.ts";
37
- export { Outlet, type OutletProps, RouterApplication } from "./router.tsx";
37
+ export {
38
+ type ApplicationProps,
39
+ Outlet,
40
+ type OutletProps,
41
+ RouterApplication,
42
+ } from "./router.tsx";
38
43
  export * from "./routes.ts";
@@ -7,7 +7,7 @@ import { navigationClick } from "../core/url.ts";
7
7
  import type { TargetedMouseEvent } from "preact";
8
8
  import type { ComponentProps, ReactNode } from "preact/compat";
9
9
  import { useRouter, useSnapshot } from "./context.ts";
10
- import { useURLLocation } from "./route-hooks.tsx";
10
+ import { useNavigation, useURLLocation } from "./route-hooks.tsx";
11
11
  import type { AppRoute } from "./route-types.ts";
12
12
  import { createElement } from "./runtime.ts";
13
13
 
@@ -130,11 +130,19 @@ export function NavLink({
130
130
  ...props
131
131
  }: NavLinkProps): ReactNode {
132
132
  const location = useURLLocation();
133
- const target = new URL(to, location.href).pathname;
133
+ const destination = resolveApplicationURL(new URL(to, location.href));
134
+ const target = destination.pathname;
134
135
  const isActive =
135
- location.pathname === target ||
136
- (!end && target !== "/" && location.pathname.startsWith(`${target}/`));
137
- const state = { isActive, isPending: false };
136
+ destination.origin === location.origin &&
137
+ (location.pathname === target ||
138
+ (!end && target !== "/" && location.pathname.startsWith(`${target}/`)));
139
+ const pending = useNavigation().location;
140
+ const isPending =
141
+ !!pending &&
142
+ pending.origin === destination.origin &&
143
+ (pending.pathname === target ||
144
+ (!end && target !== "/" && pending.pathname.startsWith(`${target}/`)));
145
+ const state = { isActive, isPending };
138
146
 
139
147
  return createElement(Link, {
140
148
  ...props,
@@ -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";
@@ -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,7 +8,6 @@ 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
 
@@ -34,6 +31,7 @@ export function Outlet({
34
31
  );
35
32
  }
36
33
 
34
+ /** Root data, document components and request-scoped router for an application. */
37
35
  export interface ApplicationProps extends ApplicationContext {
38
36
  document?: boolean;
39
37
  rootStore?: ReturnType<typeof createRootStore>;
@@ -69,48 +67,4 @@ export function RouterApplication(
69
67
  );
70
68
  }
71
69
 
72
- export function prepareRoutes(
73
- routes: AppRoute[],
74
- preserveDeferred = false,
75
- ): AppRoute[] {
76
- return routes.map((route) => {
77
- const wrapped: AppRoute = {
78
- ...route,
79
- path: route.routePath ?? route.path,
80
- shouldRevalidate: (args) => {
81
- const { currentUrl, nextUrl } = args;
82
-
83
- if (
84
- args.reason === "navigation" &&
85
- currentUrl.pathname === nextUrl.pathname &&
86
- currentUrl.search === nextUrl.search &&
87
- currentUrl.hash !== nextUrl.hash
88
- )
89
- return false;
90
-
91
- return route.shouldRevalidate?.(args) ?? args.defaultShouldRevalidate;
92
- },
93
- };
94
-
95
- for (const key of [
96
- "loader",
97
- "action",
98
- "clientLoader",
99
- "clientAction",
100
- ] as const) {
101
- const handler = route[key] as DataFunction | undefined;
102
-
103
- if (!handler) continue;
104
-
105
- wrapped[key] = (args) =>
106
- Promise.resolve()
107
- .then(() => handler(args))
108
- .then(
109
- (value) => normalizeResult(value, !preserveDeferred),
110
- (error) => normalizeResult(error, !preserveDeferred),
111
- );
112
- }
113
-
114
- return wrapped;
115
- });
116
- }
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
+ }