Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dfd2d14

Browse files
Port Remix Architect package (remix-run#11804)
1 parent d09ff87 commit dfd2d14

16 files changed

+958
-2
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# React Router Architect
2+
3+
Architect server request handler for React Router.
4+
5+
```bash
6+
npm install @react-router/architect
7+
```
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { isBinaryType } from "../binaryTypes";
2+
3+
describe("architect isBinaryType", () => {
4+
it("should detect binary contentType correctly", () => {
5+
expect(isBinaryType(undefined)).toBe(false);
6+
expect(isBinaryType(null)).toBe(false);
7+
expect(isBinaryType("text/html; charset=utf-8")).toBe(false);
8+
expect(isBinaryType("application/octet-stream")).toBe(true);
9+
expect(isBinaryType("application/octet-stream; charset=test")).toBe(true);
10+
});
11+
});
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import fsp from "node:fs/promises";
2+
import path from "node:path";
3+
import { createRequestHandler as createReactRequestHandler } from "react-router";
4+
import type {
5+
APIGatewayProxyEventV2,
6+
APIGatewayProxyStructuredResultV2,
7+
} from "aws-lambda";
8+
import lambdaTester from "lambda-tester";
9+
10+
import {
11+
createRequestHandler,
12+
createReactRouterHeaders,
13+
createReactRouterRequest,
14+
sendReactRouterResponse,
15+
} from "../server";
16+
17+
// We don't want to test that the React Router server works here,
18+
// we just want to test the architect adapter
19+
jest.mock("react-router", () => {
20+
let original = jest.requireActual("react-router");
21+
return {
22+
...original,
23+
createRequestHandler: jest.fn(),
24+
};
25+
});
26+
let mockedCreateRequestHandler =
27+
createReactRequestHandler as jest.MockedFunction<
28+
typeof createReactRequestHandler
29+
>;
30+
31+
function createMockEvent(event: Partial<APIGatewayProxyEventV2> = {}) {
32+
let now = new Date();
33+
return {
34+
headers: {
35+
host: "localhost:3333",
36+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
37+
"upgrade-insecure-requests": "1",
38+
"user-agent":
39+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15",
40+
"accept-language": "en-US,en;q=0.9",
41+
"accept-encoding": "gzip, deflate",
42+
...event.headers,
43+
},
44+
isBase64Encoded: false,
45+
rawPath: "/",
46+
rawQueryString: "",
47+
requestContext: {
48+
http: {
49+
method: "GET",
50+
path: "/",
51+
protocol: "HTTP/1.1",
52+
userAgent:
53+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15",
54+
sourceIp: "127.0.0.1",
55+
...event.requestContext?.http,
56+
},
57+
routeKey: "ANY /{proxy+}",
58+
accountId: "accountId",
59+
requestId: "requestId",
60+
apiId: "apiId",
61+
domainName: "id.execute-api.us-east-1.amazonaws.com",
62+
domainPrefix: "id",
63+
stage: "test",
64+
time: now.toISOString(),
65+
timeEpoch: now.getTime(),
66+
...event.requestContext,
67+
},
68+
routeKey: "foo",
69+
version: "2.0",
70+
...event,
71+
};
72+
}
73+
74+
describe("architect createRequestHandler", () => {
75+
describe("basic requests", () => {
76+
afterEach(() => {
77+
mockedCreateRequestHandler.mockReset();
78+
});
79+
80+
afterAll(() => {
81+
jest.restoreAllMocks();
82+
});
83+
84+
it("handles requests", async () => {
85+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
86+
return new Response(`URL: ${new URL(req.url).pathname}`);
87+
});
88+
89+
// We don't have a real app to test, but it doesn't matter. We won't ever
90+
// call through to the real createRequestHandler
91+
// @ts-expect-error
92+
await lambdaTester(createRequestHandler({ build: undefined }))
93+
.event(createMockEvent({ rawPath: "/foo/bar" }))
94+
.expectResolve((res: any) => {
95+
expect(res.statusCode).toBe(200);
96+
expect(res.body).toBe("URL: /foo/bar");
97+
});
98+
});
99+
100+
it("handles root // requests", async () => {
101+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
102+
return new Response(`URL: ${new URL(req.url).pathname}`);
103+
});
104+
105+
// We don't have a real app to test, but it doesn't matter. We won't ever
106+
// call through to the real createRequestHandler
107+
// @ts-expect-error
108+
await lambdaTester(createRequestHandler({ build: undefined }))
109+
.event(createMockEvent({ rawPath: "//" }))
110+
.expectResolve((res: any) => {
111+
expect(res.statusCode).toBe(200);
112+
expect(res.body).toBe("URL: //");
113+
});
114+
});
115+
116+
it("handles nested // requests", async () => {
117+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
118+
return new Response(`URL: ${new URL(req.url).pathname}`);
119+
});
120+
121+
// We don't have a real app to test, but it doesn't matter. We won't ever
122+
// call through to the real createRequestHandler
123+
// @ts-expect-error
124+
await lambdaTester(createRequestHandler({ build: undefined }))
125+
.event(createMockEvent({ rawPath: "//foo//bar" }))
126+
.expectResolve((res: APIGatewayProxyStructuredResultV2) => {
127+
expect(res.statusCode).toBe(200);
128+
expect(res.body).toBe("URL: //foo//bar");
129+
});
130+
});
131+
132+
it("handles null body", async () => {
133+
mockedCreateRequestHandler.mockImplementation(() => async () => {
134+
return new Response(null, { status: 200 });
135+
});
136+
137+
// We don't have a real app to test, but it doesn't matter. We won't ever
138+
// call through to the real createRequestHandler
139+
// @ts-expect-error
140+
await lambdaTester(createRequestHandler({ build: undefined }))
141+
.event(createMockEvent({ rawPath: "/foo/bar" }))
142+
.expectResolve((res: APIGatewayProxyStructuredResultV2) => {
143+
expect(res.statusCode).toBe(200);
144+
});
145+
});
146+
147+
it("handles status codes", async () => {
148+
mockedCreateRequestHandler.mockImplementation(() => async () => {
149+
return new Response(null, { status: 204 });
150+
});
151+
152+
// We don't have a real app to test, but it doesn't matter. We won't ever
153+
// call through to the real createRequestHandler
154+
// @ts-expect-error
155+
await lambdaTester(createRequestHandler({ build: undefined }))
156+
.event(createMockEvent({ rawPath: "/foo/bar" }))
157+
.expectResolve((res: APIGatewayProxyStructuredResultV2) => {
158+
expect(res.statusCode).toBe(204);
159+
});
160+
});
161+
162+
it("sets headers", async () => {
163+
mockedCreateRequestHandler.mockImplementation(() => async () => {
164+
let headers = new Headers();
165+
headers.append("X-Time-Of-Year", "most wonderful");
166+
headers.append(
167+
"Set-Cookie",
168+
"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax"
169+
);
170+
headers.append(
171+
"Set-Cookie",
172+
"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax"
173+
);
174+
headers.append(
175+
"Set-Cookie",
176+
"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax"
177+
);
178+
179+
return new Response(null, { headers });
180+
});
181+
182+
// We don't have a real app to test, but it doesn't matter. We won't ever
183+
// call through to the real createRequestHandler
184+
// @ts-expect-error
185+
await lambdaTester(createRequestHandler({ build: undefined }))
186+
.event(createMockEvent({ rawPath: "/" }))
187+
.expectResolve((res: APIGatewayProxyStructuredResultV2) => {
188+
expect(res.statusCode).toBe(200);
189+
expect(res.headers?.["x-time-of-year"]).toBe("most wonderful");
190+
expect(res.cookies).toEqual([
191+
"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax",
192+
"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax",
193+
"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax",
194+
]);
195+
});
196+
});
197+
});
198+
});
199+
200+
describe("architect createReactRouterHeaders", () => {
201+
describe("creates fetch headers from architect headers", () => {
202+
it("handles empty headers", () => {
203+
let headers = createReactRouterHeaders({});
204+
expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`{}`);
205+
});
206+
207+
it("handles simple headers", () => {
208+
let headers = createReactRouterHeaders({ "x-foo": "bar" });
209+
expect(headers.get("x-foo")).toBe("bar");
210+
});
211+
212+
it("handles multiple headers", () => {
213+
let headers = createReactRouterHeaders({
214+
"x-foo": "bar",
215+
"x-bar": "baz",
216+
});
217+
expect(headers.get("x-foo")).toBe("bar");
218+
expect(headers.get("x-bar")).toBe("baz");
219+
});
220+
221+
it("handles headers with multiple values", () => {
222+
let headers = createReactRouterHeaders({
223+
"x-foo": "bar, baz",
224+
"x-bar": "baz",
225+
});
226+
expect(headers.get("x-foo")).toEqual("bar, baz");
227+
expect(headers.get("x-bar")).toBe("baz");
228+
});
229+
230+
it("handles multiple request cookies", () => {
231+
let headers = createReactRouterHeaders({}, [
232+
"__session=some_value",
233+
"__other=some_other_value",
234+
]);
235+
expect(headers.get("cookie")).toEqual(
236+
"__session=some_value; __other=some_other_value"
237+
);
238+
});
239+
});
240+
});
241+
242+
describe("architect createReactRouterRequest", () => {
243+
it("creates a request with the correct headers", () => {
244+
let request = createReactRouterRequest(
245+
createMockEvent({ cookies: ["__session=value"] })
246+
);
247+
248+
expect(request.method).toBe("GET");
249+
expect(request.headers.get("cookie")).toBe("__session=value");
250+
});
251+
});
252+
253+
describe("sendReactRouterResponse", () => {
254+
it("handles regular responses", async () => {
255+
let response = new Response("anything");
256+
let result = await sendReactRouterResponse(response);
257+
expect(result.body).toBe("anything");
258+
});
259+
260+
it("handles resource routes with regular data", async () => {
261+
let json = JSON.stringify({ foo: "bar" });
262+
let response = new Response(json, {
263+
headers: {
264+
"Content-Type": "application/json",
265+
"content-length": json.length.toString(),
266+
},
267+
});
268+
269+
let result = await sendReactRouterResponse(response);
270+
271+
expect(result.body).toMatch(json);
272+
});
273+
274+
it("handles resource routes with binary data", async () => {
275+
let image = await fsp.readFile(path.join(__dirname, "554828.jpeg"));
276+
277+
let response = new Response(image, {
278+
headers: {
279+
"content-type": "image/jpeg",
280+
"content-length": image.length.toString(),
281+
},
282+
});
283+
284+
let result = await sendReactRouterResponse(response);
285+
286+
expect(result.body).toMatch(image.toString("base64"));
287+
});
288+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { installGlobals } from "@react-router/node";
2+
installGlobals();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Common binary MIME types
3+
* @see https://github.com/architect/functions/blob/45254fc1936a1794c185aac07e9889b241a2e5c6/src/http/helpers/binary-types.js
4+
*/
5+
const binaryTypes = [
6+
"application/octet-stream",
7+
// Docs
8+
"application/epub+zip",
9+
"application/msword",
10+
"application/pdf",
11+
"application/rtf",
12+
"application/vnd.amazon.ebook",
13+
"application/vnd.ms-excel",
14+
"application/vnd.ms-powerpoint",
15+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
16+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
17+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
18+
// Fonts
19+
"font/otf",
20+
"font/woff",
21+
"font/woff2",
22+
// Images
23+
"image/avif",
24+
"image/bmp",
25+
"image/gif",
26+
"image/jpeg",
27+
"image/png",
28+
"image/tiff",
29+
"image/vnd.microsoft.icon",
30+
"image/webp",
31+
// Audio
32+
"audio/3gpp",
33+
"audio/aac",
34+
"audio/basic",
35+
"audio/mpeg",
36+
"audio/ogg",
37+
"audio/wav",
38+
"audio/webm",
39+
"audio/x-aiff",
40+
"audio/x-midi",
41+
"audio/x-wav",
42+
// Video
43+
"video/3gpp",
44+
"video/mp2t",
45+
"video/mpeg",
46+
"video/ogg",
47+
"video/quicktime",
48+
"video/webm",
49+
"video/x-msvideo",
50+
// Archives
51+
"application/java-archive",
52+
"application/vnd.apple.installer+xml",
53+
"application/x-7z-compressed",
54+
"application/x-apple-diskimage",
55+
"application/x-bzip",
56+
"application/x-bzip2",
57+
"application/x-gzip",
58+
"application/x-java-archive",
59+
"application/x-rar-compressed",
60+
"application/x-tar",
61+
"application/x-zip",
62+
"application/zip",
63+
];
64+
65+
export function isBinaryType(contentType: string | null | undefined) {
66+
if (!contentType) return false;
67+
let [test] = contentType.split(";");
68+
return binaryTypes.includes(test);
69+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { createArcTableSessionStorage } from "./sessions/arcTableSessionStorage";
2+
3+
export type { GetLoadContextFunction, RequestHandler } from "./server";
4+
export { createRequestHandler } from "./server";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
...require("../../jest/jest.config.shared"),
4+
displayName: "architect",
5+
};

0 commit comments

Comments
 (0)