Skip to content

Commit

Permalink
Add initial @vercel/remix package (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate authored Mar 15, 2023
1 parent 1c6c670 commit 9a16847
Show file tree
Hide file tree
Showing 19 changed files with 577 additions and 8 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"packages/remix-serve",
"packages/remix-server-runtime",
"packages/remix-testing",
"packages/remix-vercel"
"packages/remix-vercel",
"packages/vercel-remix"
],
"scripts": {
"bug-report-test": "playwright test --config ./integration/playwright.config.ts integration/bug-report-test.ts",
Expand Down
13 changes: 13 additions & 0 deletions packages/vercel-remix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Welcome to Remix!

[Remix](https://remix.run) is a web framework that helps you build better websites with React.

To get started, open a new shell and run:

```sh
npx create-remix@latest
```

Then follow the prompts you see in your terminal.

For more information about Remix, [visit remix.run](https://remix.run)!
53 changes: 53 additions & 0 deletions packages/vercel-remix/edge/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { SignFunction, UnsignFunction } from "@remix-run/server-runtime";

const encoder = new TextEncoder();

export const sign: SignFunction = async (value, secret) => {
let key = await createKey(secret, ["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 const unsign: UnsignFunction = async (signed, secret) => {
let index = signed.lastIndexOf(".");
let value = signed.slice(0, index);
let hash = signed.slice(index + 1);

let key = await createKey(secret, ["verify"]);
let data = encoder.encode(value);
let signature = byteStringToUint8Array(atob(hash));
let valid = await crypto.subtle.verify("HMAC", key, signature, data);

return valid ? value : false;
};

async function createKey(
secret: string,
usages: CryptoKey["usages"]
): Promise<CryptoKey> {
let key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
usages
);

return key;
}

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;
}
27 changes: 27 additions & 0 deletions packages/vercel-remix/edge/entry.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import isbot from 'isbot';
import { renderToReadableStream } from 'react-dom/server';

export async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixServer: JSX.Element
) {
let body = await renderToReadableStream(remixServer, {
signal: request.signal,
onError(error) {
console.error(error);
responseStatusCode = 500;
},
});

if (isbot(request.headers.get('user-agent'))) {
await body.allReady;
}

responseHeaders.set('Content-Type', 'text/html');
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
13 changes: 13 additions & 0 deletions packages/vercel-remix/edge/implementations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
createCookieFactory,
createCookieSessionStorageFactory,
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);

67 changes: 67 additions & 0 deletions packages/vercel-remix/edge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export {
createCookie,
createCookieSessionStorage,
createSessionStorage,
} from './implementations';

export {
createRequestHandler,
createSession,
defer,
isCookie,
isSession,
json,
MaxPartSizeExceededError,
redirect,
unstable_composeUploadHandlers,
unstable_createMemoryUploadHandler,
unstable_parseMultipartFormData,
} from '@remix-run/server-runtime';

export type {
ActionArgs,
ActionFunction,
AppData,
AppLoadContext,
Cookie,
CookieOptions,
CookieParseOptions,
CookieSerializeOptions,
CookieSignatureOptions,
DataFunctionArgs,
EntryContext,
ErrorBoundaryComponent,
HandleDataRequestFunction,
HandleDocumentRequestFunction,
HeadersFunction,
HtmlLinkDescriptor,
HtmlMetaDescriptor,
V2_HtmlMetaDescriptor,
JsonFunction,
LinkDescriptor,
LinksFunction,
LoaderArgs,
LoaderFunction,
MemoryUploadHandlerFilterArgs,
MemoryUploadHandlerOptions,
MetaDescriptor,
MetaFunction,
V2_MetaFunction,
PageLinkDescriptor,
RequestHandler,
RouteComponent,
RouteHandle,
SerializeFrom,
ServerBuild,
ServerEntryModule,
Session,
SessionData,
SessionIdStorageStrategy,
SessionStorage,
SignFunction,
TypedDeferredData,
TypedResponse,
UnsignFunction,
UploadHandler,
UploadHandlerPart,
} from '@remix-run/server-runtime';
21 changes: 21 additions & 0 deletions packages/vercel-remix/edge/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ServerBuild, AppLoadContext, RequestHandler } from "@remix-run/server-runtime";
import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";

export type GetLoadContextFunction = (req: Request) => AppLoadContext;

export function createRequestHandler({
build,
getLoadContext,
mode,
}: {
build: ServerBuild;
getLoadContext?: GetLoadContextFunction;
mode?: string;
}): RequestHandler {
let handleRequest = createRemixRequestHandler(build, mode);

return (request) => {
let loadContext = getLoadContext?.(request);
return handleRequest(request, loadContext);
};
}
81 changes: 81 additions & 0 deletions packages/vercel-remix/entry.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { PassThrough } from 'stream';
import { renderToPipeableStream } from 'react-dom/server';
import { Response } from '@remix-run/node';
import isbot from 'isbot';

const ABORT_DELAY = 5000;

export function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixServer: JSX.Element
) {
// If the request is from a bot, we want to wait for the full
// response to render before sending it to the client. This
// ensures that bots can see the full page content.
if (isbot(request.headers.get('user-agent'))) {
return serveTheBots(responseStatusCode, responseHeaders, remixServer);
}

return serveBrowsers(responseStatusCode, responseHeaders, remixServer);
}

