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.
- package/README.md +37 -5
- package/framework/cli/dev-runtime.ts +18 -1
- package/framework/runtime/action-stub.ts +26 -0
- package/framework/runtime/client-runtime.tsx +3 -3
- package/framework/runtime/helpers.ts +75 -1
- package/framework/runtime/index.ts +53 -23
- package/framework/runtime/module-loader.ts +197 -35
- package/framework/runtime/render.tsx +1 -1
- package/framework/runtime/response-context.ts +206 -0
- package/framework/runtime/route-api.ts +51 -18
- package/framework/runtime/route-scanner.ts +104 -12
- package/framework/runtime/server.ts +427 -91
- package/framework/runtime/tree.tsx +120 -4
- package/framework/runtime/types.ts +71 -10
- package/package.json +1 -1
|
@@ -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 = (
|
|
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:
|
|
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
|
-
|
|
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
|
|
1082
|
+
const requestContext = createRequestContext({
|
|
739
1083
|
request: transitionRequest,
|
|
740
1084
|
url: targetUrl,
|
|
741
1085
|
params: transitionPageMatch.params,
|
|
742
|
-
|
|
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
|
|
757
|
-
let
|
|
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
|
-
|
|
775
|
-
|
|
1116
|
+
loaderDataForRender = prepared.dataForRender;
|
|
1117
|
+
loaderDataForPayload = prepared.dataForPayload;
|
|
776
1118
|
deferredSettleEntries = prepared.settleEntries;
|
|
777
1119
|
} else {
|
|
778
|
-
|
|
779
|
-
|
|
1120
|
+
loaderDataForRender = loaderResult;
|
|
1121
|
+
loaderDataForPayload = loaderResult;
|
|
780
1122
|
}
|
|
781
1123
|
}
|
|
782
1124
|
|
|
783
1125
|
const renderPayload = {
|
|
784
1126
|
routeId: transitionPageMatch.route.id,
|
|
785
|
-
|
|
1127
|
+
loaderData: loaderDataForRender,
|
|
786
1128
|
params: transitionPageMatch.params,
|
|
787
1129
|
url: targetUrl.toString(),
|
|
788
1130
|
};
|
|
789
1131
|
const payload = {
|
|
790
1132
|
...renderPayload,
|
|
791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1287
|
+
const requestContext = createRequestContext({
|
|
946
1288
|
request,
|
|
947
1289
|
url,
|
|
948
1290
|
params: apiMatch.params,
|
|
949
|
-
|
|
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
|
-
|
|
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
|
|
1426
|
+
const requestContext = createRequestContext({
|
|
1069
1427
|
request,
|
|
1070
1428
|
url,
|
|
1071
1429
|
params: pageMatch.params,
|
|
1072
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1192
|
-
let
|
|
1193
|
-
let dataForPayload: unknown = null;
|
|
1547
|
+
let loaderDataForRender: unknown = null;
|
|
1548
|
+
let loaderDataForPayload: unknown = null;
|
|
1194
1549
|
let deferredSettleEntries: DeferredSettleEntry[] = [];
|
|
1195
1550
|
|
|
1196
|
-
|
|
1197
|
-
if (!routeModules.route.
|
|
1198
|
-
return
|
|
1551
|
+
const resolveLoaderData = async (): Promise<Response | null> => {
|
|
1552
|
+
if (!routeModules.route.loader) {
|
|
1553
|
+
return null;
|
|
1199
1554
|
}
|
|
1200
1555
|
|
|
1201
|
-
pagePhase = "
|
|
1202
|
-
const
|
|
1203
|
-
const
|
|
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(
|
|
1211
|
-
return
|
|
1560
|
+
if (isResponse(loaderResult)) {
|
|
1561
|
+
return loaderResult;
|
|
1212
1562
|
}
|
|
1213
1563
|
|
|
1214
|
-
if (isRedirectResult(
|
|
1215
|
-
return toRedirectResponse(
|
|
1564
|
+
if (isRedirectResult(loaderResult)) {
|
|
1565
|
+
return toRedirectResponse(loaderResult.location, loaderResult.status);
|
|
1216
1566
|
}
|
|
1217
1567
|
|
|
1218
|
-
if (isDeferredLoaderResult(
|
|
1219
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|