react-bun-ssr 0.3.2 → 0.4.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.
@@ -8,6 +8,7 @@ import {
8
8
  } from "./deferred";
9
9
  import { statPath } from "./io";
10
10
  import type {
11
+ ActionResponseEnvelope,
11
12
  ActionContext,
12
13
  BuildRouteAsset,
13
14
  ClientRouteSnapshot,
@@ -69,8 +70,9 @@ import {
69
70
  stableHash,
70
71
  } from "./utils";
71
72
  import { sortRoutesBySpecificity } from "./route-order";
73
+ import { applyResponseContext, createResponseContext } from "./response-context";
72
74
 
73
- type ResponseKind = "static" | "html" | "api" | "internal-dev" | "internal-transition";
75
+ type ResponseKind = "static" | "html" | "api" | "internal-dev" | "internal-transition" | "internal-action";
74
76
 
75
77
  const HASHED_CLIENT_CHUNK_RE = /^\/client\/.+-[A-Za-z0-9]{6,}\.(?:js|css)$/;
76
78
  const STATIC_IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
@@ -192,7 +194,7 @@ function applyFrameworkDefaultHeaders(options: {
192
194
  }): void {
193
195
  const { headers, dev, kind, pathname } = options;
194
196
 
195
- if (kind === "internal-dev" || kind === "internal-transition") {
197
+ if (kind === "internal-dev" || kind === "internal-transition" || kind === "internal-action") {
196
198
  if (!headers.has("cache-control")) {
197
199
  headers.set("cache-control", "no-store");
198
200
  }
@@ -332,6 +334,22 @@ async function parseActionBody(request: Request): Promise<Pick<ActionContext, "f
332
334
  return {};
333
335
  }
334
336
 
337
+ function createRequestContext(options: {
338
+ request: Request;
339
+ url: URL;
340
+ params: RequestContext["params"];
341
+ }): RequestContext {
342
+ const cookies = parseCookieHeader(options.request.headers.get("cookie"));
343
+ return {
344
+ request: options.request,
345
+ url: options.url,
346
+ params: options.params,
347
+ cookies,
348
+ locals: {},
349
+ response: createResponseContext(cookies),
350
+ };
351
+ }
352
+
335
353
  async function loadRootOnlyModule(
336
354
  rootModulePath: string,
337
355
  options: {
@@ -505,6 +523,63 @@ function createTransitionStream(options: {
505
523
  });
506
524
  }
507
525
 
526
+ function toActionEnvelopeResponse(
527
+ envelope: ActionResponseEnvelope,
528
+ options: {
529
+ status?: number;
530
+ headers?: HeadersInit;
531
+ } = {},
532
+ ): Response {
533
+ const headers = new Headers(options.headers);
534
+ headers.set("content-type", "application/json; charset=utf-8");
535
+
536
+ return new Response(JSON.stringify(envelope), {
537
+ status: options.status ?? envelope.status,
538
+ headers,
539
+ });
540
+ }
541
+
542
+ async function readActionResponsePayload(response: Response): Promise<unknown> {
543
+ const contentType = response.headers.get("content-type") ?? "";
544
+ if (contentType.includes("application/json")) {
545
+ try {
546
+ return await response.json();
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+
552
+ try {
553
+ return await response.text();
554
+ } catch {
555
+ return null;
556
+ }
557
+ }
558
+
559
+ function resolveResponseRedirect(response: Response): { location: string; status: number } | null {
560
+ const location = response.headers.get("location");
561
+ if (!location || !isRedirectStatus(response.status)) {
562
+ return null;
563
+ }
564
+
565
+ return {
566
+ location,
567
+ status: response.status,
568
+ };
569
+ }
570
+
571
+ function isActionResponseEnvelopePayload(value: unknown): value is ActionResponseEnvelope {
572
+ if (!value || typeof value !== "object") {
573
+ return false;
574
+ }
575
+
576
+ const candidate = value as {
577
+ type?: unknown;
578
+ status?: unknown;
579
+ };
580
+ return typeof candidate.type === "string" && typeof candidate.status === "number";
581
+ }
582
+
508
583
  export function createServer(
509
584
  config: FrameworkConfig = {},
510
585
  runtimeOptions: ServerRuntimeOptions = {},
@@ -583,9 +658,16 @@ export function createServer(
583
658
  const devClientDir = path.resolve(resolvedConfig.cwd, ".rbssr/dev/client");
584
659
 
585
660
  const url = new URL(request.url);
586
- const finalize = (response: Response, kind: ResponseKind): Response => {
661
+ const finalize = (
662
+ response: Response,
663
+ kind: ResponseKind,
664
+ requestContext?: RequestContext,
665
+ ): Response => {
666
+ const finalResponse = requestContext
667
+ ? applyResponseContext(response, requestContext.response)
668
+ : response;
587
669
  return finalizeResponseHeaders({
588
- response,
670
+ response: finalResponse,
589
671
  request,
590
672
  pathname: url.pathname,
591
673
  kind,
@@ -644,7 +726,7 @@ export function createServer(
644
726
  nodeEnv,
645
727
  };
646
728
  const requestModuleLoadOptions = {
647
- cacheBustKey: undefined,
729
+ cacheBustKey: devCacheBustKey,
648
730
  serverBytecode: activeConfig.serverBytecode,
649
731
  devSourceImports: dev,
650
732
  nodeEnv,
@@ -663,6 +745,267 @@ export function createServer(
663
745
  devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
664
746
  });
665
747
 
748
+ if (request.method.toUpperCase() === "POST" && url.pathname === "/__rbssr/action") {
749
+ const toParam = url.searchParams.get("to");
750
+ if (!toParam) {
751
+ return finalize(
752
+ toActionEnvelopeResponse({
753
+ type: "error",
754
+ status: 400,
755
+ message: "Missing required `to` query parameter.",
756
+ }, { status: 400 }),
757
+ "internal-action",
758
+ );
759
+ }
760
+
761
+ let targetUrl: URL;
762
+ try {
763
+ targetUrl = new URL(toParam, url);
764
+ } catch {
765
+ return finalize(
766
+ toActionEnvelopeResponse({
767
+ type: "error",
768
+ status: 400,
769
+ message: "Invalid `to` URL.",
770
+ }, { status: 400 }),
771
+ "internal-action",
772
+ );
773
+ }
774
+
775
+ if (targetUrl.origin !== url.origin) {
776
+ return finalize(
777
+ toActionEnvelopeResponse({
778
+ type: "error",
779
+ status: 400,
780
+ message: "Cross-origin action targets are not allowed.",
781
+ }, { status: 400 }),
782
+ "internal-action",
783
+ );
784
+ }
785
+
786
+ const actionPageMatch = routeAdapter.matchPage(targetUrl.pathname);
787
+ if (!actionPageMatch) {
788
+ return finalize(
789
+ toActionEnvelopeResponse({
790
+ type: "error",
791
+ status: 404,
792
+ message: "No page route matched the action target.",
793
+ }, { status: 404 }),
794
+ "internal-action",
795
+ );
796
+ }
797
+
798
+ const [routeModules, globalMiddleware, nestedMiddleware, actionBody] = await Promise.all([
799
+ loadRouteModules({
800
+ rootFilePath: activeConfig.rootModule,
801
+ layoutFiles: actionPageMatch.route.layoutFiles,
802
+ routeFilePath: actionPageMatch.route.filePath,
803
+ routeServerFilePath: actionPageMatch.route.serverFilePath,
804
+ ...routeModuleLoadOptions,
805
+ }),
806
+ loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
807
+ loadNestedMiddleware(actionPageMatch.route.middlewareFiles, requestModuleLoadOptions),
808
+ parseActionBody(request.clone()),
809
+ ]);
810
+
811
+ const moduleMiddleware = extractRouteMiddleware(routeModules.route);
812
+ const actionRequest = new Request(targetUrl.toString(), {
813
+ method: request.method,
814
+ headers: request.headers,
815
+ });
816
+ const requestContext = createRequestContext({
817
+ request: actionRequest,
818
+ url: targetUrl,
819
+ params: actionPageMatch.params,
820
+ });
821
+ const contextBase = toRouteErrorContextBase({
822
+ requestContext,
823
+ routeId: actionPageMatch.route.id,
824
+ phase: "action",
825
+ dev,
826
+ });
827
+
828
+ try {
829
+ const middlewareResponse = await runMiddlewareChain(
830
+ [...globalMiddleware, ...nestedMiddleware, ...moduleMiddleware],
831
+ requestContext,
832
+ async () => {
833
+ if (!routeModules.route.action) {
834
+ return toActionEnvelopeResponse({
835
+ type: "error",
836
+ status: 405,
837
+ message:
838
+ "Method Not Allowed: route has no server action export. " +
839
+ "Define a server action (for example in a *.server.tsx companion) " +
840
+ "and call it from useActionState(action, initialState) with createRouteAction().",
841
+ }, { status: 405 });
842
+ }
843
+
844
+ const actionCtx: ActionContext = {
845
+ ...requestContext,
846
+ ...actionBody,
847
+ };
848
+ const actionResult = await routeModules.route.action(actionCtx);
849
+
850
+ if (isDeferredLoaderResult(actionResult)) {
851
+ return toActionEnvelopeResponse({
852
+ type: "error",
853
+ status: 500,
854
+ message: "defer() is only supported in route loaders.",
855
+ }, { status: 500 });
856
+ }
857
+
858
+ if (isRedirectResult(actionResult)) {
859
+ return toActionEnvelopeResponse({
860
+ type: "redirect",
861
+ status: actionResult.status ?? 302,
862
+ location: actionResult.location,
863
+ }, { status: 200 });
864
+ }
865
+
866
+ if (isResponse(actionResult)) {
867
+ const responseRedirect = resolveResponseRedirect(actionResult);
868
+ if (responseRedirect) {
869
+ return toActionEnvelopeResponse({
870
+ type: "redirect",
871
+ status: responseRedirect.status,
872
+ location: responseRedirect.location,
873
+ }, {
874
+ headers: actionResult.headers,
875
+ status: 200,
876
+ });
877
+ }
878
+
879
+ const data = await readActionResponsePayload(actionResult.clone());
880
+ return toActionEnvelopeResponse({
881
+ type: "data",
882
+ status: actionResult.status,
883
+ data,
884
+ }, {
885
+ headers: actionResult.headers,
886
+ status: 200,
887
+ });
888
+ }
889
+
890
+ return toActionEnvelopeResponse({
891
+ type: "data",
892
+ status: 200,
893
+ data: actionResult,
894
+ }, { status: 200 });
895
+ },
896
+ );
897
+ const middlewareRedirect = resolveResponseRedirect(middlewareResponse);
898
+ if (middlewareRedirect) {
899
+ return finalize(
900
+ toActionEnvelopeResponse({
901
+ type: "redirect",
902
+ status: middlewareRedirect.status,
903
+ location: middlewareRedirect.location,
904
+ }, {
905
+ headers: middlewareResponse.headers,
906
+ status: 200,
907
+ }),
908
+ "internal-action",
909
+ requestContext,
910
+ );
911
+ }
912
+
913
+ const parsedPayload = await readActionResponsePayload(middlewareResponse.clone());
914
+ if (isActionResponseEnvelopePayload(parsedPayload)) {
915
+ return finalize(middlewareResponse, "internal-action", requestContext);
916
+ }
917
+
918
+ if (middlewareResponse.status >= 400) {
919
+ const message = typeof parsedPayload === "string"
920
+ ? parsedPayload
921
+ : sanitizeErrorMessage(parsedPayload, !dev);
922
+ return finalize(
923
+ toActionEnvelopeResponse({
924
+ type: "error",
925
+ status: middlewareResponse.status,
926
+ message,
927
+ }, {
928
+ headers: middlewareResponse.headers,
929
+ status: 200,
930
+ }),
931
+ "internal-action",
932
+ requestContext,
933
+ );
934
+ }
935
+
936
+ return finalize(
937
+ toActionEnvelopeResponse({
938
+ type: "data",
939
+ status: middlewareResponse.status,
940
+ data: parsedPayload,
941
+ }, {
942
+ headers: middlewareResponse.headers,
943
+ status: 200,
944
+ }),
945
+ "internal-action",
946
+ requestContext,
947
+ );
948
+ } catch (error) {
949
+ const redirectResponse = resolveThrownRedirect(error);
950
+ if (redirectResponse) {
951
+ const location = redirectResponse.headers.get("location");
952
+ if (location) {
953
+ return finalize(
954
+ toActionEnvelopeResponse({
955
+ type: "redirect",
956
+ status: redirectResponse.status,
957
+ location,
958
+ }, {
959
+ headers: redirectResponse.headers,
960
+ status: 200,
961
+ }),
962
+ "internal-action",
963
+ requestContext,
964
+ );
965
+ }
966
+ }
967
+
968
+ const caught = toRouteErrorResponse(error);
969
+ if (caught) {
970
+ await notifyCatchHooks({
971
+ modules: routeModules,
972
+ context: {
973
+ ...contextBase,
974
+ error: caught,
975
+ },
976
+ });
977
+
978
+ return finalize(
979
+ toActionEnvelopeResponse({
980
+ type: "catch",
981
+ status: caught.status,
982
+ error: toCaughtErrorPayload(caught, !dev),
983
+ }, { status: 200 }),
984
+ "internal-action",
985
+ requestContext,
986
+ );
987
+ }
988
+
989
+ await notifyErrorHooks({
990
+ modules: routeModules,
991
+ context: {
992
+ ...contextBase,
993
+ error,
994
+ },
995
+ });
996
+
997
+ return finalize(
998
+ toActionEnvelopeResponse({
999
+ type: "error",
1000
+ status: 500,
1001
+ message: sanitizeErrorMessage(error, !dev),
1002
+ }, { status: 200 }),
1003
+ "internal-action",
1004
+ requestContext,
1005
+ );
1006
+ }
1007
+ }
1008
+
666
1009
  if (request.method.toUpperCase() === "GET" && url.pathname === "/__rbssr/transition") {
667
1010
  const toParam = url.searchParams.get("to");
668
1011
  if (!toParam) {
@@ -691,7 +1034,7 @@ export function createServer(
691
1034
  };
692
1035
  const payload = {
693
1036
  routeId: "__not_found__",
694
- data: null,
1037
+ loaderData: null,
695
1038
  params: {},
696
1039
  url: targetUrl.toString(),
697
1040
  };
@@ -723,6 +1066,7 @@ export function createServer(
723
1066
  rootFilePath: activeConfig.rootModule,
724
1067
  layoutFiles: transitionPageMatch.route.layoutFiles,
725
1068
  routeFilePath: transitionPageMatch.route.filePath,
1069
+ routeServerFilePath: transitionPageMatch.route.serverFilePath,
726
1070
  ...routeModuleLoadOptions,
727
1071
  }),
728
1072
  loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
@@ -735,13 +1079,11 @@ export function createServer(
735
1079
  headers: request.headers,
736
1080
  });
737
1081
 
738
- const requestContext: RequestContext = {
1082
+ const requestContext = createRequestContext({
739
1083
  request: transitionRequest,
740
1084
  url: targetUrl,
741
1085
  params: transitionPageMatch.params,
742
- cookies: parseCookieHeader(request.headers.get("cookie")),
743
- locals: {},
744
- };
1086
+ });
745
1087
 
746
1088
  let transitionInitialChunk: TransitionInitialChunk | undefined;
747
1089
  let deferredSettleEntries: DeferredSettleEntry[] = [];
@@ -753,8 +1095,8 @@ export function createServer(
753
1095
  [...globalMiddleware, ...nestedMiddleware, ...moduleMiddleware],
754
1096
  requestContext,
755
1097
  async () => {
756
- let dataForRender: unknown = null;
757
- let dataForPayload: unknown = null;
1098
+ let loaderDataForRender: unknown = null;
1099
+ let loaderDataForPayload: unknown = null;
758
1100
 
759
1101
  if (routeModules.route.loader) {
760
1102
  transitionPhase = "loader";
@@ -771,24 +1113,24 @@ export function createServer(
771
1113
 
772
1114
  if (isDeferredLoaderResult(loaderResult)) {
773
1115
  const prepared = prepareDeferredPayload(transitionPageMatch.route.id, loaderResult);
774
- dataForRender = prepared.dataForRender;
775
- dataForPayload = prepared.dataForPayload;
1116
+ loaderDataForRender = prepared.dataForRender;
1117
+ loaderDataForPayload = prepared.dataForPayload;
776
1118
  deferredSettleEntries = prepared.settleEntries;
777
1119
  } else {
778
- dataForRender = loaderResult;
779
- dataForPayload = loaderResult;
1120
+ loaderDataForRender = loaderResult;
1121
+ loaderDataForPayload = loaderResult;
780
1122
  }
781
1123
  }
782
1124
 
783
1125
  const renderPayload = {
784
1126
  routeId: transitionPageMatch.route.id,
785
- data: dataForRender,
1127
+ loaderData: loaderDataForRender,
786
1128
  params: transitionPageMatch.params,
787
1129
  url: targetUrl.toString(),
788
1130
  };
789
1131
  const payload = {
790
1132
  ...renderPayload,
791
- data: dataForPayload,
1133
+ loaderData: loaderDataForPayload,
792
1134
  };
793
1135
  transitionInitialChunk = {
794
1136
  type: "initial",
@@ -818,7 +1160,7 @@ export function createServer(
818
1160
  controlChunk: toRedirectChunk(location, redirectResponse.status),
819
1161
  sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
820
1162
  });
821
- return finalize(toTransitionStreamResponse(stream, redirectResponse.headers), "internal-transition");
1163
+ return finalize(toTransitionStreamResponse(stream, redirectResponse.headers), "internal-transition", requestContext);
822
1164
  }
823
1165
  }
824
1166
 
@@ -832,7 +1174,7 @@ export function createServer(
832
1174
  if (caught) {
833
1175
  const payload = {
834
1176
  routeId: transitionPageMatch.route.id,
835
- data: null,
1177
+ loaderData: null,
836
1178
  params: transitionPageMatch.params,
837
1179
  url: targetUrl.toString(),
838
1180
  error: toCaughtErrorPayload(caught, !dev),
@@ -863,7 +1205,7 @@ export function createServer(
863
1205
  initialChunk,
864
1206
  sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
865
1207
  });
866
- return finalize(toTransitionStreamResponse(stream), "internal-transition");
1208
+ return finalize(toTransitionStreamResponse(stream), "internal-transition", requestContext);
867
1209
  }
868
1210
 
869
1211
  await notifyErrorHooks({
@@ -875,7 +1217,7 @@ export function createServer(
875
1217
  });
876
1218
  const renderPayload = {
877
1219
  routeId: transitionPageMatch.route.id,
878
- data: null,
1220
+ loaderData: null,
879
1221
  params: transitionPageMatch.params,
880
1222
  url: targetUrl.toString(),
881
1223
  error: toUncaughtErrorPayload(error, !dev),
@@ -899,7 +1241,7 @@ export function createServer(
899
1241
  initialChunk,
900
1242
  sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
901
1243
  });
902
- return finalize(toTransitionStreamResponse(stream), "internal-transition");
1244
+ return finalize(toTransitionStreamResponse(stream), "internal-transition", requestContext);
903
1245
  }
904
1246
 
905
1247
  const redirectLocation = middlewareResponse.headers.get("location");
@@ -908,7 +1250,7 @@ export function createServer(
908
1250
  controlChunk: toRedirectChunk(redirectLocation, middlewareResponse.status),
909
1251
  sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
910
1252
  });
911
- return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
1253
+ return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition", requestContext);
912
1254
  }
913
1255
 
914
1256
  if (!transitionInitialChunk) {
@@ -916,7 +1258,7 @@ export function createServer(
916
1258
  controlChunk: toDocumentChunk(targetUrl.toString(), middlewareResponse.status),
917
1259
  sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
918
1260
  });
919
- return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
1261
+ return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition", requestContext);
920
1262
  }
921
1263
 
922
1264
  const stream = createTransitionStream({
@@ -924,7 +1266,7 @@ export function createServer(
924
1266
  deferredSettleEntries,
925
1267
  sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
926
1268
  });
927
- return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
1269
+ return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition", requestContext);
928
1270
  }
929
1271
 
930
1272
  const apiMatch = routeAdapter.matchApi(url.pathname);
@@ -942,13 +1284,11 @@ export function createServer(
942
1284
  }), "api");
943
1285
  }
944
1286
 
945
- const requestContext: RequestContext = {
1287
+ const requestContext = createRequestContext({
946
1288
  request,
947
1289
  url,
948
1290
  params: apiMatch.params,
949
- cookies: parseCookieHeader(request.headers.get("cookie")),
950
- locals: {},
951
- };
1291
+ });
952
1292
 
953
1293
  const [globalMiddleware, routeMiddleware] = await Promise.all([
954
1294
  loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
@@ -976,12 +1316,12 @@ export function createServer(
976
1316
  } catch (error) {
977
1317
  const redirectResponse = resolveThrownRedirect(error);
978
1318
  if (redirectResponse) {
979
- return finalize(redirectResponse, "api");
1319
+ return finalize(redirectResponse, "api", requestContext);
980
1320
  }
981
1321
 
982
1322
  const caught = toRouteErrorResponse(error);
983
1323
  if (caught) {
984
- return finalize(toRouteErrorHttpResponse(toCaughtErrorPayload(caught, !dev)), "api");
1324
+ return finalize(toRouteErrorHttpResponse(toCaughtErrorPayload(caught, !dev)), "api", requestContext);
985
1325
  }
986
1326
 
987
1327
  const apiErrorHook = (apiModule as Record<string, unknown>).onError;
@@ -1007,9 +1347,9 @@ export function createServer(
1007
1347
  error: sanitizeErrorMessage(error, !dev),
1008
1348
  },
1009
1349
  { status: 500 },
1010
- ), "api");
1350
+ ), "api", requestContext);
1011
1351
  }
1012
- return finalize(response, "api");
1352
+ return finalize(response, "api", requestContext);
1013
1353
  }
1014
1354
 
1015
1355
  const pageMatch = routeAdapter.matchPage(url.pathname);
@@ -1025,7 +1365,7 @@ export function createServer(
1025
1365
 
1026
1366
  const payload = {
1027
1367
  routeId: "__not_found__",
1028
- data: null,
1368
+ loaderData: null,
1029
1369
  params: {},
1030
1370
  url: url.toString(),
1031
1371
  };
@@ -1053,11 +1393,29 @@ export function createServer(
1053
1393
  return finalize(toHtmlStreamResponse(stream, 404), "html");
1054
1394
  }
1055
1395
 
1396
+ if (isMutatingMethod(request.method)) {
1397
+ return finalize(
1398
+ new Response(
1399
+ "Page route mutations are not supported via document requests. " +
1400
+ "Use useActionState(action, initialState) with createRouteAction() " +
1401
+ "(or useRouteAction for backwards compatibility).",
1402
+ {
1403
+ status: 405,
1404
+ headers: {
1405
+ allow: "GET, HEAD",
1406
+ },
1407
+ },
1408
+ ),
1409
+ "html",
1410
+ );
1411
+ }
1412
+
1056
1413
  const [routeModules, globalMiddleware, nestedMiddleware] = await Promise.all([
1057
1414
  loadRouteModules({
1058
1415
  rootFilePath: activeConfig.rootModule,
1059
1416
  layoutFiles: pageMatch.route.layoutFiles,
1060
1417
  routeFilePath: pageMatch.route.filePath,
1418
+ routeServerFilePath: pageMatch.route.serverFilePath,
1061
1419
  ...routeModuleLoadOptions,
1062
1420
  }),
1063
1421
  loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
@@ -1065,13 +1423,11 @@ export function createServer(
1065
1423
  ]);
1066
1424
  const moduleMiddleware = extractRouteMiddleware(routeModules.route);
1067
1425
 
1068
- const requestContext: RequestContext = {
1426
+ const requestContext = createRequestContext({
1069
1427
  request,
1070
1428
  url,
1071
1429
  params: pageMatch.params,
1072
- cookies: parseCookieHeader(request.headers.get("cookie")),
1073
- locals: {},
1074
- };
1430
+ });
1075
1431
 
1076
1432
  const routeAssets = routeAssetsById[pageMatch.route.id] ?? null;
1077
1433
  let pagePhase: RouteErrorPhase = "middleware";
@@ -1099,7 +1455,7 @@ export function createServer(
1099
1455
 
1100
1456
  const basePayload = {
1101
1457
  routeId: pageMatch.route.id,
1102
- data: null,
1458
+ loaderData: null,
1103
1459
  params: pageMatch.params,
1104
1460
  url: url.toString(),
1105
1461
  };
@@ -1155,7 +1511,7 @@ export function createServer(
1155
1511
 
1156
1512
  const renderPayload = {
1157
1513
  routeId: pageMatch.route.id,
1158
- data: null,
1514
+ loaderData: null,
1159
1515
  params: pageMatch.params,
1160
1516
  url: url.toString(),
1161
1517
  };
@@ -1188,75 +1544,55 @@ export function createServer(
1188
1544
  [...globalMiddleware, ...nestedMiddleware, ...moduleMiddleware],
1189
1545
  requestContext,
1190
1546
  async () => {
1191
- const method = request.method.toUpperCase();
1192
- let dataForRender: unknown = null;
1193
- let dataForPayload: unknown = null;
1547
+ let loaderDataForRender: unknown = null;
1548
+ let loaderDataForPayload: unknown = null;
1194
1549
  let deferredSettleEntries: DeferredSettleEntry[] = [];
1195
1550
 
1196
- if (isMutatingMethod(method)) {
1197
- if (!routeModules.route.action) {
1198
- return new Response("Method Not Allowed", { status: 405 });
1551
+ const resolveLoaderData = async (): Promise<Response | null> => {
1552
+ if (!routeModules.route.loader) {
1553
+ return null;
1199
1554
  }
1200
1555
 
1201
- pagePhase = "action";
1202
- const body = await parseActionBody(request.clone());
1203
- const actionCtx: ActionContext = {
1204
- ...requestContext,
1205
- ...body,
1206
- };
1207
-
1208
- const actionResult = await routeModules.route.action(actionCtx);
1556
+ pagePhase = "loader";
1557
+ const loaderCtx: LoaderContext = requestContext;
1558
+ const loaderResult = await routeModules.route.loader(loaderCtx);
1209
1559
 
1210
- if (isResponse(actionResult)) {
1211
- return actionResult;
1560
+ if (isResponse(loaderResult)) {
1561
+ return loaderResult;
1212
1562
  }
1213
1563
 
1214
- if (isRedirectResult(actionResult)) {
1215
- return toRedirectResponse(actionResult.location, actionResult.status);
1564
+ if (isRedirectResult(loaderResult)) {
1565
+ return toRedirectResponse(loaderResult.location, loaderResult.status);
1216
1566
  }
1217
1567
 
1218
- if (isDeferredLoaderResult(actionResult)) {
1219
- return new Response("defer() is only supported in route loaders", { status: 500 });
1568
+ if (isDeferredLoaderResult(loaderResult)) {
1569
+ const prepared = prepareDeferredPayload(pageMatch.route.id, loaderResult);
1570
+ loaderDataForRender = prepared.dataForRender;
1571
+ loaderDataForPayload = prepared.dataForPayload;
1572
+ deferredSettleEntries = prepared.settleEntries;
1573
+ } else {
1574
+ loaderDataForRender = loaderResult;
1575
+ loaderDataForPayload = loaderResult;
1220
1576
  }
1221
1577
 
1222
- dataForRender = actionResult;
1223
- dataForPayload = actionResult;
1224
- } else {
1225
- if (routeModules.route.loader) {
1226
- pagePhase = "loader";
1227
- const loaderCtx: LoaderContext = requestContext;
1228
- const loaderResult = await routeModules.route.loader(loaderCtx);
1229
-
1230
- if (isResponse(loaderResult)) {
1231
- return loaderResult;
1232
- }
1233
-
1234
- if (isRedirectResult(loaderResult)) {
1235
- return toRedirectResponse(loaderResult.location, loaderResult.status);
1236
- }
1578
+ return null;
1579
+ };
1237
1580
 
1238
- if (isDeferredLoaderResult(loaderResult)) {
1239
- const prepared = prepareDeferredPayload(pageMatch.route.id, loaderResult);
1240
- dataForRender = prepared.dataForRender;
1241
- dataForPayload = prepared.dataForPayload;
1242
- deferredSettleEntries = prepared.settleEntries;
1243
- } else {
1244
- dataForRender = loaderResult;
1245
- dataForPayload = loaderResult;
1246
- }
1247
- }
1581
+ const loaderResponse = await resolveLoaderData();
1582
+ if (loaderResponse) {
1583
+ return loaderResponse;
1248
1584
  }
1249
1585
 
1250
1586
  const renderPayload = {
1251
1587
  routeId: pageMatch.route.id,
1252
- data: dataForRender,
1588
+ loaderData: loaderDataForRender,
1253
1589
  params: pageMatch.params,
1254
1590
  url: url.toString(),
1255
1591
  };
1256
1592
 
1257
1593
  const clientPayload = {
1258
1594
  ...renderPayload,
1259
- data: dataForPayload,
1595
+ loaderData: loaderDataForPayload,
1260
1596
  };
1261
1597
 
1262
1598
  let appTree: ReturnType<typeof createPageAppTree>;
@@ -1287,14 +1623,14 @@ export function createServer(
1287
1623
  } catch (error) {
1288
1624
  const redirectResponse = resolveThrownRedirect(error);
1289
1625
  if (redirectResponse) {
1290
- return finalize(redirectResponse, "html");
1626
+ return finalize(redirectResponse, "html", requestContext);
1291
1627
  }
1292
1628
 
1293
1629
  const fallbackResponse = await renderFailureDocument(error, pagePhase);
1294
- return finalize(fallbackResponse, "html");
1630
+ return finalize(fallbackResponse, "html", requestContext);
1295
1631
  }
1296
1632
 
1297
- return finalize(response, "html");
1633
+ return finalize(response, "html", requestContext);
1298
1634
  };
1299
1635
 
1300
1636
  return {