react-bun-ssr 0.1.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.
@@ -0,0 +1,744 @@
1
+ import path from "node:path";
2
+ import { createElement } from "react";
3
+ import { createBunRouteAdapter, type BunRouteAdapter } from "./bun-route-adapter";
4
+ import {
5
+ isDeferredLoaderResult,
6
+ prepareDeferredPayload,
7
+ type DeferredSettleEntry,
8
+ } from "./deferred";
9
+ import { statPath } from "./io";
10
+ import type {
11
+ ActionContext,
12
+ BuildRouteAsset,
13
+ FrameworkConfig,
14
+ LoaderContext,
15
+ RequestContext,
16
+ ResolvedConfig,
17
+ RouteModule,
18
+ ServerRuntimeOptions,
19
+ } from "./types";
20
+ import { resolveConfig } from "./config";
21
+ import { isRedirectResult, json } from "./helpers";
22
+ import {
23
+ extractRouteMiddleware,
24
+ loadApiRouteModule,
25
+ loadGlobalMiddleware,
26
+ loadNestedMiddleware,
27
+ loadRouteModule,
28
+ loadRouteModules,
29
+ } from "./module-loader";
30
+ import {
31
+ collectHeadElements,
32
+ createErrorAppTree,
33
+ createNotFoundAppTree,
34
+ createPageAppTree,
35
+ renderDocumentStream,
36
+ } from "./render";
37
+ import { runMiddlewareChain } from "./middleware";
38
+ import {
39
+ ensureWithin,
40
+ isMutatingMethod,
41
+ normalizeSlashes,
42
+ parseCookieHeader,
43
+ sanitizeErrorMessage,
44
+ } from "./utils";
45
+
46
+ type ResponseKind = "static" | "html" | "api" | "internal-dev";
47
+
48
+ const HASHED_CLIENT_CHUNK_RE = /^\/client\/.+-[A-Za-z0-9]{6,}\.(?:js|css)$/;
49
+ const STATIC_IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
50
+ const STATIC_DEFAULT_CACHE = "public, max-age=3600";
51
+
52
+ function toRedirectResponse(location: string, status = 302): Response {
53
+ return Response.redirect(location, status);
54
+ }
55
+
56
+ function isResponse(value: unknown): value is Response {
57
+ return value instanceof Response;
58
+ }
59
+
60
+ function toHtmlStreamResponse(stream: ReadableStream<Uint8Array>, status: number): Response {
61
+ return new Response(stream, {
62
+ status,
63
+ headers: {
64
+ "content-type": "text/html; charset=utf-8",
65
+ },
66
+ });
67
+ }
68
+
69
+ function applyFrameworkDefaultHeaders(options: {
70
+ headers: Headers;
71
+ dev: boolean;
72
+ kind: ResponseKind;
73
+ pathname: string;
74
+ }): void {
75
+ const { headers, dev, kind, pathname } = options;
76
+
77
+ if (kind === "internal-dev") {
78
+ if (!headers.has("cache-control")) {
79
+ headers.set("cache-control", "no-store");
80
+ }
81
+ return;
82
+ }
83
+
84
+ if (dev) {
85
+ if (kind === "static") {
86
+ headers.set("cache-control", "no-store");
87
+ headers.set("pragma", "no-cache");
88
+ headers.set("expires", "0");
89
+ }
90
+ return;
91
+ }
92
+
93
+ if (kind === "static" && !headers.has("cache-control")) {
94
+ headers.set(
95
+ "cache-control",
96
+ HASHED_CLIENT_CHUNK_RE.test(pathname) ? STATIC_IMMUTABLE_CACHE : STATIC_DEFAULT_CACHE,
97
+ );
98
+ }
99
+ }
100
+
101
+ function applyConfiguredHeaders(options: {
102
+ headers: Headers;
103
+ pathname: string;
104
+ config: ResolvedConfig;
105
+ }): void {
106
+ const { headers, pathname, config } = options;
107
+ for (const rule of config.headerRules) {
108
+ if (!rule.matcher.test(pathname)) {
109
+ continue;
110
+ }
111
+
112
+ for (const [name, value] of Object.entries(rule.headers)) {
113
+ headers.set(name, value);
114
+ }
115
+ }
116
+ }
117
+
118
+ function finalizeResponseHeaders(options: {
119
+ response: Response;
120
+ request: Request;
121
+ pathname: string;
122
+ kind: ResponseKind;
123
+ dev: boolean;
124
+ config: ResolvedConfig;
125
+ }): Response {
126
+ const headers = new Headers(options.response.headers);
127
+
128
+ applyFrameworkDefaultHeaders({
129
+ headers,
130
+ dev: options.dev,
131
+ kind: options.kind,
132
+ pathname: options.pathname,
133
+ });
134
+
135
+ applyConfiguredHeaders({
136
+ headers,
137
+ pathname: options.pathname,
138
+ config: options.config,
139
+ });
140
+
141
+ return new Response(options.request.method.toUpperCase() === "HEAD" ? null : options.response.body, {
142
+ status: options.response.status,
143
+ statusText: options.response.statusText,
144
+ headers,
145
+ });
146
+ }
147
+
148
+ async function tryServeStatic(baseDir: string, pathname: string): Promise<Response | null> {
149
+ if (!pathname || pathname === "/") {
150
+ return null;
151
+ }
152
+
153
+ const decodedPath = decodeURIComponent(pathname);
154
+ const relativePath = decodedPath.replace(/^\/+/, "");
155
+
156
+ if (!relativePath) {
157
+ return null;
158
+ }
159
+
160
+ const resolved = ensureWithin(baseDir, relativePath);
161
+ if (!resolved) {
162
+ return null;
163
+ }
164
+
165
+ const stat = await statPath(resolved);
166
+ if (!stat?.isFile()) {
167
+ return null;
168
+ }
169
+
170
+ return new Response(Bun.file(resolved));
171
+ }
172
+
173
+ function getMethodHandler(moduleValue: Record<string, unknown>, method: string): unknown {
174
+ const upper = method.toUpperCase();
175
+ if (upper === "HEAD" && typeof moduleValue.HEAD !== "function" && typeof moduleValue.GET === "function") {
176
+ return moduleValue.GET;
177
+ }
178
+ return moduleValue[upper];
179
+ }
180
+
181
+ function getAllowedMethods(moduleValue: Record<string, unknown>): string[] {
182
+ return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].filter(method => {
183
+ return typeof moduleValue[method] === "function";
184
+ });
185
+ }
186
+
187
+ async function parseActionBody(request: Request): Promise<Pick<ActionContext, "formData" | "json">> {
188
+ const contentType = request.headers.get("content-type") ?? "";
189
+
190
+ if (contentType.includes("application/json")) {
191
+ try {
192
+ return { json: await request.json() };
193
+ } catch {
194
+ return {};
195
+ }
196
+ }
197
+
198
+ if (
199
+ contentType.includes("multipart/form-data") ||
200
+ contentType.includes("application/x-www-form-urlencoded")
201
+ ) {
202
+ try {
203
+ return { formData: await request.formData() };
204
+ } catch {
205
+ return {};
206
+ }
207
+ }
208
+
209
+ return {};
210
+ }
211
+
212
+ async function loadRootOnlyModule(
213
+ rootModulePath: string,
214
+ cacheBustKey?: string,
215
+ ): Promise<RouteModule> {
216
+ return loadRouteModule(rootModulePath, cacheBustKey);
217
+ }
218
+
219
+ function resolveRouteAssets(
220
+ routeId: string,
221
+ options: {
222
+ dev: boolean;
223
+ runtimeOptions: ServerRuntimeOptions;
224
+ },
225
+ ): BuildRouteAsset | null {
226
+ const { dev, runtimeOptions } = options;
227
+ if (dev) {
228
+ const assets = runtimeOptions.getDevAssets?.() ?? runtimeOptions.devAssets ?? {};
229
+ return assets[routeId] ?? null;
230
+ }
231
+
232
+ const manifest = runtimeOptions.buildManifest;
233
+ if (!manifest) {
234
+ return null;
235
+ }
236
+
237
+ return manifest.routes[routeId] ?? null;
238
+ }
239
+
240
+ function createDevReloadEventStream(options: {
241
+ getVersion: () => number;
242
+ subscribe?: (listener: (version: number) => void) => (() => void) | void;
243
+ }): Response {
244
+ const encoder = new TextEncoder();
245
+ let interval: ReturnType<typeof setInterval> | undefined;
246
+ let unsubscribe: (() => void) | void;
247
+ let cleanup: (() => void) | undefined;
248
+
249
+ const stream = new ReadableStream<Uint8Array>({
250
+ start(controller) {
251
+ let closed = false;
252
+
253
+ cleanup = (): void => {
254
+ if (closed) {
255
+ return;
256
+ }
257
+ closed = true;
258
+ if (interval) {
259
+ clearInterval(interval);
260
+ interval = undefined;
261
+ }
262
+ if (typeof unsubscribe === "function") {
263
+ unsubscribe();
264
+ unsubscribe = undefined;
265
+ }
266
+ };
267
+
268
+ const sendChunk = (chunk: string): void => {
269
+ if (closed) {
270
+ return;
271
+ }
272
+ try {
273
+ controller.enqueue(encoder.encode(chunk));
274
+ } catch {
275
+ cleanup?.();
276
+ }
277
+ };
278
+
279
+ const sendReload = (version: number): void => {
280
+ sendChunk(`event: reload\ndata: ${version}\n\n`);
281
+ };
282
+
283
+ sendChunk(": connected\n\n");
284
+ sendReload(options.getVersion());
285
+
286
+ unsubscribe = options.subscribe?.(nextVersion => {
287
+ sendReload(nextVersion);
288
+ });
289
+
290
+ interval = setInterval(() => {
291
+ sendChunk(": ping\n\n");
292
+ }, 15_000);
293
+ },
294
+ cancel() {
295
+ cleanup?.();
296
+ },
297
+ });
298
+
299
+ return new Response(stream, {
300
+ headers: {
301
+ "content-type": "text/event-stream; charset=utf-8",
302
+ "cache-control": "no-cache, no-transform",
303
+ connection: "keep-alive",
304
+ },
305
+ });
306
+ }
307
+
308
+ export function createServer(
309
+ config: FrameworkConfig = {},
310
+ runtimeOptions: ServerRuntimeOptions = {},
311
+ ): { fetch(req: Request): Promise<Response> } {
312
+ const resolvedConfig: ResolvedConfig = resolveConfig(config);
313
+
314
+ const dev = runtimeOptions.dev ?? resolvedConfig.mode !== "production";
315
+
316
+ const adapterCache = new Map<string, BunRouteAdapter>();
317
+ const pendingAdapterCache = new Map<string, Promise<BunRouteAdapter>>();
318
+
319
+ const getAdapterKey = (activeConfig: ResolvedConfig): string => {
320
+ const reloadVersion = dev ? runtimeOptions.reloadVersion?.() ?? 0 : 0;
321
+ return `${normalizeSlashes(activeConfig.routesDir)}|${dev ? "dev" : "prod"}|${reloadVersion}`;
322
+ };
323
+
324
+ const trimAdapterCache = (): void => {
325
+ if (!dev || adapterCache.size <= 3) {
326
+ return;
327
+ }
328
+
329
+ const keys = [...adapterCache.keys()];
330
+ while (keys.length > 3) {
331
+ const oldestKey = keys.shift();
332
+ if (!oldestKey) {
333
+ break;
334
+ }
335
+ adapterCache.delete(oldestKey);
336
+ pendingAdapterCache.delete(oldestKey);
337
+ }
338
+ };
339
+
340
+ const getRouteAdapter = async (activeConfig: ResolvedConfig): Promise<BunRouteAdapter> => {
341
+ const cacheKey = getAdapterKey(activeConfig);
342
+ const cached = adapterCache.get(cacheKey);
343
+ if (cached) {
344
+ return cached;
345
+ }
346
+
347
+ const pending = pendingAdapterCache.get(cacheKey);
348
+ if (pending) {
349
+ return pending;
350
+ }
351
+
352
+ const reloadVersion = dev ? runtimeOptions.reloadVersion?.() ?? 0 : 0;
353
+ const projectionRootDir = dev
354
+ ? path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", `dev-v${reloadVersion}`)
355
+ : path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", "prod");
356
+
357
+ const buildAdapterPromise = createBunRouteAdapter({
358
+ routesDir: activeConfig.routesDir,
359
+ generatedMarkdownRootDir: path.resolve(activeConfig.cwd, ".rbssr/generated/markdown-routes"),
360
+ projectionRootDir,
361
+ });
362
+
363
+ pendingAdapterCache.set(cacheKey, buildAdapterPromise);
364
+
365
+ try {
366
+ const adapter = await buildAdapterPromise;
367
+ adapterCache.set(cacheKey, adapter);
368
+ trimAdapterCache();
369
+ return adapter;
370
+ } finally {
371
+ pendingAdapterCache.delete(cacheKey);
372
+ }
373
+ };
374
+
375
+ const fetchHandler = async (request: Request): Promise<Response> => {
376
+ await runtimeOptions.onBeforeRequest?.();
377
+
378
+ const runtimePaths = runtimeOptions.resolvePaths?.() ?? {};
379
+ const activeConfig: ResolvedConfig = {
380
+ ...resolvedConfig,
381
+ ...runtimePaths,
382
+ };
383
+ const devClientDir = path.resolve(resolvedConfig.cwd, ".rbssr/dev/client");
384
+
385
+ const url = new URL(request.url);
386
+ const finalize = (response: Response, kind: ResponseKind): Response => {
387
+ return finalizeResponseHeaders({
388
+ response,
389
+ request,
390
+ pathname: url.pathname,
391
+ kind,
392
+ dev,
393
+ config: activeConfig,
394
+ });
395
+ };
396
+
397
+ if (dev && url.pathname === "/__rbssr/events") {
398
+ return finalize(createDevReloadEventStream({
399
+ getVersion: () => runtimeOptions.reloadVersion?.() ?? 0,
400
+ subscribe: runtimeOptions.subscribeReload,
401
+ }), "internal-dev");
402
+ }
403
+
404
+ if (dev && url.pathname === "/__rbssr/version") {
405
+ const version = runtimeOptions.reloadVersion?.() ?? 0;
406
+ return finalize(new Response(String(version), {
407
+ headers: {
408
+ "content-type": "text/plain; charset=utf-8",
409
+ "cache-control": "no-store",
410
+ },
411
+ }), "internal-dev");
412
+ }
413
+
414
+ if (dev && url.pathname.startsWith("/__rbssr/client/")) {
415
+ const relative = url.pathname.replace(/^\/__rbssr\/client\//, "");
416
+ const response = await tryServeStatic(devClientDir, relative);
417
+ if (response) {
418
+ return finalize(response, "static");
419
+ }
420
+ }
421
+
422
+ if (!dev && url.pathname.startsWith("/client/")) {
423
+ const relative = url.pathname.replace(/^\/client\//, "");
424
+ const response = await tryServeStatic(path.join(activeConfig.distDir, "client"), relative);
425
+ if (response) {
426
+ return finalize(response, "static");
427
+ }
428
+ }
429
+
430
+ if (!dev) {
431
+ const builtPublicResponse = await tryServeStatic(path.join(activeConfig.distDir, "client"), url.pathname);
432
+ if (builtPublicResponse) {
433
+ return finalize(builtPublicResponse, "static");
434
+ }
435
+ }
436
+
437
+ const publicResponse = await tryServeStatic(activeConfig.publicDir, url.pathname);
438
+ if (publicResponse) {
439
+ return finalize(publicResponse, "static");
440
+ }
441
+
442
+ const routeAdapter = await getRouteAdapter(activeConfig);
443
+ const cacheBustKey = dev ? String(runtimeOptions.reloadVersion?.() ?? Date.now()) : undefined;
444
+
445
+ const apiMatch = routeAdapter.matchApi(url.pathname);
446
+ if (apiMatch) {
447
+ const apiModule = await loadApiRouteModule(apiMatch.route.filePath, cacheBustKey);
448
+ const methodHandler = getMethodHandler(apiModule as Record<string, unknown>, request.method);
449
+
450
+ if (typeof methodHandler !== "function") {
451
+ const allow = getAllowedMethods(apiModule as Record<string, unknown>);
452
+ return finalize(new Response("Method Not Allowed", {
453
+ status: 405,
454
+ headers: {
455
+ allow: allow.join(", "),
456
+ },
457
+ }), "api");
458
+ }
459
+
460
+ const requestContext: RequestContext = {
461
+ request,
462
+ url,
463
+ params: apiMatch.params,
464
+ cookies: parseCookieHeader(request.headers.get("cookie")),
465
+ locals: {},
466
+ };
467
+
468
+ const [globalMiddleware, routeMiddleware] = await Promise.all([
469
+ loadGlobalMiddleware(activeConfig.middlewareFile, cacheBustKey),
470
+ loadNestedMiddleware(apiMatch.route.middlewareFiles, cacheBustKey),
471
+ ]);
472
+ const allMiddleware = [...globalMiddleware, ...routeMiddleware];
473
+
474
+ let response: Response;
475
+ try {
476
+ response = await runMiddlewareChain(allMiddleware, requestContext, async () => {
477
+ const result = await (methodHandler as (ctx: RequestContext) => unknown)(requestContext);
478
+
479
+ if (isResponse(result)) {
480
+ return result;
481
+ }
482
+
483
+ if (isRedirectResult(result)) {
484
+ return toRedirectResponse(result.location, result.status);
485
+ }
486
+
487
+ return json(result);
488
+ });
489
+ } catch (error) {
490
+ return finalize(json(
491
+ {
492
+ error: sanitizeErrorMessage(error, !dev),
493
+ },
494
+ { status: 500 },
495
+ ), "api");
496
+ }
497
+ return finalize(response, "api");
498
+ }
499
+
500
+ const pageMatch = routeAdapter.matchPage(url.pathname);
501
+
502
+ if (!pageMatch) {
503
+ const rootModule = await loadRootOnlyModule(activeConfig.rootModule, cacheBustKey);
504
+ const fallbackRoute: RouteModule = {
505
+ default: () => null,
506
+ NotFound: rootModule.NotFound,
507
+ };
508
+
509
+ const payload = {
510
+ routeId: "__not_found__",
511
+ data: null,
512
+ params: {},
513
+ url: url.toString(),
514
+ };
515
+
516
+ const modules = {
517
+ root: rootModule,
518
+ layouts: [],
519
+ route: fallbackRoute,
520
+ };
521
+
522
+ const appTree = createNotFoundAppTree(modules, payload) ?? createElement(
523
+ "main",
524
+ null,
525
+ createElement("h1", null, "404"),
526
+ createElement("p", null, "Page not found."),
527
+ );
528
+ const stream = await renderDocumentStream({
529
+ appTree,
530
+ payload,
531
+ assets: {
532
+ script: undefined,
533
+ css: [],
534
+ devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
535
+ },
536
+ headElements: collectHeadElements(modules, payload),
537
+ });
538
+
539
+ return finalize(toHtmlStreamResponse(stream, 404), "html");
540
+ }
541
+
542
+ const [routeModules, globalMiddleware, nestedMiddleware] = await Promise.all([
543
+ loadRouteModules({
544
+ rootFilePath: activeConfig.rootModule,
545
+ layoutFiles: pageMatch.route.layoutFiles,
546
+ routeFilePath: pageMatch.route.filePath,
547
+ cacheBustKey,
548
+ }),
549
+ loadGlobalMiddleware(activeConfig.middlewareFile, cacheBustKey),
550
+ loadNestedMiddleware(pageMatch.route.middlewareFiles, cacheBustKey),
551
+ ]);
552
+ const moduleMiddleware = extractRouteMiddleware(routeModules.route);
553
+
554
+ const requestContext: RequestContext = {
555
+ request,
556
+ url,
557
+ params: pageMatch.params,
558
+ cookies: parseCookieHeader(request.headers.get("cookie")),
559
+ locals: {},
560
+ };
561
+
562
+ const routeAssets = resolveRouteAssets(pageMatch.route.id, {
563
+ dev,
564
+ runtimeOptions,
565
+ });
566
+
567
+ let response: Response;
568
+ try {
569
+ response = await runMiddlewareChain(
570
+ [...globalMiddleware, ...nestedMiddleware, ...moduleMiddleware],
571
+ requestContext,
572
+ async () => {
573
+ const method = request.method.toUpperCase();
574
+ let dataForRender: unknown = null;
575
+ let dataForPayload: unknown = null;
576
+ let deferredSettleEntries: DeferredSettleEntry[] = [];
577
+
578
+ if (isMutatingMethod(method)) {
579
+ if (!routeModules.route.action) {
580
+ return new Response("Method Not Allowed", { status: 405 });
581
+ }
582
+
583
+ const body = await parseActionBody(request.clone());
584
+ const actionCtx: ActionContext = {
585
+ ...requestContext,
586
+ ...body,
587
+ };
588
+
589
+ const actionResult = await routeModules.route.action(actionCtx);
590
+
591
+ if (isResponse(actionResult)) {
592
+ return actionResult;
593
+ }
594
+
595
+ if (isRedirectResult(actionResult)) {
596
+ return toRedirectResponse(actionResult.location, actionResult.status);
597
+ }
598
+
599
+ if (isDeferredLoaderResult(actionResult)) {
600
+ return new Response("defer() is only supported in route loaders", { status: 500 });
601
+ }
602
+
603
+ dataForRender = actionResult;
604
+ dataForPayload = actionResult;
605
+ } else {
606
+ if (routeModules.route.loader) {
607
+ const loaderCtx: LoaderContext = requestContext;
608
+ const loaderResult = await routeModules.route.loader(loaderCtx);
609
+
610
+ if (isResponse(loaderResult)) {
611
+ return loaderResult;
612
+ }
613
+
614
+ if (isRedirectResult(loaderResult)) {
615
+ return toRedirectResponse(loaderResult.location, loaderResult.status);
616
+ }
617
+
618
+ if (isDeferredLoaderResult(loaderResult)) {
619
+ const prepared = prepareDeferredPayload(pageMatch.route.id, loaderResult);
620
+ dataForRender = prepared.dataForRender;
621
+ dataForPayload = prepared.dataForPayload;
622
+ deferredSettleEntries = prepared.settleEntries;
623
+ } else {
624
+ dataForRender = loaderResult;
625
+ dataForPayload = loaderResult;
626
+ }
627
+ }
628
+ }
629
+
630
+ const renderPayload = {
631
+ routeId: pageMatch.route.id,
632
+ data: dataForRender,
633
+ params: pageMatch.params,
634
+ url: url.toString(),
635
+ };
636
+
637
+ const clientPayload = {
638
+ ...renderPayload,
639
+ data: dataForPayload,
640
+ };
641
+
642
+ let appTree: ReturnType<typeof createPageAppTree>;
643
+ try {
644
+ appTree = createPageAppTree(routeModules, renderPayload);
645
+ } catch (error) {
646
+ const boundaryTree = createErrorAppTree(routeModules, renderPayload, error);
647
+ if (boundaryTree) {
648
+ const errorPayload = {
649
+ ...renderPayload,
650
+ error: {
651
+ message: sanitizeErrorMessage(error, !dev),
652
+ },
653
+ };
654
+ const stream = await renderDocumentStream({
655
+ appTree: boundaryTree,
656
+ payload: {
657
+ ...clientPayload,
658
+ error: errorPayload.error,
659
+ },
660
+ assets: {
661
+ script: routeAssets?.script,
662
+ css: routeAssets?.css ?? [],
663
+ devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
664
+ },
665
+ headElements: collectHeadElements(routeModules, errorPayload),
666
+ });
667
+ return toHtmlStreamResponse(stream, 500);
668
+ }
669
+
670
+ const message = sanitizeErrorMessage(error, !dev);
671
+ return new Response(message, { status: 500 });
672
+ }
673
+
674
+ const stream = await renderDocumentStream({
675
+ appTree,
676
+ payload: clientPayload,
677
+ assets: {
678
+ script: routeAssets?.script,
679
+ css: routeAssets?.css ?? [],
680
+ devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
681
+ },
682
+ headElements: collectHeadElements(routeModules, renderPayload),
683
+ deferredSettleEntries,
684
+ });
685
+
686
+ return toHtmlStreamResponse(stream, 200);
687
+ },
688
+ );
689
+ } catch (error) {
690
+ const renderPayload = {
691
+ routeId: pageMatch.route.id,
692
+ data: null,
693
+ params: pageMatch.params,
694
+ url: url.toString(),
695
+ };
696
+ const boundaryTree = createErrorAppTree(routeModules, renderPayload, error);
697
+ if (boundaryTree) {
698
+ const errorPayload = {
699
+ ...renderPayload,
700
+ error: {
701
+ message: sanitizeErrorMessage(error, !dev),
702
+ },
703
+ };
704
+ const stream = await renderDocumentStream({
705
+ appTree: boundaryTree,
706
+ payload: errorPayload,
707
+ assets: {
708
+ script: routeAssets?.script,
709
+ css: routeAssets?.css ?? [],
710
+ devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
711
+ },
712
+ headElements: collectHeadElements(routeModules, errorPayload),
713
+ });
714
+ return finalize(toHtmlStreamResponse(stream, 500), "html");
715
+ }
716
+
717
+ return finalize(new Response(sanitizeErrorMessage(error, !dev), { status: 500 }), "html");
718
+ }
719
+
720
+ return finalize(response, "html");
721
+ };
722
+
723
+ return {
724
+ fetch: fetchHandler,
725
+ };
726
+ }
727
+
728
+ export function startHttpServer(options: {
729
+ config: FrameworkConfig;
730
+ runtimeOptions?: ServerRuntimeOptions;
731
+ }): void {
732
+ const server = createServer(options.config, options.runtimeOptions);
733
+ const resolved = resolveConfig(options.config);
734
+
735
+ const bunServer = Bun.serve({
736
+ port: resolved.port,
737
+ hostname: resolved.host,
738
+ fetch: server.fetch,
739
+ development: resolved.mode === "development",
740
+ });
741
+
742
+ // eslint-disable-next-line no-console
743
+ console.log(`[react-bun-ssr] listening on ${bunServer.url}`);
744
+ }