-
-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Port Remix Architect package (#11804)
- Loading branch information
1 parent
d09ff87
commit dfd2d14
Showing
16 changed files
with
958 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# React Router Architect | ||
|
||
Architect server request handler for React Router. | ||
|
||
```bash | ||
npm install @react-router/architect | ||
``` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions
11
packages/react-router-architect/__tests__/binaryTypes-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { isBinaryType } from "../binaryTypes"; | ||
|
||
describe("architect isBinaryType", () => { | ||
it("should detect binary contentType correctly", () => { | ||
expect(isBinaryType(undefined)).toBe(false); | ||
expect(isBinaryType(null)).toBe(false); | ||
expect(isBinaryType("text/html; charset=utf-8")).toBe(false); | ||
expect(isBinaryType("application/octet-stream")).toBe(true); | ||
expect(isBinaryType("application/octet-stream; charset=test")).toBe(true); | ||
}); | ||
}); |
288 changes: 288 additions & 0 deletions
288
packages/react-router-architect/__tests__/server-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
import fsp from "node:fs/promises"; | ||
import path from "node:path"; | ||
import { createRequestHandler as createReactRequestHandler } from "react-router"; | ||
import type { | ||
APIGatewayProxyEventV2, | ||
APIGatewayProxyStructuredResultV2, | ||
} from "aws-lambda"; | ||
import lambdaTester from "lambda-tester"; | ||
|
||
import { | ||
createRequestHandler, | ||
createReactRouterHeaders, | ||
createReactRouterRequest, | ||
sendReactRouterResponse, | ||
} from "../server"; | ||
|
||
// We don't want to test that the React Router server works here, | ||
// we just want to test the architect adapter | ||
jest.mock("react-router", () => { | ||
let original = jest.requireActual("react-router"); | ||
return { | ||
...original, | ||
createRequestHandler: jest.fn(), | ||
}; | ||
}); | ||
let mockedCreateRequestHandler = | ||
createReactRequestHandler as jest.MockedFunction< | ||
typeof createReactRequestHandler | ||
>; | ||
|
||
function createMockEvent(event: Partial<APIGatewayProxyEventV2> = {}) { | ||
let now = new Date(); | ||
return { | ||
headers: { | ||
host: "localhost:3333", | ||
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", | ||
"upgrade-insecure-requests": "1", | ||
"user-agent": | ||
"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", | ||
"accept-language": "en-US,en;q=0.9", | ||
"accept-encoding": "gzip, deflate", | ||
...event.headers, | ||
}, | ||
isBase64Encoded: false, | ||
rawPath: "/", | ||
rawQueryString: "", | ||
requestContext: { | ||
http: { | ||
method: "GET", | ||
path: "/", | ||
protocol: "HTTP/1.1", | ||
userAgent: | ||
"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", | ||
sourceIp: "127.0.0.1", | ||
...event.requestContext?.http, | ||
}, | ||
routeKey: "ANY /{proxy+}", | ||
accountId: "accountId", | ||
requestId: "requestId", | ||
apiId: "apiId", | ||
domainName: "id.execute-api.us-east-1.amazonaws.com", | ||
domainPrefix: "id", | ||
stage: "test", | ||
time: now.toISOString(), | ||
timeEpoch: now.getTime(), | ||
...event.requestContext, | ||
}, | ||
routeKey: "foo", | ||
version: "2.0", | ||
...event, | ||
}; | ||
} | ||
|
||
describe("architect createRequestHandler", () => { | ||
describe("basic requests", () => { | ||
afterEach(() => { | ||
mockedCreateRequestHandler.mockReset(); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it("handles requests", async () => { | ||
mockedCreateRequestHandler.mockImplementation(() => async (req) => { | ||
return new Response(`URL: ${new URL(req.url).pathname}`); | ||
}); | ||
|
||
// We don't have a real app to test, but it doesn't matter. We won't ever | ||
// call through to the real createRequestHandler | ||
// @ts-expect-error | ||
await lambdaTester(createRequestHandler({ build: undefined })) | ||
.event(createMockEvent({ rawPath: "/foo/bar" })) | ||
.expectResolve((res: any) => { | ||
expect(res.statusCode).toBe(200); | ||
expect(res.body).toBe("URL: /foo/bar"); | ||
}); | ||
}); | ||
|
||
it("handles root // requests", async () => { | ||
mockedCreateRequestHandler.mockImplementation(() => async (req) => { | ||
return new Response(`URL: ${new URL(req.url).pathname}`); | ||
}); | ||
|
||
// We don't have a real app to test, but it doesn't matter. We won't ever | ||
// call through to the real createRequestHandler | ||
// @ts-expect-error | ||
await lambdaTester(createRequestHandler({ build: undefined })) | ||
.event(createMockEvent({ rawPath: "//" })) | ||
.expectResolve((res: any) => { | ||
expect(res.statusCode).toBe(200); | ||
expect(res.body).toBe("URL: //"); | ||
}); | ||
}); | ||
|
||
it("handles nested // requests", async () => { | ||
mockedCreateRequestHandler.mockImplementation(() => async (req) => { | ||
return new Response(`URL: ${new URL(req.url).pathname}`); | ||
}); | ||
|
||
// We don't have a real app to test, but it doesn't matter. We won't ever | ||
// call through to the real createRequestHandler | ||
// @ts-expect-error | ||
await lambdaTester(createRequestHandler({ build: undefined })) | ||
.event(createMockEvent({ rawPath: "//foo//bar" })) | ||
.expectResolve((res: APIGatewayProxyStructuredResultV2) => { | ||
expect(res.statusCode).toBe(200); | ||
expect(res.body).toBe("URL: //foo//bar"); | ||
}); | ||
}); | ||
|
||
it("handles null body", async () => { | ||
mockedCreateRequestHandler.mockImplementation(() => async () => { | ||
return new Response(null, { status: 200 }); | ||
}); | ||
|
||
// We don't have a real app to test, but it doesn't matter. We won't ever | ||
// call through to the real createRequestHandler | ||
// @ts-expect-error | ||
await lambdaTester(createRequestHandler({ build: undefined })) | ||
.event(createMockEvent({ rawPath: "/foo/bar" })) | ||
.expectResolve((res: APIGatewayProxyStructuredResultV2) => { | ||
expect(res.statusCode).toBe(200); | ||
}); | ||
}); | ||
|
||
it("handles status codes", async () => { | ||
mockedCreateRequestHandler.mockImplementation(() => async () => { | ||
return new Response(null, { status: 204 }); | ||
}); | ||
|
||
// We don't have a real app to test, but it doesn't matter. We won't ever | ||
// call through to the real createRequestHandler | ||
// @ts-expect-error | ||
await lambdaTester(createRequestHandler({ build: undefined })) | ||
.event(createMockEvent({ rawPath: "/foo/bar" })) | ||
.expectResolve((res: APIGatewayProxyStructuredResultV2) => { | ||
expect(res.statusCode).toBe(204); | ||
}); | ||
}); | ||
|
||
it("sets headers", async () => { | ||
mockedCreateRequestHandler.mockImplementation(() => async () => { | ||
let headers = new Headers(); | ||
headers.append("X-Time-Of-Year", "most wonderful"); | ||
headers.append( | ||
"Set-Cookie", | ||
"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax" | ||
); | ||
headers.append( | ||
"Set-Cookie", | ||
"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax" | ||
); | ||
headers.append( | ||
"Set-Cookie", | ||
"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax" | ||
); | ||
|
||
return new Response(null, { headers }); | ||
}); | ||
|
||
// We don't have a real app to test, but it doesn't matter. We won't ever | ||
// call through to the real createRequestHandler | ||
// @ts-expect-error | ||
await lambdaTester(createRequestHandler({ build: undefined })) | ||
.event(createMockEvent({ rawPath: "/" })) | ||
.expectResolve((res: APIGatewayProxyStructuredResultV2) => { | ||
expect(res.statusCode).toBe(200); | ||
expect(res.headers?.["x-time-of-year"]).toBe("most wonderful"); | ||
expect(res.cookies).toEqual([ | ||
"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax", | ||
"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax", | ||
"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax", | ||
]); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("architect createReactRouterHeaders", () => { | ||
describe("creates fetch headers from architect headers", () => { | ||
it("handles empty headers", () => { | ||
let headers = createReactRouterHeaders({}); | ||
expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`{}`); | ||
}); | ||
|
||
it("handles simple headers", () => { | ||
let headers = createReactRouterHeaders({ "x-foo": "bar" }); | ||
expect(headers.get("x-foo")).toBe("bar"); | ||
}); | ||
|
||
it("handles multiple headers", () => { | ||
let headers = createReactRouterHeaders({ | ||
"x-foo": "bar", | ||
"x-bar": "baz", | ||
}); | ||
expect(headers.get("x-foo")).toBe("bar"); | ||
expect(headers.get("x-bar")).toBe("baz"); | ||
}); | ||
|
||
it("handles headers with multiple values", () => { | ||
let headers = createReactRouterHeaders({ | ||
"x-foo": "bar, baz", | ||
"x-bar": "baz", | ||
}); | ||
expect(headers.get("x-foo")).toEqual("bar, baz"); | ||
expect(headers.get("x-bar")).toBe("baz"); | ||
}); | ||
|
||
it("handles multiple request cookies", () => { | ||
let headers = createReactRouterHeaders({}, [ | ||
"__session=some_value", | ||
"__other=some_other_value", | ||
]); | ||
expect(headers.get("cookie")).toEqual( | ||
"__session=some_value; __other=some_other_value" | ||
); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("architect createReactRouterRequest", () => { | ||
it("creates a request with the correct headers", () => { | ||
let request = createReactRouterRequest( | ||
createMockEvent({ cookies: ["__session=value"] }) | ||
); | ||
|
||
expect(request.method).toBe("GET"); | ||
expect(request.headers.get("cookie")).toBe("__session=value"); | ||
}); | ||
}); | ||
|
||
describe("sendReactRouterResponse", () => { | ||
it("handles regular responses", async () => { | ||
let response = new Response("anything"); | ||
let result = await sendReactRouterResponse(response); | ||
expect(result.body).toBe("anything"); | ||
}); | ||
|
||
it("handles resource routes with regular data", async () => { | ||
let json = JSON.stringify({ foo: "bar" }); | ||
let response = new Response(json, { | ||
headers: { | ||
"Content-Type": "application/json", | ||
"content-length": json.length.toString(), | ||
}, | ||
}); | ||
|
||
let result = await sendReactRouterResponse(response); | ||
|
||
expect(result.body).toMatch(json); | ||
}); | ||
|
||
it("handles resource routes with binary data", async () => { | ||
let image = await fsp.readFile(path.join(__dirname, "554828.jpeg")); | ||
|
||
let response = new Response(image, { | ||
headers: { | ||
"content-type": "image/jpeg", | ||
"content-length": image.length.toString(), | ||
}, | ||
}); | ||
|
||
let result = await sendReactRouterResponse(response); | ||
|
||
expect(result.body).toMatch(image.toString("base64")); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { installGlobals } from "@react-router/node"; | ||
installGlobals(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/** | ||
* Common binary MIME types | ||
* @see https://github.com/architect/functions/blob/45254fc1936a1794c185aac07e9889b241a2e5c6/src/http/helpers/binary-types.js | ||
*/ | ||
const binaryTypes = [ | ||
"application/octet-stream", | ||
// Docs | ||
"application/epub+zip", | ||
"application/msword", | ||
"application/pdf", | ||
"application/rtf", | ||
"application/vnd.amazon.ebook", | ||
"application/vnd.ms-excel", | ||
"application/vnd.ms-powerpoint", | ||
"application/vnd.openxmlformats-officedocument.presentationml.presentation", | ||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | ||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", | ||
// Fonts | ||
"font/otf", | ||
"font/woff", | ||
"font/woff2", | ||
// Images | ||
"image/avif", | ||
"image/bmp", | ||
"image/gif", | ||
"image/jpeg", | ||
"image/png", | ||
"image/tiff", | ||
"image/vnd.microsoft.icon", | ||
"image/webp", | ||
// Audio | ||
"audio/3gpp", | ||
"audio/aac", | ||
"audio/basic", | ||
"audio/mpeg", | ||
"audio/ogg", | ||
"audio/wav", | ||
"audio/webm", | ||
"audio/x-aiff", | ||
"audio/x-midi", | ||
"audio/x-wav", | ||
// Video | ||
"video/3gpp", | ||
"video/mp2t", | ||
"video/mpeg", | ||
"video/ogg", | ||
"video/quicktime", | ||
"video/webm", | ||
"video/x-msvideo", | ||
// Archives | ||
"application/java-archive", | ||
"application/vnd.apple.installer+xml", | ||
"application/x-7z-compressed", | ||
"application/x-apple-diskimage", | ||
"application/x-bzip", | ||
"application/x-bzip2", | ||
"application/x-gzip", | ||
"application/x-java-archive", | ||
"application/x-rar-compressed", | ||
"application/x-tar", | ||
"application/x-zip", | ||
"application/zip", | ||
]; | ||
|
||
export function isBinaryType(contentType: string | null | undefined) { | ||
if (!contentType) return false; | ||
let [test] = contentType.split(";"); | ||
return binaryTypes.includes(test); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export { createArcTableSessionStorage } from "./sessions/arcTableSessionStorage"; | ||
|
||
export type { GetLoadContextFunction, RequestHandler } from "./server"; | ||
export { createRequestHandler } from "./server"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @type {import('jest').Config} */ | ||
module.exports = { | ||
...require("../../jest/jest.config.shared"), | ||
displayName: "architect", | ||
}; |
Oops, something went wrong.