Skip to content

Commit

Permalink
feat: Adds RemixSite construct
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrlplusb committed Jun 17, 2022
1 parent 2ef481a commit 9c0f95c
Show file tree
Hide file tree
Showing 37 changed files with 84,874 additions and 485 deletions.
56,209 changes: 56,209 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -26,7 +26,7 @@
"prettier": "^2.2.1",
"turbo": "^1.2.8",
"typescript": "^4.6.2",
"vitest": "^0.12.7"
"vitest": "^0.15.1"
},
"lint-staged": {
"*.{js,ts,css,json,md}": [
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Expand Up @@ -48,7 +48,7 @@
"@types/node": "^14.0.27",
"@types/ws": "^8.5.3",
"replace-in-file": "^6.1.0",
"vitest": "^0.12.7"
"vitest": "^0.15.1"
},
"gitHead": "8ac2d0abc11d5de721c87658bb445e3d6c211dcf"
}
72 changes: 72 additions & 0 deletions packages/resources/assets/RemixSite/config/read-remix-config.cjs
@@ -0,0 +1,72 @@
"use strict";

// Given we can't do synchronous dynamic imports within ESM we have to create
// this script so that our CDK construct can execute it synchronously and get
// the Remix config.

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on("unhandledRejection", (err) => {
throw err;
});

const fs = require("fs-extra");
const chalk = require("chalk");

// https://remix.run/docs/en/v1/api/conventions#remixconfigjs
const configDefaults = {
assetsBuildDirectory: "public/build",
publicPath: "/build/",
serverBuildPath: "build/index.js",
serverBuildTarget: "node-cjs",
server: undefined,
};

function failWithError(msg) {
console.error(chalk.red(msg));
process.exit(1);
}

function getRemixConfig(configPath) {
if (!fs.existsSync(configPath)) {
failWithError(
`Could not find remix.config.js at expected path "${configPath}".`
);
}
const userConfig = require(configPath);
const config = {
...configDefaults,
...userConfig,
};

if (config.serverBuildTarget !== "node-cjs") {
failWithError('\nremix.config.js "serverBuildTarget" must be "node-cjs"');
}

return config;
}

const parsedArgs = parseArgs(process.argv);

// Parse default config
if (!parsedArgs["--path"]) {
throw new Error("--path parameter is required");
}

console.log(JSON.stringify(getRemixConfig(parsedArgs["--path"])));

function parseArgs(arrArgs) {
return arrArgs.slice(2).reduce((acc, key, ind, self) => {
if (key.startsWith("--")) {
if (self[ind + 1] && self[ind + 1].startsWith("-")) {
acc[key] = null;
} else if (self[ind + 1]) {
acc[key] = self[ind + 1];
} else if (!self[ind + 1]) {
acc[key] = null;
}
}
return acc;
}, {});
}
@@ -0,0 +1,5 @@
"use strict";

exports.handler = () => {
// placeholder function
};
110 changes: 110 additions & 0 deletions packages/resources/assets/RemixSite/server/server-template.js
@@ -0,0 +1,110 @@
// 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.

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

import * as remixServerBuild from "./index.js";

installGlobals();

function createNodeRequestHeaders(requestHeaders) {
const headers = new NodeHeaders();

for (const [key, values] of Object.entries(requestHeaders)) {
for (const { value } of values) {
if (value) {
headers.append(key, value);
}
}
}

return headers;
}

function createNodeRequest(event) {
const request = event.Records[0].cf.request;

const host = request.headers["host"]
? request.headers["host"][0].value
: undefined;
const search = request.querystring.length ? `?${request.querystring}` : "";
const url = new URL(request.uri + search, `https://${host}`);

return new NodeRequest(url.toString(), {
method: request.method,
headers: createNodeRequestHeaders(request.headers),
body: request.body?.data
? request.body.encoding === "base64"
? Buffer.from(request.body.data, "base64").toString()
: request.body.data
: undefined,
});
}

function createCloudFrontResponseHeaders(responseHeaders) {
const headers = {};
const rawHeaders = responseHeaders.raw();

for (const key in rawHeaders) {
const value = rawHeaders[key];
for (const v of value) {
headers[key] = [...(headers[key] || []), { key, value: v }];
}
}

return headers;
}

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

return async (event, _context) => {
const request = createNodeRequest(event);

const loadContext =
typeof getLoadContext === "function" ? getLoadContext(event) : undefined;

const response = await remixRequestHandler(request, loadContext);

return {
status: String(response.status),
headers: createCloudFrontResponseHeaders(response.headers),
bodyEncoding: "text",
body: await response.text(),
};
};
}

const cloudFrontRequestHandler = createCloudFrontRequestHandler({
remixServerBuild,
});

export 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);
}
99 changes: 99 additions & 0 deletions packages/resources/assets/RemixSite/site-sub/index.html

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions packages/resources/assets/nodejs/folder-hash.cjs
@@ -0,0 +1,66 @@
"use strict";

// Given we can't do synchronous dynamic imports within ESM we have to create
// this script so that our CDK construct can execute it synchronously and get
// the Remix config.

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on("unhandledRejection", (err) => {
throw err;
});

const fs = require("fs-extra");
const chalk = require("chalk");
const { hashElement } = require("folder-hash");

function failWithError(msg) {
console.error(chalk.red(msg));
process.exit(1);
}

async function getFolderHash(dir, ignore) {
if (!fs.existsSync(dir)) {
failWithError(`Could not find directory at path "${dir}".`);
}
if (!fs.statSync(dir).isDirectory()) {
failWithError(`Path "${dir}" is not a directory.`);
}

const { hash } = await hashElement(dir, {
folders: {
exclude: ignore ? [ignore] : undefined,
matchBasename: true,
matchPath: false,
ignoreBasename: true,
},
});
return hash;
}

const parsedArgs = parseArgs(process.argv);

// Parse default config
if (!parsedArgs["--path"]) {
throw new Error("--path parameter is required");
}

getFolderHash(parsedArgs["--path"], parsedArgs["--ignore"]).then((hash) => {
console.log(hash);
});

function parseArgs(arrArgs) {
return arrArgs.slice(2).reduce((acc, key, ind, self) => {
if (key.startsWith("--")) {
if (self[ind + 1] && self[ind + 1].startsWith("-")) {
acc[key] = null;
} else if (self[ind + 1]) {
acc[key] = self[ind + 1];
} else if (!self[ind + 1]) {
acc[key] = null;
}
}
return acc;
}, {});
}
7 changes: 6 additions & 1 deletion packages/resources/package.json
Expand Up @@ -44,20 +44,25 @@
"chalk": "^4.1.0",
"constructs": "^10.0.29",
"cross-spawn": "^7.0.3",
"folder-hash": "^4.0.2",
"fs-extra": "^9.0.1",
"glob": "^7.1.7",
"indent-string": "^5.0.0",
"read-pkg": "^7.1.0",
"zip-local": "^0.3.4",
"zod": "^3.14.3"
},
"devDependencies": {
"@graphql-tools/merge": "^8.2.12",
"@sls-next/lambda-at-edge": "^3.7.0-alpha.7",
"@types/cross-spawn": "^6.0.2",
"@types/folder-hash": "^4.0.1",
"@types/fs-extra": "^9.0.6",
"@types/glob": "^7.2.0",
"@types/node": "^14.0.27",
"esbuild-jest": "^0.5.0",
"typedoc": "^0.22.13"
"typedoc": "^0.22.13",
"vitest": "^0.15.1"
},
"optionalDependencies": {
"graphql": "^16.5.0"
Expand Down

0 comments on commit 9c0f95c

Please sign in to comment.