@kasperrt/smolter 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/{src/internal → .generated}/client/index.ts +1 -0
  2. package/.generated/core/environment.ts +43 -0
  3. package/{src/internal → .generated}/core/errors.ts +1 -1
  4. package/{src/internal → .generated}/core/index.ts +1 -0
  5. package/.generated/core/navigation-router.ts +107 -0
  6. package/.generated/core/navigation.ts +147 -0
  7. package/.generated/core/presentation.ts +108 -0
  8. package/.generated/core/resource-types.ts +44 -0
  9. package/.generated/core/resources.ts +444 -0
  10. package/.generated/core/responses.ts +86 -0
  11. package/{src/internal → .generated}/core/router.ts +136 -63
  12. package/{src/internal → .generated}/core/serialize.ts +7 -2
  13. package/.generated/core/store.ts +25 -0
  14. package/.generated/core/submission.ts +116 -0
  15. package/{src/internal → .generated}/core/types.ts +18 -1
  16. package/.generated/core/url.ts +29 -0
  17. package/.generated/core/utils.ts +5 -0
  18. package/.generated/preact/await.tsx +41 -0
  19. package/.generated/preact/client.tsx +93 -0
  20. package/.generated/preact/context.ts +59 -0
  21. package/.generated/preact/document.tsx +198 -0
  22. package/{src/internal → .generated}/preact/fetcher.ts +43 -23
  23. package/.generated/preact/form.ts +58 -0
  24. package/{src/internal → .generated}/preact/hooks.ts +25 -28
  25. package/.generated/preact/index.ts +34 -0
  26. package/.generated/preact/lazy-route.ts +100 -0
  27. package/.generated/preact/navigation-state.ts +9 -0
  28. package/.generated/preact/navigation.tsx +147 -0
  29. package/.generated/preact/root-context.ts +21 -0
  30. package/.generated/preact/root-data.ts +45 -0
  31. package/.generated/preact/route-hooks.tsx +170 -0
  32. package/.generated/preact/route-types.ts +76 -0
  33. package/.generated/preact/router.tsx +107 -0
  34. package/{src/internal → .generated}/preact/routes.ts +17 -9
  35. package/.generated/react/context.ts +59 -0
  36. package/.generated/react/data.ts +25 -0
  37. package/{src/internal → .generated}/react/fetcher.ts +13 -11
  38. package/.generated/react/form.ts +58 -0
  39. package/{src/internal → .generated}/react/hooks.ts +9 -14
  40. package/{src/internal/preact → .generated/react}/index.ts +5 -1
  41. package/{src/internal → .generated}/react/link.ts +13 -21
  42. package/.generated/react/resources/components.tsx +404 -0
  43. package/{src/internal → .generated}/react/routes.ts +17 -9
  44. package/.generated/server/application.tsx +185 -0
  45. package/.generated/server/assets.ts +19 -0
  46. package/.generated/server/handler.ts +69 -0
  47. package/.generated/server/http.ts +55 -0
  48. package/{src/internal → .generated}/server/index.ts +3 -3
  49. package/.generated/server/node.ts +4 -0
  50. package/.generated/server/preact.ts +7 -0
  51. package/.generated/vite/application/browser-boundary.ts +42 -0
  52. package/.generated/vite/application/lazy-files.ts +136 -0
  53. package/.generated/vite/application/vite.ts +144 -0
  54. package/{src/preact-vite.ts → .generated/vite/preact.ts} +9 -3
  55. package/{src/react-vite.ts → .generated/vite/react.ts} +3 -3
  56. package/CHANGELOG.md +54 -0
  57. package/README.md +227 -35
  58. package/dist/client/deserialize.js +10 -0
  59. package/dist/{internal/client → client}/index.d.ts +1 -0
  60. package/dist/client/index.js +46 -0
  61. package/dist/core/environment.d.ts +12 -0
  62. package/dist/core/environment.js +23 -0
  63. package/dist/core/errors.js +7 -0
  64. package/dist/{internal/core → core}/index.d.ts +1 -0
  65. package/dist/core/match.js +33 -0
  66. package/dist/core/navigation-router.d.ts +15 -0
  67. package/dist/core/navigation-router.js +64 -0
  68. package/dist/core/navigation.d.ts +15 -0
  69. package/dist/core/navigation.js +92 -0
  70. package/dist/core/presentation.d.ts +5 -0
  71. package/dist/core/presentation.js +69 -0
  72. package/dist/core/resource-types.d.ts +37 -0
  73. package/dist/core/resources.d.ts +19 -0
  74. package/dist/core/resources.js +345 -0
  75. package/dist/core/responses.d.ts +15 -0
  76. package/dist/core/responses.js +48 -0
  77. package/dist/{internal/core → core}/router.d.ts +1 -1
  78. package/dist/core/router.js +204 -0
  79. package/dist/{internal/core → core}/serialize.d.ts +2 -0
  80. package/dist/core/serialize.js +17 -0
  81. package/dist/core/store.d.ts +6 -0
  82. package/dist/core/store.js +21 -0
  83. package/dist/core/submission.d.ts +30 -0
  84. package/dist/core/submission.js +41 -0
  85. package/dist/{internal/core → core}/types.d.ts +16 -1
  86. package/dist/core/url.d.ts +7 -0
  87. package/dist/core/url.js +12 -0
  88. package/dist/core/utils.d.ts +5 -0
  89. package/dist/core/utils.js +5 -0
  90. package/dist/files/index.js +64 -0
  91. package/dist/preact/await.d.ts +7 -0
  92. package/dist/preact/await.js +19 -0
  93. package/dist/preact/client.d.ts +7 -0
  94. package/dist/preact/client.js +67 -0
  95. package/dist/preact/context.d.ts +18 -0
  96. package/dist/preact/context.js +23 -0
  97. package/dist/preact/document.d.ts +15 -0
  98. package/dist/preact/document.js +155 -0
  99. package/dist/{internal/preact → preact}/fetcher.d.ts +9 -5
  100. package/dist/preact/fetcher.js +45 -0
  101. package/dist/{internal/preact → preact}/form.d.ts +5 -6
  102. package/dist/preact/form.js +21 -0
  103. package/dist/{internal/preact → preact}/hooks.d.ts +7 -6
  104. package/dist/preact/hooks.js +25 -0
  105. package/dist/preact/index.d.ts +18 -0
  106. package/dist/preact/index.js +15 -0
  107. package/dist/preact/lazy-route.d.ts +19 -0
  108. package/dist/preact/lazy-route.js +51 -0
  109. package/dist/preact/navigation-state.d.ts +2 -0
  110. package/dist/preact/navigation-state.js +9 -0
  111. package/dist/preact/navigation.d.ts +28 -0
  112. package/dist/preact/navigation.js +60 -0
  113. package/dist/preact/root-context.d.ts +13 -0
  114. package/dist/preact/root-context.js +9 -0
  115. package/dist/preact/root-data.d.ts +8 -0
  116. package/dist/preact/root-data.js +28 -0
  117. package/dist/preact/route-hooks.d.ts +32 -0
  118. package/dist/preact/route-hooks.js +94 -0
  119. package/dist/preact/route-types.d.ts +54 -0
  120. package/dist/preact/router.d.ts +16 -0
  121. package/dist/preact/router.js +63 -0
  122. package/dist/{internal/preact → preact}/routes.d.ts +1 -1
  123. package/dist/preact/routes.js +12 -0
  124. package/dist/{internal/preact → preact}/runtime.js +1 -1
  125. package/dist/react/context.d.ts +18 -0
  126. package/dist/react/context.js +23 -0
  127. package/dist/react/data.d.ts +5 -0
  128. package/dist/react/data.js +3 -0
  129. package/dist/{internal/react → react}/fetcher.d.ts +2 -2
  130. package/dist/react/fetcher.js +34 -0
  131. package/dist/{internal/react → react}/form.d.ts +5 -6
  132. package/dist/react/form.js +21 -0
  133. package/dist/react/hooks.js +25 -0
  134. package/dist/{internal/react → react}/index.d.ts +1 -1
  135. package/dist/react/index.js +10 -0
  136. package/dist/{internal/react → react}/link.d.ts +2 -2
  137. package/dist/react/link.js +27 -0
  138. package/dist/react/resources/components.d.ts +64 -0
  139. package/dist/react/resources/components.js +239 -0
  140. package/dist/{internal/react → react}/routes.d.ts +1 -1
  141. package/dist/react/routes.js +12 -0
  142. package/dist/react/runtime.js +1 -0
  143. package/dist/server/application.d.ts +4 -0
  144. package/dist/server/application.js +119 -0
  145. package/dist/server/assets.d.ts +5 -0
  146. package/dist/server/assets.js +7 -0
  147. package/dist/server/handler.d.ts +19 -0
  148. package/dist/server/handler.js +22 -0
  149. package/dist/server/http.d.ts +4 -0
  150. package/dist/server/http.js +27 -0
  151. package/dist/server/index.js +76 -0
  152. package/dist/server/node.d.ts +3 -0
  153. package/dist/server/node.js +3 -0
  154. package/dist/server/preact.d.ts +3 -0
  155. package/dist/server/preact.js +4 -0
  156. package/dist/vite/application/browser-boundary.d.ts +3 -0
  157. package/dist/vite/application/browser-boundary.js +35 -0
  158. package/dist/vite/application/lazy-files.d.ts +2 -0
  159. package/dist/vite/application/lazy-files.js +87 -0
  160. package/dist/vite/application/vite.d.ts +9 -0
  161. package/dist/vite/application/vite.js +101 -0
  162. package/dist/vite/index.js +104 -0
  163. package/dist/{preact-vite.d.ts → vite/preact.d.ts} +5 -3
  164. package/dist/vite/preact.js +15 -0
  165. package/dist/{react-vite.d.ts → vite/react.d.ts} +3 -3
  166. package/dist/vite/react.js +14 -0
  167. package/dist/vite/transport.js +35 -0
  168. package/jsr.json +28 -16
  169. package/package.json +116 -36
  170. package/dist/client.d.ts +0 -2
  171. package/dist/client.js +0 -2
  172. package/dist/core.d.ts +0 -2
  173. package/dist/core.js +0 -2
  174. package/dist/files.d.ts +0 -2
  175. package/dist/files.js +0 -2
  176. package/dist/internal/client/deserialize.js +0 -11
  177. package/dist/internal/client/index.js +0 -44
  178. package/dist/internal/core/errors.js +0 -6
  179. package/dist/internal/core/index.js +0 -7
  180. package/dist/internal/core/match.js +0 -27
  181. package/dist/internal/core/router.js +0 -189
  182. package/dist/internal/core/serialize.js +0 -13
  183. package/dist/internal/core/types.js +0 -5
  184. package/dist/internal/core/url.d.ts +0 -2
  185. package/dist/internal/core/url.js +0 -6
  186. package/dist/internal/files/index.js +0 -77
  187. package/dist/internal/preact/context.d.ts +0 -18
  188. package/dist/internal/preact/context.js +0 -20
  189. package/dist/internal/preact/fetcher.js +0 -28
  190. package/dist/internal/preact/form.js +0 -41
  191. package/dist/internal/preact/hooks.js +0 -35
  192. package/dist/internal/preact/index.d.ts +0 -9
  193. package/dist/internal/preact/index.js +0 -7
  194. package/dist/internal/preact/link.d.ts +0 -12
  195. package/dist/internal/preact/link.js +0 -28
  196. package/dist/internal/preact/routes.js +0 -22
  197. package/dist/internal/react/context.d.ts +0 -18
  198. package/dist/internal/react/context.js +0 -20
  199. package/dist/internal/react/fetcher.js +0 -28
  200. package/dist/internal/react/form.js +0 -41
  201. package/dist/internal/react/hooks.js +0 -35
  202. package/dist/internal/react/index.js +0 -7
  203. package/dist/internal/react/link.js +0 -28
  204. package/dist/internal/react/routes.js +0 -22
  205. package/dist/internal/react/runtime.js +0 -2
  206. package/dist/internal/server/index.js +0 -69
  207. package/dist/internal/vite/index.js +0 -132
  208. package/dist/internal/vite/transport.js +0 -42
  209. package/dist/preact-client.d.ts +0 -2
  210. package/dist/preact-client.js +0 -2
  211. package/dist/preact-server.d.ts +0 -2
  212. package/dist/preact-server.js +0 -2
  213. package/dist/preact-vite.js +0 -9
  214. package/dist/preact.d.ts +0 -2
  215. package/dist/preact.js +0 -2
  216. package/dist/react-client.d.ts +0 -2
  217. package/dist/react-client.js +0 -2
  218. package/dist/react-server.d.ts +0 -2
  219. package/dist/react-server.js +0 -2
  220. package/dist/react-vite.js +0 -9
  221. package/dist/react.d.ts +0 -2
  222. package/dist/react.js +0 -2
  223. package/dist/server.d.ts +0 -2
  224. package/dist/server.js +0 -2
  225. package/dist/utils.d.ts +0 -5
  226. package/dist/utils.js +0 -5
  227. package/dist/vite.d.ts +0 -2
  228. package/dist/vite.js +0 -2
  229. package/src/client.ts +0 -2
  230. package/src/core.ts +0 -2
  231. package/src/files.ts +0 -2
  232. package/src/internal/core/url.ts +0 -6
  233. package/src/internal/preact/context.ts +0 -46
  234. package/src/internal/preact/form.ts +0 -79
  235. package/src/internal/preact/link.ts +0 -46
  236. package/src/internal/react/context.ts +0 -46
  237. package/src/internal/react/form.ts +0 -79
  238. package/src/internal/react/index.ts +0 -13
  239. package/src/preact-client.ts +0 -2
  240. package/src/preact-server.ts +0 -2
  241. package/src/preact.ts +0 -2
  242. package/src/react-client.ts +0 -2
  243. package/src/react-server.ts +0 -2
  244. package/src/react.ts +0 -2
  245. package/src/server.ts +0 -2
  246. package/src/utils.ts +0 -5
  247. package/src/vite.ts +0 -2
  248. /package/{src/internal → .generated}/client/deserialize.ts +0 -0
  249. /package/{src/internal → .generated}/core/match.ts +0 -0
  250. /package/{src/internal → .generated}/files/index.ts +0 -0
  251. /package/{src/internal → .generated}/preact/runtime.ts +0 -0
  252. /package/{src/internal → .generated}/react/runtime.ts +0 -0
  253. /package/{src/internal → .generated}/vite/index.ts +0 -0
  254. /package/{src/internal → .generated}/vite/transport.ts +0 -0
  255. /package/dist/{internal/client → client}/deserialize.d.ts +0 -0
  256. /package/dist/{internal/core → core}/errors.d.ts +0 -0
  257. /package/dist/{internal/core → core}/match.d.ts +0 -0
  258. /package/dist/{internal/files → files}/index.d.ts +0 -0
  259. /package/dist/{internal/preact → preact}/runtime.d.ts +0 -0
  260. /package/dist/{internal/react → react}/hooks.d.ts +0 -0
  261. /package/dist/{internal/react → react}/runtime.d.ts +0 -0
  262. /package/dist/{internal/server → server}/index.d.ts +0 -0
  263. /package/dist/{internal/vite → vite}/index.d.ts +0 -0
  264. /package/dist/{internal/vite → vite}/transport.d.ts +0 -0
