@aws/nx-plugin 1.0.4 → 1.0.6

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 (38) hide show
  1. package/LICENSE-THIRD-PARTY +70 -92
  2. package/migrations.json +6 -1
  3. package/package.json +6 -6
  4. package/src/agentcore-gateway/__snapshots__/generator.spec.ts.snap +1 -1
  5. package/src/infra/app/__snapshots__/generator.spec.ts.snap +3 -3
  6. package/src/migrations/v1.0.5/0001-api-custom-domain-runtime-config/metadata.json +3 -0
  7. package/src/migrations/v1.0.5/0001-api-custom-domain-runtime-config/migration.d.ts +6 -0
  8. package/src/migrations/v1.0.5/0001-api-custom-domain-runtime-config/migration.js +362 -0
  9. package/src/migrations/v1.0.5/0001-api-custom-domain-runtime-config/migration.js.map +1 -0
  10. package/src/preset/__snapshots__/generator.spec.ts.snap +2 -2
  11. package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +6 -6
  12. package/src/py/agent/__snapshots__/generator.frameworks.spec.ts.snap +8 -8
  13. package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +4 -4
  14. package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
  15. package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +54 -6
  16. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +390 -18
  17. package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
  18. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +4 -4
  19. package/src/py/rdb/__snapshots__/generator.spec.ts.snap +2 -2
  20. package/src/smithy/project/__snapshots__/generator.spec.ts.snap +5 -5
  21. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +260 -12
  22. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +444 -24
  23. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +4 -4
  24. package/src/ts/dcr-proxy/__snapshots__/generator.spec.ts.snap +1 -1
  25. package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
  26. package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
  27. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +2 -2
  28. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +6 -6
  29. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +21 -21
  30. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +2 -2
  31. package/src/utils/api-constructs/files/cdk/core/api/http/http-api.ts.template +34 -4
  32. package/src/utils/api-constructs/files/cdk/core/api/rest/rest-api.ts.template +20 -2
  33. package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +63 -1
  34. package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +63 -1
  35. package/src/utils/docker.d.ts +1 -1
  36. package/src/utils/versions.d.ts +56 -56
  37. package/src/utils/versions.js +55 -55
  38. package/src/utils/versions.js.map +1 -1
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { joinPathFragments } from "@nx/devkit";
5
+ import { addDestructuredImport, applyGritQL, GRIT_INSERT_PLACEHOLDER, insertViaGritQL, matchGritQL } from "../../../utils/ast.js";
6
+ import { formatFilesInSubtree } from "../../../utils/format.js";
7
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../../../utils/shared-constructs-constants.js";
8
+ /**
9
+ * Publish an API's custom domain, when one is configured, as its URL in runtime
10
+ * config, so clients call it rather than the generated execute-api endpoint.
11
+ *
12
+ * - The vended CDK `RestApi` publishes `https://<domainName>/[<basePath>/]` when
13
+ * `domainName` is set.
14
+ * - The vended CDK `HttpApi` forwards `defaultDomainMapping` to the stage it
15
+ * creates (previously rejected by CDK, since the construct disables the
16
+ * default stage) and publishes the mapping's domain URL.
17
+ * - Each vended Terraform API app module (`app/apis/<name>/<name>.tf`) gains
18
+ * `custom_domain_name` / `acm_certificate_arn` variables, the domain and
19
+ * mapping resources, an `api_url` local published in runtime config, and
20
+ * outputs for the URL and DNS record targets.
21
+ *
22
+ * These files are generated with `KeepExisting`, so an upgraded workspace keeps
23
+ * the old shape until this runs. Diverged files are left untouched and
24
+ * reported via `nextSteps`.
25
+ */ const CONSTRUCTS_API_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/api`;
26
+ const REST_API_CONSTRUCT = `${CONSTRUCTS_API_DIR}/rest-api.ts`;
27
+ const HTTP_API_CONSTRUCT = `${CONSTRUCTS_API_DIR}/http-api.ts`;
28
+ const TERRAFORM_APIS_DIR = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/app/apis`;
29
+ const hcl = (pattern)=>`language hcl\n${pattern}`;
30
+ /** Whether the pattern matches any `.tf` file in the Terraform module directory. */ const matchesInModule = async (tree, moduleDir, pattern)=>{
31
+ for (const child of tree.children(moduleDir)){
32
+ const path = joinPathFragments(moduleDir, child);
33
+ if (child.endsWith('.tf') && tree.isFile(path) && await matchGritQL(tree, path, pattern)) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ };
39
+ /** Whether every pattern matches the file. */ const matchesAll = async (tree, file, patterns)=>{
40
+ for (const pattern of patterns){
41
+ if (!await matchGritQL(tree, file, pattern)) {
42
+ return false;
43
+ }
44
+ }
45
+ return true;
46
+ };
47
+ const REST_RUNTIME_CONFIG_URL = `domainName
48
+ ? \`https://\${domainName.domainName}/\${domainName.basePath ? \`\${domainName.basePath}/\` : ''}\`
49
+ : this.api.url!`;
50
+ const REST_DOMAIN_OUTPUTS = `// Target for the custom domain's DNS record
51
+ if (domainName) {
52
+ new CfnOutput(this, \`\${apiName}DomainNameAlias\`, {
53
+ value: this.api.domainName!.domainNameAliasDomainName,
54
+ description:
55
+ 'Domain name of the API Gateway endpoint (regional or edge) to point a CNAME record at for the custom domain, with any DNS provider',
56
+ });
57
+ new CfnOutput(this, \`\${apiName}DomainNameAliasHostedZoneId\`, {
58
+ value: this.api.domainName!.domainNameAliasHostedZoneId,
59
+ description:
60
+ 'Hosted zone ID of that domain name, needed only for a Route 53 alias record',
61
+ });
62
+ }`;
63
+ const HTTP_DOMAIN_OUTPUTS = `// Target for the custom domain's DNS record
64
+ if (defaultDomainMapping) {
65
+ new CfnOutput(this, \`\${apiName}DomainNameAlias\`, {
66
+ value: defaultDomainMapping.domainName.regionalDomainName,
67
+ description:
68
+ 'Regional domain name to point a CNAME record at for the custom domain, with any DNS provider',
69
+ });
70
+ new CfnOutput(this, \`\${apiName}DomainNameAliasHostedZoneId\`, {
71
+ value: defaultDomainMapping.domainName.regionalHostedZoneId,
72
+ description:
73
+ 'Hosted zone ID of the regional domain name, needed only for a Route 53 alias record',
74
+ });
75
+ }`;
76
+ const HTTP_DOMAIN_MAPPING_PROP = `/**
77
+ * Custom domain mapped to the API's default stage
78
+ */
79
+ readonly defaultDomainMapping?: DomainMappingOptions & {
80
+ readonly domainName: IDomainName;
81
+ }`;
82
+ /** Matches a DNS record output this migration adds. */ const DOMAIN_ALIAS_OUTPUT = '`new CfnOutput(this, $id, $_)` where { $id <: r".*DomainNameAlias.*" }';
83
+ /** Rewrite inserting {@link GRIT_INSERT_PLACEHOLDER} after the runtime config registration. */ const AFTER_RUNTIME_CONFIG = `\`$call;\` as $statement where {
84
+ $call <: \`rc.set('connection', 'apis', $_)\`,
85
+ $statement => \`$call;\n\n ${GRIT_INSERT_PLACEHOLDER}\`
86
+ }`;
87
+ const migrateRestApiConstruct = async (tree, nextSteps)=>{
88
+ const file = REST_API_CONSTRUCT;
89
+ if (!tree.exists(file)) {
90
+ return;
91
+ }
92
+ if (await matchGritQL(tree, file, '`[apiName]: domainName ? $_ : $_`')) {
93
+ return; // Already migrated.
94
+ }
95
+ const shape = [
96
+ '`{ $params }: RestApiProps<$_, $_>` where { $params <: contains `...props`, $params <: not contains `domainName` }',
97
+ "`new _RestApi(this, 'Api', { $props })` where { $props <: contains `...props`, $props <: not contains `domainName` }",
98
+ "`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.api.url!` }"
99
+ ];
100
+ if (!await matchesAll(tree, file, shape) || await matchGritQL(tree, file, DOMAIN_ALIAS_OUTPUT)) {
101
+ nextSteps.push(`${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.`);
102
+ return;
103
+ }
104
+ // Taken out of `props` and passed explicitly, so it is in scope below.
105
+ await applyGritQL(tree, file, '`{ $params }: RestApiProps<$_, $_>` where { $params <: contains `...props` as $rest, $rest => `domainName,\n ...props` }');
106
+ await applyGritQL(tree, file, "`new _RestApi(this, 'Api', { $props })` where { $props <: contains `...props` as $spread, $spread => `...props,\n domainName` }");
107
+ await insertViaGritQL(tree, file, "`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.api.url!` as $url, $url => `[apiName]: __GRIT_INSERT_PLACEHOLDER__` }", REST_RUNTIME_CONFIG_URL);
108
+ await insertViaGritQL(tree, file, AFTER_RUNTIME_CONFIG, REST_DOMAIN_OUTPUTS);
109
+ // Listed first, as the generator vends it.
110
+ const imported = await applyGritQL(tree, file, "`import { $names } from 'aws-cdk-lib'` where { $names <: not contains `CfnOutput` } => `import { CfnOutput, $names } from 'aws-cdk-lib'`");
111
+ if (!imported) {
112
+ await addDestructuredImport(tree, file, [
113
+ 'CfnOutput'
114
+ ], 'aws-cdk-lib');
115
+ }
116
+ };
117
+ const HTTP_API_URL_FIELD = `/** The URL clients call: the custom domain's if one is mapped, otherwise the stage's */
118
+ private readonly apiUrl: string`;
119
+ const HTTP_API_URL_ASSIGNMENT = `this.apiUrl = defaultDomainMapping
120
+ ? this.defaultStage.domainUrl
121
+ : this.defaultStage.url!;`;
122
+ /** Matches the vended `url` getter, returning the stage URL. */ const HTTP_URL_GETTER = 'method_definition(name=$name, body=$body) where { $name <: `url`, $body <: contains `return this.defaultStage.url` }';
123
+ const migrateHttpApiConstruct = async (tree, nextSteps)=>{
124
+ const file = HTTP_API_CONSTRUCT;
125
+ if (!tree.exists(file)) {
126
+ return;
127
+ }
128
+ if (await matchGritQL(tree, file, '`[apiName]: this.apiUrl`')) {
129
+ return; // Already migrated.
130
+ }
131
+ const shape = [
132
+ '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` }',
133
+ "`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` }",
134
+ '`public readonly defaultStage: HttpStage`',
135
+ '`{ $params }: HttpApiProps<$_, $_>` where { $params <: contains `...props`, $params <: not contains `defaultDomainMapping` }',
136
+ "`this.defaultStage = new HttpStage(this, 'DefaultStage', { $props })` where { $props <: contains `httpApi: this.api`, $props <: not contains `domainMapping` }",
137
+ '`new CfnOutput(this, $_, { $props })` where { $props <: contains `value: this.defaultStage.url!` }',
138
+ "`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.defaultStage.url!` }",
139
+ HTTP_URL_GETTER
140
+ ];
141
+ if (!await matchesAll(tree, file, shape) || await matchGritQL(tree, file, DOMAIN_ALIAS_OUTPUT) || await matchGritQL(tree, file, '`this.apiUrl`')) {
142
+ nextSteps.push(`${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.`);
143
+ return;
144
+ }
145
+ // Typed as a domain exposing its regional attributes, for the DNS outputs.
146
+ await insertViaGritQL(tree, file, 'interface_declaration(name=$name, body=$body) where { $name <: `HttpApiProps`, $body <: contains `readonly throttle?: ThrottleSettings` as $throttle, $throttle => `readonly throttle?: ThrottleSettings;\n __GRIT_INSERT_PLACEHOLDER__` }', HTTP_DOMAIN_MAPPING_PROP);
147
+ // Only the `_HttpApiProps` base is replaced, keeping any others.
148
+ await applyGritQL(tree, file, "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'>` }");
149
+ // Inserted in sorted position, as the generator vends them.
150
+ await applyGritQL(tree, file, "`import { $names } from 'aws-cdk-lib/aws-apigatewayv2'` where { $names <: contains `HttpApi as _HttpApi` as $httpApi, $httpApi => `DomainMappingOptions,\n HttpApi as _HttpApi` }");
151
+ await applyGritQL(tree, file, "`import { $names } from 'aws-cdk-lib/aws-apigatewayv2'` where { $names <: contains `HttpStage` as $stage, $stage => `HttpStage,\n IDomainName` }");
152
+ await insertViaGritQL(tree, file, '`public readonly defaultStage: HttpStage` as $field => `public readonly defaultStage: HttpStage;\n\n __GRIT_INSERT_PLACEHOLDER__`', HTTP_API_URL_FIELD);
153
+ // Taken out of `props` so it isn't passed to the CDK HttpApi.
154
+ await applyGritQL(tree, file, '`{ $params }: HttpApiProps<$_, $_>` where { $params <: contains `...props` as $rest, $rest => `defaultDomainMapping,\n ...props` }');
155
+ await applyGritQL(tree, file, "`new HttpStage(this, 'DefaultStage', { $props })` where { $props <: contains `httpApi: this.api` as $httpApi, $httpApi => `httpApi: this.api,\n domainMapping: defaultDomainMapping` }");
156
+ // Computed once, for the stack output, runtime config and the `url` getter.
157
+ await insertViaGritQL(tree, file, `expression_statement() as $statement where {
158
+ $statement <: contains \`this.defaultStage = new HttpStage($_, $_, $_)\` as $assignment,
159
+ $statement => \`$assignment;\n ${GRIT_INSERT_PLACEHOLDER}\`
160
+ }`, HTTP_API_URL_ASSIGNMENT);
161
+ await applyGritQL(tree, file, '`new CfnOutput(this, $_, { $props })` where { $props <: contains `value: this.defaultStage.url!` as $value, $value => `value: this.apiUrl` }');
162
+ await applyGritQL(tree, file, "`rc.set('connection', 'apis', { $entries })` where { $entries <: contains `[apiName]: this.defaultStage.url!` as $url, $url => `[apiName]: this.apiUrl` }");
163
+ await applyGritQL(tree, file, 'method_definition(name=$name, body=$body) where { $name <: `url`, $body <: contains `return this.defaultStage.url` as $return, $return => `return this.apiUrl` }');
164
+ await insertViaGritQL(tree, file, AFTER_RUNTIME_CONFIG, HTTP_DOMAIN_OUTPUTS);
165
+ };
166
+ const TERRAFORM_VARIABLES = `# Custom domain configuration
167
+ variable "custom_domain_name" {
168
+ description = "Custom domain name for the API. Requires acm_certificate_arn."
169
+ type = string
170
+ default = null
171
+ }
172
+
173
+ variable "acm_certificate_arn" {
174
+ description = "ARN of an ACM certificate (in the API's region) for the custom domain name."
175
+ type = string
176
+ default = null
177
+ }`;
178
+ const REST_TERRAFORM = {
179
+ kind: 'REST',
180
+ identifyingBlock: '`resource "aws_api_gateway_stage" "api_stage" { $_ }`',
181
+ runtimeConfigUrl: '$v <: r"\\"\\$\\{aws_api_gateway_stage\\.api_stage\\.invoke_url\\}/\\""',
182
+ domainResourceTypes: [
183
+ 'aws_api_gateway_domain_name',
184
+ 'aws_api_gateway_base_path_mapping'
185
+ ],
186
+ domainResources: `# Custom domain for the API, served without the stage path prefix
187
+ resource "aws_api_gateway_domain_name" "custom_domain" {
188
+ count = var.custom_domain_name == null ? 0 : 1
189
+
190
+ domain_name = var.custom_domain_name
191
+ regional_certificate_arn = var.acm_certificate_arn
192
+ security_policy = "TLS_1_2"
193
+
194
+ endpoint_configuration {
195
+ types = ["REGIONAL"]
196
+ }
197
+
198
+ tags = var.tags
199
+
200
+ lifecycle {
201
+ precondition {
202
+ condition = var.acm_certificate_arn != null
203
+ error_message = "acm_certificate_arn is required when custom_domain_name is set."
204
+ }
205
+ }
206
+ }
207
+
208
+ resource "aws_api_gateway_base_path_mapping" "custom_domain" {
209
+ count = var.custom_domain_name == null ? 0 : 1
210
+
211
+ api_id = module.rest_api.api_id
212
+ stage_name = aws_api_gateway_stage.api_stage.stage_name
213
+ domain_name = aws_api_gateway_domain_name.custom_domain[0].domain_name
214
+ }`,
215
+ apiUrlLocal: `locals {
216
+ api_url = var.custom_domain_name == null ? "\${aws_api_gateway_stage.api_stage.invoke_url}/" : "https://\${var.custom_domain_name}/"
217
+ }`,
218
+ outputs: `output "api_url" {
219
+ description = "URL clients should call: the custom domain if configured, otherwise the stage invoke URL"
220
+ value = local.api_url
221
+ }
222
+
223
+ output "custom_domain_target_domain_name" {
224
+ description = "Target domain name to point a DNS alias or CNAME record at for the custom domain"
225
+ value = var.custom_domain_name == null ? null : aws_api_gateway_domain_name.custom_domain[0].regional_domain_name
226
+ }
227
+
228
+ output "custom_domain_hosted_zone_id" {
229
+ description = "Hosted zone ID to use for a Route 53 alias record for the custom domain"
230
+ value = var.custom_domain_name == null ? null : aws_api_gateway_domain_name.custom_domain[0].regional_zone_id
231
+ }`
232
+ };
233
+ const HTTP_TERRAFORM = {
234
+ kind: 'HTTP',
235
+ identifyingBlock: '`module "http_api" { $_ }`',
236
+ runtimeConfigUrl: '$v <: `module.http_api.stage_invoke_url`',
237
+ domainResourceTypes: [
238
+ 'aws_apigatewayv2_domain_name',
239
+ 'aws_apigatewayv2_api_mapping'
240
+ ],
241
+ domainResources: `# Custom domain for the API
242
+ resource "aws_apigatewayv2_domain_name" "custom_domain" {
243
+ count = var.custom_domain_name == null ? 0 : 1
244
+
245
+ domain_name = var.custom_domain_name
246
+
247
+ domain_name_configuration {
248
+ certificate_arn = var.acm_certificate_arn
249
+ endpoint_type = "REGIONAL"
250
+ security_policy = "TLS_1_2"
251
+ }
252
+
253
+ tags = var.tags
254
+
255
+ lifecycle {
256
+ precondition {
257
+ condition = var.acm_certificate_arn != null
258
+ error_message = "acm_certificate_arn is required when custom_domain_name is set."
259
+ }
260
+ }
261
+ }
262
+
263
+ resource "aws_apigatewayv2_api_mapping" "custom_domain" {
264
+ count = var.custom_domain_name == null ? 0 : 1
265
+
266
+ api_id = module.http_api.api_id
267
+ stage = module.http_api.stage_id
268
+ domain_name = aws_apigatewayv2_domain_name.custom_domain[0].domain_name
269
+ }`,
270
+ apiUrlLocal: `locals {
271
+ api_url = var.custom_domain_name == null ? module.http_api.stage_invoke_url : "https://\${var.custom_domain_name}/"
272
+ }`,
273
+ outputs: `output "api_url" {
274
+ description = "URL clients should call: the custom domain if configured, otherwise the stage invoke URL"
275
+ value = local.api_url
276
+ }
277
+
278
+ output "custom_domain_target_domain_name" {
279
+ description = "Target domain name to point a DNS alias or CNAME record at for the custom domain"
280
+ value = var.custom_domain_name == null ? null : aws_apigatewayv2_domain_name.custom_domain[0].domain_name_configuration[0].target_domain_name
281
+ }
282
+
283
+ output "custom_domain_hosted_zone_id" {
284
+ description = "Hosted zone ID to use for a Route 53 alias record for the custom domain"
285
+ value = var.custom_domain_name == null ? null : aws_apigatewayv2_domain_name.custom_domain[0].domain_name_configuration[0].hosted_zone_id
286
+ }`
287
+ };
288
+ const RUNTIME_CONFIG_MODULE = '`module "add_url_to_runtime_config" { $body }`';
289
+ /** The `locals` block every vended API app module declares its IAM policy statements in. */ const POLICY_LOCALS = '`locals { $body }` where { $body <: contains `lambda_policy_statements = $_` }';
290
+ /** Rewrite matching the block's text back unchanged, then the placeholder after it. */ const appendAfterBlock = (header)=>hcl(`\`${header} { $body }\` => \`${header} {\n $body\n}\n\n${GRIT_INSERT_PLACEHOLDER}\``);
291
+ const migrateTerraformApi = async (tree, moduleDir, file, nextSteps)=>{
292
+ const api = await matchGritQL(tree, file, hcl(REST_TERRAFORM.identifyingBlock)) ? REST_TERRAFORM : await matchGritQL(tree, file, hcl(HTTP_TERRAFORM.identifyingBlock)) ? HTTP_TERRAFORM : undefined;
293
+ if (!api) {
294
+ return; // Not a vended API app module.
295
+ }
296
+ // Everything this migration declares, so a re-run recognises its own output
297
+ // and nothing it inserts collides with an existing declaration. Terraform
298
+ // merges every `.tf` file in the module directory, so all are checked.
299
+ const [domainType, mappingType] = api.domainResourceTypes;
300
+ const declarations = [
301
+ '`variable "custom_domain_name" { $_ }`',
302
+ '`variable "acm_certificate_arn" { $_ }`',
303
+ `\`resource "${domainType}" "custom_domain" { $_ }\``,
304
+ `\`resource "${mappingType}" "custom_domain" { $_ }\``,
305
+ '`locals { $body }` where { $body <: contains `api_url = $_` }',
306
+ '`output "api_url" { $_ }`',
307
+ '`output "custom_domain_target_domain_name" { $_ }`',
308
+ '`output "custom_domain_hosted_zone_id" { $_ }`'
309
+ ];
310
+ let declared = 0;
311
+ for (const declaration of declarations){
312
+ if (await matchesInModule(tree, moduleDir, hcl(declaration))) {
313
+ declared++;
314
+ }
315
+ }
316
+ const publishesApiUrl = await matchGritQL(tree, file, hcl(`${RUNTIME_CONFIG_MODULE} where { $body <: contains \`value = { $_ = local.api_url }\` }`));
317
+ if (declared === declarations.length && publishesApiUrl) {
318
+ return; // Already migrated.
319
+ }
320
+ if (declared > 0 || publishesApiUrl) {
321
+ nextSteps.push(`${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).`);
322
+ return;
323
+ }
324
+ const runtimeConfigValue = `${RUNTIME_CONFIG_MODULE} where { $body <: contains \`value = { $_ = $v }\`, ${api.runtimeConfigUrl} }`;
325
+ const shape = [
326
+ hcl('`variable "tags" { $_ }`'),
327
+ hcl(runtimeConfigValue),
328
+ hcl(POLICY_LOCALS),
329
+ hcl('`output "stage_invoke_url" { $_ }`')
330
+ ];
331
+ if (!await matchesAll(tree, file, shape)) {
332
+ nextSteps.push(`${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).`);
333
+ return;
334
+ }
335
+ await insertViaGritQL(tree, file, appendAfterBlock('variable "tags"'), TERRAFORM_VARIABLES);
336
+ // Only the URL expression is rewritten, preserving `terraform fmt` alignment.
337
+ await applyGritQL(tree, file, hcl(`${RUNTIME_CONFIG_MODULE} where { $body <: contains \`value = { $_ = $v }\`, ${api.runtimeConfigUrl}, $v => \`local.api_url\` }`));
338
+ // Alongside the module's other locals.
339
+ await insertViaGritQL(tree, file, hcl(`${POLICY_LOCALS} => \`locals {\n $body\n}\n\n${GRIT_INSERT_PLACEHOLDER}\``), api.apiUrlLocal);
340
+ await insertViaGritQL(tree, file, appendAfterBlock('module "add_url_to_runtime_config"'), api.domainResources);
341
+ await insertViaGritQL(tree, file, appendAfterBlock('output "stage_invoke_url"'), api.outputs);
342
+ };
343
+ export default async function migration(tree) {
344
+ const nextSteps = [];
345
+ await migrateRestApiConstruct(tree, nextSteps);
346
+ await migrateHttpApiConstruct(tree, nextSteps);
347
+ if (tree.exists(TERRAFORM_APIS_DIR)) {
348
+ for (const dirName of tree.children(TERRAFORM_APIS_DIR)){
349
+ const moduleDir = joinPathFragments(TERRAFORM_APIS_DIR, dirName);
350
+ const file = joinPathFragments(moduleDir, `${dirName}.tf`);
351
+ if (tree.exists(file)) {
352
+ await migrateTerraformApi(tree, moduleDir, file, nextSteps);
353
+ }
354
+ }
355
+ }
356
+ await formatFilesInSubtree(tree);
357
+ return {
358
+ nextSteps
359
+ };
360
+ }
361
+
362
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/v1.0.5/0001-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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"}
@@ -253,7 +253,7 @@ export default {
253
253
 
254
254
  exports[`preset generator > should run successfully > biome.json 1`] = `
255
255
  "{
256
- "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
256
+ "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
257
257
  "root": true,
258
258
  "formatter": {
259
259
  "enabled": true,
@@ -422,7 +422,7 @@ catalog:
422
422
  '@nx/js': 23.2.1
423
423
  '@nx/workspace': 23.2.1
424
424
  typescript: ~6.0.3
425
- '@biomejs/biome': 2.5.14
425
+ '@biomejs/biome': 2.5.15
426
426
  '@aws/nx-plugin': ^0.0.0
427
427
  '@aws/nx-plugin-mcp': ^0.0.0
428
428
  "
@@ -764,18 +764,18 @@ version = "0.1.0"
764
764
  dependencies = [
765
765
  "proj-agent-connection",
766
766
  "aws-lambda-powertools==3.35.0",
767
- "aws-opentelemetry-distro==0.20.0",
768
- "bedrock-agentcore==1.23.1",
769
- "boto3==1.43.103",
770
- "fastapi==0.141.1",
767
+ "aws-opentelemetry-distro==0.21.0",
768
+ "bedrock-agentcore==1.24.0",
769
+ "boto3==1.43.108",
770
+ "fastapi==0.142.2",
771
771
  "mcp==1.28.1",
772
- "strands-agents==1.57.1",
772
+ "strands-agents==1.57.2",
773
773
  "strands-agents-tools==0.8.9",
774
774
  "uvicorn==0.54.0"
775
775
  ]
776
776
 
777
777
  [dependency-groups]
778
- dev = [ "fastapi[standard]==0.141.1" ]
778
+ dev = [ "fastapi[standard]==0.142.2" ]
779
779
 
780
780
  [tool.uv]
781
781
  dev-dependencies = [ ]
@@ -702,23 +702,23 @@ version = "0.1.0"
702
702
  dependencies = [
703
703
  "proj-agent-connection",
704
704
  "aws-lambda-powertools==3.35.0",
705
- "aws-opentelemetry-distro==0.20.0",
706
- "bedrock-agentcore==1.23.1",
707
- "boto3==1.43.103",
708
- "fastapi==0.141.1",
705
+ "aws-opentelemetry-distro==0.21.0",
706
+ "bedrock-agentcore==1.24.0",
707
+ "boto3==1.43.108",
708
+ "fastapi==0.142.2",
709
709
  "mcp==1.28.1",
710
- "langchain==1.4.2",
711
- "langchain-aws==1.7.9",
710
+ "langchain==1.4.3",
711
+ "langchain-aws==1.8.0",
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
- "ag-ui-langgraph==0.0.45",
716
+ "ag-ui-langgraph==0.0.46",
717
717
  "uvicorn==0.54.0"
718
718
  ]
719
719
 
720
720
  [dependency-groups]
721
- dev = [ "fastapi[standard]==0.141.1" ]
721
+ dev = [ "fastapi[standard]==0.142.2" ]
722
722
 
723
723
  [tool.uv]
724
724
  dev-dependencies = [ ]
@@ -5,7 +5,7 @@ exports[`py#agent generator > should create DynamoDB checkpoint table and S3 off
5
5
  required_providers {
6
6
  aws = {
7
7
  source = "hashicorp/aws"
8
- version = "6.66.0"
8
+ version = "6.67.0"
9
9
  }
10
10
  random = {
11
11
  source = "hashicorp/random"
@@ -381,7 +381,7 @@ exports[`py#agent generator > should match snapshot for Terraform generated file
381
381
  required_providers {
382
382
  aws = {
383
383
  source = "hashicorp/aws"
384
- version = "6.66.0"
384
+ version = "6.67.0"
385
385
  }
386
386
  random = {
387
387
  source = "hashicorp/random"
@@ -704,7 +704,7 @@ exports[`py#agent generator > should match snapshot for Terraform generated file
704
704
  required_providers {
705
705
  aws = {
706
706
  source = "hashicorp/aws"
707
- version = "6.66.0"
707
+ version = "6.67.0"
708
708
  }
709
709
  null = {
710
710
  source = "hashicorp/null"
@@ -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.103 python -c "
1066
+ uv run --with boto3==1.43.108 python -c "
1067
1067
  import boto3
1068
1068
  import json
1069
1069
  import os
@@ -35,7 +35,7 @@ exports[`py#dynamodb generator > should generate terraform modules when iac is t
35
35
  required_providers {
36
36
  aws = {
37
37
  source = "hashicorp/aws"
38
- version = "6.66.0"
38
+ version = "6.67.0"
39
39
  }
40
40
  }
41
41
  }
@@ -232,7 +232,7 @@ exports[`py#dynamodb generator > should generate terraform modules when iac is t
232
232
  required_providers {
233
233
  aws = {
234
234
  source = "hashicorp/aws"
235
- version = "6.66.0"
235
+ version = "6.67.0"
236
236
  }
237
237
  random = {
238
238
  source = "hashicorp/random"