@kasperrt/smolter 0.4.3 → 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 (112) 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/prepare-routes.ts +50 -0
  5. package/.generated/core/presentation.ts +1 -1
  6. package/.generated/preact/client.tsx +1 -26
  7. package/.generated/preact/deferred-hydration.ts +1 -98
  8. package/.generated/preact/index.ts +6 -1
  9. package/.generated/preact/navigation.tsx +13 -5
  10. package/.generated/preact/root-data.ts +1 -45
  11. package/.generated/preact/router.tsx +2 -48
  12. package/.generated/react/client.tsx +130 -0
  13. package/.generated/react/document.tsx +235 -0
  14. package/.generated/react/fetcher.ts +29 -7
  15. package/.generated/react/hooks.ts +20 -18
  16. package/.generated/react/index.ts +17 -1
  17. package/.generated/react/lazy-route.ts +116 -0
  18. package/.generated/react/link.ts +1 -38
  19. package/.generated/react/navigation-state.ts +13 -0
  20. package/.generated/react/navigation.ts +145 -0
  21. package/.generated/react/root-context.ts +22 -0
  22. package/.generated/react/route-hooks.ts +159 -0
  23. package/.generated/react/route-types.ts +91 -0
  24. package/.generated/react/router.ts +50 -0
  25. package/.generated/react/runtime.ts +1 -0
  26. package/.generated/server/application.tsx +18 -253
  27. package/.generated/server/dispatch.ts +87 -0
  28. package/.generated/server/index.ts +2 -0
  29. package/.generated/server/react.ts +34 -0
  30. package/.generated/server/render-application.ts +203 -0
  31. package/.generated/vite/application/lazy-files.ts +2 -1
  32. package/.generated/vite/application/vite.ts +12 -4
  33. package/.generated/vite/react.ts +11 -0
  34. package/CHANGELOG.md +10 -0
  35. package/README.md +41 -41
  36. package/dist/client/hash-history.d.ts +2 -0
  37. package/dist/client/hash-history.js +26 -0
  38. package/dist/client/hydration.d.ts +10 -0
  39. package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
  40. package/dist/client/root-data.d.ts +14 -0
  41. package/dist/{preact → client}/root-data.js +1 -1
  42. package/dist/core/prepare-routes.d.ts +7 -0
  43. package/dist/core/prepare-routes.js +28 -0
  44. package/dist/core/presentation.js +1 -1
  45. package/dist/preact/client.js +5 -26
  46. package/dist/preact/deferred-hydration.d.ts +1 -10
  47. package/dist/preact/document.js +2 -2
  48. package/dist/preact/index.d.ts +1 -1
  49. package/dist/preact/index.js +1 -1
  50. package/dist/preact/navigation-state.js +1 -1
  51. package/dist/preact/navigation.js +7 -5
  52. package/dist/preact/root-data.d.ts +1 -8
  53. package/dist/preact/route-hooks.js +1 -1
  54. package/dist/preact/router.d.ts +2 -2
  55. package/dist/preact/router.js +2 -26
  56. package/dist/react/client.d.ts +20 -0
  57. package/dist/react/client.js +86 -0
  58. package/dist/react/document.d.ts +25 -0
  59. package/dist/react/document.js +169 -0
  60. package/dist/react/fetcher.d.ts +8 -4
  61. package/dist/react/fetcher.js +12 -1
  62. package/dist/react/form.js +1 -1
  63. package/dist/react/hooks.d.ts +7 -6
  64. package/dist/react/index.d.ts +7 -1
  65. package/dist/react/index.js +6 -2
  66. package/dist/react/lazy-route.d.ts +22 -0
  67. package/dist/react/lazy-route.js +57 -0
  68. package/dist/react/link.d.ts +1 -12
  69. package/dist/react/navigation-state.d.ts +2 -0
  70. package/dist/react/navigation-state.js +9 -0
  71. package/dist/react/navigation.d.ts +33 -0
  72. package/dist/react/navigation.js +64 -0
  73. package/dist/react/root-context.d.ts +14 -0
  74. package/dist/react/root-context.js +10 -0
  75. package/dist/react/route-hooks.d.ts +34 -0
  76. package/dist/react/route-hooks.js +89 -0
  77. package/dist/react/route-types.d.ts +69 -0
  78. package/dist/react/router.d.ts +10 -0
  79. package/dist/react/router.js +22 -0
  80. package/dist/react/runtime.d.ts +1 -1
  81. package/dist/react/runtime.js +1 -1
  82. package/dist/server/application.d.ts +4 -8
  83. package/dist/server/application.js +9 -156
  84. package/dist/server/dispatch.d.ts +5 -0
  85. package/dist/server/dispatch.js +50 -0
  86. package/dist/server/index.d.ts +1 -0
  87. package/dist/server/index.js +2 -1
  88. package/dist/server/preact.js +2 -1
  89. package/dist/server/react.d.ts +10 -0
  90. package/dist/server/react.js +22 -0
  91. package/dist/server/render-application.d.ts +31 -0
  92. package/dist/server/render-application.js +118 -0
  93. package/dist/vite/application/lazy-files.d.ts +1 -1
  94. package/dist/vite/application/lazy-files.js +2 -2
  95. package/dist/vite/application/vite.d.ts +2 -1
  96. package/dist/vite/application/vite.js +12 -5
  97. package/dist/vite/react.d.ts +4 -0
  98. package/dist/vite/react.js +9 -1
  99. package/jsr.json +10 -6
  100. package/package.json +18 -10
  101. package/.generated/core/resource-types.ts +0 -48
  102. package/.generated/core/resources.ts +0 -446
  103. package/.generated/react/data.ts +0 -29
  104. package/.generated/react/resources/components.tsx +0 -416
  105. package/dist/core/resource-types.d.ts +0 -41
  106. package/dist/core/resources.d.ts +0 -21
  107. package/dist/core/resources.js +0 -347
  108. package/dist/react/data.d.ts +0 -9
  109. package/dist/react/data.js +0 -3
  110. package/dist/react/link.js +0 -27
  111. package/dist/react/resources/components.d.ts +0 -76
  112. package/dist/react/resources/components.js +0 -250
