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

Incremental static regeneration #1028

Merged
merged 61 commits into from
May 19, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
31f313c
wip: initial proof-of-concept incremental static regeneration
kirkness Apr 28, 2021
35cf5ff
fix: dont ts build the regeneration handler
kirkness Apr 28, 2021
65ba637
fix: dynamic paths should regenerate
kirkness Apr 28, 2021
0ee2f55
add Expires header to the regenerated S3 objects
kirkness Apr 29, 2021
6f1ce93
Merge branch 'master' into feature/incremental-static-regeneration
kirkness Apr 29, 2021
fc8bd38
use expires header rather than last-modified and a small code tidy
kirkness Apr 29, 2021
8a0d5ff
revalidation relies on the expires header
kirkness Apr 29, 2021
67cda68
fix fallback isr fallback behaviour
kirkness Apr 30, 2021
16fe62b
Merge branch 'master' into feature/incremental-static-regeneration
kirkness Apr 30, 2021
26ed898
update cdk snapshot
kirkness Apr 30, 2021
ef7d03b
remove sqs and use lambda async invocation
kirkness May 2, 2021
f4548b3
adds e2e test and serverless infrastructure
kirkness May 2, 2021
b52d762
remove uneeded isNaN check
kirkness May 2, 2021
3dc480f
remove prerenderManifest variable from handler
kirkness May 2, 2021
127698b
add e2e test for isr page with getStaticPaths
kirkness May 2, 2021
7eb5bb4
revert serverless change
kirkness May 2, 2021
a3eb635
add tests to regeneration handler
kirkness May 4, 2021
30d0e51
add tests for getStaticRegenerationResponse
kirkness May 4, 2021
c03105f
create the regeneration lambd ain the same region as the bucket
kirkness May 4, 2021
6adf890
revery async lambda changes and use SQS
kirkness May 4, 2021
9dbfb27
add sqs creation to serverless components
kirkness May 4, 2021
f0650f2
fix conflicts
kirkness May 4, 2021
57b403b
update snapshot andd mocks
kirkness May 4, 2021
43f064a
fix regeneration tests with sqs event
kirkness May 5, 2021
db31e75
Merge branch 'master' into feature/incremental-static-regeneration
kirkness May 5, 2021
6f0f45c
minor test fixes
kirkness May 5, 2021
26c8449
Merge branch 'feature/incremental-static-regeneration' of https://git…
kirkness May 5, 2021
f397f50
fix all tests
kirkness May 5, 2021
631d8c0
add cache-control header tests to default-handler
kirkness May 5, 2021
5fc9807
add tests for triggerStaticRegeneration
kirkness May 5, 2021
b8cb485
remove removeHeader call
kirkness May 5, 2021
99ec48c
remove only test
kirkness May 5, 2021
93a0826
add tests for sqs component
kirkness May 5, 2021
10f9f67
add tests to unhappy-paths in triggerStaticRegeneration
kirkness May 5, 2021
cb5ad0b
add extra test cases to sqs component
kirkness May 5, 2021
f9806d2
add case for sqs delete
kirkness May 5, 2021
3552751
check assertions against sdk api calls
kirkness May 5, 2021
09f739f
Merge branch 'master' into feature/incremental-static-regeneration
kirkness May 5, 2021
41202f3
regenerate yarn lock files
kirkness May 8, 2021
d9417b8
Merge branch 'feature/incremental-static-regeneration' of https://git…
kirkness May 8, 2021
1f757d1
chore: resolve conflicts
kirkness May 11, 2021
fbe017e
docs: add ISR related deployment permissions
kirkness May 12, 2021
027e9c1
fix: update all yarn lock files in all e2e packages
kirkness May 12, 2021
855fc88
fix: include ready checks in e2e tests for isr
kirkness May 13, 2021
436b584
fix: redirect test in trailing slash e2e tests
kirkness May 13, 2021
90ce486
fix: include isr e2e path checks in test-utils
kirkness May 13, 2021
d546e02
Merge branch 'master' into feature/incremental-static-regeneration
dphang May 13, 2021
a3c3e36
fix: dont deploy a queue if we dont need one
kirkness May 14, 2021
738335e
fix: merge conflicts
kirkness May 14, 2021
bf8038a
fix: revert normalise uri call
kirkness May 16, 2021
e49eac4
fix: update snapshot
kirkness May 16, 2021
b13cb05
fix: update regeneration fallback test
kirkness May 16, 2021
aaa0916
fix: convert secs to millis
kirkness May 16, 2021
f104d19
fix: revert 404 case
kirkness May 16, 2021
bf6ceed
fix: revert 308 case
kirkness May 16, 2021
200f195
fix: revert 404 case
kirkness May 16, 2021
e2cae31
fix: set cache folder for more reliability(??)
kirkness May 17, 2021
f4e5218
fix: 404 redirect test, update post cypress bump
kirkness May 17, 2021
2cde26b
fix: resolve conflits
kirkness May 18, 2021
3c37c3f
fix: merge conflict
kirkness May 18, 2021
7586260
Merge branch 'master' into feature/incremental-static-regeneration
dphang May 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/libs/lambda-at-edge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "1.0.0-rc.3",
"@aws-sdk/client-sqs": "1.0.0-rc.3",
kirkness marked this conversation as resolved.
Show resolved Hide resolved
"@hapi/accept": "5.0.1",
"@vercel/nft": "^0.9.3",
"cookie": "^0.4.1",
Expand Down
4 changes: 3 additions & 1 deletion packages/libs/lambda-at-edge/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@ export default [
{ filename: "api-handler", minify: false },
{ filename: "api-handler", minify: true },
{ filename: "image-handler", minify: false },
{ filename: "image-handler", minify: true }
{ filename: "image-handler", minify: true },
{ filename: "regeneration-handler", minify: false },
{ filename: "regeneration-handler", minify: true }
].map(generateConfig);
59 changes: 51 additions & 8 deletions packages/libs/lambda-at-edge/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Job } from "@vercel/nft/out/node-file-trace";
export const DEFAULT_LAMBDA_CODE_DIR = "default-lambda";
export const API_LAMBDA_CODE_DIR = "api-lambda";
export const IMAGE_LAMBDA_CODE_DIR = "image-lambda";
export const REGENERATION_LAMBDA_CODE_DIR = "regeneration-lambda";
export const ASSETS_DIR = "assets";

