rwsdk 1.0.0-beta.13-test.20251016112315 → 1.0.0-beta.15
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.
|
@@ -6,10 +6,23 @@ type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Ma
|
|
|
6
6
|
type MaybePromise<T> = T | Promise<T>;
|
|
7
7
|
type RouteComponent<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<React.JSX.Element | Response | void>;
|
|
8
8
|
type RouteHandler<T extends RequestInfo = RequestInfo> = RouteFunction<T> | RouteComponent<T> | [...RouteMiddleware<T>[], RouteFunction<T> | RouteComponent<T>];
|
|
9
|
+
declare const METHOD_VERBS: readonly ["delete", "get", "head", "patch", "post", "put"];
|
|
10
|
+
export type MethodVerb = (typeof METHOD_VERBS)[number];
|
|
11
|
+
export type MethodHandlers<T extends RequestInfo = RequestInfo> = {
|
|
12
|
+
[K in MethodVerb]?: RouteHandler<T>;
|
|
13
|
+
} & {
|
|
14
|
+
config?: {
|
|
15
|
+
disable405?: true;
|
|
16
|
+
disableOptions?: true;
|
|
17
|
+
};
|
|
18
|
+
custom?: {
|
|
19
|
+
[method: string]: RouteHandler<T>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
9
22
|
export type Route<T extends RequestInfo = RequestInfo> = RouteMiddleware<T> | RouteDefinition<T> | Array<Route<T>>;
|
|
10
23
|
export type RouteDefinition<T extends RequestInfo = RequestInfo> = {
|
|
11
24
|
path: string;
|
|
12
|
-
handler: RouteHandler<T>;
|
|
25
|
+
handler: RouteHandler<T> | MethodHandlers<T>;
|
|
13
26
|
layouts?: React.FC<LayoutProps<T>>[];
|
|
14
27
|
};
|
|
15
28
|
export declare function matchPath<T extends RequestInfo = RequestInfo>(routePath: string, requestPath: string): T["params"] | null;
|
|
@@ -24,7 +37,7 @@ export declare function defineRoutes<T extends RequestInfo = RequestInfo>(routes
|
|
|
24
37
|
rscActionHandler: (request: Request) => Promise<unknown>;
|
|
25
38
|
}) => Response | Promise<Response>;
|
|
26
39
|
};
|
|
27
|
-
export declare function route<T extends RequestInfo = RequestInfo>(path: string, handler: RouteHandler<T>): RouteDefinition<T>;
|
|
40
|
+
export declare function route<T extends RequestInfo = RequestInfo>(path: string, handler: RouteHandler<T> | MethodHandlers<T>): RouteDefinition<T>;
|
|
28
41
|
export declare function index<T extends RequestInfo = RequestInfo>(handler: RouteHandler<T>): RouteDefinition<T>;
|
|
29
42
|
export declare function prefix<T extends RequestInfo = RequestInfo>(prefixPath: string, routes: Route<T>[]): Route<T>[];
|
|
30
43
|
export declare const wrapHandlerToThrowResponses: <T extends RequestInfo = RequestInfo>(handler: RouteFunction<T> | RouteComponent<T>) => RouteHandler<T>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { isValidElementType } from "react-is";
|
|
3
|
+
const METHOD_VERBS = ["delete", "get", "head", "patch", "post", "put"];
|
|
3
4
|
export function matchPath(routePath, requestPath) {
|
|
4
5
|
// Check for invalid pattern: multiple colons in a segment (e.g., /:param1:param2/)
|
|
5
6
|
if (routePath.includes(":")) {
|
|
@@ -60,6 +61,38 @@ function flattenRoutes(routes) {
|
|
|
60
61
|
return [...acc, route];
|
|
61
62
|
}, []);
|
|
62
63
|
}
|
|
64
|
+
function isMethodHandlers(handler) {
|
|
65
|
+
return (typeof handler === "object" && handler !== null && !Array.isArray(handler));
|
|
66
|
+
}
|
|
67
|
+
function handleOptionsRequest(methodHandlers) {
|
|
68
|
+
const methods = new Set([
|
|
69
|
+
...(methodHandlers.config?.disableOptions ? [] : ["OPTIONS"]),
|
|
70
|
+
...METHOD_VERBS.filter((verb) => methodHandlers[verb]).map((verb) => verb.toUpperCase()),
|
|
71
|
+
...Object.keys(methodHandlers.custom ?? {}).map((method) => method.toUpperCase()),
|
|
72
|
+
]);
|
|
73
|
+
return new Response(null, {
|
|
74
|
+
status: 204,
|
|
75
|
+
headers: {
|
|
76
|
+
Allow: Array.from(methods).sort().join(", "),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function handleMethodNotAllowed(methodHandlers) {
|
|
81
|
+
const optionsResponse = handleOptionsRequest(methodHandlers);
|
|
82
|
+
return new Response("Method Not Allowed", {
|
|
83
|
+
status: 405,
|
|
84
|
+
headers: optionsResponse.headers,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function getHandlerForMethod(methodHandlers, method) {
|
|
88
|
+
const lowerMethod = method.toLowerCase();
|
|
89
|
+
// Check standard method verbs
|
|
90
|
+
if (METHOD_VERBS.includes(lowerMethod)) {
|
|
91
|
+
return methodHandlers[lowerMethod];
|
|
92
|
+
}
|
|
93
|
+
// Check custom methods (already normalized to lowercase)
|
|
94
|
+
return methodHandlers.custom?.[lowerMethod];
|
|
95
|
+
}
|
|
63
96
|
export function defineRoutes(routes) {
|
|
64
97
|
const flattenedRoutes = flattenRoutes(routes);
|
|
65
98
|
return {
|
|
@@ -125,9 +158,32 @@ export function defineRoutes(routes) {
|
|
|
125
158
|
if (!params) {
|
|
126
159
|
continue; // Not a match, keep going.
|
|
127
160
|
}
|
|
161
|
+
// Resolve handler if method-based routing
|
|
162
|
+
let handler;
|
|
163
|
+
if (isMethodHandlers(route.handler)) {
|
|
164
|
+
const requestMethod = request.method;
|
|
165
|
+
// Handle OPTIONS request
|
|
166
|
+
if (requestMethod === "OPTIONS" &&
|
|
167
|
+
!route.handler.config?.disableOptions) {
|
|
168
|
+
return handleOptionsRequest(route.handler);
|
|
169
|
+
}
|
|
170
|
+
// Try to find handler for the request method
|
|
171
|
+
handler = getHandlerForMethod(route.handler, requestMethod);
|
|
172
|
+
if (!handler) {
|
|
173
|
+
// Method not supported for this route
|
|
174
|
+
if (!route.handler.config?.disable405) {
|
|
175
|
+
return handleMethodNotAllowed(route.handler);
|
|
176
|
+
}
|
|
177
|
+
// If 405 is disabled, continue to next route
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
handler = route.handler;
|
|
183
|
+
}
|
|
128
184
|
// Found a match: run route-specific middlewares, then the final component, then stop.
|
|
129
185
|
return await runWithRequestInfoOverrides({ params }, async () => {
|
|
130
|
-
const { routeMiddlewares, componentHandler } = parseHandlers(
|
|
186
|
+
const { routeMiddlewares, componentHandler } = parseHandlers(handler);
|
|
131
187
|
// Route-specific middlewares
|
|
132
188
|
for (const mw of routeMiddlewares) {
|
|
133
189
|
const result = await mw(getRequestInfo());
|
|
@@ -169,6 +225,16 @@ export function route(path, handler) {
|
|
|
169
225
|
if (!path.endsWith("/")) {
|
|
170
226
|
path = path + "/";
|
|
171
227
|
}
|
|
228
|
+
// Normalize custom method keys to lowercase
|
|
229
|
+
if (isMethodHandlers(handler) && handler.custom) {
|
|
230
|
+
handler = {
|
|
231
|
+
...handler,
|
|
232
|
+
custom: Object.fromEntries(Object.entries(handler.custom).map(([method, methodHandler]) => [
|
|
233
|
+
method.toLowerCase(),
|
|
234
|
+
methodHandler,
|
|
235
|
+
])),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
172
238
|
return {
|
|
173
239
|
path,
|
|
174
240
|
handler,
|
|
@@ -595,6 +595,245 @@ describe("defineRoutes - Request Handling Behavior", () => {
|
|
|
595
595
|
expect(extractedParams).toEqual({ id: "123" });
|
|
596
596
|
});
|
|
597
597
|
});
|
|
598
|
+
describe("HTTP Method Routing", () => {
|
|
599
|
+
it("should route GET request to get handler", async () => {
|
|
600
|
+
const router = defineRoutes([
|
|
601
|
+
route("/test/", {
|
|
602
|
+
get: () => new Response("GET Response"),
|
|
603
|
+
post: () => new Response("POST Response"),
|
|
604
|
+
}),
|
|
605
|
+
]);
|
|
606
|
+
const deps = createMockDependencies();
|
|
607
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
608
|
+
method: "GET",
|
|
609
|
+
});
|
|
610
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
611
|
+
method: "GET",
|
|
612
|
+
});
|
|
613
|
+
const response = await router.handle({
|
|
614
|
+
request,
|
|
615
|
+
renderPage: deps.mockRenderPage,
|
|
616
|
+
getRequestInfo: deps.getRequestInfo,
|
|
617
|
+
onError: deps.onError,
|
|
618
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
619
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
620
|
+
});
|
|
621
|
+
expect(await response.text()).toBe("GET Response");
|
|
622
|
+
});
|
|
623
|
+
it("should route POST request to post handler", async () => {
|
|
624
|
+
const router = defineRoutes([
|
|
625
|
+
route("/test/", {
|
|
626
|
+
get: () => new Response("GET Response"),
|
|
627
|
+
post: () => new Response("POST Response"),
|
|
628
|
+
}),
|
|
629
|
+
]);
|
|
630
|
+
const deps = createMockDependencies();
|
|
631
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
632
|
+
method: "POST",
|
|
633
|
+
});
|
|
634
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
635
|
+
method: "POST",
|
|
636
|
+
});
|
|
637
|
+
const response = await router.handle({
|
|
638
|
+
request,
|
|
639
|
+
renderPage: deps.mockRenderPage,
|
|
640
|
+
getRequestInfo: deps.getRequestInfo,
|
|
641
|
+
onError: deps.onError,
|
|
642
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
643
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
644
|
+
});
|
|
645
|
+
expect(await response.text()).toBe("POST Response");
|
|
646
|
+
});
|
|
647
|
+
it("should return 405 for unsupported method with Allow header", async () => {
|
|
648
|
+
const router = defineRoutes([
|
|
649
|
+
route("/test/", {
|
|
650
|
+
get: () => new Response("GET Response"),
|
|
651
|
+
post: () => new Response("POST Response"),
|
|
652
|
+
}),
|
|
653
|
+
]);
|
|
654
|
+
const deps = createMockDependencies();
|
|
655
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
656
|
+
method: "DELETE",
|
|
657
|
+
});
|
|
658
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
659
|
+
method: "DELETE",
|
|
660
|
+
});
|
|
661
|
+
const response = await router.handle({
|
|
662
|
+
request,
|
|
663
|
+
renderPage: deps.mockRenderPage,
|
|
664
|
+
getRequestInfo: deps.getRequestInfo,
|
|
665
|
+
onError: deps.onError,
|
|
666
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
667
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
668
|
+
});
|
|
669
|
+
expect(response.status).toBe(405);
|
|
670
|
+
expect(await response.text()).toBe("Method Not Allowed");
|
|
671
|
+
expect(response.headers.get("Allow")).toBe("GET, OPTIONS, POST");
|
|
672
|
+
});
|
|
673
|
+
it("should handle OPTIONS request with Allow header", async () => {
|
|
674
|
+
const router = defineRoutes([
|
|
675
|
+
route("/test/", {
|
|
676
|
+
get: () => new Response("GET Response"),
|
|
677
|
+
post: () => new Response("POST Response"),
|
|
678
|
+
}),
|
|
679
|
+
]);
|
|
680
|
+
const deps = createMockDependencies();
|
|
681
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
682
|
+
method: "OPTIONS",
|
|
683
|
+
});
|
|
684
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
685
|
+
method: "OPTIONS",
|
|
686
|
+
});
|
|
687
|
+
const response = await router.handle({
|
|
688
|
+
request,
|
|
689
|
+
renderPage: deps.mockRenderPage,
|
|
690
|
+
getRequestInfo: deps.getRequestInfo,
|
|
691
|
+
onError: deps.onError,
|
|
692
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
693
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
694
|
+
});
|
|
695
|
+
expect(response.status).toBe(204);
|
|
696
|
+
expect(response.headers.get("Allow")).toBe("GET, OPTIONS, POST");
|
|
697
|
+
});
|
|
698
|
+
it("should support custom methods (case-insensitive)", async () => {
|
|
699
|
+
const router = defineRoutes([
|
|
700
|
+
route("/test/", {
|
|
701
|
+
custom: {
|
|
702
|
+
report: () => new Response("REPORT Response"),
|
|
703
|
+
},
|
|
704
|
+
}),
|
|
705
|
+
]);
|
|
706
|
+
const deps = createMockDependencies();
|
|
707
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
708
|
+
method: "REPORT",
|
|
709
|
+
});
|
|
710
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
711
|
+
method: "REPORT",
|
|
712
|
+
});
|
|
713
|
+
const response = await router.handle({
|
|
714
|
+
request,
|
|
715
|
+
renderPage: deps.mockRenderPage,
|
|
716
|
+
getRequestInfo: deps.getRequestInfo,
|
|
717
|
+
onError: deps.onError,
|
|
718
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
719
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
720
|
+
});
|
|
721
|
+
expect(await response.text()).toBe("REPORT Response");
|
|
722
|
+
});
|
|
723
|
+
it("should normalize custom method keys to lowercase", async () => {
|
|
724
|
+
const router = defineRoutes([
|
|
725
|
+
route("/test/", {
|
|
726
|
+
custom: {
|
|
727
|
+
REPORT: () => new Response("REPORT Response"),
|
|
728
|
+
},
|
|
729
|
+
}),
|
|
730
|
+
]);
|
|
731
|
+
const deps = createMockDependencies();
|
|
732
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
733
|
+
method: "report",
|
|
734
|
+
});
|
|
735
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
736
|
+
method: "report",
|
|
737
|
+
});
|
|
738
|
+
const response = await router.handle({
|
|
739
|
+
request,
|
|
740
|
+
renderPage: deps.mockRenderPage,
|
|
741
|
+
getRequestInfo: deps.getRequestInfo,
|
|
742
|
+
onError: deps.onError,
|
|
743
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
744
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
745
|
+
});
|
|
746
|
+
expect(await response.text()).toBe("REPORT Response");
|
|
747
|
+
});
|
|
748
|
+
it("should disable 405 when config.disable405 is true", async () => {
|
|
749
|
+
const router = defineRoutes([
|
|
750
|
+
route("/test/", {
|
|
751
|
+
get: () => new Response("GET Response"),
|
|
752
|
+
config: {
|
|
753
|
+
disable405: true,
|
|
754
|
+
},
|
|
755
|
+
}),
|
|
756
|
+
]);
|
|
757
|
+
const deps = createMockDependencies();
|
|
758
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
759
|
+
method: "POST",
|
|
760
|
+
});
|
|
761
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
762
|
+
method: "POST",
|
|
763
|
+
});
|
|
764
|
+
const response = await router.handle({
|
|
765
|
+
request,
|
|
766
|
+
renderPage: deps.mockRenderPage,
|
|
767
|
+
getRequestInfo: deps.getRequestInfo,
|
|
768
|
+
onError: deps.onError,
|
|
769
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
770
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
771
|
+
});
|
|
772
|
+
expect(response.status).toBe(404);
|
|
773
|
+
expect(await response.text()).toBe("Not Found");
|
|
774
|
+
});
|
|
775
|
+
it("should disable OPTIONS when config.disableOptions is true", async () => {
|
|
776
|
+
const router = defineRoutes([
|
|
777
|
+
route("/test/", {
|
|
778
|
+
get: () => new Response("GET Response"),
|
|
779
|
+
post: () => new Response("POST Response"),
|
|
780
|
+
config: {
|
|
781
|
+
disableOptions: true,
|
|
782
|
+
},
|
|
783
|
+
}),
|
|
784
|
+
]);
|
|
785
|
+
const deps = createMockDependencies();
|
|
786
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
787
|
+
method: "OPTIONS",
|
|
788
|
+
});
|
|
789
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
790
|
+
method: "OPTIONS",
|
|
791
|
+
});
|
|
792
|
+
const response = await router.handle({
|
|
793
|
+
request,
|
|
794
|
+
renderPage: deps.mockRenderPage,
|
|
795
|
+
getRequestInfo: deps.getRequestInfo,
|
|
796
|
+
onError: deps.onError,
|
|
797
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
798
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
799
|
+
});
|
|
800
|
+
expect(response.status).toBe(405);
|
|
801
|
+
expect(await response.text()).toBe("Method Not Allowed");
|
|
802
|
+
expect(response.headers.get("Allow")).toBe("GET, POST");
|
|
803
|
+
});
|
|
804
|
+
it("should support middleware arrays in method handlers", async () => {
|
|
805
|
+
const executionOrder = [];
|
|
806
|
+
const authMiddleware = () => {
|
|
807
|
+
executionOrder.push("authMiddleware");
|
|
808
|
+
};
|
|
809
|
+
const getHandler = () => {
|
|
810
|
+
executionOrder.push("getHandler");
|
|
811
|
+
return new Response("GET Response");
|
|
812
|
+
};
|
|
813
|
+
const router = defineRoutes([
|
|
814
|
+
route("/test/", {
|
|
815
|
+
get: [authMiddleware, getHandler],
|
|
816
|
+
}),
|
|
817
|
+
]);
|
|
818
|
+
const deps = createMockDependencies();
|
|
819
|
+
deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", {
|
|
820
|
+
method: "GET",
|
|
821
|
+
});
|
|
822
|
+
const request = new Request("http://localhost:3000/test/", {
|
|
823
|
+
method: "GET",
|
|
824
|
+
});
|
|
825
|
+
const response = await router.handle({
|
|
826
|
+
request,
|
|
827
|
+
renderPage: deps.mockRenderPage,
|
|
828
|
+
getRequestInfo: deps.getRequestInfo,
|
|
829
|
+
onError: deps.onError,
|
|
830
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
831
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
832
|
+
});
|
|
833
|
+
expect(executionOrder).toEqual(["authMiddleware", "getHandler"]);
|
|
834
|
+
expect(await response.text()).toBe("GET Response");
|
|
835
|
+
});
|
|
836
|
+
});
|
|
598
837
|
describe("Edge Cases", () => {
|
|
599
838
|
it("should handle middleware-only apps with RSC actions", async () => {
|
|
600
839
|
const executionOrder = [];
|