Skip to content
This repository was archived by the owner on May 10, 2021. It is now read-only.

Remove dependency: next-aws-lambda #92

Merged
merged 5 commits into from
Nov 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ const NEXT_CONFIG_PATH = join(".", "next.config.js");
// This is the folder that NextJS builds to; default is .next
const NEXT_DIST_DIR = getNextDistDir({ nextConfigPath: NEXT_CONFIG_PATH });

// This is the folder with templates for Netlify Functions
const TEMPLATES_DIR = join(__dirname, "templates");

// This is the Netlify Function template that wraps all SSR pages
const FUNCTION_TEMPLATE_PATH = join(
__dirname,
"templates",
"netlifyFunction.js"
);
const FUNCTION_TEMPLATE_PATH = join(TEMPLATES_DIR, "netlifyFunction.js");

// This is the file where custom redirects can be configured
const CUSTOM_REDIRECTS_PATH = join(".", "_redirects");
Expand All @@ -35,6 +34,7 @@ module.exports = {
PUBLIC_PATH,
NEXT_CONFIG_PATH,
NEXT_DIST_DIR,
TEMPLATES_DIR,
FUNCTION_TEMPLATE_PATH,
CUSTOM_REDIRECTS_PATH,
};
21 changes: 19 additions & 2 deletions lib/helpers/setupNetlifyFunctionForPage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const { copySync } = require("fs-extra");
const { join } = require("path");
const { NEXT_DIST_DIR, FUNCTION_TEMPLATE_PATH } = require("../config");
const {
NEXT_DIST_DIR,
TEMPLATES_DIR,
FUNCTION_TEMPLATE_PATH,
} = require("../config");
const getNetlifyFunctionName = require("./getNetlifyFunctionName");

// Create a Netlify Function for the page with the given file path
Expand All @@ -19,8 +23,21 @@ const setupNetlifyFunctionForPage = ({ filePath, functionsPath }) => {
errorOnExist: true,
});

// Copy function helpers
const functionHelpers = [
"renderNextPage.js",
"createRequestObject.js",
"createResponseObject.js",
];
functionHelpers.forEach((helper) => {
copySync(join(TEMPLATES_DIR, helper), join(functionDirectory, helper), {
overwrite: false,
errorOnExist: true,
});
});

