Skip to content

Commit

Permalink
Port Remix Architect package (#11804)
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish authored Jul 16, 2024
1 parent d09ff87 commit dfd2d14
Show file tree
Hide file tree
Showing 16 changed files with 958 additions and 2 deletions.
7 changes: 7 additions & 0 deletions packages/react-router-architect/README.md
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 packages/react-router-architect/__tests__/binaryTypes-test.ts
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 packages/react-router-architect/__tests__/server-test.ts
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"));
});
});
2 changes: 2 additions & 0 deletions packages/react-router-architect/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { installGlobals } from "@react-router/node";
installGlobals();
69 changes: 69 additions & 0 deletions packages/react-router-architect/binaryTypes.ts
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);
}
4 changes: 4 additions & 0 deletions packages/react-router-architect/index.ts
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";
5 changes: 5 additions & 0 deletions packages/react-router-architect/jest.config.js
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",
};
Loading

0 comments on commit dfd2d14

Please sign in to comment.