@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
@@ -0,0 +1,15 @@
1
+ import type { Router } from "./types.ts";
2
+ export type NavigationType = "POP" | "PUSH" | "REPLACE";
3
+ export interface NavigationSnapshot {
4
+ type: NavigationType;
5
+ state: unknown;
6
+ key: string;
7
+ preventScrollReset: boolean;
8
+ }
9
+ export declare function scrollPositions(router: Router): Map<string, [number, number]>;
10
+ export declare function navigationStore(router: Router): {
11
+ getSnapshot: () => NavigationSnapshot;
12
+ subscribe(listener: () => void): () => void;
13
+ update(next: NavigationSnapshot): void;
14
+ };
15
+ export declare function connectNavigation(router: Router): void;
@@ -0,0 +1,92 @@
1
+ import { navigationClick } from "./url.js";
2
+ import { handoffDocument, leaveDocument, resolveApplicationURL } from "./environment.js";
3
+ import { createStore } from "./store.js";
4
+ //#region .generated/core/navigation.ts
5
+ var scrollHistory = /* @__PURE__ */ new WeakMap();
6
+ function scrollPositions(router) {
7
+ let positions = scrollHistory.get(router);
8
+ if (!positions) {
9
+ positions = /* @__PURE__ */ new Map();
10
+ scrollHistory.set(router, positions);
11
+ }
12
+ return positions;
13
+ }
14
+ var stores = /* @__PURE__ */ new WeakMap();
15
+ function createNavigationStore() {
16
+ return createStore({
17
+ type: "POP",
18
+ state: typeof history === "undefined" ? null : history.state?.state,
19
+ key: typeof history === "undefined" ? "initial" : history.state?.key ?? "initial",
20
+ preventScrollReset: false
21
+ });
22
+ }
23
+ function navigationStore(router) {
24
+ let store = stores.get(router);
25
+ if (!store) {
26
+ store = createNavigationStore();
27
+ stores.set(router, store);
28
+ }
29
+ return store;
30
+ }
31
+ function connectNavigation(router) {
32
+ const store = navigationStore(router);
33
+ const navigate = router.navigate;
34
+ let sequence = 0;
35
+ let pending;
36
+ const committed = () => {
37
+ if (!pending || router.getSnapshot().state !== "idle") return;
38
+ const next = pending;
39
+ pending = void 0;
40
+ if (next.type !== "POP") history.replaceState(next, "", location.href);
41
+ store.update(next);
42
+ };
43
+ const unsubscribe = router.subscribe(committed);
44
+ const rememberScroll = () => {
45
+ scrollPositions(router).set(store.getSnapshot().key, [window.scrollX, window.scrollY]);
46
+ };
47
+ router.navigate = async (href, options = {}) => {
48
+ const destination = resolveApplicationURL(new URL(href, router.getSnapshot().url));
49
+ if (destination.origin !== location.origin) {
50
+ leaveDocument(destination);
51
+ return;
52
+ }
53
+ rememberScroll();
54
+ const current = ++sequence;
55
+ pending = {
56
+ type: options.replace ? "REPLACE" : "PUSH",
57
+ state: options.state ?? null,
58
+ key: `${Date.now().toString(36)}-${current}`,
59
+ preventScrollReset: options.preventScrollReset ?? false
60
+ };
61
+ await navigate(href, options);
62
+ committed();
63
+ };
64
+ const pop = () => {
65
+ rememberScroll();
66
+ pending = {
67
+ ...history.state,
68
+ type: "POP",
69
+ key: history.state?.key ?? "initial",
70
+ preventScrollReset: false
71
+ };
72
+ committed();
73
+ };
74
+ const handoff = (event) => {
75
+ const anchor = event.target instanceof Element ? event.target.closest("a[href]") : null;
76
+ if (!(anchor instanceof HTMLAnchorElement) || !navigationClick(event, anchor)) return;
77
+ const target = resolveApplicationURL(new URL(anchor.href));
78
+ handoffDocument(event, target);
79
+ };
80
+ if (typeof document !== "undefined") document.addEventListener("click", handoff);
81
+ window.addEventListener("popstate", pop);
82
+ const dispose = router.dispose;
83
+ router.dispose = () => {
84
+ pending = void 0;
85
+ unsubscribe();
86
+ window.removeEventListener("popstate", pop);
87
+ if (typeof document !== "undefined") document.removeEventListener("click", handoff);
88
+ dispose();
89
+ };
90
+ }
91
+ //#endregion
92
+ export { connectNavigation, navigationStore, scrollPositions };
@@ -0,0 +1,5 @@
1
+ import type { Router } from "./types.ts";
2
+ type CommitView = (update: () => void) => void;
3
+ export declare function pendingLocation(router: Router): URL | undefined;
4
+ export declare function retainRoute(router: Router, commitView: CommitView): void;
5
+ export {};
@@ -0,0 +1,69 @@
1
+ //#region .generated/core/presentation.ts
2
+ var pending = /* @__PURE__ */ new WeakMap();
3
+ function pendingLocation(router) {
4
+ const snapshot = pending.get(router)?.();
5
+ return snapshot?.state !== "idle" && snapshot?.url ? new URL(snapshot.url) : void 0;
6
+ }
7
+ function retainRoute(router, commitView) {
8
+ const read = router.getSnapshot;
9
+ pending.set(router, read);
10
+ let viewTransition;
11
+ const navigate = router.navigate;
12
+ router.navigate = (href, options = {}) => {
13
+ viewTransition = options.viewTransition;
14
+ return navigate(href, options);
15
+ };
16
+ let visible = read();
17
+ let revision = 0;
18
+ const listeners = /* @__PURE__ */ new Set();
19
+ const publish = () => {
20
+ for (const listener of listeners) listener();
21
+ };
22
+ const unsubscribe = router.subscribe(() => {
23
+ const next = read();
24
+ const current = ++revision;
25
+ if (next.state !== "idle" && visible.loaded) {
26
+ visible = {
27
+ ...visible,
28
+ state: next.state
29
+ };
30
+ publish();
31
+ return;
32
+ }
33
+ const commit = () => {
34
+ if (current !== revision) return;
35
+ commitView(() => {
36
+ visible = next;
37
+ publish();
38
+ });
39
+ };
40
+ const previousURL = new URL(visible.url);
41
+ const nextURL = new URL(next.url);
42
+ const changed = previousURL.pathname + previousURL.search !== nextURL.pathname + nextURL.search;
43
+ const component = (path) => {
44
+ const route = router.routes.find((item) => item.path === path);
45
+ return route?.resolveComponent?.() ?? route?.default;
46
+ };
47
+ const previousComponent = component(visible.path);
48
+ const sharedView = previousComponent && previousComponent === component(next.path);
49
+ if (changed && visible.loaded && viewTransition !== false && !sharedView && previousURL.pathname !== nextURL.pathname && typeof document !== "undefined" && typeof document.startViewTransition === "function" && !matchMedia("(prefers-reduced-motion: reduce)").matches) {
50
+ document.startViewTransition(commit).ready.catch(() => {});
51
+ return;
52
+ }
53
+ commit();
54
+ });
55
+ router.getSnapshot = () => visible;
56
+ router.subscribe = (listener) => {
57
+ listeners.add(listener);
58
+ return () => listeners.delete(listener);
59
+ };
60
+ const dispose = router.dispose;
61
+ router.dispose = () => {
62
+ unsubscribe();
63
+ pending.delete(router);
64
+ listeners.clear();
65
+ dispose();
66
+ };
67
+ }
68
+ //#endregion
69
+ export { pendingLocation, retainRoute };
@@ -0,0 +1,37 @@
1
+ export type Params = Readonly<Record<string, string>>;
2
+ export interface LoaderFunctionArgs {
3
+ params: Params;
4
+ signal: AbortSignal;
5
+ }
6
+ export interface ActionFunctionArgs<Input = unknown> {
7
+ input: Input;
8
+ params: Params;
9
+ signal: AbortSignal;
10
+ }
11
+ export interface DataResult<Data> {
12
+ data: Data | null;
13
+ error: string;
14
+ unexpected?: true;
15
+ }
16
+ export interface ResourceModule {
17
+ action?: (args: ActionFunctionArgs) => Promise<DataResult<unknown>>;
18
+ loader?: (args: LoaderFunctionArgs) => Promise<DataResult<unknown>>;
19
+ shouldRevalidate?: (args: ShouldRevalidateFunctionArgs) => boolean;
20
+ }
21
+ export interface ShouldRevalidateFunctionArgs {
22
+ actionResult: DataResult<unknown>;
23
+ defaultShouldRevalidate: boolean;
24
+ params: Params;
25
+ }
26
+ export type NavigationState = "idle" | "loading" | "submitting";
27
+ export interface RouteState {
28
+ actionData: unknown;
29
+ actionError: string;
30
+ actionState: "idle" | "submitting";
31
+ loaderData: unknown;
32
+ loaderError: string;
33
+ loaderInitialized: boolean;
34
+ loaderState: "idle" | "loading";
35
+ navigationState: NavigationState;
36
+ unexpectedError: boolean;
37
+ }
@@ -0,0 +1,19 @@
1
+ import type { DataResult, Params, ResourceModule, RouteState } from "./resource-types.ts";
2
+ export declare class ResourceRouter {
3
+ #private;
4
+ constructor(routes: ReadonlyMap<string, ResourceModule>);
5
+ dispose(): void;
6
+ getRoute(routeId: string): ResourceModule | null;
7
+ getSnapshot(routeId: string, params: Params): RouteState;
8
+ hydrateRoute<Data>(routeId: string, data: Data, params?: Params): void;
9
+ persist(routeId: string, params?: Params): void;
10
+ disposeRoute(routeId: string, params?: Params): void;
11
+ load<Data>(routeId: string, params?: Params, force?: boolean): Promise<DataResult<Data>>;
12
+ subscribe(routeId: string, params: Params, listener: () => void): () => void;
13
+ submit<Data>(routeId: string, input: unknown, params?: Params): Promise<DataResult<Data>>;
14
+ fetchLoader<Data>(routeId: string, params: Params, signal: AbortSignal): Promise<DataResult<Data>>;
15
+ fetchAction<Data>(routeId: string, input: unknown, params: Params, signal: AbortSignal): Promise<DataResult<Data>>;
16
+ retry(routeId: string, params: Params): Promise<DataResult<unknown>>;
17
+ reportUnexpected(routeId: string, params: Params): void;
18
+ }
19
+ export declare function createResourceRouter(routes: ReadonlyMap<string, ResourceModule>): ResourceRouter;
@@ -0,0 +1,345 @@
1
+ import { safeWrapAsync } from "./errors.js";
2
+ //#region .generated/core/resources.ts
3
+ var IDLE_ROUTE_STATE = {
4
+ actionData: null,
5
+ actionError: "",
6
+ actionState: "idle",
7
+ loaderData: null,
8
+ loaderError: "",
9
+ loaderInitialized: false,
10
+ loaderState: "idle",
11
+ navigationState: "idle",
12
+ unexpectedError: false
13
+ };
14
+ var ResourceRouter = class {
15
+ #cleanupTimers = /* @__PURE__ */ new Map();
16
+ #controllers = /* @__PURE__ */ new Map();
17
+ #inFlightLoads = /* @__PURE__ */ new Map();
18
+ #listeners = /* @__PURE__ */ new Map();
19
+ #persistentRoutes = /* @__PURE__ */ new Set();
20
+ #routes;
21
+ #states = /* @__PURE__ */ new Map();
22
+ constructor(routes) {
23
+ this.#routes = routes;
24
+ }
25
+ dispose() {
26
+ for (const controller of this.#controllers.values()) controller.abort();
27
+ this.#controllers.clear();
28
+ for (const timer of this.#cleanupTimers.values()) clearTimeout(timer);
29
+ this.#cleanupTimers.clear();
30
+ this.#inFlightLoads.clear();
31
+ this.#listeners.clear();
32
+ this.#persistentRoutes.clear();
33
+ }
34
+ getRoute(routeId) {
35
+ return this.#routes.get(routeId) ?? null;
36
+ }
37
+ getSnapshot(routeId, params) {
38
+ return this.#states.get(createRouteKey(routeId, params)) ?? IDLE_ROUTE_STATE;
39
+ }
40
+ hydrateRoute(routeId, data, params = {}) {
41
+ const routeKey = createRouteKey(routeId, params);
42
+ const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
43
+ this.#setState(routeKey, {
44
+ ...currentState,
45
+ loaderData: data,
46
+ loaderError: "",
47
+ loaderInitialized: true,
48
+ loaderState: "idle",
49
+ navigationState: currentState.actionState === "submitting" ? "submitting" : "idle",
50
+ unexpectedError: false
51
+ });
52
+ }
53
+ persist(routeId, params = {}) {
54
+ const routeKey = createRouteKey(routeId, params);
55
+ this.#persistentRoutes.add(routeKey);
56
+ const cleanupTimer = this.#cleanupTimers.get(routeKey);
57
+ if (!cleanupTimer) return;
58
+ clearTimeout(cleanupTimer);
59
+ this.#cleanupTimers.delete(routeKey);
60
+ }
61
+ disposeRoute(routeId, params = {}) {
62
+ this.#disposeRouteKey(createRouteKey(routeId, params));
63
+ }
64
+ #disposeRouteKey(routeKey) {
65
+ this.#persistentRoutes.delete(routeKey);
66
+ const cleanupTimer = this.#cleanupTimers.get(routeKey);
67
+ if (cleanupTimer) clearTimeout(cleanupTimer);
68
+ this.#cleanupTimers.delete(routeKey);
69
+ this.#controllers.get(routeKey)?.abort();
70
+ this.#controllers.get(`${routeKey}:action`)?.abort();
71
+ this.#controllers.delete(routeKey);
72
+ this.#controllers.delete(`${routeKey}:action`);
73
+ this.#inFlightLoads.delete(routeKey);
74
+ this.#states.delete(routeKey);
75
+ this.#notify(routeKey);
76
+ }
77
+ load(routeId, params = {}, force = false) {
78
+ const routeKey = createRouteKey(routeId, params);
79
+ const route = this.#routes.get(routeId);
80
+ if (!route?.loader) return Promise.resolve({
81
+ data: null,
82
+ error: "Route loader not found."
83
+ });
84
+ const currentState = this.#states.get(routeKey);
85
+ if (!force && currentState?.loaderInitialized) return Promise.resolve({
86
+ data: currentState?.loaderData,
87
+ error: currentState?.loaderError ?? ""
88
+ });
89
+ const currentLoad = this.#inFlightLoads.get(routeKey);
90
+ if (force && currentLoad) {
91
+ this.#controllers.get(routeKey)?.abort();
92
+ this.#inFlightLoads.delete(routeKey);
93
+ }
94
+ if (!force && currentLoad) return currentLoad;
95
+ const controller = this.#replaceController(routeKey);
96
+ this.#setState(routeKey, {
97
+ ...currentState ?? IDLE_ROUTE_STATE,
98
+ loaderError: "",
99
+ loaderState: "loading",
100
+ navigationState: currentState?.actionState === "submitting" ? "submitting" : "loading",
101
+ unexpectedError: false
102
+ });
103
+ const load = this.#runLoader(route, routeKey, params, controller);
104
+ this.#inFlightLoads.set(routeKey, load);
105
+ return load;
106
+ }
107
+ subscribe(routeId, params, listener) {
108
+ const routeKey = createRouteKey(routeId, params);
109
+ const cleanupTimer = this.#cleanupTimers.get(routeKey);
110
+ if (cleanupTimer) {
111
+ clearTimeout(cleanupTimer);
112
+ this.#cleanupTimers.delete(routeKey);
113
+ }
114
+ const listeners = this.#listeners.get(routeKey) ?? /* @__PURE__ */ new Set();
115
+ listeners.add(listener);
116
+ this.#listeners.set(routeKey, listeners);
117
+ return () => {
118
+ if (!listeners.delete(listener) || listeners.size > 0) return;
119
+ this.#listeners.delete(routeKey);
120
+ if (this.#persistentRoutes.has(routeKey)) return;
121
+ const timer = setTimeout(() => this.#disposeRouteKey(routeKey), routeCleanupGraceMs);
122
+ this.#cleanupTimers.set(routeKey, timer);
123
+ };
124
+ }
125
+ async submit(routeId, input, params = {}) {
126
+ const routeKey = createRouteKey(routeId, params);
127
+ const route = this.#routes.get(routeId);
128
+ if (!route?.action) return {
129
+ data: null,
130
+ error: "Route action not found."
131
+ };
132
+ const controllerKey = `${routeKey}:action`;
133
+ const controller = this.#replaceController(controllerKey);
134
+ const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
135
+ this.#setState(routeKey, {
136
+ ...currentState,
137
+ actionError: "",
138
+ actionState: "submitting",
139
+ navigationState: "submitting",
140
+ unexpectedError: false
141
+ });
142
+ const { error, data: result } = await safeWrapAsync(() => route.action?.({
143
+ input,
144
+ params,
145
+ signal: controller.signal
146
+ }));
147
+ if (this.#controllers.get(controllerKey) === controller) this.#controllers.delete(controllerKey);
148
+ if (controller.signal.aborted) return {
149
+ data: null,
150
+ error: ""
151
+ };
152
+ const latestState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
153
+ if (error || !result) {
154
+ const publicError = "The request could not be completed.";
155
+ this.#setState(routeKey, {
156
+ ...latestState,
157
+ actionData: null,
158
+ actionError: publicError,
159
+ actionState: "idle",
160
+ navigationState: latestState.loaderState === "loading" ? "loading" : "idle",
161
+ unexpectedError: true
162
+ });
163
+ return {
164
+ data: null,
165
+ error: publicError,
166
+ unexpected: true
167
+ };
168
+ }
169
+ this.#setState(routeKey, {
170
+ ...latestState,
171
+ actionData: result.data,
172
+ actionError: result.error,
173
+ actionState: "idle",
174
+ navigationState: latestState.loaderState === "loading" ? "loading" : "idle",
175
+ unexpectedError: false
176
+ });
177
+ const actionResult = {
178
+ data: result.data,
179
+ error: result.error
180
+ };
181
+ const { error: revalidationError, data: shouldRevalidate } = await safeWrapAsync(() => route.shouldRevalidate?.({
182
+ actionResult: result,
183
+ defaultShouldRevalidate: Boolean(route.loader),
184
+ params
185
+ }) ?? Boolean(route.loader));
186
+ if (revalidationError) {
187
+ const publicError = "The request could not be completed.";
188
+ this.#setState(routeKey, {
189
+ ...this.#states.get(routeKey) ?? IDLE_ROUTE_STATE,
190
+ actionError: publicError,
191
+ actionState: "idle",
192
+ unexpectedError: true
193
+ });
194
+ return {
195
+ data: null,
196
+ error: publicError,
197
+ unexpected: true
198
+ };
199
+ }
200
+ if (shouldRevalidate) await this.load(routeId, params, true);
201
+ return actionResult;
202
+ }
203
+ async fetchLoader(routeId, params, signal) {
204
+ const route = this.#routes.get(routeId);
205
+ if (!route?.loader) return {
206
+ data: null,
207
+ error: "Route loader not found."
208
+ };
209
+ const { error, data: result } = await safeWrapAsync(() => route.loader?.({
210
+ params,
211
+ signal
212
+ }));
213
+ if (signal.aborted) return {
214
+ data: null,
215
+ error: ""
216
+ };
217
+ if (error || !result) return {
218
+ data: null,
219
+ error: "The route could not be loaded.",
220
+ unexpected: true
221
+ };
222
+ return result;
223
+ }
224
+ async fetchAction(routeId, input, params, signal) {
225
+ const route = this.#routes.get(routeId);
226
+ if (!route?.action) return {
227
+ data: null,
228
+ error: "Route action not found."
229
+ };
230
+ const { error, data: result } = await safeWrapAsync(() => route.action?.({
231
+ input,
232
+ params,
233
+ signal
234
+ }));
235
+ if (signal.aborted) return {
236
+ data: null,
237
+ error: ""
238
+ };
239
+ if (error || !result) return {
240
+ data: null,
241
+ error: "The request could not be completed.",
242
+ unexpected: true
243
+ };
244
+ const { error: revalidationError, data: shouldRevalidate } = await safeWrapAsync(() => route.shouldRevalidate?.({
245
+ actionResult: result,
246
+ defaultShouldRevalidate: Boolean(route.loader),
247
+ params
248
+ }) ?? Boolean(route.loader));
249
+ if (signal.aborted) return {
250
+ data: null,
251
+ error: ""
252
+ };
253
+ if (revalidationError) return {
254
+ data: null,
255
+ error: "The request could not be completed.",
256
+ unexpected: true
257
+ };
258
+ if (shouldRevalidate) await this.load(routeId, params, true);
259
+ return result;
260
+ }
261
+ retry(routeId, params) {
262
+ const routeKey = createRouteKey(routeId, params);
263
+ this.#states.set(routeKey, IDLE_ROUTE_STATE);
264
+ this.#notify(routeKey);
265
+ return this.load(routeId, params, true);
266
+ }
267
+ reportUnexpected(routeId, params) {
268
+ const routeKey = createRouteKey(routeId, params);
269
+ const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
270
+ this.#setState(routeKey, {
271
+ ...currentState,
272
+ unexpectedError: true
273
+ });
274
+ }
275
+ async #runLoader(route, routeKey, params, controller) {
276
+ const { error, data: result } = await safeWrapAsync(() => route.loader?.({
277
+ params,
278
+ signal: controller.signal
279
+ }) ?? Promise.resolve({
280
+ data: null,
281
+ error: ""
282
+ }));
283
+ if (this.#controllers.get(routeKey) === controller) {
284
+ this.#controllers.delete(routeKey);
285
+ this.#inFlightLoads.delete(routeKey);
286
+ }
287
+ if (controller.signal.aborted) return {
288
+ data: null,
289
+ error: ""
290
+ };
291
+ if (error || !result) {
292
+ const publicError = "The route could not be loaded.";
293
+ this.#setState(routeKey, {
294
+ actionData: null,
295
+ actionError: "",
296
+ actionState: "idle",
297
+ loaderData: null,
298
+ loaderError: publicError,
299
+ loaderInitialized: true,
300
+ loaderState: "idle",
301
+ navigationState: "idle",
302
+ unexpectedError: true
303
+ });
304
+ return {
305
+ data: null,
306
+ error: publicError,
307
+ unexpected: true
308
+ };
309
+ }
310
+ const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
311
+ this.#setState(routeKey, {
312
+ ...currentState,
313
+ loaderData: result.data,
314
+ loaderError: result.error,
315
+ loaderInitialized: true,
316
+ loaderState: "idle",
317
+ navigationState: currentState.actionState === "submitting" ? "submitting" : "idle",
318
+ unexpectedError: false
319
+ });
320
+ return result;
321
+ }
322
+ #replaceController(key) {
323
+ this.#controllers.get(key)?.abort();
324
+ const controller = new AbortController();
325
+ this.#controllers.set(key, controller);
326
+ return controller;
327
+ }
328
+ #setState(routeKey, state) {
329
+ this.#states.set(routeKey, state);
330
+ this.#notify(routeKey);
331
+ }
332
+ #notify(routeKey) {
333
+ for (const listener of this.#listeners.get(routeKey) ?? []) listener();
334
+ }
335
+ };
336
+ function createResourceRouter(routes) {
337
+ return new ResourceRouter(routes);
338
+ }
339
+ function createRouteKey(routeId, params) {
340
+ const normalizedParams = Object.entries(params).sort(([left], [right]) => left.localeCompare(right));
341
+ return `${routeId}:${JSON.stringify(normalizedParams)}`;
342
+ }
343
+ var routeCleanupGraceMs = 100;
344
+ //#endregion
345
+ export { ResourceRouter, createResourceRouter };
@@ -0,0 +1,15 @@
1
+ export interface RouteError extends Error {
2
+ status: number;
3
+ statusText: string;
4
+ data?: unknown;
5
+ }
6
+ export interface DataResponse<T> {
7
+ value: T;
8
+ init: ResponseInit;
9
+ __routeData: true;
10
+ }
11
+ export declare function data<T>(value: T, init?: ResponseInit): DataResponse<T>;
12
+ export declare function redirect(location: string, init?: number | ResponseInit): Response;
13
+ export declare function isRouteErrorResponse(value: unknown): value is RouteError;
14
+ export declare function normalizeResult(value: unknown): Promise<unknown>;
15
+ export declare function resolveData(value: unknown): Promise<unknown>;
@@ -0,0 +1,48 @@
1
+ //#region .generated/core/responses.ts
2
+ function data(value, init = {}) {
3
+ return {
4
+ value,
5
+ init,
6
+ __routeData: true
7
+ };
8
+ }
9
+ function redirect(location, init = 302) {
10
+ const options = typeof init === "number" ? { status: init } : init;
11
+ const headers = new Headers(options.headers);
12
+ headers.set("Location", location);
13
+ return new Response(null, {
14
+ ...options,
15
+ status: options.status ?? 302,
16
+ headers
17
+ });
18
+ }
19
+ function isRouteErrorResponse(value) {
20
+ return value instanceof Error && "status" in value;
21
+ }
22
+ async function normalizeResult(value) {
23
+ if (value && typeof value === "object" && "__routeError" in value) {
24
+ const error = value.__routeError;
25
+ return Object.assign(Error(error.message), error);
26
+ }
27
+ if (value instanceof Response) {
28
+ if (value.headers.has("Location")) return value;
29
+ if (!value.ok) return Object.assign(Error(await value.text()), {
30
+ status: value.status,
31
+ statusText: value.statusText
32
+ });
33
+ return value.json();
34
+ }
35
+ if (value && typeof value === "object" && "__routeData" in value) return resolveData(value.value);
36
+ return resolveData(value);
37
+ }
38
+ async function resolveData(value) {
39
+ const resolved = await value;
40
+ if (Array.isArray(resolved)) return Promise.all(resolved.map(resolveData));
41
+ if (resolved && typeof resolved === "object" && Object.getPrototypeOf(resolved) === Object.prototype) {
42
+ const entries = await Promise.all(Object.entries(resolved).map(async ([key, field]) => [key, await resolveData(field)]));
43
+ return Object.fromEntries(entries);
44
+ }
45
+ return resolved;
46
+ }
47
+ //#endregion
48
+ export { data, isRouteErrorResponse, normalizeResult, redirect, resolveData };
@@ -1,3 +1,3 @@
1
1
  import type { Route, Router, RouterOptions } from "./types.ts";
2
2
  /** Create an isolated router. Use browser: true for browser history, or loadRouter for SSR. */
3
- export declare function createRouter(routes: readonly Route[], options?: RouterOptions): Router;
3
+ export declare function createDataRouter(routes: readonly Route[], options?: RouterOptions): Router;