sst 2.31.0 → 2.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constructs/App.js +7 -7
- package/constructs/AppSyncApi.d.ts +1 -0
- package/constructs/AppSyncApi.js +100 -26
- package/constructs/Distribution.d.ts +5 -10
- package/constructs/Distribution.js +6 -4
- package/constructs/Function.d.ts +6 -6
- package/constructs/Function.js +4 -4
- package/constructs/Job.js +1 -1
- package/constructs/NextjsSite.d.ts +13 -1
- package/constructs/NextjsSite.js +196 -35
- package/constructs/Service.d.ts +0 -1
- package/constructs/Service.js +1 -2
- package/constructs/SsrSite.d.ts +54 -17
- package/constructs/SsrSite.js +58 -48
- package/constructs/StaticSite.js +3 -2
- package/package.json +2 -2
- package/support/custom-resources/index.mjs +8510 -8439
package/constructs/NextjsSite.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import zlib from "zlib";
|
|
3
4
|
import crypto from "crypto";
|
|
5
|
+
import { globSync } from "glob";
|
|
4
6
|
import { Duration as CdkDuration, RemovalPolicy, CustomResource, } from "aws-cdk-lib/core";
|
|
5
7
|
import { Code, Runtime, Function as CdkFunction, Architecture, LayerVersion, } from "aws-cdk-lib/aws-lambda";
|
|
6
8
|
import { AttributeType, Billing, TableV2 as Table, } from "aws-cdk-lib/aws-dynamodb";
|
|
@@ -13,6 +15,9 @@ import { toCdkSize } from "./util/size.js";
|
|
|
13
15
|
import { Effect, Policy, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
14
16
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
15
17
|
import { VisibleError } from "../error.js";
|
|
18
|
+
import { Asset } from "aws-cdk-lib/aws-s3-assets";
|
|
19
|
+
import { useFunctions } from "./Function.js";
|
|
20
|
+
import { useDeferredTasks } from "./deferred_task.js";
|
|
16
21
|
const LAYER_VERSION = "2";
|
|
17
22
|
const DEFAULT_OPEN_NEXT_VERSION = "2.2.4";
|
|
18
23
|
const DEFAULT_CACHE_POLICY_ALLOWED_HEADERS = [
|
|
@@ -34,7 +39,11 @@ const DEFAULT_CACHE_POLICY_ALLOWED_HEADERS = [
|
|
|
34
39
|
* ```
|
|
35
40
|
*/
|
|
36
41
|
export class NextjsSite extends SsrSite {
|
|
37
|
-
|
|
42
|
+
_routes;
|
|
43
|
+
routesManifest;
|
|
44
|
+
appPathRoutesManifest;
|
|
45
|
+
appPathsManifest;
|
|
46
|
+
pagesManifest;
|
|
38
47
|
constructor(scope, id, props) {
|
|
39
48
|
const streaming = props?.experimental?.streaming ?? false;
|
|
40
49
|
const disableDynamoDBCache = props?.experimental?.disableDynamoDBCache ?? false;
|
|
@@ -57,6 +66,7 @@ export class NextjsSite extends SsrSite {
|
|
|
57
66
|
});
|
|
58
67
|
if (this.isPerRouteLoggingEnabled()) {
|
|
59
68
|
this.disableDefaultLogging();
|
|
69
|
+
this.uploadSourcemaps();
|
|
60
70
|
}
|
|
61
71
|
if (!disableIncrementalCache) {
|
|
62
72
|
this.createRevalidationQueue();
|
|
@@ -80,6 +90,7 @@ export class NextjsSite extends SsrSite {
|
|
|
80
90
|
CACHE_BUCKET_REGION: Stack.of(this).region,
|
|
81
91
|
},
|
|
82
92
|
});
|
|
93
|
+
this.removeSourcemaps();
|
|
83
94
|
return this.validatePlan({
|
|
84
95
|
cloudFrontFunctions: {
|
|
85
96
|
serverCfFunction: {
|
|
@@ -312,7 +323,10 @@ export class NextjsSite extends SsrSite {
|
|
|
312
323
|
const injections = [];
|
|
313
324
|
if (this.isPerRouteLoggingEnabled()) {
|
|
314
325
|
injections.push(`
|
|
315
|
-
const routeData = ${JSON.stringify(this.useRoutes()
|
|
326
|
+
const routeData = ${JSON.stringify(this.useRoutes().map(({ regex, logGroupPath }) => ({
|
|
327
|
+
regex,
|
|
328
|
+
logGroupPath,
|
|
329
|
+
})))}.find(({ regex }) => event.rawPath.match(new RegExp(regex)));
|
|
316
330
|
if (routeData) {
|
|
317
331
|
console.log("::sst::" + JSON.stringify({
|
|
318
332
|
action:"log.split",
|
|
@@ -349,53 +363,178 @@ export class NextjsSite extends SsrSite {
|
|
|
349
363
|
handler: `${wrapperName}.handler`,
|
|
350
364
|
};
|
|
351
365
|
}
|
|
366
|
+
removeSourcemaps() {
|
|
367
|
+
const { path: sitePath } = this.props;
|
|
368
|
+
const files = globSync("**/*.js.map", {
|
|
369
|
+
cwd: path.join(sitePath, ".open-next", "server-function"),
|
|
370
|
+
nodir: true,
|
|
371
|
+
dot: true,
|
|
372
|
+
});
|
|
373
|
+
for (const file of files) {
|
|
374
|
+
fs.rmSync(path.join(sitePath, ".open-next", "server-function", file));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
352
377
|
useRoutes() {
|
|
353
|
-
if (this.
|
|
354
|
-
return this.
|
|
355
|
-
const
|
|
378
|
+
if (this._routes)
|
|
379
|
+
return this._routes;
|
|
380
|
+
const routesManifest = this.useRoutesManifest();
|
|
381
|
+
this._routes = [
|
|
382
|
+
...[...routesManifest.dynamicRoutes, ...routesManifest.staticRoutes]
|
|
383
|
+
.map(({ page, regex }) => {
|
|
384
|
+
const cwRoute = NextjsSite.buildCloudWatchRouteName(page);
|
|
385
|
+
const cwHash = NextjsSite.buildCloudWatchRouteHash(page);
|
|
386
|
+
const sourcemapPath = this.getSourcemapForAppRoute(page) ||
|
|
387
|
+
this.getSourcemapForPagesRoute(page);
|
|
388
|
+
return {
|
|
389
|
+
route: page,
|
|
390
|
+
regex,
|
|
391
|
+
logGroupPath: `/${cwHash}${cwRoute}`,
|
|
392
|
+
sourcemapPath: sourcemapPath,
|
|
393
|
+
sourcemapKey: cwHash,
|
|
394
|
+
};
|
|
395
|
+
})
|
|
396
|
+
.sort((a, b) => a.route.localeCompare(b.route)),
|
|
397
|
+
...(routesManifest.dataRoutes || [])
|
|
398
|
+
.map(({ page, dataRouteRegex }) => {
|
|
399
|
+
const routeDisplayName = page.endsWith("/")
|
|
400
|
+
? `/_next/data/BUILD_ID${page}index.json`
|
|
401
|
+
: `/_next/data/BUILD_ID${page}.json`;
|
|
402
|
+
const cwRoute = NextjsSite.buildCloudWatchRouteName(routeDisplayName);
|
|
403
|
+
const cwHash = NextjsSite.buildCloudWatchRouteHash(page);
|
|
404
|
+
return {
|
|
405
|
+
route: routeDisplayName,
|
|
406
|
+
regex: dataRouteRegex,
|
|
407
|
+
logGroupPath: `/${cwHash}${cwRoute}`,
|
|
408
|
+
};
|
|
409
|
+
})
|
|
410
|
+
.sort((a, b) => a.route.localeCompare(b.route)),
|
|
411
|
+
];
|
|
412
|
+
return this._routes;
|
|
413
|
+
}
|
|
414
|
+
useRoutesManifest() {
|
|
415
|
+
if (this.routesManifest)
|
|
416
|
+
return this.routesManifest;
|
|
356
417
|
const { path: sitePath } = this.props;
|
|
418
|
+
const id = this.node.id;
|
|
357
419
|
try {
|
|
358
|
-
const content =
|
|
420
|
+
const content = fs
|
|
359
421
|
.readFileSync(path.join(sitePath, ".next/routes-manifest.json"))
|
|
360
|
-
.toString()
|
|
361
|
-
this.
|
|
362
|
-
|
|
363
|
-
.map(({ page, regex }) => {
|
|
364
|
-
const cwRoute = NextjsSite.buildCloudWatchRouteName(page);
|
|
365
|
-
const cwHash = NextjsSite.buildCloudWatchRouteHash(page);
|
|
366
|
-
return {
|
|
367
|
-
route: page,
|
|
368
|
-
regex,
|
|
369
|
-
logGroupPath: `/${cwHash}${cwRoute}`,
|
|
370
|
-
};
|
|
371
|
-
})
|
|
372
|
-
.sort((a, b) => a.route.localeCompare(b.route)),
|
|
373
|
-
...(content.dataRoutes || [])
|
|
374
|
-
.map(({ page, dataRouteRegex }) => {
|
|
375
|
-
const routeDisplayName = page.endsWith("/")
|
|
376
|
-
? `/_next/data/BUILD_ID${page}index.json`
|
|
377
|
-
: `/_next/data/BUILD_ID${page}.json`;
|
|
378
|
-
const cwRoute = NextjsSite.buildCloudWatchRouteName(routeDisplayName);
|
|
379
|
-
const cwHash = NextjsSite.buildCloudWatchRouteHash(`data:${page}`);
|
|
380
|
-
return {
|
|
381
|
-
route: routeDisplayName,
|
|
382
|
-
regex: dataRouteRegex,
|
|
383
|
-
logGroupPath: `/${cwHash}${cwRoute}`,
|
|
384
|
-
};
|
|
385
|
-
})
|
|
386
|
-
.sort((a, b) => a.route.localeCompare(b.route)),
|
|
387
|
-
];
|
|
388
|
-
return this.routes;
|
|
422
|
+
.toString();
|
|
423
|
+
this.routesManifest = JSON.parse(content);
|
|
424
|
+
return this.routesManifest;
|
|
389
425
|
}
|
|
390
426
|
catch (e) {
|
|
391
427
|
console.error(e);
|
|
392
428
|
throw new VisibleError(`Failed to read routes data from ".next/routes-manifest.json" for the "${id}" site.`);
|
|
393
429
|
}
|
|
394
430
|
}
|
|
431
|
+
useAppPathRoutesManifest() {
|
|
432
|
+
if (this.appPathRoutesManifest)
|
|
433
|
+
return this.appPathRoutesManifest;
|
|
434
|
+
const { path: sitePath } = this.props;
|
|
435
|
+
try {
|
|
436
|
+
const content = fs
|
|
437
|
+
.readFileSync(path.join(sitePath, ".next/app-path-routes-manifest.json"))
|
|
438
|
+
.toString();
|
|
439
|
+
this.appPathRoutesManifest = JSON.parse(content);
|
|
440
|
+
return this.appPathRoutesManifest;
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
return {};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
useAppPathsManifest() {
|
|
447
|
+
if (this.appPathsManifest)
|
|
448
|
+
return this.appPathsManifest;
|
|
449
|
+
const { path: sitePath } = this.props;
|
|
450
|
+
try {
|
|
451
|
+
const content = fs
|
|
452
|
+
.readFileSync(path.join(sitePath, ".next/server/app-paths-manifest.json"))
|
|
453
|
+
.toString();
|
|
454
|
+
this.appPathsManifest = JSON.parse(content);
|
|
455
|
+
return this.appPathsManifest;
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
return {};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
usePagesManifest() {
|
|
462
|
+
if (this.pagesManifest)
|
|
463
|
+
return this.pagesManifest;
|
|
464
|
+
const { path: sitePath } = this.props;
|
|
465
|
+
try {
|
|
466
|
+
const content = fs
|
|
467
|
+
.readFileSync(path.join(sitePath, ".next/server/pages-manifest.json"))
|
|
468
|
+
.toString();
|
|
469
|
+
this.pagesManifest = JSON.parse(content);
|
|
470
|
+
return this.pagesManifest;
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
return {};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
395
476
|
getBuildId() {
|
|
396
477
|
const { path: sitePath } = this.props;
|
|
397
478
|
return fs.readFileSync(path.join(sitePath, ".next/BUILD_ID")).toString();
|
|
398
479
|
}
|
|
480
|
+
getSourcemapForAppRoute(page) {
|
|
481
|
+
const { path: sitePath } = this.props;
|
|
482
|
+
// Step 1: look up in "appPathRoutesManifest" to find the key with
|
|
483
|
+
// value equal to the page
|
|
484
|
+
// {
|
|
485
|
+
// "/_not-found": "/_not-found",
|
|
486
|
+
// "/about/page": "/about",
|
|
487
|
+
// "/about/profile/page": "/about/profile",
|
|
488
|
+
// "/page": "/",
|
|
489
|
+
// "/favicon.ico/route": "/favicon.ico"
|
|
490
|
+
// }
|
|
491
|
+
const appPathRoutesManifest = this.useAppPathRoutesManifest();
|
|
492
|
+
const appPathRoute = Object.keys(appPathRoutesManifest).find((key) => appPathRoutesManifest[key] === page);
|
|
493
|
+
if (!appPathRoute)
|
|
494
|
+
return;
|
|
495
|
+
// Step 2: look up in "appPathsManifest" to find the file with key equal
|
|
496
|
+
// to the page
|
|
497
|
+
// {
|
|
498
|
+
// "/_not-found": "app/_not-found.js",
|
|
499
|
+
// "/about/page": "app/about/page.js",
|
|
500
|
+
// "/about/profile/page": "app/about/profile/page.js",
|
|
501
|
+
// "/page": "app/page.js",
|
|
502
|
+
// "/favicon.ico/route": "app/favicon.ico/route.js"
|
|
503
|
+
// }
|
|
504
|
+
const appPathsManifest = this.useAppPathsManifest();
|
|
505
|
+
const filePath = appPathsManifest[appPathRoute];
|
|
506
|
+
if (!filePath)
|
|
507
|
+
return;
|
|
508
|
+
// Step 3: check the .map file exists
|
|
509
|
+
const sourcemapPath = path.join(sitePath, ".next", "server", `${filePath}.map`);
|
|
510
|
+
if (!fs.existsSync(sourcemapPath))
|
|
511
|
+
return;
|
|
512
|
+
return sourcemapPath;
|
|
513
|
+
}
|
|
514
|
+
getSourcemapForPagesRoute(page) {
|
|
515
|
+
const { path: sitePath } = this.props;
|
|
516
|
+
// Step 1: look up in "pathsManifest" to find the file with key equal
|
|
517
|
+
// to the page
|
|
518
|
+
// {
|
|
519
|
+
// "/_app": "pages/_app.js",
|
|
520
|
+
// "/_error": "pages/_error.js",
|
|
521
|
+
// "/404": "pages/404.html",
|
|
522
|
+
// "/api/hello": "pages/api/hello.js",
|
|
523
|
+
// "/api/auth/[...nextauth]": "pages/api/auth/[...nextauth].js",
|
|
524
|
+
// "/api/next-auth-restricted": "pages/api/next-auth-restricted.js",
|
|
525
|
+
// "/": "pages/index.js",
|
|
526
|
+
// "/ssr": "pages/ssr.js"
|
|
527
|
+
// }
|
|
528
|
+
const pagesManifest = this.usePagesManifest();
|
|
529
|
+
const filePath = pagesManifest[page];
|
|
530
|
+
if (!filePath)
|
|
531
|
+
return;
|
|
532
|
+
// Step 2: check the .map file exists
|
|
533
|
+
const sourcemapPath = path.join(sitePath, ".next", "server", `${filePath}.map`);
|
|
534
|
+
if (!fs.existsSync(sourcemapPath))
|
|
535
|
+
return;
|
|
536
|
+
return sourcemapPath;
|
|
537
|
+
}
|
|
399
538
|
isPerRouteLoggingEnabled() {
|
|
400
539
|
return (!this.doNotDeploy &&
|
|
401
540
|
!this.props.edge &&
|
|
@@ -424,6 +563,28 @@ export class NextjsSite extends SsrSite {
|
|
|
424
563
|
});
|
|
425
564
|
server.role?.attachInlinePolicy(policy);
|
|
426
565
|
}
|
|
566
|
+
uploadSourcemaps() {
|
|
567
|
+
const stack = Stack.of(this);
|
|
568
|
+
const server = this.serverFunction;
|
|
569
|
+
this.useRoutes().forEach(({ sourcemapPath, sourcemapKey }) => {
|
|
570
|
+
if (!sourcemapPath || !sourcemapKey)
|
|
571
|
+
return;
|
|
572
|
+
useDeferredTasks().add(async () => {
|
|
573
|
+
// zip sourcemap
|
|
574
|
+
const zipPath = `${sourcemapPath}.gz.zip`;
|
|
575
|
+
const data = await fs.promises.readFile(sourcemapPath);
|
|
576
|
+
await fs.promises.writeFile(zipPath, zlib.gzipSync(data));
|
|
577
|
+
const asset = new Asset(this, `Sourcemap-${sourcemapKey}`, {
|
|
578
|
+
path: zipPath,
|
|
579
|
+
});
|
|
580
|
+
useFunctions().sourcemaps.add(stack.stackName, {
|
|
581
|
+
srcBucket: asset.bucket,
|
|
582
|
+
srcKey: asset.s3ObjectKey,
|
|
583
|
+
tarKey: path.join(server.functionArn, sourcemapKey),
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
}
|
|
427
588
|
static buildCloudWatchRouteName(route) {
|
|
428
589
|
return route.replace(/[^a-zA-Z0-9_\-/.#]/g, "");
|
|
429
590
|
}
|
package/constructs/Service.d.ts
CHANGED
|
@@ -375,7 +375,6 @@ type ServiceNormalizedProps = ServiceProps & {
|
|
|
375
375
|
memory: Exclude<ServiceProps["memory"], undefined>;
|
|
376
376
|
port: Exclude<ServiceProps["port"], undefined>;
|
|
377
377
|
logRetention: Exclude<ServiceProps["logRetention"], undefined>;
|
|
378
|
-
waitForInvalidation: Exclude<ServiceProps["waitForInvalidation"], undefined>;
|
|
379
378
|
};
|
|
380
379
|
/**
|
|
381
380
|
* The `Service` construct is a higher level CDK construct that makes it easy to create modern web apps with Server Side Rendering capabilities.
|
package/constructs/Service.js
CHANGED
|
@@ -159,7 +159,6 @@ export class Service extends Construct {
|
|
|
159
159
|
memory: props?.memory || "0.5 GB",
|
|
160
160
|
port: props?.port || 3000,
|
|
161
161
|
logRetention: props?.logRetention || "infinite",
|
|
162
|
-
waitForInvalidation: false,
|
|
163
162
|
...props,
|
|
164
163
|
};
|
|
165
164
|
this.doNotDeploy =
|
|
@@ -645,7 +644,7 @@ export class Service extends Construct {
|
|
|
645
644
|
platform: architecture === "arm64" ? Platform.LINUX_ARM64 : Platform.LINUX_AMD64,
|
|
646
645
|
file: dockerfile,
|
|
647
646
|
buildArgs: build?.buildArgs,
|
|
648
|
-
exclude: [".sst"],
|
|
647
|
+
exclude: [".sst/dist", ".sst/artifacts"],
|
|
649
648
|
ignoreMode: IgnoreMode.GLOB,
|
|
650
649
|
});
|
|
651
650
|
const cfnTask = taskDefinition.node.defaultChild;
|
package/constructs/SsrSite.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { Bucket, BucketProps, IBucket } from "aws-cdk-lib/aws-s3";
|
|
3
3
|
import { Function as CdkFunction, FunctionProps as CdkFunctionProps } from "aws-cdk-lib/aws-lambda";
|
|
4
|
-
import { ICachePolicy, IResponseHeadersPolicy, AllowedMethods, CachePolicyProps, ErrorResponse } from "aws-cdk-lib/aws-cloudfront";
|
|
4
|
+
import { ICachePolicy, IResponseHeadersPolicy, ViewerProtocolPolicy, AllowedMethods, CachePolicyProps, ErrorResponse } from "aws-cdk-lib/aws-cloudfront";
|
|
5
5
|
import { Schedule } from "aws-cdk-lib/aws-events";
|
|
6
6
|
import { DistributionDomainProps } from "./Distribution.js";
|
|
7
7
|
import { SSTConstruct } from "./Construct.js";
|
|
@@ -214,21 +214,6 @@ export interface SsrSiteProps {
|
|
|
214
214
|
* ```
|
|
215
215
|
*/
|
|
216
216
|
textEncoding?: "utf-8" | "iso-8859-1" | "windows-1252" | "ascii" | "none";
|
|
217
|
-
/**
|
|
218
|
-
* The strategy to use for invalidating the CDN cache. By default, the CDN cache will invalidate on changes any cached file, but this could become slow on very large projects.
|
|
219
|
-
* - "never" - No invalidation will be performed.
|
|
220
|
-
* - "all" - All files will be invalidated when any file changes. (Default, requires checking file content which will increase deployment time)
|
|
221
|
-
* - "versioned" - Only versioned files will be invalidated when versioned files change.
|
|
222
|
-
* - "always" - All files are invalidated on every deployment.
|
|
223
|
-
* @default all
|
|
224
|
-
* @example
|
|
225
|
-
* ```js
|
|
226
|
-
* assets: {
|
|
227
|
-
* cdnInvalidationStrategy: "versioned"
|
|
228
|
-
* }
|
|
229
|
-
* ```
|
|
230
|
-
*/
|
|
231
|
-
cdnInvalidationStrategy?: "never" | "all" | "versioned" | "always";
|
|
232
217
|
/**
|
|
233
218
|
* The TTL for versioned files (ex: `main-1234.css`) in the CDN and browser cache. Ignored when `versionedFilesCacheHeader` is specified.
|
|
234
219
|
* @default 1 year
|
|
@@ -294,9 +279,45 @@ export interface SsrSiteProps {
|
|
|
294
279
|
*/
|
|
295
280
|
_uploadConcurrency?: number;
|
|
296
281
|
};
|
|
282
|
+
invalidation?: {
|
|
283
|
+
/**
|
|
284
|
+
* While deploying, SST waits for the CloudFront cache invalidation process to finish. This ensures that the new content will be served once the deploy command finishes. However, this process can sometimes take more than 5 mins. For non-prod environments it might make sense to pass in `false`. That'll skip waiting for the cache to invalidate and speed up the deploy process.
|
|
285
|
+
* @default false
|
|
286
|
+
* @example
|
|
287
|
+
* ```js
|
|
288
|
+
* invalidation: {
|
|
289
|
+
* wait: true,
|
|
290
|
+
* }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
wait?: boolean;
|
|
294
|
+
/**
|
|
295
|
+
* The paths to invalidate. There are three built-in options:
|
|
296
|
+
* - "none" - No invalidation will be performed.
|
|
297
|
+
* - "all" - All files will be invalidated when any file changes.
|
|
298
|
+
* - "versioned" - Only versioned files will be invalidated when versioned files change.
|
|
299
|
+
* Alternatively you can pass in an array of paths to invalidate.
|
|
300
|
+
* @default "all"
|
|
301
|
+
* @example
|
|
302
|
+
* Disable invalidation:
|
|
303
|
+
* ```js
|
|
304
|
+
* invalidation: {
|
|
305
|
+
* paths: "none",
|
|
306
|
+
* }
|
|
307
|
+
* ```
|
|
308
|
+
* Invalidate "index.html" and all files under the "products" route:
|
|
309
|
+
* ```js
|
|
310
|
+
* invalidation: {
|
|
311
|
+
* paths: ["/index.html", "/products/*"],
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
paths?: "none" | "all" | "versioned" | string[];
|
|
316
|
+
};
|
|
297
317
|
/**
|
|
298
318
|
* While deploying, SST waits for the CloudFront cache invalidation process to finish. This ensures that the new content will be served once the deploy command finishes. However, this process can sometimes take more than 5 mins. For non-prod environments it might make sense to pass in `false`. That'll skip waiting for the cache to invalidate and speed up the deploy process.
|
|
299
319
|
* @default false
|
|
320
|
+
* @deprecated Use `invalidation.wait` instead.
|
|
300
321
|
*/
|
|
301
322
|
waitForInvalidation?: boolean;
|
|
302
323
|
cdk?: {
|
|
@@ -340,16 +361,32 @@ export interface SsrSiteProps {
|
|
|
340
361
|
* from the server rendering Lambda.
|
|
341
362
|
*/
|
|
342
363
|
responseHeadersPolicy?: IResponseHeadersPolicy;
|
|
364
|
+
/**
|
|
365
|
+
* Override the CloudFront viewer protocol policy properties.
|
|
366
|
+
* @default ViewerProtocolPolicy.REDIRECT_TO_HTTPS
|
|
367
|
+
* @example
|
|
368
|
+
* ```js
|
|
369
|
+
* import { ViewerProtocolPolicy } from "aws-cdk-lib/aws-cloudfront";
|
|
370
|
+
*
|
|
371
|
+
* cdk: {
|
|
372
|
+
* viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
373
|
+
* }
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
viewerProtocolPolicy?: ViewerProtocolPolicy;
|
|
343
377
|
server?: Pick<CdkFunctionProps, "layers" | "vpc" | "vpcSubnets" | "securityGroups" | "allowAllOutbound" | "allowPublicSubnet" | "architecture" | "logRetention"> & Pick<FunctionProps, "copyFiles">;
|
|
344
378
|
};
|
|
345
379
|
}
|
|
380
|
+
type SsrSiteInvalidationNormalizedProps = Exclude<SsrSiteProps["invalidation"], undefined>;
|
|
346
381
|
export type SsrSiteNormalizedProps = SsrSiteProps & {
|
|
347
382
|
path: Exclude<SsrSiteProps["path"], undefined>;
|
|
348
383
|
typesPath: Exclude<SsrSiteProps["typesPath"], undefined>;
|
|
349
384
|
runtime: Exclude<SsrSiteProps["runtime"], undefined>;
|
|
350
385
|
timeout: Exclude<SsrSiteProps["timeout"], undefined>;
|
|
351
386
|
memorySize: Exclude<SsrSiteProps["memorySize"], undefined>;
|
|
352
|
-
|
|
387
|
+
invalidation: Exclude<SsrSiteProps["invalidation"], undefined> & {
|
|
388
|
+
paths: Exclude<SsrSiteInvalidationNormalizedProps["paths"], undefined>;
|
|
389
|
+
};
|
|
353
390
|
};
|
|
354
391
|
/**
|
|
355
392
|
* The `SsrSite` construct is a higher level CDK construct that makes it easy to create modern web apps with Server Side Rendering capabilities.
|
package/constructs/SsrSite.js
CHANGED
|
@@ -55,18 +55,22 @@ export class SsrSite extends Construct {
|
|
|
55
55
|
const props = {
|
|
56
56
|
path: ".",
|
|
57
57
|
typesPath: ".",
|
|
58
|
-
waitForInvalidation: false,
|
|
59
58
|
runtime: "nodejs18.x",
|
|
60
59
|
timeout: "10 seconds",
|
|
61
60
|
memorySize: "1024 MB",
|
|
62
61
|
...rawProps,
|
|
62
|
+
invalidation: {
|
|
63
|
+
wait: rawProps?.waitForInvalidation,
|
|
64
|
+
paths: "all",
|
|
65
|
+
...rawProps?.invalidation,
|
|
66
|
+
},
|
|
63
67
|
};
|
|
64
68
|
this.id = id;
|
|
65
69
|
this.props = props;
|
|
66
70
|
const app = scope.node.root;
|
|
67
71
|
const stack = Stack.of(this);
|
|
68
72
|
const self = this;
|
|
69
|
-
const { path: sitePath, typesPath, buildCommand, runtime, timeout, memorySize, edge, regional, dev, assets, nodejs, permissions, environment, bind, customDomain,
|
|
73
|
+
const { path: sitePath, typesPath, buildCommand, runtime, timeout, memorySize, edge, regional, dev, assets, nodejs, permissions, environment, bind, customDomain, invalidation, warm, cdk, } = props;
|
|
70
74
|
this.doNotDeploy = !stack.isActive || (app.mode === "dev" && !dev?.deploy);
|
|
71
75
|
validateSiteExists();
|
|
72
76
|
validateTimeout();
|
|
@@ -259,7 +263,6 @@ export class SsrSite extends Construct {
|
|
|
259
263
|
const distribution = new Distribution(self, "CDN", {
|
|
260
264
|
scopeOverride: self,
|
|
261
265
|
customDomain,
|
|
262
|
-
waitForInvalidation,
|
|
263
266
|
cdk: {
|
|
264
267
|
distribution: {
|
|
265
268
|
// these values can be overwritten
|
|
@@ -305,7 +308,7 @@ export class SsrSite extends Construct {
|
|
|
305
308
|
if (behavior.cacheType === "static") {
|
|
306
309
|
return {
|
|
307
310
|
origin,
|
|
308
|
-
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
311
|
+
viewerProtocolPolicy: cdk?.viewerProtocolPolicy ?? ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
309
312
|
allowedMethods: behavior.allowedMethods ?? AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
310
313
|
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
311
314
|
compress: true,
|
|
@@ -323,7 +326,7 @@ export class SsrSite extends Construct {
|
|
|
323
326
|
}
|
|
324
327
|
else if (behavior.cacheType === "server") {
|
|
325
328
|
return {
|
|
326
|
-
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
329
|
+
viewerProtocolPolicy: cdk?.viewerProtocolPolicy ?? ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
327
330
|
origin,
|
|
328
331
|
allowedMethods: behavior.allowedMethods ?? AllowedMethods.ALLOW_ALL,
|
|
329
332
|
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
@@ -689,18 +692,7 @@ function handler(event) {
|
|
|
689
692
|
return replaceValues;
|
|
690
693
|
}
|
|
691
694
|
function createDistributionInvalidation() {
|
|
692
|
-
const
|
|
693
|
-
if (cdnInvalidationStrategy === "never")
|
|
694
|
-
return;
|
|
695
|
-
if (plan.buildId) {
|
|
696
|
-
distribution.createInvalidation(plan.buildId);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (cdnInvalidationStrategy === "always") {
|
|
700
|
-
const buildId = Date.now().toString(16) + Math.random().toString(16).slice(2);
|
|
701
|
-
distribution.createInvalidation(buildId);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
695
|
+
const paths = invalidation.paths;
|
|
704
696
|
// We will generate a hash based on the contents of the S3 files with cache enabled.
|
|
705
697
|
// This will be used to determine if we need to invalidate our CloudFront cache.
|
|
706
698
|
const s3Origin = Object.values(plan.origins).find((origin) => origin.type === "s3");
|
|
@@ -711,46 +703,64 @@ function handler(event) {
|
|
|
711
703
|
return;
|
|
712
704
|
// Build invalidation paths
|
|
713
705
|
const invalidationPaths = [];
|
|
714
|
-
if (
|
|
706
|
+
if (paths === "none") {
|
|
707
|
+
}
|
|
708
|
+
else if (paths === "all") {
|
|
709
|
+
invalidationPaths.push("/*");
|
|
710
|
+
}
|
|
711
|
+
else if (paths === "versioned") {
|
|
715
712
|
cachedS3Files.forEach((item) => {
|
|
716
713
|
if (!item.versionedSubDir)
|
|
717
714
|
return;
|
|
718
715
|
invalidationPaths.push(path.posix.join("/", item.to, item.versionedSubDir, "*"));
|
|
719
716
|
});
|
|
720
717
|
}
|
|
718
|
+
else {
|
|
719
|
+
invalidationPaths.push(...paths);
|
|
720
|
+
}
|
|
721
721
|
if (invalidationPaths.length === 0)
|
|
722
|
-
|
|
722
|
+
return;
|
|
723
723
|
// Build build ID
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
724
|
+
let invalidationBuildId;
|
|
725
|
+
if (plan.buildId) {
|
|
726
|
+
invalidationBuildId = plan.buildId;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
const hash = crypto.createHash("md5");
|
|
730
|
+
cachedS3Files.forEach((item) => {
|
|
731
|
+
// The below options are needed to support following symlinks when building zip files:
|
|
732
|
+
// - nodir: This will prevent symlinks themselves from being copied into the zip.
|
|
733
|
+
// - follow: This will follow symlinks and copy the files within.
|
|
734
|
+
// For versioned files, use file path for digest since file version in name should change on content change
|
|
735
|
+
if (item.versionedSubDir) {
|
|
736
|
+
globSync("**", {
|
|
737
|
+
dot: true,
|
|
738
|
+
nodir: true,
|
|
739
|
+
follow: true,
|
|
740
|
+
cwd: path.resolve(sitePath, item.from, item.versionedSubDir),
|
|
741
|
+
}).forEach((filePath) => hash.update(filePath));
|
|
742
|
+
}
|
|
743
|
+
// For non-versioned files, use file content for digest
|
|
744
|
+
if (paths !== "versioned") {
|
|
745
|
+
globSync("**", {
|
|
746
|
+
ignore: item.versionedSubDir
|
|
747
|
+
? [path.posix.join(item.versionedSubDir, "**")]
|
|
748
|
+
: undefined,
|
|
749
|
+
dot: true,
|
|
750
|
+
nodir: true,
|
|
751
|
+
follow: true,
|
|
752
|
+
cwd: path.resolve(sitePath, item.from),
|
|
753
|
+
}).forEach((filePath) => hash.update(fs.readFileSync(path.resolve(sitePath, item.from, filePath))));
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
invalidationBuildId = hash.digest("hex");
|
|
757
|
+
Logger.debug(`Generated build ID ${invalidationBuildId}`);
|
|
758
|
+
}
|
|
759
|
+
distribution.createInvalidation({
|
|
760
|
+
version: invalidationBuildId,
|
|
761
|
+
paths: invalidationPaths,
|
|
762
|
+
wait: invalidation.wait,
|
|
750
763
|
});
|
|
751
|
-
const buildId = hash.digest("hex");
|
|
752
|
-
Logger.debug(`Generated build ID ${buildId}`);
|
|
753
|
-
distribution.createInvalidation(buildId, invalidationPaths);
|
|
754
764
|
}
|
|
755
765
|
}
|
|
756
766
|
static buildDefaultServerCachePolicyProps(allowedHeaders) {
|
package/constructs/StaticSite.js
CHANGED
|
@@ -49,7 +49,6 @@ export class StaticSite extends Construct {
|
|
|
49
49
|
this.id = id;
|
|
50
50
|
this.props = {
|
|
51
51
|
path: ".",
|
|
52
|
-
waitForInvalidation: false,
|
|
53
52
|
...props,
|
|
54
53
|
};
|
|
55
54
|
this.doNotDeploy =
|
|
@@ -74,7 +73,9 @@ export class StaticSite extends Construct {
|
|
|
74
73
|
const s3deployCR = this.createS3Deployment(assets, filenamesAsset);
|
|
75
74
|
this.distribution.node.addDependency(s3deployCR);
|
|
76
75
|
// Invalidate CloudFront
|
|
77
|
-
this.distribution.createInvalidation(
|
|
76
|
+
this.distribution.createInvalidation({
|
|
77
|
+
version: this.generateInvalidationId(assets),
|
|
78
|
+
});
|
|
78
79
|
});
|
|
79
80
|
app.registerTypes(this);
|
|
80
81
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sideEffects": false,
|
|
3
3
|
"name": "sst",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.32.1",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sst": "cli/sst.js"
|
|
7
7
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"@types/ws": "^8.5.3",
|
|
121
121
|
"@types/yargs": "^17.0.13",
|
|
122
122
|
"archiver": "^5.3.1",
|
|
123
|
-
"astro-sst": "2.
|
|
123
|
+
"astro-sst": "2.32.1",
|
|
124
124
|
"async": "^3.2.4",
|
|
125
125
|
"tsx": "^3.12.1",
|
|
126
126
|
"typescript": "^5.2.2",
|