Skip to content

Commit bd988a4

Browse files
committed
Upload prerendered pages with revalidate-based expiry
This makes cloudfront request them again periodically. Not yet useful on its own, but required for rerendering.
1 parent f18d6a6 commit bd988a4

File tree

8 files changed

+126
-19
lines changed

8 files changed

+126
-19
lines changed

packages/libs/s3-static-assets/src/index.ts

+64-17
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,25 @@ import {
77
SERVER_NO_CACHE_CACHE_CONTROL_HEADER,
88
SERVER_CACHE_CONTROL_HEADER
99
} from "./lib/constants";
10+
import getPageName from "./lib/getPageName";
1011
import S3ClientFactory, { Credentials } from "./lib/s3";
1112
import pathToPosix from "./lib/pathToPosix";
12-
import { PrerenderManifest } from "next/dist/build/index";
13+
import { PrerenderManifest, SsgRoute } from "next/dist/build/index";
1314
import getPublicAssetCacheControl, {
1415
PublicDirectoryCache
1516
} from "./lib/getPublicAssetCacheControl";
1617

18+
type PrerenderRoutes = {
19+
[path: string]: SsgRoute;
20+
};
21+
1722
type UploadStaticAssetsOptions = {
1823
bucketName: string;
1924
basePath: string;
2025
nextConfigDir: string;
2126
nextStaticDir?: string;
2227
credentials: Credentials;
28+
prerenderRoutes?: PrerenderRoutes;
2329
publicDirectoryCache?: PublicDirectoryCache;
2430
};
2531

@@ -29,6 +35,7 @@ type AssetDirectoryFileCachePoliciesOptions = {
2935
// .i.e. by default .serverless_nextjs
3036
serverlessBuildOutDir: string;
3137
nextStaticDir?: string;
38+
prerenderRoutes: PrerenderRoutes;
3239
publicDirectoryCache?: PublicDirectoryCache;
3340
};
3441

