@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.
- package/LICENSE-THIRD-PARTY +7 -7
- package/migrations.json +10 -5
- package/package.json +4 -4
- package/src/infra/app/__snapshots__/generator.spec.ts.snap +3 -3
- package/src/license/dependency-check/collectors/npm-collector.d.ts +14 -0
- package/src/license/dependency-check/collectors/npm-collector.js +35 -1
- package/src/license/dependency-check/collectors/npm-collector.js.map +1 -1
- package/src/migrations/latest/api-custom-domain-runtime-config/metadata.json +3 -0
- package/src/migrations/latest/api-custom-domain-runtime-config/migration.js +362 -0
- package/src/migrations/latest/api-custom-domain-runtime-config/migration.js.map +1 -0
- package/src/migrations/v1.0.3/0001-terraform-init-target/migration.d.ts +6 -0
- package/src/migrations/v1.0.3/0001-terraform-init-target/migration.js.map +1 -0
- package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +3 -3
- package/src/py/agent/__snapshots__/generator.frameworks.spec.ts.snap +3 -3
- package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +1 -1
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +54 -6
- package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +378 -6
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +3 -3
- package/src/py/rdb/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/smithy/project/__snapshots__/generator.spec.ts.snap +16 -16
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +253 -5
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +432 -12
- package/src/ts/agent/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +13 -13
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +1 -1
- package/src/utils/api-constructs/files/cdk/core/api/http/http-api.ts.template +34 -4
- package/src/utils/api-constructs/files/cdk/core/api/rest/rest-api.ts.template +20 -2
- package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +63 -1
- package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +63 -1
- package/src/utils/docker.d.ts +1 -1
- package/src/utils/versions.d.ts +54 -54
- package/src/utils/versions.js +54 -54
- package/src/utils/versions.js.map +1 -1
- package/src/migrations/latest/terraform-init-target/migration.js.map +0 -1
- /package/src/migrations/latest/{terraform-init-target → api-custom-domain-runtime-config}/migration.d.ts +0 -0
- /package/src/migrations/{latest/terraform-init-target → v1.0.3/0001-terraform-init-target}/metadata.json +0 -0
- /package/src/migrations/{latest/terraform-init-target → v1.0.3/0001-terraform-init-target}/migration.js +0 -0
package/LICENSE-THIRD-PARTY
CHANGED
|
@@ -108,7 +108,7 @@ THE SOFTWARE.
|
|
|
108
108
|
|
|
109
109
|
---
|
|
110
110
|
|
|
111
|
-
The following software may be included in this product: @astral-sh/ruff-wasm-nodejs (0.16.
|
|
111
|
+
The following software may be included in this product: @astral-sh/ruff-wasm-nodejs (0.16.9)
|
|
112
112
|
This software contains the following license and notice below:
|
|
113
113
|
|
|
114
114
|
MIT License
|
|
@@ -3898,7 +3898,7 @@ SOFTWARE.
|
|
|
3898
3898
|
|
|
3899
3899
|
---
|
|
3900
3900
|
|
|
3901
|
-
The following software may be included in this product: @modelcontextprotocol/sdk (1.30.
|
|
3901
|
+
The following software may be included in this product: @modelcontextprotocol/sdk (1.30.1)
|
|
3902
3902
|
This software contains the following license and notice below:
|
|
3903
3903
|
|
|
3904
3904
|
MIT License
|
|
@@ -4274,7 +4274,7 @@ SOFTWARE.
|
|
|
4274
4274
|
|
|
4275
4275
|
---
|
|
4276
4276
|
|
|
4277
|
-
The following software may be included in this product: @oxc-project/types (0.
|
|
4277
|
+
The following software may be included in this product: @oxc-project/types (0.151.0)
|
|
4278
4278
|
This software contains the following license and notice below:
|
|
4279
4279
|
|
|
4280
4280
|
MIT License
|
|
@@ -8757,7 +8757,7 @@ THE SOFTWARE.
|
|
|
8757
8757
|
|
|
8758
8758
|
---
|
|
8759
8759
|
|
|
8760
|
-
The following software may be included in this product: fs-extra (11.4.
|
|
8760
|
+
The following software may be included in this product: fs-extra (11.4.1)
|
|
8761
8761
|
This software contains the following license and notice below:
|
|
8762
8762
|
|
|
8763
8763
|
(The MIT License)
|
|
@@ -9034,7 +9034,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
9034
9034
|
|
|
9035
9035
|
---
|
|
9036
9036
|
|
|
9037
|
-
The following software may be included in this product: hono (4.13.
|
|
9037
|
+
The following software may be included in this product: hono (4.13.8)
|
|
9038
9038
|
This software contains the following license and notice below:
|
|
9039
9039
|
|
|
9040
9040
|
MIT License
|
|
@@ -14165,7 +14165,7 @@ SOFTWARE.
|
|
|
14165
14165
|
|
|
14166
14166
|
---
|
|
14167
14167
|
|
|
14168
|
-
The following software may be included in this product: rolldown (1.2.
|
|
14168
|
+
The following software may be included in this product: rolldown (1.2.11)
|
|
14169
14169
|
This software contains the following license and notice below:
|
|
14170
14170
|
|
|
14171
14171
|
MIT License
|
|
@@ -16088,7 +16088,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
16088
16088
|
|
|
16089
16089
|
---
|
|
16090
16090
|
|
|
16091
|
-
The following software may be included in this product: vite (8.3.
|
|
16091
|
+
The following software may be included in this product: vite (8.3.1)
|
|
16092
16092
|
This software contains the following license and notice below:
|
|
16093
16093
|
|
|
16094
16094
|
# Vite core license
|
package/migrations.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/schema",
|
|
3
3
|
"name": "@aws/nx-plugin",
|
|
4
4
|
"generators": {
|
|
5
|
-
"latest-
|
|
6
|
-
"version": "1.0.
|
|
7
|
-
"description": "
|
|
8
|
-
"implementation": "./src/migrations/latest/
|
|
5
|
+
"latest-api-custom-domain-runtime-config": {
|
|
6
|
+
"version": "1.0.5",
|
|
7
|
+
"description": "Publish a configured custom domain as the API URL in runtime config for vended REST/HTTP API constructs and Terraform modules, and add custom domain inputs to the Terraform API modules",
|
|
8
|
+
"implementation": "./src/migrations/latest/api-custom-domain-runtime-config/migration"
|
|
9
9
|
},
|
|
10
10
|
"v1.0.0-rc.50-0001-modernize-function-props-cast": {
|
|
11
11
|
"version": "1.0.0-rc.50",
|
|
@@ -377,8 +377,13 @@
|
|
|
377
377
|
"description": "Send an empty map rather than null for a vended Terraform AgentCore Harness module's environment_variables, so the first apply does not fail after creating the Harness",
|
|
378
378
|
"implementation": "./src/migrations/v1.0.2/0001-terraform-harness-environment-variables/migration"
|
|
379
379
|
},
|
|
380
|
-
"
|
|
380
|
+
"v1.0.3-0001-terraform-init-target": {
|
|
381
381
|
"version": "1.0.3",
|
|
382
|
+
"description": "Add the terraform-init target to Terraform projects so the shared provider plugin cache has a single writer",
|
|
383
|
+
"implementation": "./src/migrations/v1.0.3/0001-terraform-init-target/migration"
|
|
384
|
+
},
|
|
385
|
+
"sync-vended-versions": {
|
|
386
|
+
"version": "1.0.5",
|
|
382
387
|
"description": "Sync vended dependency versions and the tracked plugin version to those vended by this release",
|
|
383
388
|
"implementation": "./src/utils/version-upgrade-migration/migration"
|
|
384
389
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws/nx-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/awslabs/nx-plugin-for-aws.git",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@apidevtools/swagger-parser": "12.1.0",
|
|
43
|
-
"@astral-sh/ruff-wasm-nodejs": "0.16.
|
|
43
|
+
"@astral-sh/ruff-wasm-nodejs": "0.16.9",
|
|
44
44
|
"@biomejs/js-api": "6.0.0",
|
|
45
45
|
"@biomejs/wasm-nodejs": "2.5.14",
|
|
46
46
|
"@getgrit/gritql": "0.0.3",
|
|
47
47
|
"@iarna/toml": "2.2.5",
|
|
48
|
-
"@modelcontextprotocol/sdk": "1.30.
|
|
48
|
+
"@modelcontextprotocol/sdk": "1.30.1",
|
|
49
49
|
"@nx/devkit": "23.2.1",
|
|
50
50
|
"@nx/js": "23.2.1",
|
|
51
51
|
"@nx/react": "23.2.1",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"typescript": "6.0.3",
|
|
80
80
|
"unified": "11.0.5",
|
|
81
81
|
"unist-util-visit": "5.1.0",
|
|
82
|
-
"vite": "8.3.
|
|
82
|
+
"vite": "8.3.1",
|
|
83
83
|
"vitest": "4.1.11",
|
|
84
84
|
"zod": "4.6.5"
|
|
85
85
|
},
|
|
@@ -50,7 +50,7 @@ exports[`infra generator > should configure Checkov target correctly > checkov-t
|
|
|
50
50
|
"default",
|
|
51
51
|
],
|
|
52
52
|
"options": {
|
|
53
|
-
"command": "uvx --from checkov==3.3.
|
|
53
|
+
"command": "uvx --from checkov==3.3.20 checkov --config-file {projectRoot}/checkov.yml --directory dist/{projectRoot}/cdk.out --framework cloudformation",
|
|
54
54
|
},
|
|
55
55
|
"outputs": [
|
|
56
56
|
"{workspaceRoot}/dist/{projectRoot}/checkov",
|
|
@@ -150,7 +150,7 @@ exports[`infra generator > should configure project.json with correct targets >
|
|
|
150
150
|
"default",
|
|
151
151
|
],
|
|
152
152
|
"options": {
|
|
153
|
-
"command": "uvx --from checkov==3.3.
|
|
153
|
+
"command": "uvx --from checkov==3.3.20 checkov --config-file {projectRoot}/checkov.yml --directory dist/{projectRoot}/cdk.out --framework cloudformation",
|
|
154
154
|
},
|
|
155
155
|
"outputs": [
|
|
156
156
|
"{workspaceRoot}/dist/{projectRoot}/checkov",
|
|
@@ -762,7 +762,7 @@ exports[`infra generator > should handle custom project names correctly > custom
|
|
|
762
762
|
"default",
|
|
763
763
|
],
|
|
764
764
|
"options": {
|
|
765
|
-
"command": "uvx --from checkov==3.3.
|
|
765
|
+
"command": "uvx --from checkov==3.3.20 checkov --config-file {projectRoot}/checkov.yml --directory dist/{projectRoot}/cdk.out --framework cloudformation",
|
|
766
766
|
},
|
|
767
767
|
"outputs": [
|
|
768
768
|
"{workspaceRoot}/dist/{projectRoot}/checkov",
|
|
@@ -7,4 +7,18 @@ export interface NpmDependency {
|
|
|
7
7
|
export interface NpmCollectorOptions {
|
|
8
8
|
start: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a license that only names a file into the license that file grants.
|
|
12
|
+
*
|
|
13
|
+
* license-checker reads a package's license file only when the package declares
|
|
14
|
+
* no license, so a package declaring `SEE LICENSE IN LICENSE` is reported as
|
|
15
|
+
* `Custom: LICENSE` and evaluates to UNKNOWN even when the file grants a
|
|
16
|
+
* pre-approved license. Read the file license-checker already located and run
|
|
17
|
+
* its text detection over it, which returns an SPDX id with the trailing `*`
|
|
18
|
+
* marking a license guessed from text.
|
|
19
|
+
*
|
|
20
|
+
* The reference is kept when the file can't be read, or when detection can't
|
|
21
|
+
* name a license, so the dependency is still reported as UNKNOWN.
|
|
22
|
+
*/
|
|
23
|
+
export declare const resolveFileReferencedLicense: (rawLicense: string, licenseFile: string | undefined) => string;
|
|
10
24
|
export declare const collectNpmDependencies: (options: NpmCollectorOptions) => Promise<NpmDependency[]>;
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/ import { existsSync, readFileSync } from "fs";
|
|
5
5
|
import * as licenseChecker from "license-checker-rseidelsohn";
|
|
6
|
+
// license-checker's own license-from-text detection, which it applies to
|
|
7
|
+
// packages that declare no license. Reused here so a `SEE LICENSE IN <file>`
|
|
8
|
+
// package resolves the same way, rather than through a second implementation.
|
|
9
|
+
import { getLicenseTitle } from "license-checker-rseidelsohn/lib/getLicenseTitle.js";
|
|
6
10
|
import { join } from "path";
|
|
7
11
|
const NAME_VERSION_RE = /^(@?[^@]+)@(.+)$/;
|
|
8
12
|
const splitNameVersion = (identifier)=>{
|
|
@@ -27,6 +31,36 @@ const flattenLicense = (raw)=>{
|
|
|
27
31
|
}
|
|
28
32
|
return raw;
|
|
29
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* A license that names a file instead of a license: the SPDX form
|
|
36
|
+
* `SEE LICENSE IN <file>`, or the `Custom: <file>` license-checker reports it
|
|
37
|
+
* as.
|
|
38
|
+
*/ const FILE_REFERENCE_RE = /^(?:custom:|see\s+license\s+in)\s+(.+)$/i;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a license that only names a file into the license that file grants.
|
|
41
|
+
*
|
|
42
|
+
* license-checker reads a package's license file only when the package declares
|
|
43
|
+
* no license, so a package declaring `SEE LICENSE IN LICENSE` is reported as
|
|
44
|
+
* `Custom: LICENSE` and evaluates to UNKNOWN even when the file grants a
|
|
45
|
+
* pre-approved license. Read the file license-checker already located and run
|
|
46
|
+
* its text detection over it, which returns an SPDX id with the trailing `*`
|
|
47
|
+
* marking a license guessed from text.
|
|
48
|
+
*
|
|
49
|
+
* The reference is kept when the file can't be read, or when detection can't
|
|
50
|
+
* name a license, so the dependency is still reported as UNKNOWN.
|
|
51
|
+
*/ export const resolveFileReferencedLicense = (rawLicense, licenseFile)=>{
|
|
52
|
+
if (!licenseFile || !FILE_REFERENCE_RE.test(rawLicense.trim())) {
|
|
53
|
+
return rawLicense;
|
|
54
|
+
}
|
|
55
|
+
let detected = null;
|
|
56
|
+
try {
|
|
57
|
+
detected = getLicenseTitle(readFileSync(licenseFile, 'utf-8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return rawLicense;
|
|
60
|
+
}
|
|
61
|
+
if (!detected || detected === 'Undefined' || FILE_REFERENCE_RE.test(detected)) return rawLicense;
|
|
62
|
+
return detected;
|
|
63
|
+
};
|
|
30
64
|
export const collectNpmDependencies = async (options)=>{
|
|
31
65
|
const nodeModules = join(options.start, 'node_modules');
|
|
32
66
|
if (!existsSync(nodeModules)) {
|
|
@@ -58,7 +92,7 @@ export const collectNpmDependencies = async (options)=>{
|
|
|
58
92
|
out.push({
|
|
59
93
|
name,
|
|
60
94
|
version,
|
|
61
|
-
rawLicense: flattenLicense(info.licenses),
|
|
95
|
+
rawLicense: resolveFileReferencedLicense(flattenLicense(info.licenses), info.licenseFile),
|
|
62
96
|
path: info.path
|
|
63
97
|
});
|
|
64
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/license/dependency-check/collectors/npm-collector.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { existsSync, readFileSync } from 'fs';\nimport * as licenseChecker from 'license-checker-rseidelsohn';\nimport { join } from 'path';\n\nexport interface NpmDependency {\n name: string;\n version: string;\n rawLicense: string;\n path?: string;\n}\n\nexport interface NpmCollectorOptions {\n start: string;\n}\n\nconst NAME_VERSION_RE = /^(@?[^@]+)@(.+)$/;\n\nconst splitNameVersion = (\n identifier: string,\n): { name: string; version: string } => {\n const match = identifier.match(NAME_VERSION_RE);\n if (!match) {\n return { name: identifier, version: '0.0.0' };\n }\n return { name: match[1], version: match[2] };\n};\n\nconst flattenLicense = (raw: string | string[] | undefined | null): string => {\n if (!raw) return '';\n if (Array.isArray(raw)) {\n if (raw.length === 0) return '';\n if (raw.length === 1) return raw[0];\n return `(${raw.join(' OR ')})`;\n }\n return raw;\n};\n\nexport const collectNpmDependencies = async (\n options: NpmCollectorOptions,\n): Promise<NpmDependency[]> => {\n const nodeModules = join(options.start, 'node_modules');\n if (!existsSync(nodeModules)) {\n return [];\n }\n\n const exclude = new Set<string>();\n\n const projectPackageJson = join(options.start, 'package.json');\n if (existsSync(projectPackageJson)) {\n try {\n const pkg = JSON.parse(readFileSync(projectPackageJson, 'utf-8')) as {\n name?: string;\n };\n if (pkg.name) exclude.add(pkg.name);\n } catch {\n // ignore\n }\n }\n\n const packages = await new Promise<licenseChecker.ModuleInfos>(\n (resolve, reject) => {\n licenseChecker.init(\n {\n start: options.start,\n excludePrivatePackages: true,\n },\n (err, results) => {\n if (err) reject(err);\n else resolve(results);\n },\n );\n },\n );\n\n const out: NpmDependency[] = [];\n for (const [identifier, info] of Object.entries(packages)) {\n const { name, version } = splitNameVersion(identifier);\n if (exclude.has(name)) continue;\n out.push({\n name,\n version,\n rawLicense: flattenLicense(info.licenses),\n path: info.path,\n });\n }\n return out;\n};\n"],"names":["existsSync","readFileSync","licenseChecker","join","NAME_VERSION_RE","splitNameVersion","identifier","match","name","version","flattenLicense","raw","Array","isArray","length","collectNpmDependencies","options","nodeModules","start","exclude","Set","projectPackageJson","pkg","JSON","parse","add","packages","Promise","resolve","reject","init","excludePrivatePackages","err","results","out","info","Object","entries","has","push","
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/license/dependency-check/collectors/npm-collector.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { existsSync, readFileSync } from 'fs';\nimport * as licenseChecker from 'license-checker-rseidelsohn';\n// license-checker's own license-from-text detection, which it applies to\n// packages that declare no license. Reused here so a `SEE LICENSE IN <file>`\n// package resolves the same way, rather than through a second implementation.\nimport { getLicenseTitle } from 'license-checker-rseidelsohn/lib/getLicenseTitle.js';\nimport { join } from 'path';\n\nexport interface NpmDependency {\n name: string;\n version: string;\n rawLicense: string;\n path?: string;\n}\n\nexport interface NpmCollectorOptions {\n start: string;\n}\n\nconst NAME_VERSION_RE = /^(@?[^@]+)@(.+)$/;\n\nconst splitNameVersion = (\n identifier: string,\n): { name: string; version: string } => {\n const match = identifier.match(NAME_VERSION_RE);\n if (!match) {\n return { name: identifier, version: '0.0.0' };\n }\n return { name: match[1], version: match[2] };\n};\n\nconst flattenLicense = (raw: string | string[] | undefined | null): string => {\n if (!raw) return '';\n if (Array.isArray(raw)) {\n if (raw.length === 0) return '';\n if (raw.length === 1) return raw[0];\n return `(${raw.join(' OR ')})`;\n }\n return raw;\n};\n\n/**\n * A license that names a file instead of a license: the SPDX form\n * `SEE LICENSE IN <file>`, or the `Custom: <file>` license-checker reports it\n * as.\n */\nconst FILE_REFERENCE_RE = /^(?:custom:|see\\s+license\\s+in)\\s+(.+)$/i;\n\n/**\n * Resolve a license that only names a file into the license that file grants.\n *\n * license-checker reads a package's license file only when the package declares\n * no license, so a package declaring `SEE LICENSE IN LICENSE` is reported as\n * `Custom: LICENSE` and evaluates to UNKNOWN even when the file grants a\n * pre-approved license. Read the file license-checker already located and run\n * its text detection over it, which returns an SPDX id with the trailing `*`\n * marking a license guessed from text.\n *\n * The reference is kept when the file can't be read, or when detection can't\n * name a license, so the dependency is still reported as UNKNOWN.\n */\nexport const resolveFileReferencedLicense = (\n rawLicense: string,\n licenseFile: string | undefined,\n): string => {\n if (!licenseFile || !FILE_REFERENCE_RE.test(rawLicense.trim())) {\n return rawLicense;\n }\n let detected: string | null = null;\n try {\n detected = getLicenseTitle(readFileSync(licenseFile, 'utf-8'));\n } catch {\n return rawLicense;\n }\n if (!detected || detected === 'Undefined' || FILE_REFERENCE_RE.test(detected))\n return rawLicense;\n return detected;\n};\n\nexport const collectNpmDependencies = async (\n options: NpmCollectorOptions,\n): Promise<NpmDependency[]> => {\n const nodeModules = join(options.start, 'node_modules');\n if (!existsSync(nodeModules)) {\n return [];\n }\n\n const exclude = new Set<string>();\n\n const projectPackageJson = join(options.start, 'package.json');\n if (existsSync(projectPackageJson)) {\n try {\n const pkg = JSON.parse(readFileSync(projectPackageJson, 'utf-8')) as {\n name?: string;\n };\n if (pkg.name) exclude.add(pkg.name);\n } catch {\n // ignore\n }\n }\n\n const packages = await new Promise<licenseChecker.ModuleInfos>(\n (resolve, reject) => {\n licenseChecker.init(\n {\n start: options.start,\n excludePrivatePackages: true,\n },\n (err, results) => {\n if (err) reject(err);\n else resolve(results);\n },\n );\n },\n );\n\n const out: NpmDependency[] = [];\n for (const [identifier, info] of Object.entries(packages)) {\n const { name, version } = splitNameVersion(identifier);\n if (exclude.has(name)) continue;\n out.push({\n name,\n version,\n rawLicense: resolveFileReferencedLicense(\n flattenLicense(info.licenses),\n info.licenseFile,\n ),\n path: info.path,\n });\n }\n return out;\n};\n"],"names":["existsSync","readFileSync","licenseChecker","getLicenseTitle","join","NAME_VERSION_RE","splitNameVersion","identifier","match","name","version","flattenLicense","raw","Array","isArray","length","FILE_REFERENCE_RE","resolveFileReferencedLicense","rawLicense","licenseFile","test","trim","detected","collectNpmDependencies","options","nodeModules","start","exclude","Set","projectPackageJson","pkg","JSON","parse","add","packages","Promise","resolve","reject","init","excludePrivatePackages","err","results","out","info","Object","entries","has","push","licenses","path"],"mappings":"AAAA;;;CAGC,GACD,SAASA,UAAU,EAAEC,YAAY,QAAQ,KAAK;AAC9C,YAAYC,oBAAoB,8BAA8B;AAC9D,yEAAyE;AACzE,6EAA6E;AAC7E,8EAA8E;AAC9E,SAASC,eAAe,QAAQ,qDAAqD;AACrF,SAASC,IAAI,QAAQ,OAAO;AAa5B,MAAMC,kBAAkB;AAExB,MAAMC,mBAAmB,CACvBC;IAEA,MAAMC,QAAQD,WAAWC,KAAK,CAACH;IAC/B,IAAI,CAACG,OAAO;QACV,OAAO;YAAEC,MAAMF;YAAYG,SAAS;QAAQ;IAC9C;IACA,OAAO;QAAED,MAAMD,KAAK,CAAC,EAAE;QAAEE,SAASF,KAAK,CAAC,EAAE;IAAC;AAC7C;AAEA,MAAMG,iBAAiB,CAACC;IACtB,IAAI,CAACA,KAAK,OAAO;IACjB,IAAIC,MAAMC,OAAO,CAACF,MAAM;QACtB,IAAIA,IAAIG,MAAM,KAAK,GAAG,OAAO;QAC7B,IAAIH,IAAIG,MAAM,KAAK,GAAG,OAAOH,GAAG,CAAC,EAAE;QACnC,OAAO,CAAC,CAAC,EAAEA,IAAIR,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC;IACA,OAAOQ;AACT;AAEA;;;;CAIC,GACD,MAAMI,oBAAoB;AAE1B;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,+BAA+B,CAC1CC,YACAC;IAEA,IAAI,CAACA,eAAe,CAACH,kBAAkBI,IAAI,CAACF,WAAWG,IAAI,KAAK;QAC9D,OAAOH;IACT;IACA,IAAII,WAA0B;IAC9B,IAAI;QACFA,WAAWnB,gBAAgBF,aAAakB,aAAa;IACvD,EAAE,OAAM;QACN,OAAOD;IACT;IACA,IAAI,CAACI,YAAYA,aAAa,eAAeN,kBAAkBI,IAAI,CAACE,WAClE,OAAOJ;IACT,OAAOI;AACT,EAAE;AAEF,OAAO,MAAMC,yBAAyB,OACpCC;IAEA,MAAMC,cAAcrB,KAAKoB,QAAQE,KAAK,EAAE;IACxC,IAAI,CAAC1B,WAAWyB,cAAc;QAC5B,OAAO,EAAE;IACX;IAEA,MAAME,UAAU,IAAIC;IAEpB,MAAMC,qBAAqBzB,KAAKoB,QAAQE,KAAK,EAAE;IAC/C,IAAI1B,WAAW6B,qBAAqB;QAClC,IAAI;YACF,MAAMC,MAAMC,KAAKC,KAAK,CAAC/B,aAAa4B,oBAAoB;YAGxD,IAAIC,IAAIrB,IAAI,EAAEkB,QAAQM,GAAG,CAACH,IAAIrB,IAAI;QACpC,EAAE,OAAM;QACN,SAAS;QACX;IACF;IAEA,MAAMyB,WAAW,MAAM,IAAIC,QACzB,CAACC,SAASC;QACRnC,eAAeoC,IAAI,CACjB;YACEZ,OAAOF,QAAQE,KAAK;YACpBa,wBAAwB;QAC1B,GACA,CAACC,KAAKC;YACJ,IAAID,KAAKH,OAAOG;iBACXJ,QAAQK;QACf;IAEJ;IAGF,MAAMC,MAAuB,EAAE;IAC/B,KAAK,MAAM,CAACnC,YAAYoC,KAAK,IAAIC,OAAOC,OAAO,CAACX,UAAW;QACzD,MAAM,EAAEzB,IAAI,EAAEC,OAAO,EAAE,GAAGJ,iBAAiBC;QAC3C,IAAIoB,QAAQmB,GAAG,CAACrC,OAAO;QACvBiC,IAAIK,IAAI,CAAC;YACPtC;YACAC;YACAQ,YAAYD,6BACVN,eAAegC,KAAKK,QAAQ,GAC5BL,KAAKxB,WAAW;YAElB8B,MAAMN,KAAKM,IAAI;QACjB;IACF;IACA,OAAOP;AACT,EAAE"}
|
|
@@ -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
|