@kasperrt/smolter 0.2.0 → 0.3.1

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 (270) hide show
  1. package/.generated/client/deployment.ts +161 -0
  2. package/{src/internal → .generated}/client/index.ts +3 -0
  3. package/.generated/core/environment.ts +43 -0
  4. package/{src/internal → .generated}/core/errors.ts +1 -1
  5. package/{src/internal → .generated}/core/index.ts +1 -0
  6. package/.generated/core/navigation-router.ts +107 -0
  7. package/.generated/core/navigation.ts +147 -0
  8. package/.generated/core/presentation.ts +108 -0
  9. package/.generated/core/resource-types.ts +44 -0
  10. package/.generated/core/resources.ts +444 -0
  11. package/.generated/core/responses.ts +86 -0
  12. package/{src/internal → .generated}/core/router.ts +136 -63
  13. package/{src/internal → .generated}/core/serialize.ts +7 -2
  14. package/.generated/core/store.ts +25 -0
  15. package/.generated/core/submission.ts +116 -0
  16. package/{src/internal → .generated}/core/types.ts +18 -1
  17. package/.generated/core/url.ts +29 -0
  18. package/.generated/core/utils.ts +5 -0
  19. package/.generated/preact/await.tsx +41 -0
  20. package/.generated/preact/client.tsx +93 -0
  21. package/.generated/preact/context.ts +59 -0
  22. package/.generated/preact/document.tsx +198 -0
  23. package/{src/internal → .generated}/preact/fetcher.ts +43 -23
  24. package/.generated/preact/form.ts +58 -0
  25. package/{src/internal → .generated}/preact/hooks.ts +25 -28
  26. package/.generated/preact/index.ts +34 -0
  27. package/.generated/preact/lazy-route.ts +100 -0
  28. package/.generated/preact/navigation-state.ts +9 -0
  29. package/.generated/preact/navigation.tsx +147 -0
  30. package/.generated/preact/root-context.ts +21 -0
  31. package/.generated/preact/root-data.ts +45 -0
  32. package/.generated/preact/route-hooks.tsx +170 -0
  33. package/.generated/preact/route-types.ts +76 -0
  34. package/.generated/preact/router.tsx +107 -0
  35. package/{src/internal → .generated}/preact/routes.ts +17 -9
  36. package/.generated/react/context.ts +59 -0
  37. package/.generated/react/data.ts +25 -0
  38. package/{src/internal → .generated}/react/fetcher.ts +13 -11
  39. package/.generated/react/form.ts +58 -0
  40. package/{src/internal → .generated}/react/hooks.ts +9 -14
  41. package/{src/internal/preact → .generated/react}/index.ts +5 -1
  42. package/{src/internal → .generated}/react/link.ts +13 -21
  43. package/.generated/react/resources/components.tsx +404 -0
  44. package/{src/internal → .generated}/react/routes.ts +17 -9
  45. package/.generated/server/application.tsx +185 -0
  46. package/.generated/server/assets.ts +19 -0
  47. package/.generated/server/handler.ts +69 -0
  48. package/.generated/server/http.ts +55 -0
  49. package/{src/internal → .generated}/server/index.ts +3 -3
  50. package/.generated/server/node.ts +4 -0
  51. package/.generated/server/preact.ts +7 -0
  52. package/.generated/vite/application/browser-boundary.ts +42 -0
  53. package/.generated/vite/application/lazy-files.ts +138 -0
  54. package/.generated/vite/application/vite.ts +144 -0
  55. package/.generated/vite/deployment.ts +46 -0
  56. package/{src/internal → .generated}/vite/index.ts +11 -4
  57. package/{src/preact-vite.ts → .generated/vite/preact.ts} +14 -3
  58. package/{src/react-vite.ts → .generated/vite/react.ts} +8 -3
  59. package/CHANGELOG.md +63 -0
  60. package/README.md +277 -35
  61. package/dist/client/deployment.d.ts +12 -0
  62. package/dist/client/deployment.js +102 -0
  63. package/dist/client/deserialize.js +10 -0
  64. package/dist/{internal/client → client}/index.d.ts +2 -0
  65. package/dist/client/index.js +47 -0
  66. package/dist/core/environment.d.ts +12 -0
  67. package/dist/core/environment.js +23 -0
  68. package/dist/core/errors.js +7 -0
  69. package/dist/{internal/core → core}/index.d.ts +1 -0
  70. package/dist/core/match.js +33 -0
  71. package/dist/core/navigation-router.d.ts +15 -0
  72. package/dist/core/navigation-router.js +64 -0
  73. package/dist/core/navigation.d.ts +15 -0
  74. package/dist/core/navigation.js +92 -0
  75. package/dist/core/presentation.d.ts +5 -0
  76. package/dist/core/presentation.js +69 -0
  77. package/dist/core/resource-types.d.ts +37 -0
  78. package/dist/core/resources.d.ts +19 -0
  79. package/dist/core/resources.js +345 -0
  80. package/dist/core/responses.d.ts +15 -0
  81. package/dist/core/responses.js +48 -0
  82. package/dist/{internal/core → core}/router.d.ts +1 -1
  83. package/dist/core/router.js +204 -0
  84. package/dist/{internal/core → core}/serialize.d.ts +2 -0
  85. package/dist/core/serialize.js +17 -0
  86. package/dist/core/store.d.ts +6 -0
  87. package/dist/core/store.js +21 -0
  88. package/dist/core/submission.d.ts +30 -0
  89. package/dist/core/submission.js +41 -0
  90. package/dist/{internal/core → core}/types.d.ts +16 -1
  91. package/dist/core/url.d.ts +7 -0
  92. package/dist/core/url.js +12 -0
  93. package/dist/core/utils.d.ts +5 -0
  94. package/dist/core/utils.js +5 -0
  95. package/dist/files/index.js +64 -0
  96. package/dist/preact/await.d.ts +7 -0
  97. package/dist/preact/await.js +19 -0
  98. package/dist/preact/client.d.ts +7 -0
  99. package/dist/preact/client.js +68 -0
  100. package/dist/preact/context.d.ts +18 -0
  101. package/dist/preact/context.js +23 -0
  102. package/dist/preact/document.d.ts +15 -0
  103. package/dist/preact/document.js +155 -0
  104. package/dist/{internal/preact → preact}/fetcher.d.ts +9 -5
  105. package/dist/preact/fetcher.js +45 -0
  106. package/dist/{internal/preact → preact}/form.d.ts +5 -6
  107. package/dist/preact/form.js +21 -0
  108. package/dist/{internal/preact → preact}/hooks.d.ts +7 -6
  109. package/dist/preact/hooks.js +25 -0
  110. package/dist/preact/index.d.ts +18 -0
  111. package/dist/preact/index.js +15 -0
  112. package/dist/preact/lazy-route.d.ts +19 -0
  113. package/dist/preact/lazy-route.js +51 -0
  114. package/dist/preact/navigation-state.d.ts +2 -0
  115. package/dist/preact/navigation-state.js +9 -0
  116. package/dist/preact/navigation.d.ts +28 -0
  117. package/dist/preact/navigation.js +60 -0
  118. package/dist/preact/root-context.d.ts +13 -0
  119. package/dist/preact/root-context.js +9 -0
  120. package/dist/preact/root-data.d.ts +8 -0
  121. package/dist/preact/root-data.js +28 -0
  122. package/dist/preact/route-hooks.d.ts +32 -0
  123. package/dist/preact/route-hooks.js +94 -0
  124. package/dist/preact/route-types.d.ts +54 -0
  125. package/dist/preact/router.d.ts +16 -0
  126. package/dist/preact/router.js +63 -0
  127. package/dist/{internal/preact → preact}/routes.d.ts +1 -1
  128. package/dist/preact/routes.js +12 -0
  129. package/dist/{internal/preact → preact}/runtime.js +1 -1
  130. package/dist/react/context.d.ts +18 -0
  131. package/dist/react/context.js +23 -0
  132. package/dist/react/data.d.ts +5 -0
  133. package/dist/react/data.js +3 -0
  134. package/dist/{internal/react → react}/fetcher.d.ts +2 -2
  135. package/dist/react/fetcher.js +34 -0
  136. package/dist/{internal/react → react}/form.d.ts +5 -6
  137. package/dist/react/form.js +21 -0
  138. package/dist/react/hooks.js +25 -0
  139. package/dist/{internal/react → react}/index.d.ts +1 -1
  140. package/dist/react/index.js +10 -0
  141. package/dist/{internal/react → react}/link.d.ts +2 -2
  142. package/dist/react/link.js +27 -0
  143. package/dist/react/resources/components.d.ts +64 -0
  144. package/dist/react/resources/components.js +239 -0
  145. package/dist/{internal/react → react}/routes.d.ts +1 -1
  146. package/dist/react/routes.js +12 -0
  147. package/dist/react/runtime.js +1 -0
  148. package/dist/server/application.d.ts +4 -0
  149. package/dist/server/application.js +119 -0
  150. package/dist/server/assets.d.ts +5 -0
  151. package/dist/server/assets.js +7 -0
  152. package/dist/server/handler.d.ts +19 -0
  153. package/dist/server/handler.js +22 -0
  154. package/dist/server/http.d.ts +4 -0
  155. package/dist/server/http.js +27 -0
  156. package/dist/server/index.js +76 -0
  157. package/dist/server/node.d.ts +3 -0
  158. package/dist/server/node.js +3 -0
  159. package/dist/server/preact.d.ts +3 -0
  160. package/dist/server/preact.js +4 -0
  161. package/dist/vite/application/browser-boundary.d.ts +3 -0
  162. package/dist/vite/application/browser-boundary.js +35 -0
  163. package/dist/vite/application/lazy-files.d.ts +2 -0
  164. package/dist/vite/application/lazy-files.js +88 -0
  165. package/dist/vite/application/vite.d.ts +9 -0
  166. package/dist/vite/application/vite.js +101 -0
  167. package/dist/vite/deployment.d.ts +8 -0
  168. package/dist/vite/deployment.js +36 -0
  169. package/dist/vite/index.js +109 -0
  170. package/dist/{preact-vite.d.ts → vite/preact.d.ts} +6 -3
  171. package/dist/vite/preact.js +16 -0
  172. package/dist/{react-vite.d.ts → vite/react.d.ts} +4 -3
  173. package/dist/vite/react.js +15 -0
  174. package/dist/vite/transport.js +35 -0
  175. package/jsr.json +28 -16
  176. package/package.json +115 -35
  177. package/dist/client.d.ts +0 -2
  178. package/dist/client.js +0 -2
  179. package/dist/core.d.ts +0 -2
  180. package/dist/core.js +0 -2
  181. package/dist/files.d.ts +0 -2
  182. package/dist/files.js +0 -2
  183. package/dist/internal/client/deserialize.js +0 -11
  184. package/dist/internal/client/index.js +0 -44
  185. package/dist/internal/core/errors.js +0 -6
  186. package/dist/internal/core/index.js +0 -7
  187. package/dist/internal/core/match.js +0 -27
  188. package/dist/internal/core/router.js +0 -189
  189. package/dist/internal/core/serialize.js +0 -13
  190. package/dist/internal/core/types.js +0 -5
  191. package/dist/internal/core/url.d.ts +0 -2
  192. package/dist/internal/core/url.js +0 -6
  193. package/dist/internal/files/index.js +0 -77
  194. package/dist/internal/preact/context.d.ts +0 -18
  195. package/dist/internal/preact/context.js +0 -20
  196. package/dist/internal/preact/fetcher.js +0 -28
  197. package/dist/internal/preact/form.js +0 -41
  198. package/dist/internal/preact/hooks.js +0 -35
  199. package/dist/internal/preact/index.d.ts +0 -9
  200. package/dist/internal/preact/index.js +0 -7
  201. package/dist/internal/preact/link.d.ts +0 -12
  202. package/dist/internal/preact/link.js +0 -28
  203. package/dist/internal/preact/routes.js +0 -22
  204. package/dist/internal/react/context.d.ts +0 -18
  205. package/dist/internal/react/context.js +0 -20
  206. package/dist/internal/react/fetcher.js +0 -28
  207. package/dist/internal/react/form.js +0 -41
  208. package/dist/internal/react/hooks.js +0 -35
  209. package/dist/internal/react/index.js +0 -7
  210. package/dist/internal/react/link.js +0 -28
  211. package/dist/internal/react/routes.js +0 -22
  212. package/dist/internal/react/runtime.js +0 -2
  213. package/dist/internal/server/index.js +0 -69
  214. package/dist/internal/vite/index.js +0 -132
  215. package/dist/internal/vite/transport.js +0 -42
  216. package/dist/preact-client.d.ts +0 -2
  217. package/dist/preact-client.js +0 -2
  218. package/dist/preact-server.d.ts +0 -2
  219. package/dist/preact-server.js +0 -2
  220. package/dist/preact-vite.js +0 -9
  221. package/dist/preact.d.ts +0 -2
  222. package/dist/preact.js +0 -2
  223. package/dist/react-client.d.ts +0 -2
  224. package/dist/react-client.js +0 -2
  225. package/dist/react-server.d.ts +0 -2
  226. package/dist/react-server.js +0 -2
  227. package/dist/react-vite.js +0 -9
  228. package/dist/react.d.ts +0 -2
  229. package/dist/react.js +0 -2
  230. package/dist/server.d.ts +0 -2
  231. package/dist/server.js +0 -2
  232. package/dist/utils.d.ts +0 -5
  233. package/dist/utils.js +0 -5
  234. package/dist/vite.d.ts +0 -2
  235. package/dist/vite.js +0 -2
  236. package/src/client.ts +0 -2
  237. package/src/core.ts +0 -2
  238. package/src/files.ts +0 -2
  239. package/src/internal/core/url.ts +0 -6
  240. package/src/internal/preact/context.ts +0 -46
  241. package/src/internal/preact/form.ts +0 -79
  242. package/src/internal/preact/link.ts +0 -46
  243. package/src/internal/react/context.ts +0 -46
  244. package/src/internal/react/form.ts +0 -79
  245. package/src/internal/react/index.ts +0 -13
  246. package/src/preact-client.ts +0 -2
  247. package/src/preact-server.ts +0 -2
  248. package/src/preact.ts +0 -2
  249. package/src/react-client.ts +0 -2
  250. package/src/react-server.ts +0 -2
  251. package/src/react.ts +0 -2
  252. package/src/server.ts +0 -2
  253. package/src/utils.ts +0 -5
  254. package/src/vite.ts +0 -2
  255. /package/{src/internal → .generated}/client/deserialize.ts +0 -0
  256. /package/{src/internal → .generated}/core/match.ts +0 -0
  257. /package/{src/internal → .generated}/files/index.ts +0 -0
  258. /package/{src/internal → .generated}/preact/runtime.ts +0 -0
  259. /package/{src/internal → .generated}/react/runtime.ts +0 -0
  260. /package/{src/internal → .generated}/vite/transport.ts +0 -0
  261. /package/dist/{internal/client → client}/deserialize.d.ts +0 -0
  262. /package/dist/{internal/core → core}/errors.d.ts +0 -0
  263. /package/dist/{internal/core → core}/match.d.ts +0 -0
  264. /package/dist/{internal/files → files}/index.d.ts +0 -0
  265. /package/dist/{internal/preact → preact}/runtime.d.ts +0 -0
  266. /package/dist/{internal/react → react}/hooks.d.ts +0 -0
  267. /package/dist/{internal/react → react}/runtime.d.ts +0 -0
  268. /package/dist/{internal/server → server}/index.d.ts +0 -0
  269. /package/dist/{internal/vite → vite}/index.d.ts +0 -0
  270. /package/dist/{internal/vite → vite}/transport.d.ts +0 -0
