@aws/nx-plugin 1.0.3 → 1.0.5

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 (39) hide show
  1. package/LICENSE-THIRD-PARTY +7 -7
  2. package/migrations.json +10 -5
  3. package/package.json +4 -4
  4. package/src/infra/app/__snapshots__/generator.spec.ts.snap +3 -3
  5. package/src/license/dependency-check/collectors/npm-collector.d.ts +14 -0
  6. package/src/license/dependency-check/collectors/npm-collector.js +35 -1
  7. package/src/license/dependency-check/collectors/npm-collector.js.map +1 -1
  8. package/src/migrations/latest/api-custom-domain-runtime-config/metadata.json +3 -0
  9. package/src/migrations/latest/api-custom-domain-runtime-config/migration.js +362 -0
  10. package/src/migrations/latest/api-custom-domain-runtime-config/migration.js.map +1 -0
  11. package/src/migrations/v1.0.3/0001-terraform-init-target/migration.d.ts +6 -0
  12. package/src/migrations/v1.0.3/0001-terraform-init-target/migration.js.map +1 -0
  13. package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +3 -3
  14. package/src/py/agent/__snapshots__/generator.frameworks.spec.ts.snap +3 -3
  15. package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +1 -1
  16. package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +54 -6
  17. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +378 -6
  18. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +3 -3
  19. package/src/py/rdb/__snapshots__/generator.spec.ts.snap +2 -2
  20. package/src/smithy/project/__snapshots__/generator.spec.ts.snap +16 -16
  21. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +253 -5
  22. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +432 -12
  23. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +2 -2
  24. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +1 -1
  25. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +1 -1
  26. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +13 -13
  27. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +1 -1
  28. package/src/utils/api-constructs/files/cdk/core/api/http/http-api.ts.template +34 -4
  29. package/src/utils/api-constructs/files/cdk/core/api/rest/rest-api.ts.template +20 -2
  30. package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +63 -1
  31. package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +63 -1
  32. package/src/utils/docker.d.ts +1 -1
  33. package/src/utils/versions.d.ts +54 -54
  34. package/src/utils/versions.js +54 -54
  35. package/src/utils/versions.js.map +1 -1
  36. package/src/migrations/latest/terraform-init-target/migration.js.map +0 -1
  37. /package/src/migrations/latest/{terraform-init-target → api-custom-domain-runtime-config}/migration.d.ts +0 -0
  38. /package/src/migrations/{latest/terraform-init-target → v1.0.3/0001-terraform-init-target}/metadata.json +0 -0
  39. /package/src/migrations/{latest/terraform-init-target → v1.0.3/0001-terraform-init-target}/migration.js +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/api-custom-domain-runtime-config/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n joinPathFragments,\n type MigrationReturnObject,\n type Tree,\n} from '@nx/devkit';\nimport {\n addDestructuredImport,\n applyGritQL,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast.js';\nimport { formatFilesInSubtree } from '../../../utils/format.js';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants.js';\n\n/**\n * Publish an API's custom domain, when one is configured, as its URL in runtime\n * config, so clients call it rather than the generated execute-api endpoint.\n *\n * - The vended CDK `RestApi` publishes `https://<domainName>/[<basePath>/]` when\n * `domainName` is set.\n * - The vended CDK `HttpApi` forwards `defaultDomainMapping` to the stage it\n * creates (previously rejected by CDK, since the construct disables the\n * default stage) and publishes the mapping's domain URL.\n * - Each vended Terraform API app module (`app/apis/<name>/<name>.tf`) gains\n * `custom_domain_name` / `acm_certificate_arn` variables, the domain and\n * mapping resources, an `api_url` local published in runtime config, and\n * outputs for the URL and DNS record targets.\n *\n * These files are generated with `KeepExisting`, so an upgraded workspace keeps\n * the old shape until this runs. Diverged files are left untouched and\n * reported via `nextSteps`.\n */\n\nconst CONSTRUCTS_API_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/api`;\nconst REST_API_CONSTRUCT = `${CONSTRUCTS_API_DIR}/rest-api.ts`;\nconst HTTP_API_CONSTRUCT = `${CONSTRUCTS_API_DIR}/http-api.ts`;\nconst TERRAFORM_APIS_DIR = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/app/apis`;\n\nconst hcl = (pattern: string) => `language hcl\\n${pattern}`;\n\n/** Whether the pattern matches any `.tf` file in the Terraform module directory. */\nconst matchesInModule = async (\n tree: Tree,\n moduleDir: string,\n pattern: string,\n) => {\n for (const child of tree.children(moduleDir)) {\n const path = joinPathFragments(moduleDir, child);\n if (\n child.endsWith('.tf') &&\n tree.isFile(path) &&\n (await matchGritQL(tree, path, pattern))\n ) {\n return true;\n }\n }\n return false;\n};\n\n/** Whether every pattern matches the file. */\nconst matchesAll = async (tree: Tree, file: string, patterns: string[]) => {\n for (const pattern of patterns) {\n if (!(await matchGritQL(tree, file, pattern))) {\n return false;\n }\n }\n return true;\n};\n\nconst REST_RUNTIME_CONFIG_URL = `domainName\n ? \\`https://\\${domainName.domainName}/\\${domainName.basePath ? \\`\\${domainName.basePath}/\\` : ''}\\`\n : this.api.url!`;\n\nconst REST_DOMAIN_OUTPUTS = `// Target for the custom domain's DNS record\n if (domainName) {\n new CfnOutput(this, \\`\\${apiName}DomainNameAlias\\`, {\n value: this.api.domainName!.domainNameAliasDomainName,\n description:\n 'Domain name of the API Gateway endpoint (regional or edge) to point a CNAME record at for the custom domain, with any DNS provider',\n });\n new CfnOutput(this, \\`\\${apiName}DomainNameAliasHostedZoneId\\`, {\n value: this.api.domainName!.domainNameAliasHostedZoneId,\n description:\n 'Hosted zone ID of that domain name, needed only for a Route 53 alias record',\n });\n }`;\n\nconst HTTP_DOMAIN_OUTPUTS = `// Target for the custom domain's DNS record\n if (defaultDomainMapping) {\n new CfnOutput(this, \\`\\${apiName}DomainNameAlias\\`, {\n value: defaultDomainMapping.domainName.regionalDomainName,\n description:\n 'Regional domain name to point a CNAME record at for the custom domain, with any DNS provider',\n });\n new CfnOutput(this, \\`\\${apiName}DomainNameAliasHostedZoneId\\`, {\n value: defaultDomainMapping.domainName.regionalHostedZoneId,\n description:\n 'Hosted zone ID of the regional domain name, needed only for a Route 53 alias record',\n });\n }`;\n\nconst HTTP_DOMAIN_MAPPING_PROP = `/**\n * Custom domain mapped to the API's default stage\n */\n readonly defaultDomainMapping?: DomainMappingOptions & {\n readonly domainName: IDomainName;\n }`;\n\n/** Matches a DNS record output this migration adds. */\nconst DOMAIN_ALIAS_OUTPUT =\n '`new CfnOutput(this, $id, $_)` where { $id <: r\".*DomainNameAlias.*\" }';\n\n/** Rewrite inserting {@link GRIT_INSERT_PLACEHOLDER} after the runtime config registration. */\nconst AFTER_RUNTIME_CONFIG = `\\`$call;\\` as $statement where {\n $call <: \\`rc.set('connection', 'apis', $_)\\`,\n $statement => \\`$call;\\n\\n ${GRIT_INSERT_PLACEHOLDER}\\`\n}`;\n\nconst migrateRestApiConstruct = async (tree: Tree, nextSteps: string[]) => {\n const file = REST_API_CONSTRUCT;\n if (!tree.exists(file)) {\n return;\n }\n\n if (await matchGritQL(tree, file, '`[apiName]: domainName ? $_ : $_`')) {\n return; // Already migrated.\n }\n\n const shape = [\n '`{ $params }: RestApiProps<$_, $_>` where { $params <: contains `...props`, $params <: not contains `domainName` }',\n \"`new _RestApi(this, 'Api', { $props })` where { $props <: contains `...props`, $props <: not contains `domainName` }\",\n \"`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.api.url!` }\",\n ];\n if (\n !(await matchesAll(tree, file, shape)) ||\n (await matchGritQL(tree, file, DOMAIN_ALIAS_OUTPUT))\n ) {\n nextSteps.push(\n `${file}: has diverged from the generated shape - left untouched. To publish a configured custom domain to clients, destructure \\`domainName\\` from the constructor props and pass it to the CDK RestApi, register \\`https://\\${domainName.domainName}/\\` (plus any \\`basePath\\`) under \\`[apiName]\\` in the \\`rc.set('connection', 'apis', ...)\\` call when it is set instead of \\`this.api.url\\`, and output \\`this.api.domainName.domainNameAliasDomainName\\` / \\`domainNameAliasHostedZoneId\\` for the DNS record.`,\n );\n return;\n }\n\n // Taken out of `props` and passed explicitly, so it is in scope below.\n await applyGritQL(\n tree,\n file,\n '`{ $params }: RestApiProps<$_, $_>` where { $params <: contains `...props` as $rest, $rest => `domainName,\\n ...props` }',\n );\n await applyGritQL(\n tree,\n file,\n \"`new _RestApi(this, 'Api', { $props })` where { $props <: contains `...props` as $spread, $spread => `...props,\\n domainName` }\",\n );\n await insertViaGritQL(\n tree,\n file,\n \"`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.api.url!` as $url, $url => `[apiName]: __GRIT_INSERT_PLACEHOLDER__` }\",\n REST_RUNTIME_CONFIG_URL,\n );\n await insertViaGritQL(tree, file, AFTER_RUNTIME_CONFIG, REST_DOMAIN_OUTPUTS);\n // Listed first, as the generator vends it.\n const imported = await applyGritQL(\n tree,\n file,\n \"`import { $names } from 'aws-cdk-lib'` where { $names <: not contains `CfnOutput` } => `import { CfnOutput, $names } from 'aws-cdk-lib'`\",\n );\n if (!imported) {\n await addDestructuredImport(tree, file, ['CfnOutput'], 'aws-cdk-lib');\n }\n};\n\nconst HTTP_API_URL_FIELD = `/** The URL clients call: the custom domain's if one is mapped, otherwise the stage's */\n private readonly apiUrl: string`;\n\nconst HTTP_API_URL_ASSIGNMENT = `this.apiUrl = defaultDomainMapping\n ? this.defaultStage.domainUrl\n : this.defaultStage.url!;`;\n\n/** Matches the vended `url` getter, returning the stage URL. */\nconst HTTP_URL_GETTER =\n 'method_definition(name=$name, body=$body) where { $name <: `url`, $body <: contains `return this.defaultStage.url` }';\n\nconst migrateHttpApiConstruct = async (tree: Tree, nextSteps: string[]) => {\n const file = HTTP_API_CONSTRUCT;\n if (!tree.exists(file)) {\n return;\n }\n\n if (await matchGritQL(tree, file, '`[apiName]: this.apiUrl`')) {\n return; // Already migrated.\n }\n\n const shape = [\n 'interface_declaration(name=$name, body=$body) as $interface where { $name <: `HttpApiProps`, $interface <: contains extends_type_clause() as $extends, $extends <: contains `_HttpApiProps`, $extends <: not contains `Omit`, $body <: contains `readonly throttle?: ThrottleSettings`, $body <: not contains `defaultDomainMapping` }',\n \"`import { $names } from 'aws-cdk-lib/aws-apigatewayv2'` where { $names <: contains `HttpApi as _HttpApi`, $names <: contains `HttpStage`, $names <: not contains `DomainMappingOptions`, $names <: not contains `IDomainName` }\",\n '`public readonly defaultStage: HttpStage`',\n '`{ $params }: HttpApiProps<$_, $_>` where { $params <: contains `...props`, $params <: not contains `defaultDomainMapping` }',\n \"`this.defaultStage = new HttpStage(this, 'DefaultStage', { $props })` where { $props <: contains `httpApi: this.api`, $props <: not contains `domainMapping` }\",\n '`new CfnOutput(this, $_, { $props })` where { $props <: contains `value: this.defaultStage.url!` }',\n \"`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.defaultStage.url!` }\",\n HTTP_URL_GETTER,\n ];\n if (\n !(await matchesAll(tree, file, shape)) ||\n (await matchGritQL(tree, file, DOMAIN_ALIAS_OUTPUT)) ||\n (await matchGritQL(tree, file, '`this.apiUrl`'))\n ) {\n nextSteps.push(\n `${file}: has diverged from the generated shape - left untouched. To support a custom domain, destructure \\`defaultDomainMapping\\` from the constructor props (so it is not passed to the CDK HttpApi, which rejects it when the default stage is disabled) and pass it as \\`domainMapping\\` to the \\`DefaultStage\\` HttpStage; use \\`this.defaultStage.domainUrl\\` instead of \\`this.defaultStage.url\\` when it is set, for the \\`<apiName>Url\\` output, runtime config and the \\`url\\` getter; and output \\`defaultDomainMapping.domainName.regionalDomainName\\` / \\`regionalHostedZoneId\\` for the DNS record.`,\n );\n return;\n }\n\n // Typed as a domain exposing its regional attributes, for the DNS outputs.\n await insertViaGritQL(\n tree,\n file,\n 'interface_declaration(name=$name, body=$body) where { $name <: `HttpApiProps`, $body <: contains `readonly throttle?: ThrottleSettings` as $throttle, $throttle => `readonly throttle?: ThrottleSettings;\\n __GRIT_INSERT_PLACEHOLDER__` }',\n HTTP_DOMAIN_MAPPING_PROP,\n );\n // Only the `_HttpApiProps` base is replaced, keeping any others.\n await applyGritQL(\n tree,\n file,\n \"interface_declaration(name=$name) as $interface where { $name <: `HttpApiProps`, $interface <: contains extends_type_clause() as $extends, $extends <: contains `_HttpApiProps` as $base, $base => `Omit<_HttpApiProps, 'defaultDomainMapping'>` }\",\n );\n // Inserted in sorted position, as the generator vends them.\n await applyGritQL(\n tree,\n file,\n \"`import { $names } from 'aws-cdk-lib/aws-apigatewayv2'` where { $names <: contains `HttpApi as _HttpApi` as $httpApi, $httpApi => `DomainMappingOptions,\\n HttpApi as _HttpApi` }\",\n );\n await applyGritQL(\n tree,\n file,\n \"`import { $names } from 'aws-cdk-lib/aws-apigatewayv2'` where { $names <: contains `HttpStage` as $stage, $stage => `HttpStage,\\n IDomainName` }\",\n );\n await insertViaGritQL(\n tree,\n file,\n '`public readonly defaultStage: HttpStage` as $field => `public readonly defaultStage: HttpStage;\\n\\n __GRIT_INSERT_PLACEHOLDER__`',\n HTTP_API_URL_FIELD,\n );\n // Taken out of `props` so it isn't passed to the CDK HttpApi.\n await applyGritQL(\n tree,\n file,\n '`{ $params }: HttpApiProps<$_, $_>` where { $params <: contains `...props` as $rest, $rest => `defaultDomainMapping,\\n ...props` }',\n );\n await applyGritQL(\n tree,\n file,\n \"`new HttpStage(this, 'DefaultStage', { $props })` where { $props <: contains `httpApi: this.api` as $httpApi, $httpApi => `httpApi: this.api,\\n domainMapping: defaultDomainMapping` }\",\n );\n // Computed once, for the stack output, runtime config and the `url` getter.\n await insertViaGritQL(\n tree,\n file,\n `expression_statement() as $statement where {\n $statement <: contains \\`this.defaultStage = new HttpStage($_, $_, $_)\\` as $assignment,\n $statement => \\`$assignment;\\n ${GRIT_INSERT_PLACEHOLDER}\\`\n}`,\n HTTP_API_URL_ASSIGNMENT,\n );\n await applyGritQL(\n tree,\n file,\n '`new CfnOutput(this, $_, { $props })` where { $props <: contains `value: this.defaultStage.url!` as $value, $value => `value: this.apiUrl` }',\n );\n await applyGritQL(\n tree,\n file,\n \"`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.defaultStage.url!` as $url, $url => `[apiName]: this.apiUrl` }\",\n );\n await applyGritQL(\n tree,\n file,\n 'method_definition(name=$name, body=$body) where { $name <: `url`, $body <: contains `return this.defaultStage.url` as $return, $return => `return this.apiUrl` }',\n );\n await insertViaGritQL(tree, file, AFTER_RUNTIME_CONFIG, HTTP_DOMAIN_OUTPUTS);\n};\n\nconst TERRAFORM_VARIABLES = `# Custom domain configuration\nvariable \"custom_domain_name\" {\n description = \"Custom domain name for the API. Requires acm_certificate_arn.\"\n type = string\n default = null\n}\n\nvariable \"acm_certificate_arn\" {\n description = \"ARN of an ACM certificate (in the API's region) for the custom domain name.\"\n type = string\n default = null\n}`;\n\n/** The differences between the REST and HTTP Terraform app modules. */\ninterface TerraformApi {\n kind: 'REST' | 'HTTP';\n /** A block only this kind of app module declares. */\n identifyingBlock: string;\n /** Matches exactly the runtime config `value` the app module was generated with. */\n runtimeConfigUrl: string;\n /** Types of the custom domain and mapping resources this migration adds. */\n domainResourceTypes: [string, string];\n domainResources: string;\n /** The `api_url` local, published in runtime config. */\n apiUrlLocal: string;\n outputs: string;\n}\n\nconst REST_TERRAFORM: TerraformApi = {\n kind: 'REST',\n identifyingBlock: '`resource \"aws_api_gateway_stage\" \"api_stage\" { $_ }`',\n runtimeConfigUrl:\n '$v <: r\"\\\\\"\\\\$\\\\{aws_api_gateway_stage\\\\.api_stage\\\\.invoke_url\\\\}/\\\\\"\"',\n domainResourceTypes: [\n 'aws_api_gateway_domain_name',\n 'aws_api_gateway_base_path_mapping',\n ],\n domainResources: `# Custom domain for the API, served without the stage path prefix\nresource \"aws_api_gateway_domain_name\" \"custom_domain\" {\n count = var.custom_domain_name == null ? 0 : 1\n\n domain_name = var.custom_domain_name\n regional_certificate_arn = var.acm_certificate_arn\n security_policy = \"TLS_1_2\"\n\n endpoint_configuration {\n types = [\"REGIONAL\"]\n }\n\n tags = var.tags\n\n lifecycle {\n precondition {\n condition = var.acm_certificate_arn != null\n error_message = \"acm_certificate_arn is required when custom_domain_name is set.\"\n }\n }\n}\n\nresource \"aws_api_gateway_base_path_mapping\" \"custom_domain\" {\n count = var.custom_domain_name == null ? 0 : 1\n\n api_id = module.rest_api.api_id\n stage_name = aws_api_gateway_stage.api_stage.stage_name\n domain_name = aws_api_gateway_domain_name.custom_domain[0].domain_name\n}`,\n apiUrlLocal: `locals {\n api_url = var.custom_domain_name == null ? \"\\${aws_api_gateway_stage.api_stage.invoke_url}/\" : \"https://\\${var.custom_domain_name}/\"\n}`,\n outputs: `output \"api_url\" {\n description = \"URL clients should call: the custom domain if configured, otherwise the stage invoke URL\"\n value = local.api_url\n}\n\noutput \"custom_domain_target_domain_name\" {\n description = \"Target domain name to point a DNS alias or CNAME record at for the custom domain\"\n value = var.custom_domain_name == null ? null : aws_api_gateway_domain_name.custom_domain[0].regional_domain_name\n}\n\noutput \"custom_domain_hosted_zone_id\" {\n description = \"Hosted zone ID to use for a Route 53 alias record for the custom domain\"\n value = var.custom_domain_name == null ? null : aws_api_gateway_domain_name.custom_domain[0].regional_zone_id\n}`,\n};\n\nconst HTTP_TERRAFORM: TerraformApi = {\n kind: 'HTTP',\n identifyingBlock: '`module \"http_api\" { $_ }`',\n runtimeConfigUrl: '$v <: `module.http_api.stage_invoke_url`',\n domainResourceTypes: [\n 'aws_apigatewayv2_domain_name',\n 'aws_apigatewayv2_api_mapping',\n ],\n domainResources: `# Custom domain for the API\nresource \"aws_apigatewayv2_domain_name\" \"custom_domain\" {\n count = var.custom_domain_name == null ? 0 : 1\n\n domain_name = var.custom_domain_name\n\n domain_name_configuration {\n certificate_arn = var.acm_certificate_arn\n endpoint_type = \"REGIONAL\"\n security_policy = \"TLS_1_2\"\n }\n\n tags = var.tags\n\n lifecycle {\n precondition {\n condition = var.acm_certificate_arn != null\n error_message = \"acm_certificate_arn is required when custom_domain_name is set.\"\n }\n }\n}\n\nresource \"aws_apigatewayv2_api_mapping\" \"custom_domain\" {\n count = var.custom_domain_name == null ? 0 : 1\n\n api_id = module.http_api.api_id\n stage = module.http_api.stage_id\n domain_name = aws_apigatewayv2_domain_name.custom_domain[0].domain_name\n}`,\n apiUrlLocal: `locals {\n api_url = var.custom_domain_name == null ? module.http_api.stage_invoke_url : \"https://\\${var.custom_domain_name}/\"\n}`,\n outputs: `output \"api_url\" {\n description = \"URL clients should call: the custom domain if configured, otherwise the stage invoke URL\"\n value = local.api_url\n}\n\noutput \"custom_domain_target_domain_name\" {\n description = \"Target domain name to point a DNS alias or CNAME record at for the custom domain\"\n value = var.custom_domain_name == null ? null : aws_apigatewayv2_domain_name.custom_domain[0].domain_name_configuration[0].target_domain_name\n}\n\noutput \"custom_domain_hosted_zone_id\" {\n description = \"Hosted zone ID to use for a Route 53 alias record for the custom domain\"\n value = var.custom_domain_name == null ? null : aws_apigatewayv2_domain_name.custom_domain[0].domain_name_configuration[0].hosted_zone_id\n}`,\n};\n\nconst RUNTIME_CONFIG_MODULE = '`module \"add_url_to_runtime_config\" { $body }`';\n\n/** The `locals` block every vended API app module declares its IAM policy statements in. */\nconst POLICY_LOCALS =\n '`locals { $body }` where { $body <: contains `lambda_policy_statements = $_` }';\n\n/** Rewrite matching the block's text back unchanged, then the placeholder after it. */\nconst appendAfterBlock = (header: string) =>\n hcl(\n `\\`${header} { $body }\\` => \\`${header} {\\n $body\\n}\\n\\n${GRIT_INSERT_PLACEHOLDER}\\``,\n );\n\nconst migrateTerraformApi = async (\n tree: Tree,\n moduleDir: string,\n file: string,\n nextSteps: string[],\n) => {\n const api = (await matchGritQL(\n tree,\n file,\n hcl(REST_TERRAFORM.identifyingBlock),\n ))\n ? REST_TERRAFORM\n : (await matchGritQL(tree, file, hcl(HTTP_TERRAFORM.identifyingBlock)))\n ? HTTP_TERRAFORM\n : undefined;\n if (!api) {\n return; // Not a vended API app module.\n }\n\n // Everything this migration declares, so a re-run recognises its own output\n // and nothing it inserts collides with an existing declaration. Terraform\n // merges every `.tf` file in the module directory, so all are checked.\n const [domainType, mappingType] = api.domainResourceTypes;\n const declarations = [\n '`variable \"custom_domain_name\" { $_ }`',\n '`variable \"acm_certificate_arn\" { $_ }`',\n `\\`resource \"${domainType}\" \"custom_domain\" { $_ }\\``,\n `\\`resource \"${mappingType}\" \"custom_domain\" { $_ }\\``,\n '`locals { $body }` where { $body <: contains `api_url = $_` }',\n '`output \"api_url\" { $_ }`',\n '`output \"custom_domain_target_domain_name\" { $_ }`',\n '`output \"custom_domain_hosted_zone_id\" { $_ }`',\n ];\n let declared = 0;\n for (const declaration of declarations) {\n if (await matchesInModule(tree, moduleDir, hcl(declaration))) {\n declared++;\n }\n }\n const publishesApiUrl = await matchGritQL(\n tree,\n file,\n hcl(\n `${RUNTIME_CONFIG_MODULE} where { $body <: contains \\`value = { $_ = local.api_url }\\` }`,\n ),\n );\n if (declared === declarations.length && publishesApiUrl) {\n return; // Already migrated.\n }\n if (declared > 0 || publishesApiUrl) {\n nextSteps.push(\n `${file}: this module (${moduleDir}) already declares some of the custom domain configuration this migration adds (custom_domain_name / acm_certificate_arn variables, the custom_domain resources, local.api_url, or the api_url / custom_domain_* outputs) - left untouched. Make sure the add_url_to_runtime_config module publishes the custom domain URL when one is configured, instead of the stage invoke URL (see the ${api.kind} API app module the plugin now generates).`,\n );\n return;\n }\n\n const runtimeConfigValue = `${RUNTIME_CONFIG_MODULE} where { $body <: contains \\`value = { $_ = $v }\\`, ${api.runtimeConfigUrl} }`;\n const shape = [\n hcl('`variable \"tags\" { $_ }`'),\n hcl(runtimeConfigValue),\n hcl(POLICY_LOCALS),\n hcl('`output \"stage_invoke_url\" { $_ }`'),\n ];\n if (!(await matchesAll(tree, file, shape))) {\n nextSteps.push(\n `${file}: has diverged from the generated shape - left untouched. To support a custom domain for this ${api.kind} API, add custom_domain_name and acm_certificate_arn variables, the custom domain and mapping resources, and publish the custom domain URL in the add_url_to_runtime_config module when it is set (see the ${api.kind} API app module the plugin now generates).`,\n );\n return;\n }\n\n await insertViaGritQL(\n tree,\n file,\n appendAfterBlock('variable \"tags\"'),\n TERRAFORM_VARIABLES,\n );\n // Only the URL expression is rewritten, preserving `terraform fmt` alignment.\n await applyGritQL(\n tree,\n file,\n hcl(\n `${RUNTIME_CONFIG_MODULE} where { $body <: contains \\`value = { $_ = $v }\\`, ${api.runtimeConfigUrl}, $v => \\`local.api_url\\` }`,\n ),\n );\n // Alongside the module's other locals.\n await insertViaGritQL(\n tree,\n file,\n hcl(\n `${POLICY_LOCALS} => \\`locals {\\n $body\\n}\\n\\n${GRIT_INSERT_PLACEHOLDER}\\``,\n ),\n api.apiUrlLocal,\n );\n await insertViaGritQL(\n tree,\n file,\n appendAfterBlock('module \"add_url_to_runtime_config\"'),\n api.domainResources,\n );\n await insertViaGritQL(\n tree,\n file,\n appendAfterBlock('output \"stage_invoke_url\"'),\n api.outputs,\n );\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n await migrateRestApiConstruct(tree, nextSteps);\n await migrateHttpApiConstruct(tree, nextSteps);\n\n if (tree.exists(TERRAFORM_APIS_DIR)) {\n for (const dirName of tree.children(TERRAFORM_APIS_DIR)) {\n const moduleDir = joinPathFragments(TERRAFORM_APIS_DIR, dirName);\n const file = joinPathFragments(moduleDir, `${dirName}.tf`);\n if (tree.exists(file)) {\n await migrateTerraformApi(tree, moduleDir, file, nextSteps);\n }\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["joinPathFragments","addDestructuredImport","applyGritQL","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","CONSTRUCTS_API_DIR","REST_API_CONSTRUCT","HTTP_API_CONSTRUCT","TERRAFORM_APIS_DIR","hcl","pattern","matchesInModule","tree","moduleDir","child","children","path","endsWith","isFile","matchesAll","file","patterns","REST_RUNTIME_CONFIG_URL","REST_DOMAIN_OUTPUTS","HTTP_DOMAIN_OUTPUTS","HTTP_DOMAIN_MAPPING_PROP","DOMAIN_ALIAS_OUTPUT","AFTER_RUNTIME_CONFIG","migrateRestApiConstruct","nextSteps","exists","shape","push","imported","HTTP_API_URL_FIELD","HTTP_API_URL_ASSIGNMENT","HTTP_URL_GETTER","migrateHttpApiConstruct","TERRAFORM_VARIABLES","REST_TERRAFORM","kind","identifyingBlock","runtimeConfigUrl","domainResourceTypes","domainResources","apiUrlLocal","outputs","HTTP_TERRAFORM","RUNTIME_CONFIG_MODULE","POLICY_LOCALS","appendAfterBlock","header","migrateTerraformApi","api","undefined","domainType","mappingType","declarations","declared","declaration","publishesApiUrl","length","runtimeConfigValue","migration","dirName"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { type MigrationReturnObject, type Tree } from '@nx/devkit';
6
+ export default function migration(tree: Tree): Promise<MigrationReturnObject>;
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/v1.0.3/0001-terraform-init-target/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type TargetConfiguration,\n type Tree,\n updateProjectConfiguration,\n} from '@nx/devkit';\nimport { relative } from 'path';\nimport { TERRAFORM_PROJECT_GENERATOR_INFO } from '../../../terraform/project/generator.js';\nimport { formatFilesInSubtree } from '../../../utils/format.js';\nimport { sortObjectKeys } from '../../../utils/object.js';\n\n/**\n * Give the shared Terraform provider plugin cache a single writer.\n *\n * `init`, `test` and `validate` each ran their own `terraform init` against one\n * per-project cache, and Nx schedules them concurrently — `plan` depends on both\n * `init` and `validate`. Two `terraform init` runs filling the cache at once each\n * compute a different hash for the same provider, because the hash covers a\n * directory the other is still writing, and terraform then rejects the mismatch\n * against the lock file: \"the cached package ... does not match any of the\n * checksums recorded in the dependency lock file\".\n *\n * The new `terraform-init` target runs that init once, and `test`, `validate` and\n * `init` depend on it — `test` and `validate` now run against the `TF_DATA_DIR`\n * it prepared rather than initialising their own.\n */\n\nconst TERRAFORM_INIT = 'terraform-init';\n\n/** `terraform init` as the pre-fix `test` and `validate` targets ran it. */\nconst BACKENDLESS_INIT = 'terraform init -backend=false';\n\nconst divergedStep = (projectName: string, targetName: string) =>\n `${projectName}: its '${targetName}' target no longer matches the shape the generator produced - left untouched. Point it at the \\`TF_DATA_DIR\\` of the new \\`${TERRAFORM_INIT}\\` target and add \\`\"dependsOn\": [\"${TERRAFORM_INIT}\"]\\`, so its \\`terraform init\\` no longer races the other targets over the provider cache.`;\n\n/** Path from a target running in `{projectRoot}/src` back to the workspace root. */\nconst rootRelativePath = (projectRoot: string) =>\n relative(joinPathFragments(projectRoot, 'src'), '.').replace(/\\\\/g, '/');\n\nconst pluginCacheDirFor = (toRoot: string) =>\n joinPathFragments(toRoot, '.terraform', 'plugin-cache', '{projectRoot}');\n\nconst dataDirFor = (toRoot: string, name: string) =>\n joinPathFragments(toRoot, 'dist', '{projectRoot}', name);\n\n/**\n * The target as the generator vends it: `-backend=false` needs no credentials,\n * so this runs on a fresh workspace before `bootstrap`, and its `TF_DATA_DIR`\n * keeps the `.terraform` it initialises out of `src`.\n */\nconst terraformInitTarget = (projectRoot: string): TargetConfiguration => {\n const toRoot = rootRelativePath(projectRoot);\n const pluginCacheDir = pluginCacheDirFor(toRoot);\n return {\n executor: 'nx:run-commands',\n options: {\n commands: [\n { command: `shx mkdir -p ${pluginCacheDir}`, forwardAllArgs: false },\n BACKENDLESS_INIT,\n ],\n forwardAllArgs: true,\n cwd: '{projectRoot}/src',\n parallel: false,\n env: {\n TF_DATA_DIR: dataDirFor(toRoot, TERRAFORM_INIT),\n TF_PLUGIN_CACHE_DIR: pluginCacheDir,\n },\n },\n };\n};\n\n/** Every command a target runs, across its options and its configurations. */\nconst commandsOf = (target: TargetConfiguration): string[] =>\n [target.options, ...Object.values(target.configurations ?? {})]\n .flatMap(\n (holder: { command?: unknown; commands?: unknown } | undefined) => [\n holder?.command,\n ...(Array.isArray(holder?.commands) ? holder.commands : []),\n ],\n )\n .map((command) =>\n typeof command === 'string'\n ? command\n : (command as { command?: unknown } | undefined)?.command,\n )\n .filter((command): command is string => typeof command === 'string');\n\n/**\n * Whether the target still runs the `terraform init` this migration orders. A\n * library's `init` runs it directly and an application's delegates to the vended\n * script.\n */\nconst runsTerraformInit = (target: TargetConfiguration): boolean =>\n commandsOf(target).some(\n (command) =>\n command.includes('terraform init') || command.includes('scripts/init.ts'),\n );\n\n/** Creating the plugin cache directory, which `terraform-init` now does. */\nconst isMakeCacheDir = (command: string) =>\n /^(shx mkdir -p|make-dir)\\s/.test(command) &&\n command.includes('plugin-cache');\n\n/**\n * Rewrites `test` or `validate` to run against the `TF_DATA_DIR` `terraform-init`\n * prepared. Only the exact shape the generator produced is rewritten: its own\n * `terraform init` (with the cache directory the plugin-cache fix added ahead of\n * it), then the one terraform command the target exists to run. Anything else is\n * the user's, and is reported instead.\n */\nconst migrateInitlessTarget = (\n target: TargetConfiguration,\n verb: 'test' | 'validate',\n toRoot: string,\n): boolean => {\n const commands = commandsOf(target).filter(\n (command) => !isMakeCacheDir(command),\n );\n if (\n commands.length !== 2 ||\n commands[0] !== BACKENDLESS_INIT ||\n commands[1] !== `terraform ${verb}`\n ) {\n return false;\n }\n const options = target.options ?? {};\n const env: Record<string, string> = options.env ?? {};\n if (env.TF_DATA_DIR !== dataDirFor(toRoot, `terraform-${verb}`)) {\n return false;\n }\n\n const {\n TF_DATA_DIR: _dataDir,\n TF_PLUGIN_CACHE_DIR: _cacheDir,\n ...rest\n } = env;\n delete options.commands;\n // `parallel: false` sequenced the init ahead of the command; one command left.\n delete options.parallel;\n target.options = {\n ...options,\n command: `terraform ${verb}`,\n env: { ...rest, TF_DATA_DIR: dataDirFor(toRoot, TERRAFORM_INIT) },\n };\n return true;\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const [projectName, project] of getProjects(tree)) {\n const generator = (project.metadata as { generator?: string } | undefined)\n ?.generator;\n if (generator !== TERRAFORM_PROJECT_GENERATOR_INFO.id) continue;\n\n let targets = project.targets ?? {};\n const toRoot = rootRelativePath(project.root);\n let changed = false;\n\n // Guarded on the target being absent, so a re-run — and a project generated\n // with it — is a no-op.\n if (!targets[TERRAFORM_INIT]) {\n targets = sortObjectKeys({\n ...targets,\n [TERRAFORM_INIT]: terraformInitTarget(project.root),\n });\n changed = true;\n }\n\n for (const targetName of ['init', 'test', 'validate'] as const) {\n const target = targets[targetName];\n if (!target) continue;\n if (target.dependsOn?.includes(TERRAFORM_INIT)) continue;\n\n // `init` keeps its own backend-configured `terraform init`; `test` and\n // `validate` hand theirs over to `terraform-init`, so nothing is ordered\n // after a target whose shape is no longer recognised.\n const migrated =\n targetName === 'init'\n ? runsTerraformInit(target)\n : migrateInitlessTarget(target, targetName, toRoot);\n if (!migrated) {\n nextSteps.push(divergedStep(projectName, targetName));\n continue;\n }\n\n // Prepended rather than appended, so a migrated workspace matches a\n // freshly generated one.\n target.dependsOn = [TERRAFORM_INIT, ...(target.dependsOn ?? [])];\n changed = true;\n }\n\n if (changed) {\n updateProjectConfiguration(tree, projectName, { ...project, targets });\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","updateProjectConfiguration","relative","TERRAFORM_PROJECT_GENERATOR_INFO","formatFilesInSubtree","sortObjectKeys","TERRAFORM_INIT","BACKENDLESS_INIT","divergedStep","projectName","targetName","rootRelativePath","projectRoot","replace","pluginCacheDirFor","toRoot","dataDirFor","name","terraformInitTarget","pluginCacheDir","executor","options","commands","command","forwardAllArgs","cwd","parallel","env","TF_DATA_DIR","TF_PLUGIN_CACHE_DIR","commandsOf","target","Object","values","configurations","flatMap","holder","Array","isArray","map","filter","runsTerraformInit","some","includes","isMakeCacheDir","test","migrateInitlessTarget","verb","length","_dataDir","_cacheDir","rest","migration","tree","nextSteps","project","generator","metadata","id","targets","root","changed","dependsOn","migrated","push"],"mappings":"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"}
@@ -766,12 +766,12 @@ dependencies = [
766
766
  "aws-lambda-powertools==3.35.0",
767
767
  "aws-opentelemetry-distro==0.20.0",
768
768
  "bedrock-agentcore==1.23.1",
769
- "boto3==1.43.98",
769
+ "boto3==1.43.103",
770
770
  "fastapi==0.141.1",
771
771
  "mcp==1.28.1",
772
- "strands-agents==1.56.0",
772
+ "strands-agents==1.57.1",
773
773
  "strands-agents-tools==0.8.9",
774
- "uvicorn==0.53.0"
774
+ "uvicorn==0.54.0"
775
775
  ]
