Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Preview Mode #562

Merged
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 {
Copy link
Collaborator

@dphang dphang Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe some logging (at info level?) is useful here, I added it for 500 page as well, not sure if it's needed for this though

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a console.error, similar to the logging for the 500 response.

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 ({
}
Copy link
Collaborator

@dphang dphang Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be moved into the else block below just before the await page.render(req, res), so we don't have to check whether it is error page every time

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean we know for a fact that data requests will not result in a possible error page?

Copy link
Collaborator

@dphang dphang Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I guess unmatched data request can still go to pages/_error.js, maybe still need this at the start


// 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