@@ -38,13 +45,19 @@ type AssetDirectoryFileCachePoliciesOptions = {
3845
const getAssetDirectoryFileCachePolicies = (
3946
options: AssetDirectoryFileCachePoliciesOptions
4047
): Array<{
41-
cacheControl: string | undefined;
48+
cacheControl?: string;
49+
expires?: Date;
4250
path: {
4351
relative: string;
4452
absolute: string;
4553
};
4654
}> => {
47-
const { basePath, publicDirectoryCache, serverlessBuildOutDir } = options;
55+
const {
56+
basePath,
57+
prerenderRoutes,
58+
publicDirectoryCache,
59+
serverlessBuildOutDir
60+
} = options;
4861

4962
const normalizedBasePath = basePath ? basePath.slice(1) : "";
5063

@@ -75,20 +88,39 @@ const getAssetDirectoryFileCachePolicies = (
7588

7689
// Upload Next.js data files
7790

78-
const nextDataFiles = readDirectoryFiles(
79-
path.join(assetsOutputDirectory, normalizedBasePath, "_next", "data")
91+
const nextDataDir = path.join(
92+
assetsOutputDirectory,
93+
normalizedBasePath,
94+
"_next",
95+
"data"
8096
);
97+
const nextDataFiles = readDirectoryFiles(nextDataDir);
8198

82-
const nextDataFilesUploads = nextDataFiles.map((fileItem) => ({
83-
path: fileItem.path,
84-
cacheControl: SERVER_CACHE_CONTROL_HEADER
85-
}));
99+
const nextDataFilesUploads = nextDataFiles.map((fileItem) => {
100+
const route = prerenderRoutes[getPageName(fileItem.path, nextDataDir)];
101+
if (route && route.initialRevalidateSeconds) {
102+
const expires = new Date(
103+
new Date().getTime() + 1000 * route.initialRevalidateSeconds
104+
);
105+
return {
106+
path: fileItem.path,
107+
expires
108+
};
109+
}
110+
return {
111+
path: fileItem.path,
112+
cacheControl: SERVER_CACHE_CONTROL_HEADER
113+
};
114+
});
86115

87116
// Upload Next.js HTML pages
88117

89-
const htmlPages = readDirectoryFiles(
90-
path.join(assetsOutputDirectory, normalizedBasePath, "static-pages")
118+
const htmlDir = path.join(
119+
assetsOutputDirectory,
120+
normalizedBasePath,
121+
"static-pages"
91122
);
123+
const htmlPages = readDirectoryFiles(htmlDir);
92124

93125
const htmlPagesUploads = htmlPages.map((fileItem) => {
94126
// Dynamic fallback HTML pages should never be cached as it will override actual pages once generated and stored in S3.
@@ -98,12 +130,23 @@ const getAssetDirectoryFileCachePolicies = (
98130
path: fileItem.path,
99131
cacheControl: SERVER_NO_CACHE_CACHE_CONTROL_HEADER
100132
};
101-
} else {
133+
}
134+
135+
const route = prerenderRoutes[getPageName(fileItem.path, htmlDir)];
136+
if (route && route.initialRevalidateSeconds) {
137+
const expires = new Date(
138+
new Date().getTime() + 1000 * route.initialRevalidateSeconds
139+
);
102140
return {
103141
path: fileItem.path,
104-
cacheControl: SERVER_CACHE_CONTROL_HEADER
142+
expires
105143
};
106144
}
145+
146+
return {
147+
path: fileItem.path,
148+
cacheControl: SERVER_CACHE_CONTROL_HEADER
149+
};
107150
});
108151

109152
// Upload user static and public files
@@ -132,14 +175,14 @@ const getAssetDirectoryFileCachePolicies = (
132175
...htmlPagesUploads,
133176
...publicAndStaticUploads,
134177
buildIdUpload
135-
].map(({ cacheControl, path: absolutePath }) => ({
136-
cacheControl,
178+
].map(({ path: absolutePath, ...rest }) => ({
137179
path: {
138180
// Path relative to the assets folder, used for the S3 upload key
139181
relative: path.relative(assetsOutputDirectory, absolutePath),
140182
// Absolute path of local asset
141183
absolute: absolutePath
142-
}
184+
},
185+
...rest
143186
}));
144187
};
145188

@@ -155,11 +198,14 @@ const uploadStaticAssetsFromBuild = async (
155198
bucketName,
156199
credentials,
157200
basePath,
201+
prerenderRoutes,
158202
publicDirectoryCache,
159203
nextConfigDir
160204
} = options;
205+
161206
const files = getAssetDirectoryFileCachePolicies({
162207
basePath,
208+
prerenderRoutes: prerenderRoutes ?? {},
163209
publicDirectoryCache,
164210
serverlessBuildOutDir: path.join(nextConfigDir, ".serverless_nextjs")
165211
});
@@ -173,7 +219,8 @@ const uploadStaticAssetsFromBuild = async (
173219
s3.uploadFile({
174220
s3Key: pathToPosix(file.path.relative),
175221
filePath: file.path.absolute,
176-
cacheControl: file.cacheControl
222+
cacheControl: file.cacheControl,
223+
expires: file.expires
177224
})
178225
)
179226
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const getPageName = (file: string, base: string): string => {
2+
const relative = file.slice(base.length + 1);
3+
const withoutBuildId = relative.split("/", 2)[1];
4+
const withoutExtension = withoutBuildId.replace(/\.(html|json)$/, "");
5+
return `/${withoutExtension}`;
6+
};
7+
8+
export default getPageName;

packages/libs/s3-static-assets/src/lib/s3.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type S3ClientFactoryOptions = {
1212
type UploadFileOptions = {
1313
filePath: string;
1414
cacheControl?: string;
15+
expires?: Date;
1516
s3Key?: string;
1617
};
1718

@@ -73,7 +74,7 @@ export default async ({
7374
uploadFile: async (
7475
options: UploadFileOptions
7576
): Promise<AWS.S3.ManagedUpload.SendData> => {
76-
const { filePath, cacheControl, s3Key } = options;
77+
const { filePath, cacheControl, expires, s3Key } = options;
7778

7879
const fileBody = await fse.readFile(filePath);
7980

@@ -83,7 +84,8 @@ export default async ({
8384
Key: s3Key || filePath,
8485
Body: fileBody,
8586
ContentType: getMimeType(filePath),
86-
CacheControl: cacheControl || undefined
87+
CacheControl: cacheControl || undefined,
88+
Expires: expires
8789
})
8890
.promise();
8991
},

packages/libs/s3-static-assets/tests/fixtures/app-basic-upload-from-build/.serverless_nextjs/assets/_next/data/zsWqBqLjpgRmswfQomanp/revalidate.json

Whitespace-only changes.

packages/libs/s3-static-assets/tests/fixtures/app-basic-upload-from-build/.serverless_nextjs/assets/static-pages/zsWqBqLjpgRmswfQomanp/revalidate.html

Whitespace-only changes.

packages/libs/s3-static-assets/tests/upload-assets-from-build.test.ts

+49
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ const upload = (
4343
secretAccessKey: "fake-secret-key",
4444
sessionToken: "fake-session-token"
4545
},
46+
prerenderRoutes: {
47+
"/revalidate": {
48+
initialRevalidateSeconds: 60,
49+
srcRoute: "/revalidate",
50+
dataRoute: "/_next/data/zsWqBqLjpgRmswfQomanp/revalidate.json"
51+
}
52+
},
4653
publicDirectoryCache: publicAssetCache
4754
});
4855
};
@@ -163,6 +170,27 @@ describe.each`
163170
);
164171
});
165172

173+
it("uploads revalidate HTML pages with expires instead of cache-control", async () => {
174+
expect(mockUpload).toBeCalledWith(
175+
expect.objectContaining({
176+
Key: "static-pages/zsWqBqLjpgRmswfQomanp/revalidate.html",
177+
ContentType: "text/html"
178+
})
179+
);
180+
const call = mockUpload.mock.calls.find(
181+
(call) =>
182+
call[0].Key === "static-pages/zsWqBqLjpgRmswfQomanp/revalidate.html"
183+
);
184+
expect(call[0]).toHaveProperty("Expires");
185+
expect(call[0].CacheControl).toEqual(undefined);
186+
expect(new Date(call[0].Expires).getTime()).toBeGreaterThan(
187+
new Date().getTime()
188+
);
189+
expect(new Date(call[0].Expires).getTime()).toBeLessThan(
190+
new Date().getTime() + 60000
191+
);
192+
});
193+
166194
it("uploads staticProps JSON files in _next/data", async () => {
167195
expect(mockUpload).toBeCalledWith(
168196
expect.objectContaining({
@@ -197,6 +225,27 @@ describe.each`
197225
);
198226
});
199227

228+
it("uploads revalidate _next/data JSON with expires instead of cache-control", async () => {
229+
expect(mockUpload).toBeCalledWith(
230+
expect.objectContaining({
231+
Key: "_next/data/zsWqBqLjpgRmswfQomanp/revalidate.json",
232+
ContentType: "application/json"
233+
})
234+
);
235+
const call = mockUpload.mock.calls.find(
236+
(call) =>
237+
call[0].Key === "_next/data/zsWqBqLjpgRmswfQomanp/revalidate.json"
238+
);
239+
expect(call[0]).toHaveProperty("Expires");
240+
expect(call[0].CacheControl).toEqual(undefined);
241+
expect(new Date(call[0].Expires).getTime()).toBeGreaterThan(
242+
new Date().getTime()
243+
);
244+
expect(new Date(call[0].Expires).getTime()).toBeLessThan(
245+
new Date().getTime() + 60000
246+
);
247+
});
248+
200249
it("uploads files in the public folder", async () => {
201250
expect(mockUpload).toBeCalledWith(
202251
expect.objectContaining({

packages/serverless-components/nextjs-component/src/component.ts

+1
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ class NextjsComponent extends Component {
347347
nextConfigDir: nextConfigPath,
348348
nextStaticDir: nextStaticPath,
349349
credentials: this.context.credentials.aws,
350+
prerenderRoutes: defaultBuildManifest.pages.ssg.nonDynamic,
350351
publicDirectoryCache: inputs.publicDirectoryCache
351352
});
352353
} else {

0 commit comments

Comments
 (0)