diff --git a/CHANGES.md b/CHANGES.md
index 6998f3cf9c1..c0a8c84eea8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -13,6 +13,8 @@
- Updated .npmrc to minimum required version for webcrypto API (v15)
- Abstracted req and res types through the platform
- Add base64 encoding primitives to node globals (atob and btoa)
+- adds and improves testing around node adapters
+- fixes the ability to set multiple Set-Cookie headers
This is a history of changes to [Remix](https://remix.run).
diff --git a/docs/guides/styling.md b/docs/guides/styling.md
index 96ef2e651fb..a9ec6885fa6 100644
--- a/docs/guides/styling.md
+++ b/docs/guides/styling.md
@@ -222,12 +222,11 @@ Here's some sample code to show how you might use Styled Components with Remix:
);
+ responseHeaders.set("Content-Type", "text/html")
+
return new Response("" + markup, {
status: responseStatusCode,
- headers: {
- ...Object.fromEntries(responseHeaders),
- "Content-Type": "text/html"
- }
+ headers: responseHeaders
});
}
```
diff --git a/fixtures/gists-app/app/entry.server.jsx b/fixtures/gists-app/app/entry.server.jsx
index b79eedb9841..2a902ea1764 100644
--- a/fixtures/gists-app/app/entry.server.jsx
+++ b/fixtures/gists-app/app/entry.server.jsx
@@ -11,11 +11,10 @@ export default function handleRequest(
);
+ responseHeaders.set("Content-Type", "text/html");
+
return new Response("" + markup, {
status: responseStatusCode,
- headers: {
- ...Object.fromEntries(responseHeaders),
- "Content-Type": "text/html"
- }
+ headers: responseHeaders
});
}
diff --git a/fixtures/gists-app/app/routes/index.jsx b/fixtures/gists-app/app/routes/index.jsx
index f294cbb373f..b49765e1dd3 100644
--- a/fixtures/gists-app/app/routes/index.jsx
+++ b/fixtures/gists-app/app/routes/index.jsx
@@ -81,6 +81,9 @@ export default function Index() {
Render error in nested route with ErrorBoundary
+
+ Multiple Set Cookie Headers
+
Preferences
diff --git a/fixtures/gists-app/app/routes/multiple-set-cookies.tsx b/fixtures/gists-app/app/routes/multiple-set-cookies.tsx
new file mode 100644
index 00000000000..43620d9ea66
--- /dev/null
+++ b/fixtures/gists-app/app/routes/multiple-set-cookies.tsx
@@ -0,0 +1,41 @@
+import * as React from "react";
+import type {
+ ActionFunction,
+ LoaderFunction,
+ MetaFunction,
+ RouteComponent
+} from "remix";
+import { redirect, json, Form } from "remix";
+import { Headers } from "@remix-run/node";
+
+let loader: LoaderFunction = async ({ request }) => {
+ let headers = new Headers();
+ headers.append("Set-Cookie", "foo=bar");
+ headers.append("Set-Cookie", "bar=baz");
+ return json({}, { headers });
+};
+
+let action: ActionFunction = async () => {
+ let headers = new Headers();
+ headers.append("Set-Cookie", "another=one");
+ headers.append("Set-Cookie", "how-about=two");
+ return redirect("/multiple-set-cookies", { headers });
+};
+
+let meta: MetaFunction = () => ({
+ title: "Multi Set Cookie Headers"
+});
+
+let MultipleSetCookiesPage: RouteComponent = () => {
+ return (
+ <>
+ 👋
+
+ >
+ );
+};
+
+export default MultipleSetCookiesPage;
+export { action, loader, meta };
diff --git a/fixtures/gists-app/tests/__snapshots__/server-html-test.ts.snap b/fixtures/gists-app/tests/__snapshots__/server-html-test.ts.snap
index 36c2481e1b9..9c817ec7742 100644
--- a/fixtures/gists-app/tests/__snapshots__/server-html-test.ts.snap
+++ b/fixtures/gists-app/tests/__snapshots__/server-html-test.ts.snap
@@ -29,6 +29,7 @@ exports[`the server HTML for the root URL is correct 1`] = `
>Render error in nested route with ErrorBoundary
+ Multiple Set Cookie Headers
Preferences
diff --git a/fixtures/gists-app/tests/multiple-set-cookies-test.ts b/fixtures/gists-app/tests/multiple-set-cookies-test.ts
new file mode 100644
index 00000000000..84744344911
--- /dev/null
+++ b/fixtures/gists-app/tests/multiple-set-cookies-test.ts
@@ -0,0 +1,36 @@
+import type { Browser, Page } from "puppeteer";
+import puppeteer from "puppeteer";
+
+import { collectResponses } from "./utils";
+
+const testPort = 3000;
+const testServer = `http://localhost:${testPort}`;
+
+describe("can set multiple set cookies headers", () => {
+ let browser: Browser;
+ let page: Page;
+
+ beforeEach(async () => {
+ browser = await puppeteer.launch();
+ page = await browser.newPage();
+ });
+
+ afterEach(() => browser.close());
+
+ describe("loader headers", () => {
+ it("are correct", async () => {
+ let responses = collectResponses(
+ page,
+ url => url.pathname === "/multiple-set-cookies"
+ );
+
+ await page.goto(`${testServer}/multiple-set-cookies`);
+
+ expect(responses).toHaveLength(1);
+ expect(responses[0].headers()["set-cookie"]).toMatchInlineSnapshot(`
+ "foo=bar
+ bar=baz"
+ `);
+ });
+ });
+});
diff --git a/fixtures/tutorial/app/entry.server.tsx b/fixtures/tutorial/app/entry.server.tsx
index edf2cc0c15f..1c277b26931 100644
--- a/fixtures/tutorial/app/entry.server.tsx
+++ b/fixtures/tutorial/app/entry.server.tsx
@@ -12,11 +12,10 @@ export default function handleRequest(
);
+ responseHeaders.set("Content-Type", "text/html");
+
return new Response("" + markup, {
status: responseStatusCode,
- headers: {
- ...Object.fromEntries(responseHeaders),
- "Content-Type": "text/html"
- }
+ headers: responseHeaders
});
}
diff --git a/jest.config.js b/jest.config.js
index 8ebf5c0e5ee..716c1827c2d 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -5,12 +5,6 @@ module.exports = {
testEnvironment: "node",
testMatch: ["/packages/remix-dev/**/*-test.[jt]s?(x)"]
},
- {
- displayName: "express",
- testEnvironment: "node",
- testMatch: ["/packages/remix-express/**/*-test.[jt]s?(x)"],
- setupFiles: ["/jest/setupNodeGlobals.ts"]
- },
{
displayName: "node",
testEnvironment: "node",
@@ -25,6 +19,25 @@ module.exports = {
],
setupFiles: ["/jest/setupNodeGlobals.ts"]
},
+ // Node Adapters
+ {
+ displayName: "architect",
+ testEnvironment: "node",
+ testMatch: ["/packages/remix-architect/**/*-test.[jt]s?(x)"]
+ },
+ {
+ displayName: "express",
+ testEnvironment: "node",
+ testMatch: ["/packages/remix-express/**/*-test.[jt]s?(x)"],
+ setupFiles: ["/jest/setupNodeGlobals.ts"]
+ },
+ {
+ displayName: "vercel",
+ testEnvironment: "node",
+ testMatch: ["/packages/remix-vercel/**/*-test.[jt]s?(x)"],
+ setupFiles: ["/jest/setupNodeGlobals.ts"]
+ },
+ // Fixture Apps
{
displayName: "gists-app",
testEnvironment: "node",
diff --git a/packages/remix-architect/__tests__/server-test.ts b/packages/remix-architect/__tests__/server-test.ts
new file mode 100644
index 00000000000..45d5c2f95f3
--- /dev/null
+++ b/packages/remix-architect/__tests__/server-test.ts
@@ -0,0 +1,292 @@
+import lambdaTester from "lambda-tester";
+import { Response, Headers } from "@remix-run/node";
+import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";
+
+import {
+ createRemixHeaders,
+ createRemixRequest,
+ createRequestHandler
+} from "../server";
+import { APIGatewayProxyEventV2 } from "aws-lambda";
+
+// We don't want to test that the remix server works here (that's what the
+// puppetteer tests do), we just want to test the architect adapter
+jest.mock("@remix-run/server-runtime");
+let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction<
+ typeof createRemixRequestHandler
+>;
+
+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",
+ 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}`);
+ });
+
+ await lambdaTester(createRequestHandler({ build: undefined }))
+ .event(createMockEvent({ rawPath: "/foo/bar" }))
+ .expectResolve(res => {
+ expect(res.statusCode).toBe(200);
+ expect(res.body).toBe("URL: /foo/bar");
+ });
+ });
+
+ it("handles status codes", async () => {
+ mockedCreateRequestHandler.mockImplementation(() => async () => {
+ return new Response("", { status: 204 });
+ });
+
+ await lambdaTester(createRequestHandler({ build: undefined }))
+ .event(createMockEvent({ rawPath: "/foo/bar" }))
+ .expectResolve(res => {
+ 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("", { headers });
+ });
+
+ await lambdaTester(createRequestHandler({ build: undefined }))
+ .event(createMockEvent({ rawPath: "/" }))
+ .expectResolve(res => {
+ 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 createRemixHeaders", () => {
+ describe("creates fetch headers from architect headers", () => {
+ it("handles empty headers", () => {
+ expect(createRemixHeaders({}, undefined)).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {},
+ }
+ `);
+ });
+
+ it("handles simple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar" }, undefined))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-foo": Array [
+ "bar",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles multiple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar", "x-bar": "baz" }, undefined))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-bar": Array [
+ "baz",
+ ],
+ "x-foo": Array [
+ "bar",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles headers with multiple values", () => {
+ expect(createRemixHeaders({ "x-foo": "bar, baz" }, undefined))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-foo": Array [
+ "bar, baz",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles headers with multiple values and multiple headers", () => {
+ expect(
+ createRemixHeaders({ "x-foo": "bar, baz", "x-bar": "baz" }, undefined)
+ ).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-bar": Array [
+ "baz",
+ ],
+ "x-foo": Array [
+ "bar, baz",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles cookies", () => {
+ expect(
+ createRemixHeaders({ "x-something-else": "true" }, [
+ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
+ "__other=some_other_value; Path=/; Secure; HttpOnly; Expires=Wed, 21 Oct 2015 07:28:00 GMT; SameSite=Lax"
+ ])
+ ).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "Cookie": Array [
+ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
+ "__other=some_other_value; Path=/; Secure; HttpOnly; Expires=Wed, 21 Oct 2015 07:28:00 GMT; SameSite=Lax",
+ ],
+ "x-something-else": Array [
+ "true",
+ ],
+ },
+ }
+ `);
+ });
+ });
+});
+
+describe("architect createRemixRequest", () => {
+ it("creates a request with the correct headers", () => {
+ expect(
+ createRemixRequest(
+ createMockEvent({
+ cookies: ["__session=value"]
+ })
+ )
+ ).toMatchInlineSnapshot(`
+ Request {
+ "agent": undefined,
+ "compress": true,
+ "counter": 0,
+ "follow": 20,
+ "size": 0,
+ "timeout": 0,
+ Symbol(Body internals): Object {
+ "body": null,
+ "disturbed": false,
+ "error": null,
+ },
+ Symbol(Request internals): Object {
+ "headers": Headers {
+ Symbol(map): Object {
+ "Cookie": Array [
+ "__session=value",
+ ],
+ "accept": Array [
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ ],
+ "accept-encoding": Array [
+ "gzip, deflate",
+ ],
+ "accept-language": Array [
+ "en-US,en;q=0.9",
+ ],
+ "host": Array [
+ "localhost:3333",
+ ],
+ "upgrade-insecure-requests": Array [
+ "1",
+ ],
+ "user-agent": Array [
+ "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",
+ ],
+ },
+ },
+ "method": "GET",
+ "parsedURL": Url {
+ "auth": null,
+ "hash": null,
+ "host": "localhost:3333",
+ "hostname": "localhost",
+ "href": "http://localhost:3333/",
+ "path": "/",
+ "pathname": "/",
+ "port": "3333",
+ "protocol": "http:",
+ "query": null,
+ "search": null,
+ "slashes": true,
+ },
+ "redirect": "follow",
+ "signal": null,
+ },
+ }
+ `);
+ });
+});
diff --git a/packages/remix-architect/package.json b/packages/remix-architect/package.json
index 7c75dab7e21..92db21316b9 100644
--- a/packages/remix-architect/package.json
+++ b/packages/remix-architect/package.json
@@ -4,11 +4,15 @@
"version": "0.17.5",
"repository": "https://github.com/remix-run/packages",
"dependencies": {
+ "@types/aws-lambda": "^8.10.82",
"@remix-run/node": "0.17.5",
"@remix-run/server-runtime": "0.17.5"
},
"peerDependencies": {
- "@architect/architect": "^8.3.7",
- "@architect/functions": "^3.13.9"
+ "@architect/architect": "^8.3.7"
+ },
+ "devDependencies": {
+ "@types/lambda-tester": "^3.6.1",
+ "lambda-tester": "^4.0.1"
}
}
diff --git a/packages/remix-architect/server.ts b/packages/remix-architect/server.ts
index bd0a0780ffc..431b13f99dd 100644
--- a/packages/remix-architect/server.ts
+++ b/packages/remix-architect/server.ts
@@ -1,7 +1,14 @@
-import type {
- Request as ArcRequest,
- Response as ArcResponse
-} from "@architect/functions";
+import { URL } from "url";
+import {
+ Headers as NodeHeaders,
+ Request as NodeRequest,
+ formatServerError
+} from "@remix-run/node";
+import {
+ APIGatewayProxyEventHeaders,
+ APIGatewayProxyEventV2,
+ APIGatewayProxyHandlerV2
+} from "aws-lambda";
import type {
AppLoadContext,
ServerBuild,
@@ -9,7 +16,6 @@ import type {
} from "@remix-run/server-runtime";
import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";
import type { Response as NodeResponse } from "@remix-run/node";
-import { Request as NodeRequest, formatServerError } from "@remix-run/node";
/**
* A function that returns the value to use as `context` in route `loader` and
@@ -19,7 +25,7 @@ import { Request as NodeRequest, formatServerError } from "@remix-run/node";
* environment/platform-specific values through to your loader/action.
*/
export interface GetLoadContextFunction {
- (req: ArcRequest): AppLoadContext;
+ (event: APIGatewayProxyEventV2): AppLoadContext;
}
export type RequestHandler = ReturnType;
@@ -36,41 +42,77 @@ export function createRequestHandler({
build: ServerBuild;
getLoadContext: GetLoadContextFunction;
mode?: string;
-}) {
+}): APIGatewayProxyHandlerV2 {
let platform: ServerPlatform = { formatServerError };
let handleRequest = createRemixRequestHandler(build, platform, mode);
- return async (req: ArcRequest): Promise => {
- let request = createRemixRequest(req);
+ return async (event, _context) => {
+ let request = createRemixRequest(event);
let loadContext =
- typeof getLoadContext === "function" ? getLoadContext(req) : undefined;
+ typeof getLoadContext === "function" ? getLoadContext(event) : undefined;
let response = ((await handleRequest(
(request as unknown) as Request,
loadContext
)) as unknown) as NodeResponse;
+ let cookies: string[] = [];
+
+ // Arc/AWS API Gateway will send back set-cookies outside of response headers.
+ for (let [key, values] of Object.entries(response.headers.raw())) {
+ if (key.toLowerCase() === "set-cookie") {
+ for (let value of values) {
+ cookies.push(value);
+ }
+ }
+ }
+
+ if (cookies.length) {
+ response.headers.delete("set-cookie");
+ }
+
return {
statusCode: response.status,
headers: Object.fromEntries(response.headers),
+ cookies,
body: await response.text()
};
};
}
-function createRemixRequest(req: ArcRequest): NodeRequest {
- let host = req.headers["x-forwarded-host"] || req.headers.host;
- let search = req.rawQueryString.length ? "?" + req.rawQueryString : "";
- let url = new URL(req.rawPath + search, `https://${host}`);
+export function createRemixHeaders(
+ requestHeaders: APIGatewayProxyEventHeaders,
+ requestCookies?: string[]
+): NodeHeaders {
+ let headers = new NodeHeaders();
+
+ 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 function createRemixRequest(event: APIGatewayProxyEventV2): NodeRequest {
+ let host = event.headers["x-forwarded-host"] || event.headers.host;
+ let proto = event.requestContext.http.protocol || "https";
+ let search = event.rawQueryString.length ? `?${event.rawQueryString}` : "";
+ let url = new URL(event.rawPath + search, `${proto}://${host}`);
return new NodeRequest(url.toString(), {
- method: req.requestContext.http.method,
- headers: req.cookies
- ? { ...req.headers, Cookie: req.cookies.join(";") }
- : req.headers,
+ method: event.requestContext.http.method,
+ headers: createRemixHeaders(event.headers, event.cookies),
body:
- req.body && req.isBase64Encoded
- ? Buffer.from(req.body, "base64").toString()
- : req.body
+ event.body && event.isBase64Encoded
+ ? Buffer.from(event.body, "base64").toString()
+ : event.body
});
}
diff --git a/packages/remix-architect/tsconfig.json b/packages/remix-architect/tsconfig.json
index 72da2ef0c76..4674d442d62 100644
--- a/packages/remix-architect/tsconfig.json
+++ b/packages/remix-architect/tsconfig.json
@@ -1,5 +1,6 @@
{
- "include": ["../../types/architect__functions.d.ts", "**/*"],
+ "include": ["**/*"],
+ "exclude": ["__tests__"],
"compilerOptions": {
"lib": ["ES2019"],
"target": "ES2019",
diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts
index 94b1d3df0c5..fb974991444 100644
--- a/packages/remix-dev/__tests__/readConfig-test.ts
+++ b/packages/remix-dev/__tests__/readConfig-test.ts
@@ -141,6 +141,13 @@ describe("readConfig", () => {
"parentId": "root",
"path": "methods",
},
+ "routes/multiple-set-cookies": Object {
+ "caseSensitive": false,
+ "file": "routes/multiple-set-cookies.tsx",
+ "id": "routes/multiple-set-cookies",
+ "parentId": "root",
+ "path": "multiple-set-cookies",
+ },
"routes/prefs": Object {
"caseSensitive": false,
"file": "routes/prefs.tsx",
diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts
index c16cd2f4910..8a0588e2f72 100644
--- a/packages/remix-express/__tests__/server-test.ts
+++ b/packages/remix-express/__tests__/server-test.ts
@@ -1,7 +1,13 @@
import express from "express";
import supertest from "supertest";
+import { Response, Headers } from "@remix-run/node";
+import { createRequest } from "node-mocks-http";
-import { createRequestHandler } from "../server";
+import {
+ createRemixHeaders,
+ createRemixRequest,
+ createRequestHandler
+} from "../server";
import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";
@@ -63,15 +69,180 @@ describe("express createRequestHandler", () => {
it("sets headers", async () => {
mockedCreateRequestHandler.mockImplementation(() => async () => {
- return new Response("", {
- headers: { "X-Time-Of-Year": "most wonderful" }
- });
+ const headers = new Headers({ "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("", { headers });
});
let request = supertest(createApp());
let res = await request.get("/");
expect(res.headers["x-time-of-year"]).toBe("most wonderful");
+ expect(res.headers["set-cookie"]).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("express createRemixHeaders", () => {
+ describe("creates fetch headers from express headers", () => {
+ it("handles empty headers", () => {
+ expect(createRemixHeaders({})).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {},
+ }
+ `);
+ });
+
+ it("handles simple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar" })).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-foo": Array [
+ "bar",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles multiple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar", "x-bar": "baz" }))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-bar": Array [
+ "baz",
+ ],
+ "x-foo": Array [
+ "bar",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles headers with multiple values", () => {
+ expect(createRemixHeaders({ "x-foo": "bar, baz" }))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-foo": Array [
+ "bar, baz",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles headers with multiple values and multiple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar, baz", "x-bar": "baz" }))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-bar": Array [
+ "baz",
+ ],
+ "x-foo": Array [
+ "bar, baz",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles multiple set-cookie headers", () => {
+ expect(
+ createRemixHeaders({
+ "set-cookie": [
+ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
+ "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax"
+ ]
+ })
+ ).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "set-cookie": Array [
+ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
+ "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax",
+ ],
+ },
+ }
+ `);
+ });
+ });
+});
+
+describe("express createRemixRequest", () => {
+ it("creates a request with the correct headers", async () => {
+ const expressRequest = createRequest({
+ url: "/foo/bar",
+ method: "GET",
+ protocol: "http",
+ hostname: "localhost",
+ headers: {
+ "Cache-Control": "max-age=300, s-maxage=3600",
+ Host: "localhost:3000"
+ }
+ });
+
+ expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(`
+ Request {
+ "agent": undefined,
+ "compress": true,
+ "counter": 0,
+ "follow": 20,
+ "size": 0,
+ "timeout": 0,
+ Symbol(Body internals): Object {
+ "body": null,
+ "disturbed": false,
+ "error": null,
+ },
+ Symbol(Request internals): Object {
+ "headers": Headers {
+ Symbol(map): Object {
+ "cache-control": Array [
+ "max-age=300, s-maxage=3600",
+ ],
+ "host": Array [
+ "localhost:3000",
+ ],
+ },
+ },
+ "method": "GET",
+ "parsedURL": Url {
+ "auth": null,
+ "hash": null,
+ "host": "localhost:3000",
+ "hostname": "localhost",
+ "href": "http://localhost:3000/foo/bar",
+ "path": "/foo/bar",
+ "pathname": "/foo/bar",
+ "port": "3000",
+ "protocol": "http:",
+ "query": null,
+ "search": null,
+ "slashes": true,
+ },
+ "redirect": "follow",
+ "signal": null,
+ },
+ }
+ `);
+ });
+});
diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json
index ccbfa45fc30..45adfcbf3ba 100644
--- a/packages/remix-express/package.json
+++ b/packages/remix-express/package.json
@@ -13,6 +13,7 @@
"devDependencies": {
"@types/express": "^4.17.9",
"@types/supertest": "^2.0.10",
+ "node-mocks-http": "^1.10.1",
"supertest": "^6.0.1"
}
}
diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts
index e4d12a206f4..2d2e7f919b1 100644
--- a/packages/remix-express/server.ts
+++ b/packages/remix-express/server.ts
@@ -71,26 +71,28 @@ export function createRequestHandler({
};
}
-function createRemixHeaders(
+export function createRemixHeaders(
requestHeaders: express.Request["headers"]
): NodeHeaders {
- return new NodeHeaders(
- Object.keys(requestHeaders).reduce((memo, key) => {
- let value = requestHeaders[key];
-
- if (typeof value === "string") {
- memo[key] = value;
- } else if (Array.isArray(value)) {
- memo[key] = value.join(",");
+ let headers = new NodeHeaders();
+
+ for (let [key, values] of Object.entries(requestHeaders)) {
+ if (values) {
+ if (Array.isArray(values)) {
+ for (const value of values) {
+ headers.append(key, value);
+ }
+ } else {
+ headers.set(key, values);
}
+ }
+ }
- return memo;
- }, {} as { [headerName: string]: string })
- );
+ return headers;
}
-function createRemixRequest(req: express.Request): NodeRequest {
- let origin = `${req.protocol}://${req.hostname}`;
+export function createRemixRequest(req: express.Request): NodeRequest {
+ let origin = `${req.protocol}://${req.get("host")}`;
let url = new URL(req.url, origin);
let init: NodeRequestInit = {
@@ -111,8 +113,10 @@ function sendRemixResponse(
): void {
res.status(response.status);
- for (let [key, value] of response.headers.entries()) {
- res.set(key, value);
+ for (let [key, values] of Object.entries(response.headers.raw())) {
+ for (const value of values) {
+ res.append(key, value);
+ }
}
if (Buffer.isBuffer(response.body)) {
diff --git a/packages/remix-express/tsconfig.json b/packages/remix-express/tsconfig.json
index 55305c7bbd0..8040ec74b95 100644
--- a/packages/remix-express/tsconfig.json
+++ b/packages/remix-express/tsconfig.json
@@ -1,5 +1,6 @@
{
- "exclude": ["__tests__/**/*"],
+ "include": ["**/*"],
+ "exclude": ["__tests__"],
"compilerOptions": {
"lib": ["ES2019"],
"target": "ES2019",
diff --git a/packages/remix-init/templates/_shared_js/app/entry.server.jsx b/packages/remix-init/templates/_shared_js/app/entry.server.jsx
index b79eedb9841..2a902ea1764 100644
--- a/packages/remix-init/templates/_shared_js/app/entry.server.jsx
+++ b/packages/remix-init/templates/_shared_js/app/entry.server.jsx
@@ -11,11 +11,10 @@ export default function handleRequest(
);
+ responseHeaders.set("Content-Type", "text/html");
+
return new Response("" + markup, {
status: responseStatusCode,
- headers: {
- ...Object.fromEntries(responseHeaders),
- "Content-Type": "text/html"
- }
+ headers: responseHeaders
});
}
diff --git a/packages/remix-init/templates/_shared_ts/app/entry.server.tsx b/packages/remix-init/templates/_shared_ts/app/entry.server.tsx
index edf2cc0c15f..1c277b26931 100644
--- a/packages/remix-init/templates/_shared_ts/app/entry.server.tsx
+++ b/packages/remix-init/templates/_shared_ts/app/entry.server.tsx
@@ -12,11 +12,10 @@ export default function handleRequest(
);
+ responseHeaders.set("Content-Type", "text/html");
+
return new Response("" + markup, {
status: responseStatusCode,
- headers: {
- ...Object.fromEntries(responseHeaders),
- "Content-Type": "text/html"
- }
+ headers: responseHeaders
});
}
diff --git a/packages/remix-init/templates/arc/package.json b/packages/remix-init/templates/arc/package.json
index 4247d5671be..d677067570f 100644
--- a/packages/remix-init/templates/arc/package.json
+++ b/packages/remix-init/templates/arc/package.json
@@ -5,7 +5,6 @@
"start": "arc sandbox"
},
"dependencies": {
- "@architect/functions": "^3.13.3",
"@remix-run/architect": "*",
"aws-sdk": "2.796.0"
}
diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts
index 3bd683c294a..e4b233e162a 100644
--- a/packages/remix-server-runtime/headers.ts
+++ b/packages/remix-server-runtime/headers.ts
@@ -1,7 +1,7 @@
+import { splitCookiesString } from "set-cookie-parser";
import type { ServerBuild } from "./build";
import type { ServerRoute } from "./routes";
import type { RouteMatch } from "./routeMatching";
-
export function getDocumentHeaders(
build: ServerBuild,
matches: RouteMatch[],
@@ -12,7 +12,6 @@ export function getDocumentHeaders(
let loaderHeaders = routeLoaderResponses[index]
? routeLoaderResponses[index].headers
: new Headers();
-
let headers = new Headers(
routeModule.headers
? routeModule.headers({ loaderHeaders, parentHeaders })
@@ -29,61 +28,12 @@ export function getDocumentHeaders(
}
function prependCookies(parentHeaders: Headers, childHeaders: Headers): void {
- if (parentHeaders.has("Set-Cookie")) {
- childHeaders.set(
- "Set-Cookie",
- concatSetCookieHeaders(
- parentHeaders.get("Set-Cookie")!,
- childHeaders.get("Set-Cookie")
- )
- );
- }
-}
-
-/**
- * Merges two `Set-Cookie` headers, eliminating duplicates and preserving the
- * original ordering.
- */
-function concatSetCookieHeaders(
- parentHeader: string,
- childHeader: string | null
-): string {
- if (!childHeader || childHeader === parentHeader) {
- return parentHeader;
- }
-
- let finalCookies: RawSetCookies = new Map();
- let parentCookies = parseSetCookieHeader(parentHeader);
- let childCookies = parseSetCookieHeader(childHeader);
-
- for (let [name, value] of parentCookies) {
- finalCookies.set(name, childCookies.get(name) || value);
- }
+ let parentSetCookieString = parentHeaders.get("Set-Cookie");
- for (let [name, value] of childCookies) {
- if (!finalCookies.has(name)) {
- finalCookies.set(name, value);
- }
+ if (parentSetCookieString) {
+ let cookies = splitCookiesString(parentSetCookieString);
+ cookies.forEach(cookie => {
+ childHeaders.append("Set-Cookie", cookie);
+ });
}
-
- return serializeSetCookieHeader(finalCookies);
-}
-
-type RawSetCookies = Map;
-
-function parseSetCookieHeader(header: string): RawSetCookies {
- return header.split(/\s*,\s*/g).reduce((map, pair) => {
- let [name, value] = pair.split("=");
- return map.set(name, value);
- }, new Map());
-}
-
-function serializeSetCookieHeader(cookies: RawSetCookies): string {
- let pairs: string[] = [];
-
- for (let [name, value] of cookies) {
- pairs.push(name + "=" + value);
- }
-
- return pairs.join(", ");
}
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index c1985e79faa..ad8009a87be 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -10,6 +10,7 @@
"history": "^5.0.0",
"jsesc": "^3.0.1",
"react-router-dom": "^6.0.0-beta.0",
+ "set-cookie-parser": "^2.4.8",
"source-map": "^0.7.3"
},
"peerDependencies": {
@@ -17,7 +18,8 @@
"react-dom": ">=16.8"
},
"devDependencies": {
- "@types/jsesc": "^2.5.1"
+ "@types/jsesc": "^2.5.1",
+ "@types/set-cookie-parser": "^2.4.1"
},
"sideEffects": false
}
diff --git a/packages/remix-vercel/__tests__/server-test.ts b/packages/remix-vercel/__tests__/server-test.ts
new file mode 100644
index 00000000000..9d8c16cfd8e
--- /dev/null
+++ b/packages/remix-vercel/__tests__/server-test.ts
@@ -0,0 +1,253 @@
+import supertest from "supertest";
+import { Response, Headers } from "@remix-run/node";
+import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";
+import { createRequest } from "node-mocks-http";
+import { createServerWithHelpers } from "@vercel/node/dist/helpers";
+import { VercelRequest } from "@vercel/node";
+
+import {
+ createRemixHeaders,
+ createRemixRequest,
+ createRequestHandler
+} from "../server";
+
+// We don't want to test that the remix server works here (that's what the
+// puppetteer tests do), we just want to test the vercel adapter
+jest.mock("@remix-run/server-runtime");
+let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction<
+ typeof createRemixRequestHandler
+>;
+
+let consumeEventMock = jest.fn();
+let mockBridge = { consumeEvent: consumeEventMock };
+
+function createApp() {
+ // TODO: get supertest args into the event
+ consumeEventMock.mockImplementationOnce(() => ({ body: "" }));
+ let server = createServerWithHelpers(
+ createRequestHandler({ build: undefined }),
+ mockBridge
+ );
+ return server;
+}
+
+describe("vercel createRequestHandler", () => {
+ describe("basic requests", () => {
+ afterEach(async () => {
+ mockedCreateRequestHandler.mockReset();
+ consumeEventMock.mockClear();
+ });
+
+ afterAll(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("handles requests", async () => {
+ mockedCreateRequestHandler.mockImplementation(() => async req => {
+ return new Response(`URL: ${new URL(req.url).pathname}`);
+ });
+
+ let request = supertest(createApp({}));
+ // note: vercel's createServerWithHelpers requires a x-now-bridge-request-id
+ let res = await request
+ .get("/foo/bar")
+ .set({ "x-now-bridge-request-id": "2" });
+
+ expect(res.status).toBe(200);
+ expect(res.text).toBe("URL: /foo/bar");
+ });
+
+ it("handles status codes", async () => {
+ mockedCreateRequestHandler.mockImplementation(() => async () => {
+ return new Response("", { status: 204 });
+ });
+
+ let request = supertest(createApp());
+ // note: vercel's createServerWithHelpers requires a x-now-bridge-request-id
+ let res = await request.get("/").set({ "x-now-bridge-request-id": "2" });
+
+ expect(res.status).toBe(204);
+ });
+
+ it("sets headers", async () => {
+ mockedCreateRequestHandler.mockImplementation(() => async () => {
+ const headers = new Headers({ "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("", { headers });
+ });
+
+ let request = supertest(createApp());
+ // note: vercel's createServerWithHelpers requires a x-now-bridge-request-id
+ let res = await request.get("/").set({ "x-now-bridge-request-id": "2" });
+
+ expect(res.headers["x-time-of-year"]).toBe("most wonderful");
+ expect(res.headers["set-cookie"]).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("vercel createRemixHeaders", () => {
+ describe("creates fetch headers from vercel headers", () => {
+ it("handles empty headers", () => {
+ expect(createRemixHeaders({})).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {},
+ }
+ `);
+ });
+
+ it("handles simple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar" })).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-foo": Array [
+ "bar",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles multiple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar", "x-bar": "baz" }))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-bar": Array [
+ "baz",
+ ],
+ "x-foo": Array [
+ "bar",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles headers with multiple values", () => {
+ expect(createRemixHeaders({ "x-foo": "bar, baz" }))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-foo": Array [
+ "bar, baz",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles headers with multiple values and multiple headers", () => {
+ expect(createRemixHeaders({ "x-foo": "bar, baz", "x-bar": "baz" }))
+ .toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "x-bar": Array [
+ "baz",
+ ],
+ "x-foo": Array [
+ "bar, baz",
+ ],
+ },
+ }
+ `);
+ });
+
+ it("handles multiple set-cookie headers", () => {
+ expect(
+ createRemixHeaders({
+ "set-cookie": [
+ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
+ "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax"
+ ]
+ })
+ ).toMatchInlineSnapshot(`
+ Headers {
+ Symbol(map): Object {
+ "set-cookie": Array [
+ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
+ "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax",
+ ],
+ },
+ }
+ `);
+ });
+ });
+});
+
+describe("vercel createRemixRequest", () => {
+ it("creates a request with the correct headers", async () => {
+ let request = createRequest({
+ method: "GET",
+ url: "/foo/bar",
+ headers: {
+ "x-forwarded-host": "localhost:3000",
+ "x-forwarded-proto": "http",
+ "Cache-Control": "max-age=300, s-maxage=3600"
+ }
+ }) as VercelRequest;
+
+ expect(createRemixRequest(request)).toMatchInlineSnapshot(`
+ Request {
+ "agent": undefined,
+ "compress": true,
+ "counter": 0,
+ "follow": 20,
+ "size": 0,
+ "timeout": 0,
+ Symbol(Body internals): Object {
+ "body": null,
+ "disturbed": false,
+ "error": null,
+ },
+ Symbol(Request internals): Object {
+ "headers": Headers {
+ Symbol(map): Object {
+ "cache-control": Array [
+ "max-age=300, s-maxage=3600",
+ ],
+ "x-forwarded-host": Array [
+ "localhost:3000",
+ ],
+ "x-forwarded-proto": Array [
+ "http",
+ ],
+ },
+ },
+ "method": "GET",
+ "parsedURL": Url {
+ "auth": null,
+ "hash": null,
+ "host": "localhost:3000",
+ "hostname": "localhost",
+ "href": "http://localhost:3000/foo/bar",
+ "path": "/foo/bar",
+ "pathname": "/foo/bar",
+ "port": "3000",
+ "protocol": "http:",
+ "query": null,
+ "search": null,
+ "slashes": true,
+ },
+ "redirect": "follow",
+ "signal": null,
+ },
+ }
+ `);
+ });
+});
diff --git a/packages/remix-vercel/package.json b/packages/remix-vercel/package.json
index 6eff198902f..d8e109550b8 100644
--- a/packages/remix-vercel/package.json
+++ b/packages/remix-vercel/package.json
@@ -11,6 +11,9 @@
"@vercel/node": "^1.8.3"
},
"devDependencies": {
- "@vercel/node": "^1.8.3"
+ "@types/supertest": "^2.0.10",
+ "@vercel/node": "^1.8.3",
+ "supertest": "^6.1.5",
+ "node-mocks-http": "^1.10.1"
}
}
diff --git a/packages/remix-vercel/server.ts b/packages/remix-vercel/server.ts
index 39e6fe63207..4cd4998bd5f 100644
--- a/packages/remix-vercel/server.ts
+++ b/packages/remix-vercel/server.ts
@@ -60,15 +60,12 @@ export function createRequestHandler({
};
}
-function createRemixRequest(req: VercelRequest): NodeRequest {
- let host = req.headers["x-forwarded-host"] || req.headers["host"];
- // doesn't seem to be available on their req object!
- let protocol = req.headers["x-forwarded-proto"] || "https";
- let url = new URL(req.url!, `${protocol}://${host}`);
-
+export function createRemixHeaders(
+ requestHeaders: VercelRequest["headers"]
+): NodeHeaders {
let headers = new NodeHeaders();
- for (let key in req.headers) {
- let header = req.headers[key]!;
+ for (let key in requestHeaders) {
+ let header = requestHeaders[key]!;
// set-cookie is an array (maybe others)
if (Array.isArray(header)) {
for (let value of header) {
@@ -79,9 +76,18 @@ function createRemixRequest(req: VercelRequest): NodeRequest {
}
}
+ return headers;
+}
+
+export function createRemixRequest(req: VercelRequest): NodeRequest {
+ let host = req.headers["x-forwarded-host"] || req.headers["host"];
+ // doesn't seem to be available on their req object!
+ let protocol = req.headers["x-forwarded-proto"] || "https";
+ let url = new URL(req.url!, `${protocol}://${host}`);
+
let init: NodeRequestInit = {
method: req.method,
- headers
+ headers: createRemixHeaders(req.headers)
};
if (req.method !== "GET" && req.method !== "HEAD") {
@@ -92,8 +98,6 @@ function createRemixRequest(req: VercelRequest): NodeRequest {
}
function sendRemixResponse(res: VercelResponse, response: NodeResponse): void {
- res.status(response.status);
-
let arrays = new Map();
for (let [key, value] of response.headers.entries()) {
if (arrays.has(key)) {
@@ -107,8 +111,12 @@ function sendRemixResponse(res: VercelResponse, response: NodeResponse): void {
}
if (Buffer.isBuffer(response.body)) {
- res.end(response.body);
+ return res
+ .writeHead(response.status, response.headers.raw())
+ .end(response.body);
} else {
- response.body.pipe(res);
+ return res
+ .writeHead(response.status, response.headers.raw())
+ .end(response.body.pipe(res));
}
}
diff --git a/packages/remix-vercel/tsconfig.json b/packages/remix-vercel/tsconfig.json
index b694acc00af..331e6d5d9d1 100644
--- a/packages/remix-vercel/tsconfig.json
+++ b/packages/remix-vercel/tsconfig.json
@@ -1,4 +1,6 @@
{
+ "include": ["**/*"],
+ "exclude": ["__tests__"],
"compilerOptions": {
"lib": ["ES2019"],
"target": "ES2019",
diff --git a/types/architect__functions.d.ts b/types/architect__functions.d.ts
deleted file mode 100644
index afa214d28eb..00000000000
--- a/types/architect__functions.d.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-declare module "@architect/functions" {
- /**
- * Requests are passed to your handler function in an object, and include the following parameters
- */
-
- export interface Request {
- /**
- * Payload version (e.g. 2.0)
- */
- version: string;
-
- /**
- * Tuple of HTTP method (GET, POST, PATCH, PUT, or DELETE) and path; URL
- * params are surrounded in braces If path is not captured by a specific
- * function, routeKey will be $default (and be handled by the get / function)
- *
- * Example: GET /, GET /shop/{product}
- */
- routeKey: string;
-
- /**
- * The absolute path of the request
- *
- * Example: /, /shop/chocolate-chip-cookies
- */
- rawPath: string;
-
- /**
- * Any URL params, if defined in your HTTP function's path (e.g. product in /shop/:product)
- *
- * Example: { product: 'chocolate-chip-cookies' }
- */
- pathParameters?: { [param: string]: string };
-
- /**
- * String containing query string params of request, if any
- *
- * Example: ?someParam=someValue, '' (if none)
- */
- rawQueryString: string;
-
- /**
- * Any query params if present in the client request
- *
- * Example: { someParam: someValue }
- */
- queryStringParameters?: { [param: string]: string };
-
- /**
- * Array containing all cookies, if present in client request
- *
- * Example: [ 'some_cookie_name=some_cookie_value' ]
- */
- cookies?: string[];
-
- /**
- * All client request headers
- *
- * Example: { 'accept-encoding': 'gzip' }
- */
- headers: { [header: string]: string };
-
- /**
- * Request metadata, including http object containing method and path (should
- * you not want to parse the routeKey)
- */
- requestContext: {
- http: {
- method: string;
- path: string;
- routeKey: string;
- };
- };
-
- /**
- * Contains unparsed, base64-encoded request body
- * We suggest parsing with a body parser helper
- */
- body?: string;
-
- /**
- * Indicates whether body is base64-encoded binary payload
- */
- isBase64Encoded: boolean;
- }
-
- export interface Response {
- /**
- * Sets the HTTP status code; usually to 200
- */
- statusCode: number;
-
- /**
- * All response headers
- */
- headers?: { [header: string]: string };
-
- /**
- * Contains response body, either as a plain string, or, if binary, a
- * base64-encoded buffer
- *
- * Note: The maximum body payload size is 6MB; files being delivered
- * non-dynamically should use the Begin CDN
- */
- body?: string;
-
- /**
- * Indicates whether body is base64-encoded binary payload; defaults to false
- *
- * Required to be set to true if emitting a binary payload
- */
- isBase64Encoded?: boolean;
- }
-
- // There's a lot more, but this is all we use
- export interface Arc {
- http: Http;
- }
-
- type session = { [key: string]: string };
-
- interface Http {
- session: {
- write: (session: session) => Promise;
- read: (request: Request) => Promise;
- };
- }
-
- const arc: Arc;
-
- export default arc;
-}
diff --git a/yarn.lock b/yarn.lock
index 55d5e47f2dc..53984453a7b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -943,6 +943,11 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@extra-number/significant-digits@^1.1.1":
+ version "1.3.9"
+ resolved "https://registry.yarnpkg.com/@extra-number/significant-digits/-/significant-digits-1.3.9.tgz#06f3acc4aa688af3ed76bf5f30bca6de9d60883f"
+ integrity sha512-E5PY/bCwrNqEHh4QS6AQBinLZ+sxM1lT8tsSVYk8VwhWIPp6fCU/BMRVq0V8iJ8LwS3FHmaA4vUzb78s4BIIyA==
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -1212,6 +1217,11 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
+"@types/aws-lambda@*", "@types/aws-lambda@^8.10.82":
+ version "8.10.82"
+ resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.82.tgz#336062d3270f52d2156eeb7d9e497fd63684572a"
+ integrity sha512-sJo8pz8hu+OzLRAj7Do2g66zYLizWtB3kGK6K45RWmGW+S54XXMoK3sNbvzKXfndBxYiSVExHoCNiSlt2gPmxw==
+
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
version "7.1.12"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d"
@@ -1400,6 +1410,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+"@types/lambda-tester@^3.6.1":
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/@types/lambda-tester/-/lambda-tester-3.6.1.tgz#e09d79a191a4cef6ffa4fc4805eb614c8b15edd6"
+ integrity sha512-cKg0SdVrtjkiAJcVTGabD+u+eAIdjBJ+qgXiolw0hffW6B/1o+h63EptSUo/BTYpkoRxnHFkHS55y4cWja4xIw==
+ dependencies:
+ "@types/aws-lambda" "*"
+
"@types/lodash.debounce@^4.0.6":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
@@ -1532,6 +1549,13 @@
"@types/mime" "^1"
"@types/node" "*"
+"@types/set-cookie-parser@^2.4.1":
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.1.tgz#49403d3150f6f296da8e51b3e9e7e562eaf105b4"
+ integrity sha512-N0IWe4vT1w5IOYdN9c9PNpQniHS+qe25W4tj4vfhJDJ9OkvA/YA55YUhaC+HNmMMeLlOSnBW9UMno0qlt5xu3Q==
+ dependencies:
+ "@types/node" "*"
+
"@types/signal-exit@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/signal-exit/-/signal-exit-3.0.0.tgz#75e3b17660cf1f6c6cb8557675b4e680e43bbf36"
@@ -1691,7 +1715,7 @@ abab@^2.0.3:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
-accepts@~1.3.5, accepts@~1.3.7:
+accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
@@ -2029,6 +2053,11 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
+app-root-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad"
+ integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==
+
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
@@ -2691,7 +2720,7 @@ clone-deep@^1.0.0:
kind-of "^5.0.0"
shallow-clone "^1.0.0"
-clone-deep@^4.0.0:
+clone-deep@^4.0.0, clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
@@ -3030,7 +3059,7 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
-depd@~1.1.2:
+depd@^1.1.0, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
@@ -3136,6 +3165,16 @@ domutils@^2.4.3, domutils@^2.4.4:
domelementtype "^2.0.1"
domhandler "^4.0.0"
+dotenv-json@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c"
+ integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ==
+
+dotenv@^8.0.0:
+ version "8.6.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
+ integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
+
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -3831,7 +3870,7 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
-fresh@0.5.2:
+fresh@0.5.2, fresh@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
@@ -5164,6 +5203,35 @@ koalas@^1.0.2:
resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd"
integrity sha1-MYQz8HQjXbePrlZhoCqMpT7ilc0=
+lambda-event-mock@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/lambda-event-mock/-/lambda-event-mock-1.5.0.tgz#9cb1ce2bec4271f918d485fef0a327d194dd120f"
+ integrity sha512-vx1d+vZqi7FF6B3+mAfHnY/6Tlp6BheL2ta0MJS0cIRB3Rc4I5cviHTkiJxHdE156gXx3ZjlQRJrS4puXvtrhA==
+ dependencies:
+ "@extra-number/significant-digits" "^1.1.1"
+ clone-deep "^4.0.1"
+ uuid "^3.3.3"
+ vandium-utils "^1.2.0"
+
+lambda-leak@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e"
+ integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4=
+
+lambda-tester@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lambda-tester/-/lambda-tester-4.0.1.tgz#91f0fc1266cdceae09a5ddbbdbc209c214beb98c"
+ integrity sha512-ft6XHk84B6/dYEzyI3anKoGWz08xQ5allEHiFYDUzaYTymgVK7tiBkCEbuWx+MFvH7OpFNsJXVtjXm0X8iH3Iw==
+ dependencies:
+ app-root-path "^3.0.0"
+ dotenv "^8.0.0"
+ dotenv-json "^1.0.0"
+ lambda-event-mock "^1.5.0"
+ lambda-leak "^2.0.0"
+ semver "^6.1.1"
+ uuid "^3.3.3"
+ vandium-utils "^2.0.0"
+
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
@@ -5390,7 +5458,7 @@ meow@^7.1.1:
type-fest "^0.13.1"
yargs-parser "^18.1.3"
-merge-descriptors@1.0.1:
+merge-descriptors@1.0.1, merge-descriptors@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
@@ -5454,7 +5522,7 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
dependencies:
mime-db "1.46.0"
-mime@1.6.0:
+mime@1.6.0, mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@@ -5648,6 +5716,21 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+node-mocks-http@^1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.10.1.tgz#0232e99a5f66f5d2a0216a47251346bf5606d2d0"
+ integrity sha512-/Nz83kiJ3z+vGqxmlDyv8+L1CJno+gH23DzG3oPH9dBSfMYa5IFVwPgZpXCB2kdiiIu/HoDpZ2BuLqQs7qjFLQ==
+ dependencies:
+ accepts "^1.3.7"
+ depd "^1.1.0"
+ fresh "^0.5.2"
+ merge-descriptors "^1.0.1"
+ methods "^1.1.2"
+ mime "^1.3.4"
+ parseurl "^1.3.3"
+ range-parser "^1.2.0"
+ type-is "^1.6.18"
+
node-modules-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
@@ -5970,7 +6053,7 @@ parse5@^6.0.0, parse5@^6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
-parseurl@~1.3.3:
+parseurl@^1.3.3, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
@@ -6312,7 +6395,7 @@ radio-symbol@^2.0.0:
ansi-green "^0.1.1"
is-windows "^1.0.1"
-range-parser@~1.2.1:
+range-parser@^1.2.0, range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
@@ -6787,7 +6870,7 @@ semver@7.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
-semver@^6.0.0, semver@^6.3.0:
+semver@^6.0.0, semver@^6.1.1, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@@ -6840,6 +6923,11 @@ set-blocking@^2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+set-cookie-parser@^2.4.8:
+ version "2.4.8"
+ resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2"
+ integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==
+
set-getter@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376"
@@ -7253,6 +7341,14 @@ supertest@^6.0.1:
methods "^1.1.2"
superagent "^6.1.0"
+supertest@^6.1.5:
+ version "6.1.5"
+ resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.5.tgz#b011c8465281b30c64e9d4be4cb3808b91cd1ec0"
+ integrity sha512-Is3pFB2TxSFPohDS2tIM8h2JOMvUQwbJ9TvTfsWAm89ZZv1CF4VTLAsQz+5+5S1wOgaMqFqSpFriU15L3e2AXQ==
+ dependencies:
+ methods "^1.1.2"
+ superagent "^6.1.0"
+
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -7566,7 +7662,7 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-type-is@~1.6.17, type-is@~1.6.18:
+type-is@^1.6.18, type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -7715,7 +7811,7 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
-uuid@^3.3.2:
+uuid@^3.3.2, uuid@^3.3.3:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
@@ -7747,6 +7843,16 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
+vandium-utils@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59"
+ integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k=
+
+vandium-utils@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/vandium-utils/-/vandium-utils-2.0.0.tgz#87389bdcb85551aaaba1cc95937ba756589214fa"
+ integrity sha512-XWbQ/0H03TpYDXk8sLScBEZpE7TbA0CHDL6/Xjt37IBYKLsHUQuBlL44ttAUs9zoBOLFxsW7HehXcuWCNyqOxQ==
+
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"