Skip to content

Commit

Permalink
feat: RemixSite can toggle between ApiG and Edge via flag
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrlplusb committed Jul 4, 2022
1 parent a9023de commit d029a70
Show file tree
Hide file tree
Showing 7 changed files with 868 additions and 557 deletions.

This file was deleted.

169 changes: 169 additions & 0 deletions packages/resources/assets/RemixSite/server/apig-server-template.js
@@ -0,0 +1,169 @@
// This is a custom APIGatewayV2 Lambda handler which imports the Remix server
// build and performs the Remix server rendering.

// We have to ensure that our polyfills are imported prior to any other modules
// which may depend on them;
import "./polyfills.js";

import {
Headers as NodeHeaders,
Request as NodeRequest,
createRequestHandler as createRemixRequestHandler,
readableStreamToString,
installGlobals,
} from "@remix-run/node";

// Import the server build that was produced by `remix build`;
import * as remixServerBuild from "./index.js";

/**
* Common binary MIME types
*/
const binaryTypes = [
"application/octet-stream",
// Docs
"application/epub+zip",
"application/msword",
"application/pdf",
"application/rtf",
"application/vnd.amazon.ebook",
"application/vnd.ms-excel",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
// Fonts
"font/otf",
"font/woff",
"font/woff2",
// Images
"image/bmp",
"image/gif",
"image/jpeg",
"image/png",
"image/tiff",
"image/vnd.microsoft.icon",
"image/webp",
// Audio
"audio/3gpp",
"audio/aac",
"audio/basic",
"audio/mpeg",
"audio/ogg",
"audio/wavaudio/webm",
"audio/x-aiff",
"audio/x-midi",
"audio/x-wav",
// Video
"video/3gpp",
"video/mp2t",
"video/mpeg",
"video/ogg",
"video/quicktime",
"video/webm",
"video/x-msvideo",
// Archives
"application/java-archive",
"application/vnd.apple.installer+xml",
"application/x-7z-compressed",
"application/x-apple-diskimage",
"application/x-bzip",
"application/x-bzip2",
"application/x-gzip",
"application/x-java-archive",
"application/x-rar-compressed",
"application/x-tar",
"application/x-zip",
"application/zip",
];

function isBinaryType(contentType) {
if (!contentType) return false;
return binaryTypes.some((t) => contentType.includes(t));
}

function createRemixRequest(event) {
let host = event.headers["x-forwarded-host"] || event.headers.host;
let search = event.rawQueryString.length ? `?${event.rawQueryString}` : "";
let scheme = "https";
let url = new URL(event.rawPath + search, `${scheme}://${host}`);
let isFormData = event.headers["content-type"]?.includes(
"multipart/form-data"
);

return new NodeRequest(url.href, {
method: event.requestContext.http.method,
headers: createRemixHeaders(event.headers, event.cookies),
body:
event.body && event.isBase64Encoded
? isFormData
? Buffer.from(event.body, "base64")
: Buffer.from(event.body, "base64").toString()
: event.body,
});
}

function createRemixHeaders(requestHeaders, requestCookies) {
let headers = new NodeHeaders();

for (let [header, value] of Object.entries(requestHeaders)) {
if (value) {
headers.append(header, value);
}
}

if (requestCookies) {
headers.append("Cookie", requestCookies.join("; "));
}

return headers;
}

async function sendRemixResponse(nodeResponse) {
let cookies = [];

// Arc/AWS API Gateway will send back set-cookies outside of response headers.
for (let [key, values] of Object.entries(nodeResponse.headers.raw())) {
if (key.toLowerCase() === "set-cookie") {
for (let value of values) {
cookies.push(value);
}
}
}

if (cookies.length) {
nodeResponse.headers.delete("Set-Cookie");
}

let contentType = nodeResponse.headers.get("Content-Type");
let isBase64Encoded = isBinaryType(contentType);
let body;

if (nodeResponse.body) {
if (isBase64Encoded) {
body = await readableStreamToString(nodeResponse.body, "base64");
} else {
body = await nodeResponse.text();
}
}

return {
statusCode: nodeResponse.status,
headers: Object.fromEntries(nodeResponse.headers.entries()),
cookies,
body,
isBase64Encoded,
};
}