776
776
 
777
777
  [dependency-groups]
@@ -704,17 +704,17 @@ dependencies = [
704
704
  "aws-lambda-powertools==3.35.0",
705
705
  "aws-opentelemetry-distro==0.20.0",
706
706
  "bedrock-agentcore==1.23.1",
707
- "boto3==1.43.98",
707
+ "boto3==1.43.103",
708
708
  "fastapi==0.141.1",
709
709
  "mcp==1.28.1",
710
710
  "langchain==1.4.2",
711
- "langchain-aws==1.7.8",
711
+ "langchain-aws==1.7.9",
712
712
  "langgraph==1.2.12",
713
713
  "langgraph-checkpoint-sqlite==3.1.1",
714
714
  "aiosqlite==0.22.1",
715
715
  "ag-ui-protocol==0.1.22",
716
716
  "ag-ui-langgraph==0.0.45",
717
- "uvicorn==0.53.0"
717
+ "uvicorn==0.54.0"
718
718
  ]
719
719
 
720
720
  [dependency-groups]
@@ -1063,7 +1063,7 @@ resource "null_resource" "runtime_ready" {
1063
1063
 
1064
1064
  provisioner "local-exec" {
1065
1065
  command = <<-EOT
1066
- uv run --with boto3==1.43.98 python -c "
1066
+ uv run --with boto3==1.43.103 python -c "
1067
1067
  import boto3
1068
1068
  import json
1069
1069
  import os
@@ -245,10 +245,12 @@ import {
245
245
  } from './utils.js';
246
246
  import { CfnOutput, Stack } from 'aws-cdk-lib';
247
247
  import {
248
+ DomainMappingOptions,
248
249
  HttpApi as _HttpApi,
249
250
  HttpApiProps as _HttpApiProps,
250
251
  HttpMethod,
251
252
  HttpStage,
253
+ IDomainName,
252
254
  LogGroupLogDestination,
253
255
  ThrottleSettings,
254
256
  } from 'aws-cdk-lib/aws-apigatewayv2';
@@ -266,7 +268,7 @@ import { suppressRules } from '../checkov.js';
266
268
  export interface HttpApiProps<
267
269
  TIntegrations extends ApiIntegrations<TOperation, HttpApiIntegration>,
268
270
  TOperation extends string,
269
- > extends _HttpApiProps {
271
+ > extends Omit<_HttpApiProps, 'defaultDomainMapping'> {
270
272
  /**
271
273
  * Unique name for the API, used in runtime configuration
272
274
  */
@@ -285,6 +287,12 @@ export interface HttpApiProps<
285
287
  * @default { rateLimit: 10000, burstLimit: 5000 }
286
288
  */
287
289
  readonly throttle?: ThrottleSettings;
290
+ /**
291
+ * Custom domain mapped to the API's default stage
292
+ */
293
+ readonly defaultDomainMapping?: DomainMappingOptions & {
294
+ readonly domainName: IDomainName;
295
+ };
288
296
  }
289
297
 
290
298
  /**
@@ -308,6 +316,9 @@ export class HttpApi<
308
316
  /** Default auto-deployed stage */
309
317
  public readonly defaultStage: HttpStage;
310
318
 
319
+ /** The URL clients call: the custom domain's if one is mapped, otherwise the stage's */
320
+ private readonly apiUrl: string;
321
+
311
322
  /** Map of operation names to their API Gateway integrations */
312
323
  public readonly integrations: TIntegrations;
313
324
 
@@ -319,6 +330,7 @@ export class HttpApi<
319
330
  operations,
320
331
  integrations,
321
332
  throttle = { rateLimit: 10000, burstLimit: 5000 },
333
+ defaultDomainMapping,
322
334
  ...props
323
335
  }: HttpApiProps<TIntegrations, TOperation>,
324
336
  ) {
@@ -351,12 +363,16 @@ export class HttpApi<
351
363
 
352
364
  this.defaultStage = new HttpStage(this, 'DefaultStage', {
353
365
  httpApi: this.api,
366
+ domainMapping: defaultDomainMapping,
354
367
  autoDeploy: true,
355
368
  throttle,
356
369
  accessLogSettings: {
357
370
  destination: new LogGroupLogDestination(accessLogGroup),
358
371
  },
359
372
  });
373
+ this.apiUrl = defaultDomainMapping
374
+ ? this.defaultStage.domainUrl
375
+ : this.defaultStage.url!;
360
376
 
361
377
  // Resolve the integration for a given operation.
362
378
  // If the operation has a dedicated integration, use it; otherwise fall back to $router.
@@ -389,22 +405,36 @@ export class HttpApi<
389
405
  );
390
406
 
391
407
  new CfnOutput(this, \`\${apiName}Url\`, {
392
- value: this.defaultStage.url!,
408
+ value: this.apiUrl,
393
409
  });
394
410
 
395
411
  // Register the API URL in runtime configuration for client discovery
396
412
  const rc = RuntimeConfig.ensure(this);
397
413
  rc.set('connection', 'apis', {
398
414
  ...rc.get('connection').apis,
399
- [apiName]: this.defaultStage.url!,
415
+ [apiName]: this.apiUrl,
400
416
  });
417
+
418
+ // Target for the custom domain's DNS record
419
+ if (defaultDomainMapping) {
420
+ new CfnOutput(this, \`\${apiName}DomainNameAlias\`, {
421
+ value: defaultDomainMapping.domainName.regionalDomainName,
422
+ description:
423
+ 'Regional domain name to point a CNAME record at for the custom domain, with any DNS provider',
424
+ });
425
+ new CfnOutput(this, \`\${apiName}DomainNameAliasHostedZoneId\`, {
426
+ value: defaultDomainMapping.domainName.regionalHostedZoneId,
427
+ description:
428
+ 'Hosted zone ID of the regional domain name, needed only for a Route 53 alias record',
429
+ });
430
+ }
401
431
  }
402
432
 
403
433
  /**
404
434
  * Return the API url
405
435
  */
406
436
  public get url() {
407
- return this.defaultStage.url;
437
+ return this.apiUrl;
408
438
  }
409
439
  }
410
440
  "
@@ -1085,7 +1115,7 @@ import {
1085
1115
  Stage,
1086
1116
  ThrottleSettings,
1087
1117
  } from 'aws-cdk-lib/aws-apigateway';
1088
- import { IAspect, RemovalPolicy, Stack } from 'aws-cdk-lib';
1118
+ import { CfnOutput, IAspect, RemovalPolicy, Stack } from 'aws-cdk-lib';
1089
1119
  import {
1090
1120
  CfnLoggingConfiguration,
1091
1121
  CfnWebACL,
@@ -1174,6 +1204,7 @@ export class RestApi<
1174
1204
  integrations,
1175
1205
  enableWaf = true,
1176
1206
  throttle = { rateLimit: 10000, burstLimit: 5000 },
1207
+ domainName,
1177
1208
  ...props
1178
1209
  }: RestApiProps<TIntegrations, TOperation>,
1179
1210
  ) {
@@ -1199,6 +1230,7 @@ export class RestApi<
1199
1230
  // Create the API Gateway REST API
1200
1231
  this.api = new _RestApi(this, 'Api', {
1201
1232
  ...props,
1233
+ domainName,
1202
1234
  deployOptions: {
1203
1235
  accessLogDestination: new LogGroupLogDestination(accessLogs),
1204
1236
  accessLogFormat: AccessLogFormat.jsonWithStandardFields(),
@@ -1340,8 +1372,24 @@ export class RestApi<
1340
1372
  const rc = RuntimeConfig.ensure(this);
1341
1373
  rc.set('connection', 'apis', {
1342
1374
  ...rc.get('connection').apis,
1343
- [apiName]: this.api.url!,
1375
+ [apiName]: domainName
1376
+ ? \`https://\${domainName.domainName}/\${domainName.basePath ? \`\${domainName.basePath}/\` : ''}\`
1377
+ : this.api.url!,
1344
1378
  });
1379
+
1380
+ // Target for the custom domain's DNS record
1381
+ if (domainName) {
1382
+ new CfnOutput(this, \`\${apiName}DomainNameAlias\`, {
1383
+ value: this.api.domainName!.domainNameAliasDomainName,
1384
+ description:
1385
+ 'Domain name of the API Gateway endpoint (regional or edge) to point a CNAME record at for the custom domain, with any DNS provider',
1386
+ });
1387
+ new CfnOutput(this, \`\${apiName}DomainNameAliasHostedZoneId\`, {
1388
+ value: this.api.domainName!.domainNameAliasHostedZoneId,
1389
+ description:
1390
+ 'Hosted zone ID of that domain name, needed only for a Route 53 alias record',
1391
+ });
1392
+ }
1345
1393
  }
1346
1394
 
1347
1395
  /**