@aws/nx-plugin 0.46.0 → 0.48.0

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 (104) hide show
  1. package/package.json +1 -1
  2. package/src/infra/app/generator.js +4 -1
  3. package/src/infra/app/generator.js.map +1 -1
  4. package/src/mcp-server/tools/create-workspace-command.js +4 -0
  5. package/src/mcp-server/tools/create-workspace-command.js.map +1 -1
  6. package/src/mcp-server/tools/general-guidance.js +3 -1
  7. package/src/mcp-server/tools/general-guidance.js.map +1 -1
  8. package/src/preset/__snapshots__/generator.spec.ts.snap +9 -0
  9. package/src/preset/generator.d.ts +1 -1
  10. package/src/preset/generator.js +4 -1
  11. package/src/preset/generator.js.map +1 -1
  12. package/src/preset/schema.d.ts +3 -0
  13. package/src/preset/schema.json +9 -1
  14. package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +60 -0
  15. package/src/py/fast-api/generator.js +5 -3
  16. package/src/py/fast-api/generator.js.map +1 -1
  17. package/src/py/fast-api/react/__snapshots__/generator.spec.ts.snap +1 -0
  18. package/src/py/fast-api/schema.d.ts +3 -1
  19. package/src/py/fast-api/schema.json +4 -4
  20. package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +310 -0
  21. package/src/py/lambda-function/generator.js +17 -25
  22. package/src/py/lambda-function/generator.js.map +1 -1
  23. package/src/py/lambda-function/schema.d.ts +3 -0
  24. package/src/py/lambda-function/schema.json +8 -0
  25. package/src/py/mcp-server/generator.js +4 -2
  26. package/src/py/mcp-server/generator.js.map +1 -1
  27. package/src/py/mcp-server/schema.d.ts +4 -1
  28. package/src/py/mcp-server/schema.json +4 -4
  29. package/src/py/strands-agent/generator.js +4 -2
  30. package/src/py/strands-agent/generator.js.map +1 -1
  31. package/src/py/strands-agent/schema.d.ts +3 -1
  32. package/src/py/strands-agent/schema.json +4 -4
  33. package/src/terraform/project/generator.js +2 -1
  34. package/src/terraform/project/generator.js.map +1 -1
  35. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +60 -0
  36. package/src/trpc/backend/generator.js +4 -2
  37. package/src/trpc/backend/generator.js.map +1 -1
  38. package/src/trpc/backend/schema.d.ts +2 -1
  39. package/src/trpc/backend/schema.json +4 -4
  40. package/src/trpc/react/__snapshots__/generator.spec.ts.snap +1 -0
  41. package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +310 -0
  42. package/src/ts/lambda-function/generator.js +17 -27
  43. package/src/ts/lambda-function/generator.js.map +1 -1
  44. package/src/ts/lambda-function/schema.d.ts +3 -0
  45. package/src/ts/lambda-function/schema.json +8 -0
  46. package/src/ts/mcp-server/generator.js +4 -2
  47. package/src/ts/mcp-server/generator.js.map +1 -1
  48. package/src/ts/mcp-server/schema.d.ts +3 -1
  49. package/src/ts/mcp-server/schema.json +4 -4
  50. package/src/ts/nx-generator/__snapshots__/generator.spec.ts.snap +27 -1
  51. package/src/ts/nx-generator/files/nx-plugin-for-aws/docs/__nameKebabCase__.mdx.template +31 -0
  52. package/src/ts/nx-generator/files/nx-plugin-for-aws/generator/generator.spec.ts.template +1 -1
  53. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +756 -0
  54. package/src/ts/react-website/app/generator.js +44 -43
  55. package/src/ts/react-website/app/generator.js.map +1 -1
  56. package/src/ts/react-website/app/schema.d.ts +2 -0
  57. package/src/ts/react-website/app/schema.json +8 -0
  58. package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +259 -0
  59. package/src/ts/react-website/cognito-auth/generator.js +12 -13
  60. package/src/ts/react-website/cognito-auth/generator.js.map +1 -1
  61. package/src/ts/react-website/cognito-auth/schema.d.ts +4 -0
  62. package/src/ts/react-website/cognito-auth/schema.json +8 -0
  63. package/src/ts/react-website/runtime-config/__snapshots__/generator.spec.ts.snap +0 -40
  64. package/src/ts/react-website/runtime-config/generator.js +0 -2
  65. package/src/ts/react-website/runtime-config/generator.js.map +1 -1
  66. package/src/utils/agent-core-constructs/agent-core-constructs.d.ts +4 -2
  67. package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
  68. package/src/utils/api-constructs/api-constructs.d.ts +2 -1
  69. package/src/utils/api-constructs/api-constructs.js.map +1 -1
  70. package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +10 -0
  71. package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +10 -0
  72. package/src/utils/config/index.d.ts +6 -0
  73. package/src/utils/config/index.js.map +1 -1
  74. package/src/utils/files/terraform/src/core/runtime-config/entry/entry.tf.template +119 -0
  75. package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +28 -0
  76. package/src/utils/files/terraform/src/metrics/metrics.tf.template +7 -1
  77. package/src/{py/lambda-function/files/common/constructs/src/app/lambda-functions/__constructFunctionKebabCase__.ts.template → utils/function-constructs/files/cdk/app/lambda-functions/__functionNameKebabCase__.ts.template} +5 -5
  78. package/src/utils/function-constructs/files/terraform/app/lambda-functions/__functionNameKebabCase__/__functionNameKebabCase__.tf.template +150 -0
  79. package/src/utils/function-constructs/function-constructs.d.ts +20 -0
  80. package/src/utils/function-constructs/function-constructs.js +57 -0
  81. package/src/utils/function-constructs/function-constructs.js.map +1 -0
  82. package/src/utils/iac.d.ts +21 -0
  83. package/src/utils/iac.js +25 -0
  84. package/src/utils/iac.js.map +1 -0
  85. package/src/utils/identity-constructs/files/terraform/core/user-identity/add-callback-url/add-callback-url.tf.template +123 -0
  86. package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +421 -0
  87. package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +47 -0
  88. package/src/utils/identity-constructs/identity-constructs.d.ts +16 -0
  89. package/src/utils/identity-constructs/identity-constructs.js +84 -0
  90. package/src/utils/identity-constructs/identity-constructs.js.map +1 -0
  91. package/src/utils/metrics.js +1 -1
  92. package/src/utils/metrics.js.map +1 -1
  93. package/src/utils/shared-constructs.d.ts +3 -2
  94. package/src/utils/shared-constructs.js +27 -3
  95. package/src/utils/shared-constructs.js.map +1 -1
  96. package/src/utils/website-constructs/files/terraform/app/static-websites/__websiteNameKebabCase__/__websiteNameKebabCase__.tf.template +42 -0
  97. package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +709 -0
  98. package/src/utils/website-constructs/website-constructs.d.ts +19 -0
  99. package/src/utils/website-constructs/website-constructs.js +61 -0
  100. package/src/utils/website-constructs/website-constructs.js.map +1 -0
  101. package/src/ts/lambda-function/files/common/constructs/src/app/lambda-functions/__constructFunctionNameKebabCase__.ts.template +0 -24
  102. /package/src/{ts/react-website/cognito-auth/files/common/constructs/src → utils/identity-constructs/files/cdk}/core/user-identity.ts.template +0 -0
  103. /package/src/{ts/react-website/app/files/common/constructs/src → utils/website-constructs/files/cdk}/app/static-websites/__websiteNameKebabCase__.ts.template +0 -0
  104. /package/src/{ts/react-website/app/files/common/constructs/src → utils/website-constructs/files/cdk}/core/static-website.ts.template +0 -0