const createAPIGatewayV2RequestHandler = (build) => {
const requestHandler = createRemixRequestHandler(build, process.env.NODE_ENV);

return async (event) => {
const request = createRemixRequest(event);
const response = await requestHandler(request);
return sendRemixResponse(response);
};
};

export const handler = createAPIGatewayV2RequestHandler(remixServerBuild);
@@ -1,25 +1,20 @@
// This is a custom CloudFront Lambda@Edge handler which we will utilise to wrap
// the Remix server build with.
//
// It additionally exposes an environment variable token which is used by our
// Lambda Replacer code to inject the appropriate environment variables. This
// strategy is required as Lambda@Edge doesn't natively support environment
// variables - they need to be inlined.
//
// Shout out to the Architect team, from which we drew inspiration.

const { installGlobals, readableStreamToString } = require("@remix-run/node");
installGlobals();

const {
Headers: NodeHeaders,
Request: NodeRequest,
} = require("@remix-run/node");
const {
createRequestHandler: createRemixRequestHandler,
} = require("@remix-run/server-runtime");
const { URL } = require("url");
const remixServerBuild = require("./index.js");
// This is a custom CloudFront Lambda@Edge handler which imports the Remix server
// build and performs the Remix server rendering.

// We have to ensure that our polyfills are imported prior to any other modules
// which may depend on them;
import "./polyfills.js";

import { readableStreamToString } from "@remix-run/node";
import {
Headers as NodeHeaders,
Request as NodeRequest,
} from "@remix-run/node";
import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";
import { URL } from "url";

// Import the server build that was produced by `remix build`;
import * as remixServerBuild from "./index.js";

/**
* Common binary MIME types
Expand Down Expand Up @@ -135,21 +130,32 @@ function createCloudFrontResponseHeaders(responseHeaders) {
return headers;
}

function createCloudFrontRequestHandler({
remixServerBuild,
getLoadContext,
mode = process.env.NODE_ENV,
}) {
const remixRequestHandler = createRemixRequestHandler(remixServerBuild, mode);

return async (event, _context) => {
const request = createNodeRequest(event);
function createCloudFrontEdgeRequestHandler(build) {
// We expose an environment variable token which is used by our Lambda Replacer
// code to inject the environment variables assigned to the RemixSite construct.
// This inlining strategy is required as Lambda@Edge doesn't natively support
// runtime environment variables. A downside of this approach is that environment
// variables cannot be toggled after deployment, each change to one requires a
// redeployment.
try {
// The right hand side of this assignment will get replaced during
// deployment with an object of environment key-value pairs;
const environment = "{{ _SST_REMIX_SITE_ENVIRONMENT_ }}";

const loadContext =
typeof getLoadContext === "function" ? getLoadContext(event) : undefined;
process.env = { ...process.env, ...environment };
} catch (e) {
console.log("Failed to set SST RemixSite environment.");
console.log(e);
}

const response = await remixRequestHandler(request, loadContext);
const remixRequestHandler = createRemixRequestHandler(
build,
process.env.NODE_ENV
);

return async (event, _context) => {
const request = createNodeRequest(event);
const response = await remixRequestHandler(request);
const contentType = response.headers.get("Content-Type");
const isBase64Encoded = isBinaryType(contentType);

Expand All @@ -172,21 +178,4 @@ function createCloudFrontRequestHandler({
};
}

const cloudFrontRequestHandler = createCloudFrontRequestHandler({
remixServerBuild,
});

exports.handler = async function handler(...args) {
try {
// The right hand side of this assignment will get replaced during
// deployment with an object of environment key-value pairs;
const environment = "{{ _SST_REMIX_SITE_ENVIRONMENT_ }}";

process.env = { ...process.env, ...environment };
} catch (e) {
console.log("Failed to set SST RemixSite environment.");
console.log(e);
}

return await cloudFrontRequestHandler(...args);
};
export const handler = createCloudFrontEdgeRequestHandler(remixServerBuild);
3 changes: 3 additions & 0 deletions packages/resources/assets/RemixSite/server/polyfills.js
@@ -0,0 +1,3 @@
import { installGlobals } from "@remix-run/node";

installGlobals();

0 comments on commit d029a70

Please sign in to comment.