@@ -1,274 +1,39 @@
1
1
  /** @jsxImportSource preact */
2
-
3
- import { createDataRouter, matchRoutes, safeWrapAsync } from "../core/index.ts";
4
- import {
5
- type DataResponse,
6
- normalizeResult,
7
- resolveData,
8
- } from "../core/responses.ts";
9
2
  import type { AppRoute, RootModule } from "../preact/route-types.ts";
10
- import { prepareRoutes, RouterApplication } from "../preact/router.tsx";
11
- import { loadRouter } from "./index.ts";
3
+ import { RouterApplication } from "../preact/router.tsx";
12
4
  import type { ComponentChildren } from "preact";
13
5
  import { Suspense } from "preact/compat";
14
6
  import { renderToReadableStream } from "preact-render-to-string/stream";
15
- import { deferredHydration } from "./deferred-hydration.ts";
16
- import { privateResponse } from "./private-response.ts";
17
- import { progressiveDocument } from "./progressive-document.ts";
18
- import { resolveHydration, streamDocument } from "./stream-document.ts";
7
+ import {
8
+ type RenderApplicationOptions,
9
+ renderDocument,
10
+ } from "./render-application.ts";
19
11
 
20
- /** SSR settings for CSP nonce and early deferred-data hydration. */
21
- export interface RenderApplicationOptions {
22
- nonce?: string;
23
- earlyHydration?: boolean;
24
- }
12
+ export { dispatchRoute } from "./dispatch.ts";
13
+ export type { RenderApplicationOptions } from "./render-application.ts";
25
14
 
