Skip to content

Commit

Permalink
feat(lambda-at-edge, next-aws-cloudfront): support Preview Mode (#562)
Browse files Browse the repository at this point in the history
* Added preview mode support.

* Rebased and updated tests.

* - Added rollup and bundle default handler
- Fixed expected uri in origin response.
- Added util to rollup externals as it was breaking.

* Rebased and updated tests

* Updated another test

* - Add API Lambda to rollup
- Fixed some erroneous logic in the router, since it's now being used in the response handler
- Mocked console errors in tests

* Revert some changes which were superseded by @dphang's changes.

* Fixed integration test

* Updated tests

* Added explicit return to getPreviewCookies

* Added error logging to preview token verification catch block.

* Added preview path test case to redirect

* Optimised getPreviewCookies function.
  • Loading branch information
thchia authored Sep 2, 2020
1 parent 84f2d6f commit 5e1ea38
Show file tree
Hide file tree
Showing 22 changed files with 551 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ describe("Response Tests", () => {
});
});

it("writeHead can be chained", () => {
const { res, responsePromise } = create({
request: { uri: "/", headers: {} }
});

res.writeHead(200, { "Content-Length": "1234" }).end();

return responsePromise;
});

it("setHeader (multiple headers with same name)", () => {
const { res, responsePromise } = create({
request: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ const handler = (event) => {
if (headers) {
res.headers = Object.assign(res.headers, headers);
}
return res;
};
res.write = (chunk) => {
if (!response.body) {
Expand Down
2 changes: 1 addition & 1 deletion packages/libs/lambda-at-edge/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
!tests/**
dist/
dist/
13 changes: 11 additions & 2 deletions packages/libs/lambda-at-edge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"types": "dist/index.d.ts",
"scripts": {
"prepare": "yarn build",
"build": "tsc -p tsconfig.build.json"
"build": "rollup --config && tsc -p tsconfig.build.json"
},
"files": [
"dist"
Expand All @@ -31,18 +31,27 @@
},
"homepage": "https://github.com/danielcondemarin/serverless-next.js#readme",
"devDependencies": {
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^5.0.2",
"@types/aws-lambda": "^8.10.57",
"@types/cookie": "^0.4.0",
"@types/execa": "^2.0.0",
"@types/fs-extra": "^9.0.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/node": "^14.0.14",
"@types/path-to-regexp": "^1.7.0",
"path-to-regexp": "^6.1.0",
"rollup": "^2.26.6",
"rollup-plugin-node-externals": "^2.2.0",
"ts-loader": "^7.0.5",
"typescript": "^3.9.6"
},
"dependencies": {
"@zeit/node-file-trace": "^0.6.5",
"cookie": "^0.4.1",
"execa": "^4.0.2",
"fs-extra": "^9.0.1",
"path-to-regexp": "^6.1.0"
"jsonwebtoken": "^8.5.1"
}
}
32 changes: 32 additions & 0 deletions packages/libs/lambda-at-edge/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import externals from "rollup-plugin-node-externals";

const LOCAL_EXTERNALS = [
"./manifest.json",
"./routes-manifest.json",
"./prerender-manifest.json"
];
const NPM_EXTERNALS = ["aws-lambda", "aws-sdk/clients/s3"];

const generateConfig = (filename) => ({
input: `./src/${filename}.ts`,
output: {
file: `./dist/${filename}.js`,
format: "cjs"
},
plugins: [
commonjs(),
externals({
exclude: "@sls-next/next-aws-cloudfront"
}),
nodeResolve(),
typescript({
tsconfig: "tsconfig.bundle.json"
})
],
external: [...NPM_EXTERNALS, ...LOCAL_EXTERNALS]
});

export default ["default-handler", "api-handler"].map(generateConfig);
16 changes: 0 additions & 16 deletions packages/libs/lambda-at-edge/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,6 @@ class Builder {
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, "manifest.json"),
buildManifest
),
fse.copy(
require.resolve("@sls-next/next-aws-cloudfront"),
join(
this.outputDir,
DEFAULT_LAMBDA_CODE_DIR,
"node_modules/@sls-next/next-aws-cloudfront/index.js"
)
),
fse.copy(
join(this.serverlessDir, "pages"),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, "pages"),
Expand Down Expand Up @@ -245,14 +237,6 @@ class Builder {
require.resolve("@sls-next/lambda-at-edge/dist/api-handler.js"),
join(this.outputDir, API_LAMBDA_CODE_DIR, "index.js")
),
fse.copy(
require.resolve("@sls-next/next-aws-cloudfront"),
join(
this.outputDir,
API_LAMBDA_CODE_DIR,
"node_modules/@sls-next/next-aws-cloudfront/index.js"
)
),
fse.copy(
join(this.serverlessDir, "pages/api"),
join(this.outputDir, API_LAMBDA_CODE_DIR, "pages/api")
Expand Down
65 changes: 61 additions & 4 deletions packages/libs/lambda-at-edge/src/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Manifest from "./manifest.json";
// @ts-ignore
import { basePath } from "./routes-manifest.json";
import lambdaAtEdgeCompat from "@sls-next/next-aws-cloudfront";
import cookie from "cookie";
import {
CloudFrontRequest,
CloudFrontS3Origin,
Expand All @@ -21,6 +22,29 @@ import {
import S3 from "aws-sdk/clients/s3";
import { performance } from "perf_hooks";
import { ServerResponse } from "http";
import jsonwebtoken from "jsonwebtoken";

const NEXT_PREVIEW_DATA_COOKIE = "__next_preview_data";
const NEXT_PRERENDER_BYPASS_COOKIE = "__prerender_bypass";
const defaultPreviewCookies = {
[NEXT_PRERENDER_BYPASS_COOKIE]: "",
[NEXT_PREVIEW_DATA_COOKIE]: ""
};

const getPreviewCookies = (request: CloudFrontRequest) => {
const targetCookie = request.headers.cookie || [];
return targetCookie.reduce((previewCookies, cookieObj) => {
const cookieValue = cookie.parse(cookieObj.value);
if (
cookieValue[NEXT_PREVIEW_DATA_COOKIE] &&
cookieValue[NEXT_PRERENDER_BYPASS_COOKIE]
) {
return cookieValue as typeof defaultPreviewCookies;
} else {
return previewCookies;
}
}, defaultPreviewCookies);
};

const perfLogger = (logLambdaExecutionTimes: boolean): PerfLogger => {
if (logLambdaExecutionTimes) {
Expand Down Expand Up @@ -211,11 +235,34 @@ const handleOriginRequest = async ({
const normalisedS3DomainName = normaliseS3OriginDomain(s3Origin);
const hasFallback = hasFallbackForUri(uri, prerenderManifest);
const { now, log } = perfLogger(manifest.logLambdaExecutionTimes);
const previewCookies = getPreviewCookies(request);
const isPreviewRequest =
previewCookies[NEXT_PREVIEW_DATA_COOKIE] &&
previewCookies[NEXT_PRERENDER_BYPASS_COOKIE];

if (isPreviewRequest) {
try {
jsonwebtoken.verify(
previewCookies[NEXT_PREVIEW_DATA_COOKIE],
prerenderManifest.preview.previewModeSigningKey
);
} catch (e) {
console.error("Failed preview mode verification for URI:", request.uri);
return {
status: "403",
statusDescription: "Forbidden"
};
}
}

s3Origin.domainName = normalisedS3DomainName;

// Check if we can serve request from S3
S3Check: if (isHTMLPage || isPublicFile || hasFallback || isDataReq) {
S3Check: if (
isPublicFile ||
(isHTMLPage && !isPreviewRequest) ||
(hasFallback && !isPreviewRequest) ||
(isDataReq && !isPreviewRequest)
) {
if (isHTMLPage || hasFallback) {
s3Origin.path = `${basePath}/static-pages`;
const pageName = uri === "/" ? "/index" : uri;
Expand Down Expand Up @@ -249,7 +296,7 @@ const handleOriginRequest = async ({

const pagePath = router(manifest)(uri);

if (pagePath.endsWith(".html")) {
if (pagePath.endsWith(".html") && !isPreviewRequest) {
s3Origin.path = `${basePath}/static-pages`;
request.uri = pagePath.replace("pages", "");
addS3HostHeader(request, normalisedS3DomainName);
Expand All @@ -272,7 +319,17 @@ const handleOriginRequest = async ({
}

// Render page
await page.render(req, res);
if (isDataReq) {
const { renderOpts } = await page.renderReqToHTML(
req,
res,
"passthrough"
);
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(renderOpts.pageData));
} else {
await page.render(req, res);
}
} catch (error) {
// Set status to 500 so _error.js will render a 500 page
console.error(
Expand Down
22 changes: 2 additions & 20 deletions packages/libs/lambda-at-edge/tests/build/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe("Builder Tests", () => {

describe("Default Handler Artefact Files", () => {
it("copies build files", async () => {
expect.assertions(7);
expect.assertions(6);

const files = await fse.readdir(
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}`)
Expand All @@ -170,24 +170,15 @@ describe("Builder Tests", () => {
const apiDirExists = await fse.pathExists(
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/pages/api`)
);
const compatLayerIncluded = await fse.pathExists(
join(
outputDir,
`${DEFAULT_LAMBDA_CODE_DIR}/node_modules/@sls-next/next-aws-cloudfront/index.js`
)
);

expect(files).toEqual([
"index.js",
"manifest.json",
"node_modules",
"pages",
"prerender-manifest.json",
"routes-manifest.json"
]);

expect(compatLayerIncluded).toEqual(true);

// api pages should not be included in the default lambda
expect(apiDirExists).toEqual(false);

Expand All @@ -203,7 +194,7 @@ describe("Builder Tests", () => {

describe("API Handler Artefact Files", () => {
it("copies build files", async () => {
expect.assertions(3);
expect.assertions(2);

const files = await fse.readdir(
join(outputDir, `${API_LAMBDA_CODE_DIR}`)
Expand All @@ -212,18 +203,9 @@ describe("Builder Tests", () => {
join(outputDir, `${API_LAMBDA_CODE_DIR}/pages`)
);

const compatLayerIncluded = await fse.pathExists(
join(
outputDir,
`${API_LAMBDA_CODE_DIR}/node_modules/@sls-next/next-aws-cloudfront/index.js`
)
);

expect(compatLayerIncluded).toEqual(true);
expect(files).toEqual([
"index.js",
"manifest.json",
"node_modules",
"pages",
"routes-manifest.json"
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"/api/getCustomers": "pages/api/getCustomers.js",
"/_error": "pages/_error.js",
"/erroredPage": "pages/erroredPage.js",
"/404": "pages/404.html"
"/404": "pages/404.html",
"/preview": "pages/preview.js"
}
},
"html": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"/api/getCustomers": "pages/api/getCustomers.js",
"/_error": "pages/_error.js",
"/erroredPage": "pages/erroredPage.js",
"/404": "pages/404.html"
"/404": "pages/404.html",
"/preview": "pages/preview.js"
}
},
"html": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ const mockPageRequire = (mockPagePath: string): void => {
};

describe("Lambda@Edge", () => {
let consoleWarnSpy: jest.SpyInstance;

beforeEach(() => {
consoleWarnSpy = jest.spyOn(console, "error").mockReturnValue();
});

afterEach(() => {
consoleWarnSpy.mockRestore();
});
describe.each`
trailingSlash
${false}
Expand Down Expand Up @@ -537,7 +546,11 @@ describe("Lambda@Edge", () => {
const body = response.body as string;
const decodedBody = new Buffer(body, "base64").toString("utf8");

expect(decodedBody).toEqual("pages/_error.js - 404");
expect(decodedBody).toEqual(
JSON.stringify({
page: "pages/_error.js - 404"
})
);
expect(response.status).toEqual("404");
}
);
Expand Down
Loading

0 comments on commit 5e1ea38

Please sign in to comment.