@@ -123,6 +123,762 @@ exports[`react-website generator > TailwindCSS integration > should not include
123
123
  "
124
124
  `;
125
125
 
126
+ exports[`react-website generator > TailwindCSS integration > terraform iacProvider > should generate terraform files for static website and snapshot them > terraform-static-website-files 1`] = `
127
+ {
128
+ "static-website.tf": "terraform {
129
+ required_providers {
130
+ aws = {
131
+ source = "hashicorp/aws"
132
+ version = "~> 6.0"
133
+ configuration_aliases = [aws.us_east_1]
134
+ }
135
+ random = {
136
+ source = "hashicorp/random"
137
+ version = "~> 3.1"
138
+ }
139
+ }
140
+ }
141
+
142
+ # Variables
143
+ variable "website_name" {
144
+ description = "Name of the website"
145
+ type = string
146
+ }
147
+
148
+ variable "website_file_path" {
149
+ description = "Path to the website files"
150
+ type = string
151
+ }
152
+
153
+
154
+ # Data sources
155
+ data "aws_caller_identity" "current" {}
156
+ data "aws_region" "current" {}
157
+
158
+
159
+ # KMS Key for encryption
160
+ resource "aws_kms_key" "website_key" {
161
+ description = "KMS key for \${var.website_name} website encryption"
162
+ deletion_window_in_days = 7
163
+ enable_key_rotation = true
164
+
165
+ policy = jsonencode({
166
+ Version = "2012-10-17"
167
+ Statement = [
168
+ {
169
+ Sid = "Enable IAM User Permissions"
170
+ Effect = "Allow"
171
+ Principal = {
172
+ AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root"
173
+ }
174
+ Action = "kms:*"
175
+ Resource = "*"
176
+ },
177
+ {
178
+ Sid = "Allow S3 Service"
179
+ Effect = "Allow"
180
+ Principal = {
181
+ Service = "s3.amazonaws.com"
182
+ }
183
+ Action = [
184
+ "kms:Decrypt",
185
+ "kms:GenerateDataKey"
186
+ ]
187
+ Resource = "*"
188
+ },
189
+ {
190
+ Sid = "AllowCloudFrontServicePrincipalSSE-KMS"
191
+ Effect = "Allow"
192
+ Principal = {
193
+ Service = "cloudfront.amazonaws.com"
194
+ }
195
+ Action = [
196
+ "kms:Decrypt",
197
+ "kms:Encrypt",
198
+ "kms:GenerateDataKey*"
199
+ ]
200
+ Resource = "*"
201
+ Condition = {
202
+ StringEquals = {
203
+ "AWS:SourceArn" = aws_cloudfront_distribution.website.arn
204
+ }
205
+ }
206
+ }
207
+ ]
208
+ })
209
+
210
+ tags = {
211
+ Name = "\${lower(var.website_name)}-website-key-\${random_id.unique_suffix.hex}"
212
+ }
213
+ }
214
+
215
+ resource "aws_kms_alias" "website_key_alias" {
216
+ name = "alias/\${lower(var.website_name)}-website-key-\${random_id.unique_suffix.hex}"
217
+ target_key_id = aws_kms_key.website_key.key_id
218
+ }
219
+
220
+ # Access Logs Bucket
221
+ resource "aws_s3_bucket" "access_logs" {
222
+ #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for access logs bucket
223
+ #checkov:skip=CKV_AWS_144:Cross-region replication not required for access logs
224
+ #checkov:skip=CKV2_AWS_62:Event notifications not required for access logs bucket
225
+ #checkov:skip=CKV_AWS_21:Versioning disabled for access logs to reduce storage costs
226
+ bucket = "\${lower(var.website_name)}-access-logs-\${random_id.bucket_suffix.hex}"
227
+ force_destroy = true
228
+
229
+ tags = {
230
+ Name = "\${lower(var.website_name)}-access-logs"
231
+ }
232
+ }
233
+
234
+ resource "random_id" "unique_suffix" {
235
+ byte_length = 4
236
+ }
237
+
238
+ resource "random_id" "bucket_suffix" {
239
+ byte_length = 8
240
+ }
241
+
242
+ resource "aws_s3_bucket_versioning" "access_logs_versioning" {
243
+ bucket = aws_s3_bucket.access_logs.id
244
+ versioning_configuration {
245
+ status = "Disabled"
246
+ }
247
+ }
248
+
249
+ resource "aws_s3_bucket_server_side_encryption_configuration" "access_logs_encryption" {
250
+ bucket = aws_s3_bucket.access_logs.id
251
+
252
+ rule {
253
+ apply_server_side_encryption_by_default {
254
+ kms_master_key_id = aws_kms_key.website_key.arn
255
+ sse_algorithm = "aws:kms"
256
+ }
257
+ }
258
+ }
259
+
260
+ resource "aws_s3_bucket_public_access_block" "access_logs_pab" {
261
+ bucket = aws_s3_bucket.access_logs.id
262
+
263
+ block_public_acls = true
264
+ block_public_policy = true
265
+ ignore_public_acls = true
266
+ restrict_public_buckets = true
267
+ }
268
+
269
+ resource "aws_s3_bucket_policy" "access_logs_ssl_policy" {
270
+ bucket = aws_s3_bucket.access_logs.id
271
+
272
+ policy = jsonencode({
273
+ Version = "2012-10-17"
274
+ Statement = [
275
+ {
276
+ Sid = "DenyInsecureConnections"
277
+ Effect = "Deny"
278
+ Principal = "*"
279
+ Action = "s3:*"
280
+ Resource = [
281
+ aws_s3_bucket.access_logs.arn,
282
+ "\${aws_s3_bucket.access_logs.arn}/*"
283
+ ]
284
+ Condition = {
285
+ Bool = {
286
+ "aws:SecureTransport" = "false"
287
+ }
288
+ }
289
+ }
290
+ ]
291
+ })
292
+ }
293
+
294
+ # Website Bucket
295
+ resource "aws_s3_bucket" "website" {
296
+ #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for static website content
297
+ #checkov:skip=CKV_AWS_144:Cross-region replication not required for static website
298
+ #checkov:skip=CKV2_AWS_62:Event notifications not required for static website bucket
299
+ bucket = "\${lower(var.website_name)}-website-\${random_id.bucket_suffix.hex}"
300
+ force_destroy = true
301
+
302
+ tags = {
303
+ Name = "\${lower(var.website_name)}-website"
304
+ }
305
+ }
306
+
307
+ resource "aws_s3_bucket_versioning" "website_versioning" {
308
+ bucket = aws_s3_bucket.website.id
309
+ versioning_configuration {
310
+ status = "Enabled"
311
+ }
312
+ }
313
+
314
+ resource "aws_s3_bucket_server_side_encryption_configuration" "website_encryption" {
315
+ bucket = aws_s3_bucket.website.id
316
+
317
+ rule {
318
+ apply_server_side_encryption_by_default {
319
+ kms_master_key_id = aws_kms_key.website_key.arn
320
+ sse_algorithm = "aws:kms"
321
+ }
322
+ }
323
+ }
324
+
325
+ resource "aws_s3_bucket_public_access_block" "website_pab" {
326
+ bucket = aws_s3_bucket.website.id
327
+
328
+ block_public_acls = true
329
+ block_public_policy = true
330
+ ignore_public_acls = true
331
+ restrict_public_buckets = true
332
+ }
333
+
334
+ resource "aws_s3_bucket_ownership_controls" "website_ownership" {
335
+ bucket = aws_s3_bucket.website.id
336
+
337
+ rule {
338
+ object_ownership = "BucketOwnerEnforced"
339
+ }
340
+ }
341
+
342
+ resource "aws_s3_bucket_logging" "website_logging" {
343
+ bucket = aws_s3_bucket.website.id
344
+
345
+ target_bucket = aws_s3_bucket.access_logs.id
346
+ target_prefix = "website-access-logs/"
347
+ }
348
+
349
+
350
+ # Distribution Log Bucket
351
+ resource "aws_s3_bucket" "distribution_logs" {
352
+ #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for CloudFront logs
353
+ #checkov:skip=CKV_AWS_144:Cross-region replication not required for CloudFront logs
354
+ #checkov:skip=CKV2_AWS_62:Event notifications not required for CloudFront logs bucket
355
+ #checkov:skip=CKV_AWS_21:Versioning not required for CloudFront access logs
356
+ bucket = "\${lower(var.website_name)}-distribution-logs-\${random_id.bucket_suffix.hex}"
357
+ force_destroy = true
358
+
359
+ tags = {
360
+ Name = "\${lower(var.website_name)}-distribution-logs"
361
+ }
362
+ }
363
+
364
+ resource "aws_s3_bucket_server_side_encryption_configuration" "distribution_logs_encryption" {
365
+ bucket = aws_s3_bucket.distribution_logs.id
366
+
367
+ rule {
368
+ apply_server_side_encryption_by_default {
369
+ kms_master_key_id = aws_kms_key.website_key.arn
370
+ sse_algorithm = "aws:kms"
371
+ }
372
+ }
373
+ }
374
+
375
+ resource "aws_s3_bucket_public_access_block" "distribution_logs_pab" {
376
+ bucket = aws_s3_bucket.distribution_logs.id
377
+
378
+ block_public_acls = true
379
+ block_public_policy = true
380
+ ignore_public_acls = true
381
+ restrict_public_buckets = true
382
+ }
383
+
384
+ resource "aws_s3_bucket_ownership_controls" "distribution_logs_ownership" {
385
+ #checkov:skip=CKV2_AWS_65:BucketOwnerPreferred required for CloudFront logging compatibility
386
+ bucket = aws_s3_bucket.distribution_logs.id
387
+
388
+ rule {
389
+ object_ownership = "BucketOwnerPreferred"
390
+ }
391
+ }
392
+
393
+
394
+ resource "aws_s3_bucket_logging" "distribution_logs_logging" {
395
+ bucket = aws_s3_bucket.distribution_logs.id
396
+
397
+ target_bucket = aws_s3_bucket.access_logs.id
398
+ target_prefix = "distribution-access-logs/"
399
+ }
400
+
401
+ resource "aws_s3_bucket_policy" "distribution_logs_ssl_policy" {
402
+ bucket = aws_s3_bucket.distribution_logs.id
403
+
404
+ policy = jsonencode({
405
+ Version = "2012-10-17"
406
+ Statement = [
407
+ {
408
+ Sid = "DenyInsecureConnections"
409
+ Effect = "Deny"
410
+ Principal = "*"
411
+ Action = "s3:*"
412
+ Resource = [
413
+ aws_s3_bucket.distribution_logs.arn,
414
+ "\${aws_s3_bucket.distribution_logs.arn}/*"
415
+ ]
416
+ Condition = {
417
+ Bool = {
418
+ "aws:SecureTransport" = "false"
419
+ }
420
+ }
421
+ }
422
+ ]
423
+ })
424
+ }
425
+
426
+ # WAF Web ACL (must be in us-east-1 for CloudFront)
427
+ resource "aws_wafv2_web_acl" "cloudfront_waf" {
428
+ #checkov:skip=CKV2_AWS_31:WAF logging disabled
429
+ provider = aws.us_east_1
430
+ name = "\${lower(var.website_name)}-cloudfront-waf-\${random_id.unique_suffix.hex}"
431
+ scope = "CLOUDFRONT"
432
+
433
+ default_action {
434
+ allow {}
435
+ }
436
+
437
+ rule {
438
+ name = "CRSRule"
439
+ priority = 0
440
+
441
+ override_action {
442
+ none {}
443
+ }
444
+
445
+ statement {
446
+ managed_rule_group_statement {
447
+ name = "AWSManagedRulesCommonRuleSet"
448
+ vendor_name = "AWS"
449
+ }
450
+ }
451
+
452
+ visibility_config {
453
+ cloudwatch_metrics_enabled = true
454
+ metric_name = "MetricForWebACLCDK-CRS"
455
+ sampled_requests_enabled = true
456
+ }
457
+ }
458
+
459
+ rule {
460
+ name = "KnownBadInputsRule"
461
+ priority = 1
462
+
463
+ override_action {
464
+ none {}
465
+ }
466
+
467
+ statement {
468
+ managed_rule_group_statement {
469
+ name = "AWSManagedRulesKnownBadInputsRuleSet"
470
+ vendor_name = "AWS"
471
+ }
472
+ }
473
+
474
+ visibility_config {
475
+ cloudwatch_metrics_enabled = true
476
+ metric_name = "MetricForWebACLCDK-KnownBadInputs"
477
+ sampled_requests_enabled = true
478
+ }
479
+ }
480
+
481
+ visibility_config {
482
+ cloudwatch_metrics_enabled = true
483
+ metric_name = "\${lower(var.website_name)}-waf"
484
+ sampled_requests_enabled = true
485
+ }
486
+
487
+ tags = {
488
+ Name = "\${lower(var.website_name)}-cloudfront-waf-\${random_id.unique_suffix.hex}"
489
+ }
490
+
491
+ lifecycle {
492
+ create_before_destroy = true
493
+ }
494
+ }
495
+
496
+
497
+ # Origin Access Control
498
+ resource "aws_cloudfront_origin_access_control" "website_oac" {
499
+ name = "\${lower(var.website_name)}-oac-\${random_id.unique_suffix.hex}"
500
+ description = "Origin Access Control for \${lower(var.website_name)}"
501
+ origin_access_control_origin_type = "s3"
502
+ signing_behavior = "always"
503
+ signing_protocol = "sigv4"
504
+ }
505
+
506
+ # CloudFront Distribution
507
+ resource "aws_cloudfront_distribution" "website" {
508
+ #checkov:skip=CKV_AWS_174:Using CloudFront default certificate which does not support TLS v1.2
509
+ #checkov:skip=CKV_AWS_310:Origin failover not required for single S3 origin static website
510
+ #checkov:skip=CKV_AWS_374:Geo restrictions not required for global web application
511
+ #checkov:skip=CKV2_AWS_42:Custom SSL certificate not required for development - using CloudFront default
512
+ #checkov:skip=CKV2_AWS_32:Response headers policy not required for basic static website
513
+ #checkov:skip=CKV2_AWS_47:WAF includes AWSManagedRulesKnownBadInputsRuleSet which provides Log4j protection
514
+ origin {
515
+ domain_name = aws_s3_bucket.website.bucket_regional_domain_name
516
+ origin_access_control_id = aws_cloudfront_origin_access_control.website_oac.id
517
+ origin_id = "S3-\${aws_s3_bucket.website.bucket}"
518
+ }
519
+
520
+ enabled = true
521
+ is_ipv6_enabled = true
522
+ default_root_object = "index.html"
523
+ web_acl_id = aws_wafv2_web_acl.cloudfront_waf.arn
524
+
525
+ logging_config {
526
+ include_cookies = false
527
+ bucket = aws_s3_bucket.distribution_logs.bucket_regional_domain_name
528
+ }
529
+
530
+ default_cache_behavior {
531
+ allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
532
+ cached_methods = ["GET", "HEAD"]
533
+ target_origin_id = "S3-\${aws_s3_bucket.website.bucket}"
534
+
535
+ forwarded_values {
536
+ query_string = false
537
+ cookies {
538
+ forward = "none"
539
+ }
540
+ }
541
+
542
+ viewer_protocol_policy = "redirect-to-https"
543
+ min_ttl = 0
544
+ default_ttl = 3600
545
+ max_ttl = 86400
546
+ compress = true
547
+ }
548
+
549
+ # Custom error responses for SPA routing
550
+ custom_error_response {
551
+ error_code = 404
552
+ response_code = 200
553
+ response_page_path = "/index.html"
554
+ }
555
+
556
+ custom_error_response {
557
+ error_code = 403
558
+ response_code = 200
559
+ response_page_path = "/index.html"
560
+ }
561
+
562
+ restrictions {
563
+ geo_restriction {
564
+ restriction_type = "none"
565
+ }
566
+ }
567
+
568
+ viewer_certificate {
569
+ cloudfront_default_certificate = true
570
+ }
571
+
572
+ tags = {
573
+ Name = "\${lower(var.website_name)}-distribution-\${random_id.unique_suffix.hex}"
574
+ }
575
+
576
+ lifecycle {
577
+ replace_triggered_by = [
578
+ aws_wafv2_web_acl.cloudfront_waf
579
+ ]
580
+ }
581
+ }
582
+
583
+ # S3 Bucket Policy for CloudFront OAC
584
+ resource "aws_s3_bucket_policy" "website_cloudfront_policy" {
585
+ bucket = aws_s3_bucket.website.id
586
+
587
+ policy = jsonencode({
588
+ Version = "2012-10-17"
589
+ Statement = [
590
+ {
591
+ Sid = "AllowCloudFrontServicePrincipal"
592
+ Effect = "Allow"
593
+ Principal = {
594
+ Service = "cloudfront.amazonaws.com"
595
+ }
596
+ Action = "s3:GetObject"
597
+ Resource = "\${aws_s3_bucket.website.arn}/*"
598
+ Condition = {
599
+ StringEquals = {
600
+ "AWS:SourceArn" = aws_cloudfront_distribution.website.arn
601
+ }
602
+ }
603
+ },
604
+ {
605
+ Sid = "DenyInsecureConnections"
606
+ Effect = "Deny"
607
+ Principal = "*"
608
+ Action = "s3:*"
609
+ Resource = [
610
+ aws_s3_bucket.website.arn,
611
+ "\${aws_s3_bucket.website.arn}/*"
612
+ ]
613
+ Condition = {
614
+ Bool = {
615
+ "aws:SecureTransport" = "false"
616
+ }
617
+ }
618
+ }
619
+ ]
620
+ })
621
+
622
+ depends_on = [aws_cloudfront_distribution.website]
623
+ }
624
+
625
+ # Read runtime config using the reader module
626
+ module "runtime_config_reader" {
627
+ source = "../runtime-config/read"
628
+ }
629
+
630
+ # Upload website files to S3
631
+ resource "null_resource" "upload_website_files" {
632
+ triggers = {
633
+ # Trigger on any change to the website directory
634
+ website_path = var.website_file_path
635
+ # Trigger if any file in the directory changes using directory hash
636
+ directory_hash = sha256(join("", [for f in fileset(var.website_file_path, "**") : filesha256("\${var.website_file_path}/\${f}")]))
637
+ }
638
+
639
+ provisioner "local-exec" {
640
+ command = <<-EOT
641
+ cd "\${path.root}"
642
+ uv run --with boto3 python3 -c "
643
+ import os
644
+ import sys
645
+ import boto3
646
+ import mimetypes
647
+ from pathlib import Path
648
+ from botocore.exceptions import ClientError, NoCredentialsError
649
+
650
+ def sync_to_s3(local_path, bucket_name):
651
+ try:
652
+ s3_client = boto3.client('s3')
653
+
654
+ # Check if local directory exists
655
+ if not os.path.isdir(local_path):
656
+ print(f'Error: Website directory not found at {local_path}')
657
+ sys.exit(1)
658
+
659
+ # Get existing objects in bucket (for deletion)
660
+ try:
661
+ existing_objects = set()
662
+ paginator = s3_client.get_paginator('list_objects_v2')
663
+ for page in paginator.paginate(Bucket=bucket_name):
664
+ if 'Contents' in page:
665
+ for obj in page['Contents']:
666
+ existing_objects.add(obj['Key'])
667
+ except ClientError as e:
668
+ if e.response['Error']['Code'] != 'NoSuchBucket':
669
+ raise
670
+ existing_objects = set()
671
+
672
+ # Upload files
673
+ uploaded_objects = set()
674
+ local_path_obj = Path(local_path)
675
+
676
+ for file_path in local_path_obj.rglob('*'):
677
+ if file_path.is_file():
678
+ # Skip runtime-config.json as it's handled separately
679
+ if file_path.name == 'runtime-config.json':
680
+ continue
681
+
682
+ # Calculate S3 key (relative path from local_path)
683
+ relative_path = file_path.relative_to(local_path_obj)
684
+ s3_key = str(relative_path).replace('\\\\\\\\', '/')
685
+ uploaded_objects.add(s3_key)
686
+
687
+ # Determine content type
688
+ content_type, _ = mimetypes.guess_type(str(file_path))
689
+ if content_type is None:
690
+ content_type = 'binary/octet-stream'
691
+
692
+ # Upload file
693
+ try:
694
+ s3_client.upload_file(
695
+ str(file_path),
696
+ bucket_name,
697
+ s3_key,
698
+ ExtraArgs={'ContentType': content_type}
699
+ )
700
+ print(f'Uploaded: {s3_key}')
701
+ except ClientError as e:
702
+ print(f'Error uploading {s3_key}: {e}')
703
+ sys.exit(1)
704
+
705
+ # Delete objects that no longer exist locally (excluding runtime-config.json)
706
+ objects_to_delete = existing_objects - uploaded_objects - {'runtime-config.json'}
707
+ if objects_to_delete:
708
+ delete_objects = [{'Key': key} for key in objects_to_delete]
709
+ try:
710
+ s3_client.delete_objects(
711
+ Bucket=bucket_name,
712
+ Delete={'Objects': delete_objects}
713
+ )
714
+ for obj in delete_objects:
715
+ print(f'Deleted: {obj[\\"Key\\"]}')
716
+ except ClientError as e:
717
+ print(f'Error deleting objects: {e}')
718
+ sys.exit(1)
719
+
720
+ print(f'Website files synced to s3://{bucket_name}/')
721
+
722
+ except NoCredentialsError:
723
+ print('Error: AWS credentials not found')
724
+ sys.exit(1)
725
+ except Exception as e:
726
+ print(f'Error: {e}')
727
+ sys.exit(1)
728
+
729
+ # Execute sync
730
+ sync_to_s3('\${var.website_file_path}', '\${aws_s3_bucket.website.bucket}')
731
+ "
732
+ EOT
733
+ }
734
+
735
+ depends_on = [aws_s3_bucket_policy.website_cloudfront_policy]
736
+ }
737
+
738
+ # Upload runtime config file
739
+ resource "aws_s3_object" "runtime_config" {
740
+ bucket = aws_s3_bucket.website.id
741
+ key = "runtime-config.json"
742
+ content = module.runtime_config_reader.config_json
743
+ content_type = "application/json"
744
+ etag = md5(module.runtime_config_reader.config_json)
745
+
746
+ depends_on = [null_resource.upload_website_files]
747
+ }
748
+
749
+ # Invalidate CloudFront cache after uploads
750
+ resource "null_resource" "cloudfront_invalidation" {
751
+ triggers = {
752
+ # Trigger when files or runtime config change
753
+ files_trigger = null_resource.upload_website_files.id
754
+ config_trigger = aws_s3_object.runtime_config.etag
755
+ }
756
+
757
+ provisioner "local-exec" {
758
+ command = <<-EOT
759
+ uv run --with boto3 python3 -c "
760
+ import boto3
761
+ import sys
762
+ from botocore.exceptions import ClientError, NoCredentialsError
763
+
764
+ def create_invalidation(distribution_id):
765
+ try:
766
+ cloudfront_client = boto3.client('cloudfront')
767
+
768
+ # Create invalidation for all paths
769
+ response = cloudfront_client.create_invalidation(
770
+ DistributionId=distribution_id,
771
+ InvalidationBatch={
772
+ 'Paths': {
773
+ 'Quantity': 1,
774
+ 'Items': ['/*']
775
+ },
776
+ 'CallerReference': f'terraform-invalidation-{distribution_id}-{hash(distribution_id) % 1000000}'
777
+ }
778
+ )
779
+
780
+ invalidation_id = response['Invalidation']['Id']
781
+ print(f'CloudFront cache invalidation created: {invalidation_id}')
782
+ print(f'Distribution: \${aws_cloudfront_distribution.website.id}')
783
+ print(f'Status: {response[\\"Invalidation\\"][\\"Status\\"]}')
784
+
785
+ except NoCredentialsError:
786
+ print('Error: AWS credentials not found')
787
+ sys.exit(1)
788
+ except ClientError as e:
789
+ print(f'Error creating CloudFront invalidation: {e}')
790
+ sys.exit(1)
791
+ except Exception as e:
792
+ print(f'Error: {e}')
793
+ sys.exit(1)
794
+
795
+ # Execute invalidation
796
+ create_invalidation('\${aws_cloudfront_distribution.website.id}')
797
+ "
798
+ EOT
799
+ }
800
+
801
+ depends_on = [
802
+ null_resource.upload_website_files,
803
+ aws_s3_object.runtime_config
804
+ ]
805
+ }
806
+
807
+ # Outputs
808
+ output "website_bucket_name" {
809
+ description = "Name of the S3 bucket hosting the website"
810
+ value = aws_s3_bucket.website.bucket
811
+ }
812
+
813
+ output "website_bucket_arn" {
814
+ description = "ARN of the S3 bucket hosting the website"
815
+ value = aws_s3_bucket.website.arn
816
+ }
817
+
818
+ output "cloudfront_distribution_id" {
819
+ description = "ID of the CloudFront distribution"
820
+ value = aws_cloudfront_distribution.website.id
821
+ }
822
+
823
+ output "cloudfront_distribution_arn" {
824
+ description = "ARN of the CloudFront distribution"
825
+ value = aws_cloudfront_distribution.website.arn
826
+ }
827
+
828
+ output "cloudfront_domain_name" {
829
+ description = "Domain name of the CloudFront distribution"
830
+ value = aws_cloudfront_distribution.website.domain_name
831
+ }
832
+
833
+ output "waf_web_acl_arn" {
834
+ description = "ARN of the WAF Web ACL"
835
+ value = aws_wafv2_web_acl.cloudfront_waf.arn
836
+ }",
837
+ "test-app.tf": "terraform {
838
+ required_providers {
839
+ aws = {
840
+ source = "hashicorp/aws"
841
+ version = "~> 6.0"
842
+ configuration_aliases = [aws.us_east_1]
843
+ }
844
+ }
845
+ }
846
+
847
+ # Static website module configured for the web package
848
+ module "static_website" {
849
+ source = "../../../core/static-website"
850
+
851
+ website_name = "test-app"
852
+ website_file_path = "\${path.module}/../../../../../../../dist/test-app/bundle"
853
+
854
+ providers = {
855
+ aws.us_east_1 = aws.us_east_1
856
+ }
857
+ }
858
+
859
+ # Outputs
860
+ output "website_url" {
861
+ description = "URL of the deployed website"
862
+ value = "https://\${module.static_website.cloudfront_domain_name}"
863
+ }
864
+
865
+ output "website_bucket_name" {
866
+ description = "Name of the S3 bucket hosting the website"
867
+ value = module.static_website.website_bucket_name
868
+ }
869
+
870
+ output "cloudfront_distribution_id" {
871
+ description = "ID of the CloudFront distribution"
872
+ value = module.static_website.cloudfront_distribution_id
873
+ }
874
+
875
+ output "cloudfront_domain_name" {
876
+ description = "Domain name of the CloudFront distribution"
877
+ value = module.static_website.cloudfront_domain_name
878
+ }",
879
+ }
880
+ `;
881
+
126
882
  exports[`react-website generator > Tanstack router integration > should generate website with no router correctly > .gitignore 1`] = `
127
883
  "vite.config.*.timestamp*
128
884
  vitest.config.*.timestamp*