@@ -0,0 +1,30 @@
1
+ import type { Fetcher, NavigateOptions, Router } from "./types.ts";
2
+ /** Values accepted by programmatic route submissions. */
3
+ export type Submission = FormData | HTMLFormElement | Record<string, unknown>;
4
+ /** Destination and encoding for a programmatic route submission. */
5
+ export interface SubmissionOptions {
6
+ action?: string;
7
+ encType?: string;
8
+ method?: string;
9
+ replace?: boolean;
10
+ preventScrollReset?: boolean;
11
+ }
12
+ export declare function formData(value: Submission): FormData;
13
+ export declare function submissionBody(value: Submission, encoding?: string): FormData;
14
+ /** Encode successful form controls for a GET route request. */
15
+ export declare function submissionSearch(body: FormData): string;
16
+ /** Read the JSON payload of a programmatic submission. */
17
+ export declare function readSubmission(request: Request): Promise<unknown>;
18
+ /** Browser form behavior shared by the React and Preact adapters. */
19
+ export interface RouteFormOptions extends NavigateOptions {
20
+ action?: string;
21
+ method?: "get" | "post" | "put" | "patch" | "delete";
22
+ reloadDocument?: boolean;
23
+ }
24
+ export interface FormSubmissionEvent {
25
+ currentTarget: HTMLFormElement;
26
+ defaultPrevented: boolean;
27
+ preventDefault: () => void;
28
+ }
29
+ /** Submit an internal form while leaving external actions to the browser. */
30
+ export declare function submitForm(router: Router, event: FormSubmissionEvent, submitter: HTMLElement | null, options: RouteFormOptions, fetcher?: Fetcher): void;
@@ -0,0 +1,41 @@
1
+ import { internalURL } from "./url.js";
2
+ //#region .generated/core/submission.ts
3
+ function formData(value) {
4
+ if (value instanceof FormData) return value;
5
+ if (typeof HTMLFormElement !== "undefined" && value instanceof HTMLFormElement) return new FormData(value);
6
+ const body = new FormData();
7
+ for (const [key, field] of Object.entries(value)) body.append(key, String(field));
8
+ return body;
9
+ }
10
+ function submissionBody(value, encoding) {
11
+ return formData(encoding === "application/json" ? { _payload: JSON.stringify(value) } : value);
12
+ }
13
+ /** Encode successful form controls for a GET route request. */
14
+ function submissionSearch(body) {
15
+ const search = new URLSearchParams();
16
+ for (const [key, value] of body) search.append(key, typeof value === "string" ? value : value.name);
17
+ return String(search);
18
+ }
19
+ /** Read the JSON payload of a programmatic submission. */
20
+ async function readSubmission(request) {
21
+ if (request.headers.get("Content-Type")?.includes("application/json")) return request.json();
22
+ const body = await request.formData();
23
+ return JSON.parse(String(body.get("_payload") ?? "{}"));
24
+ }
25
+ /** Submit an internal form while leaving external actions to the browser. */
26
+ function submitForm(router, event, submitter, options, fetcher) {
27
+ if (event.defaultPrevented || options.reloadDocument) return;
28
+ const target = internalURL(router.getSnapshot().url, submitter?.getAttribute("formaction") ?? options.action);
29
+ if (!target) return;
30
+ const data = new FormData(event.currentTarget, submitter);
31
+ const method = (submitter?.getAttribute("formmethod") ?? options.method)?.toUpperCase();
32
+ event.preventDefault();
33
+ if (method === "GET") {
34
+ target.search = submissionSearch(data);
35
+ fetcher ? fetcher.load(target.href) : router.navigate(target.href, options);
36
+ return;
37
+ }
38
+ (fetcher ?? router).submit(target.href, data, { method });
39
+ }
40
+ //#endregion
41
+ export { formData, readSubmission, submissionBody, submissionSearch, submitForm };
@@ -41,6 +41,8 @@ export interface RevalidationArgs extends RevalidationContext {
41
41
  }
