From 1e35de05cf435e73eae12ecb5f9d92efe5eee3d3 Mon Sep 17 00:00:00 2001 From: Finn Woelm <public@finnwoelm.com> Date: Mon, 14 Dec 2020 18:34:40 +0800 Subject: [PATCH 1/5] Netlify function template: Remove requestContext object This object is not needed on Netlify. It is a relict of next-aws-lambda. --- lib/templates/netlifyFunction.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/templates/netlifyFunction.js b/lib/templates/netlifyFunction.js index 0fc926e..96b72ee 100644 --- a/lib/templates/netlifyFunction.js +++ b/lib/templates/netlifyFunction.js @@ -18,8 +18,6 @@ exports.handler = async (event, context, 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 From c5375452369347b5a87c531d5df04ff6b498fefc Mon Sep 17 00:00:00 2001 From: Finn Woelm <public@finnwoelm.com> Date: Tue, 15 Dec 2020 10:36:49 +0800 Subject: [PATCH 2/5] Expose event + context of Netlify Functions on the req object When a page is being SSR-ed by a Netlify Function, allow users to access the function's event and context parameters. These can be accessed as a property on the `req` object in all SSR-ed pages and in API routes: - req.netlifyFunction.event - req.netlifyFunction.context This allows users to access/leverage Netlify identity for their Next.js page. See: https://github.com/netlify/next-on-netlify/issues/20 It also allows users to modify the callbackWaitsForEmptyEventLoop behavior. See: https://github.com/netlify/next-on-netlify/issues/66#issuecomment-719988804) --- cypress/fixtures/pages/api/context.js | 3 ++ .../pages/getServerSideProps/context.js | 11 +++++ cypress/integration/default_spec.js | 46 +++++++++++++++++++ lib/templates/createRequestObject.js | 10 +++- lib/templates/netlifyFunction.js | 4 +- lib/templates/renderNextPage.js | 4 +- 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 cypress/fixtures/pages/api/context.js create mode 100644 cypress/fixtures/pages/getServerSideProps/context.js diff --git a/cypress/fixtures/pages/api/context.js b/cypress/fixtures/pages/api/context.js new file mode 100644 index 0000000..bf45c18 --- /dev/null +++ b/cypress/fixtures/pages/api/context.js @@ -0,0 +1,3 @@ +export default async function context(req, res) { + res.json({ req, res }); +} diff --git a/cypress/fixtures/pages/getServerSideProps/context.js b/cypress/fixtures/pages/getServerSideProps/context.js new file mode 100644 index 0000000..76d4b09 --- /dev/null +++ b/cypress/fixtures/pages/getServerSideProps/context.js @@ -0,0 +1,11 @@ +const Context = ({ context }) => <pre>{JSON.stringify(context, 2, " ")}</pre>; + +export const getServerSideProps = async (context) => { + return { + props: { + context, + }, + }; +}; + +export default Context; diff --git a/cypress/integration/default_spec.js b/cypress/integration/default_spec.js index 4df81ee..08dcaa6 100644 --- a/cypress/integration/default_spec.js +++ b/cypress/integration/default_spec.js @@ -136,6 +136,31 @@ describe("getInitialProps", () => { }); describe("getServerSideProps", () => { + it("exposes function context on the req object", () => { + cy.visit("/getServerSideProps/context"); + + cy.get("pre") + .first() + .then((json) => { + const { + req: { + netlifyFunction: { event, context }, + }, + } = JSON.parse(json.html()); + + expect(event).to.have.property("path", "/getServerSideProps/context"); + expect(event).to.have.property("httpMethod", "GET"); + expect(event).to.have.property("headers"); + expect(event).to.have.property("multiValueHeaders"); + expect(event).to.have.property("isBase64Encoded"); + expect(context.done).to.be.undefined; + expect(context.getRemainingTimeInMillis).to.be.undefined; + expect(context).to.have.property("awsRequestId"); + expect(context).to.have.property("callbackWaitsForEmptyEventLoop"); + expect(context).to.have.property("clientContext"); + }); + }); + context("with static route", () => { it("loads TV shows", () => { cy.visit("/getServerSideProps/static"); @@ -534,6 +559,27 @@ describe("API endpoint", () => { cy.get("h1").should("contain", "Show #999"); cy.get("p").should("contain", "Flash Gordon"); }); + + it("exposes function context on the req object", () => { + cy.request("/api/context").then((response) => { + const { + req: { + netlifyFunction: { event, context }, + }, + } = response.body; + + expect(event).to.have.property("path", "/api/context"); + expect(event).to.have.property("httpMethod", "GET"); + expect(event).to.have.property("headers"); + expect(event).to.have.property("multiValueHeaders"); + expect(event).to.have.property("isBase64Encoded"); + expect(context.done).to.be.undefined; + expect(context.getRemainingTimeInMillis).to.be.undefined; + expect(context).to.have.property("awsRequestId"); + expect(context).to.have.property("callbackWaitsForEmptyEventLoop"); + expect(context).to.have.property("clientContext"); + }); + }); }); describe("Preview Mode", () => { diff --git a/lib/templates/createRequestObject.js b/lib/templates/createRequestObject.js index 39ce1d6..04e70a8 100644 --- a/lib/templates/createRequestObject.js +++ b/lib/templates/createRequestObject.js @@ -6,7 +6,7 @@ const http = require("http"); // 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 createRequestObject = ({ event, context }) => { const { requestContext = {}, path = "", @@ -52,6 +52,14 @@ const createRequestObject = ({ event }) => { req.rawHeaders = []; req.headers = {}; + // Expose Netlify Function event and callback on request object. + // This makes it possible to access the clientContext, for example. + // See: https://github.com/netlify/next-on-netlify/issues/20 + // It also allows users to change the behavior of waiting for empty event + // loop. + // See: https://github.com/netlify/next-on-netlify/issues/66#issuecomment-719988804 + req.netlifyFunction = { event, context }; + for (const key of Object.keys(multiValueHeaders)) { for (const value of multiValueHeaders[key]) { req.rawHeaders.push(key); diff --git a/lib/templates/netlifyFunction.js b/lib/templates/netlifyFunction.js index 96b72ee..6271157 100644 --- a/lib/templates/netlifyFunction.js +++ b/lib/templates/netlifyFunction.js @@ -16,9 +16,7 @@ exports.handler = async (event, context, callback) => { console.log("[request]", path); // Render the Next.js page - const response = await renderNextPage({ - ...event, - }); + const response = await renderNextPage({ event, context }); // Convert header values to string. Netlify does not support integers as // header values. See: https://github.com/netlify/cli/issues/451 diff --git a/lib/templates/renderNextPage.js b/lib/templates/renderNextPage.js index 38d3f13..742c54a 100644 --- a/lib/templates/renderNextPage.js +++ b/lib/templates/renderNextPage.js @@ -4,14 +4,14 @@ const createRequestObject = require("./createRequestObject"); const createResponseObject = require("./createResponseObject"); // Render the Next.js page -const renderNextPage = (event) => { +const renderNextPage = ({ event, context }) => { // 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 req = createRequestObject({ event, context }); const res = createResponseObject({ onResEnd: (response) => resolve(response), }); From 356046e2b69506c27126b36d677b8227d07453c5 Mon Sep 17 00:00:00 2001 From: Finn Woelm <public@finnwoelm.com> Date: Tue, 15 Dec 2020 11:37:46 +0800 Subject: [PATCH 3/5] Cypress: Test that user can modify callbackWaitsForEmptyEventLoop Test that users can modify the Netlify Function's callbackWaitsForEmptyEventLoop behavior via the newly exposed function event and context objects. When callbackWaitsForEmptyEventLoop is true (default), the function does not finish until all async processes and timeouts are completed (or cleared). The user can set this to false to not wait for other processes to finish. --- .../wait-on-empty-event-loop/[wait].js | 17 +++++++++++++++++ cypress/integration/default_spec.js | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js diff --git a/cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js b/cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js new file mode 100644 index 0000000..9b84f47 --- /dev/null +++ b/cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js @@ -0,0 +1,17 @@ +const WaitForEmptyEventLoop = () => <p>Successfully rendered page!</p>; + +export const getServerSideProps = async ({ params, req }) => { + // Set up long-running process + const timeout = setTimeout(() => {}, 100000); + + // Set behavior of whether to wait for empty event loop + const wait = String(params.wait).toLowerCase() === "true"; + const { context: functionContext } = req.netlifyFunction; + functionContext.callbackWaitsForEmptyEventLoop = wait; + + return { + props: {}, + }; +}; + +export default WaitForEmptyEventLoop; diff --git a/cypress/integration/default_spec.js b/cypress/integration/default_spec.js index 08dcaa6..cc9b8ba 100644 --- a/cypress/integration/default_spec.js +++ b/cypress/integration/default_spec.js @@ -161,6 +161,24 @@ describe("getServerSideProps", () => { }); }); + it("can modify the callbackWaitsForEmptyEventLoop behavior", () => { + // netlify dev never waits on empty event loop + if (Cypress.env("DEPLOY") !== "local") { + cy.request({ + url: "/getServerSideProps/wait-on-empty-event-loop/true", + failOnStatusCode: false, + // Functions time out after 10s, so we need to wait a bit + timeout: 15000, + }).then((response) => { + expect(response.status).to.eq(502); + expect(response.body).to.contain("Task timed out"); + }); + } + + cy.visit("/getServerSideProps/wait-on-empty-event-loop/false"); + cy.get("p").should("contain", "Successfully rendered page!"); + }); + context("with static route", () => { it("loads TV shows", () => { cy.visit("/getServerSideProps/static"); From 1ba11f86463bb22d6e6823f1353b4ccbbb3090a6 Mon Sep 17 00:00:00 2001 From: Finn Woelm <public@finnwoelm.com> Date: Tue, 15 Dec 2020 11:50:25 +0800 Subject: [PATCH 4/5] README: Explain how to use Netlify Identity with next-on-netlify Add instructions, details, and an example for using Netlify Identity with next-on-netlify. --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index c3252e6..5a2406c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The plugin can be found on [npm here](https://www.npmjs.com/package/@netlify/plu - [Preview Locally](#preview-locally) - [Custom Netlify Redirects](#custom-netlify-redirects) - [Custom Netlify Functions](#custom-netlify-functions) + - [Using Netlify Identity](#using-netlify-identity) - [Caveats](#caveats) - [Fallbacks for Pages with `getStaticPaths`](#fallbacks-for-pages-with-getstaticpaths) - [Credits](#credits) @@ -199,6 +200,52 @@ Currently, there is no support for redirects set in your `netlify.toml` file. `next-on-netlify` creates one Netlify Function for each of your SSR pages and API endpoints. It is currently not possible to create custom Netlify Functions. This feature is on our list to do. +#### Using Netlify Identity + +You can use [Netlify Identity](https://docs.netlify.com/visitor-access/identity/) with `next-on-netlify`. For all pages with server-side rendering (getInitialProps*, getServerSideProps, and API routes), you can access the [clientContext object](https://docs.netlify.com/functions/functions-and-identity/#access-identity-info-via-clientcontext) via the `req` parameter. + +For example: + +```js +const Page = () => <p>Hello World!</p>; + +export const getServerSideProps = async ({ req }) => { + // Get event and context from Netlify Function + const { + netlifyFunction: { event, context }, + } = req; + + // Access Netlify identity + const { identity, user } = context.clientContext; + + return { + props: {}, + }; +}; + +export default Page; +``` + +To access Netlify Identity from pages without server-side rendering, you can create a [Next API route](https://nextjs.org/docs/api-routes/introduction) that performs identity-related logic: + +```js +export default async function getUser(req, res) { + // Get event and context from Netlify Function + const { + netlifyFunction: { event, context }, + } = req; + + // Access Netlify identity + const { user } = context.clientContext; + + // Respond with user object + res.json({ user }); +} +``` + +\* Note that pages using getInitialProps are only server-side rendered on initial page load and not when the user navigates client-side between pages. + + ## Caveats ### Fallbacks for Pages with `getStaticPaths` From 5fa6c5a2f6fa9759a8c31dfd1e2c087a7050842d Mon Sep 17 00:00:00 2001 From: Finn Woelm <public@finnwoelm.com> Date: Sat, 19 Dec 2020 11:08:35 +0800 Subject: [PATCH 5/5] Expose netlifyFunction params as netlifyFunctionParams Expose event and context of Netlify Function in Next.js pages and API routes as `netlifyFunctionParams` (rather than `netlifyFunction`). The new name is clearer, since we're just making the parameters of the function available and not the function itself. --- README.md | 4 ++-- .../getServerSideProps/wait-on-empty-event-loop/[wait].js | 2 +- cypress/integration/default_spec.js | 4 ++-- lib/templates/createRequestObject.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5a2406c..e9d6e05 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ const Page = () => <p>Hello World!</p>; export const getServerSideProps = async ({ req }) => { // Get event and context from Netlify Function const { - netlifyFunction: { event, context }, + netlifyFunctionParams: { event, context }, } = req; // Access Netlify identity @@ -232,7 +232,7 @@ To access Netlify Identity from pages without server-side rendering, you can cre export default async function getUser(req, res) { // Get event and context from Netlify Function const { - netlifyFunction: { event, context }, + netlifyFunctionParams: { event, context }, } = req; // Access Netlify identity diff --git a/cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js b/cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js index 9b84f47..1d6e22d 100644 --- a/cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js +++ b/cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js @@ -6,7 +6,7 @@ export const getServerSideProps = async ({ params, req }) => { // Set behavior of whether to wait for empty event loop const wait = String(params.wait).toLowerCase() === "true"; - const { context: functionContext } = req.netlifyFunction; + const { context: functionContext } = req.netlifyFunctionParams; functionContext.callbackWaitsForEmptyEventLoop = wait; return { diff --git a/cypress/integration/default_spec.js b/cypress/integration/default_spec.js index cc9b8ba..87304bd 100644 --- a/cypress/integration/default_spec.js +++ b/cypress/integration/default_spec.js @@ -144,7 +144,7 @@ describe("getServerSideProps", () => { .then((json) => { const { req: { - netlifyFunction: { event, context }, + netlifyFunctionParams: { event, context }, }, } = JSON.parse(json.html()); @@ -582,7 +582,7 @@ describe("API endpoint", () => { cy.request("/api/context").then((response) => { const { req: { - netlifyFunction: { event, context }, + netlifyFunctionParams: { event, context }, }, } = response.body; diff --git a/lib/templates/createRequestObject.js b/lib/templates/createRequestObject.js index 04e70a8..06e5cf4 100644 --- a/lib/templates/createRequestObject.js +++ b/lib/templates/createRequestObject.js @@ -58,7 +58,7 @@ const createRequestObject = ({ event, context }) => { // It also allows users to change the behavior of waiting for empty event // loop. // See: https://github.com/netlify/next-on-netlify/issues/66#issuecomment-719988804 - req.netlifyFunction = { event, context }; + req.netlifyFunctionParams = { event, context }; for (const key of Object.keys(multiValueHeaders)) { for (const value of multiValueHeaders[key]) {