diff --git a/packages/react-router-architect/README.md b/packages/react-router-architect/README.md new file mode 100644 index 0000000000..7180258125 --- /dev/null +++ b/packages/react-router-architect/README.md @@ -0,0 +1,7 @@ +# React Router Architect + +Architect server request handler for React Router. + +```bash +npm install @react-router/architect +``` diff --git a/packages/react-router-architect/__tests__/554828.jpeg b/packages/react-router-architect/__tests__/554828.jpeg new file mode 100644 index 0000000000..830d2dbb3c Binary files /dev/null and b/packages/react-router-architect/__tests__/554828.jpeg differ diff --git a/packages/react-router-architect/__tests__/binaryTypes-test.ts b/packages/react-router-architect/__tests__/binaryTypes-test.ts new file mode 100644 index 0000000000..1418856d48 --- /dev/null +++ b/packages/react-router-architect/__tests__/binaryTypes-test.ts @@ -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); + }); +}); diff --git a/packages/react-router-architect/__tests__/server-test.ts b/packages/react-router-architect/__tests__/server-test.ts new file mode 100644 index 0000000000..bc818e1c46 --- /dev/null +++ b/packages/react-router-architect/__tests__/server-test.ts @@ -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 = {}) { + 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")); + }); +}); diff --git a/packages/react-router-architect/__tests__/setup.ts b/packages/react-router-architect/__tests__/setup.ts new file mode 100644 index 0000000000..996e99893d --- /dev/null +++ b/packages/react-router-architect/__tests__/setup.ts @@ -0,0 +1,2 @@ +import { installGlobals } from "@react-router/node"; +installGlobals(); diff --git a/packages/react-router-architect/binaryTypes.ts b/packages/react-router-architect/binaryTypes.ts new file mode 100644 index 0000000000..fee3d9619b --- /dev/null +++ b/packages/react-router-architect/binaryTypes.ts @@ -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); +} diff --git a/packages/react-router-architect/index.ts b/packages/react-router-architect/index.ts new file mode 100644 index 0000000000..c66f26f567 --- /dev/null +++ b/packages/react-router-architect/index.ts @@ -0,0 +1,4 @@ +export { createArcTableSessionStorage } from "./sessions/arcTableSessionStorage"; + +export type { GetLoadContextFunction, RequestHandler } from "./server"; +export { createRequestHandler } from "./server"; diff --git a/packages/react-router-architect/jest.config.js b/packages/react-router-architect/jest.config.js new file mode 100644 index 0000000000..94bdf00603 --- /dev/null +++ b/packages/react-router-architect/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('jest').Config} */ +module.exports = { + ...require("../../jest/jest.config.shared"), + displayName: "architect", +}; diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json new file mode 100644 index 0000000000..dab257ba9b --- /dev/null +++ b/packages/react-router-architect/package.json @@ -0,0 +1,59 @@ +{ + "name": "@react-router/architect", + "version": "2.9.0-pre.0", + "description": "Architect server request handler for React Router", + "bugs": { + "url": "https://github.com/remix-run/react-router/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/remix-run/react-router", + "directory": "packages/react-router-architect" + }, + "license": "MIT", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "tsc": "tsc" + }, + "dependencies": { + "@architect/functions": "^5.2.0", + "@types/aws-lambda": "^8.10.82" + }, + "devDependencies": { + "@react-router/node": "workspace:*", + "@types/lambda-tester": "^3.6.1", + "@types/node": "^18.17.1", + "lambda-tester": "^4.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "workspace:*", + "typescript": "^5.1.0" + }, + "peerDependencies": { + "@react-router/node": "workspace:^", + "react-router": "workspace:^", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "dist/", + "CHANGELOG.md", + "LICENSE.md", + "README.md" + ] +} diff --git a/packages/react-router-architect/rollup.config.js b/packages/react-router-architect/rollup.config.js new file mode 100644 index 0000000000..4e48ff954e --- /dev/null +++ b/packages/react-router-architect/rollup.config.js @@ -0,0 +1,55 @@ +const path = require("node:path"); + +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const typescript = require("@rollup/plugin-typescript"); +const copy = require("rollup-plugin-copy"); + +const { + isBareModuleId, + getBuildDirectories, + createBanner, + WATCH, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + const { SOURCE_DIR, OUTPUT_DIR } = getBuildDirectories( + packageName, + // We don't live in a folder matching our package name + "react-router-architect" + ); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: `${SOURCE_DIR}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: OUTPUT_DIR, + format: "cjs", + preserveModules: true, + exports: "auto", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + typescript({ + tsconfig: path.join(__dirname, "tsconfig.json"), + exclude: ["__tests__"], + noEmitOnError: !WATCH, + }), + nodeResolve({ extensions: [".ts"] }), + copy({ + targets: [{ src: "LICENSE.md", dest: SOURCE_DIR }], + }), + ], + }, + ]; +}; diff --git a/packages/react-router-architect/server.ts b/packages/react-router-architect/server.ts new file mode 100644 index 0000000000..bf342dfccd --- /dev/null +++ b/packages/react-router-architect/server.ts @@ -0,0 +1,134 @@ +import type { AppLoadContext, ServerBuild } from "react-router"; +import { createRequestHandler as createReactRouterRequestHandler } from "react-router"; +import { readableStreamToString } from "@react-router/node"; +import type { + APIGatewayProxyEventHeaders, + APIGatewayProxyEventV2, + APIGatewayProxyHandlerV2, + APIGatewayProxyStructuredResultV2, +} from "aws-lambda"; + +import { isBinaryType } from "./binaryTypes"; + +/** + * A function that returns the value to use as `context` in route `loader` and + * `action` functions. + * + * You can think of this as an escape hatch that allows you to pass + * environment/platform-specific values through to your loader/action. + */ +export type GetLoadContextFunction = ( + event: APIGatewayProxyEventV2 +) => Promise | AppLoadContext; + +export type RequestHandler = APIGatewayProxyHandlerV2; + +/** + * Returns a request handler for Architect that serves the response using + * React Router. + */ +export function createRequestHandler({ + build, + getLoadContext, + mode = process.env.NODE_ENV, +}: { + build: ServerBuild; + getLoadContext?: GetLoadContextFunction; + mode?: string; +}): RequestHandler { + let handleRequest = createReactRouterRequestHandler(build, mode); + + return async (event) => { + let request = createReactRouterRequest(event); + let loadContext = await getLoadContext?.(event); + + let response = await handleRequest(request, loadContext); + + return sendReactRouterResponse(response); + }; +} + +export function createReactRouterRequest( + event: APIGatewayProxyEventV2 +): Request { + let host = event.headers["x-forwarded-host"] || event.headers.host; + let search = event.rawQueryString.length ? `?${event.rawQueryString}` : ""; + let scheme = process.env.ARC_SANDBOX ? "http" : "https"; + let url = new URL(`${scheme}://${host}${event.rawPath}${search}`); + let isFormData = event.headers["content-type"]?.includes( + "multipart/form-data" + ); + // Note: No current way to abort these for Architect, but our router expects + // requests to contain a signal, so it can detect aborted requests + let controller = new AbortController(); + + return new Request(url.href, { + method: event.requestContext.http.method, + headers: createReactRouterHeaders(event.headers, event.cookies), + signal: controller.signal, + body: + event.body && event.isBase64Encoded + ? isFormData + ? Buffer.from(event.body, "base64") + : Buffer.from(event.body, "base64").toString() + : event.body, + }); +} + +export function createReactRouterHeaders( + requestHeaders: APIGatewayProxyEventHeaders, + requestCookies?: string[] +): Headers { + let headers = new Headers(); + + for (let [header, value] of Object.entries(requestHeaders)) { + if (value) { + headers.append(header, value); + } + } + + if (requestCookies) { + for (let cookie of requestCookies) { + headers.append("Cookie", cookie); + } + } + + return headers; +} + +export async function sendReactRouterResponse( + nodeResponse: Response +): Promise { + let cookies: string[] = []; + + // Arc/AWS API Gateway will send back set-cookies outside of response headers. + for (let [key, value] of nodeResponse.headers.entries()) { + if (key.toLowerCase() === "set-cookie") { + cookies.push(value); + } + } + + if (cookies.length) { + nodeResponse.headers.delete("Set-Cookie"); + } + + let contentType = nodeResponse.headers.get("Content-Type"); + let isBase64Encoded = isBinaryType(contentType); + let body: string | undefined; + + if (nodeResponse.body) { + if (isBase64Encoded) { + body = await readableStreamToString(nodeResponse.body, "base64"); + } else { + body = await nodeResponse.text(); + } + } + + return { + statusCode: nodeResponse.status, + headers: Object.fromEntries(nodeResponse.headers.entries()), + cookies, + body, + isBase64Encoded, + }; +} diff --git a/packages/react-router-architect/sessions/arcTableSessionStorage.ts b/packages/react-router-architect/sessions/arcTableSessionStorage.ts new file mode 100644 index 0000000000..40988a4b2f --- /dev/null +++ b/packages/react-router-architect/sessions/arcTableSessionStorage.ts @@ -0,0 +1,121 @@ +import * as crypto from "node:crypto"; +import type { + SessionData, + SessionStorage, + SessionIdStorageStrategy, +} from "react-router"; +import { createSessionStorage } from "@react-router/node"; +import arc from "@architect/functions"; +import type { ArcTable } from "@architect/functions/types/tables"; + +interface ArcTableSessionStorageOptions { + /** + * The Cookie used to store the session id on the client, or options used + * to automatically create one. + */ + cookie?: SessionIdStorageStrategy["cookie"]; + + /** + * The table used to store sessions, or its name as it appears in your + * project's app.arc file. + */ + table: ArcTable | string; + + /** + * The name of the DynamoDB attribute used to store the session ID. + * This should be the table's partition key. + */ + idx: string; + + /** + * The name of the DynamoDB attribute used to store the expiration time. + * If absent, then no TTL will be stored and session records will not expire. + */ + ttl?: string; +} + +/** + * Session storage using a DynamoDB table managed by Architect. + * + * Add the following lines to your project's `app.arc` file: + * + * @tables + * arc-sessions + * _idx *String + * _ttl TTL + */ +export function createArcTableSessionStorage< + Data = SessionData, + FlashData = Data +>({ + cookie, + ...props +}: ArcTableSessionStorageOptions): SessionStorage { + async function getTable() { + if (typeof props.table === "string") { + let tables = await arc.tables(); + return tables[props.table]; + } else { + return props.table; + } + } + return createSessionStorage({ + cookie, + async createData(data, expires) { + let table = await getTable(); + while (true) { + let randomBytes = crypto.randomBytes(8); + // This storage manages an id space of 2^64 ids, which is far greater + // than the maximum number of files allowed on an NTFS or ext4 volume + // (2^32). However, the larger id space should help to avoid collisions + // with existing ids when creating new sessions, which speeds things up. + let id = [...randomBytes] + .map((x) => x.toString(16).padStart(2, "0")) + .join(""); + + if (await table.get({ [props.idx]: id })) { + continue; + } + + let params: Record = { + [props.idx]: id, + ...data, + }; + if (props.ttl) { + params[props.ttl] = expires + ? Math.round(expires.getTime() / 1000) + : undefined; + } + await table.put(params); + + return id; + } + }, + async readData(id) { + let table = await getTable(); + let data = await table.get({ [props.idx]: id }); + if (data) { + delete data[props.idx]; + if (props.ttl) delete data[props.ttl]; + } + return data; + }, + async updateData(id, data, expires) { + let table = await getTable(); + let params: Record = { + [props.idx]: id, + ...data, + }; + if (props.ttl) { + params[props.ttl] = expires + ? Math.round(expires.getTime() / 1000) + : undefined; + } + await table.put(params); + }, + async deleteData(id) { + let table = await getTable(); + await table.delete({ [props.idx]: id }); + }, + }); +} diff --git a/packages/react-router-architect/tsconfig.json b/packages/react-router-architect/tsconfig.json new file mode 100644 index 0000000000..4a4e39de83 --- /dev/null +++ b/packages/react-router-architect/tsconfig.json @@ -0,0 +1,20 @@ +{ + "include": ["**/*.ts"], + "exclude": ["dist", "node_modules"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "target": "ES2022", + + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": ".", + "outDir": "./dist", + + // Avoid naming conflicts between history and react-router-dom relying on + // lib.dom.d.ts Window and this being a WebWorker env. + "skipLibCheck": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a87aa62b5..89287bf457 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -554,6 +554,40 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + packages/react-router-architect: + dependencies: + '@architect/functions': + specifier: ^5.2.0 + version: 5.4.1 + '@types/aws-lambda': + specifier: ^8.10.82 + version: 8.10.141 + devDependencies: + '@react-router/node': + specifier: workspace:* + version: link:../react-router-node + '@types/lambda-tester': + specifier: ^3.6.1 + version: 3.6.2 + '@types/node': + specifier: ^18.17.1 + version: 18.19.26 + lambda-tester: + specifier: ^4.0.1 + version: 4.0.1 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-router: + specifier: workspace:* + version: link:../react-router + typescript: + specifier: ^5.1.0 + version: 5.4.5 + packages/react-router-cloudflare: devDependencies: '@cloudflare/workers-types': @@ -994,6 +1028,19 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + /@architect/functions@5.4.1: + resolution: {integrity: sha512-F13FBUvVHjerUaSdnXIC3pZUnI10lxyVZ7HsrU8vM2qB5pFNqw5EgqcnVgORz/eqCcqevpIKayQ9yOUf/HuBAA==} + engines: {node: '>=12'} + dependencies: + cookie: 0.5.0 + cookie-signature: 1.2.1 + csrf: 3.1.0 + node-webtokens: 1.0.4 + run-parallel: 1.2.0 + run-waterfall: 1.1.7 + uid-safe: 2.1.5 + dev: false + /@babel/code-frame@7.22.5: resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} engines: {node: '>=6.9.0'} @@ -4361,6 +4408,11 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@extra-number/significant-digits@1.3.9: + resolution: {integrity: sha512-E5PY/bCwrNqEHh4QS6AQBinLZ+sxM1lT8tsSVYk8VwhWIPp6fCU/BMRVq0V8iJ8LwS3FHmaA4vUzb78s4BIIyA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + dev: true + /@fastify/busboy@2.1.1: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -5338,6 +5390,9 @@ packages: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: false + /@types/aws-lambda@8.10.141: + resolution: {integrity: sha512-SMWlRBukG9KV8ZNjwemp2AzDibp/czIAeKKTw09nCPbWxVskIxactCJCGOp4y6I1hCMY7T7UGfySvBLXNeUbEw==} + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -5539,6 +5594,12 @@ packages: '@types/node': 18.19.26 dev: false + /@types/lambda-tester@3.6.2: + resolution: {integrity: sha512-nQRUx0AuvTq5KOz1SaxMOOFJvnybo1oAzvSy/p9bVGthZVvg1Dar/051mhMuzdN1DWg++bs+eGq1MdCbm6wjSQ==} + dependencies: + '@types/aws-lambda': 8.10.141 + dev: true + /@types/lodash@4.17.0: resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} dev: true @@ -6220,6 +6281,11 @@ packages: normalize-path: 3.0.0 picomatch: 2.3.1 + /app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + dev: true + /arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -7049,6 +7115,15 @@ packages: wrap-ansi: 7.0.0 dev: false + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + dev: true + /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -7256,6 +7331,15 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /csrf@3.1.0: + resolution: {integrity: sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==} + engines: {node: '>= 0.8'} + dependencies: + rndm: 1.2.0 + tsscmp: 1.0.6 + uid-safe: 2.1.5 + dev: false + /css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} dependencies: @@ -7599,6 +7683,10 @@ packages: domhandler: 5.0.3 dev: false + /dotenv-json@1.0.0: + resolution: {integrity: sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ==} + dev: true + /dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -7607,7 +7695,6 @@ packages: /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} - dev: false /duplexify@3.7.1: resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} @@ -9422,6 +9509,13 @@ packages: engines: {node: '>=12'} dev: false + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + /is-plain-object@3.0.1: resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} engines: {node: '>=0.10.0'} @@ -9552,6 +9646,11 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: true + /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -10256,7 +10355,6 @@ packages: /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - dev: false /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} @@ -10268,6 +10366,35 @@ packages: engines: {node: '>=6'} dev: false + /lambda-event-mock@1.5.0: + resolution: {integrity: sha512-vx1d+vZqi7FF6B3+mAfHnY/6Tlp6BheL2ta0MJS0cIRB3Rc4I5cviHTkiJxHdE156gXx3ZjlQRJrS4puXvtrhA==} + engines: {node: '>=12.13'} + dependencies: + '@extra-number/significant-digits': 1.3.9 + clone-deep: 4.0.1 + uuid: 3.4.0 + vandium-utils: 1.2.0 + dev: true + + /lambda-leak@2.0.0: + resolution: {integrity: sha512-2c9jwUN3ZLa2GEiOhObbx2BMGQplEUCDHSIkhDtYwUjsTfiV/3jCF6ThIuEXfsvqbUK+0QpZcugIKB8YMbSevQ==} + engines: {node: '>=6.10.0'} + dev: true + + /lambda-tester@4.0.1: + resolution: {integrity: sha512-ft6XHk84B6/dYEzyI3anKoGWz08xQ5allEHiFYDUzaYTymgVK7tiBkCEbuWx+MFvH7OpFNsJXVtjXm0X8iH3Iw==} + engines: {node: '>=10.0'} + dependencies: + app-root-path: 3.1.0 + dotenv: 8.6.0 + dotenv-json: 1.0.0 + lambda-event-mock: 1.5.0 + lambda-leak: 2.0.0 + semver: 6.3.1 + uuid: 3.4.0 + vandium-utils: 2.0.0 + dev: true + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: false @@ -11474,6 +11601,11 @@ packages: /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + /node-webtokens@1.0.4: + resolution: {integrity: sha512-Sla56CeSLWvPbwud2kogqf5edQtKNXZBtXDDpmOzAgNZjwETbK/Am6PXfs54iZPLBm8K8amZ9XWaCQwGqZmKyQ==} + engines: {node: '>=6.6.0'} + dev: false + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -12154,6 +12286,11 @@ packages: engines: {node: '>=8'} dev: false + /random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -12513,6 +12650,10 @@ packages: dependencies: glob: 7.2.3 + /rndm@1.2.0: + resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==} + dev: false + /rollup-plugin-copy@3.4.0: resolution: {integrity: sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==} engines: {node: '>=8.3'} @@ -12616,6 +12757,10 @@ packages: dependencies: queue-microtask: 1.2.3 + /run-waterfall@1.1.7: + resolution: {integrity: sha512-iFPgh7SatHXOG1ClcpdwHI63geV3Hc/iL6crGSyBlH2PY7Rm/za+zoKz6FfY/Qlw5K7JwSol8pseO8fN6CMhhQ==} + dev: false + /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -12787,6 +12932,13 @@ packages: /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + dev: true + /shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -13403,6 +13555,11 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + dev: false + /tsutils@3.21.0(typescript@5.4.5): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -13592,6 +13749,13 @@ packages: /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + /uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + dependencies: + random-bytes: 1.0.0 + dev: false + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -13822,6 +13986,12 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: true + /uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'} @@ -13856,6 +14026,15 @@ packages: builtins: 5.0.1 dev: false + /vandium-utils@1.2.0: + resolution: {integrity: sha512-yxYUDZz4BNo0CW/z5w4mvclitt5zolY7zjW97i6tTE+sU63cxYs1A6Bl9+jtIQa3+0hkeqY87k+7ptRvmeHe3g==} + dev: true + + /vandium-utils@2.0.0: + resolution: {integrity: sha512-XWbQ/0H03TpYDXk8sLScBEZpE7TbA0CHDL6/Xjt37IBYKLsHUQuBlL44ttAUs9zoBOLFxsW7HehXcuWCNyqOxQ==} + engines: {node: '>=10.16'} + dev: true + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} diff --git a/rollup.config.js b/rollup.config.js index d95efad9f2..b64cc5bc8b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,6 +4,7 @@ const path = require("path"); module.exports = function rollup(options) { return [ "react-router", + "react-router-architect", "react-router-cloudflare", "react-router-dom", "react-router-dev", diff --git a/tsconfig.json b/tsconfig.json index 1eb60da9a9..2cf584a90c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "packages/react-router" }, + { "path": "packages/react-router-architect" }, { "path": "packages/react-router-cloudflare" }, { "path": "packages/react-router-dom" }, { "path": "packages/react-router-dev" },