42
42
  /** A route component and its optional server and client data handlers. */
43
43
  export interface RouteModule {
44
+ /** Stable identifier for targeted revalidation; defaults to the path. */
45
+ id?: string;
44
46
  default?: unknown;
45
47
  HydrateFallback?: unknown;
46
48
  ErrorBoundary?: unknown;
@@ -80,7 +82,11 @@ export interface RouterOptions {
80
82
  }
81
83
  /** Options for replacing rather than appending a browser history entry. */
82
84
  export interface NavigateOptions {
85
+ state?: unknown;
86
+ viewTransition?: boolean;
83
87
  replace?: boolean;
88
+ /** Preserve scroll in integrations that manage scroll restoration. */
89
+ preventScrollReset?: boolean;
84
90
  }
85
91
  /** HTTP method for a route action submission; defaults to POST. */
86
92
  export interface SubmitOptions {
@@ -104,6 +110,13 @@ export interface Fetcher {
104
110
  /** Abort work and release subscriptions when this imperative fetcher is no longer used. */
105
111
  dispose: () => void;
106
112
  }
113
+ /** Refresh active route data without navigation, optionally selecting route IDs or paths. */
114
+ export interface RevalidateOptions extends RevalidationContext {
115
+ route?: string | readonly string[];
116
+ reason?: RevalidationArgs["reason"];
117
+ }
118
+ /** A registered layout loader's revalidation callback. */
119
+ export type Revalidator = (args: RevalidationArgs) => void | Promise<void>;
107
120
  /** Route navigation, subscriptions, fetchers, and loader revalidation. */
108
121
  export interface Router {
109
122
  routes: readonly Route[];
@@ -111,7 +124,9 @@ export interface Router {
111
124
  subscribe: (listener: () => void) => () => void;
112
125
  start: () => () => void;
113
126
  navigate: (url: string, options?: NavigateOptions) => Promise<void>;
114
- revalidate: (reason?: RevalidationArgs["reason"], context?: RevalidationContext) => Promise<void>;
127
+ revalidate: (options?: RevalidateOptions | RevalidationArgs["reason"], context?: RevalidationContext) => Promise<void>;
128
+ /** Register layout data under a route ID. The returned function unregisters it. */
129
+ registerRevalidator: (id: string, revalidate: Revalidator) => () => void;
115
130
  submit: (url: string, body: FormData, options?: SubmitOptions) => Promise<Result>;
116
131
  /** Create an independent fetcher. Framework useFetcher hooks manage disposal automatically. */
117
132
  createFetcher: () => Fetcher;
@@ -0,0 +1,7 @@
1
+ /** Resolve an internal URL; external links remain the browser's responsibility. */
2
+ export declare function internalURL(base: string, href?: string): URL | undefined;
3
+ /** Browser and framework event fields used by navigation interception. */
4
+ type NavigationClickEvent = Pick<MouseEvent, "defaultPrevented" | "button" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">;
5
+ /** Whether an anchor click should use router navigation. */
6
+ export declare function navigationClick(event: NavigationClickEvent, anchor: HTMLAnchorElement): boolean;
7
+ export {};
@@ -0,0 +1,12 @@
1
+ //#region .generated/core/url.ts
2
+ /** Resolve an internal URL; external links remain the browser's responsibility. */
3
+ function internalURL(base, href = base) {
4
+ const url = new URL(href, base);
5
+ if (url.origin === new URL(base).origin) return url;
6
+ }
7
+ /** Whether an anchor click should use router navigation. */
8
+ function navigationClick(event, anchor) {
9
+ return !(event.defaultPrevented || event.button || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || anchor.hasAttribute("download") || anchor.target && anchor.target !== "_self");
10
+ }
11
+ //#endregion
12
+ export { internalURL, navigationClick };
@@ -0,0 +1,5 @@
1
+ /** Framework-independent, browser-safe utilities. @module */
2
+ export * from "./errors.ts";
3
+ export * from "./match.ts";
4
+ export * from "./serialize.ts";
5
+ export * from "./url.ts";
@@ -0,0 +1,5 @@
1
+ import { safeWrapAsync } from "./errors.js";
2
+ import { defineRoute, matchRoutes } from "./match.js";
3
+ import { internalURL, navigationClick } from "./url.js";
4
+ import { serialize, serializeData } from "./serialize.js";
5
+ export { defineRoute, internalURL, matchRoutes, navigationClick, safeWrapAsync, serialize, serializeData };
@@ -0,0 +1,64 @@
1
+ import { safeWrapAsync } from "../core/errors.js";
2
+ import { readdir } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ //#region .generated/files/index.ts
5
+ /**
6
+ * Build-time discovery of flat file routes.
7
+ * @module
8
+ */
9
+ var routeFile = /^route\.[jt]sx?$/;
10
+ var flatFile = /\.[jt]sx?$/;
11
+ /** Read only flat files and immediate route folders. Colocated helpers are ignored. */
12
+ async function flatRoutes(directory) {
13
+ const result = await safeWrapAsync(async () => {
14
+ const entries = await readdir(directory, { withFileTypes: true });
15
+ const routes = [];
16
+ const patterns = /* @__PURE__ */ new Set();
17
+ for (const entry of entries) {
18
+ let name = entry.name;
19
+ let file = resolve(directory, name);
20
+ if (entry.isDirectory()) {
21
+ const candidates = (await readdir(file)).filter((name) => routeFile.test(name));
22
+ if (candidates.length > 1) return /* @__PURE__ */ new Error(`Multiple route files in ${file}`);
23
+ if (!candidates.length) continue;
24
+ file = resolve(file, candidates[0]);
25
+ } else {
26
+ if (!entry.isFile() || !flatFile.test(name) || name.endsWith(".d.ts")) continue;
27
+ name = name.replace(flatFile, "");
28
+ }
29
+ if (/[()[\]:*?]/.test(name)) return /* @__PURE__ */ new Error(`Unsupported flat route name: ${name}`);
30
+ const segments = name.split(".");
31
+ const parts = [];
32
+ for (const [index, segment] of segments.entries()) {
33
+ if (!segment) return /* @__PURE__ */ new Error(`Empty route segment: ${name}`);
34
+ if (segment === "_index" && index !== segments.length - 1) return /* @__PURE__ */ new Error(`_index must be last: ${name}`);
35
+ if (segment === "$" && index !== segments.length - 1) return /* @__PURE__ */ new Error(`Splat must be last: ${name}`);
36
+ if (segment.startsWith("_")) continue;
37
+ const part = segment.replace(/_$/, "");
38
+ parts.push(part === "$" ? "*" : part.replace(/^\$/, ":"));
39
+ }
40
+ if (segments.at(-1)?.startsWith("_") && segments.at(-1) !== "_index") continue;
41
+ const path = `/${parts.join("/")}`;
42
+ const pattern = path.replace(/:[^/]+/g, ":");
43
+ if (patterns.has(pattern)) return /* @__PURE__ */ new Error(`Conflicting flat route: ${path}`);
44
+ patterns.add(pattern);
45
+ routes.push({
46
+ path,
47
+ file
48
+ });
49
+ }
50
+ return routes.sort((a, b) => {
51
+ const left = a.path.split("/");
52
+ const right = b.path.split("/");
53
+ const rank = (part) => part === "*" ? 0 : part?.startsWith(":") ? 1 : part === void 0 ? 2 : 3;
54
+ for (let index = 0; index < Math.max(left.length, right.length); index++) {
55
+ const difference = rank(right[index]) - rank(left[index]);
56
+ if (difference) return difference;
57
+ }
58
+ return a.path.localeCompare(b.path);
59
+ });
60
+ });
61
+ return result.error ?? result.data;
62
+ }
63
+ //#endregion
64
+ export { flatRoutes };
@@ -0,0 +1,7 @@
1
+ import type { ComponentChildren } from "preact";
2
+ export interface AwaitProps<T> {
3
+ resolve: T | Promise<T>;
4
+ children: (value: Awaited<T>) => ComponentChildren;
5
+ errorElement?: ComponentChildren;
6
+ }
7
+ export declare function Await<T>({ resolve, children, errorElement, }: AwaitProps<T>): ComponentChildren;
@@ -0,0 +1,19 @@
1
+ import { use } from "preact/compat";
2
+ //#region .generated/preact/await.tsx
3
+ var resolutions = /* @__PURE__ */ new WeakMap();
4
+ function Await({ resolve, children, errorElement }) {
5
+ if (!(resolve instanceof Promise)) return children(resolve);
6
+ let pending = resolutions.get(resolve);
7
+ if (!pending) {
8
+ pending = resolve.then((value) => ({ value }), (error) => ({ error }));
9
+ resolutions.set(resolve, pending);
10
+ }
11
+ const result = use(pending);
12
+ if ("error" in result) {
13
+ if (errorElement !== void 0) return errorElement;
14
+ throw result.error;
15
+ }
16
+ return children(result.value);
17
+ }
18
+ //#endregion
19
+ export { Await };
@@ -0,0 +1,7 @@
1
+ /** Browser mounting and hydration for Preact applications. @module */
2
+ /** @jsxImportSource preact */
3
+ import { type Router } from "../core/index.ts";
4
+ import type { AppRoute, RootModule } from "./route-types.ts";
5
+ export * from "../client/index.ts";
6
+ export { configureApplication } from "../core/environment.ts";
7
+ export declare function mountApplication(routes: AppRoute[], rootModule?: RootModule, hash?: boolean): Promise<Router>;
@@ -0,0 +1,68 @@
1
+ import { matchRoutes } from "../core/match.js";
2
+ import { createDataRouter } from "../core/router.js";
3
+ import { configureApplication } from "../core/environment.js";
4
+ import { retainRoute } from "../core/presentation.js";
5
+ import { connectNavigation } from "../core/navigation.js";
6
+ import { attachRootData } from "./root-data.js";
7
+ import { RouterApplication, prepareRoutes } from "./router.js";
8
+ import { deserialize } from "../client/deserialize.js";
9
+ import { watchDeployment } from "../client/deployment.js";
10
+ import { serverRoute } from "../client/index.js";
11
+ import { flushSync } from "preact/compat";
12
+ import { jsx } from "preact/jsx-runtime";
13
+ import { hydrate, render } from "preact";
14
+ //#region .generated/preact/client.tsx
15
+ /** Browser mounting and hydration for Preact applications. @module */
16
+ /** @jsxImportSource preact */
17
+ async function mountApplication(routes, rootModule, hash = false) {
18
+ const initialURL = new URL(hash ? location.hash.slice(1) || "/" : location.href, location.origin);
19
+ await (matchRoutes(routes, initialURL.pathname)?.route)?.preload?.();
20
+ routes = prepareRoutes(routes);
21
+ const element = document.getElementById("smolter-data");
22
+ const hydration = element?.textContent ? JSON.parse(element.textContent) : void 0;
23
+ if (hydration?.snapshot.error) hydration.snapshot.error = Object.assign(new Error(hydration.snapshot.error.message), hydration.snapshot.error);
24
+ const router = createDataRouter(routes, {
25
+ browser: !hash,
26
+ url: hash ? new URL(location.hash.slice(1) || "/", location.origin).href : location.href,
27
+ ...hydration && { hydrationData: hydration.snapshot }
28
+ });
29
+ retainRoute(router, flushSync);
30
+ if (hash) connectHashHistory(router);
31
+ connectNavigation(router);
32
+ const rootStore = attachRootData(router, hydration?.root, rootModule);
33
+ const props = {
34
+ routes,
35
+ router,
36
+ root: hydration?.root,
37
+ rootModule,
38
+ rootStore
39
+ };
40
+ if (hydration) {
41
+ hydrate(/* @__PURE__ */ jsx(RouterApplication, {
42
+ ...props,
43
+ document: true
44
+ }), document);
45
+ return router;
46
+ }
47
+ const target = document.getElementById("root");
48
+ if (target) render(/* @__PURE__ */ jsx(RouterApplication, { ...props }), target);
49
+ return router;
50
+ }
51
+ function connectHashHistory(router) {
52
+ const navigate = router.navigate;
53
+ router.navigate = async (url, options) => {
54
+ const target = new URL(url, router.getSnapshot().url);
55
+ const hash = `#${target.pathname}${target.search}`;
56
+ history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
57
+ await navigate(url, options);
58
+ };
59
+ const onHashChange = () => void navigate(location.hash.slice(1) || "/");
60
+ window.addEventListener("hashchange", onHashChange);
61
+ const dispose = router.dispose;
62
+ router.dispose = () => {
63
+ window.removeEventListener("hashchange", onHashChange);
64
+ dispose();
65
+ };
66
+ }
67
+ //#endregion
68
+ export { configureApplication, deserialize, mountApplication, serverRoute, watchDeployment };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Preact components and typed data hooks. Import from @kasperrt/smolter/preact.
3
+ * @module
4
+ */
5
+ import type { NavigationRouter, Snapshot } from "../core/index.ts";
6
+ import { type ReactNode } from "./runtime.ts";
7
+ /** Router instance and children rendered within its context. */
8
+ export interface RouterProviderProps {
9
+ router: NavigationRouter;
10
+ children?: ReactNode;
11
+ }
12
+ /** Provide a router to descendant hooks and connect its browser lifecycle. */
13
+ export declare function RouterProvider({ router, children, }: RouterProviderProps): ReactNode;
14
+ /** Hooks require a RouterProvider. */
15
+ export declare function useRouter<T extends NavigationRouter = NavigationRouter>(): T;
16
+ export declare function useSnapshot(): Snapshot;
17
+ export declare function useSnapshot<K extends keyof Snapshot>(key: K): Snapshot[K];
18
+ export declare function useSnapshot<K extends keyof Snapshot>(key: K, path: string | undefined): Snapshot[K] | undefined;
@@ -0,0 +1,23 @@
1
+ import { createContext, createElement, useContext, useEffect, useSyncExternalStore } from "./runtime.js";
2
+ //#region .generated/preact/context.ts
3
+ var RouterContext = createContext(void 0);
4
+ /** Provide a router to descendant hooks and connect its browser lifecycle. */
5
+ function RouterProvider({ router, children }) {
6
+ useEffect(router.start, [router]);
7
+ return createElement(RouterContext, { value: router }, children);
8
+ }
9
+ /** Hooks require a RouterProvider. */
10
+ function useRouter() {
11
+ return useContext(RouterContext);
12
+ }
13
+ function useSnapshot(key, path) {
14
+ const router = useRouter();
15
+ const getSnapshot = () => {
16
+ const snapshot = router.getSnapshot();
17
+ if (path !== void 0 && path !== snapshot.path) return void 0;
18
+ return key ? snapshot[key] : snapshot;
19
+ };
20
+ return useSyncExternalStore(router.subscribe, getSnapshot);
21
+ }
22
+ //#endregion
23
+ export { RouterProvider, useRouter, useSnapshot };
@@ -0,0 +1,15 @@
1
+ /** @jsxImportSource preact */
2
+ import type { ComponentProps } from "preact/compat";
3
+ export declare function Meta(): import("preact").JSX.Element;
4
+ export declare function Links(): import("preact").JSX.Element;
5
+ export interface ScriptProps {
6
+ nonce?: string;
7
+ }
8
+ export declare function Scripts({ nonce }: ScriptProps): import("preact").JSX.Element;
9
+ export declare function ScrollRestoration(_props: ScriptProps): null;
10
+ /** HTML attributes for server markup and client document reconciliation. */
11
+ export interface DocumentProps extends ComponentProps<"html"> {
12
+ "data-debug"?: boolean;
13
+ }
14
+ /** Render the server document and preserve client-managed theme classes on hydration. */
15
+ export declare function Document(props: DocumentProps): import("preact").JSX.Element;
@@ -0,0 +1,155 @@
1
+ import { serializeData } from "../core/serialize.js";
2
+ import { useSnapshot } from "./context.js";
3
+ import { applicationEnvironment } from "../core/environment.js";
4
+ import { scrollPositions } from "../core/navigation.js";
5
+ import { useNavigationSnapshot } from "./navigation-state.js";
6
+ import { useApplication } from "./root-context.js";
7
+ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
8
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
9
+ import { Fragment as Fragment$1, createElement } from "preact";
10
+ //#region .generated/preact/document.tsx
11
+ /** @jsxImportSource preact */
12
+ function renderMeta(item, index) {
13
+ if (typeof item.title === "string") return /* @__PURE__ */ jsx("title", { children: item.title }, index);
14
+ if ("script:ld+json" in item) return /* @__PURE__ */ jsx("script", {
15
+ type: "application/ld+json",
16
+ dangerouslySetInnerHTML: { __html: serializeData(item["script:ld+json"]) }
17
+ }, index);
18
+ const { tagName, ...props } = item;
19
+ return createElement(tagName === "link" ? "link" : "meta", {
20
+ ...props,
21
+ key: index
22
+ });
23
+ }
24
+ function Meta() {
25
+ const { routes, rootModule } = useApplication();
26
+ const snapshot = useSnapshot();
27
+ const route = routes.find((item) => item.path === snapshot.path);
28
+ const args = {
29
+ loaderData: snapshot.data,
30
+ data: snapshot.data,
31
+ params: snapshot.params,
32
+ error: snapshot.error,
33
+ location: new URL(snapshot.url)
34
+ };
35
+ const values = route?.meta?.(args) ?? rootModule?.meta?.(args) ?? [];
36
+ useEffect(() => {
37
+ const title = values.find((item) => typeof item.title === "string")?.title;
38
+ if (typeof title === "string") document.title = title;
39
+ }, [values]);
40
+ return /* @__PURE__ */ jsx(Fragment$1, { children: values.map(renderMeta) });
41
+ }
42
+ function Links() {
43
+ const { rootModule } = useApplication();
44
+ return /* @__PURE__ */ jsx(Fragment$1, { children: rootModule?.links?.().map((props, index) => /* @__PURE__ */ jsx("link", { ...props }, index)) });
45
+ }
46
+ function Scripts({ nonce }) {
47
+ const { root, router, entry } = useApplication();
48
+ const [json] = useState(() => {
49
+ const current = router.getSnapshot();
50
+ const snapshot = {
51
+ ...current,
52
+ error: current.error && {
53
+ ...current.error,
54
+ name: current.error.name,
55
+ message: current.error.message
56
+ }
57
+ };
58
+ return serializeData({
59
+ root,
60
+ snapshot
61
+ });
62
+ });
63
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
64
+ /* @__PURE__ */ jsx("script", {
65
+ ...nonce && { nonce },
66
+ dangerouslySetInnerHTML: { __html: applicationEnvironment?.entryScript ?? "" }
67
+ }),
68
+ /* @__PURE__ */ jsx("script", {
69
+ id: "smolter-data",
70
+ type: "application/json",
71
+ ...nonce && { nonce },
72
+ dangerouslySetInnerHTML: { __html: json }
73
+ }),
74
+ entry && /* @__PURE__ */ jsx("script", {
75
+ type: "module",
76
+ src: entry,
77
+ ...nonce && { nonce }
78
+ })
79
+ ] });
80
+ }
81
+ function ScrollRestoration(_props) {
82
+ const { router } = useApplication();
83
+ const { url, state } = useSnapshot();
84
+ const navigation = useNavigationSnapshot(router);
85
+ const positions = scrollPositions(router);
86
+ const restoredLocation = useRef("");
87
+ useEffect(() => {
88
+ const previous = history.scrollRestoration;
89
+ history.scrollRestoration = "manual";
90
+ return () => {
91
+ history.scrollRestoration = previous;
92
+ };
93
+ }, []);
94
+ useLayoutEffect(() => {
95
+ if (state !== "idle") return;
96
+ if ((history.state?.key ?? "initial") !== navigation.key) return;
97
+ const locationKey = `${navigation.key}:${url}`;
98
+ if (restoredLocation.current === locationKey) return;
99
+ const frame = requestAnimationFrame(() => {
100
+ restoredLocation.current = locationKey;
101
+ const target = new URL(location.href);
102
+ const saved = navigation.type === "POP" && positions.get(navigation.key);
103
+ if (!saved && target.hash) {
104
+ document.getElementById(target.hash.slice(1))?.scrollIntoView();
105
+ return;
106
+ }
107
+ if (!saved && navigation.preventScrollReset) return;
108
+ const [left, top] = saved || [0, 0];
109
+ window.scrollTo({
110
+ left,
111
+ top,
112
+ behavior: "instant"
113
+ });
114
+ });
115
+ return () => cancelAnimationFrame(frame);
116
+ }, [
117
+ url,
118
+ state,
119
+ positions,
120
+ navigation.key,
121
+ navigation.type,
122
+ navigation.preventScrollReset
123
+ ]);
124
+ return null;
125
+ }
126
+ /** Render the server document and preserve client-managed theme classes on hydration. */
127
+ function Document(props) {
128
+ const { children, ...attributes } = props;
129
+ const className = String(props.className ?? "");
130
+ const previousClassName = useRef(className);
131
+ const lang = String(props.lang ?? "en");
132
+ const debug = String(props["data-debug"] ?? false);
133
+ useEffect(() => {
134
+ if (previousClassName.current !== className) {
135
+ const previous = previousClassName.current.split(/\s+/).filter(Boolean);
136
+ const next = className.split(/\s+/).filter(Boolean);
137
+ document.documentElement.classList.remove(...previous);
138
+ document.documentElement.classList.add(...next);
139
+ previousClassName.current = className;
140
+ }
141
+ document.documentElement.lang = lang;
142
+ document.documentElement.dataset.debug = debug;
143
+ }, [
144
+ className,
145
+ lang,
146
+ debug
147
+ ]);
148
+ if (typeof document === "undefined") return /* @__PURE__ */ jsx("html", {
149
+ ...attributes,
150
+ children
151
+ });
152
+ return /* @__PURE__ */ jsx(Fragment, { children });
153
+ }
154
+ //#endregion
155
+ export { Document, Links, Meta, Scripts, ScrollRestoration };
@@ -2,11 +2,13 @@
2
2
  * Preact components and typed data hooks. Import from @kasperrt/smolter/preact.
3
3
  * @module
4
4
  */
5
- import type { AnyDataFunction, DataFunction, DataOf, FetcherSnapshot, Result, Route as RouteDefinition, RouteModule, SubmitOptions } from "../core/index.ts";
5
+ import { type AnyDataFunction, type DataFunction, type DataOf, type FetcherSnapshot, type Result, type Route as RouteDefinition, type RouteModule } from "../core/index.ts";
6
+ import { type Submission, type SubmissionOptions } from "../core/submission.ts";
6
7
  import { type FormProps } from "./form.ts";
7
- import type { ComponentType } from "./runtime.ts";
8
+ import type { RouteData } from "./route-types.ts";
9
+ import { type ComponentType } from "./runtime.ts";
8
10
  /** Action method and optional explicit route URL for a fetcher submission. */
9
- export interface FetcherSubmitOptions extends SubmitOptions {
11
+ export interface FetcherSubmitOptions extends SubmissionOptions {
10
12
  action?: string;
11
13
  }
12
14
  /** Typed independent request state, methods, and a bound Form component. */
@@ -14,9 +16,11 @@ export interface TypedFetcher<T> extends Omit<FetcherSnapshot, "data"> {
14
16
  data?: T;
15
17
  Form: ComponentType<FormProps>;
16
18
  load: (url?: string) => Promise<Result>;
17
- submit: (body: FormData, options?: FetcherSubmitOptions) => Promise<Result>;
19
+ submit: (body: Submission, options?: FetcherSubmitOptions) => Promise<Result>;
18
20
  }
19
21
  /** Create a fetcher for independent route reads and mutations; its data stays local. */
20
22
  export declare function useFetcher<T extends RouteModule>(route: RouteDefinition<T>): TypedFetcher<DataOf<Extract<T["action"] | T["clientAction"], AnyDataFunction>> | DataOf<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>>;
21
23
  /** Create a fetcher for independent route reads and mutations; its data stays local. */
22
- export declare function useFetcher<T extends AnyDataFunction = DataFunction>(): TypedFetcher<DataOf<T>>;
24
+ export declare function useFetcher<T = DataFunction>(): TypedFetcher<RouteData<T>>;
25
+ /** A fetcher with a bound form and inferred result data. */
26
+ export type FetcherWithComponents<T> = TypedFetcher<RouteData<T>>;
@@ -0,0 +1,45 @@
1
+ import { submissionBody, submissionSearch } from "../core/submission.js";
2
+ import { createElement, useEffect, useMemo, useSyncExternalStore } from "./runtime.js";
3
+ import { useRouter } from "./context.js";
4
+ import { Form } from "./form.js";
5
+ //#region .generated/preact/fetcher.ts
6
+ /** Create a fetcher for independent route reads and mutations; its data stays local. */
7
+ function useFetcher(route) {
8
+ const router = useRouter();
9
+ const path = route?.path;
10
+ const fetcher = useMemo(() => {
11
+ const store = router.createFetcher();
12
+ const target = () => {
13
+ const snapshot = router.getSnapshot();
14
+ return path === void 0 || path === snapshot.path ? snapshot.url : path;
15
+ };
16
+ return {
17
+ ...store,
18
+ Form: (props) => createElement(Form, {
19
+ action: target(),
20
+ ...props,
21
+ fetcher: store
22
+ }),
23
+ load: (url) => store.load(url ?? target()),
24
+ submit: (body, options = {}) => {
25
+ const action = options.action ?? target();
26
+ const data = submissionBody(body, options.encType);
27
+ const method = options.method?.toUpperCase();
28
+ if (method === "GET") {
29
+ const url = new URL(action, router.getSnapshot().url);
30
+ url.search = submissionSearch(data);
31
+ return store.load(url.href);
32
+ }
33
+ return store.submit(action, data, { method });
34
+ }
35
+ };
36
+ }, [router, path]);
37
+ useEffect(() => fetcher.dispose, [fetcher]);
38
+ const state = useSyncExternalStore(fetcher.subscribe, fetcher.getSnapshot);
39
+ return {
40
+ ...fetcher,
41
+ ...state
42
+ };
43
+ }
44
+ //#endregion
45
+ export { useFetcher };
@@ -2,16 +2,15 @@
2
2
  * Preact components and typed data hooks. Import from @kasperrt/smolter/preact.
3
3
  * @module
4
4
  */
5
- import type { Fetcher } from "../core/index.ts";
6
- import type { FormHTMLAttributes, ReactNode } from "./runtime.ts";
5
+ import { type Fetcher } from "../core/index.ts";
6
+ import { type RouteFormOptions } from "../core/submission.ts";
7
+ import { type FormHTMLAttributes, type ReactNode } from "./runtime.ts";
7
8
  /** Form attributes with GET loader requests or POST, PUT, PATCH, and DELETE actions. */
8
- export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "method"> {
9
- action?: string;
10
- method?: "get" | "post" | "put" | "patch" | "delete";
9
+ export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "method">, RouteFormOptions {
11
10
  }
12
11
  interface InternalFormProps extends FormProps {
13
12
  fetcher?: Fetcher;
14
13
  }
15
14
  /** Submit form data to a registered route without a document reload. */
16
- export declare function Form({ action, method, fetcher, onSubmit, ...props }: InternalFormProps): ReactNode;
15
+ export declare function Form(options: InternalFormProps): ReactNode;
17
16
  export {};