sst 2.26.2 → 2.26.4
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/cli/commands/types.js +3 -7
- package/constructs/Api.js +2 -0
- package/constructs/ApiGatewayV1Api.js +2 -0
- package/constructs/App.d.ts +3 -1
- package/constructs/App.js +63 -62
- package/constructs/AppSyncApi.js +2 -0
- package/constructs/AstroSite.d.ts +50 -11
- package/constructs/AstroSite.js +69 -51
- package/constructs/Auth.js +2 -0
- package/constructs/Bucket.js +2 -0
- package/constructs/Cognito.js +2 -0
- package/constructs/Cron.js +2 -0
- package/constructs/EdgeFunction.d.ts +3 -4
- package/constructs/EdgeFunction.js +13 -9
- package/constructs/EventBus.js +2 -0
- package/constructs/Function.d.ts +5 -1
- package/constructs/Function.js +8 -6
- package/constructs/Job.js +3 -3
- package/constructs/KinesisStream.js +2 -0
- package/constructs/NextjsSite.d.ts +84 -22
- package/constructs/NextjsSite.js +150 -254
- package/constructs/Parameter.js +2 -0
- package/constructs/Queue.js +2 -0
- package/constructs/RDS.js +5 -4
- package/constructs/RemixSite.d.ts +65 -11
- package/constructs/RemixSite.js +112 -71
- package/constructs/Script.js +2 -0
- package/constructs/Secret.js +2 -0
- package/constructs/Service.js +4 -3
- package/constructs/SolidStartSite.d.ts +48 -9
- package/constructs/SolidStartSite.js +68 -40
- package/constructs/SsrFunction.d.ts +4 -5
- package/constructs/SsrFunction.js +18 -13
- package/constructs/SsrSite.d.ts +74 -68
- package/constructs/SsrSite.js +657 -682
- package/constructs/StaticSite.js +2 -0
- package/constructs/SvelteKitSite.d.ts +80 -12
- package/constructs/SvelteKitSite.js +90 -64
- package/constructs/Table.js +2 -0
- package/constructs/Topic.js +2 -0
- package/constructs/WebSocketApi.js +2 -0
- package/constructs/deprecated/NextjsSite.js +1 -0
- package/constructs/future/Auth.js +1 -0
- package/package.json +1 -1
- package/support/remix-site-function/edge-server.js +2 -4
- package/support/remix-site-function/regional-server.js +2 -4
package/constructs/SsrSite.js
CHANGED
|
@@ -25,12 +25,12 @@ import { Secret } from "./Secret.js";
|
|
|
25
25
|
import { SsrFunction } from "./SsrFunction.js";
|
|
26
26
|
import { EdgeFunction } from "./EdgeFunction.js";
|
|
27
27
|
import { getBuildCmdEnvironment, } from "./BaseSite.js";
|
|
28
|
-
import { useDeferredTasks } from "./deferred_task.js";
|
|
29
28
|
import { toCdkDuration } from "./util/duration.js";
|
|
30
29
|
import { attachPermissionsToRole } from "./util/permission.js";
|
|
31
30
|
import { getParameterPath, } from "./util/functionBinding.js";
|
|
32
31
|
import { useProject } from "../project.js";
|
|
33
32
|
import { VisibleError } from "../error.js";
|
|
33
|
+
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
34
34
|
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
|
35
35
|
/**
|
|
36
36
|
* The `SsrSite` construct is a higher level CDK construct that makes it easy to create modern web apps with Server Side Rendering capabilities.
|
|
@@ -47,90 +47,670 @@ export class SsrSite extends Construct {
|
|
|
47
47
|
id;
|
|
48
48
|
props;
|
|
49
49
|
doNotDeploy;
|
|
50
|
-
|
|
51
|
-
deferredTaskCallbacks = [];
|
|
52
|
-
serverLambdaForEdge;
|
|
53
|
-
serverLambdaForRegional;
|
|
54
|
-
serverLambdaForDev;
|
|
55
|
-
serverUrlSigningFunction;
|
|
50
|
+
typesPath = ".";
|
|
56
51
|
bucket;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
serverBehaviorOriginRequestPolicy;
|
|
60
|
-
staticCfFunction;
|
|
61
|
-
s3Origin;
|
|
52
|
+
serverFunction;
|
|
53
|
+
serverFunctionForDev;
|
|
62
54
|
distribution;
|
|
63
|
-
constructor(scope, id,
|
|
64
|
-
super(scope,
|
|
65
|
-
const
|
|
66
|
-
const stack = Stack.of(this);
|
|
67
|
-
this.id = id;
|
|
68
|
-
this.props = {
|
|
55
|
+
constructor(scope, id, rawProps) {
|
|
56
|
+
super(scope, rawProps?.cdk?.id || id);
|
|
57
|
+
const props = {
|
|
69
58
|
path: ".",
|
|
70
59
|
waitForInvalidation: false,
|
|
71
60
|
runtime: "nodejs18.x",
|
|
72
61
|
timeout: "10 seconds",
|
|
73
62
|
memorySize: "1024 MB",
|
|
74
|
-
...
|
|
63
|
+
...rawProps,
|
|
75
64
|
};
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
this.id = id;
|
|
66
|
+
this.props = props;
|
|
67
|
+
const app = scope.node.root;
|
|
68
|
+
const stack = Stack.of(this);
|
|
69
|
+
const self = this;
|
|
70
|
+
const { path: sitePath, buildCommand, runtime, timeout, memorySize, edge, regional, dev, nodejs, permissions, environment, bind, customDomain, waitForInvalidation, fileOptions, warm, cdk, } = props;
|
|
71
|
+
this.doNotDeploy = !stack.isActive || (app.mode === "dev" && !dev?.deploy);
|
|
72
|
+
validateSiteExists();
|
|
73
|
+
validateTimeout();
|
|
74
|
+
writeTypesFile(this.typesPath);
|
|
75
|
+
useSites().add(stack.stackName, id, this.constructor.name, props);
|
|
83
76
|
if (this.doNotDeploy) {
|
|
84
77
|
// @ts-expect-error
|
|
85
|
-
this.bucket = this.
|
|
86
|
-
this.
|
|
78
|
+
this.bucket = this.distribution = null;
|
|
79
|
+
this.serverFunctionForDev = createServerFunctionForDev();
|
|
80
|
+
app.registerTypes(this);
|
|
87
81
|
return;
|
|
88
82
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
let s3DeployCRs = [];
|
|
84
|
+
let ssrFunctions = [];
|
|
85
|
+
let singletonAwsCliLayer;
|
|
86
|
+
let singletonUrlSigner;
|
|
87
|
+
let singletonCachePolicy;
|
|
88
|
+
let singletonOriginRequestPolicy;
|
|
89
|
+
// Create Bucket
|
|
90
|
+
const bucket = createS3Bucket();
|
|
91
|
+
// Build app
|
|
92
|
+
buildApp();
|
|
93
|
+
const plan = this.plan(bucket);
|
|
94
|
+
// Create CloudFront
|
|
95
|
+
const cfFunctions = createCloudFrontFunctions();
|
|
96
|
+
const edgeFunctions = createEdgeFunctions();
|
|
97
|
+
const origins = createOrigins();
|
|
98
|
+
const distribution = createCloudFrontDistribution();
|
|
99
|
+
distribution.createInvalidation(plan.buildId ?? generateBuildId());
|
|
100
|
+
// Create Warmer
|
|
101
|
+
createWarmer();
|
|
102
|
+
this.bucket = bucket;
|
|
103
|
+
this.distribution = distribution;
|
|
104
|
+
this.serverFunction =
|
|
105
|
+
ssrFunctions.length > 0
|
|
106
|
+
? ssrFunctions[0]
|
|
107
|
+
: Object.values(edgeFunctions).length > 0
|
|
108
|
+
? Object.values(edgeFunctions)[0]
|
|
109
|
+
: undefined;
|
|
110
|
+
app.registerTypes(this);
|
|
111
|
+
function validateSiteExists() {
|
|
112
|
+
if (!fs.existsSync(sitePath)) {
|
|
113
|
+
throw new Error(`No site found at "${path.resolve(sitePath)}"`);
|
|
114
|
+
}
|
|
94
115
|
}
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
function validateTimeout() {
|
|
117
|
+
const num = typeof timeout === "number"
|
|
118
|
+
? timeout
|
|
119
|
+
: toCdkDuration(timeout).toSeconds();
|
|
120
|
+
const limit = edge ? 30 : 180;
|
|
121
|
+
if (num > limit) {
|
|
122
|
+
throw new Error(edge
|
|
123
|
+
? `Timeout must be less than or equal to 30 seconds when the "edge" flag is enabled.`
|
|
124
|
+
: `Timeout must be less than or equal to 180 seconds.`);
|
|
125
|
+
}
|
|
97
126
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
function writeTypesFile(typesPath) {
|
|
128
|
+
const filePath = path.resolve(sitePath, typesPath, "sst-env.d.ts");
|
|
129
|
+
// Do not override the types file if it already exists
|
|
130
|
+
if (fs.existsSync(filePath))
|
|
131
|
+
return;
|
|
132
|
+
const relPathToSstTypesFile = path.join(path.relative(path.dirname(filePath), useProject().paths.root), ".sst/types/index.ts");
|
|
133
|
+
fs.writeFileSync(filePath, `/// <reference path="${relPathToSstTypesFile}" />`);
|
|
134
|
+
}
|
|
135
|
+
function buildApp() {
|
|
136
|
+
if (app.isRunningSSTTest())
|
|
137
|
+
return;
|
|
138
|
+
const defaultCommand = "npm run build";
|
|
139
|
+
const cmd = buildCommand || defaultCommand;
|
|
140
|
+
if (cmd === defaultCommand) {
|
|
141
|
+
// Ensure that the site has a build script defined
|
|
142
|
+
if (!fs.existsSync(path.join(sitePath, "package.json"))) {
|
|
143
|
+
throw new Error(`No package.json found at "${sitePath}".`);
|
|
144
|
+
}
|
|
145
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(sitePath, "package.json")).toString());
|
|
146
|
+
if (!packageJson.scripts || !packageJson.scripts.build) {
|
|
147
|
+
throw new Error(`No "build" script found within package.json in "${sitePath}".`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Run build
|
|
151
|
+
Logger.debug(`Running "${cmd}" script`);
|
|
152
|
+
try {
|
|
153
|
+
execSync(cmd, {
|
|
154
|
+
cwd: sitePath,
|
|
155
|
+
stdio: "inherit",
|
|
156
|
+
env: {
|
|
157
|
+
SST: "1",
|
|
158
|
+
...process.env,
|
|
159
|
+
...getBuildCmdEnvironment(environment),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
throw new VisibleError(`There was a problem building the "${id}" site.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function createS3Bucket() {
|
|
168
|
+
// cdk.bucket is an imported construct
|
|
169
|
+
if (cdk?.bucket && isCDKConstruct(cdk?.bucket)) {
|
|
170
|
+
return cdk.bucket;
|
|
171
|
+
}
|
|
172
|
+
// cdk.bucket is a prop
|
|
173
|
+
return new Bucket(self, "S3Bucket", {
|
|
174
|
+
publicReadAccess: false,
|
|
175
|
+
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
176
|
+
autoDeleteObjects: true,
|
|
177
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
178
|
+
enforceSSL: true,
|
|
179
|
+
...cdk?.bucket,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function createServerFunctionForDev() {
|
|
183
|
+
const role = new Role(self, "ServerFunctionRole", {
|
|
184
|
+
assumedBy: new CompositePrincipal(new AccountPrincipal(app.account), new ServicePrincipal("lambda.amazonaws.com")),
|
|
185
|
+
maxSessionDuration: CdkDuration.hours(12),
|
|
186
|
+
});
|
|
187
|
+
return new SsrFunction(self, `ServerFunction`, {
|
|
188
|
+
description: "Server handler placeholder",
|
|
189
|
+
bundle: path.join(__dirname, "../support/ssr-site-function-stub"),
|
|
190
|
+
handler: "index.handler",
|
|
191
|
+
runtime,
|
|
192
|
+
memorySize,
|
|
193
|
+
timeout,
|
|
194
|
+
role,
|
|
195
|
+
bind,
|
|
196
|
+
environment,
|
|
197
|
+
permissions,
|
|
198
|
+
// note: do not need to set vpc settings b/c this function is not being used
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function createWarmer() {
|
|
202
|
+
// note: Currently all sites have a single server function. When we add
|
|
203
|
+
// support for multiple server functions (ie. route splitting), we
|
|
204
|
+
// need to handle warming multiple functions.
|
|
205
|
+
if (!warm)
|
|
206
|
+
return;
|
|
207
|
+
if (warm && edge) {
|
|
208
|
+
throw new VisibleError(`In the "${id}" Site, warming is currently supported only for the regional mode.`);
|
|
209
|
+
}
|
|
210
|
+
if (ssrFunctions.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
// Create warmer function
|
|
213
|
+
const warmer = new CdkFunction(self, "WarmerFunction", {
|
|
214
|
+
description: "Next.js warmer",
|
|
215
|
+
code: Code.fromAsset(plan.warmerConfig?.function ??
|
|
216
|
+
path.join(__dirname, "../support/ssr-warmer")),
|
|
217
|
+
runtime: Runtime.NODEJS_18_X,
|
|
218
|
+
handler: "index.handler",
|
|
219
|
+
timeout: CdkDuration.minutes(15),
|
|
220
|
+
memorySize: 1024,
|
|
221
|
+
environment: {
|
|
222
|
+
FUNCTION_NAME: ssrFunctions[0].functionName,
|
|
223
|
+
CONCURRENCY: warm.toString(),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
ssrFunctions[0].grantInvoke(warmer);
|
|
227
|
+
// Create cron job
|
|
228
|
+
new Rule(self, "WarmerRule", {
|
|
229
|
+
schedule: Schedule.rate(CdkDuration.minutes(5)),
|
|
230
|
+
targets: [new LambdaFunction(warmer, { retryAttempts: 0 })],
|
|
231
|
+
});
|
|
232
|
+
// Create custom resource to prewarm on deploy
|
|
233
|
+
const policy = new Policy(self, "PrewarmerPolicy", {
|
|
234
|
+
statements: [
|
|
235
|
+
new PolicyStatement({
|
|
236
|
+
effect: Effect.ALLOW,
|
|
237
|
+
actions: ["lambda:InvokeFunction"],
|
|
238
|
+
resources: [warmer.functionArn],
|
|
239
|
+
}),
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
stack.customResourceHandler.role?.attachInlinePolicy(policy);
|
|
243
|
+
const resource = new CustomResource(self, "Prewarmer", {
|
|
244
|
+
serviceToken: stack.customResourceHandler.functionArn,
|
|
245
|
+
resourceType: "Custom::FunctionInvoker",
|
|
246
|
+
properties: {
|
|
247
|
+
version: Date.now().toString(),
|
|
248
|
+
functionName: warmer.functionName,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
resource.node.addDependency(policy);
|
|
252
|
+
}
|
|
253
|
+
function createCloudFrontDistribution() {
|
|
254
|
+
const distribution = new Distribution(self, "CDN", {
|
|
255
|
+
scopeOverride: self,
|
|
256
|
+
customDomain,
|
|
257
|
+
waitForInvalidation,
|
|
258
|
+
cdk: {
|
|
259
|
+
distribution: {
|
|
260
|
+
// these values can be overwritten
|
|
261
|
+
defaultRootObject: "",
|
|
262
|
+
// override props.
|
|
263
|
+
...cdk?.distribution,
|
|
264
|
+
// these values can NOT be overwritten
|
|
265
|
+
defaultBehavior: buildBehavior(plan.behaviors.find((behavior) => !behavior.pattern)),
|
|
266
|
+
additionalBehaviors: {
|
|
267
|
+
...plan.behaviors
|
|
268
|
+
.filter((behavior) => behavior.pattern)
|
|
269
|
+
.reduce((acc, behavior) => {
|
|
270
|
+
acc[behavior.pattern] = buildBehavior(behavior);
|
|
271
|
+
return acc;
|
|
272
|
+
}, {}),
|
|
273
|
+
...(cdk?.distribution?.additionalBehaviors || {}),
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
// allow all functions to invalidate the distribution
|
|
279
|
+
const policy = new Policy(self, "ServerFunctionInvalidatorPolicy", {
|
|
280
|
+
statements: [
|
|
281
|
+
new PolicyStatement({
|
|
282
|
+
actions: ["cloudfront:CreateInvalidation"],
|
|
283
|
+
resources: [
|
|
284
|
+
`arn:${stack.partition}:cloudfront::${stack.account}:distribution/${distribution.cdk.distribution.distributionId}`,
|
|
285
|
+
],
|
|
286
|
+
}),
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
ssrFunctions.forEach((fn) => fn.role?.attachInlinePolicy(policy));
|
|
290
|
+
Object.values(edgeFunctions).forEach((fn) => fn.role?.attachInlinePolicy(policy));
|
|
291
|
+
// create distribution after s3 upload finishes
|
|
292
|
+
s3DeployCRs.forEach((cr) => distribution.node.addDependency(cr));
|
|
293
|
+
return distribution;
|
|
294
|
+
}
|
|
295
|
+
function buildBehavior(behavior) {
|
|
296
|
+
const origin = origins[behavior.origin];
|
|
297
|
+
const edgeFunction = edgeFunctions[behavior.edgeFunction || ""];
|
|
298
|
+
const cfFunction = cfFunctions[behavior.cfFunction || ""];
|
|
299
|
+
if (behavior.cacheType === "static") {
|
|
300
|
+
return {
|
|
301
|
+
origin,
|
|
302
|
+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
303
|
+
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
304
|
+
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
305
|
+
compress: true,
|
|
306
|
+
cachePolicy: CachePolicy.CACHING_OPTIMIZED,
|
|
307
|
+
responseHeadersPolicy: cdk?.responseHeadersPolicy,
|
|
308
|
+
functionAssociations: cfFunction
|
|
309
|
+
? [
|
|
310
|
+
{
|
|
311
|
+
eventType: CfFunctionEventType.VIEWER_REQUEST,
|
|
312
|
+
function: cfFunction,
|
|
313
|
+
},
|
|
314
|
+
]
|
|
315
|
+
: undefined,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
else if (behavior.cacheType === "server") {
|
|
319
|
+
return {
|
|
320
|
+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
321
|
+
origin,
|
|
322
|
+
allowedMethods: AllowedMethods.ALLOW_ALL,
|
|
323
|
+
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
324
|
+
compress: true,
|
|
325
|
+
cachePolicy: cdk?.serverCachePolicy ?? useServerBehaviorCachePolicy(),
|
|
326
|
+
responseHeadersPolicy: cdk?.responseHeadersPolicy,
|
|
327
|
+
originRequestPolicy: useServerBehaviorOriginRequestPolicy(),
|
|
328
|
+
...(cdk?.distribution?.defaultBehavior || {}),
|
|
329
|
+
functionAssociations: [
|
|
330
|
+
...(cfFunction
|
|
331
|
+
? [
|
|
332
|
+
{
|
|
333
|
+
eventType: CfFunctionEventType.VIEWER_REQUEST,
|
|
334
|
+
function: cfFunction,
|
|
335
|
+
},
|
|
336
|
+
]
|
|
337
|
+
: []),
|
|
338
|
+
...(cdk?.distribution?.defaultBehavior?.functionAssociations || []),
|
|
339
|
+
],
|
|
340
|
+
edgeLambdas: [
|
|
341
|
+
...(edgeFunction
|
|
342
|
+
? [
|
|
343
|
+
{
|
|
344
|
+
includeBody: true,
|
|
345
|
+
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
|
|
346
|
+
functionVersion: edgeFunction.currentVersion,
|
|
347
|
+
},
|
|
348
|
+
]
|
|
349
|
+
: []),
|
|
350
|
+
...(regional?.enableServerUrlIamAuth
|
|
351
|
+
? [
|
|
352
|
+
{
|
|
353
|
+
includeBody: true,
|
|
354
|
+
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
|
|
355
|
+
functionVersion: useFunctionUrlSigningFunction().currentVersion,
|
|
356
|
+
},
|
|
357
|
+
]
|
|
358
|
+
: []),
|
|
359
|
+
...(cdk?.distribution?.defaultBehavior?.edgeLambdas || []),
|
|
360
|
+
],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
throw new Error(`Invalid behavior type in the "${id}" site.`);
|
|
364
|
+
}
|
|
365
|
+
function createCloudFrontFunctions() {
|
|
366
|
+
const functions = {};
|
|
367
|
+
Object.entries(plan.cloudFrontFunctions ?? {}).forEach(([name, { constructId, injections }]) => {
|
|
368
|
+
functions[name] = new CfFunction(self, constructId, {
|
|
369
|
+
code: CfFunctionCode.fromInline(`
|
|
370
|
+
function handler(event) {
|
|
371
|
+
var request = event.request;
|
|
372
|
+
${injections.join("\n")}
|
|
373
|
+
return request;
|
|
374
|
+
}`),
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
return functions;
|
|
378
|
+
}
|
|
379
|
+
function createEdgeFunctions() {
|
|
380
|
+
const functions = {};
|
|
381
|
+
Object.entries(plan.edgeFunctions ?? {}).forEach(([name, { constructId, function: props }]) => {
|
|
382
|
+
const fn = new EdgeFunction(self, constructId, {
|
|
383
|
+
runtime,
|
|
384
|
+
timeout,
|
|
385
|
+
memorySize,
|
|
386
|
+
bind,
|
|
387
|
+
permissions,
|
|
388
|
+
...props,
|
|
389
|
+
nodejs: {
|
|
390
|
+
format: "esm",
|
|
391
|
+
...nodejs,
|
|
392
|
+
...props.nodejs,
|
|
393
|
+
esbuild: {
|
|
394
|
+
...nodejs?.esbuild,
|
|
395
|
+
...props.nodejs?.esbuild,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
environment: {
|
|
399
|
+
...environment,
|
|
400
|
+
...props.environment,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
bucket.grantReadWrite(fn.role);
|
|
404
|
+
functions[name] = fn;
|
|
405
|
+
});
|
|
406
|
+
return functions;
|
|
407
|
+
}
|
|
408
|
+
function createOrigins() {
|
|
409
|
+
const origins = {};
|
|
410
|
+
Object.entries(plan.origins ?? {}).forEach(([name, props]) => {
|
|
411
|
+
if (!props)
|
|
412
|
+
return;
|
|
413
|
+
// S3 Origin
|
|
414
|
+
if (props.type === "s3") {
|
|
415
|
+
origins[name] = new S3Origin(bucket, {
|
|
416
|
+
originPath: "/" + (props.originPath ?? ""),
|
|
417
|
+
});
|
|
418
|
+
const assets = createS3OriginAssets(props.copy);
|
|
419
|
+
const assetFileOptions = fileOptions || createS3OriginAssetFileOptions(props.copy);
|
|
420
|
+
const s3deployCR = createS3OriginDeployment(assets, assetFileOptions);
|
|
421
|
+
s3DeployCRs.push(s3deployCR);
|
|
422
|
+
}
|
|
423
|
+
// Server Origin
|
|
424
|
+
else if (props.type === "function") {
|
|
425
|
+
const fn = new SsrFunction(self, props.constructId, {
|
|
426
|
+
runtime,
|
|
427
|
+
timeout,
|
|
428
|
+
memorySize,
|
|
429
|
+
bind,
|
|
430
|
+
permissions,
|
|
431
|
+
...props.function,
|
|
432
|
+
nodejs: {
|
|
433
|
+
format: "esm",
|
|
434
|
+
...nodejs,
|
|
435
|
+
...props.function.nodejs,
|
|
436
|
+
esbuild: {
|
|
437
|
+
...nodejs?.esbuild,
|
|
438
|
+
...props.function.nodejs?.esbuild,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
environment: {
|
|
442
|
+
...environment,
|
|
443
|
+
...props.function.environment,
|
|
444
|
+
},
|
|
445
|
+
...cdk?.server,
|
|
446
|
+
});
|
|
447
|
+
ssrFunctions.push(fn);
|
|
448
|
+
bucket.grantReadWrite(fn?.role);
|
|
449
|
+
const fnUrl = fn.addFunctionUrl({
|
|
450
|
+
authType: regional?.enableServerUrlIamAuth
|
|
451
|
+
? FunctionUrlAuthType.AWS_IAM
|
|
452
|
+
: FunctionUrlAuthType.NONE,
|
|
453
|
+
invokeMode: props.streaming
|
|
454
|
+
? InvokeMode.RESPONSE_STREAM
|
|
455
|
+
: undefined,
|
|
456
|
+
});
|
|
457
|
+
if (regional?.enableServerUrlIamAuth) {
|
|
458
|
+
useFunctionUrlSigningFunction().attachPermissions([
|
|
459
|
+
new PolicyStatement({
|
|
460
|
+
actions: ["lambda:InvokeFunctionUrl"],
|
|
461
|
+
resources: [fn.functionArn],
|
|
462
|
+
}),
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
465
|
+
origins[name] = new HttpOrigin(Fn.parseDomainName(fnUrl.url), {
|
|
466
|
+
readTimeout: typeof timeout === "string"
|
|
467
|
+
? toCdkDuration(timeout)
|
|
468
|
+
: CdkDuration.seconds(timeout),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
// Image Optimization Origin
|
|
472
|
+
else if (props.type === "image-optimization-function") {
|
|
473
|
+
const fn = new CdkFunction(self, `ImageFunction`, {
|
|
474
|
+
currentVersionOptions: {
|
|
475
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
476
|
+
},
|
|
477
|
+
logRetention: RetentionDays.THREE_DAYS,
|
|
478
|
+
timeout: CdkDuration.seconds(25),
|
|
479
|
+
initialPolicy: [
|
|
480
|
+
new PolicyStatement({
|
|
481
|
+
actions: ["s3:GetObject"],
|
|
482
|
+
resources: [bucket.arnForObjects("*")],
|
|
483
|
+
}),
|
|
484
|
+
],
|
|
485
|
+
...props.function,
|
|
486
|
+
});
|
|
487
|
+
const fnUrl = fn.addFunctionUrl({
|
|
488
|
+
authType: regional?.enableServerUrlIamAuth
|
|
489
|
+
? FunctionUrlAuthType.AWS_IAM
|
|
490
|
+
: FunctionUrlAuthType.NONE,
|
|
491
|
+
});
|
|
492
|
+
if (regional?.enableServerUrlIamAuth) {
|
|
493
|
+
useFunctionUrlSigningFunction().attachPermissions([
|
|
494
|
+
new PolicyStatement({
|
|
495
|
+
actions: ["lambda:InvokeFunctionUrl"],
|
|
496
|
+
resources: [fn.functionArn],
|
|
497
|
+
}),
|
|
498
|
+
]);
|
|
499
|
+
}
|
|
500
|
+
origins[name] = new HttpOrigin(Fn.parseDomainName(fnUrl.url));
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
return origins;
|
|
504
|
+
}
|
|
505
|
+
function createS3OriginAssets(copy) {
|
|
506
|
+
// Create temp folder, clean up if exists
|
|
507
|
+
const zipOutDir = path.resolve(path.join(useProject().paths.artifacts, `Site-${id}-${self.node.addr}`));
|
|
508
|
+
fs.rmSync(zipOutDir, { recursive: true, force: true });
|
|
509
|
+
// Create zip files
|
|
510
|
+
const script = path.resolve(__dirname, "../support/base-site-archiver.mjs");
|
|
511
|
+
const fileSizeLimit = app.isRunningSSTTest()
|
|
512
|
+
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
513
|
+
// @ts-ignore: "sstTestFileSizeLimitOverride" not exposed in props
|
|
514
|
+
props.sstTestFileSizeLimitOverride || 200
|
|
515
|
+
: 200;
|
|
516
|
+
const result = spawn.sync("node", [
|
|
517
|
+
script,
|
|
518
|
+
Buffer.from(JSON.stringify(copy.map((files) => ({
|
|
519
|
+
src: path.join(sitePath, files.from),
|
|
520
|
+
tar: files.to,
|
|
521
|
+
})))).toString("base64"),
|
|
522
|
+
zipOutDir,
|
|
523
|
+
`${fileSizeLimit}`,
|
|
524
|
+
], {
|
|
525
|
+
stdio: "inherit",
|
|
526
|
+
});
|
|
527
|
+
if (result.status !== 0) {
|
|
528
|
+
throw new Error(`There was a problem generating the assets package.`);
|
|
529
|
+
}
|
|
530
|
+
// Create S3 Assets for each zip file
|
|
531
|
+
const assets = [];
|
|
532
|
+
for (let partId = 0;; partId++) {
|
|
533
|
+
const zipFilePath = path.join(zipOutDir, `part${partId}.zip`);
|
|
534
|
+
if (!fs.existsSync(zipFilePath)) {
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
assets.push(new Asset(self, `Asset${partId}`, {
|
|
538
|
+
path: zipFilePath,
|
|
539
|
+
}));
|
|
128
540
|
}
|
|
129
|
-
|
|
541
|
+
return assets;
|
|
542
|
+
}
|
|
543
|
+
function createS3OriginAssetFileOptions(copy) {
|
|
544
|
+
const fileOptions = [];
|
|
545
|
+
for (const files of copy) {
|
|
546
|
+
if (!files.cached)
|
|
547
|
+
continue;
|
|
548
|
+
const filesPath = path.join(sitePath, files.from);
|
|
549
|
+
for (const item of fs.readdirSync(filesPath)) {
|
|
550
|
+
const itemPath = path.join(filesPath, item);
|
|
551
|
+
const isDir = fs.statSync(itemPath).isDirectory();
|
|
552
|
+
fileOptions.push({
|
|
553
|
+
exclude: "*",
|
|
554
|
+
include: path.posix.join(files.to, item, isDir ? "*" : ""),
|
|
555
|
+
cacheControl: item === files.versionedSubDir
|
|
556
|
+
? // Versioned files will be cached for 1 year (immutable) both at
|
|
557
|
+
// the CDN and browser level.
|
|
558
|
+
"public,max-age=31536000,immutable"
|
|
559
|
+
: // Un-versioned files will be cached for 1 year at the CDN level.
|
|
560
|
+
// But not at the browser level. CDN cache will be invalidated on deploy.
|
|
561
|
+
"public,max-age=0,s-maxage=31536000,must-revalidate",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return fileOptions;
|
|
566
|
+
}
|
|
567
|
+
function createS3OriginDeployment(assets, fileOptions) {
|
|
568
|
+
// Create a Lambda function that will be doing the uploading
|
|
569
|
+
const uploader = new CdkFunction(self, "S3Uploader", {
|
|
570
|
+
code: Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
|
|
571
|
+
layers: [useAwsCliLayer()],
|
|
572
|
+
runtime: Runtime.PYTHON_3_11,
|
|
573
|
+
handler: "s3-upload.handler",
|
|
574
|
+
timeout: CdkDuration.minutes(15),
|
|
575
|
+
memorySize: 1024,
|
|
576
|
+
});
|
|
577
|
+
bucket.grantReadWrite(uploader);
|
|
578
|
+
assets.forEach((asset) => asset.grantRead(uploader));
|
|
579
|
+
// Create the custom resource function
|
|
580
|
+
const handler = new CdkFunction(self, "S3Handler", {
|
|
581
|
+
code: Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
|
|
582
|
+
layers: [useAwsCliLayer()],
|
|
583
|
+
runtime: Runtime.PYTHON_3_11,
|
|
584
|
+
handler: "s3-handler.handler",
|
|
585
|
+
timeout: CdkDuration.minutes(15),
|
|
586
|
+
memorySize: 1024,
|
|
587
|
+
environment: {
|
|
588
|
+
UPLOADER_FUNCTION_NAME: uploader.functionName,
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
bucket.grantReadWrite(handler);
|
|
592
|
+
uploader.grantInvoke(handler);
|
|
593
|
+
// Create custom resource
|
|
594
|
+
return new CustomResource(self, "S3Deployment", {
|
|
595
|
+
serviceToken: handler.functionArn,
|
|
596
|
+
resourceType: "Custom::SSTBucketDeployment",
|
|
597
|
+
properties: {
|
|
598
|
+
Sources: assets.map((asset) => ({
|
|
599
|
+
BucketName: asset.s3BucketName,
|
|
600
|
+
ObjectKey: asset.s3ObjectKey,
|
|
601
|
+
})),
|
|
602
|
+
DestinationBucketName: bucket.bucketName,
|
|
603
|
+
FileOptions: (fileOptions || []).map(({ exclude, include, cacheControl, contentType }) => {
|
|
604
|
+
if (typeof exclude === "string") {
|
|
605
|
+
exclude = [exclude];
|
|
606
|
+
}
|
|
607
|
+
if (typeof include === "string") {
|
|
608
|
+
include = [include];
|
|
609
|
+
}
|
|
610
|
+
return [
|
|
611
|
+
...exclude.map((per) => ["--exclude", per]),
|
|
612
|
+
...include.map((per) => ["--include", per]),
|
|
613
|
+
["--cache-control", cacheControl],
|
|
614
|
+
contentType ? ["--content-type", contentType] : [],
|
|
615
|
+
].flat();
|
|
616
|
+
}),
|
|
617
|
+
ReplaceValues: getS3ContentReplaceValues(),
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function useFunctionUrlSigningFunction() {
|
|
622
|
+
singletonUrlSigner =
|
|
623
|
+
singletonUrlSigner ??
|
|
624
|
+
new EdgeFunction(self, "ServerUrlSigningFunction", {
|
|
625
|
+
bundle: path.join(__dirname, "../support/signing-function"),
|
|
626
|
+
runtime: "nodejs18.x",
|
|
627
|
+
handler: "index.handler",
|
|
628
|
+
timeout: 10,
|
|
629
|
+
memorySize: 128,
|
|
630
|
+
});
|
|
631
|
+
return singletonUrlSigner;
|
|
632
|
+
}
|
|
633
|
+
function useServerBehaviorCachePolicy() {
|
|
634
|
+
const allowedHeaders = plan.cachePolicyAllowedHeaders || [];
|
|
635
|
+
singletonCachePolicy =
|
|
636
|
+
singletonCachePolicy ??
|
|
637
|
+
new CachePolicy(self, "ServerCache", {
|
|
638
|
+
queryStringBehavior: CacheQueryStringBehavior.all(),
|
|
639
|
+
headerBehavior: allowedHeaders.length > 0
|
|
640
|
+
? CacheHeaderBehavior.allowList(...allowedHeaders)
|
|
641
|
+
: CacheHeaderBehavior.none(),
|
|
642
|
+
cookieBehavior: CacheCookieBehavior.none(),
|
|
643
|
+
defaultTtl: CdkDuration.days(0),
|
|
644
|
+
maxTtl: CdkDuration.days(365),
|
|
645
|
+
minTtl: CdkDuration.days(0),
|
|
646
|
+
enableAcceptEncodingBrotli: true,
|
|
647
|
+
enableAcceptEncodingGzip: true,
|
|
648
|
+
comment: "SST server response cache policy",
|
|
649
|
+
});
|
|
650
|
+
return singletonCachePolicy;
|
|
651
|
+
}
|
|
652
|
+
function useServerBehaviorOriginRequestPolicy() {
|
|
653
|
+
// CloudFront's Managed-AllViewerExceptHostHeader policy
|
|
654
|
+
singletonOriginRequestPolicy =
|
|
655
|
+
singletonOriginRequestPolicy ??
|
|
656
|
+
OriginRequestPolicy.fromOriginRequestPolicyId(self, "ServerOriginRequestPolicy", "b689b0a8-53d0-40ab-baf2-68738e2966ac");
|
|
657
|
+
return singletonOriginRequestPolicy;
|
|
658
|
+
}
|
|
659
|
+
function useAwsCliLayer() {
|
|
660
|
+
singletonAwsCliLayer =
|
|
661
|
+
singletonAwsCliLayer ?? new AwsCliLayer(self, "AwsCliLayer");
|
|
662
|
+
return singletonAwsCliLayer;
|
|
663
|
+
}
|
|
664
|
+
function getS3ContentReplaceValues() {
|
|
665
|
+
const replaceValues = [];
|
|
666
|
+
Object.entries(environment || {})
|
|
667
|
+
.filter(([, value]) => Token.isUnresolved(value))
|
|
668
|
+
.forEach(([key, value]) => {
|
|
669
|
+
const token = `{{ ${key} }}`;
|
|
670
|
+
replaceValues.push({
|
|
671
|
+
files: "**/*.html",
|
|
672
|
+
search: token,
|
|
673
|
+
replace: value,
|
|
674
|
+
}, {
|
|
675
|
+
files: "**/*.js",
|
|
676
|
+
search: token,
|
|
677
|
+
replace: value,
|
|
678
|
+
}, {
|
|
679
|
+
files: "**/*.json",
|
|
680
|
+
search: token,
|
|
681
|
+
replace: value,
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
return replaceValues;
|
|
685
|
+
}
|
|
686
|
+
function generateBuildId() {
|
|
687
|
+
// We will generate a hash based on the contents of the S3 files with cache enabled.
|
|
688
|
+
// This will be used to determine if we need to invalidate our CloudFront cache.
|
|
689
|
+
const s3Origin = Object.values(plan.origins).find((origin) => origin.type === "s3");
|
|
690
|
+
if (s3Origin?.type !== "s3")
|
|
691
|
+
return "unchanged";
|
|
692
|
+
const cachedS3Files = s3Origin.copy.find((item) => item.cached);
|
|
693
|
+
if (!cachedS3Files)
|
|
694
|
+
return "unchanged";
|
|
695
|
+
// The below options are needed to support following symlinks when building zip files:
|
|
696
|
+
// - nodir: This will prevent symlinks themselves from being copied into the zip.
|
|
697
|
+
// - follow: This will follow symlinks and copy the files within.
|
|
698
|
+
const globOptions = {
|
|
699
|
+
dot: true,
|
|
700
|
+
nodir: true,
|
|
701
|
+
follow: true,
|
|
702
|
+
cwd: path.resolve(sitePath, cachedS3Files.from),
|
|
703
|
+
};
|
|
704
|
+
const files = glob.sync("**", globOptions);
|
|
705
|
+
const hash = crypto.createHash("sha1");
|
|
706
|
+
for (const file of files) {
|
|
707
|
+
hash.update(file);
|
|
708
|
+
}
|
|
709
|
+
const buildId = hash.digest("hex");
|
|
710
|
+
Logger.debug(`Generated build ID ${buildId}`);
|
|
711
|
+
return buildId;
|
|
712
|
+
}
|
|
130
713
|
}
|
|
131
|
-
/////////////////////
|
|
132
|
-
// Public Properties
|
|
133
|
-
/////////////////////
|
|
134
714
|
/**
|
|
135
715
|
* The CloudFront URL of the website.
|
|
136
716
|
*/
|
|
@@ -155,8 +735,7 @@ export class SsrSite extends Construct {
|
|
|
155
735
|
if (this.doNotDeploy)
|
|
156
736
|
return;
|
|
157
737
|
return {
|
|
158
|
-
function: this.
|
|
159
|
-
this.serverLambdaForRegional?.function,
|
|
738
|
+
function: this.serverFunction?.function,
|
|
160
739
|
bucket: this.bucket,
|
|
161
740
|
distribution: this.distribution.cdk.distribution,
|
|
162
741
|
hostedZone: this.distribution.cdk.hostedZone,
|
|
@@ -176,9 +755,7 @@ export class SsrSite extends Construct {
|
|
|
176
755
|
* ```
|
|
177
756
|
*/
|
|
178
757
|
attachPermissions(permissions) {
|
|
179
|
-
const server = this.
|
|
180
|
-
this.serverLambdaForRegional ||
|
|
181
|
-
this.serverLambdaForDev;
|
|
758
|
+
const server = this.serverFunction || this.serverFunctionForDev;
|
|
182
759
|
attachPermissionsToRole(server?.role, permissions);
|
|
183
760
|
}
|
|
184
761
|
/** @internal */
|
|
@@ -193,9 +770,8 @@ export class SsrSite extends Construct {
|
|
|
193
770
|
customDomainUrl: this.customDomainUrl,
|
|
194
771
|
url: this.url,
|
|
195
772
|
edge: this.props.edge,
|
|
196
|
-
server: (this.
|
|
197
|
-
|
|
198
|
-
this.serverLambdaForEdge)?.functionArn,
|
|
773
|
+
server: (this.serverFunctionForDev || this.serverFunction)
|
|
774
|
+
?.functionArn,
|
|
199
775
|
secrets: (this.props.bind || [])
|
|
200
776
|
.filter((c) => c instanceof Secret)
|
|
201
777
|
.map((c) => c.name),
|
|
@@ -229,612 +805,11 @@ export class SsrSite extends Construct {
|
|
|
229
805
|
},
|
|
230
806
|
};
|
|
231
807
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
/////////////////////
|
|
235
|
-
initBuildConfig() {
|
|
236
|
-
return {
|
|
237
|
-
typesPath: ".",
|
|
238
|
-
serverBuildOutputFile: "placeholder",
|
|
239
|
-
clientBuildOutputDir: "placeholder",
|
|
240
|
-
clientBuildVersionedSubDir: "placeholder",
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
buildApp() {
|
|
244
|
-
const app = this.node.root;
|
|
245
|
-
if (!app.isRunningSSTTest()) {
|
|
246
|
-
this.runBuild();
|
|
247
|
-
}
|
|
248
|
-
this.validateBuildOutput();
|
|
249
|
-
}
|
|
250
|
-
validateBuildOutput() {
|
|
251
|
-
const serverBuildFile = path.join(this.props.path, this.buildConfig.serverBuildOutputFile);
|
|
252
|
-
if (!fs.existsSync(serverBuildFile)) {
|
|
253
|
-
throw new Error(`No server build output found at "${serverBuildFile}"`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
runBuild() {
|
|
257
|
-
const { path: sitePath, buildCommand: rawBuildCommand, environment, } = this.props;
|
|
258
|
-
const defaultCommand = "npm run build";
|
|
259
|
-
const buildCommand = rawBuildCommand || defaultCommand;
|
|
260
|
-
if (buildCommand === defaultCommand) {
|
|
261
|
-
// Ensure that the site has a build script defined
|
|
262
|
-
if (!fs.existsSync(path.join(sitePath, "package.json"))) {
|
|
263
|
-
throw new Error(`No package.json found at "${sitePath}".`);
|
|
264
|
-
}
|
|
265
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(sitePath, "package.json")).toString());
|
|
266
|
-
if (!packageJson.scripts || !packageJson.scripts.build) {
|
|
267
|
-
throw new Error(`No "build" script found within package.json in "${sitePath}".`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
// Run build
|
|
271
|
-
Logger.debug(`Running "${buildCommand}" script`);
|
|
272
|
-
try {
|
|
273
|
-
execSync(buildCommand, {
|
|
274
|
-
cwd: sitePath,
|
|
275
|
-
stdio: "inherit",
|
|
276
|
-
env: {
|
|
277
|
-
SST: "1",
|
|
278
|
-
...process.env,
|
|
279
|
-
...getBuildCmdEnvironment(environment),
|
|
280
|
-
},
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
catch (e) {
|
|
284
|
-
throw new Error(`There was a problem building the "${this.node.id}" ${this.getConstructMetadata().type}.`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
/////////////////////
|
|
288
|
-
// Bundle S3 Assets
|
|
289
|
-
/////////////////////
|
|
290
|
-
createS3Assets() {
|
|
291
|
-
// Create temp folder, clean up if exists
|
|
292
|
-
const zipOutDir = path.resolve(path.join(useProject().paths.artifacts, `Site-${this.node.id}-${this.node.addr}`));
|
|
293
|
-
fs.rmSync(zipOutDir, { recursive: true, force: true });
|
|
294
|
-
// Create zip files
|
|
295
|
-
const app = this.node.root;
|
|
296
|
-
const script = path.resolve(__dirname, "../support/base-site-archiver.mjs");
|
|
297
|
-
const fileSizeLimit = app.isRunningSSTTest()
|
|
298
|
-
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
299
|
-
// @ts-ignore: "sstTestFileSizeLimitOverride" not exposed in props
|
|
300
|
-
this.props.sstTestFileSizeLimitOverride || 200
|
|
301
|
-
: 200;
|
|
302
|
-
const result = spawn.sync("node", [
|
|
303
|
-
script,
|
|
304
|
-
Buffer.from(JSON.stringify([
|
|
305
|
-
{
|
|
306
|
-
src: path.join(this.props.path, this.buildConfig.clientBuildOutputDir),
|
|
307
|
-
tar: this.buildConfig.clientBuildS3KeyPrefix || "",
|
|
308
|
-
},
|
|
309
|
-
...(this.buildConfig.prerenderedBuildOutputDir
|
|
310
|
-
? [
|
|
311
|
-
{
|
|
312
|
-
src: path.join(this.props.path, this.buildConfig.prerenderedBuildOutputDir),
|
|
313
|
-
tar: this.buildConfig.prerenderedBuildS3KeyPrefix || "",
|
|
314
|
-
},
|
|
315
|
-
]
|
|
316
|
-
: []),
|
|
317
|
-
])).toString("base64"),
|
|
318
|
-
zipOutDir,
|
|
319
|
-
`${fileSizeLimit}`,
|
|
320
|
-
], {
|
|
321
|
-
stdio: "inherit",
|
|
322
|
-
});
|
|
323
|
-
if (result.status !== 0) {
|
|
324
|
-
throw new Error(`There was a problem generating the assets package.`);
|
|
325
|
-
}
|
|
326
|
-
// Create S3 Assets for each zip file
|
|
327
|
-
const assets = [];
|
|
328
|
-
for (let partId = 0;; partId++) {
|
|
329
|
-
const zipFilePath = path.join(zipOutDir, `part${partId}.zip`);
|
|
330
|
-
if (!fs.existsSync(zipFilePath)) {
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
assets.push(new Asset(this, `Asset${partId}`, {
|
|
334
|
-
path: zipFilePath,
|
|
335
|
-
}));
|
|
336
|
-
}
|
|
337
|
-
return assets;
|
|
338
|
-
}
|
|
339
|
-
createS3AssetFileOptions() {
|
|
340
|
-
if (this.props.fileOptions)
|
|
341
|
-
return this.props.fileOptions;
|
|
342
|
-
// Build file options
|
|
343
|
-
const fileOptions = [];
|
|
344
|
-
const clientPath = path.join(this.props.path, this.buildConfig.clientBuildOutputDir);
|
|
345
|
-
for (const item of fs.readdirSync(clientPath)) {
|
|
346
|
-
// Versioned files will be cached for 1 year (immutable) both at
|
|
347
|
-
// the CDN and browser level.
|
|
348
|
-
if (item === this.buildConfig.clientBuildVersionedSubDir) {
|
|
349
|
-
fileOptions.push({
|
|
350
|
-
exclude: "*",
|
|
351
|
-
include: path.posix.join(this.buildConfig.clientBuildS3KeyPrefix ?? "", this.buildConfig.clientBuildVersionedSubDir, "*"),
|
|
352
|
-
cacheControl: "public,max-age=31536000,immutable",
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
// Un-versioned files will be cached for 1 year at the CDN level.
|
|
356
|
-
// But not at the browser level. CDN cache will be invalidated on deploy.
|
|
357
|
-
else {
|
|
358
|
-
const itemPath = path.join(clientPath, item);
|
|
359
|
-
fileOptions.push({
|
|
360
|
-
exclude: "*",
|
|
361
|
-
include: path.posix.join(this.buildConfig.clientBuildS3KeyPrefix ?? "", item, fs.statSync(itemPath).isDirectory() ? "*" : ""),
|
|
362
|
-
cacheControl: "public,max-age=0,s-maxage=31536000,must-revalidate",
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return fileOptions;
|
|
367
|
-
}
|
|
368
|
-
createS3Bucket() {
|
|
369
|
-
const { cdk } = this.props;
|
|
370
|
-
// cdk.bucket is an imported construct
|
|
371
|
-
if (cdk?.bucket && isCDKConstruct(cdk?.bucket)) {
|
|
372
|
-
return cdk.bucket;
|
|
373
|
-
}
|
|
374
|
-
// cdk.bucket is a prop
|
|
375
|
-
return new Bucket(this, "S3Bucket", {
|
|
376
|
-
publicReadAccess: false,
|
|
377
|
-
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
378
|
-
autoDeleteObjects: true,
|
|
379
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
380
|
-
enforceSSL: true,
|
|
381
|
-
...cdk?.bucket,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
createS3Deployment(cliLayer, assets, fileOptions) {
|
|
385
|
-
// Create a Lambda function that will be doing the uploading
|
|
386
|
-
const uploader = new CdkFunction(this, "S3Uploader", {
|
|
387
|
-
code: Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
|
|
388
|
-
layers: [cliLayer],
|
|
389
|
-
runtime: Runtime.PYTHON_3_11,
|
|
390
|
-
handler: "s3-upload.handler",
|
|
391
|
-
timeout: CdkDuration.minutes(15),
|
|
392
|
-
memorySize: 1024,
|
|
393
|
-
});
|
|
394
|
-
this.bucket.grantReadWrite(uploader);
|
|
395
|
-
assets.forEach((asset) => asset.grantRead(uploader));
|
|
396
|
-
// Create the custom resource function
|
|
397
|
-
const handler = new CdkFunction(this, "S3Handler", {
|
|
398
|
-
code: Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
|
|
399
|
-
layers: [cliLayer],
|
|
400
|
-
runtime: Runtime.PYTHON_3_11,
|
|
401
|
-
handler: "s3-handler.handler",
|
|
402
|
-
timeout: CdkDuration.minutes(15),
|
|
403
|
-
memorySize: 1024,
|
|
404
|
-
environment: {
|
|
405
|
-
UPLOADER_FUNCTION_NAME: uploader.functionName,
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
this.bucket.grantReadWrite(handler);
|
|
409
|
-
uploader.grantInvoke(handler);
|
|
410
|
-
// Create custom resource
|
|
411
|
-
return new CustomResource(this, "S3Deployment", {
|
|
412
|
-
serviceToken: handler.functionArn,
|
|
413
|
-
resourceType: "Custom::SSTBucketDeployment",
|
|
414
|
-
properties: {
|
|
415
|
-
Sources: assets.map((asset) => ({
|
|
416
|
-
BucketName: asset.s3BucketName,
|
|
417
|
-
ObjectKey: asset.s3ObjectKey,
|
|
418
|
-
})),
|
|
419
|
-
DestinationBucketName: this.bucket.bucketName,
|
|
420
|
-
FileOptions: (fileOptions || []).map(({ exclude, include, cacheControl, contentType }) => {
|
|
421
|
-
if (typeof exclude === "string") {
|
|
422
|
-
exclude = [exclude];
|
|
423
|
-
}
|
|
424
|
-
if (typeof include === "string") {
|
|
425
|
-
include = [include];
|
|
426
|
-
}
|
|
427
|
-
return [
|
|
428
|
-
...exclude.map((per) => ["--exclude", per]),
|
|
429
|
-
...include.map((per) => ["--include", per]),
|
|
430
|
-
["--cache-control", cacheControl],
|
|
431
|
-
contentType ? ["--content-type", contentType] : [],
|
|
432
|
-
].flat();
|
|
433
|
-
}),
|
|
434
|
-
ReplaceValues: this.getS3ContentReplaceValues(),
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
/////////////////////
|
|
439
|
-
// Bundle Lambda Server
|
|
440
|
-
/////////////////////
|
|
441
|
-
createFunctionForRegional() {
|
|
442
|
-
return {};
|
|
443
|
-
}
|
|
444
|
-
createFunctionForEdge() {
|
|
445
|
-
return {};
|
|
446
|
-
}
|
|
447
|
-
createFunctionForDev() {
|
|
448
|
-
const { runtime, timeout, memorySize, permissions, environment, bind } = this.props;
|
|
449
|
-
const app = this.node.root;
|
|
450
|
-
const role = new Role(this, "ServerFunctionRole", {
|
|
451
|
-
assumedBy: new CompositePrincipal(new AccountPrincipal(app.account), new ServicePrincipal("lambda.amazonaws.com")),
|
|
452
|
-
maxSessionDuration: CdkDuration.hours(12),
|
|
453
|
-
});
|
|
454
|
-
const ssrFn = new SsrFunction(this, `ServerFunction`, {
|
|
455
|
-
description: "Server handler placeholder",
|
|
456
|
-
bundle: path.join(__dirname, "../support/ssr-site-function-stub"),
|
|
457
|
-
handler: "index.handler",
|
|
458
|
-
runtime,
|
|
459
|
-
memorySize,
|
|
460
|
-
timeout,
|
|
461
|
-
role,
|
|
462
|
-
bind,
|
|
463
|
-
environment,
|
|
464
|
-
permissions,
|
|
465
|
-
// note: do not need to set vpc settings b/c this function is not being used
|
|
466
|
-
});
|
|
467
|
-
useDeferredTasks().add(async () => {
|
|
468
|
-
await ssrFn.build();
|
|
469
|
-
});
|
|
470
|
-
return ssrFn;
|
|
471
|
-
}
|
|
472
|
-
grantServerS3Permissions() {
|
|
473
|
-
const server = this.serverLambdaForEdge || this.serverLambdaForRegional;
|
|
474
|
-
this.bucket.grantReadWrite(server.role);
|
|
475
|
-
}
|
|
476
|
-
grantServerCloudFrontPermissions() {
|
|
477
|
-
const stack = Stack.of(this);
|
|
478
|
-
const server = this.serverLambdaForEdge || this.serverLambdaForRegional;
|
|
479
|
-
const policy = new Policy(this, "ServerFunctionInvalidatorPolicy", {
|
|
480
|
-
statements: [
|
|
481
|
-
new PolicyStatement({
|
|
482
|
-
actions: ["cloudfront:CreateInvalidation"],
|
|
483
|
-
resources: [
|
|
484
|
-
`arn:${stack.partition}:cloudfront::${stack.account}:distribution/${this.distribution.cdk.distribution.distributionId}`,
|
|
485
|
-
],
|
|
486
|
-
}),
|
|
487
|
-
],
|
|
488
|
-
});
|
|
489
|
-
server?.role?.attachInlinePolicy(policy);
|
|
490
|
-
}
|
|
491
|
-
createWarmer() {
|
|
492
|
-
const { warm, edge } = this.props;
|
|
493
|
-
if (!warm)
|
|
494
|
-
return;
|
|
495
|
-
if (warm && edge) {
|
|
496
|
-
throw new VisibleError(`In the "${this.node.id}" Site, warming is currently supported only for the regional mode.`);
|
|
497
|
-
}
|
|
498
|
-
if (!this.serverLambdaForRegional)
|
|
499
|
-
return;
|
|
500
|
-
// Create warmer function
|
|
501
|
-
const warmer = new CdkFunction(this, "WarmerFunction", {
|
|
502
|
-
description: "Next.js warmer",
|
|
503
|
-
code: Code.fromAsset(this.buildConfig.warmerFunctionAssetPath ??
|
|
504
|
-
path.join(__dirname, "../support/ssr-warmer")),
|
|
505
|
-
runtime: Runtime.NODEJS_18_X,
|
|
506
|
-
handler: "index.handler",
|
|
507
|
-
timeout: CdkDuration.minutes(15),
|
|
508
|
-
memorySize: 1024,
|
|
509
|
-
environment: {
|
|
510
|
-
FUNCTION_NAME: this.serverLambdaForRegional.functionName,
|
|
511
|
-
CONCURRENCY: warm.toString(),
|
|
512
|
-
},
|
|
513
|
-
});
|
|
514
|
-
this.serverLambdaForRegional.grantInvoke(warmer);
|
|
515
|
-
// Create cron job
|
|
516
|
-
new Rule(this, "WarmerRule", {
|
|
517
|
-
schedule: Schedule.rate(CdkDuration.minutes(5)),
|
|
518
|
-
targets: [new LambdaFunction(warmer, { retryAttempts: 0 })],
|
|
519
|
-
});
|
|
520
|
-
// Create custom resource to prewarm on deploy
|
|
521
|
-
const stack = Stack.of(this);
|
|
522
|
-
const policy = new Policy(this, "PrewarmerPolicy", {
|
|
523
|
-
statements: [
|
|
524
|
-
new PolicyStatement({
|
|
525
|
-
effect: Effect.ALLOW,
|
|
526
|
-
actions: ["lambda:InvokeFunction"],
|
|
527
|
-
resources: [warmer.functionArn],
|
|
528
|
-
}),
|
|
529
|
-
],
|
|
530
|
-
});
|
|
531
|
-
stack.customResourceHandler.role?.attachInlinePolicy(policy);
|
|
532
|
-
const resource = new CustomResource(this, "Prewarmer", {
|
|
533
|
-
serviceToken: stack.customResourceHandler.functionArn,
|
|
534
|
-
resourceType: "Custom::FunctionInvoker",
|
|
535
|
-
properties: {
|
|
536
|
-
version: Date.now().toString(),
|
|
537
|
-
functionName: warmer.functionName,
|
|
538
|
-
},
|
|
539
|
-
});
|
|
540
|
-
resource.node.addDependency(policy);
|
|
541
|
-
}
|
|
542
|
-
/////////////////////
|
|
543
|
-
// CloudFront Distribution
|
|
544
|
-
/////////////////////
|
|
545
|
-
createCloudFrontS3Origin() {
|
|
546
|
-
return new S3Origin(this.bucket, {
|
|
547
|
-
originPath: "/" + (this.buildConfig.clientBuildS3KeyPrefix ?? ""),
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
createCloudFrontDistributionForRegional() {
|
|
551
|
-
const { customDomain, cdk } = this.props;
|
|
552
|
-
const cfDistributionProps = cdk?.distribution || {};
|
|
553
|
-
return new Distribution(this, "CDN", {
|
|
554
|
-
scopeOverride: this,
|
|
555
|
-
customDomain,
|
|
556
|
-
cdk: {
|
|
557
|
-
distribution: {
|
|
558
|
-
// these values can be overwritten by cfDistributionProps
|
|
559
|
-
defaultRootObject: "",
|
|
560
|
-
// Override props.
|
|
561
|
-
...cfDistributionProps,
|
|
562
|
-
// these values can NOT be overwritten by cfDistributionProps
|
|
563
|
-
defaultBehavior: this.buildDefaultBehaviorForRegional(),
|
|
564
|
-
additionalBehaviors: {
|
|
565
|
-
...(cfDistributionProps.additionalBehaviors || {}),
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
createCloudFrontDistributionForEdge() {
|
|
572
|
-
const { customDomain, cdk } = this.props;
|
|
573
|
-
const cfDistributionProps = cdk?.distribution || {};
|
|
574
|
-
return new Distribution(this, "CDN", {
|
|
575
|
-
scopeOverride: this,
|
|
576
|
-
customDomain,
|
|
577
|
-
cdk: {
|
|
578
|
-
distribution: {
|
|
579
|
-
// these values can be overwritten by cfDistributionProps
|
|
580
|
-
defaultRootObject: "",
|
|
581
|
-
// Override props.
|
|
582
|
-
...cfDistributionProps,
|
|
583
|
-
// these values can NOT be overwritten by cfDistributionProps
|
|
584
|
-
defaultBehavior: this.buildDefaultBehaviorForEdge(),
|
|
585
|
-
additionalBehaviors: {
|
|
586
|
-
...(cfDistributionProps.additionalBehaviors || {}),
|
|
587
|
-
},
|
|
588
|
-
},
|
|
589
|
-
},
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
buildDefaultBehaviorForRegional() {
|
|
593
|
-
const { timeout, regional, cdk } = this.props;
|
|
594
|
-
const cfDistributionProps = cdk?.distribution || {};
|
|
595
|
-
const fnUrl = this.serverLambdaForRegional.addFunctionUrl({
|
|
596
|
-
authType: regional?.enableServerUrlIamAuth
|
|
597
|
-
? FunctionUrlAuthType.AWS_IAM
|
|
598
|
-
: FunctionUrlAuthType.NONE,
|
|
599
|
-
invokeMode: this.supportsStreaming()
|
|
600
|
-
? InvokeMode.RESPONSE_STREAM
|
|
601
|
-
: undefined,
|
|
602
|
-
});
|
|
603
|
-
return {
|
|
604
|
-
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
605
|
-
origin: new HttpOrigin(Fn.parseDomainName(fnUrl.url), {
|
|
606
|
-
readTimeout: typeof timeout === "string"
|
|
607
|
-
? toCdkDuration(timeout)
|
|
608
|
-
: CdkDuration.seconds(timeout),
|
|
609
|
-
}),
|
|
610
|
-
allowedMethods: AllowedMethods.ALLOW_ALL,
|
|
611
|
-
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
612
|
-
compress: true,
|
|
613
|
-
cachePolicy: cdk?.serverCachePolicy ?? this.useServerBehaviorCachePolicy(),
|
|
614
|
-
responseHeadersPolicy: cdk?.responseHeadersPolicy,
|
|
615
|
-
originRequestPolicy: this.useServerBehaviorOriginRequestPolicy(),
|
|
616
|
-
...(cfDistributionProps.defaultBehavior || {}),
|
|
617
|
-
functionAssociations: [
|
|
618
|
-
...this.useServerBehaviorFunctionAssociations(),
|
|
619
|
-
...(cfDistributionProps.defaultBehavior?.functionAssociations || []),
|
|
620
|
-
],
|
|
621
|
-
edgeLambdas: [
|
|
622
|
-
...(regional?.enableServerUrlIamAuth
|
|
623
|
-
? [
|
|
624
|
-
{
|
|
625
|
-
includeBody: true,
|
|
626
|
-
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
|
|
627
|
-
functionVersion: this.useServerUrlSigningFunction().currentVersion,
|
|
628
|
-
},
|
|
629
|
-
]
|
|
630
|
-
: []),
|
|
631
|
-
...(cfDistributionProps.defaultBehavior?.edgeLambdas || []),
|
|
632
|
-
],
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
buildDefaultBehaviorForEdge() {
|
|
636
|
-
const { cdk } = this.props;
|
|
637
|
-
const cfDistributionProps = cdk?.distribution || {};
|
|
638
|
-
return {
|
|
639
|
-
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
640
|
-
origin: this.s3Origin,
|
|
641
|
-
allowedMethods: AllowedMethods.ALLOW_ALL,
|
|
642
|
-
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
643
|
-
compress: true,
|
|
644
|
-
cachePolicy: cdk?.serverCachePolicy ?? this.useServerBehaviorCachePolicy(),
|
|
645
|
-
responseHeadersPolicy: cdk?.responseHeadersPolicy,
|
|
646
|
-
originRequestPolicy: this.useServerBehaviorOriginRequestPolicy(),
|
|
647
|
-
...(cfDistributionProps.defaultBehavior || {}),
|
|
648
|
-
functionAssociations: [
|
|
649
|
-
...this.useServerBehaviorFunctionAssociations(),
|
|
650
|
-
...(cfDistributionProps.defaultBehavior?.functionAssociations || []),
|
|
651
|
-
],
|
|
652
|
-
edgeLambdas: [
|
|
653
|
-
{
|
|
654
|
-
includeBody: true,
|
|
655
|
-
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
|
|
656
|
-
functionVersion: this.serverLambdaForEdge.currentVersion,
|
|
657
|
-
},
|
|
658
|
-
...(cfDistributionProps.defaultBehavior?.edgeLambdas || []),
|
|
659
|
-
],
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
addStaticFileBehaviors() {
|
|
663
|
-
const { cdk } = this.props;
|
|
664
|
-
// Create a template for statics behaviours
|
|
665
|
-
const publicDir = path.join(this.props.path, this.buildConfig.clientBuildOutputDir);
|
|
666
|
-
for (const item of fs.readdirSync(publicDir)) {
|
|
667
|
-
const isDir = fs.statSync(path.join(publicDir, item)).isDirectory();
|
|
668
|
-
this.distribution.cdk.distribution.addBehavior(isDir ? `${item}/*` : item, this.s3Origin, {
|
|
669
|
-
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
670
|
-
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
671
|
-
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
672
|
-
compress: true,
|
|
673
|
-
cachePolicy: CachePolicy.CACHING_OPTIMIZED,
|
|
674
|
-
responseHeadersPolicy: cdk?.responseHeadersPolicy,
|
|
675
|
-
functionAssociations: [
|
|
676
|
-
...this.useStaticBehaviorFunctionAssociations(),
|
|
677
|
-
],
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
useServerBehaviorFunctionAssociations() {
|
|
682
|
-
this.serverCfFunction =
|
|
683
|
-
this.serverCfFunction ??
|
|
684
|
-
new CfFunction(this, "CloudFrontFunction", {
|
|
685
|
-
code: CfFunctionCode.fromInline(`
|
|
686
|
-
function handler(event) {
|
|
687
|
-
var request = event.request;
|
|
688
|
-
request.headers["x-forwarded-host"] = request.headers.host;
|
|
689
|
-
${this.buildConfig.serverCFFunctionInjection || ""}
|
|
690
|
-
return request;
|
|
691
|
-
}`),
|
|
692
|
-
});
|
|
693
|
-
return [
|
|
694
|
-
{
|
|
695
|
-
eventType: CfFunctionEventType.VIEWER_REQUEST,
|
|
696
|
-
function: this.serverCfFunction,
|
|
697
|
-
},
|
|
698
|
-
];
|
|
699
|
-
}
|
|
700
|
-
useStaticBehaviorFunctionAssociations() {
|
|
701
|
-
if (!this.buildConfig.clientCFFunctionInjection)
|
|
702
|
-
return [];
|
|
703
|
-
this.staticCfFunction =
|
|
704
|
-
this.staticCfFunction ??
|
|
705
|
-
new CfFunction(this, "CloudFrontFunctionForStaticBehavior", {
|
|
706
|
-
code: CfFunctionCode.fromInline(`
|
|
707
|
-
function handler(event) {
|
|
708
|
-
var request = event.request;
|
|
709
|
-
${this.buildConfig.clientCFFunctionInjection || ""}
|
|
710
|
-
return request;
|
|
711
|
-
}`),
|
|
712
|
-
});
|
|
713
|
-
return [
|
|
714
|
-
{
|
|
715
|
-
eventType: CfFunctionEventType.VIEWER_REQUEST,
|
|
716
|
-
function: this.staticCfFunction,
|
|
717
|
-
},
|
|
718
|
-
];
|
|
719
|
-
}
|
|
720
|
-
useServerUrlSigningFunction() {
|
|
721
|
-
this.serverUrlSigningFunction =
|
|
722
|
-
this.serverUrlSigningFunction ??
|
|
723
|
-
new EdgeFunction(this, "ServerUrlSigningFunction", {
|
|
724
|
-
bundle: path.join(__dirname, "../support/signing-function"),
|
|
725
|
-
runtime: "nodejs18.x",
|
|
726
|
-
handler: "index.handler",
|
|
727
|
-
timeout: 10,
|
|
728
|
-
memorySize: 128,
|
|
729
|
-
permissions: [
|
|
730
|
-
new PolicyStatement({
|
|
731
|
-
actions: ["lambda:InvokeFunctionUrl"],
|
|
732
|
-
resources: [this.serverLambdaForRegional?.functionArn],
|
|
733
|
-
}),
|
|
734
|
-
],
|
|
735
|
-
});
|
|
736
|
-
return this.serverUrlSigningFunction;
|
|
737
|
-
}
|
|
738
|
-
useServerBehaviorCachePolicy(allowedHeaders) {
|
|
739
|
-
this.serverBehaviorCachePolicy =
|
|
740
|
-
this.serverBehaviorCachePolicy ??
|
|
741
|
-
new CachePolicy(this, "ServerCache", {
|
|
742
|
-
queryStringBehavior: CacheQueryStringBehavior.all(),
|
|
743
|
-
headerBehavior: allowedHeaders && allowedHeaders.length > 0
|
|
744
|
-
? CacheHeaderBehavior.allowList(...allowedHeaders)
|
|
745
|
-
: CacheHeaderBehavior.none(),
|
|
746
|
-
cookieBehavior: CacheCookieBehavior.none(),
|
|
747
|
-
defaultTtl: CdkDuration.days(0),
|
|
748
|
-
maxTtl: CdkDuration.days(365),
|
|
749
|
-
minTtl: CdkDuration.days(0),
|
|
750
|
-
enableAcceptEncodingBrotli: true,
|
|
751
|
-
enableAcceptEncodingGzip: true,
|
|
752
|
-
comment: "SST server response cache policy",
|
|
753
|
-
});
|
|
754
|
-
return this.serverBehaviorCachePolicy;
|
|
755
|
-
}
|
|
756
|
-
useServerBehaviorOriginRequestPolicy() {
|
|
757
|
-
// CloudFront's Managed-AllViewerExceptHostHeader policy
|
|
758
|
-
this.serverBehaviorOriginRequestPolicy =
|
|
759
|
-
this.serverBehaviorOriginRequestPolicy ??
|
|
760
|
-
OriginRequestPolicy.fromOriginRequestPolicyId(this, "ServerOriginRequestPolicy", "b689b0a8-53d0-40ab-baf2-68738e2966ac");
|
|
761
|
-
return this.serverBehaviorOriginRequestPolicy;
|
|
762
|
-
}
|
|
763
|
-
/////////////////////
|
|
764
|
-
// Helper Functions
|
|
765
|
-
/////////////////////
|
|
766
|
-
getS3ContentReplaceValues() {
|
|
767
|
-
const replaceValues = [];
|
|
768
|
-
Object.entries(this.props.environment || {})
|
|
769
|
-
.filter(([, value]) => Token.isUnresolved(value))
|
|
770
|
-
.forEach(([key, value]) => {
|
|
771
|
-
const token = `{{ ${key} }}`;
|
|
772
|
-
replaceValues.push({
|
|
773
|
-
files: "**/*.html",
|
|
774
|
-
search: token,
|
|
775
|
-
replace: value,
|
|
776
|
-
}, {
|
|
777
|
-
files: "**/*.js",
|
|
778
|
-
search: token,
|
|
779
|
-
replace: value,
|
|
780
|
-
}, {
|
|
781
|
-
files: "**/*.json",
|
|
782
|
-
search: token,
|
|
783
|
-
replace: value,
|
|
784
|
-
});
|
|
785
|
-
});
|
|
786
|
-
return replaceValues;
|
|
787
|
-
}
|
|
788
|
-
validateSiteExists() {
|
|
789
|
-
const { path: sitePath } = this.props;
|
|
790
|
-
if (!fs.existsSync(sitePath)) {
|
|
791
|
-
throw new Error(`No site found at "${path.resolve(sitePath)}"`);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
validateTimeout() {
|
|
795
|
-
const { edge, timeout } = this.props;
|
|
796
|
-
const num = typeof timeout === "number"
|
|
797
|
-
? timeout
|
|
798
|
-
: toCdkDuration(timeout).toSeconds();
|
|
799
|
-
const limit = edge ? 30 : 180;
|
|
800
|
-
if (num > limit) {
|
|
801
|
-
throw new Error(edge
|
|
802
|
-
? `Timeout must be less than or equal to 30 seconds when the "edge" flag is enabled.`
|
|
803
|
-
: `Timeout must be less than or equal to 180 seconds.`);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
writeTypesFile() {
|
|
807
|
-
const typesPath = path.resolve(this.props.path, this.buildConfig.typesPath, "sst-env.d.ts");
|
|
808
|
-
// Do not override the types file if it already exists
|
|
809
|
-
if (fs.existsSync(typesPath))
|
|
810
|
-
return;
|
|
811
|
-
const relPathToSstTypesFile = path.join(path.relative(path.dirname(typesPath), useProject().paths.root), ".sst/types/index.ts");
|
|
812
|
-
fs.writeFileSync(typesPath, `/// <reference path="${relPathToSstTypesFile}" />`);
|
|
813
|
-
}
|
|
814
|
-
generateBuildId() {
|
|
815
|
-
// We will generate a hash based on the contents of the "public" folder
|
|
816
|
-
// which will be used to indicate if we need to invalidate our CloudFront
|
|
817
|
-
// cache.
|
|
818
|
-
// The below options are needed to support following symlinks when building zip files:
|
|
819
|
-
// - nodir: This will prevent symlinks themselves from being copied into the zip.
|
|
820
|
-
// - follow: This will follow symlinks and copy the files within.
|
|
821
|
-
const globOptions = {
|
|
822
|
-
dot: true,
|
|
823
|
-
nodir: true,
|
|
824
|
-
follow: true,
|
|
825
|
-
cwd: path.resolve(this.props.path, this.buildConfig.clientBuildOutputDir),
|
|
826
|
-
};
|
|
827
|
-
const files = glob.sync("**", globOptions);
|
|
828
|
-
const hash = crypto.createHash("sha1");
|
|
829
|
-
for (const file of files) {
|
|
830
|
-
hash.update(file);
|
|
831
|
-
}
|
|
832
|
-
const buildId = hash.digest("hex");
|
|
833
|
-
Logger.debug(`Generated build ID ${buildId}`);
|
|
834
|
-
return buildId;
|
|
808
|
+
useCloudFrontFunctionHostHeaderInjection() {
|
|
809
|
+
return `request.headers["x-forwarded-host"] = request.headers.host;`;
|
|
835
810
|
}
|
|
836
|
-
|
|
837
|
-
return
|
|
811
|
+
validatePlan(input) {
|
|
812
|
+
return input;
|
|
838
813
|
}
|
|
839
814
|
}
|
|
840
815
|
export const useSites = createAppContext(() => {
|