@kasperrt/smolter 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.generated/client/hash-history.ts +27 -0
  2. package/.generated/client/hydration.ts +98 -0
  3. package/.generated/client/root-data.ts +52 -0
  4. package/.generated/core/environment.ts +1 -0
  5. package/.generated/core/navigation.ts +1 -0
  6. package/.generated/core/prepare-routes.ts +50 -0
  7. package/.generated/core/presentation.ts +1 -1
  8. package/.generated/core/responses.ts +12 -0
  9. package/.generated/core/utils.ts +5 -2
  10. package/.generated/preact/await.tsx +1 -0
  11. package/.generated/preact/client.tsx +8 -27
  12. package/.generated/preact/deferred-hydration.ts +1 -98
  13. package/.generated/preact/document.tsx +10 -0
  14. package/.generated/preact/index.ts +11 -2
  15. package/.generated/preact/lazy-route.ts +7 -0
  16. package/.generated/preact/navigation.tsx +19 -5
  17. package/.generated/preact/root-data.ts +1 -45
  18. package/.generated/preact/route-hooks.tsx +12 -2
  19. package/.generated/preact/route-types.ts +14 -0
  20. package/.generated/preact/router.tsx +5 -48
  21. package/.generated/react/client.tsx +130 -0
  22. package/.generated/react/document.tsx +235 -0
  23. package/.generated/react/fetcher.ts +29 -7
  24. package/.generated/react/hooks.ts +20 -18
  25. package/.generated/react/index.ts +28 -2
  26. package/.generated/react/lazy-route.ts +116 -0
  27. package/.generated/react/link.ts +1 -38
  28. package/.generated/react/navigation-state.ts +13 -0
  29. package/.generated/react/navigation.ts +145 -0
  30. package/.generated/react/outlet.ts +48 -0
  31. package/.generated/react/root-context.ts +22 -0
  32. package/.generated/react/route-hooks.ts +159 -0
  33. package/.generated/react/route-types.ts +91 -0
  34. package/.generated/react/router.ts +50 -0
  35. package/.generated/react/runtime.ts +1 -0
  36. package/.generated/server/application.tsx +18 -250
  37. package/.generated/server/assets.ts +7 -0
  38. package/.generated/server/dispatch.ts +87 -0
  39. package/.generated/server/handler.ts +3 -0
  40. package/.generated/server/http.ts +2 -0
  41. package/.generated/server/index.ts +2 -0
  42. package/.generated/server/node.ts +5 -2
  43. package/.generated/server/preact.ts +5 -2
  44. package/.generated/server/react.ts +34 -0
  45. package/.generated/server/render-application.ts +203 -0
  46. package/.generated/vite/application/lazy-files.ts +2 -1
  47. package/.generated/vite/application/vite.ts +12 -4
  48. package/.generated/vite/react.ts +11 -0
  49. package/CHANGELOG.md +20 -0
  50. package/README.md +48 -48
  51. package/dist/client/hash-history.d.ts +2 -0
  52. package/dist/client/hash-history.js +26 -0
  53. package/dist/client/hydration.d.ts +10 -0
  54. package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
  55. package/dist/client/root-data.d.ts +14 -0
  56. package/dist/{preact → client}/root-data.js +1 -1
  57. package/dist/core/environment.d.ts +1 -0
  58. package/dist/core/navigation.d.ts +1 -0
  59. package/dist/core/prepare-routes.d.ts +7 -0
  60. package/dist/core/prepare-routes.js +28 -0
  61. package/dist/core/presentation.js +1 -1
  62. package/dist/core/responses.d.ts +12 -0
  63. package/dist/core/responses.js +5 -0
  64. package/dist/core/utils.d.ts +5 -2
  65. package/dist/core/utils.js +1 -2
  66. package/dist/preact/await.d.ts +1 -0
  67. package/dist/preact/await.js +1 -0
  68. package/dist/preact/client.d.ts +7 -1
  69. package/dist/preact/client.js +11 -27
  70. package/dist/preact/deferred-hydration.d.ts +1 -10
  71. package/dist/preact/document.d.ts +10 -0
  72. package/dist/preact/document.js +11 -2
  73. package/dist/preact/index.d.ts +6 -2
  74. package/dist/preact/index.js +1 -1
  75. package/dist/preact/lazy-route.d.ts +2 -0
  76. package/dist/preact/lazy-route.js +6 -0
  77. package/dist/preact/navigation-state.js +1 -1
  78. package/dist/preact/navigation.d.ts +6 -0
  79. package/dist/preact/navigation.js +9 -5
  80. package/dist/preact/root-data.d.ts +1 -8
  81. package/dist/preact/route-hooks.d.ts +10 -0
  82. package/dist/preact/route-hooks.js +13 -3
  83. package/dist/preact/route-types.d.ts +14 -0
  84. package/dist/preact/router.d.ts +5 -2
  85. package/dist/preact/router.js +4 -26
  86. package/dist/react/client.d.ts +20 -0
  87. package/dist/react/client.js +86 -0
  88. package/dist/react/document.d.ts +25 -0
  89. package/dist/react/document.js +169 -0
  90. package/dist/react/fetcher.d.ts +8 -4
  91. package/dist/react/fetcher.js +12 -1
  92. package/dist/react/form.js +1 -1
  93. package/dist/react/hooks.d.ts +7 -6
  94. package/dist/react/index.d.ts +13 -2
  95. package/dist/react/index.js +7 -2
  96. package/dist/react/lazy-route.d.ts +22 -0
  97. package/dist/react/lazy-route.js +57 -0
  98. package/dist/react/link.d.ts +1 -12
  99. package/dist/react/navigation-state.d.ts +2 -0
  100. package/dist/react/navigation-state.js +9 -0
  101. package/dist/react/navigation.d.ts +33 -0
  102. package/dist/react/navigation.js +64 -0
  103. package/dist/react/outlet.d.ts +16 -0
  104. package/dist/react/outlet.js +26 -0
  105. package/dist/react/root-context.d.ts +14 -0
  106. package/dist/react/root-context.js +10 -0
  107. package/dist/react/route-hooks.d.ts +34 -0
  108. package/dist/react/route-hooks.js +89 -0
  109. package/dist/react/route-types.d.ts +69 -0
  110. package/dist/react/router.d.ts +10 -0
  111. package/dist/react/router.js +22 -0
  112. package/dist/react/runtime.d.ts +1 -1
  113. package/dist/react/runtime.js +1 -1
  114. package/dist/server/application.d.ts +4 -5
  115. package/dist/server/application.js +9 -154
  116. package/dist/server/assets.d.ts +2 -0
  117. package/dist/server/assets.js +6 -0
  118. package/dist/server/dispatch.d.ts +5 -0
  119. package/dist/server/dispatch.js +50 -0
  120. package/dist/server/handler.d.ts +3 -0
  121. package/dist/server/http.d.ts +2 -0
  122. package/dist/server/http.js +2 -0
  123. package/dist/server/index.d.ts +1 -0
  124. package/dist/server/index.js +2 -1
  125. package/dist/server/node.d.ts +5 -1
  126. package/dist/server/preact.d.ts +5 -1
  127. package/dist/server/preact.js +2 -1
  128. package/dist/server/react.d.ts +10 -0
  129. package/dist/server/react.js +22 -0
  130. package/dist/server/render-application.d.ts +31 -0
  131. package/dist/server/render-application.js +118 -0
  132. package/dist/vite/application/lazy-files.d.ts +1 -1
  133. package/dist/vite/application/lazy-files.js +2 -2
  134. package/dist/vite/application/vite.d.ts +2 -1
  135. package/dist/vite/application/vite.js +12 -5
  136. package/dist/vite/react.d.ts +4 -0
  137. package/dist/vite/react.js +9 -1
  138. package/jsr.json +10 -6
  139. package/package.json +21 -12
  140. package/.generated/core/resource-types.ts +0 -44
  141. package/.generated/core/resources.ts +0 -444
  142. package/.generated/react/data.ts +0 -25
  143. package/.generated/react/resources/components.tsx +0 -404
  144. package/dist/core/resource-types.d.ts +0 -37
  145. package/dist/core/resources.d.ts +0 -19
  146. package/dist/core/resources.js +0 -345
  147. package/dist/react/data.d.ts +0 -5
  148. package/dist/react/data.js +0 -3
  149. package/dist/react/link.js +0 -27
  150. package/dist/react/resources/components.d.ts +0 -64
  151. package/dist/react/resources/components.js +0 -239
