@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.
- package/.generated/client/hash-history.ts +27 -0
- package/.generated/client/hydration.ts +98 -0
- package/.generated/client/root-data.ts +52 -0
- package/.generated/core/prepare-routes.ts +50 -0
- package/.generated/core/presentation.ts +1 -1
- package/.generated/preact/client.tsx +1 -26
- package/.generated/preact/deferred-hydration.ts +1 -98
- package/.generated/preact/index.ts +6 -1
- package/.generated/preact/navigation.tsx +13 -5
- package/.generated/preact/root-data.ts +1 -45
- package/.generated/preact/router.tsx +2 -48
- package/.generated/react/client.tsx +130 -0
- package/.generated/react/document.tsx +235 -0
- package/.generated/react/fetcher.ts +29 -7
- package/.generated/react/hooks.ts +20 -18
- package/.generated/react/index.ts +17 -1
- package/.generated/react/lazy-route.ts +116 -0
- package/.generated/react/link.ts +1 -38
- package/.generated/react/navigation-state.ts +13 -0
- package/.generated/react/navigation.ts +145 -0
- package/.generated/react/root-context.ts +22 -0
- package/.generated/react/route-hooks.ts +159 -0
- package/.generated/react/route-types.ts +91 -0
- package/.generated/react/router.ts +50 -0
- package/.generated/react/runtime.ts +1 -0
- package/.generated/server/application.tsx +18 -253
- package/.generated/server/dispatch.ts +87 -0
- package/.generated/server/index.ts +2 -0
- package/.generated/server/react.ts +34 -0
- package/.generated/server/render-application.ts +203 -0
- package/.generated/vite/application/lazy-files.ts +2 -1
- package/.generated/vite/application/vite.ts +12 -4
- package/.generated/vite/react.ts +11 -0
- package/CHANGELOG.md +10 -0
- package/README.md +41 -41
- package/dist/client/hash-history.d.ts +2 -0
- package/dist/client/hash-history.js +26 -0
- package/dist/client/hydration.d.ts +10 -0
- package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
- package/dist/client/root-data.d.ts +14 -0
- package/dist/{preact → client}/root-data.js +1 -1
- package/dist/core/prepare-routes.d.ts +7 -0
- package/dist/core/prepare-routes.js +28 -0
- package/dist/core/presentation.js +1 -1
- package/dist/preact/client.js +5 -26
- package/dist/preact/deferred-hydration.d.ts +1 -10
- package/dist/preact/document.js +2 -2
- package/dist/preact/index.d.ts +1 -1
- package/dist/preact/index.js +1 -1
- package/dist/preact/navigation-state.js +1 -1
- package/dist/preact/navigation.js +7 -5
- package/dist/preact/root-data.d.ts +1 -8
- package/dist/preact/route-hooks.js +1 -1
- package/dist/preact/router.d.ts +2 -2
- package/dist/preact/router.js +2 -26
- package/dist/react/client.d.ts +20 -0
- package/dist/react/client.js +86 -0
- package/dist/react/document.d.ts +25 -0
- package/dist/react/document.js +169 -0
- package/dist/react/fetcher.d.ts +8 -4
- package/dist/react/fetcher.js +12 -1
- package/dist/react/form.js +1 -1
- package/dist/react/hooks.d.ts +7 -6
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +6 -2
- package/dist/react/lazy-route.d.ts +22 -0
- package/dist/react/lazy-route.js +57 -0
- package/dist/react/link.d.ts +1 -12
- package/dist/react/navigation-state.d.ts +2 -0
- package/dist/react/navigation-state.js +9 -0
- package/dist/react/navigation.d.ts +33 -0
- package/dist/react/navigation.js +64 -0
- package/dist/react/root-context.d.ts +14 -0
- package/dist/react/root-context.js +10 -0
- package/dist/react/route-hooks.d.ts +34 -0
- package/dist/react/route-hooks.js +89 -0
- package/dist/react/route-types.d.ts +69 -0
- package/dist/react/router.d.ts +10 -0
- package/dist/react/router.js +22 -0
- package/dist/react/runtime.d.ts +1 -1
- package/dist/react/runtime.js +1 -1
- package/dist/server/application.d.ts +4 -8
- package/dist/server/application.js +9 -156
- package/dist/server/dispatch.d.ts +5 -0
- package/dist/server/dispatch.js +50 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -1
- package/dist/server/preact.js +2 -1
- package/dist/server/react.d.ts +10 -0
- package/dist/server/react.js +22 -0
- package/dist/server/render-application.d.ts +31 -0
- package/dist/server/render-application.js +118 -0
- package/dist/vite/application/lazy-files.d.ts +1 -1
- package/dist/vite/application/lazy-files.js +2 -2
- package/dist/vite/application/vite.d.ts +2 -1
- package/dist/vite/application/vite.js +12 -5
- package/dist/vite/react.d.ts +4 -0
- package/dist/vite/react.js +9 -1
- package/jsr.json +10 -6
- package/package.json +18 -10
- package/.generated/core/resource-types.ts +0 -48
- package/.generated/core/resources.ts +0 -446
- package/.generated/react/data.ts +0 -29
- package/.generated/react/resources/components.tsx +0 -416
- package/dist/core/resource-types.d.ts +0 -41
- package/dist/core/resources.d.ts +0 -21
- package/dist/core/resources.js +0 -347
- package/dist/react/data.d.ts +0 -9
- package/dist/react/data.js +0 -3
- package/dist/react/link.js +0 -27
- package/dist/react/resources/components.d.ts +0 -76
- 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 {
|
|
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 {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
import {
|
|
8
|
+
type RenderApplicationOptions,
|
|
9
|
+
renderDocument,
|
|
10
|
+
} from "./render-application.ts";
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
export
|
|
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
|
|
27
|
-
export
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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: () =>
|
|
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(
|
|
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
|
|
155
|
+
...(splitRoutes ? [lazyFiles(directory, dataEndpoint, framework)] : []),
|
|
156
|
+
routerPlugin({ directory, framework, dataEndpoint }),
|
|
149
157
|
];
|
|
150
158
|
}
|