type BuildOptions = {
Expand Down Expand Up @@ -230,7 +231,11 @@ class Builder {
* @param shouldMinify
*/
async processAndCopyHandler(
handlerType: "api-handler" | "default-handler" | "image-handler",
handlerType:
| "api-handler"
| "default-handler"
| "image-handler"
| "regeneration-handler",
destination: string,
shouldMinify: boolean
) {
Expand All @@ -245,9 +250,9 @@ class Builder {
await fse.copy(source, destination);
}

async buildDefaultLambda(
async copyTraces(
buildManifest: OriginRequestDefaultHandlerManifest
): Promise<void[]> {
): Promise<void> {
let copyTraces: Promise<void>[] = [];

if (this.buildOptions.useServerlessTraceTarget) {
Expand Down Expand Up @@ -284,7 +289,13 @@ class Builder {
);
}

let prerenderManifest = require(join(
await Promise.all(copyTraces);
}

async buildDefaultLambda(
buildManifest: OriginRequestDefaultHandlerManifest
): Promise<void[]> {
const prerenderManifest = require(join(
this.dotNextDir,
"prerender-manifest.json"
));
Expand All @@ -294,7 +305,7 @@ class Builder {
);

return Promise.all([
...copyTraces,
this.copyTraces(buildManifest),
this.processAndCopyHandler(
"default-handler",
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR),
Expand Down Expand Up @@ -414,6 +425,36 @@ class Builder {
]);
}

async buildRegenerationHandler(
buildManifest: OriginRequestDefaultHandlerManifest
): Promise<void> {
await Promise.all([
this.copyTraces(buildManifest),
this.processAndCopyHandler(
"regeneration-handler",
join(this.outputDir, REGENERATION_LAMBDA_CODE_DIR, "index.js"),
kirkness marked this conversation as resolved.
Show resolved Hide resolved
!!this.buildOptions.minifyHandlers
),
fse.copy(
join(this.serverlessDir, "pages"),
join(this.outputDir, REGENERATION_LAMBDA_CODE_DIR, "pages"),
{
filter: (file: string) => {
const isNotPrerenderedHTMLPage = path.extname(file) !== ".html";
const isNotStaticPropsJSONFile = path.extname(file) !== ".json";
const isNotApiPage = pathToPosix(file).indexOf("pages/api") === -1;

return (
isNotPrerenderedHTMLPage &&
isNotStaticPropsJSONFile &&
isNotApiPage
);
}
}
)
]);
}

/**
* Build image optimization lambda (supported by Next.js 10)
* @param buildManifest
Expand Down Expand Up @@ -943,9 +984,9 @@ class Builder {
path.join(dotNextDirectory, "prerender-manifest.json")
);

let prerenderManifestJSONPropFileAssets: Promise<void>[] = [];
let prerenderManifestHTMLPageAssets: Promise<void>[] = [];
let fallbackHTMLPageAssets: Promise<void>[] = [];
const prerenderManifestJSONPropFileAssets: Promise<void>[] = [];
const prerenderManifestHTMLPageAssets: Promise<void>[] = [];
const fallbackHTMLPageAssets: Promise<void>[] = [];

// Copy locale-specific prerendered files if defined, otherwise use empty locale
// which would copy to root only
Expand Down Expand Up @@ -1121,6 +1162,7 @@ class Builder {
await fse.emptyDir(join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR));
await fse.emptyDir(join(this.outputDir, API_LAMBDA_CODE_DIR));
await fse.emptyDir(join(this.outputDir, IMAGE_LAMBDA_CODE_DIR));
await fse.emptyDir(join(this.outputDir, REGENERATION_LAMBDA_CODE_DIR));
await fse.emptyDir(join(this.outputDir, ASSETS_DIR));

const { restoreUserConfig } = await createServerlessConfig(
Expand Down Expand Up @@ -1162,6 +1204,7 @@ class Builder {
} = await this.prepareBuildManifests(routesManifest, prerenderManifest);

await this.buildDefaultLambda(defaultBuildManifest);
await this.buildRegenerationHandler(defaultBuildManifest);

const hasAPIPages =
Object.keys(apiBuildManifest.apis.nonDynamic).length > 0 ||
Expand Down
114 changes: 71 additions & 43 deletions packages/libs/lambda-at-edge/src/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ import {
getLocalePrefixFromUri
} from "./routing/locale-utils";
import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
import { getStaticRegenerationResponse } from "./lib/getStaticRegenerationResponse";
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";
import { triggerStaticRegeneration } from "./lib/triggerStaticRegeneration";
import { s3StorePage } from "./s3/s3StorePage";
import { cleanRequestUriForRouter } from "./lib/cleanRequestUriForRouter";

const basePath = RoutesManifestJson.basePath;

Expand Down Expand Up @@ -325,8 +330,8 @@ const handleOriginRequest = async ({
const decodedUri = decodeURI(uri);
const { pages, publicFiles } = manifest;

let isPublicFile = publicFiles[decodedUri];
let isDataReq = isDataRequest(uri);
const isPublicFile = publicFiles[decodedUri];
const isDataReq = isDataRequest(uri);

// Handle redirects
// TODO: refactor redirect logic to another file since this is getting quite large
Expand Down Expand Up @@ -590,7 +595,6 @@ const handleOriginRequest = async ({
const handleOriginResponse = async ({
event,
manifest,
prerenderManifest,
kirkness marked this conversation as resolved.
Show resolved Hide resolved
routesManifest
}: {
event: OriginResponseEvent;
Expand All @@ -602,12 +606,46 @@ const handleOriginResponse = async ({
const request = event.Records[0].cf.request;
const { uri } = request;
const { status } = response;
const bucketName = s3BucketNameFromEventRequest(request);

if (status !== "403") {
// Set 404 status code for 404.html page. We do not need normalised URI as it will always be "/404.html"
if (uri.endsWith("/404.html")) {
response.status = "404";
response.statusDescription = "Not Found";
return response;
}

const staticRegenerationResponse = getStaticRegenerationResponse({
requestedOriginUri: uri,
expiresHeader: response.headers.expires?.[0]?.value || "",
lastModifiedHeader: response.headers["last-modified"]?.[0]?.value || "",
manifest
});

if (staticRegenerationResponse) {
response.headers["cache-control"] = [
{
key: "Cache-Control",
value: staticRegenerationResponse.cacheControl
}
];

// We don't want the `expires` header to be sent to the client we manage
// the cache at the edge using the s-maxage directive in the cache-control
// header
delete response.headers.expires;

if (staticRegenerationResponse.secondsRemainingUntilRevalidation === 0) {
await triggerStaticRegeneration({
basePath,
manifest,
request,
response
});
}
}

return response;
}

Expand All @@ -616,9 +654,6 @@ const handleOriginResponse = async ({
return response;
}

const { domainName, region } = request.origin!.s3!;
const bucketName = domainName.replace(`.s3.${region}.amazonaws.com`, "");

// Lazily import only S3Client to reduce init times until actually needed
const { S3Client } = await import("@aws-sdk/client-s3/S3Client");

Expand All @@ -643,12 +678,7 @@ const handleOriginResponse = async ({
// eslint-disable-next-line
const page = require(`./${pagePath}`);
// Reconstruct original uri for next/router
if (uri.endsWith(".html")) {
request.uri = uri.slice(0, uri.length - 5);
if (manifest.trailingSlash) {
request.uri += "/";
}
}
request.uri = cleanRequestUriForRouter(request.uri, manifest.trailingSlash);
const { req, res, responsePromise } = lambdaAtEdgeCompat(
event.Records[0].cf,
{
Expand All @@ -661,44 +691,42 @@ const handleOriginResponse = async ({
res,
"passthrough"
);
let cacheControl = "public, max-age=0, s-maxage=2678400, must-revalidate";
if (isSSG) {
const baseKey = uri
.replace(/^\//, "")
.replace(/\.(json|html)$/, "")
.replace(/^_next\/data\/[^\/]*\//, "");
const jsonKey = `_next/data/${manifest.buildId}/${baseKey}.json`;
const htmlKey = `static-pages/${manifest.buildId}/${baseKey}.html`;
const s3JsonParams = {
Bucket: bucketName,
Key: `${s3BasePath}${jsonKey}`,
Body: JSON.stringify(renderOpts.pageData),
ContentType: "application/json",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
};
const s3HtmlParams = {
Bucket: bucketName,
Key: `${s3BasePath}${htmlKey}`,
Body: html,
ContentType: "text/html",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
};
const { PutObjectCommand } = await import(
"@aws-sdk/client-s3/commands/PutObjectCommand"
);
await Promise.all([
s3.send(new PutObjectCommand(s3JsonParams)),
s3.send(new PutObjectCommand(s3HtmlParams))
]);
const { expires } = await s3StorePage({
html,
uri,
basePath,
bucketName: bucketName || "",
buildId: manifest.buildId,
pageData: renderOpts.pageData,
region: request.origin?.s3?.region || "",
revalidate: renderOpts.revalidate
});

const isrResponse = expires
? getStaticRegenerationResponse({
expiresHeader: expires.toJSON(),
manifest,
requestedOriginUri: uri,
lastModifiedHeader: undefined
})
: null;

cacheControl = (isrResponse && isrResponse.cacheControl) || cacheControl;
}
const outHeaders: OutgoingHttpHeaders = {};
Object.entries(response.headers).map(([name, headers]) => {
outHeaders[name] = headers.map(({ value }) => value);
});
res.writeHead(200, outHeaders);
res.setHeader(
"Cache-Control",
"public, max-age=0, s-maxage=2678400, must-revalidate"
);

if (cacheControl) {
res.setHeader("Cache-Control", cacheControl);
} else {
res.removeHeader("Cache-Control");
}

if (isDataRequest(uri)) {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(renderOpts.pageData));
Expand Down
7 changes: 4 additions & 3 deletions packages/libs/lambda-at-edge/src/image-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "./routing/redirector";
import { getUnauthenticatedResponse } from "./auth/authenticator";
import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";

const basePath = RoutesManifestJson.basePath;

Expand Down Expand Up @@ -88,11 +89,11 @@ export const handler = async (
true
);

const { domainName, region } = request.origin!.s3!;
const bucketName = domainName.replace(`.s3.${region}.amazonaws.com`, "");
const { region } = request.origin!.s3!;
const bucketName = s3BucketNameFromEventRequest(request);

await imageOptimizer(
{ basePath: basePath, bucketName: bucketName, region: region },
{ basePath: basePath, bucketName: bucketName || "", region: region },
imagesManifest,
req,
res,
Expand Down
16 changes: 16 additions & 0 deletions packages/libs/lambda-at-edge/src/lib/cleanRequestUriForRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know you started around or before @jvarho refactored a lot to core package, but maybe you can at least move some of these isolated files there that are not related to AWS/Lambda@Edge stuff?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've yet to touch the response handler in by refactoring precisely to avoid conflicting with this pull request. Much of the remaining response handler routing logic can be refactored away, I believe. But it will be easier after both this and my currently open PR are merged to master.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cool, let's update this in a follow-up PR.

* Removes html and adds the trailing slash if needed. This is used when
* regenerating an SSG page.
*/
export const cleanRequestUriForRouter = (
uri: string,
trailingSlash?: boolean
): string => {
if (uri.endsWith(".html")) {
uri = uri.slice(0, uri.length - 5);
if (trailingSlash) {
uri += "/";
}
}
return uri;
};
Loading