@@ -8,6 +8,7 @@ export interface ServerBuild {
8
8
  handleDataRequest: (request: Request) => Response | Promise<Response>;
9
9
  }
10
10
 
11
+ /** SSR build, browser entry URL and application-scoped data endpoint for Node or Express. */
11
12
  export interface RequestHandlerOptions {
12
13
  build: ServerBuild;
13
14
  /** Browser entry URL resolved from Vite's production manifest. */
@@ -16,12 +17,14 @@ export interface RequestHandlerOptions {
16
17
  dataEndpoint?: string;
17
18
  }
18
19
 
20
+ /** Node request with optional Express protocol and original URL information. */
19
21
  export interface ServerIncomingMessage extends IncomingMessage {
20
22
  /** Express supplies this using the application's trust proxy policy. */
21
23
  protocol?: string;
22
24
  originalUrl?: string;
23
25
  }
24
26
 
27
+ /** Express-compatible callback for forwarding middleware errors. */
25
28
  export type NextHandler = (error?: unknown) => void;
26
29
 
27
30
  /** Node/Express middleware; mount after metrics, tracing and static assets. */
@@ -3,6 +3,7 @@ import { Readable, Transform } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
4
  import type { ServerIncomingMessage } from "./handler.ts";
5
5
 
6
+ /** Convert a Node request to a Web Request with cancellation and an optional normalized route target. */
6
7
  export async function webRequest(
7
8
  incoming: ServerIncomingMessage,
8
9
  outgoing: ServerResponse,
@@ -40,6 +41,7 @@ interface StreamingResponse extends ServerResponse {
40
41
  flush?: () => void;
41
42
  }
42
43
 
44
+ /** Write a Web Response to Node with streaming backpressure and disconnect cancellation. */
43
45
  export async function send(response: Response, outgoing: StreamingResponse) {
44
46
  outgoing.statusCode = response.status;
45
47
  response.headers.forEach((value, name) => {
@@ -117,3 +117,5 @@ export async function handleRequest(
117
117
  ): Promise<Response> {
118
118
  return privateResponse(await dispatchRequest(routes, request));
119
119
  }
120
+
121
+ export { dispatchRoute } from "./dispatch.ts";
@@ -1,4 +1,7 @@
1
- /** Optional application integration. @module */
2
-
1
+ /**
2
+ * Streaming Node and Express adapters for Web requests and responses.
3
+ *
4
+ * @module
5
+ */
3
6
  export * from "./handler.ts";
4
7
  export * from "./http.ts";
@@ -1,5 +1,8 @@
1
- /** Preact server rendering and HTTP data dispatch. @module */
2
-
1
+ /**
2
+ * Preact SSR rendering and route data dispatch.
3
+ *
4
+ * @module
5
+ */
3
6
  export {
4
7
  dispatchRoute,
5
8
  type RenderApplicationOptions,
@@ -0,0 +1,34 @@
1
+ /**
2
+ * React SSR rendering and route data dispatch.
3
+ * @module
4
+ */
5
+ import type { AppRoute, RootModule } from "../react/route-types.ts";
6
+ import { RouterApplication } from "../react/router.ts";
7
+ import { createElement } from "react";
8
+ import * as ReactDOMServer from "react-dom/server";
9
+ import {
10
+ type RenderApplicationOptions,
11
+ renderDocument,
12
+ } from "./render-application.ts";
13
+
14
+ export * from "./index.ts";
15
+ export type { RenderApplicationOptions } from "./render-application.ts";
16
+
17
+ /** Render a React document with root and route data using React's streaming renderer. */
18
+ export async function renderApplication(
19
+ request: Request,
20
+ routes: AppRoute[],
21
+ rootModule: RootModule,
22
+ entry: string,
23
+ options: RenderApplicationOptions = {},
24
+ ): Promise<Response> {
25
+ const { renderToReadableStream } = ReactDOMServer;
26
+ if (!renderToReadableStream)
27
+ throw new Error("renderApplication requires React DOM 19 or newer.");
28
+ return renderDocument(request, routes, rootModule, entry, options, (props) =>
29
+ renderToReadableStream(
30
+ createElement(RouterApplication, { ...props, rootModule }),
31
+ { nonce: options.nonce, signal: request.signal },
32
+ ),
33
+ );
34
+ }
@@ -0,0 +1,203 @@
1
+ import type { DataArgs, Route, Router } from "../core/index.ts";
2
+ import { createDataRouter, matchRoutes, safeWrapAsync } from "../core/index.ts";
3
+ import { prepareRoutes } from "../core/prepare-routes.ts";
4
+ import { normalizeResult } from "../core/responses.ts";
5
+ import { loadRouter } from "./index.ts";
6
+ import { deferredHydration } from "./deferred-hydration.ts";
7
+ import { dispatchRoute } from "./dispatch.ts";
8
+
9
+ interface ServerRoot {
10
+ loader?: (args: DataArgs) => unknown;
11
+ ErrorBoundary?: unknown;
12
+ }
13
+
14
+ export interface RenderProps {
15
+ router: Router;
16
+ routes: (Route & { routePath?: string })[];
17
+ root: unknown;
18
+ entry: string;
19
+ rootError: boolean;
20
+ streaming: boolean;
21
+ initialData?: string;
22
+ document: boolean;
23
+ }
24
+
25
+ import { progressiveDocument } from "./progressive-document.ts";
26
+ import { resolveHydration, streamDocument } from "./stream-document.ts";
27
+
28
+ /** SSR settings for CSP nonce and early deferred-data hydration. */
29
+ export interface RenderApplicationOptions {
30
+ nonce?: string;
31
+ earlyHydration?: boolean;
32
+ }
33
+
34
+ /** Render a Preact application response with root and route data, streaming deferred content when enabled. */
35
+ export async function renderDocument(
36
+ request: Request,
37
+ routes: (Route & { routePath?: string })[],
38
+ rootModule: ServerRoot,
39
+ entry: string,
40
+ options: RenderApplicationOptions,
41
+ render: (
42
+ props: RenderProps,
43
+ ) =>
44
+ | (ReadableStream<Uint8Array> & { allReady: Promise<unknown> })
45
+ | Promise<ReadableStream<Uint8Array> & { allReady: Promise<unknown> }>,
46
+ ): Promise<Response> {
47
+ routes = routes.map((route) => ({
48
+ ...route,
49
+ path: route.routePath ?? route.path,
50
+ }));
51
+ const match = matchRoutes(routes, new URL(request.url).pathname);
52
+
53
+ if (match && !match.route.default)
54
+ return dispatchRoute(request, routes, true);
55
+
56
+ let actionResponse: Response | undefined;
57
+ let actionData: unknown;
58
+
59
+ if (request.method !== "GET" && request.method !== "HEAD") {
60
+ actionResponse = await dispatchRoute(request, routes, true);
61
+
62
+ if (
63
+ actionResponse.headers.has("Location") ||
64
+ !actionResponse.headers.get("Content-Type")?.includes("application/json")
65
+ )
66
+ return actionResponse;
67
+
68
+ actionData = await normalizeResult(await actionResponse.json());
69
+ }
70
+
71
+ const rootResult = safeWrapAsync(() =>
72
+ Promise.resolve()
73
+ .then(() =>
74
+ rootModule.loader?.({ request, params: {}, signal: request.signal }),
75
+ )
76
+ .then(
77
+ (value) => normalizeResult(value, false),
78
+ (error: unknown) =>
79
+ error instanceof Response
80
+ ? normalizeResult(error, false)
81
+ : error instanceof Error
82
+ ? error
83
+ : new Error(String(error), { cause: error }),
84
+ ),
85
+ );
86
+ let redirectResponse: Response | undefined;
87
+ const prepared = prepareRoutes(
88
+ routes.map((route) => ({
89
+ ...route,
90
+ ErrorBoundary: route.ErrorBoundary ?? rootModule.ErrorBoundary,
91
+ })),
92
+ true,
93
+ ).map((route) => {
94
+ if (!route.loader) return route;
95
+
96
+ const loader = route.loader;
97
+
98
+ return {
99
+ ...route,
100
+ loader: async (args: Parameters<typeof loader>[0]) => {
101
+ const value = await loader(args);
102
+
103
+ if (value instanceof Response && value.headers.has("Location"))
104
+ redirectResponse = value;
105
+
106
+ return value;
107
+ },
108
+ };
109
+ });
110
+ const [rootState, loadedRouter] = await Promise.all([
111
+ rootResult,
112
+ loadRouter(prepared, request),
113
+ ]);
114
+ let router = loadedRouter;
115
+ const rootError =
116
+ rootState.error ??
117
+ (rootState.data instanceof Error ? rootState.data : undefined);
118
+ const root = rootError ? undefined : rootState.data;
119
+
120
+ if (root instanceof Response && root.headers.has("Location")) {
121
+ router.dispose();
122
+ return root;
123
+ }
124
+
125
+ if (redirectResponse) {
126
+ router.dispose();
127
+
128
+ return redirectResponse;
129
+ }
130
+
131
+ if (actionResponse || rootError) {
132
+ const hydrationData = {
133
+ ...router.getSnapshot(),
134
+ actionData: actionData instanceof Error ? undefined : actionData,
135
+ ...(actionData instanceof Error && { error: actionData }),
136
+ ...(rootError && { error: rootError }),
137
+ };
138
+ router.dispose();
139
+ router = createDataRouter(router.routes, { hydrationData });
140
+ }
141
+
142
+ const snapshot = router.getSnapshot();
143
+ const hydrationData = {
144
+ root,
145
+ snapshot: {
146
+ ...snapshot,
147
+ error: snapshot.error && {
148
+ ...snapshot.error,
149
+ name: snapshot.error.name,
150
+ message: snapshot.error.message,
151
+ },
152
+ },
153
+ ...(rootError && { rootError: true }),
154
+ };
155
+ const deferred = options.earlyHydration
156
+ ? deferredHydration(hydrationData, options.nonce)
157
+ : undefined;
158
+ const stream = await Promise.resolve()
159
+ .then(() =>
160
+ render({
161
+ router,
162
+ routes,
163
+ root,
164
+ entry,
165
+ rootError: Boolean(rootError),
166
+ streaming: true,
167
+ initialData: deferred?.initial,
168
+ document: true,
169
+ }),
170
+ )
171
+ .catch(async (error: unknown) => {
172
+ router.dispose();
173
+ await deferred?.stream.cancel();
174
+ throw error;
175
+ });
176
+ // The renderer reports the same rejection through its stream reader.
177
+ void stream.allReady.catch(() => {});
178
+ const headers = new Headers(actionResponse?.headers);
179
+ headers.delete("Content-Length");
180
+ headers.delete("Content-Encoding");
181
+ headers.set("Content-Type", "text/html; charset=utf-8");
182
+ headers.set("Cache-Control", "no-store");
183
+
184
+ return new Response(
185
+ deferred
186
+ ? progressiveDocument(stream, deferred.stream, {
187
+ signal: request.signal,
188
+ dispose: () => router.dispose(),
189
+ })
190
+ : streamDocument(stream, {
191
+ hydration: resolveHydration(hydrationData),
192
+ signal: request.signal,
193
+ nonce: options.nonce,
194
+ dispose: () => router.dispose(),
195
+ }),
196
+ {
197
+ status: snapshot.error
198
+ ? Number("status" in snapshot.error ? snapshot.error.status : 500)
199
+ : (actionResponse?.status ?? 200),
200
+ headers,
201
+ },
202
+ );
203
+ }
@@ -12,6 +12,7 @@ interface ChunkMetadata {
12
12
  export function lazyFiles(
13
13
  directory: string,
14
14
  dataEndpoint = "/_smolter",
15
+ framework: "react" | "preact" = "preact",
15
16
  ): Plugin {
16
17
  let root: string;
17
18
  const modules = new Map<string, string>();
@@ -139,7 +140,7 @@ export function lazyFiles(
139
140
  }
140
141
 
141
142
  return [
142
- 'import { lazyRoute } from "@kasperrt/smolter/preact/lazy-route";',
143
+ `import { lazyRoute } from "@kasperrt/smolter/${framework}/lazy-route";`,
143
144
  `export default [${definitions.join(",\n")}];`,
144
145
  ].join("\n");
145
146
  },
@@ -6,6 +6,7 @@ import { lazyFiles } from "./lazy-files.ts";
6
6
 
7
7
  export interface ApplicationOptions {
8
8
  directory?: string;
9
+ framework?: "react" | "preact";
9
10
  dataEndpoint?: string;
10
11
  ssr?: boolean;
11
12
  splitRoutes?: boolean;
@@ -43,6 +44,7 @@ export const preactConfig: UserConfig = {
43
44
 
44
45
  export function applicationPlugins({
45
46
  directory = "src/routes",
47
+ framework = "preact",
46
48
  dataEndpoint = "/_smolter",
47
49
  ssr = true,
48
50
  splitRoutes = true,
@@ -51,7 +53,13 @@ export function applicationPlugins({
51
53
  const application: Plugin = {
52
54
  name: "smolter-application",
53
55
  enforce: "pre",
54
- config: () => preactConfig,
56
+ config: () =>
57
+ framework === "preact"
58
+ ? preactConfig
59
+ : {
60
+ resolve: { dedupe: ["react", "react-dom", "@kasperrt/smolter"] },
61
+ ssr: { noExternal: ["@kasperrt/smolter"] },
62
+ },
55
63
  transformIndexHtml: {
56
64
  order: "post",
57
65
  handler(html, context) {
@@ -93,7 +101,7 @@ export function applicationPlugins({
93
101
  const request = await webRequest(incoming, outgoing, target);
94
102
  const [{ default: routes }, { dispatchRoute }] = await Promise.all([
95
103
  server.ssrLoadModule("virtual:smolter/routes"),
96
- server.ssrLoadModule("@kasperrt/smolter/preact/server"),
104
+ server.ssrLoadModule(`@kasperrt/smolter/${framework}/server`),
97
105
  ]);
98
106
  await send(await dispatchRoute(request, routes), outgoing);
99
107
  };
@@ -144,7 +152,7 @@ export function applicationPlugins({
144
152
  return [
145
153
  application,
146
154
  browserBoundary(),
147
- ...(splitRoutes ? [lazyFiles(directory, dataEndpoint)] : []),
148
- routerPlugin({ directory, framework: "preact", dataEndpoint }),
155
+ ...(splitRoutes ? [lazyFiles(directory, dataEndpoint, framework)] : []),
156
+ routerPlugin({ directory, framework, dataEndpoint }),
149
157
  ];
150
158
  }
@@ -17,7 +17,18 @@ export function routerPlugin(options: FileRouterOptions = {}): Plugin {
17
17
  /** File routing is the default; this alias keeps configuration explicit. */
18
18
  export const fileRouter = routerPlugin;
19
19
 
20
+ export type { ApplicationOptions } from "./application/vite.ts";
20
21
  export {
21
22
  type DeploymentPluginOptions,
22
23
  deploymentPlugin,
23
24
  } from "./deployment.ts";
25
+
26
+ import {
27
+ type ApplicationOptions,
28
+ applicationPlugins as plugins,
29
+ } from "./application/vite.ts";
30
+
31
+ /** Configure React application SSR, scoped data transport and lazy file routing. */
32
+ export function applicationPlugins(options: ApplicationOptions = {}): Plugin[] {
33
+ return plugins({ ...options, framework: "react" });
34
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4
4
+
5
+ - Remove the separate `/react/data` resource router. Both main adapters provide route data, forms, submissions and independent fetchers through `createDataRouter`; migrate ResourceProvider/Resource hooks to RouterProvider and the main data hooks. The old pathless resource store and tuple fetcher API are removed, not aliased to incompatible signatures.
6
+
7
+ - Complete React routing-hook parity: navigation/submission state, URL/search/history helpers, route errors, root data and route-ID data. Add React RouterApplication and match Preact loader/fetcher type inference.
8
+ - Support React application SSR and hydration, document helpers, lazy routes and application Vite middleware. React DOM 19+ powers integrated documents; routing hooks and manual loadRouter SSR retain React 18 support. Share transport, root revalidation, hash history, hydration payloads and server orchestration across renderers.
9
+ - Export renderer-independent dispatchRoute from both server entries. Preserve scoped endpoints, private caching, redirects, cancellation and mutation ownership.
10
+ - Add React link intent prefetching, structured/GET fetcher submissions and real NavLink pending state in both adapters. Report pending destinations under plain providers as well as retained application views.
11
+ - Update compatibility badges, guides, package-isolation checks and bundle tables. Verify React production early hydration, persistent input, root refresh, CSP and abandoned deferred boundaries.
12
+
13
+ ## 0.4.3
14
+
15
+ - Prefetch individual guide articles on pointer, keyboard and touch intent. Keep the shared header, sidebar and current article visible during cold guide navigation; show a quiet pending status without replacing the layout. Link the header directly to the canonical Getting started guide.
16
+ - Publish compact React/Preact and SSR compatibility indicators in guide navigation and detailed support information on each page.
17
+ - Document every public JSR entrypoint and exported symbol. Validate the generated documentation before publishing, preserving JSR-before-npm release ordering.
18
+
19
+ - Add React `Outlet`, `useMatches` and `useOutletContext`; allow Preact matches under a plain RouterProvider. Show renderer and SSR compatibility in documentation navigation and on each page. Integrated document/early-hydration APIs remain Preact-only.
20
+
21
+ - Remove the public `safeWrapAsync` utility; retain it internally for router and server error handling. Existing consumers of this export must provide their own error handling. `useMatches` and `useOutletContext` remain public Preact APIs.
22
+
3
23
  ## 0.4.2
4
24
 
5
25
  - Add integrated `Await fallback` for early hydration: pending and resolved content belongs to the mounted component, so navigation, root revalidation and late settlement cannot leave abandoned content behind. Migrate early-hydrated deferred sections from outer Preact `Suspense` to this prop, including root sections. Native Preact streamed Suspense remains unsupported for this ownership contract; it is not patched. Ordinary non-early SSR still streams resolved HTML.
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.4.2%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.4.2%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.4.2%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.4.2%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.4.4%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.4.4%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.4.4%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.4.4%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.**
@@ -14,9 +14,15 @@ loader; `shouldRevalidate` decides when that work is needed.
14
14
  Inspired by [wouter](https://github.com/molefrog/wouter)
15
15
  and [React Router](https://reactrouter.com/).
16
16
 
17
- [Read the documentation](https://kasperrt.github.io/smolter/react/docs/) for guides and the complete API reference. The site itself uses smolter routes.
17
+ [Read the documentation](https://kasperrt.github.io/smolter/docs/) for guides and the complete API reference. The prerendered site uses Preact and Smolter routes. Articles load individually; React examples remain available in the repository.
18
18
 
19
- Start with [loaders](https://kasperrt.github.io/smolter/react/docs/loaders/), [actions](https://kasperrt.github.io/smolter/react/docs/actions/), [forms and validation](https://kasperrt.github.io/smolter/react/docs/forms/), or [fetcher reads and mutations](https://kasperrt.github.io/smolter/react/docs/fetchers/). Each guide connects the handler to an actual route URL and its consuming view.
19
+ Start with [loaders](https://kasperrt.github.io/smolter/docs/loaders/), [actions](https://kasperrt.github.io/smolter/docs/actions/), [forms and validation](https://kasperrt.github.io/smolter/docs/forms/), or [fetcher reads and mutations](https://kasperrt.github.io/smolter/docs/fetchers/). Each guide connects the handler to an actual route URL and its consuming view.
20
+
21
+ ## React and Preact application APIs
22
+
23
+ Both adapters expose navigation, submission, error, root-data and route-data hooks. Both server entries expose `dispatchRoute` and `renderApplication`; both client entries provide `mountApplication`, scoped transport and deployment recovery. Import `applicationPlugins` from the matching renderer's `/vite` entry for lazy file routes and development SSR.
24
+
25
+ Integrated React documents use React DOM 19+ (`react/document`, `react/client`, `react/server`). React 18 remains supported for routing hooks and manual `loadRouter` SSR. Use React's `use(promise)` inside `Suspense` for deferred rendering. Preact uses `Await` with an explicit `fallback` for early hydration. These renderer choices share transport and data ownership, without loading the other framework.
20
26
 
21
27
  ## Contents
22
28
 
@@ -445,18 +451,18 @@ Choose one renderer for your application. Both expose the same routing API:
445
451
  | SSR and HTTP data dispatch | `@kasperrt/smolter/react/server` | `@kasperrt/smolter/preact/server` |
446
452
  | Vite plugin and flat-file discovery | `@kasperrt/smolter/react/vite` | `@kasperrt/smolter/preact/vite` |
447
453
 
448
- `@kasperrt/smolter/utils` contains browser-safe matching and error helpers.
454
+ `@kasperrt/smolter/utils` contains browser-safe matching and URL helpers.
449
455
  The main renderer entries never import server or build modules. Named imports
450
456
  are tree-shaken; optional transport is included only when you import `/client`.
451
457
  Server entries do not import either UI renderer. Vite entries are build-time only.
452
458
 
453
459
  **Migrating renderer subpaths:** replace `/core` imports for types, router factories, and
454
460
  `defineRoute` with your renderer entry. Use `/utils` for `matchRoutes`,
455
- `internalURL` and `safeWrapAsync`. Move `/client`, `/server`,
461
+ and `internalURL`. Move `/client`, `/server`,
456
462
  `/files`, and `/vite` imports to the corresponding renderer-prefixed path above.
457
463
  The bare package root and old top-level subpaths are no longer exported.
458
464
 
459
- See the [0.3 migration guide](https://kasperrt.github.io/smolter/react/docs/migration-0.3/) and [changelog](./CHANGELOG.md).
465
+ See the [changelog](./CHANGELOG.md) for release history.
460
466
 
461
467
  **Router factory migration:** code that previously used `createRouter` with
462
468
  loaders, actions, fetchers or revalidation must now use `createDataRouter`.
@@ -525,7 +531,7 @@ re-exports and installs server proxies. `entry.client.tsx` and `entry.server.tsx
525
531
  are application-owned files wiring those manifests to hydration and SSR.
526
532
  Manual tables can be imported directly instead.
527
533
 
528
- See the [virtual routes guide](https://kasperrt.github.io/smolter/react/docs/virtual-routes)
534
+ See the [virtual routes guide](https://kasperrt.github.io/smolter/docs/virtual-routes)
529
535
  for the exact contract, TypeScript declaration, and both entrypoint examples.
530
536
 
531
537
  ### Express hosting
@@ -537,39 +543,33 @@ server entry and `entry` is the browser entry URL from Vite's manifest. Do not
537
543
  consume request bodies with Express body parsers before this handler.
538
544
  The adapter has no Express dependency and forwards failures to `next(error)`.
539
545
 
540
- See the [Express guide](https://kasperrt.github.io/smolter/react/docs/express)
546
+ See the [Express guide](https://kasperrt.github.io/smolter/docs/express)
541
547
  for a complete server example and streaming and hydration behavior.
542
548
 
543
- ## Resource subscriptions
549
+ ## Independent data requests
544
550
 
545
- `@kasperrt/smolter/react/data` adds subscriptions to named resources when their
546
- lifetime is independent of URL navigation. The state and cancellation logic
547
- lives in core; the entry adds React bindings without DOM or HTTP globals.
551
+ Both adapters include `useFetcher`, `useSubmit`, `Form` and the loader/action hooks
552
+ in their main entrypoint. Use `createDataRouter` for data routes and `useFetcher`
553
+ for reads or mutations that should not navigate the page. There is no separate
554
+ React resource router or `/react/data` entrypoint.
548
555
 
549
- ```ts
550
- import { createResourceRouter } from "@kasperrt/smolter/react/data";
551
-
552
- const resources = createResourceRouter(new Map([
553
- ["settings", {
554
- loader: async ({ signal }) => ({
555
- data: await readSettings(signal),
556
- error: "",
557
- }),
558
- action: async ({ input, signal }) => ({
559
- data: await saveSettings(input, signal),
560
- error: "",
561
- }),
562
- }],
563
- ]));
564
- ```
565
-
566
- `ResourceProvider` owns the store lifetime. `Resource` selects a resource by
567
- `routeId` and params. `useResourceData` reads it; `useRouteLoaderData` reads a
568
- named resource. `useResourceFetcher` returns `[state, operations]`, with
569
- independent `load` and `submit` operations. Successful mutations consult the
570
- resource's `shouldRevalidate` before reloading its data. Last-subscriber cleanup
571
- aborts pending work; `persistent` keeps a resource until explicitly disposed.
572
- This optional entry does not contribute to the base React bundle measurement.
556
+ ```tsx
557
+ import { useFetcher } from "@kasperrt/smolter/react";
558
+
559
+ function Settings() {
560
+ const settings = useFetcher();
561
+ return (
562
+ <settings.Form method="post" action="/settings">
563
+ <input name="name" aria-label="Profile name" />
564
+ <button disabled={settings.state !== "idle"}>Save</button>
565
+ </settings.Form>
566
+ );
567
+ }
568
+ ```
569
+
570
+ Use the same imports from `/preact` in a Preact application. Independent
571
+ fetchers share the application router's cancellation, revalidation and deployment
572
+ recovery ownership; they do not require another provider or store.
573
573
 
574
574
  ## Size and checks
575
575
 
@@ -581,7 +581,6 @@ This optional entry does not contribute to the base React bundle measurement.
581
581
  | Preact application hydration | `@kasperrt/smolter/preact/client` |
582
582
  | Preact document components | `@kasperrt/smolter/preact/document` |
583
583
  | Lazy Preact route modules | `@kasperrt/smolter/preact/lazy-route` |
584
- | Optional React resources | `@kasperrt/smolter/react/data` |
585
584
  | Server rendering / Vite integration | Renderer `/server` / `/vite` subpaths; keep out of browser imports |
586
585
 
587
586
  The measurements below describe retained exports, not the cost of an import
@@ -605,16 +604,17 @@ a claim of identical features.
605
604
  | --- | ---: |
606
605
  | `React Router: declarative routing subset` | 12882 |
607
606
  | `React Router: data routing subset` | 29780 |
608
- | `React: routing-only named exports` | 1245 |
607
+ | `React: routing-only named exports` | 1419 |
609
608
  | `Preact: routing-only named exports` | 1405 |
610
- | `@kasperrt/smolter/react` | 2948 |
611
- | `@kasperrt/smolter/preact` | 5024 |
612
- | `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 4281 |
613
- | `@kasperrt/smolter/preact + server transport` | 6300 |
614
- | `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 9405 |
615
- | `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 9692 |
616
- | `@kasperrt/smolter/react/data (optional resource API)` | 2962 |
617
- | `React: common routing/data exports` | 2761 |
609
+ | `@kasperrt/smolter/react` | 4801 |
610
+ | `@kasperrt/smolter/preact` | 5076 |
611
+ | `@kasperrt/smolter/react + server transport (named client exports)` | 6060 |
612
+ | `@kasperrt/smolter/preact + server transport` | 6345 |
613
+ | `@kasperrt/smolter/react + client + document (application)` | 9124 |
614
+ | `@kasperrt/smolter/react + client + document + lazy-route` | 9401 |
615
+ | `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 9435 |
616
+ | `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 9721 |
617
+ | `React: common routing/data exports` | 3071 |
618
618
  | `Preact: common routing/data exports` | 3046 |
619
619
  | `wouter: routing subset (Router, Route, Switch, Link, useParams, useLocation)` | 2854 |
620
620
  | `wouter-preact: same routing subset` | 2211 |
@@ -735,7 +735,7 @@ normally, and a session marker prevents reload loops. Development/SSR builds
735
735
  disable the helper. This optional client module adds nothing to the routing-only
736
736
  entrypoints and never replays form submissions.
737
737
 
738
- See [deployment updates](https://kasperrt.github.io/smolter/react/docs/deployments/)
738
+ See [deployment updates](https://kasperrt.github.io/smolter/docs/deployments/)
739
739
  for Express, CDN paths, TypeScript declarations and unsaved-state handling.
740
740
 
741
741
 
@@ -0,0 +1,2 @@
1
+ import type { Router } from "../core/index.ts";
2
+ export declare function connectHashHistory(router: Router): void;
@@ -0,0 +1,26 @@
1
+ //#region .generated/client/hash-history.ts
2
+ function connectHashHistory(router) {
3
+ const read = router.getSnapshot;
4
+ const navigate = router.navigate;
5
+ router.navigate = async (url, options) => {
6
+ const target = new URL(url, router.getSnapshot().url);
7
+ const hash = `#${target.pathname}${target.search}${target.hash}`;
8
+ history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
9
+ await navigate(url, options);
10
+ };
11
+ const unsubscribe = router.subscribe(() => {
12
+ const target = new URL(read().url);
13
+ const hash = `#${target.pathname}${target.search}${target.hash}`;
14
+ if (location.hash !== hash) history.replaceState(history.state, "", hash);
15
+ });
16
+ const onHashChange = () => void navigate(location.hash.slice(1) || "/");
17
+ window.addEventListener("hashchange", onHashChange);
18
+ const dispose = router.dispose;
19
+ router.dispose = () => {
20
+ unsubscribe();
21
+ window.removeEventListener("hashchange", onHashChange);
22
+ dispose();
23
+ };
24
+ }
25
+ //#endregion
26
+ export { connectHashHistory };
@@ -0,0 +1,10 @@
1
+ type Settlement = [number, unknown, string | null];
2
+ export interface DeferredWindow extends Window {
3
+ __smolterDeferred?: Settlement[];
4
+ }
5
+ /** Restore promise identity before hydrating Suspense boundaries. */
6
+ export declare function readHydration(json: string, timeout?: number): {
7
+ hydration: any;
8
+ dispose: () => void;
9
+ };
10
+ export {};
@@ -1,4 +1,4 @@
1
- //#region .generated/preact/deferred-hydration.ts
1
+ //#region .generated/client/hydration.ts
2
2
  /** Restore promise identity before hydrating Suspense boundaries. */
3
3
  function readHydration(json, timeout = 3e4) {
4
4
  if (!Number.isInteger(timeout) || timeout < 1 || timeout > 2147483647) throw new RangeError("deferredTimeout must be an integer between 1 and 2147483647 ms");
@@ -0,0 +1,14 @@
1
+ import type { RevalidationArgs, Router } from "../core/index.ts";
2
+ interface RootModule {
3
+ default?: unknown;
4
+ Layout?: unknown;
5
+ id?: string;
6
+ loaderPath?: string;
7
+ shouldRevalidate?: (args: RevalidationArgs) => boolean;
8
+ }
9
+ export { createStore as createRootStore } from "../core/store.ts";
10
+ export declare function attachRootData(router: Router, initial: unknown, rootModule?: RootModule): {
11
+ getSnapshot: () => unknown;
12
+ subscribe(listener: () => void): () => void;
13
+ update(next: unknown): void;
14
+ };