Skip to content

Commit

Permalink
feat(serverless-component,lambda-at-edge): getServerSideProps support (
Browse files Browse the repository at this point in the history
  • Loading branch information
danielcondemarin authored and sclaughl committed Jul 16, 2020
1 parent d47964f commit d73251c
Show file tree
Hide file tree
Showing 23 changed files with 142 additions and 30 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"/.next/",
"/node_modules/",
"/fixtures/",
"/fixture/",
"/examples/",
"/integration/"
],
Expand Down
5 changes: 5 additions & 0 deletions packages/lambda-at-edge/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,12 @@ class Builder {
}> {
const pagesManifest = await this.readPagesManifest();

const buildId = await fse.readFile(
path.join(this.dotNextDir, "BUILD_ID"),
"utf-8"
);
const defaultBuildManifest: OriginRequestDefaultHandlerManifest = {
buildId,
pages: {
ssr: {
dynamic: {},
Expand Down
33 changes: 25 additions & 8 deletions packages/lambda-at-edge/src/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,35 @@ const addS3HostHeader = (
req.headers["host"] = [{ key: "host", value: s3DomainName }];
};

const isDataRequest = (uri: string): boolean => uri.startsWith("/_next/data");

const router = (
manifest: OriginRequestDefaultHandlerManifest
): ((path: string) => string) => {
): ((uri: string) => string) => {
const {
pages: { ssr, html }
} = manifest;

const allDynamicRoutes = { ...ssr.dynamic, ...html.dynamic };

return (path: string): string => {
if (ssr.nonDynamic[path]) {
return ssr.nonDynamic[path];
return (uri: string): string => {
let normalisedUri = uri;

if (isDataRequest(uri)) {
normalisedUri = uri
.replace(`/_next/data/${manifest.buildId}`, "")
.replace(".json", "");
}

if (ssr.nonDynamic[normalisedUri]) {
return ssr.nonDynamic[normalisedUri];
}

for (const route in allDynamicRoutes) {
const { file, regex } = allDynamicRoutes[route];

const re = new RegExp(regex, "i");
const pathMatchesRoute = re.test(path);
const pathMatchesRoute = re.test(normalisedUri);

if (pathMatchesRoute) {
return file;
Expand Down Expand Up @@ -92,11 +102,18 @@ export const handler = async (
return request;
}

const { req, res, responsePromise } = lambdaAtEdgeCompat(event.Records[0].cf);

// eslint-disable-next-line
const page = require(`./${pagePath}`);
page.render(req, res);

const { req, res, responsePromise } = lambdaAtEdgeCompat(event.Records[0].cf);

if (isDataRequest(uri)) {
const { renderOpts } = await page.renderReqToHTML(req, res, "passthrough");
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(renderOpts.pageData));
} else {
page.render(req, res);
}

return responsePromise;
};
1 change: 1 addition & 0 deletions packages/lambda-at-edge/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type OriginRequestApiHandlerManifest = {
};

export type OriginRequestDefaultHandlerManifest = {
buildId: string;
pages: {
ssr: {
dynamic: DynamicPageKeyValue;
Expand Down
4 changes: 3 additions & 1 deletion packages/lambda-at-edge/tests/build/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fse from "fs-extra";
import execa from "execa";
import Builder from "../../src/build";
import { DEFAULT_LAMBDA_CODE_DIR, API_LAMBDA_CODE_DIR } from "../../src/build";
import { cleanupDir } from "../test-utils";
import { cleanupDir, removeNewLineChars } from "../test-utils";
import {
OriginRequestDefaultHandlerManifest,
OriginRequestApiHandlerManifest
Expand Down Expand Up @@ -71,13 +71,15 @@ describe("Builder Tests", () => {
describe("Default Handler Manifest", () => {
it("adds full manifest", () => {
const {
buildId,
publicFiles,
pages: {
ssr: { dynamic, nonDynamic },
html
}
} = defaultBuildManifest;

expect(removeNewLineChars(buildId)).toEqual("test-build-id");
expect(dynamic).toEqual({
"/:root": {
file: "pages/[root].js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-build-id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { target: 'serverless' };
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
{
"cloudFrontOrigins": {
"ssrApi": {
"domainName": "ssr-api.execute-api.us-east-1.amazonaws.com"
},
"staticOrigin": {
"domainName": "my-bucket.s3.amazonaws.com"
}
},
"buildId": "build-id",
"pages": {
"ssr": {
"dynamic": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,12 @@ describe("Lambda@Edge", () => {
});
});

describe("SSR Pages routing", () => {
describe("SSR pages routing", () => {
it.each`
path | expectedPage
${"/abc"} | ${"pages/[root].js"}
${"/blog/foo"} | ${"pages/blog/[id].js"}
${"/customers"} | ${"pages/customers/index.js"}
${"/customers/superman"} | ${"pages/customers/[customer].js"}
${"/customers/superman/howtofly"} | ${"pages/customers/[customer]/[post].js"}
${"/customers/superman/profile"} | ${"pages/customers/[customer]/profile.js"}
Expand Down Expand Up @@ -137,6 +138,42 @@ describe("Lambda@Edge", () => {
}
);
});

describe("Data Requests", () => {
it.each`
path | expectedPage
${"/_next/data/build-id/customers.json"} | ${"pages/customers/index.js"}
${"/_next/data/build-id/customers/superman.json"} | ${"pages/customers/[customer].js"}
${"/_next/data/build-id/customers/superman/profile.json"} | ${"pages/customers/[customer]/profile.js"}
`("serves json data for path $path", async ({ path, expectedPage }) => {
const event = createCloudFrontEvent({
uri: path,
host: "mydistribution.cloudfront.net",
origin: {
s3: {
domainName: "my-bucket.amazonaws.com"
}
}
});

mockPageRequire(expectedPage);

const response = await handler(event);

const cfResponse = response as CloudFrontResultResponse;
const decodedBody = new Buffer(cfResponse.body, "base64").toString(
"utf8"
);

expect(cfResponse.headers["content-type"][0].value).toEqual(
"application/json"
);
expect(JSON.parse(decodedBody)).toEqual({
page: expectedPage
});
expect(cfResponse.status).toEqual(200);
});
});
});

it("renders 404 page if request path can't be matched to any page / api routes", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-build-id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { target: 'serverless' };
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getNextBinary } from "../../test-utils";
import { getNextBinary, removeNewLineChars } from "../../test-utils";
import os from "os";
import path from "path";
import Builder from "../../../src/build";
Expand Down Expand Up @@ -33,9 +33,6 @@ describe("With Empty Next Config Build", () => {

it("keeps user next.config.js intact after build", async () => {
const nextConfigPath = path.join(fixtureDir, "next.config.js");
const removeNewLineChars = (text: string): string =>
text.replace(/(\r\n|\n|\r)/gm, "");

expect(await pathExists(nextConfigPath)).toBe(true);
expect(removeNewLineChars(await readFile(nextConfigPath, "utf-8"))).toEqual(
"module.exports = () => ({});"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getNextBinary } from "../../test-utils";
import { getNextBinary, removeNewLineChars } from "../../test-utils";
import os from "os";
import path from "path";
import Builder from "../../../src/build";
Expand Down Expand Up @@ -38,8 +38,6 @@ describe("With Next Build Failure", () => {

it("keeps user next.config.js intact after build failure", async () => {
const nextConfigPath = path.join(fixtureDir, "next.config.js");
const removeNewLineChars = (text: string): string =>
text.replace(/(\r\n|\n|\r)/gm, "");

expect(await pathExists(nextConfigPath)).toBe(true);
expect(removeNewLineChars(await readFile(nextConfigPath, "utf-8"))).toEqual(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-build-id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { target: 'serverless' };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-build-id
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
module.exports = {
render: (req, res) => {
res.end("pages/customers/[customer].js");
},
renderReqToHTML: (req, res) => {
return Promise.resolve({
renderOpts: {
pageData: {
page: "pages/customers/[customer].js"
}
}
});
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
module.exports = {
render: (req, res) => {
res.end("pages/customers/[customer]/profile.js");
},
renderReqToHTML: (req, res) => {
return Promise.resolve({
renderOpts: {
pageData: {
page: "pages/customers/[customer]/profile.js"
}
}
});
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
module.exports = {
render: (req, res) => {
res.end("pages/customers/index.js");
},
renderReqToHTML: (req, res) => {
return {
renderOpts: {
pageData: {
page: "pages/customers/index.js"
}
}
};
}
};
3 changes: 3 additions & 0 deletions packages/lambda-at-edge/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const cleanupDir = (dir: string): Promise<void> => {
return remove(dir);
};

export const removeNewLineChars = (text: string): string =>
text.replace(/(\r\n|\n|\r)/gm, "");

export const getNextBinary = (): string =>
path.join(require.resolve("next"), "../../../../.bin/next");

Expand Down
12 changes: 10 additions & 2 deletions packages/serverless-component/__tests__/custom-inputs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,10 +605,18 @@ describe("Custom inputs", () => {
{
pathPatterns: {
...customPageCacheBehaviours,
"_next/*": {
...customPageCacheBehaviours["_next/*"],
"_next/static/*": {
...customPageCacheBehaviours["_next/static/*"],
ttl: 86400
},
"_next/data/*": {
ttl: 0,
allowedHttpMethods: ["HEAD", "GET"],
"lambda@edge": {
"origin-request":
"arn:aws:lambda:us-east-1:123456789012:function:my-func:v1"
}
},
"api/*": {
ttl: 0,
...expectedApiCacheBehaviour
Expand Down
10 changes: 9 additions & 1 deletion packages/serverless-component/__tests__/deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,17 @@ describe("deploy tests", () => {
url: "http://bucket-xyz.s3.amazonaws.com",
private: true,
pathPatterns: {
"_next/*": {
"_next/static/*": {
ttl: 86400
},
"_next/data/*": {
ttl: 0,
allowedHttpMethods: ["HEAD", "GET"],
"lambda@edge": {
"origin-request":
"arn:aws:lambda:us-east-1:123456789012:function:default-cachebehavior-func:v1"
}
},
"static/*": {
ttl: 86400
},
Expand Down
12 changes: 10 additions & 2 deletions packages/serverless-component/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class NextjsComponent extends Component {
// there wont be pages for these paths for this so we can remove them
stillToMatch.delete("api/*");
stillToMatch.delete("static/*");
stillToMatch.delete("_next/*");
stillToMatch.delete("_next/static/*");
// check for other api like paths
for (const path of stillToMatch) {
if (/^(\/?api\/.*|\/?api)$/.test(path)) {
Expand Down Expand Up @@ -227,7 +227,7 @@ class NextjsComponent extends Component {
url: bucketUrl,
private: true,
pathPatterns: {
"_next/*": {
"_next/static/*": {
ttl: 86400
},
"static/*": {
Expand Down Expand Up @@ -394,6 +394,14 @@ class NextjsComponent extends Component {
};
});

cloudFrontOrigins[0].pathPatterns["_next/data/*"] = {
ttl: 0,
allowedHttpMethods: ["HEAD", "GET"],
"lambda@edge": {
"origin-request": `${defaultEdgeLambdaOutputs.arn}:${defaultEdgeLambdaPublishOutputs.version}`
}
};

// make sure that origin-response is not set.
// this is reserved for serverless-next.js usage
let defaultLambdaAtEdgeConfig = {
Expand Down

0 comments on commit d73251c

Please sign in to comment.