function serveTheBots(
responseStatusCode: number,
responseHeaders: Headers,
remixServer: JSX.Element
) {
return new Promise((resolve, reject) => {
let { pipe, abort } = renderToPipeableStream(remixServer, {
// Use onAllReady to wait for the entire document to be ready
onAllReady() {
responseHeaders.set('Content-Type', 'text/html');
let body = new PassThrough();
pipe(body);
resolve(
new Response(body, {
status: responseStatusCode,
headers: responseHeaders,
})
);
},
onShellError(err) {
reject(err);
},
});
setTimeout(abort, ABORT_DELAY);
});
}

function serveBrowsers(
responseStatusCode: number,
responseHeaders: Headers,
remixServer: JSX.Element
) {
return new Promise((resolve, reject) => {
let didError = false;
let { pipe, abort } = renderToPipeableStream(remixServer, {
// use onShellReady to wait until a suspense boundary is triggered
onShellReady() {
responseHeaders.set('Content-Type', 'text/html');
let body = new PassThrough();
pipe(body);
resolve(
new Response(body, {
status: didError ? 500 : responseStatusCode,
headers: responseHeaders,
})
);
},
onShellError(err) {
reject(err);
},
onError(err) {
didError = true;
console.error(err);
},
});
setTimeout(abort, ABORT_DELAY);
});
}
2 changes: 2 additions & 0 deletions packages/vercel-remix/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { installGlobals } from "@remix-run/node";
installGlobals();
67 changes: 67 additions & 0 deletions packages/vercel-remix/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export {
createCookie,
createCookieSessionStorage,
createSessionStorage,
} from "@remix-run/node";

export {
createRequestHandler,
createSession,
defer,
isCookie,
isSession,
json,
MaxPartSizeExceededError,
redirect,
unstable_composeUploadHandlers,
unstable_createMemoryUploadHandler,
unstable_parseMultipartFormData,
} from "@remix-run/server-runtime";

export type {
ActionArgs,
ActionFunction,
AppData,
AppLoadContext,
Cookie,
CookieOptions,
CookieParseOptions,
CookieSerializeOptions,
CookieSignatureOptions,
DataFunctionArgs,
EntryContext,
ErrorBoundaryComponent,
HandleDataRequestFunction,
HandleDocumentRequestFunction,
HeadersFunction,
HtmlLinkDescriptor,
HtmlMetaDescriptor,
V2_HtmlMetaDescriptor,
JsonFunction,
LinkDescriptor,
LinksFunction,
LoaderArgs,
LoaderFunction,
MemoryUploadHandlerFilterArgs,
MemoryUploadHandlerOptions,
MetaDescriptor,
MetaFunction,
V2_MetaFunction,
PageLinkDescriptor,
RequestHandler,
RouteComponent,
RouteHandle,
SerializeFrom,
ServerBuild,
ServerEntryModule,
Session,
SessionData,
SessionIdStorageStrategy,
SessionStorage,
SignFunction,
TypedDeferredData,
TypedResponse,
UnsignFunction,
UploadHandler,
UploadHandlerPart,
} from "@remix-run/server-runtime";
5 changes: 5 additions & 0 deletions packages/vercel-remix/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
...require("../../jest/jest.config.shared"),
displayName: "vercel",
};
Loading

0 comments on commit 9a16847

Please sign in to comment.