// Copy page
const nextPageCopyPath = join(functionDirectory, "nextJsPage.js");
const nextPageCopyPath = join(functionDirectory, "nextPage.js");
copySync(join(NEXT_DIST_DIR, "serverless", filePath), nextPageCopyPath, {
overwrite: false,
errorOnExist: true,
Expand Down
81 changes: 81 additions & 0 deletions lib/templates/createRequestObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const Stream = require("stream");
const queryString = require("querystring");
const http = require("http");

// Mock a HTTP IncomingMessage object from the Netlify Function event parameters
// Based on API Gateway Lambda Compat
// Source: https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/compat-layers/apigw-lambda-compat/lib/compatLayer.js

const createRequestObject = ({ event }) => {
const {
requestContext = {},
path = "",
multiValueQueryStringParameters,
pathParameters,
httpMethod,
multiValueHeaders = {},
body,
isBase64Encoded,
} = event;

const newStream = new Stream.Readable();
const req = Object.assign(newStream, http.IncomingMessage.prototype);
req.url =
(requestContext.path || path || "").replace(
new RegExp("^/" + requestContext.stage),
""
) || "/";

let qs = "";

if (multiValueQueryStringParameters) {
qs += queryString.stringify(multiValueQueryStringParameters);
}

if (pathParameters) {
const pathParametersQs = queryString.stringify(pathParameters);

if (qs.length > 0) {
qs += `&${pathParametersQs}`;
} else {
qs += pathParametersQs;
}
}

const hasQueryString = qs.length > 0;

if (hasQueryString) {
req.url += `?${qs}`;
}

req.method = httpMethod;
req.rawHeaders = [];
req.headers = {};

for (const key of Object.keys(multiValueHeaders)) {
for (const value of multiValueHeaders[key]) {
req.rawHeaders.push(key);
req.rawHeaders.push(value);
}
req.headers[key.toLowerCase()] = multiValueHeaders[key].toString();
}

req.getHeader = (name) => {
return req.headers[name.toLowerCase()];
};
req.getHeaders = () => {
return req.headers;
};

req.connection = {};

if (body) {
req.push(body, isBase64Encoded ? "base64" : undefined);
}

req.push(null);

return req;
};

module.exports = createRequestObject;
81 changes: 81 additions & 0 deletions lib/templates/createResponseObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const Stream = require("stream");

// Mock a HTTP ServerResponse object that returns a Netlify Function-compatible
// response via the onResEnd callback when res.end() is called.
// Based on API Gateway Lambda Compat
// Source: https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/compat-layers/apigw-lambda-compat/lib/compatLayer.js

const createResponseObject = ({ onResEnd }) => {
const response = {
isBase64Encoded: true,
multiValueHeaders: {},
};

const res = new Stream();
Object.defineProperty(res, "statusCode", {
get() {
return response.statusCode;
},
set(statusCode) {
response.statusCode = statusCode;
},
});
res.headers = {};
res.writeHead = (status, headers) => {
response.statusCode = status;
if (headers) res.headers = Object.assign(res.headers, headers);
};
res.write = (chunk) => {
if (!response.body) {
response.body = Buffer.from("");
}

response.body = Buffer.concat([
Buffer.isBuffer(response.body)
? response.body
: Buffer.from(response.body),
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
]);
};
res.setHeader = (name, value) => {
res.headers[name.toLowerCase()] = value;
};
res.removeHeader = (name) => {
delete res.headers[name.toLowerCase()];
};
res.getHeader = (name) => {
return res.headers[name.toLowerCase()];
};
res.getHeaders = () => {
return res.headers;
};
res.hasHeader = (name) => {
return !!res.getHeader(name);
};
res.end = (text) => {
if (text) res.write(text);
if (!res.statusCode) {
res.statusCode = 200;
}

if (response.body) {
response.body = Buffer.from(response.body).toString("base64");
}
response.multiValueHeaders = res.headers;
res.writeHead(response.statusCode);

// Convert all multiValueHeaders into arrays
for (const key of Object.keys(response.multiValueHeaders)) {
if (!Array.isArray(response.multiValueHeaders[key])) {
response.multiValueHeaders[key] = [response.multiValueHeaders[key]];
}
}

// Call onResEnd handler with the response object
onResEnd(response);
};

return res;
};

module.exports = createResponseObject;
64 changes: 21 additions & 43 deletions lib/templates/netlifyFunction.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,10 @@
// TEMPLATE: This file will be copied to the Netlify functions directory when
// running next-on-netlify

// Compatibility wrapper for NextJS page
const compat = require("next-aws-lambda");
// Load the NextJS page
const page = require("./nextJsPage");

// next-aws-lambda is made for AWS. There are some minor differences between
// Netlify and AWS which we resolve here.
const callbackHandler = (callback) =>
// The callbackHandler wraps the callback
(argument, response) => {
// Convert header values to string. Netlify does not support integers as
// header values. See: https://github.com/netlify/cli/issues/451
Object.keys(response.multiValueHeaders).forEach((key) => {
response.multiValueHeaders[key] = response.multiValueHeaders[
key
].map((value) => String(value));
});

response.multiValueHeaders["Cache-Control"] = ["no-cache"];

// Invoke callback
callback(argument, response);
};

exports.handler = (event, context, callback) => {
// Enable support for base64 encoding.
// This is used by next-aws-lambda to determine whether to encode the response
// body as base64.
if (!process.env.hasOwnProperty("BINARY_SUPPORT")) {
process.env.BINARY_SUPPORT = "yes";
}
// Render function for the Next.js page
const renderNextPage = require("./renderNextPage");

exports.handler = async (event, context, callback) => {
// x-forwarded-host is undefined on Netlify for proxied apps that need it
// fixes https://github.com/netlify/next-on-netlify/issues/46
if (!event.multiValueHeaders.hasOwnProperty("x-forwarded-host")) {
Expand All @@ -43,16 +15,22 @@ exports.handler = (event, context, callback) => {
const { path } = event;
console.log("[request]", path);

// Render the page
compat(page)(
{
...event,
// Required. Otherwise, compat() will complain
requestContext: {},
},
context,
// Wrap the Netlify callback, so that we can resolve differences between
// Netlify and AWS (which next-aws-lambda optimizes for)
callbackHandler(callback)
);
// Render the Next.js page
const response = await renderNextPage({
...event,
// Required. Otherwise, reqResMapper will complain
requestContext: {},
});

// Convert header values to string. Netlify does not support integers as
// header values. See: https://github.com/netlify/cli/issues/451
Object.keys(response.multiValueHeaders).forEach((key) => {
response.multiValueHeaders[key] = response.multiValueHeaders[
key
].map((value) => String(value));
});

response.multiValueHeaders["Cache-Control"] = ["no-cache"];

callback(null, response);
};
32 changes: 32 additions & 0 deletions lib/templates/renderNextPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Load the NextJS page
const nextPage = require("./nextPage");
const createRequestObject = require("./createRequestObject");
const createResponseObject = require("./createResponseObject");

// Render the Next.js page
const renderNextPage = (event) => {
// The Next.js page is rendered inside a promise that is resolved when the
// Next.js page ends the response via `res.end()`
const promise = new Promise((resolve) => {
// Create a Next.js-compatible request and response object
// These mock the ClientRequest and ServerResponse classes from node http
// See: https://nodejs.org/api/http.html
const req = createRequestObject({ event });
const res = createResponseObject({
onResEnd: (response) => resolve(response),
});

// Check if page is a Next.js page or an API route
const isNextPage = nextPage.render instanceof Function;
const isApiRoute = !isNextPage;

// Perform the render: render() for Next.js page or default() for API route
if (isNextPage) return nextPage.render(req, res);
if (isApiRoute) return nextPage.default(req, res);
});

// Return the promise
return promise;
};

module.exports = renderNextPage;
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@
"dependencies": {
"@sls-next/lambda-at-edge": "^1.5.2",
"commander": "^6.0.0",
"fs-extra": "^9.0.1",
"next-aws-lambda": "^2.5.0"
"fs-extra": "^9.0.1"
},
"husky": {
"hooks": {
Expand Down