react-bun-ssr 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -17
- package/framework/cli/commands.ts +104 -246
- package/framework/cli/dev-client-watch.ts +288 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +142 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +134 -13
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +150 -159
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/config.ts +7 -2
- package/framework/runtime/index.ts +1 -1
- package/framework/runtime/link.tsx +3 -11
- package/framework/runtime/markdown-routes.ts +1 -14
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +75 -25
- package/framework/runtime/render.tsx +150 -22
- package/framework/runtime/route-api.ts +1 -1
- package/framework/runtime/router.ts +75 -4
- package/framework/runtime/server.ts +57 -106
- package/framework/runtime/tree.tsx +24 -2
- package/framework/runtime/types.ts +13 -2
- package/framework/runtime/utils.ts +3 -0
- package/package.json +13 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Children,
|
|
3
3
|
cloneElement,
|
|
4
|
+
Fragment,
|
|
4
5
|
isValidElement,
|
|
5
6
|
Suspense,
|
|
6
7
|
use,
|
|
@@ -29,6 +30,50 @@ import {
|
|
|
29
30
|
createPageAppTree,
|
|
30
31
|
} from "./tree";
|
|
31
32
|
|
|
33
|
+
function isTitleElement(node: ReactNode): node is ReactElement {
|
|
34
|
+
return isValidElement(node) && node.type === "title";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isMetaElement(node: ReactNode): node is ReactElement {
|
|
38
|
+
return isValidElement(node) && node.type === "meta";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getMetaDedupeKey(node: ReactNode): string | null {
|
|
42
|
+
if (!isMetaElement(node)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = node.props as {
|
|
47
|
+
name?: string;
|
|
48
|
+
property?: string;
|
|
49
|
+
httpEquiv?: string;
|
|
50
|
+
charSet?: string;
|
|
51
|
+
itemProp?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (typeof props.name === "string" && props.name.length > 0) {
|
|
55
|
+
return `name:${props.name}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof props.property === "string" && props.property.length > 0) {
|
|
59
|
+
return `property:${props.property}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof props.httpEquiv === "string" && props.httpEquiv.length > 0) {
|
|
63
|
+
return `httpEquiv:${props.httpEquiv}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof props.itemProp === "string" && props.itemProp.length > 0) {
|
|
67
|
+
return `itemProp:${props.itemProp}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (props.charSet !== undefined) {
|
|
71
|
+
return "charSet";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
32
77
|
export function renderPageApp(modules: RouteModuleBundle, payload: RenderPayload): string {
|
|
33
78
|
return renderToString(createPageAppTree(modules, payload));
|
|
34
79
|
}
|
|
@@ -84,6 +129,27 @@ function normalizeTitleChildren(node: ReactNode): ReactNode {
|
|
|
84
129
|
return cloneElement(node, undefined, nextChildren);
|
|
85
130
|
}
|
|
86
131
|
|
|
132
|
+
function expandHeadNodes(node: ReactNode): ReactNode[] {
|
|
133
|
+
if (Array.isArray(node)) {
|
|
134
|
+
return node.flatMap(value => expandHeadNodes(value));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (node === null || node === undefined || typeof node === "boolean") {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!isValidElement(node)) {
|
|
142
|
+
return [node];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (node.type === Fragment) {
|
|
146
|
+
const props = node.props as { children?: ReactNode };
|
|
147
|
+
return Children.toArray(props.children).flatMap(child => expandHeadNodes(child));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [node];
|
|
151
|
+
}
|
|
152
|
+
|
|
87
153
|
function moduleHeadToElements(moduleValue: RouteModule, payload: RenderPayload, keyPrefix: string): ReactNode[] {
|
|
88
154
|
const tags: ReactNode[] = [];
|
|
89
155
|
|
|
@@ -99,7 +165,7 @@ function moduleHeadToElements(moduleValue: RouteModule, payload: RenderPayload,
|
|
|
99
165
|
if (typeof headResult === "string") {
|
|
100
166
|
tags.push(<title key={`${keyPrefix}:title`}>{headResult}</title>);
|
|
101
167
|
} else if (headResult !== null && headResult !== undefined) {
|
|
102
|
-
const nodes =
|
|
168
|
+
const nodes = expandHeadNodes(normalizeTitleChildren(headResult));
|
|
103
169
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
104
170
|
const node = nodes[index]!;
|
|
105
171
|
if (isValidElement(node)) {
|
|
@@ -122,11 +188,37 @@ function moduleHeadToElements(moduleValue: RouteModule, payload: RenderPayload,
|
|
|
122
188
|
}
|
|
123
189
|
|
|
124
190
|
export function collectHeadElements(modules: RouteModuleBundle, payload: RenderPayload): ReactNode[] {
|
|
125
|
-
|
|
191
|
+
const elements = [
|
|
126
192
|
...moduleHeadToElements(modules.root, payload, "root"),
|
|
127
193
|
...modules.layouts.flatMap((layout, index) => moduleHeadToElements(layout, payload, `layout:${index}`)),
|
|
128
194
|
...moduleHeadToElements(modules.route, payload, "route"),
|
|
129
195
|
];
|
|
196
|
+
|
|
197
|
+
let lastTitleIndex = -1;
|
|
198
|
+
const lastMetaIndexes = new Map<string, number>();
|
|
199
|
+
for (let index = 0; index < elements.length; index += 1) {
|
|
200
|
+
if (isTitleElement(elements[index])) {
|
|
201
|
+
lastTitleIndex = index;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const metaKey = getMetaDedupeKey(elements[index]);
|
|
205
|
+
if (metaKey) {
|
|
206
|
+
lastMetaIndexes.set(metaKey, index);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return elements.filter((element, index) => {
|
|
211
|
+
if (isTitleElement(element)) {
|
|
212
|
+
return lastTitleIndex === -1 || index === lastTitleIndex;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const metaKey = getMetaDedupeKey(element);
|
|
216
|
+
if (metaKey) {
|
|
217
|
+
return lastMetaIndexes.get(metaKey) === index;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
130
222
|
}
|
|
131
223
|
|
|
132
224
|
export function collectHeadMarkup(modules: RouteModuleBundle, payload: RenderPayload): string {
|
|
@@ -135,7 +227,7 @@ export function collectHeadMarkup(modules: RouteModuleBundle, payload: RenderPay
|
|
|
135
227
|
}
|
|
136
228
|
|
|
137
229
|
function buildDevReloadClientScript(version: number): string {
|
|
138
|
-
return `(() => {\n
|
|
230
|
+
return `(() => {\n let currentVersion = ${version};\n let closed = false;\n let socket;\n\n const connect = () => {\n socket = new WebSocket(\`\${location.protocol === 'https:' ? 'wss' : 'ws'}://\${location.host}/__rbssr/ws\`);\n\n socket.addEventListener('message', event => {\n try {\n const payload = JSON.parse(String(event.data));\n const nextVersion = Number(payload?.token);\n if (Number.isFinite(nextVersion) && nextVersion > currentVersion) {\n currentVersion = nextVersion;\n location.reload();\n }\n } catch {\n // ignore malformed dev reload payloads\n }\n });\n\n socket.addEventListener('close', () => {\n if (!closed) {\n setTimeout(connect, 150);\n }\n });\n };\n\n connect();\n\n window.addEventListener('beforeunload', () => {\n closed = true;\n socket?.close();\n });\n})();`;
|
|
139
231
|
}
|
|
140
232
|
|
|
141
233
|
function buildDeferredBootstrapScript(): string {
|
|
@@ -154,6 +246,34 @@ function createVersionedCssHrefs(assets: HydrationDocumentAssets): string[] {
|
|
|
154
246
|
return assets.css.map(href => withVersionQuery(href, assets.devVersion));
|
|
155
247
|
}
|
|
156
248
|
|
|
249
|
+
interface DocumentRenderState {
|
|
250
|
+
cssHrefs: string[];
|
|
251
|
+
deferredBootstrapScript: string;
|
|
252
|
+
devReloadScript?: string;
|
|
253
|
+
payloadJson: string;
|
|
254
|
+
routerSnapshotJson: string;
|
|
255
|
+
versionedScript?: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createDocumentRenderState(options: {
|
|
259
|
+
assets: HydrationDocumentAssets;
|
|
260
|
+
payload: RenderPayload;
|
|
261
|
+
routerSnapshot: ClientRouterSnapshot;
|
|
262
|
+
}): DocumentRenderState {
|
|
263
|
+
return {
|
|
264
|
+
cssHrefs: createVersionedCssHrefs(options.assets),
|
|
265
|
+
deferredBootstrapScript: buildDeferredBootstrapScript(),
|
|
266
|
+
devReloadScript: typeof options.assets.devVersion === "number"
|
|
267
|
+
? buildDevReloadClientScript(options.assets.devVersion)
|
|
268
|
+
: undefined,
|
|
269
|
+
payloadJson: safeJsonSerialize(options.payload),
|
|
270
|
+
routerSnapshotJson: safeJsonSerialize(options.routerSnapshot),
|
|
271
|
+
versionedScript: options.assets.script
|
|
272
|
+
? withVersionQuery(options.assets.script, options.assets.devVersion)
|
|
273
|
+
: undefined,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
157
277
|
export function createManagedHeadMarkup(options: {
|
|
158
278
|
headMarkup: string;
|
|
159
279
|
assets: HydrationDocumentAssets;
|
|
@@ -190,9 +310,13 @@ function HtmlDocument(options: {
|
|
|
190
310
|
routerSnapshot: ClientRouterSnapshot;
|
|
191
311
|
deferredSettleEntries: DeferredSettleEntry[];
|
|
192
312
|
}): ReactElement {
|
|
193
|
-
const { appTree,
|
|
194
|
-
const
|
|
195
|
-
|
|
313
|
+
const { appTree, managedHeadElements, deferredSettleEntries } = options;
|
|
314
|
+
const documentState = createDocumentRenderState({
|
|
315
|
+
assets: options.assets,
|
|
316
|
+
payload: options.payload,
|
|
317
|
+
routerSnapshot: options.routerSnapshot,
|
|
318
|
+
});
|
|
319
|
+
const cssLinks = documentState.cssHrefs.map((versionedHref, index) => {
|
|
196
320
|
return <link key={`css:${index}:${versionedHref}`} rel="stylesheet" href={versionedHref} />;
|
|
197
321
|
});
|
|
198
322
|
|
|
@@ -210,24 +334,24 @@ function HtmlDocument(options: {
|
|
|
210
334
|
<div id="rbssr-root">{appTree}</div>
|
|
211
335
|
<script
|
|
212
336
|
dangerouslySetInnerHTML={{
|
|
213
|
-
__html:
|
|
337
|
+
__html: documentState.deferredBootstrapScript,
|
|
214
338
|
}}
|
|
215
339
|
/>
|
|
216
340
|
<script
|
|
217
341
|
id={RBSSR_PAYLOAD_SCRIPT_ID}
|
|
218
342
|
type="application/json"
|
|
219
|
-
dangerouslySetInnerHTML={{ __html:
|
|
343
|
+
dangerouslySetInnerHTML={{ __html: documentState.payloadJson }}
|
|
220
344
|
/>
|
|
221
345
|
<script
|
|
222
346
|
id={RBSSR_ROUTER_SCRIPT_ID}
|
|
223
347
|
type="application/json"
|
|
224
|
-
dangerouslySetInnerHTML={{ __html:
|
|
348
|
+
dangerouslySetInnerHTML={{ __html: documentState.routerSnapshotJson }}
|
|
225
349
|
/>
|
|
226
|
-
{versionedScript ? <script type="module" src={versionedScript} /> : null}
|
|
227
|
-
{
|
|
350
|
+
{documentState.versionedScript ? <script type="module" src={documentState.versionedScript} /> : null}
|
|
351
|
+
{documentState.devReloadScript ? (
|
|
228
352
|
<script
|
|
229
353
|
dangerouslySetInnerHTML={{
|
|
230
|
-
__html:
|
|
354
|
+
__html: documentState.devReloadScript,
|
|
231
355
|
}}
|
|
232
356
|
/>
|
|
233
357
|
) : null}
|
|
@@ -296,22 +420,26 @@ export function renderDocument(options: {
|
|
|
296
420
|
headMarkup: string;
|
|
297
421
|
routerSnapshot: ClientRouterSnapshot;
|
|
298
422
|
}): string {
|
|
299
|
-
const { appMarkup,
|
|
300
|
-
const
|
|
423
|
+
const { appMarkup, headMarkup } = options;
|
|
424
|
+
const documentState = createDocumentRenderState({
|
|
425
|
+
assets: options.assets,
|
|
426
|
+
payload: options.payload,
|
|
427
|
+
routerSnapshot: options.routerSnapshot,
|
|
428
|
+
});
|
|
301
429
|
const managedHeadMarkup = createManagedHeadMarkup({
|
|
302
430
|
headMarkup,
|
|
303
|
-
assets,
|
|
431
|
+
assets: options.assets,
|
|
304
432
|
});
|
|
305
433
|
|
|
306
|
-
const payloadScript = `<script id="${RBSSR_PAYLOAD_SCRIPT_ID}" type="application/json">${
|
|
307
|
-
const routerScript = `<script id="${RBSSR_ROUTER_SCRIPT_ID}" type="application/json">${
|
|
308
|
-
const entryScript = versionedScript
|
|
309
|
-
? `<script type="module" src="${Bun.escapeHTML(versionedScript)}"></script>`
|
|
434
|
+
const payloadScript = `<script id="${RBSSR_PAYLOAD_SCRIPT_ID}" type="application/json">${documentState.payloadJson}</script>`;
|
|
435
|
+
const routerScript = `<script id="${RBSSR_ROUTER_SCRIPT_ID}" type="application/json">${documentState.routerSnapshotJson}</script>`;
|
|
436
|
+
const entryScript = documentState.versionedScript
|
|
437
|
+
? `<script type="module" src="${Bun.escapeHTML(documentState.versionedScript)}"></script>`
|
|
310
438
|
: "";
|
|
311
|
-
const devScript =
|
|
312
|
-
? `<script>${
|
|
439
|
+
const devScript = documentState.devReloadScript
|
|
440
|
+
? `<script>${documentState.devReloadScript}</script>`
|
|
313
441
|
: "";
|
|
314
|
-
const deferredBootstrapScript = `<script>${
|
|
442
|
+
const deferredBootstrapScript = `<script>${documentState.deferredBootstrapScript}</script>`;
|
|
315
443
|
|
|
316
444
|
return `<!doctype html>
|
|
317
445
|
<html lang="en">
|
|
@@ -19,5 +19,5 @@ export type {
|
|
|
19
19
|
export { defer, json, redirect } from "./helpers";
|
|
20
20
|
export { isRouteErrorResponse, notFound, routeError } from "./route-errors";
|
|
21
21
|
export { Link, type LinkProps } from "./link";
|
|
22
|
-
export { useRouter, type Router, type RouterNavigateOptions } from "./router";
|
|
22
|
+
export { useRouter, type Router, type RouterNavigateInfo, type RouterNavigateListener, type RouterNavigateOptions } from "./router";
|
|
23
23
|
export { Outlet, useLoaderData, useParams, useRequestUrl, useRouteError } from "./tree";
|
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { goBack, goForward, reloadPage } from "./navigation-api";
|
|
3
3
|
|
|
4
4
|
export interface RouterNavigateOptions {
|
|
5
5
|
scroll?: boolean;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface RouterNavigateInfo {
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
nextUrl: URL;
|
|
12
|
+
status: number;
|
|
13
|
+
kind: "page" | "not_found" | "catch" | "error";
|
|
14
|
+
redirected: boolean;
|
|
15
|
+
prefetched: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RouterNavigateListener = (nextUrl: URL) => void;
|
|
19
|
+
|
|
20
|
+
export function notifyRouterNavigateListeners(
|
|
21
|
+
listeners: readonly RouterNavigateListener[],
|
|
22
|
+
nextUrl: URL,
|
|
23
|
+
): void {
|
|
24
|
+
for (const listener of listeners) {
|
|
25
|
+
try {
|
|
26
|
+
listener(nextUrl);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.warn("[rbssr] router onNavigate listener failed", error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
export interface Router {
|
|
9
35
|
push(href: string, options?: RouterNavigateOptions): void;
|
|
10
36
|
replace(href: string, options?: RouterNavigateOptions): void;
|
|
@@ -12,6 +38,7 @@ export interface Router {
|
|
|
12
38
|
back(): void;
|
|
13
39
|
forward(): void;
|
|
14
40
|
refresh(): void;
|
|
41
|
+
onNavigate(listener: RouterNavigateListener): void;
|
|
15
42
|
}
|
|
16
43
|
|
|
17
44
|
function toAbsoluteHref(href: string): string {
|
|
@@ -28,9 +55,10 @@ const SERVER_ROUTER: Router = {
|
|
|
28
55
|
back: () => undefined,
|
|
29
56
|
forward: () => undefined,
|
|
30
57
|
refresh: () => undefined,
|
|
58
|
+
onNavigate: () => undefined,
|
|
31
59
|
};
|
|
32
60
|
|
|
33
|
-
function createClientRouter(): Router {
|
|
61
|
+
function createClientRouter(onNavigate: Router["onNavigate"]): Router {
|
|
34
62
|
return {
|
|
35
63
|
push: (href, options) => {
|
|
36
64
|
const absoluteHref = toAbsoluteHref(href);
|
|
@@ -69,12 +97,55 @@ function createClientRouter(): Router {
|
|
|
69
97
|
refresh: () => {
|
|
70
98
|
reloadPage();
|
|
71
99
|
},
|
|
100
|
+
onNavigate,
|
|
72
101
|
};
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
export function useRouter(): Router {
|
|
105
|
+
const navigateListenersRef = useRef<RouterNavigateListener[]>([]);
|
|
106
|
+
const didEmitInitialNavigationRef = useRef(false);
|
|
107
|
+
navigateListenersRef.current = [];
|
|
108
|
+
|
|
109
|
+
const onNavigate = useCallback<Router["onNavigate"]>((listener) => {
|
|
110
|
+
navigateListenersRef.current.push(listener);
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (typeof window === "undefined") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!didEmitInitialNavigationRef.current) {
|
|
119
|
+
didEmitInitialNavigationRef.current = true;
|
|
120
|
+
notifyRouterNavigateListeners(
|
|
121
|
+
navigateListenersRef.current,
|
|
122
|
+
new URL(window.location.href),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let unsubscribe: () => void = () => undefined;
|
|
127
|
+
let active = true;
|
|
128
|
+
|
|
129
|
+
void import("./client-runtime")
|
|
130
|
+
.then(runtime => {
|
|
131
|
+
if (!active) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
unsubscribe = runtime.subscribeToNavigation((info) => {
|
|
136
|
+
notifyRouterNavigateListeners(navigateListenersRef.current, info.nextUrl);
|
|
137
|
+
});
|
|
138
|
+
})
|
|
139
|
+
.catch(() => undefined);
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
active = false;
|
|
143
|
+
unsubscribe();
|
|
144
|
+
};
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
76
147
|
return useMemo(
|
|
77
|
-
() => (typeof window === "undefined" ? SERVER_ROUTER : createClientRouter()),
|
|
78
|
-
[],
|
|
148
|
+
() => (typeof window === "undefined" ? SERVER_ROUTER : createClientRouter(onNavigate)),
|
|
149
|
+
[onNavigate],
|
|
79
150
|
);
|
|
80
151
|
}
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
ServerRuntimeOptions,
|
|
29
29
|
TransitionChunk,
|
|
30
30
|
TransitionDeferredChunk,
|
|
31
|
+
TransitionDocumentChunk,
|
|
31
32
|
TransitionInitialChunk,
|
|
32
33
|
TransitionRedirectChunk,
|
|
33
34
|
} from "./types";
|
|
@@ -226,6 +227,11 @@ function applyConfiguredHeaders(options: {
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
for (const [name, value] of Object.entries(rule.headers)) {
|
|
230
|
+
if (value === null) {
|
|
231
|
+
headers.delete(name);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
229
235
|
headers.set(name, value);
|
|
230
236
|
}
|
|
231
237
|
}
|
|
@@ -330,6 +336,7 @@ async function loadRootOnlyModule(
|
|
|
330
336
|
options: {
|
|
331
337
|
cacheBustKey?: string;
|
|
332
338
|
serverBytecode: boolean;
|
|
339
|
+
devSourceImports?: boolean;
|
|
333
340
|
},
|
|
334
341
|
): Promise<RouteModule> {
|
|
335
342
|
return loadRouteModule(rootModulePath, options);
|
|
@@ -436,17 +443,25 @@ function toRedirectChunk(location: string, status: number): TransitionRedirectCh
|
|
|
436
443
|
};
|
|
437
444
|
}
|
|
438
445
|
|
|
446
|
+
function toDocumentChunk(location: string, status: number): TransitionDocumentChunk {
|
|
447
|
+
return {
|
|
448
|
+
type: "document",
|
|
449
|
+
location,
|
|
450
|
+
status,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
439
454
|
function createTransitionStream(options: {
|
|
440
455
|
initialChunk?: TransitionInitialChunk;
|
|
441
|
-
|
|
456
|
+
controlChunk?: TransitionRedirectChunk | TransitionDocumentChunk;
|
|
442
457
|
deferredSettleEntries?: DeferredSettleEntry[];
|
|
443
458
|
sanitizeDeferredError: (message: string) => string;
|
|
444
459
|
}): ReadableStream<Uint8Array> {
|
|
445
460
|
return new ReadableStream<Uint8Array>({
|
|
446
461
|
async start(controller) {
|
|
447
462
|
try {
|
|
448
|
-
if (options.
|
|
449
|
-
controller.enqueue(toTransitionChunkLine(options.
|
|
463
|
+
if (options.controlChunk) {
|
|
464
|
+
controller.enqueue(toTransitionChunkLine(options.controlChunk));
|
|
450
465
|
controller.close();
|
|
451
466
|
return;
|
|
452
467
|
}
|
|
@@ -489,74 +504,6 @@ function createTransitionStream(options: {
|
|
|
489
504
|
});
|
|
490
505
|
}
|
|
491
506
|
|
|
492
|
-
function createDevReloadEventStream(options: {
|
|
493
|
-
getVersion: () => number;
|
|
494
|
-
subscribe?: (listener: (version: number) => void) => (() => void) | void;
|
|
495
|
-
}): Response {
|
|
496
|
-
const encoder = new TextEncoder();
|
|
497
|
-
let interval: ReturnType<typeof setInterval> | undefined;
|
|
498
|
-
let unsubscribe: (() => void) | void;
|
|
499
|
-
let cleanup: (() => void) | undefined;
|
|
500
|
-
|
|
501
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
502
|
-
start(controller) {
|
|
503
|
-
let closed = false;
|
|
504
|
-
|
|
505
|
-
cleanup = (): void => {
|
|
506
|
-
if (closed) {
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
closed = true;
|
|
510
|
-
if (interval) {
|
|
511
|
-
clearInterval(interval);
|
|
512
|
-
interval = undefined;
|
|
513
|
-
}
|
|
514
|
-
if (typeof unsubscribe === "function") {
|
|
515
|
-
unsubscribe();
|
|
516
|
-
unsubscribe = undefined;
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
const sendChunk = (chunk: string): void => {
|
|
521
|
-
if (closed) {
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
try {
|
|
525
|
-
controller.enqueue(encoder.encode(chunk));
|
|
526
|
-
} catch {
|
|
527
|
-
cleanup?.();
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
const sendReload = (version: number): void => {
|
|
532
|
-
sendChunk(`event: reload\ndata: ${version}\n\n`);
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
sendChunk(": connected\n\n");
|
|
536
|
-
sendReload(options.getVersion());
|
|
537
|
-
|
|
538
|
-
unsubscribe = options.subscribe?.(nextVersion => {
|
|
539
|
-
sendReload(nextVersion);
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
interval = setInterval(() => {
|
|
543
|
-
sendChunk(": ping\n\n");
|
|
544
|
-
}, 15_000);
|
|
545
|
-
},
|
|
546
|
-
cancel() {
|
|
547
|
-
cleanup?.();
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
return new Response(stream, {
|
|
552
|
-
headers: {
|
|
553
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
554
|
-
"cache-control": "no-cache, no-transform",
|
|
555
|
-
connection: "keep-alive",
|
|
556
|
-
},
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
|
|
560
507
|
export function createServer(
|
|
561
508
|
config: FrameworkConfig = {},
|
|
562
509
|
runtimeOptions: ServerRuntimeOptions = {},
|
|
@@ -569,8 +516,10 @@ export function createServer(
|
|
|
569
516
|
const pendingAdapterCache = new Map<string, Promise<BunRouteAdapter>>();
|
|
570
517
|
|
|
571
518
|
const getAdapterKey = (activeConfig: ResolvedConfig): string => {
|
|
572
|
-
const
|
|
573
|
-
|
|
519
|
+
const routeVersion = dev
|
|
520
|
+
? runtimeOptions.routeManifestVersion?.() ?? runtimeOptions.reloadVersion?.() ?? 0
|
|
521
|
+
: 0;
|
|
522
|
+
return `${normalizeSlashes(activeConfig.routesDir)}|${dev ? "dev" : "prod"}|${routeVersion}`;
|
|
574
523
|
};
|
|
575
524
|
|
|
576
525
|
const trimAdapterCache = (): void => {
|
|
@@ -601,10 +550,9 @@ export function createServer(
|
|
|
601
550
|
return pending;
|
|
602
551
|
}
|
|
603
552
|
|
|
604
|
-
const reloadVersion = dev ? runtimeOptions.reloadVersion?.() ?? 0 : 0;
|
|
605
553
|
const routesHash = stableHash(normalizeSlashes(activeConfig.routesDir));
|
|
606
554
|
const projectionRootDir = dev
|
|
607
|
-
? path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", `dev-${routesHash}
|
|
555
|
+
? path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", `dev-${routesHash}`)
|
|
608
556
|
: path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", "prod", routesHash);
|
|
609
557
|
|
|
610
558
|
const buildAdapterPromise = createBunRouteAdapter({
|
|
@@ -626,8 +574,6 @@ export function createServer(
|
|
|
626
574
|
};
|
|
627
575
|
|
|
628
576
|
const fetchHandler = async (request: Request): Promise<Response> => {
|
|
629
|
-
await runtimeOptions.onBeforeRequest?.();
|
|
630
|
-
|
|
631
577
|
const runtimePaths = runtimeOptions.resolvePaths?.() ?? {};
|
|
632
578
|
const activeConfig: ResolvedConfig = {
|
|
633
579
|
...resolvedConfig,
|
|
@@ -647,13 +593,6 @@ export function createServer(
|
|
|
647
593
|
});
|
|
648
594
|
};
|
|
649
595
|
|
|
650
|
-
if (dev && url.pathname === "/__rbssr/events") {
|
|
651
|
-
return finalize(createDevReloadEventStream({
|
|
652
|
-
getVersion: () => runtimeOptions.reloadVersion?.() ?? 0,
|
|
653
|
-
subscribe: runtimeOptions.subscribeReload,
|
|
654
|
-
}), "internal-dev");
|
|
655
|
-
}
|
|
656
|
-
|
|
657
596
|
if (dev && url.pathname === "/__rbssr/version") {
|
|
658
597
|
const version = runtimeOptions.reloadVersion?.() ?? 0;
|
|
659
598
|
return finalize(new Response(String(version), {
|
|
@@ -692,8 +631,23 @@ export function createServer(
|
|
|
692
631
|
return finalize(publicResponse, "static");
|
|
693
632
|
}
|
|
694
633
|
|
|
634
|
+
await runtimeOptions.onBeforeRequest?.();
|
|
635
|
+
|
|
695
636
|
const routeAdapter = await getRouteAdapter(activeConfig);
|
|
696
|
-
const
|
|
637
|
+
const devCacheBustKey = dev ? String(runtimeOptions.reloadVersion?.() ?? 0) : undefined;
|
|
638
|
+
const nodeEnv: "development" | "production" = dev ? "development" : "production";
|
|
639
|
+
const routeModuleLoadOptions = {
|
|
640
|
+
cacheBustKey: devCacheBustKey,
|
|
641
|
+
serverBytecode: activeConfig.serverBytecode,
|
|
642
|
+
devSourceImports: false,
|
|
643
|
+
nodeEnv,
|
|
644
|
+
};
|
|
645
|
+
const requestModuleLoadOptions = {
|
|
646
|
+
cacheBustKey: undefined,
|
|
647
|
+
serverBytecode: activeConfig.serverBytecode,
|
|
648
|
+
devSourceImports: dev,
|
|
649
|
+
nodeEnv,
|
|
650
|
+
};
|
|
697
651
|
const routeAssetsById = resolveAllRouteAssets({
|
|
698
652
|
dev,
|
|
699
653
|
runtimeOptions,
|
|
@@ -728,8 +682,7 @@ export function createServer(
|
|
|
728
682
|
const transitionPageMatch = routeAdapter.matchPage(targetUrl.pathname);
|
|
729
683
|
if (!transitionPageMatch) {
|
|
730
684
|
const rootModule = await loadRootOnlyModule(activeConfig.rootModule, {
|
|
731
|
-
|
|
732
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
685
|
+
...routeModuleLoadOptions,
|
|
733
686
|
});
|
|
734
687
|
const fallbackRoute: RouteModule = {
|
|
735
688
|
default: () => null,
|
|
@@ -769,11 +722,10 @@ export function createServer(
|
|
|
769
722
|
rootFilePath: activeConfig.rootModule,
|
|
770
723
|
layoutFiles: transitionPageMatch.route.layoutFiles,
|
|
771
724
|
routeFilePath: transitionPageMatch.route.filePath,
|
|
772
|
-
|
|
773
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
725
|
+
...routeModuleLoadOptions,
|
|
774
726
|
}),
|
|
775
|
-
loadGlobalMiddleware(activeConfig.middlewareFile,
|
|
776
|
-
loadNestedMiddleware(transitionPageMatch.route.middlewareFiles,
|
|
727
|
+
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
728
|
+
loadNestedMiddleware(transitionPageMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
777
729
|
]);
|
|
778
730
|
const moduleMiddleware = extractRouteMiddleware(routeModules.route);
|
|
779
731
|
const routeAssets = routeAssetsById[transitionPageMatch.route.id] ?? null;
|
|
@@ -862,7 +814,7 @@ export function createServer(
|
|
|
862
814
|
const location = redirectResponse.headers.get("location");
|
|
863
815
|
if (location) {
|
|
864
816
|
const stream = createTransitionStream({
|
|
865
|
-
|
|
817
|
+
controlChunk: toRedirectChunk(location, redirectResponse.status),
|
|
866
818
|
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
867
819
|
});
|
|
868
820
|
return finalize(toTransitionStreamResponse(stream, redirectResponse.headers), "internal-transition");
|
|
@@ -952,17 +904,18 @@ export function createServer(
|
|
|
952
904
|
const redirectLocation = middlewareResponse.headers.get("location");
|
|
953
905
|
if (redirectLocation && isRedirectStatus(middlewareResponse.status)) {
|
|
954
906
|
const stream = createTransitionStream({
|
|
955
|
-
|
|
907
|
+
controlChunk: toRedirectChunk(redirectLocation, middlewareResponse.status),
|
|
956
908
|
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
957
909
|
});
|
|
958
910
|
return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
|
|
959
911
|
}
|
|
960
912
|
|
|
961
913
|
if (!transitionInitialChunk) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
);
|
|
914
|
+
const stream = createTransitionStream({
|
|
915
|
+
controlChunk: toDocumentChunk(targetUrl.toString(), middlewareResponse.status),
|
|
916
|
+
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
917
|
+
});
|
|
918
|
+
return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
|
|
966
919
|
}
|
|
967
920
|
|
|
968
921
|
const stream = createTransitionStream({
|
|
@@ -975,7 +928,7 @@ export function createServer(
|
|
|
975
928
|
|
|
976
929
|
const apiMatch = routeAdapter.matchApi(url.pathname);
|
|
977
930
|
if (apiMatch) {
|
|
978
|
-
const apiModule = await loadApiRouteModule(apiMatch.route.filePath,
|
|
931
|
+
const apiModule = await loadApiRouteModule(apiMatch.route.filePath, requestModuleLoadOptions);
|
|
979
932
|
const methodHandler = getMethodHandler(apiModule as Record<string, unknown>, request.method);
|
|
980
933
|
|
|
981
934
|
if (typeof methodHandler !== "function") {
|
|
@@ -997,8 +950,8 @@ export function createServer(
|
|
|
997
950
|
};
|
|
998
951
|
|
|
999
952
|
const [globalMiddleware, routeMiddleware] = await Promise.all([
|
|
1000
|
-
loadGlobalMiddleware(activeConfig.middlewareFile,
|
|
1001
|
-
loadNestedMiddleware(apiMatch.route.middlewareFiles,
|
|
953
|
+
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
954
|
+
loadNestedMiddleware(apiMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
1002
955
|
]);
|
|
1003
956
|
const allMiddleware = [...globalMiddleware, ...routeMiddleware];
|
|
1004
957
|
let apiPhase: RouteErrorPhase = "middleware";
|
|
@@ -1062,8 +1015,7 @@ export function createServer(
|
|
|
1062
1015
|
|
|
1063
1016
|
if (!pageMatch) {
|
|
1064
1017
|
const rootModule = await loadRootOnlyModule(activeConfig.rootModule, {
|
|
1065
|
-
|
|
1066
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
1018
|
+
...routeModuleLoadOptions,
|
|
1067
1019
|
});
|
|
1068
1020
|
const fallbackRoute: RouteModule = {
|
|
1069
1021
|
default: () => null,
|
|
@@ -1105,11 +1057,10 @@ export function createServer(
|
|
|
1105
1057
|
rootFilePath: activeConfig.rootModule,
|
|
1106
1058
|
layoutFiles: pageMatch.route.layoutFiles,
|
|
1107
1059
|
routeFilePath: pageMatch.route.filePath,
|
|
1108
|
-
|
|
1109
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
1060
|
+
...routeModuleLoadOptions,
|
|
1110
1061
|
}),
|
|
1111
|
-
loadGlobalMiddleware(activeConfig.middlewareFile,
|
|
1112
|
-
loadNestedMiddleware(pageMatch.route.middlewareFiles,
|
|
1062
|
+
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
1063
|
+
loadNestedMiddleware(pageMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
1113
1064
|
]);
|
|
1114
1065
|
const moduleMiddleware = extractRouteMiddleware(routeModules.route);
|
|
1115
1066
|
|