@@ -16,6 +16,7 @@ export { deserialize } from "./deserialize.ts";
16
16
 
17
17
  /** Browser-safe route exports, excluding server implementation code. */
18
18
  export interface ClientRouteModule {
19
+ id?: string;
19
20
  default?: unknown;
20
21
  HydrateFallback?: unknown;
21
22
  ErrorBoundary?: unknown;
@@ -0,0 +1,43 @@
1
+ export interface ApplicationEnvironment {
2
+ resolveURL?: (url: URL) => URL;
3
+ isDocumentHandoff?: (from: URL, to: URL) => boolean;
4
+ leaveDocument?: (destination: URL) => void;
5
+ entryScript?: string;
6
+ }
7
+
8
+ // An absent configuration lets bundlers remove unused deployment callbacks.
9
+ export let applicationEnvironment: ApplicationEnvironment | undefined;
10
+
11
+ /** Configure deployment-specific navigation before rendering or mounting. */
12
+ export function configureApplication(
13
+ environment: ApplicationEnvironment,
14
+ ): void {
15
+ applicationEnvironment = environment;
16
+ }
17
+
18
+ export function resolveApplicationURL(url: URL): URL {
19
+ return applicationEnvironment?.resolveURL?.(url) ?? url;
20
+ }
21
+
22
+ export function handoffDocument(event: MouseEvent, destination: URL): void {
23
+ if (
24
+ !applicationEnvironment?.isDocumentHandoff?.(
25
+ new URL(location.href),
26
+ destination,
27
+ )
28
+ )
29
+ return;
30
+
31
+ event.preventDefault();
32
+ leaveDocument(destination);
33
+ }
34
+
35
+ export function leaveDocument(destination: URL): void {
36
+ if (applicationEnvironment?.leaveDocument) {
37
+ applicationEnvironment.leaveDocument(destination);
38
+
39
+ return;
40
+ }
41
+
42
+ location.assign(destination.href);
43
+ }
@@ -8,6 +8,6 @@ export function safeWrapAsync<T>(
8
8
  .then(work)
9
9
  .then(
10
10
  (data) => (data instanceof Error ? { error: data } : { data }),
11
- (cause: unknown) => ({ error: new Error(String(cause), { cause }) }),
11
+ (cause: unknown) => ({ error: Error(String(cause), { cause }) }),
12
12
  );
13
13
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  export * from "./errors.ts";
4
4
  export * from "./match.ts";
5
+ export * from "./navigation-router.ts";
5
6
  export * from "./router.ts";
6
7
  export * from "./serialize.ts";
7
8
  export * from "./types.ts";
@@ -0,0 +1,107 @@
1
+ import { matchRoutes } from "./match.ts";
2
+ import type {
3
+ NavigateOptions,
4
+ Route,
5
+ Router,
6
+ RouterOptions,
7
+ Snapshot,
8
+ } from "./types.ts";
9
+ import { internalURL } from "./url.ts";
10
+
11
+ /** Navigation and subscriptions without the loader/action engine. */
12
+ export type NavigationRouter = Pick<
13
+ Router,
14
+ "routes" | "getSnapshot" | "subscribe" | "start" | "navigate" | "dispose"
15
+ >;
16
+
17
+ /** Route modules for navigation only; data handlers require createDataRouter. */
18
+ export type NavigationRoute = Route & {
19
+ loader?: never;
20
+ action?: never;
21
+ clientLoader?: never;
22
+ clientAction?: never;
23
+ shouldRevalidate?: never;
24
+ };
25
+
26
+ /** Initial URL and optional browser history for navigation-only routing. */
27
+ export type NavigationRouterOptions = Pick<RouterOptions, "url" | "browser">;
28
+
29
+ /** Create a route matcher and history store without data requests or fetchers. */
30
+ export function createRouter(
31
+ routes: readonly NavigationRoute[],
32
+ options: NavigationRouterOptions = {},
33
+ ): NavigationRouter {
34
+ const browser = options.browser && typeof window !== "undefined";
35
+ const listeners = new Set<() => void>();
36
+ let users = 0;
37
+ const resolve = (url: URL): Snapshot => {
38
+ const match = matchRoutes(routes, url.pathname);
39
+
40
+ return {
41
+ url: url.href,
42
+ path: match?.route.path,
43
+ params: match?.params ?? {},
44
+ state: "idle",
45
+ loaded: true,
46
+ };
47
+ };
48
+ let snapshot = resolve(
49
+ new URL(options.url ?? (browser ? location.href : "/"), "http://localhost"),
50
+ );
51
+ const update = (url: URL) => {
52
+ if (url.href === snapshot.url) return;
53
+
54
+ snapshot = resolve(url);
55
+
56
+ for (const listener of listeners) listener();
57
+ };
58
+ const pop = () => update(new URL(location.href));
59
+
60
+ return {
61
+ routes,
62
+ getSnapshot: () => snapshot,
63
+ subscribe(listener) {
64
+ listeners.add(listener);
65
+
66
+ return () => {
67
+ listeners.delete(listener);
68
+ };
69
+ },
70
+ start() {
71
+ if (!users++ && browser) {
72
+ addEventListener("popstate", pop);
73
+ addEventListener("hashchange", pop);
74
+ pop();
75
+ }
76
+
77
+ return () => {
78
+ if (!--users && browser) {
79
+ removeEventListener("popstate", pop);
80
+ removeEventListener("hashchange", pop);
81
+ }
82
+ };
83
+ },
84
+ async navigate(href: string, operation?: NavigateOptions) {
85
+ const url = internalURL(snapshot.url, href);
86
+
87
+ if (!url) return;
88
+
89
+ if (browser)
90
+ history[operation?.replace ? "replaceState" : "pushState"](
91
+ operation?.state ?? null,
92
+ "",
93
+ url,
94
+ );
95
+
96
+ update(url);
97
+ },
98
+ dispose() {
99
+ listeners.clear();
100
+
101
+ if (browser) {
102
+ removeEventListener("popstate", pop);
103
+ removeEventListener("hashchange", pop);
104
+ }
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,147 @@
1
+ import {
2
+ handoffDocument,
3
+ leaveDocument,
4
+ resolveApplicationURL,
5
+ } from "./environment.ts";
6
+ import { createStore } from "./store.ts";
7
+ import type { NavigateOptions, Router } from "./types.ts";
8
+ import { navigationClick } from "./url.ts";
9
+
10
+ export type NavigationType = "POP" | "PUSH" | "REPLACE";
11
+
12
+ export interface NavigationSnapshot {
13
+ type: NavigationType;
14
+ state: unknown;
15
+ key: string;
16
+ preventScrollReset: boolean;
17
+ }
18
+
19
+ const scrollHistory = new WeakMap<Router, Map<string, [number, number]>>();
20
+
21
+ export function scrollPositions(router: Router) {
22
+ let positions = scrollHistory.get(router);
23
+
24
+ if (!positions) {
25
+ positions = new Map();
26
+ scrollHistory.set(router, positions);
27
+ }
28
+
29
+ return positions;
30
+ }
31
+
32
+ const stores = new WeakMap<Router, ReturnType<typeof createNavigationStore>>();
33
+
34
+ function createNavigationStore() {
35
+ return createStore<NavigationSnapshot>({
36
+ type: "POP",
37
+ state: typeof history === "undefined" ? null : history.state?.state,
38
+ key:
39
+ typeof history === "undefined"
40
+ ? "initial"
41
+ : (history.state?.key ?? "initial"),
42
+ preventScrollReset: false,
43
+ });
44
+ }
45
+
46
+ export function navigationStore(router: Router) {
47
+ let store = stores.get(router);
48
+
49
+ if (!store) {
50
+ store = createNavigationStore();
51
+ stores.set(router, store);
52
+ }
53
+
54
+ return store;
55
+ }
56
+
57
+ export function connectNavigation(router: Router) {
58
+ const store = navigationStore(router);
59
+ const navigate = router.navigate;
60
+ let sequence = 0;
61
+ let pending: NavigationSnapshot | undefined;
62
+ const committed = () => {
63
+ if (!pending || router.getSnapshot().state !== "idle") return;
64
+
65
+ const next = pending;
66
+ pending = undefined;
67
+
68
+ if (next.type !== "POP") history.replaceState(next, "", location.href);
69
+
70
+ store.update(next);
71
+ };
72
+ const unsubscribe = router.subscribe(committed);
73
+
74
+ const rememberScroll = () => {
75
+ scrollPositions(router).set(store.getSnapshot().key, [
76
+ window.scrollX,
77
+ window.scrollY,
78
+ ]);
79
+ };
80
+
81
+ router.navigate = async (href, options: NavigateOptions = {}) => {
82
+ const destination = resolveApplicationURL(
83
+ new URL(href, router.getSnapshot().url),
84
+ );
85
+
86
+ if (destination.origin !== location.origin) {
87
+ leaveDocument(destination);
88
+
89
+ return;
90
+ }
91
+
92
+ rememberScroll();
93
+ const current = ++sequence;
94
+ pending = {
95
+ type: options.replace ? "REPLACE" : "PUSH",
96
+ state: options.state ?? null,
97
+ key: `${Date.now().toString(36)}-${current}`,
98
+ preventScrollReset: options.preventScrollReset ?? false,
99
+ };
100
+ // Retained views may commit after navigate resolves; the subscription waits
101
+ // for that final idle snapshot rather than starting another listener.
102
+ await navigate(href, options);
103
+ committed();
104
+ };
105
+
106
+ const pop = () => {
107
+ rememberScroll();
108
+ pending = {
109
+ ...history.state,
110
+ type: "POP",
111
+ key: history.state?.key ?? "initial",
112
+ preventScrollReset: false,
113
+ };
114
+ committed();
115
+ };
116
+
117
+ const handoff = (event: MouseEvent) => {
118
+ const anchor =
119
+ event.target instanceof Element ? event.target.closest("a[href]") : null;
120
+
121
+ if (
122
+ !(anchor instanceof HTMLAnchorElement) ||
123
+ !navigationClick(event, anchor)
124
+ )
125
+ return;
126
+
127
+ const target = resolveApplicationURL(new URL(anchor.href));
128
+
129
+ handoffDocument(event, target);
130
+ };
131
+
132
+ if (typeof document !== "undefined")
133
+ document.addEventListener("click", handoff);
134
+
135
+ window.addEventListener("popstate", pop);
136
+ const dispose = router.dispose;
137
+ router.dispose = () => {
138
+ pending = undefined;
139
+ unsubscribe();
140
+ window.removeEventListener("popstate", pop);
141
+
142
+ if (typeof document !== "undefined")
143
+ document.removeEventListener("click", handoff);
144
+
145
+ dispose();
146
+ };
147
+ }
@@ -0,0 +1,108 @@
1
+ import type { NavigateOptions, Route, Router, Snapshot } from "./types.ts";
2
+
3
+ interface ResolvableRoute extends Route {
4
+ resolveComponent?: () => unknown;
5
+ }
6
+
7
+ type CommitView = (update: () => void) => void;
8
+
9
+ const pending = new WeakMap<Router, () => Snapshot>();
10
+
11
+ export function pendingLocation(router: Router) {
12
+ const snapshot = pending.get(router)?.();
13
+
14
+ return snapshot?.state !== "idle" && snapshot?.url
15
+ ? new URL(snapshot.url)
16
+ : undefined;
17
+ }
18
+
19
+ // Keep the committed route's view and data together while navigation loads.
20
+ // The underlying router still owns cancellation, redirects and fetcher state.
21
+ export function retainRoute(router: Router, commitView: CommitView) {
22
+ const read = router.getSnapshot;
23
+ pending.set(router, read);
24
+ let viewTransition: boolean | undefined;
25
+ const navigate = router.navigate;
26
+ router.navigate = (href, options: NavigateOptions = {}) => {
27
+ viewTransition = options.viewTransition;
28
+
29
+ return navigate(href, options);
30
+ };
31
+ let visible = read();
32
+ let revision = 0;
33
+ const listeners = new Set<() => void>();
34
+
35
+ const publish = () => {
36
+ for (const listener of listeners) listener();
37
+ };
38
+
39
+ const unsubscribe = router.subscribe(() => {
40
+ const next = read();
41
+ const current = ++revision;
42
+
43
+ if (next.state !== "idle" && visible.loaded) {
44
+ visible = { ...visible, state: next.state };
45
+ publish();
46
+
47
+ return;
48
+ }
49
+
50
+ const commit = () => {
51
+ if (current !== revision) return;
52
+
53
+ commitView(() => {
54
+ visible = next;
55
+ publish();
56
+ });
57
+ };
58
+
59
+ const previousURL = new URL(visible.url);
60
+ const nextURL = new URL(next.url);
61
+ const changed =
62
+ previousURL.pathname + previousURL.search !==
63
+ nextURL.pathname + nextURL.search;
64
+ const component = (path: string | undefined) => {
65
+ const route = router.routes.find((item) => item.path === path) as
66
+ | ResolvableRoute
67
+ | undefined;
68
+
69
+ return route?.resolveComponent?.() ?? route?.default;
70
+ };
71
+ const previousComponent = component(visible.path);
72
+ const sharedView =
73
+ previousComponent && previousComponent === component(next.path);
74
+ const animate =
75
+ changed &&
76
+ visible.loaded &&
77
+ viewTransition !== false &&
78
+ !sharedView &&
79
+ previousURL.pathname !== nextURL.pathname &&
80
+ typeof document !== "undefined" &&
81
+ typeof document.startViewTransition === "function" &&
82
+ !matchMedia("(prefers-reduced-motion: reduce)").matches;
83
+
84
+ if (animate) {
85
+ const transition = document.startViewTransition(commit);
86
+ void transition.ready.catch(() => {});
87
+
88
+ return;
89
+ }
90
+
91
+ commit();
92
+ });
93
+
94
+ router.getSnapshot = () => visible;
95
+ router.subscribe = (listener) => {
96
+ listeners.add(listener);
97
+
98
+ return () => listeners.delete(listener);
99
+ };
100
+
101
+ const dispose = router.dispose;
102
+ router.dispose = () => {
103
+ unsubscribe();
104
+ pending.delete(router);
105
+ listeners.clear();
106
+ dispose();
107
+ };
108
+ }
@@ -0,0 +1,44 @@
1
+ export type Params = Readonly<Record<string, string>>;
2
+
3
+ export interface LoaderFunctionArgs {
4
+ params: Params;
5
+ signal: AbortSignal;
6
+ }
7
+
8
+ export interface ActionFunctionArgs<Input = unknown> {
9
+ input: Input;
10
+ params: Params;
11
+ signal: AbortSignal;
12
+ }
13
+
14
+ export interface DataResult<Data> {
15
+ data: Data | null;
16
+ error: string;
17
+ unexpected?: true;
18
+ }
19
+
20
+ export interface ResourceModule {
21
+ action?: (args: ActionFunctionArgs) => Promise<DataResult<unknown>>;
22
+ loader?: (args: LoaderFunctionArgs) => Promise<DataResult<unknown>>;
23
+ shouldRevalidate?: (args: ShouldRevalidateFunctionArgs) => boolean;
24
+ }
25
+
26
+ export interface ShouldRevalidateFunctionArgs {
27
+ actionResult: DataResult<unknown>;
28
+ defaultShouldRevalidate: boolean;
29
+ params: Params;
30
+ }
31
+
32
+ export type NavigationState = "idle" | "loading" | "submitting";
33
+
34
+ export interface RouteState {
35
+ actionData: unknown;
36
+ actionError: string;
37
+ actionState: "idle" | "submitting";
38
+ loaderData: unknown;
39
+ loaderError: string;
40
+ loaderInitialized: boolean;
41
+ loaderState: "idle" | "loading";
42
+ navigationState: NavigationState;
43
+ unexpectedError: boolean;
44
+ }