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.
Files changed (46) hide show
  1. package/cli/commands/types.js +3 -7
  2. package/constructs/Api.js +2 -0
  3. package/constructs/ApiGatewayV1Api.js +2 -0
  4. package/constructs/App.d.ts +3 -1
  5. package/constructs/App.js +63 -62
  6. package/constructs/AppSyncApi.js +2 -0
  7. package/constructs/AstroSite.d.ts +50 -11
  8. package/constructs/AstroSite.js +69 -51
  9. package/constructs/Auth.js +2 -0
  10. package/constructs/Bucket.js +2 -0
  11. package/constructs/Cognito.js +2 -0
  12. package/constructs/Cron.js +2 -0
  13. package/constructs/EdgeFunction.d.ts +3 -4
  14. package/constructs/EdgeFunction.js +13 -9
  15. package/constructs/EventBus.js +2 -0
  16. package/constructs/Function.d.ts +5 -1
  17. package/constructs/Function.js +8 -6
  18. package/constructs/Job.js +3 -3
  19. package/constructs/KinesisStream.js +2 -0
  20. package/constructs/NextjsSite.d.ts +84 -22
  21. package/constructs/NextjsSite.js +150 -254
  22. package/constructs/Parameter.js +2 -0
  23. package/constructs/Queue.js +2 -0
  24. package/constructs/RDS.js +5 -4
  25. package/constructs/RemixSite.d.ts +65 -11
  26. package/constructs/RemixSite.js +112 -71
  27. package/constructs/Script.js +2 -0
  28. package/constructs/Secret.js +2 -0
  29. package/constructs/Service.js +4 -3
  30. package/constructs/SolidStartSite.d.ts +48 -9
  31. package/constructs/SolidStartSite.js +68 -40
  32. package/constructs/SsrFunction.d.ts +4 -5
  33. package/constructs/SsrFunction.js +18 -13
  34. package/constructs/SsrSite.d.ts +74 -68
  35. package/constructs/SsrSite.js +657 -682
  36. package/constructs/StaticSite.js +2 -0
  37. package/constructs/SvelteKitSite.d.ts +80 -12
  38. package/constructs/SvelteKitSite.js +90 -64
  39. package/constructs/Table.js +2 -0
  40. package/constructs/Topic.js +2 -0
  41. package/constructs/WebSocketApi.js +2 -0
  42. package/constructs/deprecated/NextjsSite.js +1 -0
  43. package/constructs/future/Auth.js +1 -0
  44. package/package.json +1 -1
  45. package/support/remix-site-function/edge-server.js +2 -4
  46. package/support/remix-site-function/regional-server.js +2 -4
@@ -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
- buildConfig;
51
- deferredTaskCallbacks = [];
52
- serverLambdaForEdge;
53
- serverLambdaForRegional;
54
- serverLambdaForDev;
55
- serverUrlSigningFunction;
50
+ typesPath = ".";
56
51
  bucket;
57
- serverCfFunction;
58
- serverBehaviorCachePolicy;
59
- serverBehaviorOriginRequestPolicy;
60
- staticCfFunction;
61
- s3Origin;
52
+ serverFunction;
53
+ serverFunctionForDev;
62
54
  distribution;
63
- constructor(scope, id, props) {
64
- super(scope, props?.cdk?.id || id);
65
- const app = scope.node.root;
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
- ...props,
63
+ ...rawProps,
75
64
  };
76
- this.doNotDeploy =
77
- !stack.isActive || (app.mode === "dev" && !this.props.dev?.deploy);
78
- this.buildConfig = this.initBuildConfig();
79
- this.validateSiteExists();
80
- this.validateTimeout();
81
- this.writeTypesFile();
82
- useSites().add(stack.stackName, id, this.constructor.name, this.props);
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.s3Origin = this.distribution = null;
86
- this.serverLambdaForDev = this.createFunctionForDev();
78
+ this.bucket = this.distribution = null;
79
+ this.serverFunctionForDev = createServerFunctionForDev();
80
+ app.registerTypes(this);
87
81
  return;
88
82
  }
89
- // Create Bucket which will be utilised to contain the statics
90
- this.bucket = this.createS3Bucket();
91
- // Create Server functions
92
- if (this.props.edge) {
93
- this.serverLambdaForEdge = this.createFunctionForEdge();
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
- else {
96
- this.serverLambdaForRegional = this.createFunctionForRegional();
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
- this.grantServerS3Permissions();
99
- // Create CloudFront
100
- this.s3Origin = this.createCloudFrontS3Origin();
101
- this.distribution = this.props.edge
102
- ? this.createCloudFrontDistributionForEdge()
103
- : this.createCloudFrontDistributionForRegional();
104
- this.grantServerCloudFrontPermissions();
105
- useDeferredTasks().add(async () => {
106
- // Build app
107
- this.buildApp();
108
- // Build server functions
109
- await this.serverLambdaForEdge?.build();
110
- await this.serverLambdaForRegional?.build();
111
- await this.serverUrlSigningFunction?.build();
112
- // Create warmer
113
- // Note: create warmer after build app b/c the warmer code
114
- // for NextjsSite depends on OpenNext build output
115
- this.createWarmer();
116
- // Create S3 Deployment
117
- const cliLayer = new AwsCliLayer(this, "AwsCliLayer");
118
- const assets = this.createS3Assets();
119
- const assetFileOptions = this.createS3AssetFileOptions();
120
- const s3deployCR = this.createS3Deployment(cliLayer, assets, assetFileOptions);
121
- this.distribution.node.addDependency(s3deployCR);
122
- // Add static file behaviors
123
- this.addStaticFileBehaviors();
124
- // Invalidate CloudFront
125
- this.distribution.createInvalidation(this.generateBuildId());
126
- for (const task of this.deferredTaskCallbacks) {
127
- await task();
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.serverLambdaForEdge?.function ||
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.serverLambdaForEdge ||
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.serverLambdaForDev ||
197
- this.serverLambdaForRegional ||
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
- // Build App
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
- supportsStreaming() {
837
- return false;
811
+ validatePlan(input) {
812
+ return input;
838
813
  }
839
814
  }
840
815
  export const useSites = createAppContext(() => {