@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
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # smolter _
2
2
 
3
3
  <!-- bundlejs:start -->
4
- [![react gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.2.0%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.2.0%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
5
- [![preact gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.2.0%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.2.0%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
4
+ [![react gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.0%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.0%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
5
+ [![preact gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.0%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.0%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
6
6
  <!-- bundlejs:end -->
7
7
 
8
8
  A typed data router for React and Preact. **Zero runtime dependencies.**
@@ -29,7 +29,10 @@ Start with [loaders](https://kasperrt.github.io/smolter/react/docs/loaders/), [a
29
29
  - [Types follow the handler](#types-follow-the-handler)
30
30
  - [Server rendering](#server-rendering)
31
31
  - [Package entries](#package-entries)
32
+ - [Application integration](#application-integration)
33
+ - [Resource subscriptions](#resource-subscriptions)
32
34
  - [Size and checks](#size-and-checks)
35
+ - [Routing without data hooks](#routing-without-data-hooks)
33
36
 
34
37
  ## Install
35
38
 
@@ -40,7 +43,7 @@ pnpm add @kasperrt/smolter
40
43
  ```
41
44
 
42
45
  ```ts
43
- import { createRouter, useFetcher } from "@kasperrt/smolter/react";
46
+ import { createDataRouter, useFetcher } from "@kasperrt/smolter/react";
44
47
  // Preact: import from "@kasperrt/smolter/preact".
45
48
  ```
46
49
 
@@ -61,7 +64,8 @@ pnpm dev
61
64
  Each app has its own entrypoints, routes, and data code. Only UI components are
62
65
  shared. Preact examples do not import React. Each includes:
63
66
 
64
- - `/`: counter, fetcher POST, automatic and manual revalidation.
67
+ - `/`: a short introduction and a link to the docs.
68
+ - `/examples/counter`: fetcher POST, automatic and manual revalidation.
65
69
  - `/tasks`: list, create form, and a fetcher-only task preview.
66
70
  - `/tasks/:id`: PATCH, DELETE, validation, and skipping no-op revalidation.
67
71
  - `/about`: the separation between data and rendering.
@@ -94,10 +98,10 @@ export default defineConfig({
94
98
 
95
99
  ```ts
96
100
  // app.ts
97
- import { createRouter } from "@kasperrt/smolter/react";
101
+ import { createDataRouter } from "@kasperrt/smolter/react";
98
102
  import routes from "virtual:smolter/routes";
99
103
 
100
- export const router = createRouter(routes, { browser: true });
104
+ export const router = createDataRouter(routes, { browser: true });
101
105
  ```
102
106
 
103
107
  | File or folder | URL |
@@ -136,14 +140,14 @@ Outside Vite, `flatRoutes(directory)` from `@kasperrt/smolter/react/vite` return
136
140
  ### Manual routes
137
141
 
138
142
  ```tsx
139
- import { createRouter, defineRoute } from "@kasperrt/smolter/react";
143
+ import { createDataRouter, defineRoute } from "@kasperrt/smolter/react";
140
144
  import * as tasks from "./tasks/route";
141
145
  import * as task from "./task/route";
142
146
 
143
147
  export const tasksRoute = defineRoute("/tasks", tasks);
144
148
  export const taskRoute = defineRoute("/tasks/:id", task);
145
149
  export const routes = [tasksRoute, taskRoute];
146
- export const router = createRouter(routes, { browser: true });
150
+ export const router = createDataRouter(routes, { browser: true });
147
151
  ```
148
152
 
149
153
  Or pass handlers directly to `defineRoute(path, { loader, action, default: View })`.
@@ -171,11 +175,12 @@ export { default } from "./view";
171
175
  export { loader } from "./loader";
172
176
  export { action } from "./action";
173
177
 
174
- export { clientLoader } from "./client-loader";
175
- export { clientAction } from "./client-action";
178
+ export { clientLoader } from "./clientLoader";
179
+ export { clientAction } from "./clientAction";
176
180
 
177
- export { shouldRevalidate } from "./should-revalidate";
178
- export { HydrateFallback, ErrorBoundary } from "../../boundaries";
181
+ export { shouldRevalidate } from "./shouldRevalidate";
182
+ export { HydrateFallback } from "./HydrateFallback";
183
+ export { ErrorBoundary } from "./ErrorBoundary";
179
184
  ```
180
185
 
181
186
  With the optional server integration:
@@ -187,7 +192,7 @@ With the optional server integration:
187
192
  - SSR executes the server loader and hydration reuses that data.
188
193
 
189
194
  ```ts
190
- // client-loader.ts
195
+ // clientLoader.ts
191
196
  import type { ClientLoaderArgs } from "@kasperrt/smolter/react/client";
192
197
  import type { loader } from "./loader";
193
198
 
@@ -197,7 +202,7 @@ export async function clientLoader({ serverLoader }: ClientLoaderArgs<typeof loa
197
202
  ```
198
203
 
199
204
  ```ts
200
- // client-action.ts
205
+ // clientAction.ts
201
206
  import type { ClientActionArgs } from "@kasperrt/smolter/react/client";
202
207
  import type { action } from "./action";
203
208
 
@@ -258,7 +263,7 @@ routerPlugin({
258
263
 
259
264
  Each table exports `routes`. The standalone examples contain both tables.
260
265
 
261
- Without the optional integration, `createRouter` invokes handlers locally:
266
+ Without the optional integration, `createDataRouter` invokes handlers locally:
262
267
  client handlers take precedence, with ordinary handlers as fallbacks.
263
268
  `loadRouter` selects server handlers only. Core-only usage does not create HTTP
264
269
  endpoints or compiler boundaries by itself.
@@ -319,7 +324,7 @@ Fetcher results belong to `fetcher.data`, not `useActionData()`.
319
324
  ## Decide when to revalidate
320
325
 
321
326
  ```ts
322
- // should-revalidate.ts
327
+ // shouldRevalidate.ts
323
328
  import type { RevalidationArgs } from "@kasperrt/smolter/react";
324
329
 
325
330
  export function shouldRevalidate({ actionResult, defaultShouldRevalidate }: RevalidationArgs) {
@@ -344,7 +349,25 @@ and explicit `fetcher.load()` calls always load their destination.
344
349
  This controls loader refreshes; it does not rerun actions or reload the document.
345
350
  Existing page data stays visible during manual and post-action refreshes.
346
351
 
347
- `const revalidate = useRevalidator(); await revalidate()` requests a manual refresh.
352
+ `useRevalidator()` returns an async function. Target a registered route ID or path
353
+ pattern to refresh only its active data, including loaded fetchers:
354
+
355
+ ```tsx
356
+ const revalidate = useRevalidator();
357
+
358
+ await revalidate({ route: "/tasks/:id" });
359
+ await revalidate({ route: "root" });
360
+ await revalidate({ route: ["root", "/tasks/:id"] });
361
+ ```
362
+
363
+ Without a target, `revalidate()` refreshes the active page, loaded fetchers, and
364
+ registered root/layout data. `shouldRevalidate` still applies. Inactive routes
365
+ are not loaded, unknown targets do nothing, and revalidation never navigates.
366
+
367
+ The Preact root integration uses `root` as its default ID. Configure its
368
+ `loaderPath` with a registered data route to enable root refreshes. Custom React
369
+ or Preact layouts can use `router.registerRevalidator(id, callback)`; call its
370
+ returned cleanup function when the layout is removed.
348
371
  The task-detail examples return `{ changed: false }` for an unchanged title;
349
372
  the corresponding loader is skipped.
350
373
 
@@ -381,17 +404,17 @@ const hydrationJSON = serialize(router.getSnapshot());
381
404
 
382
405
  Create a fresh router per request. Embed the serialized snapshot in a JSON
383
406
  script, restore it with `deserialize` from `@kasperrt/smolter/react/client`, and pass it as
384
- `hydrationData` to `createRouter`. This preserves loader errors through hydration
407
+ `hydrationData` to `createDataRouter`. This preserves loader errors through hydration
385
408
  without exposing server stacks or causes. Error boundaries should display the
386
409
  error message, not its stack.
387
410
 
388
411
  ```ts
389
412
  import { deserialize } from "@kasperrt/smolter/react/client";
390
- import { createRouter } from "@kasperrt/smolter/react";
413
+ import { createDataRouter } from "@kasperrt/smolter/react";
391
414
  import routes from "virtual:smolter/routes";
392
415
 
393
416
  const data = document.getElementById("smolter-data");
394
- const router = createRouter(routes, {
417
+ const router = createDataRouter(routes, {
395
418
  browser: true,
396
419
  hydrationData: data?.textContent ? deserialize(data.textContent) : undefined,
397
420
  });
@@ -406,7 +429,7 @@ The SSR examples use the same server route definitions for HTML and data request
406
429
 
407
430
  ## Package entries
408
431
 
409
- Use `@kasperrt/smolter/react` for React 18+ and `@kasperrt/smolter/preact` for Preact 10.25+.
432
+ Use `@kasperrt/smolter/react` for React 18+ and `@kasperrt/smolter/preact` for Preact 11.0.1+.
410
433
  Choose one renderer for your application. Both expose the same routing API:
411
434
 
412
435
  | Purpose | React | Preact |
@@ -421,39 +444,171 @@ The main renderer entries never import server or build modules. Named imports
421
444
  are tree-shaken; optional transport is included only when you import `/client`.
422
445
  Server entries do not import either UI renderer. Vite entries are build-time only.
423
446
 
424
- **Migrating to 0.2.0:** replace `/core` imports for types, `createRouter`, and
447
+ **Migrating renderer subpaths:** replace `/core` imports for types, router factories, and
425
448
  `defineRoute` with your renderer entry. Use `/utils` for `matchRoutes`,
426
449
  `internalURL` and `safeWrapAsync`. Move `/client`, `/server`,
427
450
  `/files`, and `/vite` imports to the corresponding renderer-prefixed path above.
428
451
  The bare package root and old top-level subpaths are no longer exported.
429
452
 
453
+ See the [0.3 migration guide](https://kasperrt.github.io/smolter/react/docs/migration-0.3/) and [changelog](./CHANGELOG.md).
454
+
455
+ **Router factory migration:** code that previously used `createRouter` with
456
+ loaders, actions, fetchers or revalidation must now use `createDataRouter`.
457
+ `createRouter` now creates a navigation-only router. SSR and file-router
458
+ application helpers select `createDataRouter` automatically.
459
+
430
460
  On JSR, install `@kasperrt/smolter` using the
431
461
  [JSR package instructions](https://jsr.io/docs/npm-compatibility) and import
432
- `@kasperrt/smolter/react` or `@kasperrt/smolter/preact`. Its `core`, `client`,
433
- `server`, and `files` subpaths match npm. The Vite plugin is npm-only:
462
+ `@kasperrt/smolter/react` or `@kasperrt/smolter/preact`. Its renderer, client, server and utility subpaths match npm. The Vite plugin is npm-only:
434
463
  use `@kasperrt/smolter/react/vite` from the npm package when adding file routing.
435
464
 
465
+ ## Application integration
466
+
467
+ Import routing, forms and data hooks from `@kasperrt/smolter/preact`.
468
+ Document rendering is optional: `@kasperrt/smolter/preact/document` supplies
469
+ `Document`, `Meta`, `Links`, `Scripts` and `ScrollRestoration`.
470
+ `@kasperrt/smolter/preact/client` supplies `mountApplication`,
471
+ `configureApplication` and the server transport helpers. Mounting connects
472
+ hydration, root data and navigation presentation to the same core router.
473
+ These modules remain tree-shakable; basic routing does not import the document
474
+ or mounting code. The size table also measures all three together, because
475
+ moving APIs to subpaths does not make their implementation disappear.
476
+
477
+ ```ts
478
+ // vite.config.ts
479
+ import { defineConfig } from "vite";
480
+ import { applicationPlugins } from "@kasperrt/smolter/preact/vite";
481
+
482
+ export default defineConfig({
483
+ plugins: applicationPlugins({ directory: "src/routes" }),
484
+ });
485
+ ```
486
+
487
+ ```tsx
488
+ // src/entry.client.tsx
489
+ import routes from "virtual:smolter/routes";
490
+ import { mountApplication } from "@kasperrt/smolter/preact/client";
491
+ import * as root from "./root";
492
+
493
+ mountApplication(routes, root);
494
+ ```
495
+
496
+ `root` exports a default component containing `Outlet` and a `Layout` wrapping
497
+ it in `Document`, a head, and a body containing `Scripts`. Its optional loader
498
+ supplies root data. `renderApplication(request, routes, root, entry)` from
499
+ `preact/server` renders the same tree and serializes hydration data;
500
+ `dispatchRoute(request, routes)` handles route data requests. Server rendering
501
+ requires the optional `preact-render-to-string` peer.
502
+
503
+ Deployment-specific URL mapping can be configured once at startup with
504
+ `configureApplication({ resolveURL, isDocumentHandoff, leaveDocument,
505
+ entryScript })`. Do not put user-specific or request-specific state there.
506
+ The library does not define application domains, ports, or branding.
507
+
508
+ `utils/http` contains the Node request/response adapter; `utils/assets` contains
509
+ manifest/preload helpers; `utils/responses` contains response normalization.
510
+ Keep the first two in server/build entrypoints. Browser application imports do
511
+ not include them.
512
+
513
+ ### Virtual modules and entrypoints
514
+
515
+ `virtual:smolter/routes` is the default route-array export generated by the Vite
516
+ plugin from `src/routes`. It is not a physical file or a runtime package.
517
+ The server build retains loaders/actions; the browser build strips their
518
+ re-exports and installs server proxies. `entry.client.tsx` and `entry.server.tsx`
519
+ are application-owned files wiring those manifests to hydration and SSR.
520
+ Manual tables can be imported directly instead.
521
+
522
+ See the [virtual routes guide](https://kasperrt.github.io/smolter/react/docs/virtual-routes)
523
+ for the exact contract, TypeScript declaration, and both entrypoint examples.
524
+
525
+ ### Express hosting
526
+
527
+ Keep Express in control of metrics, tracing, authentication and static files.
528
+ Mount `createRequestHandler({ build, entry })` from
529
+ `@kasperrt/smolter/utils/http` after those middleware. `build` is the compiled
530
+ server entry and `entry` is the browser entry URL from Vite's manifest. Do not
531
+ consume request bodies with Express body parsers before this handler.
532
+ The adapter has no Express dependency and forwards failures to `next(error)`.
533
+
534
+ See the [Express guide](https://kasperrt.github.io/smolter/react/docs/express)
535
+ for a complete server example and current buffering limitations.
536
+
537
+ ## Resource subscriptions
538
+
539
+ `@kasperrt/smolter/react/data` adds subscriptions to named resources when their
540
+ lifetime is independent of URL navigation. The state and cancellation logic
541
+ lives in core; the entry adds React bindings without DOM or HTTP globals.
542
+
543
+ ```ts
544
+ import { createResourceRouter } from "@kasperrt/smolter/react/data";
545
+
546
+ const resources = createResourceRouter(new Map([
547
+ ["settings", {
548
+ loader: async ({ signal }) => ({
549
+ data: await readSettings(signal),
550
+ error: "",
551
+ }),
552
+ action: async ({ input, signal }) => ({
553
+ data: await saveSettings(input, signal),
554
+ error: "",
555
+ }),
556
+ }],
557
+ ]));
558
+ ```
559
+
560
+ `ResourceProvider` owns the store lifetime. `Resource` selects a resource by
561
+ `routeId` and params. `useResourceData` reads it; `useRouteLoaderData` reads a
562
+ named resource. `useResourceFetcher` returns `[state, operations]`, with
563
+ independent `load` and `submit` operations. Successful mutations consult the
564
+ resource's `shouldRevalidate` before reloading its data. Last-subscriber cleanup
565
+ aborts pending work; `persistent` keeps a resource until explicitly disposed.
566
+ This optional entry does not contribute to the base React bundle measurement.
567
+
436
568
  ## Size and checks
437
569
 
438
- `pnpm size` measures every browser export with esbuild, Terser, and gzip level 9.
439
- Frameworks are excluded; library dependencies are included. It compares the same
440
- build settings against pinned wouter entries. Both adapters must be strictly
441
- smaller than their corresponding wouter entry; exceeding either fails the check. Optional server transport is measured separately and included in SSR
442
- example labels. File discovery is build-only.
570
+ Use `createRouter` for navigation only, or `createDataRouter` for loaders,
571
+ actions, fetchers and revalidation. Both are named exports from
572
+ `@kasperrt/smolter/preact` and `/react`. No extra package or plugin is required.
573
+ The production bundler can remove the data engine when only `createRouter` is used.
574
+
575
+ `pnpm size` regenerates this table and the site's size data using identical
576
+ minifiers. The routing-only gate compares our selected exports with wouter's
577
+ routing subset: `Router`, `Route`, `Switch`, `Link`, `useParams`, and `useLocation`.
578
+ Both complete wouter entries are reported too. The APIs differ; this is not
579
+ a claim of identical features.
443
580
 
444
581
  <!-- size-comparison:start -->
445
582
  | Browser exports | Gzip bytes |
446
583
  | --- | ---: |
447
- | `@kasperrt/smolter/react` | 2445 |
448
- | `@kasperrt/smolter/preact` | 2445 |
449
- | `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 2782 |
450
- | `@kasperrt/smolter/preact + @kasperrt/smolter/preact/client` | 2781 |
584
+ | `React: routing-only named exports` | 1245 |
585
+ | `Preact: routing-only named exports` | 1405 |
586
+ | `@kasperrt/smolter/react` | 2873 |
587
+ | `@kasperrt/smolter/preact` | 4748 |
588
+ | `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 3219 |
589
+ | `@kasperrt/smolter/preact + server transport` | 5059 |
590
+ | `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 7415 |
591
+ | `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 7696 |
592
+ | `@kasperrt/smolter/react/data (optional resource API)` | 2962 |
593
+ | `React: common routing/data exports` | 2683 |
594
+ | `Preact: common routing/data exports` | 2966 |
595
+ | `wouter: routing subset (Router, Route, Switch, Link, useParams, useLocation)` | 2854 |
596
+ | `wouter-preact: same routing subset` | 2211 |
451
597
  | `wouter@3.13.0` | 3091 |
452
598
  | `wouter-preact@3.13.0` | 2450 |
453
599
  | `react-router@8.4.0` | 60994 |
454
600
  <!-- size-comparison:end -->
455
601
 
456
- The table is generated by `pnpm size` from all exports of each listed entry,
602
+ The common-export rows measure the same named API set: `createDataRouter`,
603
+ `defineRoute`, `RouterProvider`, `Routes`, `Link`, `Form`, `useRouter`,
604
+ `useLoaderData`, `useActionData`, `useParams`, `useLocation`,
605
+ `useRevalidator` and `useFetcher`. Preact’s link and submission APIs currently
606
+ have additional behavior; equal names do not imply identical implementations.
607
+ The routing-only rows retain `createRouter`, `defineRoute`, `RouterProvider`,
608
+ `Routes`, `Link`, `useParams` and `useLocation`. Full Preact also exposes richer
609
+ application APIs. These tiers overlap: do not add the rows together.
610
+
611
+ The table is generated by `pnpm size` from the named subsets above or all exports of each other listed entry,
457
612
  using production ESM bundles, esbuild, Terser, and gzip level 9. React, React DOM,
458
613
  and Preact are external in every bundle. Transitive library dependencies are
459
614
  included. React Router provides a broader API; this compares complete entry
@@ -461,6 +616,43 @@ sizes, not equivalent feature sets or the minimum code for one route.
461
616
  `@kasperrt/smolter/react/client` adds browser transport for server loaders/actions; server-only
462
617
  rendering code and the Vite plugin are not shipped to the browser.
463
618
 
619
+ ### Routing without data hooks
620
+
621
+ ```tsx
622
+ import { render } from "preact";
623
+ import {
624
+ createRouter,
625
+ defineRoute,
626
+ Link,
627
+ RouterProvider,
628
+ Routes,
629
+ } from "@kasperrt/smolter/preact";
630
+ import Home from "./routes/home";
631
+ import About from "./routes/about";
632
+
633
+ const router = createRouter([
634
+ defineRoute("/", { default: Home }),
635
+ defineRoute("/about", { default: About }),
636
+ ], { browser: true });
637
+
638
+ render(
639
+ <RouterProvider router={router}>
640
+ <nav>
641
+ <Link href="/">Home</Link>
642
+ <Link href="/about">About</Link>
643
+ </nav>
644
+ <Routes />
645
+ </RouterProvider>,
646
+ document.getElementById("app")!,
647
+ );
648
+ ```
649
+
650
+ Add `useParams` for dynamic paths such as `/tasks/:id` and `useLocation` for
651
+ the current URL. Both are included in the routing-only measurement, even though
652
+ this example does not need them. Switch the factory to `createDataRouter` when
653
+ adding loaders, actions or fetchers, importing it from the same entry. SSR hydration and file-routed applications use the
654
+ larger client/document/lazy-route tier; they are not represented by the routing-only number.
655
+
464
656
  Exact current measurements are generated in
465
657
  [`packages/ui/src/size.json`](./packages/ui/src/size.json).
466
658
 
@@ -486,7 +678,7 @@ PRs. [Release workflow](https://github.com/kasperrt/smolter/blob/main/RELEASING.
486
678
  To test a local package build in another project:
487
679
 
488
680
  ```sh
489
- pnpm --dir packages/smolter pack --pack-destination ../..
681
+ pnpm pack
490
682
  pnpm add /absolute/path/to/kasperrt-smolter-0.0.1.tgz
491
683
  ```
492
684
 
@@ -0,0 +1,10 @@
1
+ //#region .generated/client/deserialize.ts
2
+ /** Restore a trusted snapshot produced by @kasperrt/smolter/server serialize, including its route error. */
3
+ function deserialize(json) {
4
+ const snapshot = JSON.parse(json);
5
+ const error = snapshot.error;
6
+ if (error) snapshot.error = Object.assign(new Error(error.message), { name: error.name });
7
+ return snapshot;
8
+ }
9
+ //#endregion
10
+ export { deserialize };
@@ -7,6 +7,7 @@ export type { ClientActionArgs, ClientLoaderArgs } from "../core/index.ts";
7
7
  export { deserialize } from "./deserialize.ts";
8
8
  /** Browser-safe route exports, excluding server implementation code. */
9
9
  export interface ClientRouteModule {
10
+ id?: string;
10
11
  default?: unknown;
11
12
  HydrateFallback?: unknown;
12
13
  ErrorBoundary?: unknown;
@@ -0,0 +1,46 @@
1
+ import { deserialize } from "./deserialize.js";
2
+ //#region .generated/client/index.ts
3
+ /** Browser transport for server routes. File-router builds generate this call. */
4
+ function serverRoute(path, module, handlers = {
5
+ loader: true,
6
+ action: true
7
+ }) {
8
+ const remote = async ({ request, signal }) => {
9
+ const url = new URL(request.url);
10
+ const response = await fetch(`/_smolter?url=${encodeURIComponent(url.pathname + url.search)}`, {
11
+ method: request.method,
12
+ signal,
13
+ ...request.method !== "GET" && { body: await request.formData() }
14
+ });
15
+ const location = response.headers.get("X-Smolter-Redirect");
16
+ if (location) return new Response(null, {
17
+ status: 303,
18
+ headers: { Location: location }
19
+ });
20
+ const data = await response.json();
21
+ if (!response.ok) return new Error(data.error ?? "Route request failed");
22
+ return data;
23
+ };
24
+ return {
25
+ ...module,
26
+ path,
27
+ ...handlers.loader && { loader: remote },
28
+ ...handlers.action && { action: remote },
29
+ ...module.clientLoader && { clientLoader: (args) => module.clientLoader({
30
+ ...args,
31
+ serverLoader: () => remote(args)
32
+ }) },
33
+ ...module.clientAction && { clientAction: (args) => {
34
+ const request = args.request.clone();
35
+ return module.clientAction({
36
+ ...args,
37
+ serverAction: () => remote({
38
+ ...args,
39
+ request: request.clone()
40
+ })
41
+ });
42
+ } }
43
+ };
44
+ }
45
+ //#endregion
46
+ export { deserialize, serverRoute };
@@ -0,0 +1,12 @@
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
+ export declare let applicationEnvironment: ApplicationEnvironment | undefined;
8
+ /** Configure deployment-specific navigation before rendering or mounting. */
9
+ export declare function configureApplication(environment: ApplicationEnvironment): void;
10
+ export declare function resolveApplicationURL(url: URL): URL;
11
+ export declare function handoffDocument(event: MouseEvent, destination: URL): void;
12
+ export declare function leaveDocument(destination: URL): void;
@@ -0,0 +1,23 @@
1
+ //#region .generated/core/environment.ts
2
+ var applicationEnvironment;
3
+ /** Configure deployment-specific navigation before rendering or mounting. */
4
+ function configureApplication(environment) {
5
+ applicationEnvironment = environment;
6
+ }
7
+ function resolveApplicationURL(url) {
8
+ return applicationEnvironment?.resolveURL?.(url) ?? url;
9
+ }
10
+ function handoffDocument(event, destination) {
11
+ if (!applicationEnvironment?.isDocumentHandoff?.(new URL(location.href), destination)) return;
12
+ event.preventDefault();
13
+ leaveDocument(destination);
14
+ }
15
+ function leaveDocument(destination) {
16
+ if (applicationEnvironment?.leaveDocument) {
17
+ applicationEnvironment.leaveDocument(destination);
18
+ return;
19
+ }
20
+ location.assign(destination.href);
21
+ }
22
+ //#endregion
23
+ export { applicationEnvironment, configureApplication, handoffDocument, leaveDocument, resolveApplicationURL };
@@ -0,0 +1,7 @@
1
+ //#region .generated/core/errors.ts
2
+ /** Resolve returned or rejected errors into Error values without losing their cause. */
3
+ function safeWrapAsync(work) {
4
+ return Promise.resolve().then(work).then((data) => data instanceof Error ? { error: data } : { data }, (cause) => ({ error: Error(String(cause), { cause }) }));
5
+ }
6
+ //#endregion
7
+ export { safeWrapAsync };
@@ -1,6 +1,7 @@
1
1
  /** Framework-independent data routing. @module */
2
2
  export * from "./errors.ts";
3
3
  export * from "./match.ts";
4
+ export * from "./navigation-router.ts";
4
5
  export * from "./router.ts";
5
6
  export * from "./serialize.ts";
6
7
  export * from "./types.ts";
@@ -0,0 +1,33 @@
1
+ //#region .generated/core/match.ts
2
+ /** Associate a URL pattern with a module while retaining its inferred handler types. */
3
+ function defineRoute(path, module) {
4
+ return {
5
+ ...module,
6
+ path
7
+ };
8
+ }
9
+ /** Static segments, :params and a terminal * wildcard; first match wins. */
10
+ function matchRoutes(routes, pathname) {
11
+ const segments = pathname.replace(/\/$/, "").split("/");
12
+ for (const route of routes) {
13
+ const parts = route.path.replace(/\/$/, "").split("/");
14
+ const params = {};
15
+ if ((parts.length === segments.length || parts.at(-1) === "*") && parts.every((part, index) => {
16
+ const value = segments[index];
17
+ if (part === "*") {
18
+ params["*"] = segments.slice(index).join("/");
19
+ return true;
20
+ }
21
+ if (part[0] === ":" && value) {
22
+ params[part.slice(1)] = new URLSearchParams(`=${value.replace(/[+&]/g, encodeURIComponent)}`).get("");
23
+ return true;
24
+ }
25
+ return part === value;
26
+ })) return {
27
+ route,
28
+ params
29
+ };
30
+ }
31
+ }
32
+ //#endregion
33
+ export { defineRoute, matchRoutes };
@@ -0,0 +1,15 @@
1
+ import type { Route, Router, RouterOptions } from "./types.ts";
2
+ /** Navigation and subscriptions without the loader/action engine. */
3
+ export type NavigationRouter = Pick<Router, "routes" | "getSnapshot" | "subscribe" | "start" | "navigate" | "dispose">;
4
+ /** Route modules for navigation only; data handlers require createDataRouter. */
5
+ export type NavigationRoute = Route & {
6
+ loader?: never;
7
+ action?: never;
8
+ clientLoader?: never;
9
+ clientAction?: never;
10
+ shouldRevalidate?: never;
11
+ };
12
+ /** Initial URL and optional browser history for navigation-only routing. */
13
+ export type NavigationRouterOptions = Pick<RouterOptions, "url" | "browser">;
14
+ /** Create a route matcher and history store without data requests or fetchers. */
15
+ export declare function createRouter(routes: readonly NavigationRoute[], options?: NavigationRouterOptions): NavigationRouter;
@@ -0,0 +1,64 @@
1
+ import { matchRoutes } from "./match.js";
2
+ import { internalURL } from "./url.js";
3
+ //#region .generated/core/navigation-router.ts
4
+ /** Create a route matcher and history store without data requests or fetchers. */
5
+ function createRouter(routes, options = {}) {
6
+ const browser = options.browser && typeof window !== "undefined";
7
+ const listeners = /* @__PURE__ */ new Set();
8
+ let users = 0;
9
+ const resolve = (url) => {
10
+ const match = matchRoutes(routes, url.pathname);
11
+ return {
12
+ url: url.href,
13
+ path: match?.route.path,
14
+ params: match?.params ?? {},
15
+ state: "idle",
16
+ loaded: true
17
+ };
18
+ };
19
+ let snapshot = resolve(new URL(options.url ?? (browser ? location.href : "/"), "http://localhost"));
20
+ const update = (url) => {
21
+ if (url.href === snapshot.url) return;
22
+ snapshot = resolve(url);
23
+ for (const listener of listeners) listener();
24
+ };
25
+ const pop = () => update(new URL(location.href));
26
+ return {
27
+ routes,
28
+ getSnapshot: () => snapshot,
29
+ subscribe(listener) {
30
+ listeners.add(listener);
31
+ return () => {
32
+ listeners.delete(listener);
33
+ };
34
+ },
35
+ start() {
36
+ if (!users++ && browser) {
37
+ addEventListener("popstate", pop);
38
+ addEventListener("hashchange", pop);
39
+ pop();
40
+ }
41
+ return () => {
42
+ if (!--users && browser) {
43
+ removeEventListener("popstate", pop);
44
+ removeEventListener("hashchange", pop);
45
+ }
46
+ };
47
+ },
48
+ async navigate(href, operation) {
49
+ const url = internalURL(snapshot.url, href);
50
+ if (!url) return;
51
+ if (browser) history[operation?.replace ? "replaceState" : "pushState"](operation?.state ?? null, "", url);
52
+ update(url);
53
+ },
54
+ dispose() {
55
+ listeners.clear();
56
+ if (browser) {
57
+ removeEventListener("popstate", pop);
58
+ removeEventListener("hashchange", pop);
59
+ }
60
+ }
61
+ };
62
+ }
63
+ //#endregion
64
+ export { createRouter };