26
- /** Render a Preact application response with root and route data, streaming deferred content when enabled. */
27
- export async function renderApplication(
15
+ /** Render a Preact application response with root data and optional early deferred hydration. */
16
+ export function renderApplication(
28
17
  request: Request,
29
18
  routes: AppRoute[],
30
19
  rootModule: RootModule,
31
20
  entry: string,
32
21
  options: RenderApplicationOptions = {},
33
22
  ): Promise<Response> {
34
- routes = routes.map((route) => ({
35
- ...route,
36
- path: route.routePath ?? route.path,
37
- }));
38
- const match = matchRoutes(routes, new URL(request.url).pathname);
39
-
40
- if (match && !match.route.default)
41
- return dispatchRoute(request, routes, true);
42
-
43
- let actionResponse: Response | undefined;
44
- let actionData: unknown;
45
-
46
- if (request.method !== "GET" && request.method !== "HEAD") {
47
- actionResponse = await dispatchRoute(request, routes, true);
48
-
49
- if (
50
- actionResponse.headers.has("Location") ||
51
- !actionResponse.headers.get("Content-Type")?.includes("application/json")
52
- )
53
- return actionResponse;
54
-
55
- actionData = await normalizeResult(await actionResponse.json());
56
- }
57
-
58
- const rootResult = safeWrapAsync(() =>
59
- Promise.resolve()
60
- .then(() =>
61
- rootModule.loader?.({ request, params: {}, signal: request.signal }),
62
- )
63
- .then(
64
- (value) => normalizeResult(value, false),
65
- (error: unknown) =>
66
- error instanceof Response
67
- ? normalizeResult(error, false)
68
- : error instanceof Error
69
- ? error
70
- : new Error(String(error), { cause: error }),
71
- ),
72
- );
73
- let redirectResponse: Response | undefined;
74
- const prepared = prepareRoutes(
75
- routes.map((route) => ({
76
- ...route,
77
- ErrorBoundary: route.ErrorBoundary ?? rootModule.ErrorBoundary,
78
- })),
79
- true,
80
- ).map((route) => {
81
- if (!route.loader) return route;
82
-
83
- const loader = route.loader;
84
-
85
- return {
86
- ...route,
87
- loader: async (args: Parameters<typeof loader>[0]) => {
88
- const value = await loader(args);
89
-
90
- if (value instanceof Response && value.headers.has("Location"))
91
- redirectResponse = value;
92
-
93
- return value;
94
- },
95
- };
96
- });
97
- const [rootState, loadedRouter] = await Promise.all([
98
- rootResult,
99
- loadRouter(prepared, request),
100
- ]);
101
- let router = loadedRouter;
102
- const rootError =
103
- rootState.error ??
104
- (rootState.data instanceof Error ? rootState.data : undefined);
105
- const root = rootError ? undefined : rootState.data;
106
-
107
- if (root instanceof Response && root.headers.has("Location")) {
108
- router.dispose();
109
- return root;
110
- }
111
-
112
- if (redirectResponse) {
113
- router.dispose();
114
-
115
- return redirectResponse;
116
- }
117
-
118
- if (actionResponse || rootError) {
119
- const hydrationData = {
120
- ...router.getSnapshot(),
121
- actionData: actionData instanceof Error ? undefined : actionData,
122
- ...(actionData instanceof Error && { error: actionData }),
123
- ...(rootError && { error: rootError }),
124
- };
125
- router.dispose();
126
- router = createDataRouter(router.routes, { hydrationData });
127
- }
128
-
129
- const snapshot = router.getSnapshot();
130
- const hydrationData = {
131
- root,
132
- snapshot: {
133
- ...snapshot,
134
- error: snapshot.error && {
135
- ...snapshot.error,
136
- name: snapshot.error.name,
137
- message: snapshot.error.message,
138
- },
139
- },
140
- ...(rootError && { rootError: true }),
141
- };
142
- const deferred = options.earlyHydration
143
- ? deferredHydration(hydrationData, options.nonce)
144
- : undefined;
145
- const stream = renderToReadableStream(
146
- <RouterApplication
147
- router={router}
148
- routes={routes}
149
- root={root}
150
- rootModule={rootModule}
151
- entry={entry}
152
- rootError={Boolean(rootError)}
153
- streaming
154
- deferredBoundary={options.earlyHydration ? undefined : deferredBoundary}
155
- initialData={deferred?.initial}
156
- document
157
- />,
158
- options,
159
- );
160
- // The renderer reports the same rejection through its stream reader.
161
- void stream.allReady.catch(() => {});
162
- const headers = new Headers(actionResponse?.headers);
163
- headers.delete("Content-Length");
164
- headers.delete("Content-Encoding");
165
- headers.set("Content-Type", "text/html; charset=utf-8");
166
- headers.set("Cache-Control", "no-store");
167
-
168
- return new Response(
169
- deferred
170
- ? progressiveDocument(stream, deferred.stream, {
171
- signal: request.signal,
172
- dispose: () => router.dispose(),
173
- })
174
- : streamDocument(stream, {
175
- hydration: resolveHydration(hydrationData),
176
- signal: request.signal,
177
- nonce: options.nonce,
178
- dispose: () => router.dispose(),
179
- }),
180
- {
181
- status: snapshot.error
182
- ? Number("status" in snapshot.error ? snapshot.error.status : 500)
183
- : (actionResponse?.status ?? 200),
184
- headers,
185
- },
23
+ return renderDocument(request, routes, rootModule, entry, options, (props) =>
24
+ renderToReadableStream(
25
+ <RouterApplication
26
+ {...props}
27
+ rootModule={rootModule}
28
+ deferredBoundary={options.earlyHydration ? undefined : deferredBoundary}
29
+ />,
30
+ options,
31
+ ),
186
32
  );
187
33
  }
188
-
189
34
  function deferredBoundary(
190
35
  children: ComponentChildren,
191
36
  fallback: ComponentChildren,
192
37
  ) {
193
38
  return <Suspense fallback={fallback}>{children}</Suspense>;
194
39
  }
195
-
196
- async function dispatch(
197
- request: Request,
198
- routes: AppRoute[],
199
- document = false,
200
- ): Promise<Response> {
201
- const match = matchRoutes(
202
- routes.map((route) => ({ ...route, path: route.routePath ?? route.path })),
203
- new URL(request.url).pathname,
204
- );
205
-
206
- if (!match) return Response.json({ error: "No route" }, { status: 404 });
207
-
208
- const handler =
209
- request.method === "GET" || request.method === "HEAD"
210
- ? match.route.loader
211
- : match.route.action;
212
-
213
- if (!handler) return Response.json({ error: "No handler" }, { status: 405 });
214
-
215
- const value = await Promise.resolve()
216
- .then(() =>
217
- handler({ request, params: match.params, signal: request.signal }),
218
- )
219
- .then(
220
- (result) => result,
221
- (error: unknown) => error,
222
- );
223
-
224
- if (value instanceof Response) {
225
- const location = value.headers.get("Location");
226
-
227
- if (document) return value;
228
-
229
- if (!location) {
230
- if (value.ok) return value;
231
-
232
- return Response.json({
233
- __routeError: {
234
- message: await value.text(),
235
- status: value.status,
236
- statusText: value.statusText,
237
- },
238
- });
239
- }
240
-
241
- const headers = new Headers(value.headers);
242
- headers.delete("Location");
243
- headers.set("X-Smolter-Redirect", location);
244
-
245
- return new Response(null, { headers });
246
- }
247
-
248
- if (value instanceof Error)
249
- return Response.json({
250
- __routeError: { ...value, message: value.message },
251
- });
252
-
253
- if (value && typeof value === "object" && "__routeData" in value) {
254
- const result = value as DataResponse<unknown>;
255
-
256
- return Response.json(
257
- await resolveData(result.value),
258
- document ? result.init : { headers: result.init.headers },
259
- );
260
- }
261
-
262
- return Response.json((await normalizeResult(value)) ?? null);
263
- }
264
-
265
- /** Dispatch a normalized application URL to its route loader or action and return an HTTP response. */
266
- export async function dispatchRoute(
267
- request: Request,
268
- routes: AppRoute[],
269
- document = false,
270
- ): Promise<Response> {
271
- const response = await dispatch(request, routes, document);
272
-
273
- return document ? response : privateResponse(response);
274
- }
@@ -0,0 +1,87 @@
1
+ import { matchRoutes, type Route } from "../core/index.ts";
2
+ import {
3
+ type DataResponse,
4
+ normalizeResult,
5
+ resolveData,
6
+ } from "../core/responses.ts";
7
+ import { privateResponse } from "./private-response.ts";
8
+
9
+ async function dispatch(
10
+ request: Request,
11
+ routes: readonly (Route & { routePath?: string })[],
12
+ document = false,
13
+ ): Promise<Response> {
14
+ const match = matchRoutes(
15
+ routes.map((route) => ({ ...route, path: route.routePath ?? route.path })),
16
+ new URL(request.url).pathname,
17
+ );
18
+
19
+ if (!match) return Response.json({ error: "No route" }, { status: 404 });
20
+
21
+ const handler =
22
+ request.method === "GET" || request.method === "HEAD"
23
+ ? match.route.loader
24
+ : match.route.action;
25
+
26
+ if (!handler) return Response.json({ error: "No handler" }, { status: 405 });
27
+
28
+ const value = await Promise.resolve()
29
+ .then(() =>
30
+ handler({ request, params: match.params, signal: request.signal }),
31
+ )
32
+ .then(
33
+ (result) => result,
34
+ (error: unknown) => error,
35
+ );
36
+
37
+ if (value instanceof Response) {
38
+ const location = value.headers.get("Location");
39
+
40
+ if (document) return value;
41
+
42
+ if (!location) {
43
+ if (value.ok) return value;
44
+
45
+ return Response.json({
46
+ __routeError: {
47
+ message: await value.text(),
48
+ status: value.status,
49
+ statusText: value.statusText,
50
+ },
51
+ });
52
+ }
53
+
54
+ const headers = new Headers(value.headers);
55
+ headers.delete("Location");
56
+ headers.set("X-Smolter-Redirect", location);
57
+
58
+ return new Response(null, { headers });
59
+ }
60
+
61
+ if (value instanceof Error)
62
+ return Response.json({
63
+ __routeError: { ...value, message: value.message },
64
+ });
65
+
66
+ if (value && typeof value === "object" && "__routeData" in value) {
67
+ const result = value as DataResponse<unknown>;
68
+
69
+ return Response.json(
70
+ await resolveData(result.value),
71
+ document ? result.init : { headers: result.init.headers },
72
+ );
73
+ }
74
+
75
+ return Response.json((await normalizeResult(value)) ?? null);
76
+ }
77
+
78
+ /** Dispatch a normalized application URL to its route loader or action and return an HTTP response. */
79
+ export async function dispatchRoute(
80
+ request: Request,
81
+ routes: readonly (Route & { routePath?: string })[],
82
+ document = false,
83
+ ): Promise<Response> {
84
+ const response = await dispatch(request, routes, document);
85
+
86
+ return document ? response : privateResponse(response);
87
+ }
@@ -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";
@@ -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
  }