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.
@@ -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 = Children.toArray(normalizeTitleChildren(headResult));
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
- return [
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 const currentVersion = ${version};\n const source = new EventSource('/__rbssr/events');\n\n source.addEventListener('reload', event => {\n const nextVersion = Number(event.data);\n if (Number.isFinite(nextVersion) && nextVersion > currentVersion) {\n location.reload();\n }\n });\n\n window.addEventListener('beforeunload', () => {\n source.close();\n });\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, payload, assets, managedHeadElements, deferredSettleEntries } = options;
194
- const versionedScript = assets.script ? withVersionQuery(assets.script, assets.devVersion) : undefined;
195
- const cssLinks = createVersionedCssHrefs(assets).map((versionedHref, index) => {
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: buildDeferredBootstrapScript(),
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: safeJsonSerialize(payload) }}
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: safeJsonSerialize(options.routerSnapshot) }}
348
+ dangerouslySetInnerHTML={{ __html: documentState.routerSnapshotJson }}
225
349
  />
226
- {versionedScript ? <script type="module" src={versionedScript} /> : null}
227
- {typeof assets.devVersion === "number" ? (
350
+ {documentState.versionedScript ? <script type="module" src={documentState.versionedScript} /> : null}
351
+ {documentState.devReloadScript ? (
228
352
  <script
229
353
  dangerouslySetInnerHTML={{
230
- __html: buildDevReloadClientScript(assets.devVersion),
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, payload, assets, headMarkup, routerSnapshot } = options;
300
- const versionedScript = assets.script ? withVersionQuery(assets.script, assets.devVersion) : undefined;
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">${safeJsonSerialize(payload)}</script>`;
307
- const routerScript = `<script id="${RBSSR_ROUTER_SCRIPT_ID}" type="application/json">${safeJsonSerialize(routerSnapshot)}</script>`;
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 = typeof assets.devVersion === "number"
312
- ? `<script>${buildDevReloadClientScript(assets.devVersion)}</script>`
439
+ const devScript = documentState.devReloadScript
440
+ ? `<script>${documentState.devReloadScript}</script>`
313
441
  : "";
314
- const deferredBootstrapScript = `<script>${buildDeferredBootstrapScript()}</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
- redirectChunk?: TransitionRedirectChunk;
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.redirectChunk) {
449
- controller.enqueue(toTransitionChunkLine(options.redirectChunk));
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 reloadVersion = dev ? runtimeOptions.reloadVersion?.() ?? 0 : 0;
573
- return `${normalizeSlashes(activeConfig.routesDir)}|${dev ? "dev" : "prod"}|${reloadVersion}`;
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}-v${reloadVersion}`)
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 cacheBustKey = dev ? String(runtimeOptions.reloadVersion?.() ?? Date.now()) : undefined;
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
- cacheBustKey,
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
- cacheBustKey,
773
- serverBytecode: activeConfig.serverBytecode,
725
+ ...routeModuleLoadOptions,
774
726
  }),
775
- loadGlobalMiddleware(activeConfig.middlewareFile, cacheBustKey),
776
- loadNestedMiddleware(transitionPageMatch.route.middlewareFiles, cacheBustKey),
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
- redirectChunk: toRedirectChunk(location, redirectResponse.status),
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
- redirectChunk: toRedirectChunk(redirectLocation, middlewareResponse.status),
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
- return finalize(
963
- new Response("Transition fallback required for non-streamable response.", { status: 409 }),
964
- "internal-transition",
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, cacheBustKey);
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, cacheBustKey),
1001
- loadNestedMiddleware(apiMatch.route.middlewareFiles, cacheBustKey),
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
- cacheBustKey,
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
- cacheBustKey,
1109
- serverBytecode: activeConfig.serverBytecode,
1060
+ ...routeModuleLoadOptions,
1110
1061
  }),
1111
- loadGlobalMiddleware(activeConfig.middlewareFile, cacheBustKey),
1112
- loadNestedMiddleware(pageMatch.route.middlewareFiles, cacheBustKey),
1062
+ loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
1063
+ loadNestedMiddleware(pageMatch.route.middlewareFiles, requestModuleLoadOptions),
1113
1064
  ]);
1114
1065
  const moduleMiddleware = extractRouteMiddleware(routeModules.route);
1115
1066