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

Commit 6f1a1b3

Browse files
authored
Remove dependency: next-aws-lambda (#92)
* Copy files from next-aws-lambda Copy over the files from the next-aws-lambda package and manually bundle them into our Netlify Functions. This gives us more flexibility to customize the compatibility layer between Netlify Functions and Next.js. For now, no changes have been made to the next-aws-lambda files and they have been copied as-is. next-aws-lambda source: https://github.com/serverless-nextjs/serverless-next.js/tree/master/packages/compat-layers/apigw-lambda-compat * Remove workaround: base64 support in Netlify Functions As far as I know, Netlify Functions always support base64 encoding. So we can remove the code for checking if base64 encoding is supported. * Netlify Function Handler: Use promise rather than callback Use the promise approach of next-aws-lambda rather than the callback approach, because it makes the code easier to read and puts it in the correct order of execution. * Refactor: Replace compat.js with renderNextPage.js Wrap the logic for rendering the Next.js page into its own function. That keeps the netlifyFunction.js (function handler) minimal and easy to read. It will also make it easier for users to modify the function handler while keeping the render function unchanged (down the line, once we support this feature). * Refactor: Split reqResMapper into createRequest/ResponseObject Split the reqResMapper function into two separate files. One for creating the request object and one for creating the response object. This is easier to read and understand.
1 parent e154c81 commit 6f1a1b3

8 files changed

+240
-57
lines changed

lib/config.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ const NEXT_CONFIG_PATH = join(".", "next.config.js");
1919
// This is the folder that NextJS builds to; default is .next
2020
const NEXT_DIST_DIR = getNextDistDir({ nextConfigPath: NEXT_CONFIG_PATH });
2121

22+
// This is the folder with templates for Netlify Functions
23+
const TEMPLATES_DIR = join(__dirname, "templates");
24+
2225
// This is the Netlify Function template that wraps all SSR pages
23-
const FUNCTION_TEMPLATE_PATH = join(
24-
__dirname,
25-
"templates",
26-
"netlifyFunction.js"
27-
);
26+
const FUNCTION_TEMPLATE_PATH = join(TEMPLATES_DIR, "netlifyFunction.js");
2827

2928
// This is the file where custom redirects can be configured
3029
const CUSTOM_REDIRECTS_PATH = join(".", "_redirects");
@@ -35,6 +34,7 @@ module.exports = {
3534
PUBLIC_PATH,
3635
NEXT_CONFIG_PATH,
3736
NEXT_DIST_DIR,
37+
TEMPLATES_DIR,
3838
FUNCTION_TEMPLATE_PATH,
3939
CUSTOM_REDIRECTS_PATH,
4040
};

lib/helpers/setupNetlifyFunctionForPage.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const { copySync } = require("fs-extra");
22
const { join } = require("path");
3-
const { NEXT_DIST_DIR, FUNCTION_TEMPLATE_PATH } = require("../config");
3+
const {
4+
NEXT_DIST_DIR,
5+
TEMPLATES_DIR,
6+
FUNCTION_TEMPLATE_PATH,
7+
} = require("../config");
48
const getNetlifyFunctionName = require("./getNetlifyFunctionName");
59

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

26+
// Copy function helpers
27+
const functionHelpers = [
28+
"renderNextPage.js",
29+
"createRequestObject.js",
30+
"createResponseObject.js",
31+
];
32+
functionHelpers.forEach((helper) => {
33+
copySync(join(TEMPLATES_DIR, helper), join(functionDirectory, helper), {
34+
overwrite: false,
35+
errorOnExist: true,
36+
});
37+
});
38+
2239
// Copy page
23-
const nextPageCopyPath = join(functionDirectory, "nextJsPage.js");
40+
const nextPageCopyPath = join(functionDirectory, "nextPage.js");
2441
copySync(join(NEXT_DIST_DIR, "serverless", filePath), nextPageCopyPath, {
2542
overwrite: false,
2643
errorOnExist: true,

lib/templates/createRequestObject.js

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const Stream = require("stream");
2+
const queryString = require("querystring");
3+
const http = require("http");
4+
5+
// Mock a HTTP IncomingMessage object from the Netlify Function event parameters
6+
// Based on API Gateway Lambda Compat
7+
// Source: https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/compat-layers/apigw-lambda-compat/lib/compatLayer.js
8+
9+
const createRequestObject = ({ event }) => {
10+
const {
11+
requestContext = {},
12+
path = "",
13+
multiValueQueryStringParameters,
14+
pathParameters,
15+
httpMethod,
16+
multiValueHeaders = {},
17+
body,
18+
isBase64Encoded,
19+
} = event;
20+
21+
const newStream = new Stream.Readable();
22+
const req = Object.assign(newStream, http.IncomingMessage.prototype);
23+
req.url =
24+
(requestContext.path || path || "").replace(
25+
new RegExp("^/" + requestContext.stage),
26+
""
27+
) || "/";
28+
29+
let qs = "";
30+
31+
if (multiValueQueryStringParameters) {
32+
qs += queryString.stringify(multiValueQueryStringParameters);
33+
}
34+
35+
if (pathParameters) {
36+
const pathParametersQs = queryString.stringify(pathParameters);
37+
38+
if (qs.length > 0) {
39+
qs += `&${pathParametersQs}`;
40+
} else {
41+
qs += pathParametersQs;
42+
}
43+
}
44+
45+
const hasQueryString = qs.length > 0;
46+
47+
if (hasQueryString) {
48+
req.url += `?${qs}`;
49+
}
50+
51+
req.method = httpMethod;
52+
req.rawHeaders = [];
53+
req.headers = {};
54+
55+
for (const key of Object.keys(multiValueHeaders)) {
56+
for (const value of multiValueHeaders[key]) {
57+
req.rawHeaders.push(key);
58+
req.rawHeaders.push(value);
59+
}
60+
req.headers[key.toLowerCase()] = multiValueHeaders[key].toString();
61+
}
62+
63+
req.getHeader = (name) => {
64+
return req.headers[name.toLowerCase()];
65+
};
66+
req.getHeaders = () => {
67+
return req.headers;
68+
};
69+
70+
req.connection = {};
71+
72+
if (body) {
73+
req.push(body, isBase64Encoded ? "base64" : undefined);
74+
}
75+
76+
req.push(null);
77+
78+
return req;
79+
};
80+
81+
module.exports = createRequestObject;

lib/templates/createResponseObject.js

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const Stream = require("stream");
2+
3+
// Mock a HTTP ServerResponse object that returns a Netlify Function-compatible
4+
// response via the onResEnd callback when res.end() is called.
5+
// Based on API Gateway Lambda Compat
6+
// Source: https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/compat-layers/apigw-lambda-compat/lib/compatLayer.js
7+
8+
const createResponseObject = ({ onResEnd }) => {
9+
const response = {
10+
isBase64Encoded: true,
11+
multiValueHeaders: {},
12+
};
13+
14+
const res = new Stream();
15+
Object.defineProperty(res, "statusCode", {
16+
get() {
17+
return response.statusCode;
18+
},
19+
set(statusCode) {
20+
response.statusCode = statusCode;
21+
},
22+
});
23+
res.headers = {};
24+
res.writeHead = (status, headers) => {
25+
response.statusCode = status;
26+
if (headers) res.headers = Object.assign(res.headers, headers);
27+
};
28+
res.write = (chunk) => {
29+
if (!response.body) {
30+
response.body = Buffer.from("");
31+
}
32+
33+
response.body = Buffer.concat([
34+
Buffer.isBuffer(response.body)
35+
? response.body
36+
: Buffer.from(response.body),
37+
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
38+
]);
39+
};
40+
res.setHeader = (name, value) => {
41+
res.headers[name.toLowerCase()] = value;
42+
};
43+
res.removeHeader = (name) => {
44+
delete res.headers[name.toLowerCase()];
45+
};
46+
res.getHeader = (name) => {
47+
return res.headers[name.toLowerCase()];
48+
};
49+
res.getHeaders = () => {
50+
return res.headers;
51+
};
52+
res.hasHeader = (name) => {
53+
return !!res.getHeader(name);
54+
};
55+
res.end = (text) => {
56+
if (text) res.write(text);
57+
if (!res.statusCode) {
58+
res.statusCode = 200;
59+
}
60+
61+
if (response.body) {
62+
response.body = Buffer.from(response.body).toString("base64");
63+
}
64+
response.multiValueHeaders = res.headers;
65+
res.writeHead(response.statusCode);
66+
67+
// Convert all multiValueHeaders into arrays
68+
for (const key of Object.keys(response.multiValueHeaders)) {
69+
if (!Array.isArray(response.multiValueHeaders[key])) {
70+
response.multiValueHeaders[key] = [response.multiValueHeaders[key]];
71+
}
72+
}
73+
74+
// Call onResEnd handler with the response object
75+
onResEnd(response);
76+
};
77+
78+
return res;
79+
};
80+
81+
module.exports = createResponseObject;

lib/templates/netlifyFunction.js

+21-43
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,10 @@
11
// TEMPLATE: This file will be copied to the Netlify functions directory when
22
// running next-on-netlify
33

4-
// Compatibility wrapper for NextJS page
5-
const compat = require("next-aws-lambda");
6-
// Load the NextJS page
7-
const page = require("./nextJsPage");
8-
9-
// next-aws-lambda is made for AWS. There are some minor differences between
10-
// Netlify and AWS which we resolve here.
11-
const callbackHandler = (callback) =>
12-
// The callbackHandler wraps the callback
13-
(argument, response) => {
14-
// Convert header values to string. Netlify does not support integers as
15-
// header values. See: https://github.com/netlify/cli/issues/451
16-
Object.keys(response.multiValueHeaders).forEach((key) => {
17-
response.multiValueHeaders[key] = response.multiValueHeaders[
18-
key
19-
].map((value) => String(value));
20-
});
21-
22-
response.multiValueHeaders["Cache-Control"] = ["no-cache"];
23-
24-
// Invoke callback
25-
callback(argument, response);
26-
};
27-
28-
exports.handler = (event, context, callback) => {
29-
// Enable support for base64 encoding.
30-
// This is used by next-aws-lambda to determine whether to encode the response
31-
// body as base64.
32-
if (!process.env.hasOwnProperty("BINARY_SUPPORT")) {
33-
process.env.BINARY_SUPPORT = "yes";
34-
}
4+
// Render function for the Next.js page
5+
const renderNextPage = require("./renderNextPage");
356

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

46-
// Render the page
47-
compat(page)(
48-
{
49-
...event,
50-
// Required. Otherwise, compat() will complain
51-
requestContext: {},
52-
},
53-
context,
54-
// Wrap the Netlify callback, so that we can resolve differences between
55-
// Netlify and AWS (which next-aws-lambda optimizes for)
56-
callbackHandler(callback)
57-
);
18+
// Render the Next.js page
19+
const response = await renderNextPage({
20+
...event,
21+
// Required. Otherwise, reqResMapper will complain
22+
requestContext: {},
23+
});
24+
25+
// Convert header values to string. Netlify does not support integers as
26+
// header values. See: https://github.com/netlify/cli/issues/451
27+
Object.keys(response.multiValueHeaders).forEach((key) => {
28+
response.multiValueHeaders[key] = response.multiValueHeaders[
29+
key
30+
].map((value) => String(value));
31+
});
32+
33+
response.multiValueHeaders["Cache-Control"] = ["no-cache"];
34+
35+
callback(null, response);
5836
};

lib/templates/renderNextPage.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Load the NextJS page
2+
const nextPage = require("./nextPage");
3+
const createRequestObject = require("./createRequestObject");
4+
const createResponseObject = require("./createResponseObject");
5+
6+
// Render the Next.js page
7+
const renderNextPage = (event) => {
8+
// The Next.js page is rendered inside a promise that is resolved when the
9+
// Next.js page ends the response via `res.end()`
10+
const promise = new Promise((resolve) => {
11+
// Create a Next.js-compatible request and response object
12+
// These mock the ClientRequest and ServerResponse classes from node http
13+
// See: https://nodejs.org/api/http.html
14+
const req = createRequestObject({ event });
15+
const res = createResponseObject({
16+
onResEnd: (response) => resolve(response),
17+
});
18+
19+
// Check if page is a Next.js page or an API route
20+
const isNextPage = nextPage.render instanceof Function;
21+
const isApiRoute = !isNextPage;
22+
23+
// Perform the render: render() for Next.js page or default() for API route
24+
if (isNextPage) return nextPage.render(req, res);
25+
if (isApiRoute) return nextPage.default(req, res);
26+
});
27+
28+
// Return the promise
29+
return promise;
30+
};
31+
32+
module.exports = renderNextPage;

package-lock.json

-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@
5252
"dependencies": {
5353
"@sls-next/lambda-at-edge": "^1.5.2",
5454
"commander": "^6.0.0",
55-
"fs-extra": "^9.0.1",
56-
"next-aws-lambda": "^2.5.0"
55+
"fs-extra": "^9.0.1"
5756
},
5857
"husky": {
5958
"hooks": {

0 commit comments

Comments
 (0)