Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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