From 7736444542ab0d64f7b855a91b8ca0b8573580b4 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 21 Mar 2022 13:30:28 -0500 Subject: [PATCH] Refactor runtime packages to conform to the Remix server runtime interface (#2359) * feat(@remix-run/server-runtime): define interface for remix server runtimes * refactor(@remix-run/server-runtime): organize exports group types that should be re-exported by each remix server runtime * refactor(@remix-run/server-runtime): factories for crypto-dependent interface functions * docs(@remix-run/server-runtime): rewrite readme to explain the "remix server runtime interface" * refactor(@remix-run/node): conform to the remix server runtime interface instead of relying on magic exports * refactor(@remix-run/node): implement crypto-dependent interface functions * feat(@remix-run/cloudflare): create new server runtime package for cloudflare * refactor(@remix-run/cloudflare): implement crypto-dependent interface functions * refactor(remix-deno): conform to the remix server runtime interface * refactor(remix-deno): implement crypto-dependent interface functions * refactor(@remix-run/architect): use `@remix-run/node` instead of `@remix-run/server-runtime` * refactor(@remix-run/express): use `@remix-run/node` instead of `@remix-run/server-runtime` * refactor(@remix-run/netlify): use `@remix-run/node` instead of `@remix-run/server-runtime` * refactor(@remix-run/vercel): use `@remix-run/node` instead of `@remix-run/server-runtime` * refactor(@remix-run/cloudflare-workers): use `@remix-run/cloudflare` instead of `@remix-run/server-runtime` * refactor(@remix-run/cloudflare-pages): use `@remix-run/cloudflare` instead of `@remix-run/server-runtime` * refactor(server-runtimes,adapters): remove unused `platform` arg from `createRequestHandler` --- package.json | 1 + .../remix-architect/__tests__/server-test.ts | 10 ++- packages/remix-architect/package.json | 3 +- packages/remix-architect/server.ts | 10 +-- .../sessions/arcTableSessionStorage.ts | 7 +- packages/remix-cloudflare-pages/globals.ts | 13 --- packages/remix-cloudflare-pages/index.ts | 6 -- .../magicExports/remix.ts | 2 +- packages/remix-cloudflare-pages/package.json | 2 +- packages/remix-cloudflare-pages/responses.ts | 1 - .../sessions/cloudflareKVSessionStorage.ts | 77 ---------------- packages/remix-cloudflare-pages/worker.ts | 3 +- .../remix-cloudflare-workers/cookieSigning.ts | 52 ----------- packages/remix-cloudflare-workers/globals.ts | 18 ---- packages/remix-cloudflare-workers/index.ts | 6 -- .../magicExports/remix.ts | 2 +- .../remix-cloudflare-workers/package.json | 2 +- .../remix-cloudflare-workers/responses.ts | 1 - packages/remix-cloudflare-workers/worker.ts | 8 +- .../crypto.ts} | 11 ++- packages/remix-cloudflare/globals.ts | 12 +++ packages/remix-cloudflare/implementations.ts | 13 +++ packages/remix-cloudflare/index.ts | 54 ++++++++++++ .../remix-cloudflare/magicExports/remix.ts | 10 +++ packages/remix-cloudflare/package.json | 24 +++++ .../sessions/cloudflareKVSessionStorage.ts | 7 +- packages/remix-cloudflare/tsconfig.json | 22 +++++ packages/remix-dev/__tests__/cli-test.ts | 2 +- packages/remix-dev/cli/commands.ts | 13 ++- packages/remix-dev/cli/run.ts | 2 +- packages/remix-dev/cli/setup.ts | 6 +- .../remix-express/__tests__/server-test.ts | 14 ++- packages/remix-express/package.json | 3 +- packages/remix-express/server.ts | 8 +- .../remix-netlify/__tests__/server-test.ts | 10 ++- packages/remix-netlify/package.json | 3 +- packages/remix-netlify/server.ts | 12 +-- packages/remix-node/cookieSigning.ts | 16 ---- packages/remix-node/crypto.ts | 16 ++++ packages/remix-node/globals.ts | 13 --- packages/remix-node/implementations.ts | 13 +++ packages/remix-node/index.ts | 51 +++++++++++ packages/remix-node/magicExports/remix.ts | 4 + packages/remix-node/sessions/fileStorage.ts | 3 +- packages/remix-server-runtime/README.md | 29 ++++-- .../__tests__/cookies-test.ts | 17 +++- .../__tests__/server-test.ts | 88 +++++++++---------- .../__tests__/sessions-test.ts | 25 +++++- packages/remix-server-runtime/cookies.ts | 38 +++++--- .../{cookieSigning.ts => crypto.ts} | 11 +-- packages/remix-server-runtime/index.ts | 80 +++++++++-------- packages/remix-server-runtime/interface.ts | 10 +++ .../magicExports/remix.ts | 4 - packages/remix-server-runtime/platform.ts | 16 ---- packages/remix-server-runtime/reexport.ts | 49 +++++++++++ packages/remix-server-runtime/responses.ts | 24 ++--- packages/remix-server-runtime/server.ts | 17 ++-- packages/remix-server-runtime/sessions.ts | 34 +++++-- .../sessions/cookieStorage.ts | 15 +++- .../sessions/memoryStorage.ts | 14 ++- .../remix-vercel/__tests__/server-test.ts | 14 ++- packages/remix-vercel/package.json | 7 +- packages/remix-vercel/server.ts | 8 +- rollup.config.js | 85 +++++++++--------- scripts/publish-private.js | 1 + scripts/publish.js | 1 + 66 files changed, 657 insertions(+), 496 deletions(-) delete mode 100644 packages/remix-cloudflare-pages/globals.ts delete mode 100644 packages/remix-cloudflare-pages/responses.ts delete mode 100644 packages/remix-cloudflare-pages/sessions/cloudflareKVSessionStorage.ts delete mode 100644 packages/remix-cloudflare-workers/cookieSigning.ts delete mode 100644 packages/remix-cloudflare-workers/globals.ts delete mode 100644 packages/remix-cloudflare-workers/responses.ts rename packages/{remix-cloudflare-pages/cookieSigning.ts => remix-cloudflare/crypto.ts} (84%) create mode 100644 packages/remix-cloudflare/globals.ts create mode 100644 packages/remix-cloudflare/implementations.ts create mode 100644 packages/remix-cloudflare/index.ts create mode 100644 packages/remix-cloudflare/magicExports/remix.ts create mode 100644 packages/remix-cloudflare/package.json rename packages/{remix-cloudflare-workers => remix-cloudflare}/sessions/cloudflareKVSessionStorage.ts (92%) create mode 100644 packages/remix-cloudflare/tsconfig.json delete mode 100644 packages/remix-node/cookieSigning.ts create mode 100644 packages/remix-node/crypto.ts create mode 100644 packages/remix-node/implementations.ts rename packages/remix-server-runtime/{cookieSigning.ts => crypto.ts} (85%) create mode 100644 packages/remix-server-runtime/interface.ts delete mode 100644 packages/remix-server-runtime/platform.ts create mode 100644 packages/remix-server-runtime/reexport.ts diff --git a/package.json b/package.json index 5e1604ed16d..2468921f30f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "packages/create-remix", "packages/remix", "packages/remix-architect", + "packages/remix-cloudflare", "packages/remix-cloudflare-pages", "packages/remix-cloudflare-workers", "packages/remix-dev", diff --git a/packages/remix-architect/__tests__/server-test.ts b/packages/remix-architect/__tests__/server-test.ts index c2910b1c853..f9046a917b3 100644 --- a/packages/remix-architect/__tests__/server-test.ts +++ b/packages/remix-architect/__tests__/server-test.ts @@ -2,10 +2,10 @@ import fsp from "fs/promises"; import path from "path"; import lambdaTester from "lambda-tester"; import type { APIGatewayProxyEventV2 } from "aws-lambda"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; import { // This has been added as a global in node 15+ AbortController, + createRequestHandler as createRemixRequestHandler, Response as NodeResponse, } from "@remix-run/node"; @@ -18,7 +18,13 @@ import { // 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"); +jest.mock("@remix-run/node", () => { + let original = jest.requireActual("@remix-run/node"); + return { + ...original, + createRequestHandler: jest.fn() + }; +}); let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< typeof createRemixRequestHandler diff --git a/packages/remix-architect/package.json b/packages/remix-architect/package.json index a5c97a70123..5834f6ef85d 100644 --- a/packages/remix-architect/package.json +++ b/packages/remix-architect/package.json @@ -13,9 +13,8 @@ }, "dependencies": { "@architect/functions": "^5.0.2", - "@types/aws-lambda": "^8.10.82", "@remix-run/node": "1.3.2", - "@remix-run/server-runtime": "1.3.2" + "@types/aws-lambda": "^8.10.82" }, "devDependencies": { "@types/architect__functions": "^3.13.6", diff --git a/packages/remix-architect/server.ts b/packages/remix-architect/server.ts index 62d980877bd..76806c4d8a2 100644 --- a/packages/remix-architect/server.ts +++ b/packages/remix-architect/server.ts @@ -3,6 +3,7 @@ import { AbortController, Headers as NodeHeaders, Request as NodeRequest, + createRequestHandler as createRemixRequestHandler, } from "@remix-run/node"; import type { APIGatewayProxyEventHeaders, @@ -13,10 +14,8 @@ import type { import type { AppLoadContext, ServerBuild, - ServerPlatform, -} from "@remix-run/server-runtime"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; -import type { Response as NodeResponse } from "@remix-run/node"; + Response as NodeResponse, +} from "@remix-run/node"; import { isBinaryType } from "./binary-types"; @@ -46,8 +45,7 @@ export function createRequestHandler({ getLoadContext?: GetLoadContextFunction; mode?: string; }): APIGatewayProxyHandlerV2 { - let platform: ServerPlatform = {}; - let handleRequest = createRemixRequestHandler(build, platform, mode); + let handleRequest = createRemixRequestHandler(build, mode); return async (event, _context) => { let abortController = new AbortController(); diff --git a/packages/remix-architect/sessions/arcTableSessionStorage.ts b/packages/remix-architect/sessions/arcTableSessionStorage.ts index 01eb63e5c49..a91f20245df 100644 --- a/packages/remix-architect/sessions/arcTableSessionStorage.ts +++ b/packages/remix-architect/sessions/arcTableSessionStorage.ts @@ -1,9 +1,6 @@ import * as crypto from "crypto"; -import type { - SessionStorage, - SessionIdStorageStrategy, -} from "@remix-run/server-runtime"; -import { createSessionStorage } from "@remix-run/server-runtime"; +import type { SessionStorage, SessionIdStorageStrategy } from "@remix-run/node"; +import { createSessionStorage } from "@remix-run/node"; import arc from "@architect/functions"; import type { ArcTable } from "@architect/functions/tables"; diff --git a/packages/remix-cloudflare-pages/globals.ts b/packages/remix-cloudflare-pages/globals.ts deleted file mode 100644 index 18cc482865a..00000000000 --- a/packages/remix-cloudflare-pages/globals.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { sign, unsign } from "./cookieSigning"; - -declare global { - interface WorkerGlobalScope { - sign: typeof sign; - unsign: typeof unsign; - } -} - -export function installGlobals() { - self.sign = sign; - self.unsign = unsign; -} diff --git a/packages/remix-cloudflare-pages/index.ts b/packages/remix-cloudflare-pages/index.ts index 09d70bf7890..aa14b710309 100644 --- a/packages/remix-cloudflare-pages/index.ts +++ b/packages/remix-cloudflare-pages/index.ts @@ -1,8 +1,2 @@ -import { installGlobals } from "./globals"; - -export { createCloudflareKVSessionStorage } from "./sessions/cloudflareKVSessionStorage"; - export type { createPagesFunctionHandlerParams } from "./worker"; export { createPagesFunctionHandler, createRequestHandler } from "./worker"; - -installGlobals(); diff --git a/packages/remix-cloudflare-pages/magicExports/remix.ts b/packages/remix-cloudflare-pages/magicExports/remix.ts index 3de2c371416..3b2dbefafa4 100644 --- a/packages/remix-cloudflare-pages/magicExports/remix.ts +++ b/packages/remix-cloudflare-pages/magicExports/remix.ts @@ -1,3 +1,3 @@ // Re-export everything from this package that is available in `remix`. -export { createCloudflareKVSessionStorage } from "@remix-run/cloudflare-pages"; +export { createCloudflareKVSessionStorage } from "@remix-run/cloudflare"; diff --git a/packages/remix-cloudflare-pages/package.json b/packages/remix-cloudflare-pages/package.json index 9edd8b43260..34b6e600951 100644 --- a/packages/remix-cloudflare-pages/package.json +++ b/packages/remix-cloudflare-pages/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.3.2" + "@remix-run/cloudflare": "1.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^3.2.0" diff --git a/packages/remix-cloudflare-pages/responses.ts b/packages/remix-cloudflare-pages/responses.ts deleted file mode 100644 index c79ebc9087d..00000000000 --- a/packages/remix-cloudflare-pages/responses.ts +++ /dev/null @@ -1 +0,0 @@ -export { json, redirect } from "@remix-run/server-runtime"; diff --git a/packages/remix-cloudflare-pages/sessions/cloudflareKVSessionStorage.ts b/packages/remix-cloudflare-pages/sessions/cloudflareKVSessionStorage.ts deleted file mode 100644 index 29fcff07387..00000000000 --- a/packages/remix-cloudflare-pages/sessions/cloudflareKVSessionStorage.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { - SessionStorage, - SessionIdStorageStrategy, -} from "@remix-run/server-runtime"; -import { createSessionStorage } from "@remix-run/server-runtime"; - -interface KVSessionStorageOptions { - /** - * The Cookie used to store the session id on the client, or options used - * to automatically create one. - */ - cookie?: SessionIdStorageStrategy["cookie"]; - - /** - * The KVNamespace used to store the sessions. - */ - kv: KVNamespace; -} - -/** - * Creates a SessionStorage that stores session data in the Clouldflare KV Store. - * - * The advantage of using this instead of cookie session storage is that - * KV Store may contain much more data than cookies. - * - * @see https://remix.run/api/remix#createcloudflarekvsessionstorage-cloudflare-workers - */ -export function createCloudflareKVSessionStorage({ - cookie, - kv, -}: KVSessionStorageOptions): SessionStorage { - return createSessionStorage({ - cookie, - async createData(data, expires) { - while (true) { - let randomBytes = new Uint8Array(8); - crypto.getRandomValues(randomBytes); - // 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 kv.get(id, "json")) { - continue; - } - - await kv.put(id, JSON.stringify(data), { - expiration: expires - ? Math.round(expires.getTime() / 1000) - : undefined, - }); - - return id; - } - }, - async readData(id) { - let session = await kv.get(id); - - if (!session) { - return null; - } - - return JSON.parse(session); - }, - async updateData(id, data, expires) { - await kv.put(id, JSON.stringify(data), { - expiration: expires ? Math.round(expires.getTime() / 1000) : undefined, - }); - }, - async deleteData(id) { - await kv.delete(id); - }, - }); -} diff --git a/packages/remix-cloudflare-pages/worker.ts b/packages/remix-cloudflare-pages/worker.ts index bd477776c15..1d379060dde 100644 --- a/packages/remix-cloudflare-pages/worker.ts +++ b/packages/remix-cloudflare-pages/worker.ts @@ -12,8 +12,7 @@ export function createRequestHandler({ getLoadContext, mode, }: createPagesFunctionHandlerParams): PagesFunction { - let platform = {}; - let handleRequest = createRemixRequestHandler(build, platform, mode); + let handleRequest = createRemixRequestHandler(build, mode); return (context) => { let loadContext = diff --git a/packages/remix-cloudflare-workers/cookieSigning.ts b/packages/remix-cloudflare-workers/cookieSigning.ts deleted file mode 100644 index fed5cef6aed..00000000000 --- a/packages/remix-cloudflare-workers/cookieSigning.ts +++ /dev/null @@ -1,52 +0,0 @@ -const encoder = new TextEncoder(); - -export async function sign(value: string, secret: string): Promise { - let key = await crypto.subtle.importKey( - "raw", - encoder.encode(secret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"] - ); - - let data = encoder.encode(value); - let signature = await crypto.subtle.sign("HMAC", key, data); - let hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace( - /=+$/, - "" - ); - - return value + "." + hash; -} - -export async function unsign( - cookie: string, - secret: string -): Promise { - let key = await crypto.subtle.importKey( - "raw", - encoder.encode(secret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["verify"] - ); - - let value = cookie.slice(0, cookie.lastIndexOf(".")); - let hash = cookie.slice(cookie.lastIndexOf(".") + 1); - - let data = encoder.encode(value); - let signature = byteStringToUint8Array(atob(hash)); - let valid = await crypto.subtle.verify("HMAC", key, signature, data); - - return valid ? value : false; -} - -function byteStringToUint8Array(byteString: string): Uint8Array { - let array = new Uint8Array(byteString.length); - - for (let i = 0; i < byteString.length; i++) { - array[i] = byteString.charCodeAt(i); - } - - return array; -} diff --git a/packages/remix-cloudflare-workers/globals.ts b/packages/remix-cloudflare-workers/globals.ts deleted file mode 100644 index bb04608cf6a..00000000000 --- a/packages/remix-cloudflare-workers/globals.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { sign, unsign } from "./cookieSigning"; - -declare global { - interface ProcessEnv { - NODE_ENV: "development" | "production" | "test"; - } - - interface WorkerGlobalScope { - sign: typeof sign; - unsign: typeof unsign; - process: { env: ProcessEnv }; - } -} - -export function installGlobals() { - self.sign = sign; - self.unsign = unsign; -} diff --git a/packages/remix-cloudflare-workers/index.ts b/packages/remix-cloudflare-workers/index.ts index b0de6b2e066..d2cf7d729e0 100644 --- a/packages/remix-cloudflare-workers/index.ts +++ b/packages/remix-cloudflare-workers/index.ts @@ -1,12 +1,6 @@ -import { installGlobals } from "./globals"; - -export { createCloudflareKVSessionStorage } from "./sessions/cloudflareKVSessionStorage"; - export type { GetLoadContextFunction, RequestHandler } from "./worker"; export { createEventHandler, createRequestHandler, handleAsset, } from "./worker"; - -installGlobals(); diff --git a/packages/remix-cloudflare-workers/magicExports/remix.ts b/packages/remix-cloudflare-workers/magicExports/remix.ts index 1544e0bbdb2..3b2dbefafa4 100644 --- a/packages/remix-cloudflare-workers/magicExports/remix.ts +++ b/packages/remix-cloudflare-workers/magicExports/remix.ts @@ -1,3 +1,3 @@ // Re-export everything from this package that is available in `remix`. -export { createCloudflareKVSessionStorage } from "@remix-run/cloudflare-workers"; +export { createCloudflareKVSessionStorage } from "@remix-run/cloudflare"; diff --git a/packages/remix-cloudflare-workers/package.json b/packages/remix-cloudflare-workers/package.json index ed5ba8e9522..f2893e59709 100644 --- a/packages/remix-cloudflare-workers/package.json +++ b/packages/remix-cloudflare-workers/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@cloudflare/kv-asset-handler": "^0.1.3", - "@remix-run/server-runtime": "1.3.2" + "@remix-run/cloudflare": "1.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^2.2.2" diff --git a/packages/remix-cloudflare-workers/responses.ts b/packages/remix-cloudflare-workers/responses.ts deleted file mode 100644 index c79ebc9087d..00000000000 --- a/packages/remix-cloudflare-workers/responses.ts +++ /dev/null @@ -1 +0,0 @@ -export { json, redirect } from "@remix-run/server-runtime"; diff --git a/packages/remix-cloudflare-workers/worker.ts b/packages/remix-cloudflare-workers/worker.ts index f7f9f8df6ff..152d0490dcb 100644 --- a/packages/remix-cloudflare-workers/worker.ts +++ b/packages/remix-cloudflare-workers/worker.ts @@ -7,9 +7,8 @@ import { import type { AppLoadContext, ServerBuild, - ServerPlatform, -} from "@remix-run/server-runtime"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; +} from "@remix-run/cloudflare"; +import { createRequestHandler as createRemixRequestHandler } from "@remix-run/cloudflare"; /** * A function that returns the value to use as `context` in route `loader` and @@ -37,8 +36,7 @@ export function createRequestHandler({ getLoadContext?: GetLoadContextFunction; mode?: string; }) { - let platform: ServerPlatform = {}; - let handleRequest = createRemixRequestHandler(build, platform, mode); + let handleRequest = createRemixRequestHandler(build, mode); return (event: FetchEvent) => { let loadContext = diff --git a/packages/remix-cloudflare-pages/cookieSigning.ts b/packages/remix-cloudflare/crypto.ts similarity index 84% rename from packages/remix-cloudflare-pages/cookieSigning.ts rename to packages/remix-cloudflare/crypto.ts index fed5cef6aed..85448bbb28a 100644 --- a/packages/remix-cloudflare-pages/cookieSigning.ts +++ b/packages/remix-cloudflare/crypto.ts @@ -1,6 +1,8 @@ +import type { SignFunction, UnsignFunction } from "@remix-run/server-runtime"; + const encoder = new TextEncoder(); -export async function sign(value: string, secret: string): Promise { +export const sign: SignFunction = async (value, secret) => { let key = await crypto.subtle.importKey( "raw", encoder.encode(secret), @@ -19,10 +21,7 @@ export async function sign(value: string, secret: string): Promise { return value + "." + hash; } -export async function unsign( - cookie: string, - secret: string -): Promise { +export const unsign: UnsignFunction = async (cookie, secret) => { let key = await crypto.subtle.importKey( "raw", encoder.encode(secret), @@ -49,4 +48,4 @@ function byteStringToUint8Array(byteString: string): Uint8Array { } return array; -} +} \ No newline at end of file diff --git a/packages/remix-cloudflare/globals.ts b/packages/remix-cloudflare/globals.ts new file mode 100644 index 00000000000..68e088da498 --- /dev/null +++ b/packages/remix-cloudflare/globals.ts @@ -0,0 +1,12 @@ +// https://stackoverflow.com/a/59499895 +export {}; + +declare global { + interface ProcessEnv { + NODE_ENV: "development" | "production" | "test"; + } + + interface WorkerGlobalScope { + process: { env: ProcessEnv }; + } +} diff --git a/packages/remix-cloudflare/implementations.ts b/packages/remix-cloudflare/implementations.ts new file mode 100644 index 00000000000..a5b951f83ad --- /dev/null +++ b/packages/remix-cloudflare/implementations.ts @@ -0,0 +1,13 @@ +import { + createCookieFactory, + createCookieSessionStorageFactory, + createMemorySessionStorageFactory, + createSessionStorageFactory, +} from "@remix-run/server-runtime"; + +import { sign, unsign } from "./crypto"; + +export const createCookie = createCookieFactory({ sign, unsign }); +export const createCookieSessionStorage = createCookieSessionStorageFactory(createCookie); +export const createSessionStorage = createSessionStorageFactory(createCookie); +export const createMemorySessionStorage = createMemorySessionStorageFactory(createSessionStorage); diff --git a/packages/remix-cloudflare/index.ts b/packages/remix-cloudflare/index.ts new file mode 100644 index 00000000000..cc91ea54c3a --- /dev/null +++ b/packages/remix-cloudflare/index.ts @@ -0,0 +1,54 @@ +import "./globals" + +export { createCloudflareKVSessionStorage } from "./sessions/cloudflareKVSessionStorage"; + +export { + createCookie, + createCookieSessionStorage, + createMemorySessionStorage, + createSessionStorage, +} from './implementations'; + +export { + createRequestHandler, + createSession, + isCookie, + isSession, + json, + redirect, +} from "@remix-run/server-runtime"; + +export type { + ActionFunction, + AppData, + AppLoadContext, + CreateRequestHandlerFunction, + Cookie, + CookieOptions, + CookieParseOptions, + CookieSerializeOptions, + CookieSignatureOptions, + DataFunctionArgs, + EntryContext, + ErrorBoundaryComponent, + HandleDataRequestFunction, + HandleDocumentRequestFunction, + HeadersFunction, + HtmlLinkDescriptor, + HtmlMetaDescriptor, + LinkDescriptor, + LinksFunction, + LoaderFunction, + MetaDescriptor, + MetaFunction, + PageLinkDescriptor, + RequestHandler, + RouteComponent, + RouteHandle, + ServerBuild, + ServerEntryModule, + Session, + SessionData, + SessionIdStorageStrategy, + SessionStorage, +} from "@remix-run/server-runtime"; \ No newline at end of file diff --git a/packages/remix-cloudflare/magicExports/remix.ts b/packages/remix-cloudflare/magicExports/remix.ts new file mode 100644 index 00000000000..3b20389675b --- /dev/null +++ b/packages/remix-cloudflare/magicExports/remix.ts @@ -0,0 +1,10 @@ +// Re-export everything from this package that is available in `remix`. + + +export { + createCloudflareKVSessionStorage, + createCookie, + createSessionStorage, + createCookieSessionStorage, + createMemorySessionStorage, +} from "@remix-run/cloudflare"; \ No newline at end of file diff --git a/packages/remix-cloudflare/package.json b/packages/remix-cloudflare/package.json new file mode 100644 index 00000000000..bb6e7501431 --- /dev/null +++ b/packages/remix-cloudflare/package.json @@ -0,0 +1,24 @@ +{ + "name": "@remix-run/cloudflare", + "description": "Cloudflare platform abstractions for Remix", + "version": "1.3.2", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-cloudflare" + }, + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, + "dependencies": { + "@cloudflare/kv-asset-handler": "^0.1.3", + "@remix-run/server-runtime": "1.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^2.2.2" + }, + "devDependencies": { + "@cloudflare/workers-types": "^2.2.2" + } +} diff --git a/packages/remix-cloudflare-workers/sessions/cloudflareKVSessionStorage.ts b/packages/remix-cloudflare/sessions/cloudflareKVSessionStorage.ts similarity index 92% rename from packages/remix-cloudflare-workers/sessions/cloudflareKVSessionStorage.ts rename to packages/remix-cloudflare/sessions/cloudflareKVSessionStorage.ts index 6c4221eea74..455c74e3900 100644 --- a/packages/remix-cloudflare-workers/sessions/cloudflareKVSessionStorage.ts +++ b/packages/remix-cloudflare/sessions/cloudflareKVSessionStorage.ts @@ -2,9 +2,10 @@ import type { SessionStorage, SessionIdStorageStrategy, } from "@remix-run/server-runtime"; -import { createSessionStorage } from "@remix-run/server-runtime"; -interface KVSessionStorageOptions { +import { createSessionStorage } from "../implementations"; + +interface CloudflareKVSessionStorageOptions { /** * The Cookie used to store the session id on the client, or options used * to automatically create one. @@ -26,7 +27,7 @@ interface KVSessionStorageOptions { export function createCloudflareKVSessionStorage({ cookie, kv, -}: KVSessionStorageOptions): SessionStorage { +}: CloudflareKVSessionStorageOptions): SessionStorage { return createSessionStorage({ cookie, async createData(data, expires) { diff --git a/packages/remix-cloudflare/tsconfig.json b/packages/remix-cloudflare/tsconfig.json new file mode 100644 index 00000000000..42d0b0af571 --- /dev/null +++ b/packages/remix-cloudflare/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["**/*.ts"], + "compilerOptions": { + "lib": ["ES2019", "WebWorker"], + "target": "ES2019", + "types": ["@cloudflare/workers-types"], + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "outDir": "../../build/node_modules/@remix-run/cloudflare", + "rootDir": ".", + + // 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/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index d68a085a2df..79c9a136ca5 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -67,7 +67,7 @@ describe("remix cli", () => { Values: - projectDir The Remix project directory - template The project template to use - - remixPlatform node, cloudflare-pages, or cloudflare-workers + - remixPlatform node or cloudflare Creating a new project: diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 4f06ec75b72..9dc73d5a0ae 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -93,9 +93,16 @@ export async function init(projectDir: string) { } export async function setup(platformArg?: string) { - let platform = isSetupPlatform(platformArg) - ? platformArg - : SetupPlatform.Node; + let platform: SetupPlatform + if (platformArg === "cloudflare-workers" || platformArg === "cloudflare-pages") { + console.warn(`Using '${platformArg}' as a platform value is deprecated. Use 'cloudflare' instead.`); + console.log('HINT: check the `postinstall` script in `package.json`'); + platform = SetupPlatform.Cloudflare; + } else { + platform = isSetupPlatform(platformArg) + ? platformArg + : SetupPlatform.Node; + } await setupRemix(platform); diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index afdb0e4d5c9..796eb3be1dc 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -43,7 +43,7 @@ ${colors.heading("Values")}: - ${colors.arg("template")} The project template to use - ${colors.arg( "remixPlatform" - )} node, cloudflare-pages, or cloudflare-workers + )} node or cloudflare ${colors.heading("Creating a new project")}: diff --git a/packages/remix-dev/cli/setup.ts b/packages/remix-dev/cli/setup.ts index 297e4111404..ded38c93828 100644 --- a/packages/remix-dev/cli/setup.ts +++ b/packages/remix-dev/cli/setup.ts @@ -2,15 +2,13 @@ import * as path from "path"; import * as fse from "fs-extra"; export enum SetupPlatform { - CloudflarePages = "cloudflare-pages", - CloudflareWorkers = "cloudflare-workers", + Cloudflare = "cloudflare", Node = "node", } export function isSetupPlatform(platform: any): platform is SetupPlatform { return [ - SetupPlatform.CloudflarePages, - SetupPlatform.CloudflareWorkers, + SetupPlatform.Cloudflare, SetupPlatform.Node, ].includes(platform); } diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index de647efa1f1..c5c66a766c1 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -1,8 +1,10 @@ import express from "express"; import supertest from "supertest"; import { createRequest } from "node-mocks-http"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; -import { Response as NodeResponse } from "@remix-run/node"; +import { + createRequestHandler as createRemixRequestHandler, + Response as NodeResponse, +} from "@remix-run/node"; import { Readable } from "stream"; import { @@ -13,7 +15,13 @@ import { // 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 express adapter -jest.mock("@remix-run/server-runtime"); +jest.mock("@remix-run/node", () => { + let original = jest.requireActual("@remix-run/node"); + return { + ...original, + createRequestHandler: jest.fn() + }; +}); let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< typeof createRemixRequestHandler diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index c578e6b91b2..4884fad82ca 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -12,8 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.3.2", - "@remix-run/server-runtime": "1.3.2" + "@remix-run/node": "1.3.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index f0ddfc0dd4b..4ca0cfe69df 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -3,16 +3,13 @@ import type * as express from "express"; import type { AppLoadContext, ServerBuild, - ServerPlatform, -} from "@remix-run/server-runtime"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; -import type { RequestInit as NodeRequestInit, Response as NodeResponse, } from "@remix-run/node"; import { // This has been added as a global in node 15+ AbortController, + createRequestHandler as createRemixRequestHandler, Headers as NodeHeaders, Request as NodeRequest, } from "@remix-run/node"; @@ -43,8 +40,7 @@ export function createRequestHandler({ getLoadContext?: GetLoadContextFunction; mode?: string; }) { - let platform: ServerPlatform = {}; - let handleRequest = createRemixRequestHandler(build, platform, mode); + let handleRequest = createRemixRequestHandler(build, mode); return async ( req: express.Request, diff --git a/packages/remix-netlify/__tests__/server-test.ts b/packages/remix-netlify/__tests__/server-test.ts index 471e15e8f8f..f07e05e60a6 100644 --- a/packages/remix-netlify/__tests__/server-test.ts +++ b/packages/remix-netlify/__tests__/server-test.ts @@ -1,10 +1,10 @@ import fsp from "fs/promises"; import path from "path"; import lambdaTester from "lambda-tester"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; import { // This has been added as a global in node 15+ AbortController, + createRequestHandler as createRemixRequestHandler, Response as NodeResponse, } from "@remix-run/node"; import type { HandlerEvent } from "@netlify/functions"; @@ -18,7 +18,13 @@ import { // 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 netlify adapter -jest.mock("@remix-run/server-runtime"); +jest.mock("@remix-run/node", () => { + let original = jest.requireActual("@remix-run/node"); + return { + ...original, + createRequestHandler: jest.fn() + }; +}); let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< typeof createRemixRequestHandler diff --git a/packages/remix-netlify/package.json b/packages/remix-netlify/package.json index 5c094c70a61..32819e9ae24 100644 --- a/packages/remix-netlify/package.json +++ b/packages/remix-netlify/package.json @@ -12,8 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.3.2", - "@remix-run/server-runtime": "1.3.2" + "@remix-run/node": "1.3.2" }, "peerDependencies": { "@netlify/functions": "^0.10.0" diff --git a/packages/remix-netlify/server.ts b/packages/remix-netlify/server.ts index a260ed2a89c..beb3fb445a9 100644 --- a/packages/remix-netlify/server.ts +++ b/packages/remix-netlify/server.ts @@ -1,15 +1,10 @@ import { // This has been added as a global in node 15+ AbortController, + createRequestHandler as createRemixRequestHandler, Headers as NodeHeaders, Request as NodeRequest, } from "@remix-run/node"; -import type { - AppLoadContext, - ServerBuild, - ServerPlatform, -} from "@remix-run/server-runtime"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; import type { Handler, HandlerEvent, @@ -17,6 +12,8 @@ import type { HandlerResponse, } from "@netlify/functions"; import type { + AppLoadContext, + ServerBuild, Response as NodeResponse, RequestInit as NodeRequestInit, } from "@remix-run/node"; @@ -45,8 +42,7 @@ export function createRequestHandler({ getLoadContext?: AppLoadContext; mode?: string; }): Handler { - let platform: ServerPlatform = {}; - let handleRequest = createRemixRequestHandler(build, platform, mode); + let handleRequest = createRemixRequestHandler(build, mode); return async (event, context) => { let abortController = new AbortController(); diff --git a/packages/remix-node/cookieSigning.ts b/packages/remix-node/cookieSigning.ts deleted file mode 100644 index 10c56a71aad..00000000000 --- a/packages/remix-node/cookieSigning.ts +++ /dev/null @@ -1,16 +0,0 @@ -import cookieSignature from "cookie-signature"; -import type { - InternalSignFunctionDoNotUseMe, - InternalUnsignFunctionDoNotUseMe, -} from "@remix-run/server-runtime/cookieSigning"; - -export const sign: InternalSignFunctionDoNotUseMe = async (value, secret) => { - return cookieSignature.sign(value, secret); -}; - -export const unsign: InternalUnsignFunctionDoNotUseMe = async ( - signed: string, - secret: string -) => { - return cookieSignature.unsign(signed, secret); -}; diff --git a/packages/remix-node/crypto.ts b/packages/remix-node/crypto.ts new file mode 100644 index 00000000000..557b370b849 --- /dev/null +++ b/packages/remix-node/crypto.ts @@ -0,0 +1,16 @@ +import cookieSignature from "cookie-signature"; +import type { + SignFunction, + UnsignFunction, +} from "@remix-run/server-runtime"; + +export const sign: SignFunction = async (value, secret) => { + return cookieSignature.sign(value, secret); +}; + +export const unsign: UnsignFunction = async ( + signed: string, + secret: string +) => { + return cookieSignature.unsign(signed, secret); +}; \ No newline at end of file diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index 4a3a94090a1..dbb00e88be1 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -1,11 +1,6 @@ -import type { - InternalSignFunctionDoNotUseMe, - InternalUnsignFunctionDoNotUseMe, -} from "@remix-run/server-runtime/cookieSigning"; import { Blob as NodeBlob, File as NodeFile } from "@web-std/file"; import { atob, btoa } from "./base64"; -import { sign as remixSign, unsign as remixUnsign } from "./cookieSigning"; import { Headers as NodeHeaders, Request as NodeRequest, @@ -32,11 +27,6 @@ declare global { Response: typeof Response; fetch: typeof fetch; FormData: typeof FormData; - - // TODO: Once node v16 is available on AWS we should remove these globals - // and provide the webcrypto API instead. - sign: InternalSignFunctionDoNotUseMe; - unsign: InternalUnsignFunctionDoNotUseMe; } } } @@ -53,7 +43,4 @@ export function installGlobals() { global.Response = NodeResponse as unknown as typeof Response; global.fetch = nodeFetch as unknown as typeof fetch; global.FormData = NodeFormData as unknown as typeof FormData; - - global.sign = remixSign; - global.unsign = remixUnsign; } diff --git a/packages/remix-node/implementations.ts b/packages/remix-node/implementations.ts new file mode 100644 index 00000000000..a5b951f83ad --- /dev/null +++ b/packages/remix-node/implementations.ts @@ -0,0 +1,13 @@ +import { + createCookieFactory, + createCookieSessionStorageFactory, + createMemorySessionStorageFactory, + createSessionStorageFactory, +} from "@remix-run/server-runtime"; + +import { sign, unsign } from "./crypto"; + +export const createCookie = createCookieFactory({ sign, unsign }); +export const createCookieSessionStorage = createCookieSessionStorageFactory(createCookie); +export const createSessionStorage = createSessionStorageFactory(createCookie); +export const createMemorySessionStorage = createMemorySessionStorageFactory(createSessionStorage); diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 34eef8735c8..6a88d3dd85b 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -26,3 +26,54 @@ export { NodeOnDiskFile, } from "./upload/fileUploadHandler"; export { createMemoryUploadHandler as unstable_createMemoryUploadHandler } from "./upload/memoryUploadHandler"; + +export { + createCookie, + createCookieSessionStorage, + createMemorySessionStorage, + createSessionStorage, +} from './implementations'; + +export { + createRequestHandler, + createSession, + isCookie, + isSession, + json, + redirect, +} from "@remix-run/server-runtime"; + +export type { + ActionFunction, + AppData, + AppLoadContext, + CreateRequestHandlerFunction, + Cookie, + CookieOptions, + CookieParseOptions, + CookieSerializeOptions, + CookieSignatureOptions, + DataFunctionArgs, + EntryContext, + ErrorBoundaryComponent, + HandleDataRequestFunction, + HandleDocumentRequestFunction, + HeadersFunction, + HtmlLinkDescriptor, + HtmlMetaDescriptor, + LinkDescriptor, + LinksFunction, + LoaderFunction, + MetaDescriptor, + MetaFunction, + PageLinkDescriptor, + RequestHandler, + RouteComponent, + RouteHandle, + ServerBuild, + ServerEntryModule, + Session, + SessionData, + SessionIdStorageStrategy, + SessionStorage, +} from "@remix-run/server-runtime"; diff --git a/packages/remix-node/magicExports/remix.ts b/packages/remix-node/magicExports/remix.ts index 01a70161bbd..59500ec0325 100644 --- a/packages/remix-node/magicExports/remix.ts +++ b/packages/remix-node/magicExports/remix.ts @@ -1,6 +1,10 @@ // Re-export everything from this package that is available in `remix`. export { + createCookie, + createSessionStorage, + createCookieSessionStorage, + createMemorySessionStorage, createFileSessionStorage, unstable_createFileUploadHandler, unstable_createMemoryUploadHandler, diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index ca0f94a3694..da85cbd3745 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -5,7 +5,8 @@ import type { SessionStorage, SessionIdStorageStrategy, } from "@remix-run/server-runtime"; -import { createSessionStorage } from "@remix-run/server-runtime"; + +import { createSessionStorage } from "../implementations"; interface FileSessionStorageOptions { /** diff --git a/packages/remix-server-runtime/README.md b/packages/remix-server-runtime/README.md index 40685a7476f..157c8a8346b 100644 --- a/packages/remix-server-runtime/README.md +++ b/packages/remix-server-runtime/README.md @@ -1,13 +1,26 @@ -# Welcome to Remix! +# @remix-run/server-runtime -[Remix](https://remix.run) is a web framework that helps you build better websites with React. +[Remix](https://remix.run) supports multiple server runtimes: -To get started, open a new shell and run: +- [Node](https://nodejs.org/en/) +- [Cloudflare](https://developers.cloudflare.com/workers/learning/how-workers-works/) +- [Deno](https://deno.land/) (Experimental 🧪) -```sh -npx create-remix@latest -``` +Support for each runtime is provided by a corresponding Remix package: -Then follow the prompts you see in your terminal. +- [`@remix-run/node`](https://github.com/remix-run/remix/tree/main/packages/remix-node) +- [`@remix-run/cloudflare`](https://github.com/remix-run/remix/tree/main/packages/remix-cloudflare) +- [`remix-deno`](https://github.com/remix-run/remix/tree/main/templates/deno-ts/remix-deno) (will be renamed to `@remix-run/deno` when Deno support is stable) -For more information about Remix, [visit remix.run](https://remix.run)! +This package defines a "Remix server runtime interface" that each runtime package must conform to. + +Each Remix server runtime package MUST: + +- Implement and export values for each type in [`interface.ts`](./interface.ts) +- Re-export types in [`reexport.ts`](./reexport.ts) + +Each Remix server runtime package MAY: + +- Re-export the [default implementations](./index.ts) as its implementations +- Export custom implementations adhering to the [interface types](./interface.ts) +- Provide additional exports relevant for that runtime diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts index 2b6552b21f8..bf59389d538 100644 --- a/packages/remix-server-runtime/__tests__/cookies-test.ts +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -1,4 +1,19 @@ -import { createCookie, isCookie } from "../cookies"; +import { createCookieFactory, isCookie } from "../cookies"; +import type { SignFunction, UnsignFunction } from "../crypto"; + +const sign: SignFunction = async (value, secret) => { + return JSON.stringify({ value, secret }); +}; +const unsign: UnsignFunction = async (signed, secret) => { + try { + let unsigned = JSON.parse(signed); + if (unsigned.secret !== secret) return false; + return unsigned.value; + } catch (e: unknown) { + return false; + } +}; +const createCookie = createCookieFactory({ sign, unsign }); function getCookieFromSetCookie(setCookie: string): string { return setCookie.split(/;\s*/)[0]; diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts index 021f2cf2fe4..93a3087e6d6 100644 --- a/packages/remix-server-runtime/__tests__/server-test.ts +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -68,7 +68,7 @@ describe("server", () => { ]; for (let [method, to] of allowThrough) { it(`allows through ${method} request to ${to}`, async () => { - let handler = createRequestHandler(build, {}); + let handler = createRequestHandler(build); let response = await handler( new Request(`http://localhost:3000${to}`, { method, @@ -80,7 +80,7 @@ describe("server", () => { } it("strips body for HEAD requests", async () => { - let handler = createRequestHandler(build, {}); + let handler = createRequestHandler(build); let response = await handler( new Request("http://localhost:3000/", { method: "HEAD", @@ -119,7 +119,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource`, { method: "get" }); @@ -154,7 +154,7 @@ describe("shared server runtime", () => { path: "resource/sub", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource/sub`, { method: "get" }); @@ -183,7 +183,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource`, { method: "get" }); @@ -205,7 +205,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource`, { method: "get" }); @@ -224,7 +224,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Development); + let handler = createRequestHandler(build, ServerMode.Development); let request = new Request(`${baseUrl}/resource`, { method: "get" }); @@ -250,7 +250,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource`, { method: "post" }); @@ -285,7 +285,7 @@ describe("shared server runtime", () => { path: "resource/sub", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource/sub`, { method: "post" }); @@ -314,7 +314,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource`, { method: "post" }); @@ -336,7 +336,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/resource`, { method: "post" }); @@ -355,7 +355,7 @@ describe("shared server runtime", () => { path: "resource", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Development); + let handler = createRequestHandler(build, ServerMode.Development); let request = new Request(`${baseUrl}/resource`, { method: "post" }); @@ -376,7 +376,7 @@ describe("shared server runtime", () => { index: true, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?_data=routes/index`, { method: "get", @@ -406,7 +406,7 @@ describe("shared server runtime", () => { index: true, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?_data=routes/index`, { method: "get", @@ -437,7 +437,7 @@ describe("shared server runtime", () => { path: "test", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=root`, { method: "get", @@ -471,7 +471,7 @@ describe("shared server runtime", () => { path: "test", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Development); + let handler = createRequestHandler(build, ServerMode.Development); let request = new Request(`${baseUrl}/test?_data=root`, { method: "get", @@ -504,7 +504,7 @@ describe("shared server runtime", () => { path: "test", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=root`, { method: "get", @@ -536,7 +536,7 @@ describe("shared server runtime", () => { path: "test", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { method: "post", @@ -567,7 +567,7 @@ describe("shared server runtime", () => { path: "test", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { method: "post", @@ -601,7 +601,7 @@ describe("shared server runtime", () => { path: "test", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Development); + let handler = createRequestHandler(build, ServerMode.Development); let request = new Request(`${baseUrl}/test?_data=routes/test`, { method: "post", @@ -634,7 +634,7 @@ describe("shared server runtime", () => { path: "test", }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { method: "post", @@ -666,7 +666,7 @@ describe("shared server runtime", () => { index: true, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?_data=root`, { method: "post" }); @@ -695,7 +695,7 @@ describe("shared server runtime", () => { index: true, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?index&_data=routes/index`, { method: "post", @@ -720,7 +720,7 @@ describe("shared server runtime", () => { loader: rootLoader, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -748,7 +748,7 @@ describe("shared server runtime", () => { CatchBoundary: {}, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -786,7 +786,7 @@ describe("shared server runtime", () => { loader: indexLoader, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -828,7 +828,7 @@ describe("shared server runtime", () => { CatchBoundary: {}, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -873,7 +873,7 @@ describe("shared server runtime", () => { action: testAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test`, { method: "post" }); @@ -919,7 +919,7 @@ describe("shared server runtime", () => { action: indexAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?index`, { method: "post" }); @@ -966,7 +966,7 @@ describe("shared server runtime", () => { CatchBoundary: {}, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test`, { method: "post" }); @@ -1013,7 +1013,7 @@ describe("shared server runtime", () => { CatchBoundary: {}, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?index`, { method: "post" }); @@ -1068,7 +1068,7 @@ describe("shared server runtime", () => { action: testAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test`, { method: "post" }); @@ -1125,7 +1125,7 @@ describe("shared server runtime", () => { action: indexAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?index`, { method: "post" }); @@ -1169,7 +1169,7 @@ describe("shared server runtime", () => { loader: indexLoader, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -1211,7 +1211,7 @@ describe("shared server runtime", () => { ErrorBoundary: {}, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -1256,7 +1256,7 @@ describe("shared server runtime", () => { action: testAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test`, { method: "post" }); @@ -1302,7 +1302,7 @@ describe("shared server runtime", () => { action: indexAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?index`, { method: "post" }); @@ -1349,7 +1349,7 @@ describe("shared server runtime", () => { ErrorBoundary: {}, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test`, { method: "post" }); @@ -1396,7 +1396,7 @@ describe("shared server runtime", () => { ErrorBoundary: {}, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?index`, { method: "post" }); @@ -1451,7 +1451,7 @@ describe("shared server runtime", () => { action: testAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/test`, { method: "post" }); @@ -1508,7 +1508,7 @@ describe("shared server runtime", () => { action: indexAction, }, }); - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/?index`, { method: "post" }); @@ -1560,7 +1560,7 @@ describe("shared server runtime", () => { calledBefore = true; return ogHandleDocumentRequest.call(null, arguments); }) as any; - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -1602,7 +1602,7 @@ describe("shared server runtime", () => { lastThrownError = new Error("rofl"); throw lastThrownError; }) as any; - let handler = createRequestHandler(build, {}, ServerMode.Test); + let handler = createRequestHandler(build, ServerMode.Test); let request = new Request(`${baseUrl}/`, { method: "get" }); @@ -1642,7 +1642,7 @@ describe("shared server runtime", () => { lastThrownError = new Error(errorMessage); throw lastThrownError; }) as any; - let handler = createRequestHandler(build, {}, ServerMode.Development); + let handler = createRequestHandler(build, ServerMode.Development); let request = new Request(`${baseUrl}/`, { method: "get" }); diff --git a/packages/remix-server-runtime/__tests__/sessions-test.ts b/packages/remix-server-runtime/__tests__/sessions-test.ts index 7fe66e8e165..bc9d30d294d 100644 --- a/packages/remix-server-runtime/__tests__/sessions-test.ts +++ b/packages/remix-server-runtime/__tests__/sessions-test.ts @@ -1,11 +1,30 @@ -import { createSession, isSession } from "../sessions"; -import { createCookieSessionStorage } from "../sessions/cookieStorage"; -import { createMemorySessionStorage } from "../sessions/memoryStorage"; +import { createCookieFactory } from "../cookies"; +import type { SignFunction, UnsignFunction } from "../crypto"; +import { createSession, createSessionStorageFactory, isSession } from "../sessions"; +import { createCookieSessionStorageFactory } from "../sessions/cookieStorage"; +import { createMemorySessionStorageFactory } from "../sessions/memoryStorage"; function getCookieFromSetCookie(setCookie: string): string { return setCookie.split(/;\s*/)[0]; } +const sign: SignFunction = async (value, secret) => { + return JSON.stringify({ value, secret }); +}; +const unsign: UnsignFunction = async (signed, secret) => { + try { + let unsigned = JSON.parse(signed); + if (unsigned.secret !== secret) return false; + return unsigned.value; + } catch (e: unknown) { + return false; + } +}; +const createCookie = createCookieFactory({ sign, unsign }); +const createCookieSessionStorage = createCookieSessionStorageFactory(createCookie); +const createSessionStorage = createSessionStorageFactory(createCookie); +const createMemorySessionStorage = createMemorySessionStorageFactory(createSessionStorage); + describe("Session", () => { it("has an empty id by default", () => { expect(createSession().id).toEqual(""); diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index 4c86f65182f..35d9cc5d2f7 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -1,10 +1,7 @@ import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; import { parse, serialize } from "cookie"; -// TODO: Once node v16 is available on AWS we should use these instead of the -// global `sign` and `unsign` functions. -//import { sign, unsign } from "./cookieSigning"; -import "./cookieSigning"; +import type { SignFunction, UnsignFunction } from "./crypto"; export type { CookieParseOptions, CookieSerializeOptions }; @@ -69,15 +66,26 @@ export interface Cookie { serialize(value: any, options?: CookieSerializeOptions): Promise; } +export type CreateCookieFunction = ( + name: string, + cookieOptions?: CookieOptions +) => Cookie; + /** * Creates a logical container for managing a browser cookie from the server. * * @see https://remix.run/api/remix#createcookie */ -export function createCookie( - name: string, - cookieOptions: CookieOptions = {} -): Cookie { +export const createCookieFactory = ({ + sign, + unsign, +}: { + sign: SignFunction + unsign: UnsignFunction +}): CreateCookieFunction => ( + name, + cookieOptions = {} +) => { let { secrets, ...options } = { secrets: [], path: "/", @@ -103,13 +111,13 @@ export function createCookie( return name in cookies ? cookies[name] === "" ? "" - : await decodeCookieValue(cookies[name], secrets) + : await decodeCookieValue(unsign, cookies[name], secrets) : null; }, async serialize(value, serializeOptions) { return serialize( name, - value === "" ? "" : await encodeCookieValue(value, secrets), + value === "" ? "" : await encodeCookieValue(sign, value, secrets), { ...options, ...serializeOptions, @@ -117,14 +125,16 @@ export function createCookie( ); }, }; -} +}; + +export type IsCookieFunction = (object: any) => object is Cookie; /** * Returns true if an object is a Remix cookie container. * * @see https://remix.run/api/remix#iscookie */ -export function isCookie(object: any): object is Cookie { +export const isCookie: IsCookieFunction = (object): object is Cookie => { return ( object != null && typeof object.name === "string" && @@ -132,9 +142,10 @@ export function isCookie(object: any): object is Cookie { typeof object.parse === "function" && typeof object.serialize === "function" ); -} +}; async function encodeCookieValue( + sign: SignFunction, value: any, secrets: string[] ): Promise { @@ -148,6 +159,7 @@ async function encodeCookieValue( } async function decodeCookieValue( + unsign: UnsignFunction, value: string, secrets: string[] ): Promise { diff --git a/packages/remix-server-runtime/cookieSigning.ts b/packages/remix-server-runtime/crypto.ts similarity index 85% rename from packages/remix-server-runtime/cookieSigning.ts rename to packages/remix-server-runtime/crypto.ts index 0280ec3bf9d..28382ee55c4 100644 --- a/packages/remix-server-runtime/cookieSigning.ts +++ b/packages/remix-server-runtime/crypto.ts @@ -1,20 +1,13 @@ -export type InternalSignFunctionDoNotUseMe = ( +export type SignFunction = ( value: string, secret: string ) => Promise; -export type InternalUnsignFunctionDoNotUseMe = ( +export type UnsignFunction = ( cookie: string, secret: string ) => Promise; -/* eslint-disable prefer-let/prefer-let */ -declare global { - var sign: InternalSignFunctionDoNotUseMe; - var unsign: InternalUnsignFunctionDoNotUseMe; -} -/* eslint-enable prefer-let/prefer-let */ - // TODO: Once node v16 is available on AWS we should use the globally provided // webcrypto "crypto" variable and re-enable this code-path in "./cookies.ts" // instead of referencing the sign and unsign globals. diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index 4ff6a3152b9..bc8457a31d8 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -1,56 +1,58 @@ -export type { - ServerBuild, - ServerEntryModule, - HandleDataRequestFunction, - HandleDocumentRequestFunction, -} from "./build"; +// Default implementations for the Remix server runtime interface +export { createCookieFactory, isCookie } from "./cookies"; +export { json, redirect } from "./responses"; +export { createRequestHandler } from "./server"; +export { createSession, isSession, createSessionStorageFactory } from "./sessions"; +export { createCookieSessionStorageFactory } from "./sessions/cookieStorage"; +export { createMemorySessionStorageFactory } from "./sessions/memoryStorage"; +// Types for the Remix server runtime interface +export type { + CreateCookieFunction, + CreateCookieSessionStorageFunction, + CreateMemorySessionStorageFunction, + CreateRequestHandlerFunction, + CreateSessionFunction, + CreateSessionStorageFunction, + IsCookieFunction, + IsSessionFunction, + JsonFunction, + RedirectFunction, +} from "./interface"; + +// Remix server runtime packages should re-export these types export type { + ActionFunction, + AppData, + AppLoadContext, + Cookie, + CookieOptions, CookieParseOptions, CookieSerializeOptions, CookieSignatureOptions, - CookieOptions, - Cookie, -} from "./cookies"; -export { createCookie, isCookie } from "./cookies"; - -export type { AppLoadContext, AppData } from "./data"; - -export type { EntryContext } from "./entry"; - -export type { - LinkDescriptor, - HtmlLinkDescriptor, - PageLinkDescriptor, -} from "./links"; - -export type { ServerPlatform } from "./platform"; - -export type { - ActionFunction, DataFunctionArgs, + EntryContext, ErrorBoundaryComponent, + HandleDataRequestFunction, + HandleDocumentRequestFunction, HeadersFunction, + HtmlLinkDescriptor, HtmlMetaDescriptor, + LinkDescriptor, LinksFunction, LoaderFunction, MetaDescriptor, MetaFunction, + PageLinkDescriptor, + RequestHandler, RouteComponent, RouteHandle, -} from "./routeModules"; - -export { json, redirect } from "./responses"; - -export type { RequestHandler } from "./server"; -export { createRequestHandler } from "./server"; - -export type { - SessionData, + ServerBuild, + ServerEntryModule, Session, - SessionStorage, + SessionData, SessionIdStorageStrategy, -} from "./sessions"; -export { createSession, isSession, createSessionStorage } from "./sessions"; -export { createCookieSessionStorage } from "./sessions/cookieStorage"; -export { createMemorySessionStorage } from "./sessions/memoryStorage"; + SessionStorage, + SignFunction, + UnsignFunction, +} from "./reexport"; diff --git a/packages/remix-server-runtime/interface.ts b/packages/remix-server-runtime/interface.ts new file mode 100644 index 00000000000..be54c937c35 --- /dev/null +++ b/packages/remix-server-runtime/interface.ts @@ -0,0 +1,10 @@ +export type { CreateCookieFunction, IsCookieFunction } from "./cookies"; +export type { JsonFunction, RedirectFunction } from "./responses"; +export type { CreateRequestHandlerFunction } from "./server"; +export type { + CreateSessionFunction, + CreateSessionStorageFunction, + IsSessionFunction, +} from "./sessions"; +export type { CreateCookieSessionStorageFunction } from "./sessions/cookieStorage"; +export type { CreateMemorySessionStorageFunction } from "./sessions/memoryStorage"; diff --git a/packages/remix-server-runtime/magicExports/remix.ts b/packages/remix-server-runtime/magicExports/remix.ts index a7f7ac13466..8c6fc24cf2c 100644 --- a/packages/remix-server-runtime/magicExports/remix.ts +++ b/packages/remix-server-runtime/magicExports/remix.ts @@ -34,13 +34,9 @@ export type { } from "@remix-run/server-runtime"; export { - createCookie, isCookie, createSession, isSession, - createSessionStorage, - createCookieSessionStorage, - createMemorySessionStorage, json, redirect, } from "@remix-run/server-runtime"; diff --git a/packages/remix-server-runtime/platform.ts b/packages/remix-server-runtime/platform.ts deleted file mode 100644 index 1008823e8c7..00000000000 --- a/packages/remix-server-runtime/platform.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This also probably warrants some explanation. - * - * The whole point here is to abstract out the server functionality that is required - * by the server runtime but is dependent on the platform runtime. - * - * The origional use of this was error beautification as it depends on loading sourcemaps - * from the file system in node, while functions hosted on cloudflare workers will not - * need to format as they have built in sourcemap support. This is no longer needed though - * as we utlize the `source-map-support` library to do this for us. - */ - -/** - * Abstracts functionality that is platform specific (node vs workers, etc.) - */ -export interface ServerPlatform {} diff --git a/packages/remix-server-runtime/reexport.ts b/packages/remix-server-runtime/reexport.ts new file mode 100644 index 00000000000..3747a38bf48 --- /dev/null +++ b/packages/remix-server-runtime/reexport.ts @@ -0,0 +1,49 @@ +export type { + HandleDataRequestFunction, + HandleDocumentRequestFunction, + ServerBuild, + ServerEntryModule, +} from "./build"; + +export type { + Cookie, + CookieOptions, + CookieParseOptions, + CookieSerializeOptions, + CookieSignatureOptions, +} from "./cookies"; + +export type { SignFunction, UnsignFunction } from "./crypto"; + +export type { AppLoadContext, AppData } from "./data"; + +export type { EntryContext } from "./entry"; + +export type { + HtmlLinkDescriptor, + LinkDescriptor, + PageLinkDescriptor, +} from "./links"; + +export type { + ActionFunction, + DataFunctionArgs, + ErrorBoundaryComponent, + HeadersFunction, + HtmlMetaDescriptor, + LinksFunction, + LoaderFunction, + MetaDescriptor, + MetaFunction, + RouteComponent, + RouteHandle, +} from "./routeModules"; + +export type { RequestHandler } from "./server"; + +export type { + Session, + SessionData, + SessionIdStorageStrategy, + SessionStorage, +} from "./sessions"; diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index 5a48e9d87b6..0e84a0eef7f 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -1,13 +1,15 @@ +export type JsonFunction = ( + data: Data, + init?: number | ResponseInit +) => Response; + /** * This is a shortcut for creating `application/json` responses. Converts `data` * to JSON and sets the `Content-Type` header. * * @see https://remix.run/api/remix#json */ -export function json( - data: Data, - init: number | ResponseInit = {} -): Response { +export const json: JsonFunction = (data, init = {}) => { let responseInit: any = init; if (typeof init === "number") { responseInit = { status: init }; @@ -22,7 +24,12 @@ export function json( ...responseInit, headers, }); -} +}; + +export type RedirectFunction = ( + url: string, + init?: number | ResponseInit +) => Response; /** * A redirect response. Sets the status code and the `Location` header. @@ -30,10 +37,7 @@ export function json( * * @see https://remix.run/api/remix#redirect */ -export function redirect( - url: string, - init: number | ResponseInit = 302 -): Response { +export const redirect: RedirectFunction = (url, init = 302) => { let responseInit = init; if (typeof responseInit === "number") { responseInit = { status: responseInit }; @@ -48,7 +52,7 @@ export function redirect( ...responseInit, headers, }); -} +}; export function isResponse(value: any): value is Response { return ( diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index c3f86b29f71..e6788772b0a 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -6,7 +6,6 @@ import type { EntryContext } from "./entry"; import { createEntryMatches, createEntryRouteModules } from "./entry"; import { serializeError } from "./errors"; import { getDocumentHeaders } from "./headers"; -import type { ServerPlatform } from "./platform"; import type { RouteMatch } from "./routeMatching"; import { matchServerRoutes } from "./routeMatching"; import { ServerMode, isServerMode } from "./mode"; @@ -24,14 +23,18 @@ export interface RequestHandler { (request: Request, loadContext?: AppLoadContext): Promise; } +export type CreateRequestHandlerFunction = ( + build: ServerBuild, + mode?: string +) => RequestHandler; + /** * Creates a function that serves HTTP requests. */ -export function createRequestHandler( - build: ServerBuild, - platform: ServerPlatform, - mode?: string -): RequestHandler { +export const createRequestHandler: CreateRequestHandlerFunction = ( + build, + mode +) => { let routes = createRoutes(build.routes); let serverMode = isServerMode(mode) ? mode : ServerMode.Production; @@ -81,7 +84,7 @@ export function createRequestHandler( return response; }; -} +}; async function handleDataRequest({ handleDataRequest, loadContext, diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index bd8b1e2dbb8..8709c997058 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -1,7 +1,7 @@ import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; -import type { Cookie, CookieOptions } from "./cookies"; -import { createCookie, isCookie } from "./cookies"; +import type { Cookie, CookieOptions, CreateCookieFunction } from "./cookies"; +import { isCookie } from "./cookies"; import { warnOnce } from "./warnings"; /** @@ -65,6 +65,11 @@ function flash(name: string): string { return `__flash_${name}__`; } +export type CreateSessionFunction = ( + initialData?: SessionData, + id?: string +) => Session; + /** * Creates a new Session object. * @@ -73,7 +78,10 @@ function flash(name: string): string { * * @see https://remix.run/api/remix#createsession */ -export function createSession(initialData: SessionData = {}, id = ""): Session { +export const createSession: CreateSessionFunction = ( + initialData = {}, + id = "" +) => { let map = new Map(Object.entries(initialData)); return { @@ -108,14 +116,16 @@ export function createSession(initialData: SessionData = {}, id = ""): Session { map.delete(name); }, }; -} +}; + +export type IsSessionFunction = (object: any) => object is Session; /** * Returns true if an object is a Remix session. * * @see https://remix.run/api/remix#issession */ -export function isSession(object: any): object is Session { +export const isSession: IsSessionFunction = (object): object is Session => { return ( object != null && typeof object.id === "string" && @@ -126,7 +136,7 @@ export function isSession(object: any): object is Session { typeof object.flash === "function" && typeof object.unset === "function" ); -} +}; /** * SessionStorage stores session data between HTTP requests and knows how to @@ -202,6 +212,10 @@ export interface SessionIdStorageStrategy { deleteData: (id: string) => Promise; } +export type CreateSessionStorageFunction = ( + strategy: SessionIdStorageStrategy +) => SessionStorage; + /** * Creates a SessionStorage object using a SessionIdStorageStrategy. * @@ -210,13 +224,15 @@ export interface SessionIdStorageStrategy { * * @see https://remix.run/api/remix#createsessionstorage */ -export function createSessionStorage({ +export const createSessionStorageFactory = ( + createCookie: CreateCookieFunction +): CreateSessionStorageFunction => ({ cookie: cookieArg, createData, readData, updateData, deleteData, -}: SessionIdStorageStrategy): SessionStorage { +}) => { let cookie = isCookie(cookieArg) ? cookieArg : createCookie(cookieArg?.name || "__session", cookieArg); @@ -248,7 +264,7 @@ export function createSessionStorage({ }); }, }; -} +}; export function warnOnceAboutSigningSessionCookie(cookie: Cookie) { warnOnce( diff --git a/packages/remix-server-runtime/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts index 8e6ee92d9ac..763b0d7c6a3 100644 --- a/packages/remix-server-runtime/sessions/cookieStorage.ts +++ b/packages/remix-server-runtime/sessions/cookieStorage.ts @@ -1,4 +1,5 @@ -import { createCookie, isCookie } from "../cookies"; +import type { CreateCookieFunction} from "../cookies"; +import { isCookie } from "../cookies"; import type { SessionStorage, SessionIdStorageStrategy } from "../sessions"; import { warnOnceAboutSigningSessionCookie, createSession } from "../sessions"; @@ -10,6 +11,10 @@ interface CookieSessionStorageOptions { cookie?: SessionIdStorageStrategy["cookie"]; } +export type CreateCookieSessionStorageFunction = ( + options?: CookieSessionStorageOptions +) => SessionStorage; + /** * Creates and returns a SessionStorage object that stores all session data * directly in the session cookie itself. @@ -21,9 +26,11 @@ interface CookieSessionStorageOptions { * * @see https://remix.run/api/remix#createcookiesessionstorage */ -export function createCookieSessionStorage({ +export const createCookieSessionStorageFactory = ( + createCookie: CreateCookieFunction +): CreateCookieSessionStorageFunction => ({ cookie: cookieArg, -}: CookieSessionStorageOptions = {}): SessionStorage { +} = {}) => { let cookie = isCookie(cookieArg) ? cookieArg : createCookie(cookieArg?.name || "__session", cookieArg); @@ -46,4 +53,4 @@ export function createCookieSessionStorage({ }); }, }; -} +}; diff --git a/packages/remix-server-runtime/sessions/memoryStorage.ts b/packages/remix-server-runtime/sessions/memoryStorage.ts index edc7f3668c0..6f5c93cb471 100644 --- a/packages/remix-server-runtime/sessions/memoryStorage.ts +++ b/packages/remix-server-runtime/sessions/memoryStorage.ts @@ -2,8 +2,8 @@ import type { SessionData, SessionStorage, SessionIdStorageStrategy, + CreateSessionStorageFunction, } from "../sessions"; -import { createSessionStorage } from "../sessions"; interface MemorySessionStorageOptions { /** @@ -13,6 +13,10 @@ interface MemorySessionStorageOptions { cookie?: SessionIdStorageStrategy["cookie"]; } +export type CreateMemorySessionStorageFunction = ( + options?: MemorySessionStorageOptions +) => SessionStorage; + /** * Creates and returns a simple in-memory SessionStorage object, mostly useful * for testing and as a reference implementation. @@ -22,9 +26,11 @@ interface MemorySessionStorageOptions { * * @see https://remix.run/api/remix#creatememorysessionstorage */ -export function createMemorySessionStorage({ +export const createMemorySessionStorageFactory = ( + createSessionStorage: CreateSessionStorageFunction +): CreateMemorySessionStorageFunction => ({ cookie, -}: MemorySessionStorageOptions = {}): SessionStorage { +} = {}) => { let uniqueId = 0; let map = new Map(); @@ -56,4 +62,4 @@ export function createMemorySessionStorage({ map.delete(id); }, }); -} +}; diff --git a/packages/remix-vercel/__tests__/server-test.ts b/packages/remix-vercel/__tests__/server-test.ts index 42b94b91892..00562e87996 100644 --- a/packages/remix-vercel/__tests__/server-test.ts +++ b/packages/remix-vercel/__tests__/server-test.ts @@ -1,9 +1,11 @@ import supertest from "supertest"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; import { createRequest } from "node-mocks-http"; import { createServerWithHelpers } from "@vercel/node/dist/helpers"; import type { VercelRequest } from "@vercel/node"; -import { Response as NodeResponse } from "@remix-run/node"; +import { + createRequestHandler as createRemixRequestHandler, + Response as NodeResponse, +} from "@remix-run/node"; import { Readable } from "stream"; import { @@ -14,7 +16,13 @@ import { // 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"); +jest.mock("@remix-run/node", () => { + let original = jest.requireActual("@remix-run/node"); + return { + ...original, + createRequestHandler: jest.fn() + }; +}); let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< typeof createRemixRequestHandler diff --git a/packages/remix-vercel/package.json b/packages/remix-vercel/package.json index bbd2c8bd7b9..6add7a6697b 100644 --- a/packages/remix-vercel/package.json +++ b/packages/remix-vercel/package.json @@ -12,8 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.3.2", - "@remix-run/server-runtime": "1.3.2" + "@remix-run/node": "1.3.2" }, "peerDependencies": { "@vercel/node": "^1.8.3" @@ -21,7 +20,7 @@ "devDependencies": { "@types/supertest": "^2.0.10", "@vercel/node": "^1.8.3", - "supertest": "^6.1.5", - "node-mocks-http": "^1.10.1" + "node-mocks-http": "^1.10.1", + "supertest": "^6.1.5" } } diff --git a/packages/remix-vercel/server.ts b/packages/remix-vercel/server.ts index 568707b3c36..cd4224fc864 100644 --- a/packages/remix-vercel/server.ts +++ b/packages/remix-vercel/server.ts @@ -2,16 +2,13 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; import type { AppLoadContext, ServerBuild, - ServerPlatform, -} from "@remix-run/server-runtime"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; -import type { RequestInit as NodeRequestInit, Response as NodeResponse, } from "@remix-run/node"; import { // This has been added as a global in node 15+ AbortController, + createRequestHandler as createRemixRequestHandler, Headers as NodeHeaders, Request as NodeRequest, } from "@remix-run/node"; @@ -42,8 +39,7 @@ export function createRequestHandler({ getLoadContext?: GetLoadContextFunction; mode?: string; }) { - let platform: ServerPlatform = {}; - let handleRequest = createRemixRequestHandler(build, platform, mode); + let handleRequest = createRemixRequestHandler(build, mode); return async (req: VercelRequest, res: VercelResponse) => { let abortController = new AbortController(); diff --git a/rollup.config.js b/rollup.config.js index 2fd140fbc54..ce0b9f17cac 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -383,40 +383,23 @@ function remixNode() { } /** @returns {import("rollup").RollupOptions[]} */ -function remixCloudflareWorkers() { - let sourceDir = "packages/remix-cloudflare-workers"; - let outputDir = "build/node_modules/@remix-run/cloudflare-workers"; +function remixCloudflare() { + let sourceDir = "packages/remix-cloudflare"; + let outputDir = "build/node_modules/@remix-run/cloudflare"; let version = getVersion(sourceDir); return [ - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/cloudflare-workers", version), - dir: `${outputDir}/magicExports`, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - ], - }, { external(id) { return isBareModuleId(id); }, input: `${sourceDir}/index.ts`, output: { - banner: createBanner("@remix-run/cloudflare-workers", version), - dir: `${outputDir}/esm`, - format: "esm", + banner: createBanner("@remix-run/cloudflare", version), + dir: outputDir, + format: "cjs", preserveModules: true, + exports: "named", }, plugins: [ babel({ @@ -425,6 +408,13 @@ function remixCloudflareWorkers() { extensions: [".ts", ".tsx"], }), nodeResolve({ extensions: [".ts", ".tsx"] }), + copy({ + targets: [ + { src: `LICENSE.md`, dest: outputDir }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + ], + }), ], }, { @@ -433,7 +423,7 @@ function remixCloudflareWorkers() { }, input: `${sourceDir}/magicExports/remix.ts`, output: { - banner: createBanner("@remix-run/cloudflare-workers", version), + banner: createBanner("@remix-run/cloudflare", version), dir: `${outputDir}/magicExports/esm`, format: "esm", }, @@ -445,23 +435,13 @@ function remixCloudflareWorkers() { }), ], }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixCloudflarePages() { - let sourceDir = "packages/remix-cloudflare-pages"; - let outputDir = "build/node_modules/@remix-run/cloudflare-pages"; - let version = getVersion(sourceDir); - - return [ { external() { return true; }, input: `${sourceDir}/magicExports/remix.ts`, output: { - banner: createBanner("@remix-run/cloudflare-pages", version), + banner: createBanner("@remix-run/cloudflare", version), dir: `${outputDir}/magicExports`, format: "cjs", }, @@ -473,13 +453,23 @@ function remixCloudflarePages() { }), ], }, + ]; +} + +/** @returns {import("rollup").RollupOptions[]} */ +function remixCloudflareWorkers() { + let sourceDir = "packages/remix-cloudflare-workers"; + let outputDir = "build/node_modules/@remix-run/cloudflare-workers"; + let version = getVersion(sourceDir); + + return [ { external(id) { return isBareModuleId(id); }, input: `${sourceDir}/index.ts`, output: { - banner: createBanner("@remix-run/cloudflare-pages", version), + banner: createBanner("@remix-run/cloudflare-workers", version), dir: `${outputDir}/esm`, format: "esm", preserveModules: true, @@ -493,15 +483,26 @@ function remixCloudflarePages() { nodeResolve({ extensions: [".ts", ".tsx"] }), ], }, + ]; +} + +/** @returns {import("rollup").RollupOptions[]} */ +function remixCloudflarePages() { + let sourceDir = "packages/remix-cloudflare-pages"; + let outputDir = "build/node_modules/@remix-run/cloudflare-pages"; + let version = getVersion(sourceDir); + + return [ { - external() { - return true; + external(id) { + return isBareModuleId(id); }, - input: `${sourceDir}/magicExports/remix.ts`, + input: `${sourceDir}/index.ts`, output: { banner: createBanner("@remix-run/cloudflare-pages", version), - dir: `${outputDir}/magicExports/esm`, + dir: `${outputDir}/esm`, format: "esm", + preserveModules: true, }, plugins: [ babel({ @@ -509,6 +510,7 @@ function remixCloudflarePages() { exclude: /node_modules/, extensions: [".ts", ".tsx"], }), + nodeResolve({ extensions: [".ts", ".tsx"] }), ], }, ]; @@ -780,6 +782,7 @@ export default function rollup(options) { ...remixDev(options), ...remixServerRuntime(options), ...remixNode(options), + ...remixCloudflare(options), ...remixCloudflarePages(options), ...remixCloudflareWorkers(options), ...remixServerAdapters(options), diff --git a/scripts/publish-private.js b/scripts/publish-private.js index 50e28bed391..1525401bd47 100644 --- a/scripts/publish-private.js +++ b/scripts/publish-private.js @@ -29,6 +29,7 @@ async function run() { for (let name of [ "dev", "server-runtime", // publish before platforms + "cloudflare", "cloudflare-pages", "cloudflare-workers", "node", // publish node before node servers diff --git a/scripts/publish.js b/scripts/publish.js index c9824496273..a50fb4b01d0 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -34,6 +34,7 @@ async function run() { for (let name of [ "dev", "server-runtime", // publish before platforms + "cloudflare", "cloudflare-pages", "cloudflare-workers", "node", // publish node before node servers