@aws/nx-plugin 1.0.0-rc.55 → 1.0.0-rc.56
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/migrations.json +8 -3
- package/package.json +1 -1
- package/src/migrations/latest/dynamodb-local-remove-optimize-flag/migration.d.ts +6 -0
- package/src/migrations/latest/dynamodb-local-remove-optimize-flag/migration.js +89 -0
- package/src/migrations/latest/dynamodb-local-remove-optimize-flag/migration.js.map +1 -0
- package/src/migrations/latest/modernize-function-props-cast/migration.js +0 -1
- package/src/migrations/latest/modernize-function-props-cast/migration.js.map +1 -1
- package/src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration.js +0 -1
- package/src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration.js.map +1 -1
- package/src/migrations/latest/restrict-cors-to-custom-domains/migration.js +0 -2
- package/src/migrations/latest/restrict-cors-to-custom-domains/migration.js.map +1 -1
- package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.js +0 -1
- package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.js.map +1 -1
- package/src/migrations/latest/user-identity-waf-allow-localhost-callback/migration.js +0 -2
- package/src/migrations/latest/user-identity-waf-allow-localhost-callback/migration.js.map +1 -1
- package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +0 -2
- package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +0 -2
- package/src/ts/nx-migration/files/migration.ts.template +4 -0
- package/src/utils/files/common/scripts/src/dynamodb/start-container.ts.template +0 -1
- package/src/utils/migration-versions.d.ts +15 -10
- package/src/utils/migration-versions.js +48 -36
- package/src/utils/migration-versions.js.map +1 -1
- package/src/utils/version-upgrade-migration/nx-package-updates.d.ts +18 -8
- package/src/utils/version-upgrade-migration/nx-package-updates.js +19 -11
- package/src/utils/version-upgrade-migration/nx-package-updates.js.map +1 -1
- package/src/utils/version-upgrade-migration/register.d.ts +6 -1
- package/src/utils/version-upgrade-migration/register.js +8 -3
- package/src/utils/version-upgrade-migration/register.js.map +1 -1
package/migrations.json
CHANGED
|
@@ -32,15 +32,20 @@
|
|
|
32
32
|
"description": "Backfill the project metadata the version sync reads, recovered from the files the generators left behind",
|
|
33
33
|
"implementation": "./src/migrations/latest/backfill-generator-metadata/migration"
|
|
34
34
|
},
|
|
35
|
+
"latest-dynamodb-local-remove-optimize-flag": {
|
|
36
|
+
"version": "1.0.0-rc.56",
|
|
37
|
+
"description": "Drop -optimizeDbBeforeStartup from the vended DynamoDB Local container script, which can corrupt shared-local-instance.db",
|
|
38
|
+
"implementation": "./src/migrations/latest/dynamodb-local-remove-optimize-flag/migration"
|
|
39
|
+
},
|
|
35
40
|
"sync-vended-versions": {
|
|
36
|
-
"version": "1.0.0-rc.
|
|
41
|
+
"version": "1.0.0-rc.56",
|
|
37
42
|
"description": "Sync vended dependency versions and the tracked plugin version to those vended by this release",
|
|
38
43
|
"implementation": "./src/utils/version-upgrade-migration/migration"
|
|
39
44
|
}
|
|
40
45
|
},
|
|
41
46
|
"packageJsonUpdates": {
|
|
42
|
-
"
|
|
43
|
-
"version": "1.0.0-rc.
|
|
47
|
+
"nx-23.1.0-nx-packages": {
|
|
48
|
+
"version": "1.0.0-rc.56",
|
|
44
49
|
"packages": {
|
|
45
50
|
"nx": {
|
|
46
51
|
"version": "23.1.0",
|
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { applyGritQL } from "../../../utils/ast.js";
|
|
5
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
6
|
+
import { PACKAGES_DIR, SHARED_SCRIPTS_DIR } from "../../../utils/shared-constructs-constants.js";
|
|
7
|
+
/**
|
|
8
|
+
* Drop -optimizeDbBeforeStartup from the vended DynamoDB Local container script
|
|
9
|
+
*
|
|
10
|
+
* DynamoDB Local's startup vacuum drops primary key / GSI uniqueness on the
|
|
11
|
+
* underlying sqlite file, letting PutItem/UpdateItem write duplicate rows for
|
|
12
|
+
* the same key.
|
|
13
|
+
*
|
|
14
|
+
* The edit is expressed as a GritQL rewrite so the script is matched on its
|
|
15
|
+
* AST rather than its formatting. The rewrite matches the whole `runArgs`
|
|
16
|
+
* array rather than just the flag, because this GritQL/tree-sitter grammar
|
|
17
|
+
* can't match a partial slice of array elements, and a literal backtick
|
|
18
|
+
* template string breaks matching anywhere it appears alongside other
|
|
19
|
+
* elements - so every templated element (the port/volume args) is matched via
|
|
20
|
+
* a metavariable instead of being spelled out.
|
|
21
|
+
*
|
|
22
|
+
* How to write a migration:
|
|
23
|
+
* - https://nx.dev/docs/kb/migration-generators
|
|
24
|
+
* - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject
|
|
25
|
+
*
|
|
26
|
+
* Guardrails:
|
|
27
|
+
* - Pattern-match before writing: skip files that have diverged from the shape
|
|
28
|
+
* the generator produces and report them via `nextSteps`.
|
|
29
|
+
* - Idempotent: re-running must be a no-op.
|
|
30
|
+
* - Format what you write: finish with `formatFilesInSubtree`.
|
|
31
|
+
*/ const START_CONTAINER_FILE = `${PACKAGES_DIR}/${SHARED_SCRIPTS_DIR}/src/dynamodb/start-container.ts`;
|
|
32
|
+
const FLAG = "'-optimizeDbBeforeStartup'";
|
|
33
|
+
const REMOVE_FLAG_PATTERN = `\`[
|
|
34
|
+
'run',
|
|
35
|
+
...(containerEngine === 'docker' ? ['--rm'] : []),
|
|
36
|
+
'--name', containerName,
|
|
37
|
+
'-u', 'root',
|
|
38
|
+
'-w', '/home/dynamodblocal',
|
|
39
|
+
$flagP, $portColonPort,
|
|
40
|
+
'-v', $volume,
|
|
41
|
+
'-d', image,
|
|
42
|
+
'-jar', 'DynamoDBLocal.jar',
|
|
43
|
+
'-sharedDb',
|
|
44
|
+
'-dbPath', './data',
|
|
45
|
+
'-port', $portArg,
|
|
46
|
+
${FLAG},
|
|
47
|
+
]\` => \`[
|
|
48
|
+
'run',
|
|
49
|
+
...(containerEngine === 'docker' ? ['--rm'] : []),
|
|
50
|
+
'--name', containerName,
|
|
51
|
+
'-u', 'root',
|
|
52
|
+
'-w', '/home/dynamodblocal',
|
|
53
|
+
$flagP, $portColonPort,
|
|
54
|
+
'-v', $volume,
|
|
55
|
+
'-d', image,
|
|
56
|
+
'-jar', 'DynamoDBLocal.jar',
|
|
57
|
+
'-sharedDb',
|
|
58
|
+
'-dbPath', './data',
|
|
59
|
+
'-port', $portArg,
|
|
60
|
+
]\``;
|
|
61
|
+
const divergedNextStep = `${START_CONTAINER_FILE}: the container script has diverged from the generated shape - left untouched. Manually drop ${FLAG} from the DynamoDB Local container run args - its startup vacuum can drop primary key / GSI uniqueness on the sqlite file, letting PutItem/UpdateItem write duplicate rows for the same key.`;
|
|
62
|
+
export default async function migration(tree) {
|
|
63
|
+
const nextSteps = [];
|
|
64
|
+
if (!tree.exists(START_CONTAINER_FILE)) {
|
|
65
|
+
return {
|
|
66
|
+
nextSteps
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const contents = tree.read(START_CONTAINER_FILE, 'utf-8') ?? '';
|
|
70
|
+
if (!contents.includes(FLAG)) {
|
|
71
|
+
// Already migrated, or doesn't use this shape.
|
|
72
|
+
return {
|
|
73
|
+
nextSteps
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const rewrote = await applyGritQL(tree, START_CONTAINER_FILE, REMOVE_FLAG_PATTERN);
|
|
77
|
+
if (!rewrote) {
|
|
78
|
+
nextSteps.push(divergedNextStep);
|
|
79
|
+
return {
|
|
80
|
+
nextSteps
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
await formatFilesInSubtree(tree);
|
|
84
|
+
return {
|
|
85
|
+
nextSteps
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/dynamodb-local-remove-optimize-flag/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport { applyGritQL } from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_SCRIPTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Drop -optimizeDbBeforeStartup from the vended DynamoDB Local container script\n *\n * DynamoDB Local's startup vacuum drops primary key / GSI uniqueness on the\n * underlying sqlite file, letting PutItem/UpdateItem write duplicate rows for\n * the same key.\n *\n * The edit is expressed as a GritQL rewrite so the script is matched on its\n * AST rather than its formatting. The rewrite matches the whole `runArgs`\n * array rather than just the flag, because this GritQL/tree-sitter grammar\n * can't match a partial slice of array elements, and a literal backtick\n * template string breaks matching anywhere it appears alongside other\n * elements - so every templated element (the port/volume args) is matched via\n * a metavariable instead of being spelled out.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * the generator produces and report them via `nextSteps`.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree`.\n */\n\nconst START_CONTAINER_FILE = `${PACKAGES_DIR}/${SHARED_SCRIPTS_DIR}/src/dynamodb/start-container.ts`;\n\nconst FLAG = \"'-optimizeDbBeforeStartup'\";\n\nconst REMOVE_FLAG_PATTERN = `\\`[\n 'run',\n ...(containerEngine === 'docker' ? ['--rm'] : []),\n '--name', containerName,\n '-u', 'root',\n '-w', '/home/dynamodblocal',\n $flagP, $portColonPort,\n '-v', $volume,\n '-d', image,\n '-jar', 'DynamoDBLocal.jar',\n '-sharedDb',\n '-dbPath', './data',\n '-port', $portArg,\n ${FLAG},\n ]\\` => \\`[\n 'run',\n ...(containerEngine === 'docker' ? ['--rm'] : []),\n '--name', containerName,\n '-u', 'root',\n '-w', '/home/dynamodblocal',\n $flagP, $portColonPort,\n '-v', $volume,\n '-d', image,\n '-jar', 'DynamoDBLocal.jar',\n '-sharedDb',\n '-dbPath', './data',\n '-port', $portArg,\n ]\\``;\n\nconst divergedNextStep = `${START_CONTAINER_FILE}: the container script has diverged from the generated shape - left untouched. Manually drop ${FLAG} from the DynamoDB Local container run args - its startup vacuum can drop primary key / GSI uniqueness on the sqlite file, letting PutItem/UpdateItem write duplicate rows for the same key.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(START_CONTAINER_FILE)) {\n return { nextSteps };\n }\n\n const contents = tree.read(START_CONTAINER_FILE, 'utf-8') ?? '';\n if (!contents.includes(FLAG)) {\n // Already migrated, or doesn't use this shape.\n return { nextSteps };\n }\n\n const rewrote = await applyGritQL(\n tree,\n START_CONTAINER_FILE,\n REMOVE_FLAG_PATTERN,\n );\n\n if (!rewrote) {\n nextSteps.push(divergedNextStep);\n return { nextSteps };\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["applyGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_SCRIPTS_DIR","START_CONTAINER_FILE","FLAG","REMOVE_FLAG_PATTERN","divergedNextStep","migration","tree","nextSteps","exists","contents","read","includes","rewrote","push"],"mappings":"AAAA;;;CAGC,GAED,SAASA,WAAW,QAAQ,wBAAqB;AACjD,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SACEC,YAAY,EACZC,kBAAkB,QACb,gDAA6C;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,MAAMC,uBAAuB,GAAGF,aAAa,CAAC,EAAEC,mBAAmB,gCAAgC,CAAC;AAEpG,MAAME,OAAO;AAEb,MAAMC,sBAAsB,CAAC;;;;;;;;;;;;;IAazB,EAAED,KAAK;;;;;;;;;;;;;;KAcN,CAAC;AAEN,MAAME,mBAAmB,GAAGH,qBAAqB,6FAA6F,EAAEC,KAAK,4LAA4L,CAAC;AAElV,eAAe,eAAeG,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,IAAI,CAACD,KAAKE,MAAM,CAACP,uBAAuB;QACtC,OAAO;YAAEM;QAAU;IACrB;IAEA,MAAME,WAAWH,KAAKI,IAAI,CAACT,sBAAsB,YAAY;IAC7D,IAAI,CAACQ,SAASE,QAAQ,CAACT,OAAO;QAC5B,+CAA+C;QAC/C,OAAO;YAAEK;QAAU;IACrB;IAEA,MAAMK,UAAU,MAAMf,YACpBS,MACAL,sBACAE;IAGF,IAAI,CAACS,SAAS;QACZL,UAAUM,IAAI,CAACT;QACf,OAAO;YAAEG;QAAU;IACrB;IAEA,MAAMT,qBAAqBQ;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
@@ -55,7 +55,6 @@ export default async function migration(tree) {
|
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
tree.write(filePath, contents.replace(OLD_CAST_OPEN, NEW_CAST_OPEN).replace(OLD_CAST_CLOSE, NEW_CAST_CLOSE));
|
|
58
|
-
nextSteps.push(`${filePath}: replaced the legacy <FunctionProps> cast with the modern as FunctionProps syntax.`);
|
|
59
58
|
}
|
|
60
59
|
await formatFilesInSubtree(tree);
|
|
61
60
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/modernize-function-props-cast/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type MigrationReturnObject,\n type Tree,\n visitNotIgnoredFiles,\n} from '@nx/devkit';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Replace the legacy angle-bracket FunctionProps type assertion with the modern as syntax in generated API constructs\n *\n * The legacy `<FunctionProps>{ ... }` cast form is not supported by GritQL's\n * TypeScript parser, which silently breaks parsing (and therefore matching)\n * for the entire containing file. Since several other migrations rely on\n * GritQL to update these API construct files, this cast is replaced here with\n * the equivalent, GritQL-compatible `{ ... } as FunctionProps` form. This is\n * matched as an exact literal (rather than via GritQL) since GritQL cannot\n * parse the file while the legacy cast is still present.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, or consider a\n * hybrid migration, rather than clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;\n\nconst OLD_CAST_OPEN = 'defaultIntegrationOptions: <FunctionProps>{';\nconst NEW_CAST_OPEN = 'defaultIntegrationOptions: {';\nconst OLD_CAST_CLOSE = '\\n },\\n buildDefaultIntegration:';\nconst NEW_CAST_CLOSE =\n '\\n } as FunctionProps,\\n buildDefaultIntegration:';\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(APIS_APP_DIR)) {\n return { nextSteps };\n }\n\n const apiAppFiles: string[] = [];\n visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath) => {\n apiAppFiles.push(filePath);\n });\n\n for (const filePath of apiAppFiles) {\n if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {\n continue;\n }\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (!contents.includes(OLD_CAST_OPEN)) {\n // Already migrated, or doesn't use this shape.\n continue;\n }\n if (!contents.includes(OLD_CAST_CLOSE)) {\n nextSteps.push(\n `${filePath}: the defaultIntegrationOptions cast has diverged from the generated shape - left untouched. Manually replace \\`<FunctionProps>{ ... }\\` with \\`{ ... } as FunctionProps\\`.`,\n );\n continue;\n }\n tree.write(\n filePath,\n contents\n .replace(OLD_CAST_OPEN, NEW_CAST_OPEN)\n .replace(OLD_CAST_CLOSE, NEW_CAST_CLOSE),\n );\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/modernize-function-props-cast/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type MigrationReturnObject,\n type Tree,\n visitNotIgnoredFiles,\n} from '@nx/devkit';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Replace the legacy angle-bracket FunctionProps type assertion with the modern as syntax in generated API constructs\n *\n * The legacy `<FunctionProps>{ ... }` cast form is not supported by GritQL's\n * TypeScript parser, which silently breaks parsing (and therefore matching)\n * for the entire containing file. Since several other migrations rely on\n * GritQL to update these API construct files, this cast is replaced here with\n * the equivalent, GritQL-compatible `{ ... } as FunctionProps` form. This is\n * matched as an exact literal (rather than via GritQL) since GritQL cannot\n * parse the file while the legacy cast is still present.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, or consider a\n * hybrid migration, rather than clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;\n\nconst OLD_CAST_OPEN = 'defaultIntegrationOptions: <FunctionProps>{';\nconst NEW_CAST_OPEN = 'defaultIntegrationOptions: {';\nconst OLD_CAST_CLOSE = '\\n },\\n buildDefaultIntegration:';\nconst NEW_CAST_CLOSE =\n '\\n } as FunctionProps,\\n buildDefaultIntegration:';\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(APIS_APP_DIR)) {\n return { nextSteps };\n }\n\n const apiAppFiles: string[] = [];\n visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath) => {\n apiAppFiles.push(filePath);\n });\n\n for (const filePath of apiAppFiles) {\n if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {\n continue;\n }\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (!contents.includes(OLD_CAST_OPEN)) {\n // Already migrated, or doesn't use this shape.\n continue;\n }\n if (!contents.includes(OLD_CAST_CLOSE)) {\n nextSteps.push(\n `${filePath}: the defaultIntegrationOptions cast has diverged from the generated shape - left untouched. Manually replace \\`<FunctionProps>{ ... }\\` with \\`{ ... } as FunctionProps\\`.`,\n );\n continue;\n }\n tree.write(\n filePath,\n contents\n .replace(OLD_CAST_OPEN, NEW_CAST_OPEN)\n .replace(OLD_CAST_CLOSE, NEW_CAST_CLOSE),\n );\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["visitNotIgnoredFiles","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","APIS_APP_DIR","OLD_CAST_OPEN","NEW_CAST_OPEN","OLD_CAST_CLOSE","NEW_CAST_CLOSE","migration","tree","nextSteps","exists","apiAppFiles","filePath","push","endsWith","contents","read","includes","write","replace"],"mappings":"AAAA;;;CAGC,GACD,SAGEA,oBAAoB,QACf,aAAa;AACpB,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SACEC,YAAY,EACZC,qBAAqB,QAChB,gDAA6C;AAEpD;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,MAAMC,eAAe,GAAGF,aAAa,CAAC,EAAEC,sBAAsB,aAAa,CAAC;AAE5E,MAAME,gBAAgB;AACtB,MAAMC,gBAAgB;AACtB,MAAMC,iBAAiB;AACvB,MAAMC,iBACJ;AAEF,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,IAAI,CAACD,KAAKE,MAAM,CAACR,eAAe;QAC9B,OAAO;YAAEO;QAAU;IACrB;IAEA,MAAME,cAAwB,EAAE;IAChCb,qBAAqBU,MAAMN,cAAc,CAACU;QACxCD,YAAYE,IAAI,CAACD;IACnB;IAEA,KAAK,MAAMA,YAAYD,YAAa;QAClC,IAAI,CAACC,SAASE,QAAQ,CAAC,UAAUF,SAASE,QAAQ,CAAC,cAAc;YAC/D;QACF;QACA,MAAMC,WAAWP,KAAKQ,IAAI,CAACJ,UAAU,YAAY;QACjD,IAAI,CAACG,SAASE,QAAQ,CAACd,gBAAgB;YAErC;QACF;QACA,IAAI,CAACY,SAASE,QAAQ,CAACZ,iBAAiB;YACtCI,UAAUI,IAAI,CACZ,GAAGD,SAAS,2KAA2K,CAAC;YAE1L;QACF;QACAJ,KAAKU,KAAK,CACRN,UACAG,SACGI,OAAO,CAAChB,eAAeC,eACvBe,OAAO,CAACd,gBAAgBC;IAE/B;IAEA,MAAMP,qBAAqBS;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
@@ -84,7 +84,6 @@ export default async function migration(tree) {
|
|
|
84
84
|
await addDestructuredImport(tree, STATIC_WEBSITE_FILE, [
|
|
85
85
|
'Bucket'
|
|
86
86
|
], 'aws-cdk-lib/aws-s3');
|
|
87
|
-
nextSteps.push(`${STATIC_WEBSITE_FILE}: the S3 server access log delivery source is now created after the policy of the bucket it targets, avoiding a 409 (OperationAborted) from concurrent bucket configuration writes.`);
|
|
88
87
|
await formatFilesInSubtree(tree);
|
|
89
88
|
return {
|
|
90
89
|
nextSteps
|
package/src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport {\n addDestructuredImport,\n applyGritQL,\n captureGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Order the S3 server access log delivery source after the bucket policy.\n *\n * S3 rejects concurrent configuration writes against the same bucket with a 409\n * (OperationAborted). The generated StaticWebsite construct left the delivery\n * source and the bucket policy unordered, so CloudFormation could submit both at\n * once and fail the stack.\n */\n\nconst STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/static-website.ts`;\n\nconst BUCKET_ARN_SUFFIX = '.bucketArn';\n\n/**\n * The bucket whose policy the delivery source must be ordered after is the one\n * the delivery source itself targets, so read it off `resourceArn` rather than\n * assuming the generated parameter name. Returns undefined unless exactly one\n * simple identifier is found, so a diverged helper is left alone.\n */\nconst findDeliverySourceBucket = async (\n tree: Tree,\n filePath: string,\n): Promise<string | undefined> => {\n const captured = await captureGritQL(\n tree,\n filePath,\n '`resourceArn: $bucket.bucketArn`',\n );\n if (!captured) return undefined;\n\n const bucket = captured\n .slice(captured.indexOf(':') + 1)\n .replace(BUCKET_ARN_SUFFIX, '')\n .trim();\n\n // Only a plain identifier can be cast and dereferenced safely in the\n // statement this migration writes.\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(bucket) ? bucket : undefined;\n};\n\n/**\n * Confirm the bucket the delivery source targets is declared as a parameter of\n * the enclosing helper, so the statement this migration writes references a\n * variable that is actually in scope.\n */\nconst isBucketDeclaredParameter = async (\n tree: Tree,\n filePath: string,\n bucket: string,\n): Promise<boolean> =>\n await matchGritQL(\n tree,\n filePath,\n `\\`private deliverAccessLogsToCloudWatch($params) { $_ }\\` where {\n $params <: contains \\`${bucket}: $bucketType\\`,\n $bucketType <: or { \\`IBucket\\`, \\`Bucket\\` }\n }`,\n );\n\n// Inserts the bucket policy dependency between the delivery source and the\n// delivery destination, matching the shape generators produced prior to this\n// fix. Anchored on the destination declaration so the source's own (multi-line,\n// Lazy-valued) arguments don't need to be matched.\nconst addDependencyPattern = (bucket: string) =>\n `\\`const $dest: CfnDeliveryDestination = new CfnDeliveryDestination($destArgs)\\` as $decl where {\n $dest <: \\`destination\\`\n} => \\`const bucketPolicy = (${bucket} as Bucket).policy;\n if (bucketPolicy) {\n source.node.addDependency(bucketPolicy);\n }\n $decl\\``;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(STATIC_WEBSITE_FILE)) {\n // No vended StaticWebsite construct in this workspace - nothing to migrate.\n return { nextSteps };\n }\n\n const contents = tree.read(STATIC_WEBSITE_FILE, 'utf-8') ?? '';\n if (contents.includes('source.node.addDependency(bucketPolicy)')) {\n // Already migrated.\n return { nextSteps };\n }\n\n const divergedMessage = `${STATIC_WEBSITE_FILE}: deliverAccessLogsToCloudWatch has diverged from the generated shape - left untouched. Manually order the S3 server access log delivery source after the bucket policy of the bucket it targets (\\`source.node.addDependency(bucketPolicy)\\`), avoiding a 409 (OperationAborted) from concurrent bucket configuration writes.`;\n\n // The rewrite is anchored on the delivery destination but references the\n // `source` variable, so only apply it when the delivery source still has the\n // generated shape.\n const hasGeneratedSource = await matchGritQL(\n tree,\n STATIC_WEBSITE_FILE,\n '`const source: CfnDeliverySource = new CfnDeliverySource($_)`',\n );\n\n const bucket = hasGeneratedSource\n ? await findDeliverySourceBucket(tree, STATIC_WEBSITE_FILE)\n : undefined;\n\n if (\n !bucket ||\n !(await isBucketDeclaredParameter(tree, STATIC_WEBSITE_FILE, bucket))\n ) {\n nextSteps.push(divergedMessage);\n return { nextSteps };\n }\n\n const rewrote = await applyGritQL(\n tree,\n STATIC_WEBSITE_FILE,\n addDependencyPattern(bucket),\n );\n\n if (!rewrote) {\n nextSteps.push(divergedMessage);\n return { nextSteps };\n }\n\n // The inserted statement casts to the concrete Bucket to reach its policy,\n // which the helper's own `IBucket` parameter type does not expose.\n await addDestructuredImport(\n tree,\n STATIC_WEBSITE_FILE,\n ['Bucket'],\n 'aws-cdk-lib/aws-s3',\n );\n\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport {\n addDestructuredImport,\n applyGritQL,\n captureGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Order the S3 server access log delivery source after the bucket policy.\n *\n * S3 rejects concurrent configuration writes against the same bucket with a 409\n * (OperationAborted). The generated StaticWebsite construct left the delivery\n * source and the bucket policy unordered, so CloudFormation could submit both at\n * once and fail the stack.\n */\n\nconst STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/static-website.ts`;\n\nconst BUCKET_ARN_SUFFIX = '.bucketArn';\n\n/**\n * The bucket whose policy the delivery source must be ordered after is the one\n * the delivery source itself targets, so read it off `resourceArn` rather than\n * assuming the generated parameter name. Returns undefined unless exactly one\n * simple identifier is found, so a diverged helper is left alone.\n */\nconst findDeliverySourceBucket = async (\n tree: Tree,\n filePath: string,\n): Promise<string | undefined> => {\n const captured = await captureGritQL(\n tree,\n filePath,\n '`resourceArn: $bucket.bucketArn`',\n );\n if (!captured) return undefined;\n\n const bucket = captured\n .slice(captured.indexOf(':') + 1)\n .replace(BUCKET_ARN_SUFFIX, '')\n .trim();\n\n // Only a plain identifier can be cast and dereferenced safely in the\n // statement this migration writes.\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(bucket) ? bucket : undefined;\n};\n\n/**\n * Confirm the bucket the delivery source targets is declared as a parameter of\n * the enclosing helper, so the statement this migration writes references a\n * variable that is actually in scope.\n */\nconst isBucketDeclaredParameter = async (\n tree: Tree,\n filePath: string,\n bucket: string,\n): Promise<boolean> =>\n await matchGritQL(\n tree,\n filePath,\n `\\`private deliverAccessLogsToCloudWatch($params) { $_ }\\` where {\n $params <: contains \\`${bucket}: $bucketType\\`,\n $bucketType <: or { \\`IBucket\\`, \\`Bucket\\` }\n }`,\n );\n\n// Inserts the bucket policy dependency between the delivery source and the\n// delivery destination, matching the shape generators produced prior to this\n// fix. Anchored on the destination declaration so the source's own (multi-line,\n// Lazy-valued) arguments don't need to be matched.\nconst addDependencyPattern = (bucket: string) =>\n `\\`const $dest: CfnDeliveryDestination = new CfnDeliveryDestination($destArgs)\\` as $decl where {\n $dest <: \\`destination\\`\n} => \\`const bucketPolicy = (${bucket} as Bucket).policy;\n if (bucketPolicy) {\n source.node.addDependency(bucketPolicy);\n }\n $decl\\``;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(STATIC_WEBSITE_FILE)) {\n // No vended StaticWebsite construct in this workspace - nothing to migrate.\n return { nextSteps };\n }\n\n const contents = tree.read(STATIC_WEBSITE_FILE, 'utf-8') ?? '';\n if (contents.includes('source.node.addDependency(bucketPolicy)')) {\n // Already migrated.\n return { nextSteps };\n }\n\n const divergedMessage = `${STATIC_WEBSITE_FILE}: deliverAccessLogsToCloudWatch has diverged from the generated shape - left untouched. Manually order the S3 server access log delivery source after the bucket policy of the bucket it targets (\\`source.node.addDependency(bucketPolicy)\\`), avoiding a 409 (OperationAborted) from concurrent bucket configuration writes.`;\n\n // The rewrite is anchored on the delivery destination but references the\n // `source` variable, so only apply it when the delivery source still has the\n // generated shape.\n const hasGeneratedSource = await matchGritQL(\n tree,\n STATIC_WEBSITE_FILE,\n '`const source: CfnDeliverySource = new CfnDeliverySource($_)`',\n );\n\n const bucket = hasGeneratedSource\n ? await findDeliverySourceBucket(tree, STATIC_WEBSITE_FILE)\n : undefined;\n\n if (\n !bucket ||\n !(await isBucketDeclaredParameter(tree, STATIC_WEBSITE_FILE, bucket))\n ) {\n nextSteps.push(divergedMessage);\n return { nextSteps };\n }\n\n const rewrote = await applyGritQL(\n tree,\n STATIC_WEBSITE_FILE,\n addDependencyPattern(bucket),\n );\n\n if (!rewrote) {\n nextSteps.push(divergedMessage);\n return { nextSteps };\n }\n\n // The inserted statement casts to the concrete Bucket to reach its policy,\n // which the helper's own `IBucket` parameter type does not expose.\n await addDestructuredImport(\n tree,\n STATIC_WEBSITE_FILE,\n ['Bucket'],\n 'aws-cdk-lib/aws-s3',\n );\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["addDestructuredImport","applyGritQL","captureGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","STATIC_WEBSITE_FILE","BUCKET_ARN_SUFFIX","findDeliverySourceBucket","tree","filePath","captured","undefined","bucket","slice","indexOf","replace","trim","test","isBucketDeclaredParameter","addDependencyPattern","migration","nextSteps","exists","contents","read","includes","divergedMessage","hasGeneratedSource","push","rewrote"],"mappings":"AAAA;;;CAGC,GAED,SACEA,qBAAqB,EACrBC,WAAW,EACXC,aAAa,EACbC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SACEC,YAAY,EACZC,qBAAqB,QAChB,gDAA6C;AAEpD;;;;;;;CAOC,GAED,MAAMC,sBAAsB,GAAGF,aAAa,CAAC,EAAEC,sBAAsB,2BAA2B,CAAC;AAEjG,MAAME,oBAAoB;AAE1B;;;;;CAKC,GACD,MAAMC,2BAA2B,OAC/BC,MACAC;IAEA,MAAMC,WAAW,MAAMV,cACrBQ,MACAC,UACA;IAEF,IAAI,CAACC,UAAU,OAAOC;IAEtB,MAAMC,SAASF,SACZG,KAAK,CAACH,SAASI,OAAO,CAAC,OAAO,GAC9BC,OAAO,CAACT,mBAAmB,IAC3BU,IAAI;IAEP,qEAAqE;IACrE,mCAAmC;IACnC,OAAO,6BAA6BC,IAAI,CAACL,UAAUA,SAASD;AAC9D;AAEA;;;;CAIC,GACD,MAAMO,4BAA4B,OAChCV,MACAC,UACAG,SAEA,MAAMX,YACJO,MACAC,UACA,CAAC;4BACuB,EAAEG,OAAO;;KAEhC,CAAC;AAGN,2EAA2E;AAC3E,6EAA6E;AAC7E,gFAAgF;AAChF,mDAAmD;AACnD,MAAMO,uBAAuB,CAACP,SAC5B,CAAC;;6BAE0B,EAAEA,OAAO;;;;WAI3B,CAAC;AAEZ,eAAe,eAAeQ,UAC5BZ,IAAU;IAEV,MAAMa,YAAsB,EAAE;IAE9B,IAAI,CAACb,KAAKc,MAAM,CAACjB,sBAAsB;QACrC,4EAA4E;QAC5E,OAAO;YAAEgB;QAAU;IACrB;IAEA,MAAME,WAAWf,KAAKgB,IAAI,CAACnB,qBAAqB,YAAY;IAC5D,IAAIkB,SAASE,QAAQ,CAAC,4CAA4C;QAChE,oBAAoB;QACpB,OAAO;YAAEJ;QAAU;IACrB;IAEA,MAAMK,kBAAkB,GAAGrB,oBAAoB,8TAA8T,CAAC;IAE9W,yEAAyE;IACzE,6EAA6E;IAC7E,mBAAmB;IACnB,MAAMsB,qBAAqB,MAAM1B,YAC/BO,MACAH,qBACA;IAGF,MAAMO,SAASe,qBACX,MAAMpB,yBAAyBC,MAAMH,uBACrCM;IAEJ,IACE,CAACC,UACD,CAAE,MAAMM,0BAA0BV,MAAMH,qBAAqBO,SAC7D;QACAS,UAAUO,IAAI,CAACF;QACf,OAAO;YAAEL;QAAU;IACrB;IAEA,MAAMQ,UAAU,MAAM9B,YACpBS,MACAH,qBACAc,qBAAqBP;IAGvB,IAAI,CAACiB,SAAS;QACZR,UAAUO,IAAI,CAACF;QACf,OAAO;YAAEL;QAAU;IACrB;IAEA,2EAA2E;IAC3E,mEAAmE;IACnE,MAAMvB,sBACJU,MACAH,qBACA;QAAC;KAAS,EACV;IAGF,MAAMH,qBAAqBM;IAE3B,OAAO;QAAEa;IAAU;AACrB"}
|
|
@@ -75,7 +75,6 @@ export default async function migration(tree) {
|
|
|
75
75
|
await addDestructuredImport(tree, filePath, [
|
|
76
76
|
'findCloudFrontDomainNames'
|
|
77
77
|
], apiCloudFrontImportSpecifier);
|
|
78
|
-
nextSteps.push(`${filePath}: restrictCorsTo now includes CloudFront custom domain aliases automatically.`);
|
|
79
78
|
continue;
|
|
80
79
|
}
|
|
81
80
|
const contents = tree.read(filePath, 'utf-8') ?? '';
|
|
@@ -92,7 +91,6 @@ export default async function migration(tree) {
|
|
|
92
91
|
await addDestructuredImport(tree, USER_IDENTITY_FILE, [
|
|
93
92
|
'findCloudFrontDomainNames'
|
|
94
93
|
], coreCloudFrontImportSpecifier);
|
|
95
|
-
nextSteps.push(`${USER_IDENTITY_FILE}: now reuses the shared findCloudFrontDomainNames helper.`);
|
|
96
94
|
} else {
|
|
97
95
|
const contents = tree.read(USER_IDENTITY_FILE, 'utf-8') ?? '';
|
|
98
96
|
if (!contents.includes('.flatMap(findCloudFrontDomainNames)')) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/restrict-cors-to-custom-domains/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type MigrationReturnObject,\n type Tree,\n visitNotIgnoredFiles,\n} from '@nx/devkit';\nimport {\n addDestructuredImport,\n addStarExport,\n applyGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport { isEsmWorkspace } from '../../../utils/module-format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Include CloudFront custom domain aliases in restrictCorsTo and UserIdentity callback URLs\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CORE_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core`;\nconst APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;\nconst CLOUDFRONT_CORE_FILE = `${CORE_DIR}/cloudfront.ts`;\nconst CORE_INDEX_FILE = `${CORE_DIR}/index.ts`;\nconst USER_IDENTITY_FILE = `${CORE_DIR}/user-identity.ts`;\n\nconst CLOUDFRONT_HELPER_CONTENT = `import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront';\n\n/**\n * Finds the domain names associated with a CloudFront distribution.\n *\n * Includes the distribution's default \\`*.cloudfront.net\\` domain name plus any custom\n * domain names (aliases) configured on it.\n */\nexport const findCloudFrontDomainNames = (\n distribution: Distribution,\n): string[] => {\n const cfnDistribution = distribution.node.defaultChild as CfnDistribution;\n const distributionConfig =\n cfnDistribution.distributionConfig as CfnDistribution.DistributionConfigProperty;\n return [distribution.domainName, ...(distributionConfig.aliases ?? [])];\n};\n`;\n\n// Rewrites the `restrictCorsTo` body as produced by generators prior to this fix.\nconst RESTRICT_CORS_TO_GRITQL_PATTERN =\n \"`origins.map(($o) => typeof $o === 'string' ? $o : 'cloudFrontDistribution' in $o ? $branch1 : $branch2)` where { $branch1 <: contains `distributionDomainName`, $branch2 <: contains `distributionDomainName` } => raw`origins.flatMap(($o) => typeof $o === 'string' ? [$o] : findCloudFrontDomainNames('cloudFrontDistribution' in $o ? $o.cloudFrontDistribution : $o).map((domain) => \\\\`https://${domain}\\\\`))`\";\n\n// Rewrites UserIdentity's callback/logout URL logic as produced by generators\n// prior to this fix.\nconst USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN =\n '`this.findCloudFrontDomainNames()` => `Stack.of(this).node.findAll().filter((child): child is Distribution => child instanceof Distribution).flatMap(findCloudFrontDomainNames)`';\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(CORE_INDEX_FILE)) {\n // No common/constructs shared library in this workspace - nothing to migrate.\n return { nextSteps };\n }\n\n const esm = isEsmWorkspace(tree);\n const apiCloudFrontImportSpecifier = esm\n ? '../../core/cloudfront.js'\n : '../../core/cloudfront';\n const coreCloudFrontImportSpecifier = esm\n ? './cloudfront.js'\n : './cloudfront';\n\n if (!tree.exists(CLOUDFRONT_CORE_FILE)) {\n tree.write(CLOUDFRONT_CORE_FILE, CLOUDFRONT_HELPER_CONTENT);\n }\n await addStarExport(tree, CORE_INDEX_FILE, './cloudfront.js');\n\n const apiAppFiles: string[] = [];\n visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath) => {\n apiAppFiles.push(filePath);\n });\n\n for (const filePath of apiAppFiles) {\n if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {\n continue;\n }\n const rewrote = await applyGritQL(\n tree,\n filePath,\n RESTRICT_CORS_TO_GRITQL_PATTERN,\n );\n if (rewrote) {\n await addDestructuredImport(\n tree,\n filePath,\n ['findCloudFrontDomainNames'],\n apiCloudFrontImportSpecifier,\n );\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/restrict-cors-to-custom-domains/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type MigrationReturnObject,\n type Tree,\n visitNotIgnoredFiles,\n} from '@nx/devkit';\nimport {\n addDestructuredImport,\n addStarExport,\n applyGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport { isEsmWorkspace } from '../../../utils/module-format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Include CloudFront custom domain aliases in restrictCorsTo and UserIdentity callback URLs\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CORE_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core`;\nconst APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;\nconst CLOUDFRONT_CORE_FILE = `${CORE_DIR}/cloudfront.ts`;\nconst CORE_INDEX_FILE = `${CORE_DIR}/index.ts`;\nconst USER_IDENTITY_FILE = `${CORE_DIR}/user-identity.ts`;\n\nconst CLOUDFRONT_HELPER_CONTENT = `import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront';\n\n/**\n * Finds the domain names associated with a CloudFront distribution.\n *\n * Includes the distribution's default \\`*.cloudfront.net\\` domain name plus any custom\n * domain names (aliases) configured on it.\n */\nexport const findCloudFrontDomainNames = (\n distribution: Distribution,\n): string[] => {\n const cfnDistribution = distribution.node.defaultChild as CfnDistribution;\n const distributionConfig =\n cfnDistribution.distributionConfig as CfnDistribution.DistributionConfigProperty;\n return [distribution.domainName, ...(distributionConfig.aliases ?? [])];\n};\n`;\n\n// Rewrites the `restrictCorsTo` body as produced by generators prior to this fix.\nconst RESTRICT_CORS_TO_GRITQL_PATTERN =\n \"`origins.map(($o) => typeof $o === 'string' ? $o : 'cloudFrontDistribution' in $o ? $branch1 : $branch2)` where { $branch1 <: contains `distributionDomainName`, $branch2 <: contains `distributionDomainName` } => raw`origins.flatMap(($o) => typeof $o === 'string' ? [$o] : findCloudFrontDomainNames('cloudFrontDistribution' in $o ? $o.cloudFrontDistribution : $o).map((domain) => \\\\`https://${domain}\\\\`))`\";\n\n// Rewrites UserIdentity's callback/logout URL logic as produced by generators\n// prior to this fix.\nconst USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN =\n '`this.findCloudFrontDomainNames()` => `Stack.of(this).node.findAll().filter((child): child is Distribution => child instanceof Distribution).flatMap(findCloudFrontDomainNames)`';\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(CORE_INDEX_FILE)) {\n // No common/constructs shared library in this workspace - nothing to migrate.\n return { nextSteps };\n }\n\n const esm = isEsmWorkspace(tree);\n const apiCloudFrontImportSpecifier = esm\n ? '../../core/cloudfront.js'\n : '../../core/cloudfront';\n const coreCloudFrontImportSpecifier = esm\n ? './cloudfront.js'\n : './cloudfront';\n\n if (!tree.exists(CLOUDFRONT_CORE_FILE)) {\n tree.write(CLOUDFRONT_CORE_FILE, CLOUDFRONT_HELPER_CONTENT);\n }\n await addStarExport(tree, CORE_INDEX_FILE, './cloudfront.js');\n\n const apiAppFiles: string[] = [];\n visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath) => {\n apiAppFiles.push(filePath);\n });\n\n for (const filePath of apiAppFiles) {\n if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {\n continue;\n }\n const rewrote = await applyGritQL(\n tree,\n filePath,\n RESTRICT_CORS_TO_GRITQL_PATTERN,\n );\n if (rewrote) {\n await addDestructuredImport(\n tree,\n filePath,\n ['findCloudFrontDomainNames'],\n apiCloudFrontImportSpecifier,\n );\n continue;\n }\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (!contents.includes('findCloudFrontDomainNames(')) {\n nextSteps.push(\n `${filePath}: restrictCorsTo has diverged from the generated shape - left untouched. Manually apply the CloudFront custom domain fix (see findCloudFrontDomainNames in common/constructs/src/core/cloudfront.ts).`,\n );\n }\n // Otherwise already migrated - silent skip.\n }\n\n if (tree.exists(USER_IDENTITY_FILE)) {\n const rewrote = await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN,\n );\n if (rewrote) {\n await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n \"`import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront'` => `import { Distribution } from 'aws-cdk-lib/aws-cloudfront';`\",\n );\n await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n \"or { `// Includes each distribution's default domain name plus any custom domain names (aliases) configured on it.` => ., `private findCloudFrontDomainNames = (): string[] => $body` => . }\",\n );\n await addDestructuredImport(\n tree,\n USER_IDENTITY_FILE,\n ['findCloudFrontDomainNames'],\n coreCloudFrontImportSpecifier,\n );\n } else {\n const contents = tree.read(USER_IDENTITY_FILE, 'utf-8') ?? '';\n if (!contents.includes('.flatMap(findCloudFrontDomainNames)')) {\n nextSteps.push(\n `${USER_IDENTITY_FILE}: the callback URL logic has diverged from the generated shape - left untouched. Manually apply the shared findCloudFrontDomainNames helper (see common/constructs/src/core/cloudfront.ts).`,\n );\n }\n // Otherwise already migrated - silent skip.\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["visitNotIgnoredFiles","addDestructuredImport","addStarExport","applyGritQL","formatFilesInSubtree","isEsmWorkspace","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","CORE_DIR","APIS_APP_DIR","CLOUDFRONT_CORE_FILE","CORE_INDEX_FILE","USER_IDENTITY_FILE","CLOUDFRONT_HELPER_CONTENT","RESTRICT_CORS_TO_GRITQL_PATTERN","USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN","migration","tree","nextSteps","exists","esm","apiCloudFrontImportSpecifier","coreCloudFrontImportSpecifier","write","apiAppFiles","filePath","push","endsWith","rewrote","contents","read","includes"],"mappings":"AAAA;;;CAGC,GACD,SAGEA,oBAAoB,QACf,aAAa;AACpB,SACEC,qBAAqB,EACrBC,aAAa,EACbC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SAASC,cAAc,QAAQ,kCAA+B;AAC9D,SACEC,YAAY,EACZC,qBAAqB,QAChB,gDAA6C;AAEpD;;;;;;;;;;;;;;CAcC,GAED,MAAMC,WAAW,GAAGF,aAAa,CAAC,EAAEC,sBAAsB,SAAS,CAAC;AACpE,MAAME,eAAe,GAAGH,aAAa,CAAC,EAAEC,sBAAsB,aAAa,CAAC;AAC5E,MAAMG,uBAAuB,GAAGF,SAAS,cAAc,CAAC;AACxD,MAAMG,kBAAkB,GAAGH,SAAS,SAAS,CAAC;AAC9C,MAAMI,qBAAqB,GAAGJ,SAAS,iBAAiB,CAAC;AAEzD,MAAMK,4BAA4B,CAAC;;;;;;;;;;;;;;;;AAgBnC,CAAC;AAED,kFAAkF;AAClF,MAAMC,kCACJ;AAEF,8EAA8E;AAC9E,qBAAqB;AACrB,MAAMC,6CACJ;AAEF,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,IAAI,CAACD,KAAKE,MAAM,CAACR,kBAAkB;QACjC,8EAA8E;QAC9E,OAAO;YAAEO;QAAU;IACrB;IAEA,MAAME,MAAMf,eAAeY;IAC3B,MAAMI,+BAA+BD,MACjC,6BACA;IACJ,MAAME,gCAAgCF,MAClC,oBACA;IAEJ,IAAI,CAACH,KAAKE,MAAM,CAACT,uBAAuB;QACtCO,KAAKM,KAAK,CAACb,sBAAsBG;IACnC;IACA,MAAMX,cAAce,MAAMN,iBAAiB;IAE3C,MAAMa,cAAwB,EAAE;IAChCxB,qBAAqBiB,MAAMR,cAAc,CAACgB;QACxCD,YAAYE,IAAI,CAACD;IACnB;IAEA,KAAK,MAAMA,YAAYD,YAAa;QAClC,IAAI,CAACC,SAASE,QAAQ,CAAC,UAAUF,SAASE,QAAQ,CAAC,cAAc;YAC/D;QACF;QACA,MAAMC,UAAU,MAAMzB,YACpBc,MACAQ,UACAX;QAEF,IAAIc,SAAS;YACX,MAAM3B,sBACJgB,MACAQ,UACA;gBAAC;aAA4B,EAC7BJ;YAEF;QACF;QACA,MAAMQ,WAAWZ,KAAKa,IAAI,CAACL,UAAU,YAAY;QACjD,IAAI,CAACI,SAASE,QAAQ,CAAC,+BAA+B;YACpDb,UAAUQ,IAAI,CACZ,GAAGD,SAAS,qMAAqM,CAAC;QAEtN;IACA,4CAA4C;IAC9C;IAEA,IAAIR,KAAKE,MAAM,CAACP,qBAAqB;QACnC,MAAMgB,UAAU,MAAMzB,YACpBc,MACAL,oBACAG;QAEF,IAAIa,SAAS;YACX,MAAMzB,YACJc,MACAL,oBACA;YAEF,MAAMT,YACJc,MACAL,oBACA;YAEF,MAAMX,sBACJgB,MACAL,oBACA;gBAAC;aAA4B,EAC7BU;QAEJ,OAAO;YACL,MAAMO,WAAWZ,KAAKa,IAAI,CAAClB,oBAAoB,YAAY;YAC3D,IAAI,CAACiB,SAASE,QAAQ,CAAC,wCAAwC;gBAC7Db,UAAUQ,IAAI,CACZ,GAAGd,mBAAmB,2LAA2L,CAAC;YAEtN;QACA,4CAA4C;QAC9C;IACF;IAEA,MAAMR,qBAAqBa;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
@@ -118,7 +118,6 @@ export default async function migration(tree) {
|
|
|
118
118
|
// migrated workspace matches a freshly generated one.
|
|
119
119
|
await applyGritQL(tree, filePath, `${S3_IMPORT_PATTERN} => \`import { $before, GetObjectCommand, HeadBucketCommand, $after } from '@aws-sdk/client-s3'\``);
|
|
120
120
|
await applyGritQL(tree, filePath, `${HEADER_COMMENT_PATTERN} => \`${NEW_HEADER_COMMENT}\``);
|
|
121
|
-
nextSteps.push(`${filePath}: now adopts an existing Terraform state bucket when its bootstrap.tfstate object is missing.`);
|
|
122
121
|
}
|
|
123
122
|
await formatFilesInSubtree(tree);
|
|
124
123
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type Tree,\n} from '@nx/devkit';\nimport { TERRAFORM_PROJECT_GENERATOR_INFO } from '../../../terraform/project/generator';\nimport {\n applyGritQL,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\n\n/**\n * Adopt an existing Terraform state bucket in the vended bootstrap script when its state object is missing\n *\n * The state bucket stores its own tfstate inside itself, so if that object is\n * deleted while the bucket survives, `bootstrap` reads the 404 as a first run\n * and asks terraform to create a bucket that already exists — failing with\n * `BucketAlreadyOwnedByYou` on every subsequent run, with no retry that clears\n * it. The vended script now imports the existing bucket instead.\n *\n * Every edit is expressed as a GritQL rewrite so the script is matched on its\n * AST rather than its formatting.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, or consider a\n * hybrid migration, rather than clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\n// Guards the whole migration: present only once the import step exists.\nconst MIGRATED_PATTERN = '`bucketExists($_, $_)`';\n\n// Every edit site, matched structurally so formatting and argument layout\n// don't affect whether the script is recognised.\nconst STATE_FETCH_TRY_PATTERN =\n 'try_statement() as $try where { $try <: contains `GetObjectCommand` }';\nconst STATE_FLAG_SET_PATTERN = '`if (out.Body) { $body }`';\nconst HELPER_PATTERN = '`const main = async () => { $body }`';\nconst INIT_PATTERN = \"`execFileSync('terraform', ['init'], $opts)`\";\nconst S3_IMPORT_PATTERN =\n \"`import { $before, GetObjectCommand, $after } from '@aws-sdk/client-s3'`\";\nconst HEADER_COMMENT_PATTERN =\n 'comment() as $c where { $c <: includes \"state back to S3.\" }';\n\n// GritQL snippets are parsed as backtick-quoted patterns, so any backtick in\n// the inserted source has to survive that layer escaped.\nconst BUCKET_EXISTS_HELPER = `// S3 answers 404 only when the name is genuinely free; 403 means it exists\n// but is not readable with these credentials.\nconst bucketExists = async (s3: S3Client, bucket: string) => {\n try {\n await s3.send(new HeadBucketCommand({ Bucket: bucket }));\n return true;\n } catch (err: any) {\n const status = err?.$metadata?.httpStatusCode;\n if (status === 404) return false;\n if (status === 403) return true;\n throw err;\n }\n};`;\n\nconst IMPORT_STEP = `// The bucket holds its own state, so losing the state object while the\n // bucket survives would otherwise wedge bootstrap on a permanent\n // \\`BucketAlreadyOwnedByYou\\`. Adopt the existing bucket instead.\n if (!haveState && (await bucketExists(s3, bucket))) {\n console.log(\n \\`State bucket \\${bucket} already exists but its bootstrap state is missing — importing it.\\`,\n );\n execFileSync(\n 'terraform',\n [\n 'import',\n \\`-state=\\${tfStatePath}\\`,\n \\`-var=aws_region=\\${region}\\`,\n 'aws_s3_bucket.terraform_state',\n bucket,\n ],\n { cwd: bootstrapDir, stdio: 'inherit' },\n );\n }`;\n\n// Rewritten wholesale rather than appended to, since the summary sentence\n// changes rather than gaining a clause.\nconst NEW_HEADER_COMMENT = `/**\n * Bootstraps the remote Terraform state bucket.\n *\n * Equivalent to \\\\\\`cdk bootstrap\\\\\\` — resolves account + region from the AWS\n * SDK credential chain, pulls any existing bootstrap tfstate from S3,\n * runs \\\\\\`terraform apply\\\\\\` in the \\\\\\`bootstrap\\\\\\` dir, then pushes the new\n * state back to S3. Adopts an already-existing state bucket when its\n * state object is missing.\n */`;\n\nconst DIVERGED_NEXT_STEP = (filePath: string) =>\n `${filePath}: the bootstrap script has diverged from the generated shape - left untouched. Manually \\`terraform import aws_s3_bucket.terraform_state <bucket>\\` before \\`terraform apply\\` when the state bucket exists but its \\`bootstrap.tfstate\\` object does not, otherwise bootstrap fails with BucketAlreadyOwnedByYou.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n // Terraform application projects are the only ones that vend bootstrap.ts.\n const bootstrapScripts = [...getProjects(tree).values()]\n .filter(\n (project) =>\n (project.metadata as any)?.generator ===\n TERRAFORM_PROJECT_GENERATOR_INFO.id,\n )\n .map((project) => joinPathFragments(project.root, 'scripts/bootstrap.ts'))\n .filter((filePath) => tree.exists(filePath));\n\n for (const filePath of bootstrapScripts) {\n if (await matchGritQL(tree, filePath, MIGRATED_PATTERN)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n continue;\n }\n\n // Confirm every edit site is present before writing any of them, so a\n // script that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n [\n STATE_FETCH_TRY_PATTERN,\n STATE_FLAG_SET_PATTERN,\n HELPER_PATTERN,\n INIT_PATTERN,\n S3_IMPORT_PATTERN,\n HEADER_COMMENT_PATTERN,\n ].map((pattern) => matchGritQL(tree, filePath, pattern)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(DIVERGED_NEXT_STEP(filePath));\n continue;\n }\n\n // `haveState` distinguishes \"no remote state\" from \"no bucket\", so the\n // import only runs when terraform has no prior knowledge of the bucket.\n await insertViaGritQL(\n tree,\n filePath,\n `${STATE_FETCH_TRY_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n $try\\``,\n 'let haveState = false;',\n );\n await applyGritQL(\n tree,\n filePath,\n `${STATE_FLAG_SET_PATTERN} => \\`if (out.Body) {\\n $body\\n haveState = true;\\n }\\``,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${HELPER_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n\\nconst main = async () => { $body }\\``,\n BUCKET_EXISTS_HELPER,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${INIT_PATTERN} => \\`execFileSync('terraform', ['init'], $opts);\\n\\n ${GRIT_INSERT_PLACEHOLDER}\\n\\``,\n IMPORT_STEP,\n );\n\n // Placed in the generator's import position rather than appended, so a\n // migrated workspace matches a freshly generated one.\n await applyGritQL(\n tree,\n filePath,\n `${S3_IMPORT_PATTERN} => \\`import { $before, GetObjectCommand, HeadBucketCommand, $after } from '@aws-sdk/client-s3'\\``,\n );\n\n await applyGritQL(\n tree,\n filePath,\n `${HEADER_COMMENT_PATTERN} => \\`${NEW_HEADER_COMMENT}\\``,\n );\n\n nextSteps.push(\n `${filePath}: now adopts an existing Terraform state bucket when its bootstrap.tfstate object is missing.`,\n );\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","TERRAFORM_PROJECT_GENERATOR_INFO","applyGritQL","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","MIGRATED_PATTERN","STATE_FETCH_TRY_PATTERN","STATE_FLAG_SET_PATTERN","HELPER_PATTERN","INIT_PATTERN","S3_IMPORT_PATTERN","HEADER_COMMENT_PATTERN","BUCKET_EXISTS_HELPER","IMPORT_STEP","NEW_HEADER_COMMENT","DIVERGED_NEXT_STEP","filePath","migration","tree","nextSteps","bootstrapScripts","values","filter","project","metadata","generator","id","map","root","exists","allSitesPresent","Promise","all","pattern","every","Boolean","push"],"mappings":"AAAA;;;CAGC,GACD,SACEA,WAAW,EACXC,iBAAiB,QAGZ,aAAa;AACpB,SAASC,gCAAgC,QAAQ,0CAAuC;AACxF,SACEC,WAAW,EACXC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAE7D;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,wEAAwE;AACxE,MAAMC,mBAAmB;AAEzB,0EAA0E;AAC1E,iDAAiD;AACjD,MAAMC,0BACJ;AACF,MAAMC,yBAAyB;AAC/B,MAAMC,iBAAiB;AACvB,MAAMC,eAAe;AACrB,MAAMC,oBACJ;AACF,MAAMC,yBACJ;AAEF,6EAA6E;AAC7E,yDAAyD;AACzD,MAAMC,uBAAuB,CAAC;;;;;;;;;;;;EAY5B,CAAC;AAEH,MAAMC,cAAc,CAAC;;;;;;;;;;;;;;;;;;GAkBlB,CAAC;AAEJ,0EAA0E;AAC1E,wCAAwC;AACxC,MAAMC,qBAAqB,CAAC;;;;;;;;GAQzB,CAAC;AAEJ,MAAMC,qBAAqB,CAACC,WAC1B,GAAGA,SAAS,kTAAkT,CAAC;AAEjU,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,2EAA2E;IAC3E,MAAMC,mBAAmB;WAAIvB,YAAYqB,MAAMG,MAAM;KAAG,CACrDC,MAAM,CACL,CAACC,UACC,AAACA,QAAQC,QAAQ,EAAUC,cAC3B1B,iCAAiC2B,EAAE,EAEtCC,GAAG,CAAC,CAACJ,UAAYzB,kBAAkByB,QAAQK,IAAI,EAAE,yBACjDN,MAAM,CAAC,CAACN,WAAaE,KAAKW,MAAM,CAACb;IAEpC,KAAK,MAAMA,YAAYI,iBAAkB;QACvC,IAAI,MAAMjB,YAAYe,MAAMF,UAAUX,mBAAmB;YAEvD;QACF;QAEA,sEAAsE;QACtE,yEAAyE;QACzE,MAAMyB,kBAAkB,AACtB,CAAA,MAAMC,QAAQC,GAAG,CACf;YACE1B;YACAC;YACAC;YACAC;YACAC;YACAC;SACD,CAACgB,GAAG,CAAC,CAACM,UAAY9B,YAAYe,MAAMF,UAAUiB,UACjD,EACAC,KAAK,CAACC;QAER,IAAI,CAACL,iBAAiB;YACpBX,UAAUiB,IAAI,CAACrB,mBAAmBC;YAClC;QACF;QAEA,uEAAuE;QACvE,wEAAwE;QACxE,MAAMd,gBACJgB,MACAF,UACA,GAAGV,wBAAwB,MAAM,EAAEL,wBAAwB,UAAU,CAAC,EACtE;QAEF,MAAMD,YACJkB,MACAF,UACA,GAAGT,uBAAuB,oEAAoE,CAAC;QAEjG,MAAML,gBACJgB,MACAF,UACA,GAAGR,eAAe,MAAM,EAAEP,wBAAwB,wCAAwC,CAAC,EAC3FW;QAEF,MAAMV,gBACJgB,MACAF,UACA,GAAGP,aAAa,uDAAuD,EAAER,wBAAwB,IAAI,CAAC,EACtGY;QAGF,uEAAuE;QACvE,sDAAsD;QACtD,MAAMb,YACJkB,MACAF,UACA,GAAGN,kBAAkB,iGAAiG,CAAC;QAGzH,MAAMV,YACJkB,MACAF,UACA,GAAGL,uBAAuB,MAAM,EAAEG,mBAAmB,EAAE,CAAC;QAG1DK,UAAUiB,IAAI,CACZ,GAAGpB,SAAS,6FAA6F,CAAC;IAE9G;IAEA,MAAMZ,qBAAqBc;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type Tree,\n} from '@nx/devkit';\nimport { TERRAFORM_PROJECT_GENERATOR_INFO } from '../../../terraform/project/generator';\nimport {\n applyGritQL,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\n\n/**\n * Adopt an existing Terraform state bucket in the vended bootstrap script when its state object is missing\n *\n * The state bucket stores its own tfstate inside itself, so if that object is\n * deleted while the bucket survives, `bootstrap` reads the 404 as a first run\n * and asks terraform to create a bucket that already exists — failing with\n * `BucketAlreadyOwnedByYou` on every subsequent run, with no retry that clears\n * it. The vended script now imports the existing bucket instead.\n *\n * Every edit is expressed as a GritQL rewrite so the script is matched on its\n * AST rather than its formatting.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, or consider a\n * hybrid migration, rather than clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\n// Guards the whole migration: present only once the import step exists.\nconst MIGRATED_PATTERN = '`bucketExists($_, $_)`';\n\n// Every edit site, matched structurally so formatting and argument layout\n// don't affect whether the script is recognised.\nconst STATE_FETCH_TRY_PATTERN =\n 'try_statement() as $try where { $try <: contains `GetObjectCommand` }';\nconst STATE_FLAG_SET_PATTERN = '`if (out.Body) { $body }`';\nconst HELPER_PATTERN = '`const main = async () => { $body }`';\nconst INIT_PATTERN = \"`execFileSync('terraform', ['init'], $opts)`\";\nconst S3_IMPORT_PATTERN =\n \"`import { $before, GetObjectCommand, $after } from '@aws-sdk/client-s3'`\";\nconst HEADER_COMMENT_PATTERN =\n 'comment() as $c where { $c <: includes \"state back to S3.\" }';\n\n// GritQL snippets are parsed as backtick-quoted patterns, so any backtick in\n// the inserted source has to survive that layer escaped.\nconst BUCKET_EXISTS_HELPER = `// S3 answers 404 only when the name is genuinely free; 403 means it exists\n// but is not readable with these credentials.\nconst bucketExists = async (s3: S3Client, bucket: string) => {\n try {\n await s3.send(new HeadBucketCommand({ Bucket: bucket }));\n return true;\n } catch (err: any) {\n const status = err?.$metadata?.httpStatusCode;\n if (status === 404) return false;\n if (status === 403) return true;\n throw err;\n }\n};`;\n\nconst IMPORT_STEP = `// The bucket holds its own state, so losing the state object while the\n // bucket survives would otherwise wedge bootstrap on a permanent\n // \\`BucketAlreadyOwnedByYou\\`. Adopt the existing bucket instead.\n if (!haveState && (await bucketExists(s3, bucket))) {\n console.log(\n \\`State bucket \\${bucket} already exists but its bootstrap state is missing — importing it.\\`,\n );\n execFileSync(\n 'terraform',\n [\n 'import',\n \\`-state=\\${tfStatePath}\\`,\n \\`-var=aws_region=\\${region}\\`,\n 'aws_s3_bucket.terraform_state',\n bucket,\n ],\n { cwd: bootstrapDir, stdio: 'inherit' },\n );\n }`;\n\n// Rewritten wholesale rather than appended to, since the summary sentence\n// changes rather than gaining a clause.\nconst NEW_HEADER_COMMENT = `/**\n * Bootstraps the remote Terraform state bucket.\n *\n * Equivalent to \\\\\\`cdk bootstrap\\\\\\` — resolves account + region from the AWS\n * SDK credential chain, pulls any existing bootstrap tfstate from S3,\n * runs \\\\\\`terraform apply\\\\\\` in the \\\\\\`bootstrap\\\\\\` dir, then pushes the new\n * state back to S3. Adopts an already-existing state bucket when its\n * state object is missing.\n */`;\n\nconst DIVERGED_NEXT_STEP = (filePath: string) =>\n `${filePath}: the bootstrap script has diverged from the generated shape - left untouched. Manually \\`terraform import aws_s3_bucket.terraform_state <bucket>\\` before \\`terraform apply\\` when the state bucket exists but its \\`bootstrap.tfstate\\` object does not, otherwise bootstrap fails with BucketAlreadyOwnedByYou.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n // Terraform application projects are the only ones that vend bootstrap.ts.\n const bootstrapScripts = [...getProjects(tree).values()]\n .filter(\n (project) =>\n (project.metadata as any)?.generator ===\n TERRAFORM_PROJECT_GENERATOR_INFO.id,\n )\n .map((project) => joinPathFragments(project.root, 'scripts/bootstrap.ts'))\n .filter((filePath) => tree.exists(filePath));\n\n for (const filePath of bootstrapScripts) {\n if (await matchGritQL(tree, filePath, MIGRATED_PATTERN)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n continue;\n }\n\n // Confirm every edit site is present before writing any of them, so a\n // script that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n [\n STATE_FETCH_TRY_PATTERN,\n STATE_FLAG_SET_PATTERN,\n HELPER_PATTERN,\n INIT_PATTERN,\n S3_IMPORT_PATTERN,\n HEADER_COMMENT_PATTERN,\n ].map((pattern) => matchGritQL(tree, filePath, pattern)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(DIVERGED_NEXT_STEP(filePath));\n continue;\n }\n\n // `haveState` distinguishes \"no remote state\" from \"no bucket\", so the\n // import only runs when terraform has no prior knowledge of the bucket.\n await insertViaGritQL(\n tree,\n filePath,\n `${STATE_FETCH_TRY_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n $try\\``,\n 'let haveState = false;',\n );\n await applyGritQL(\n tree,\n filePath,\n `${STATE_FLAG_SET_PATTERN} => \\`if (out.Body) {\\n $body\\n haveState = true;\\n }\\``,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${HELPER_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n\\nconst main = async () => { $body }\\``,\n BUCKET_EXISTS_HELPER,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${INIT_PATTERN} => \\`execFileSync('terraform', ['init'], $opts);\\n\\n ${GRIT_INSERT_PLACEHOLDER}\\n\\``,\n IMPORT_STEP,\n );\n\n // Placed in the generator's import position rather than appended, so a\n // migrated workspace matches a freshly generated one.\n await applyGritQL(\n tree,\n filePath,\n `${S3_IMPORT_PATTERN} => \\`import { $before, GetObjectCommand, HeadBucketCommand, $after } from '@aws-sdk/client-s3'\\``,\n );\n\n await applyGritQL(\n tree,\n filePath,\n `${HEADER_COMMENT_PATTERN} => \\`${NEW_HEADER_COMMENT}\\``,\n );\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","TERRAFORM_PROJECT_GENERATOR_INFO","applyGritQL","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","MIGRATED_PATTERN","STATE_FETCH_TRY_PATTERN","STATE_FLAG_SET_PATTERN","HELPER_PATTERN","INIT_PATTERN","S3_IMPORT_PATTERN","HEADER_COMMENT_PATTERN","BUCKET_EXISTS_HELPER","IMPORT_STEP","NEW_HEADER_COMMENT","DIVERGED_NEXT_STEP","filePath","migration","tree","nextSteps","bootstrapScripts","values","filter","project","metadata","generator","id","map","root","exists","allSitesPresent","Promise","all","pattern","every","Boolean","push"],"mappings":"AAAA;;;CAGC,GACD,SACEA,WAAW,EACXC,iBAAiB,QAGZ,aAAa;AACpB,SAASC,gCAAgC,QAAQ,0CAAuC;AACxF,SACEC,WAAW,EACXC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAE7D;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,wEAAwE;AACxE,MAAMC,mBAAmB;AAEzB,0EAA0E;AAC1E,iDAAiD;AACjD,MAAMC,0BACJ;AACF,MAAMC,yBAAyB;AAC/B,MAAMC,iBAAiB;AACvB,MAAMC,eAAe;AACrB,MAAMC,oBACJ;AACF,MAAMC,yBACJ;AAEF,6EAA6E;AAC7E,yDAAyD;AACzD,MAAMC,uBAAuB,CAAC;;;;;;;;;;;;EAY5B,CAAC;AAEH,MAAMC,cAAc,CAAC;;;;;;;;;;;;;;;;;;GAkBlB,CAAC;AAEJ,0EAA0E;AAC1E,wCAAwC;AACxC,MAAMC,qBAAqB,CAAC;;;;;;;;GAQzB,CAAC;AAEJ,MAAMC,qBAAqB,CAACC,WAC1B,GAAGA,SAAS,kTAAkT,CAAC;AAEjU,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,2EAA2E;IAC3E,MAAMC,mBAAmB;WAAIvB,YAAYqB,MAAMG,MAAM;KAAG,CACrDC,MAAM,CACL,CAACC,UACC,AAACA,QAAQC,QAAQ,EAAUC,cAC3B1B,iCAAiC2B,EAAE,EAEtCC,GAAG,CAAC,CAACJ,UAAYzB,kBAAkByB,QAAQK,IAAI,EAAE,yBACjDN,MAAM,CAAC,CAACN,WAAaE,KAAKW,MAAM,CAACb;IAEpC,KAAK,MAAMA,YAAYI,iBAAkB;QACvC,IAAI,MAAMjB,YAAYe,MAAMF,UAAUX,mBAAmB;YAEvD;QACF;QAEA,sEAAsE;QACtE,yEAAyE;QACzE,MAAMyB,kBAAkB,AACtB,CAAA,MAAMC,QAAQC,GAAG,CACf;YACE1B;YACAC;YACAC;YACAC;YACAC;YACAC;SACD,CAACgB,GAAG,CAAC,CAACM,UAAY9B,YAAYe,MAAMF,UAAUiB,UACjD,EACAC,KAAK,CAACC;QAER,IAAI,CAACL,iBAAiB;YACpBX,UAAUiB,IAAI,CAACrB,mBAAmBC;YAClC;QACF;QAEA,uEAAuE;QACvE,wEAAwE;QACxE,MAAMd,gBACJgB,MACAF,UACA,GAAGV,wBAAwB,MAAM,EAAEL,wBAAwB,UAAU,CAAC,EACtE;QAEF,MAAMD,YACJkB,MACAF,UACA,GAAGT,uBAAuB,oEAAoE,CAAC;QAEjG,MAAML,gBACJgB,MACAF,UACA,GAAGR,eAAe,MAAM,EAAEP,wBAAwB,wCAAwC,CAAC,EAC3FW;QAEF,MAAMV,gBACJgB,MACAF,UACA,GAAGP,aAAa,uDAAuD,EAAER,wBAAwB,IAAI,CAAC,EACtGY;QAGF,uEAAuE;QACvE,sDAAsD;QACtD,MAAMb,YACJkB,MACAF,UACA,GAAGN,kBAAkB,iGAAiG,CAAC;QAGzH,MAAMV,YACJkB,MACAF,UACA,GAAGL,uBAAuB,MAAM,EAAEG,mBAAmB,EAAE,CAAC;IAE5D;IAEA,MAAMV,qBAAqBc;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
@@ -175,7 +175,6 @@ const TERRAFORM_EDITS = [
|
|
|
175
175
|
]
|
|
176
176
|
];
|
|
177
177
|
const divergedNextStep = (filePath)=>`${filePath}: the UserIdentity Web ACL has diverged from the generated shape - left untouched. To sign in against a local dev server, override ${SSRF_RULE_NAME} in the AWSManagedRulesCommonRuleSet rule group to Count, otherwise the Cognito Hosted UI returns 403 for localhost redirect URIs.`;
|
|
178
|
-
const migratedNextStep = (filePath)=>`${filePath}: ${SSRF_RULE_NAME} is now counted rather than blocked while a local callback URL is allowed, so signing in against a local dev server works. Redeploy to apply it.`;
|
|
179
178
|
export default async function migration(tree) {
|
|
180
179
|
const nextSteps = [];
|
|
181
180
|
for (const [filePath, migratedPattern, edits] of [
|
|
@@ -206,7 +205,6 @@ export default async function migration(tree) {
|
|
|
206
205
|
for (const [, rewrite] of edits){
|
|
207
206
|
await applyGritQL(tree, filePath, rewrite);
|
|
208
207
|
}
|
|
209
|
-
nextSteps.push(migratedNextStep(filePath));
|
|
210
208
|
}
|
|
211
209
|
await formatFilesInSubtree(tree);
|
|
212
210
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/user-identity-waf-allow-localhost-callback/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport { applyGritQL, matchGritQL } from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Count the EC2MetaDataSSRF_QUERYARGUMENTS WAF rule on the UserIdentity Web ACL\n *\n * `AWSManagedRulesCommonRuleSet` treats the loopback `redirect_uri` the Cognito\n * Hosted UI receives during local sign-in as an SSRF attempt, so sign-in against\n * a local dev server fails with an opaque 403 before the login page renders. The\n * rule is overridden to Count while a local callback URL is allowed; every other\n * rule in the group still blocks.\n *\n * The local callback URLs are lifted into a named constant so the override can be\n * derived from them rather than restating the condition.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CDK_USER_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/user-identity.ts`;\nconst TERRAFORM_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/identity/identity.tf`;\n\nconst SSRF_RULE_NAME = 'EC2MetaDataSSRF_QUERYARGUMENTS';\n\n// Guards each file: present only once the override has been added.\nconst CDK_MIGRATED_PATTERN = `\\`ruleActionOverrides: allowsLocalCallback ? $_ : undefined\\``;\nconst TERRAFORM_MIGRATED_PATTERN = `language hcl\\n\\`name = \"${SSRF_RULE_NAME}\"\\``;\n\n// Every CDK edit site, matched structurally so formatting doesn't affect whether\n// the construct is recognised.\nconst CDK_CONSTANT_ANCHOR_PATTERN = \"`const WEB_CLIENT_ID = 'WebClient'`\";\n// Scoped to the `.concat` callsite so it can't also rewrite the array literal\n// inside the constant this migration inserts.\nconst CDK_LOCAL_URLS_PATTERN =\n \"`['http://localhost:4200', 'http://localhost:4300'].concat($rest)`\";\nconst CDK_CALL_PATTERN = '`this.createWebAcl($id, this.userPool)`';\nconst CDK_SIGNATURE_PATTERN =\n '`private createWebAcl = ($id: string, $pool: UserPool) => $body`';\nconst CDK_STATEMENT_PATTERN =\n \"`managedRuleGroupStatement: { name: 'AWSManagedRulesCommonRuleSet', vendorName: 'AWS' }`\";\n\nconst CDK_EDITS: Array<[string, string]> = [\n // Local callback URLs become a named constant the override can key off.\n [\n CDK_CONSTANT_ANCHOR_PATTERN,\n `${CDK_CONSTANT_ANCHOR_PATTERN} => \\`const WEB_CLIENT_ID = 'WebClient';\n\n/** Local dev server origins permitted to complete the sign-in redirect */\nconst LOCAL_CALLBACK_URLS = ['http://localhost:4200', 'http://localhost:4300']\\``,\n ],\n [\n CDK_LOCAL_URLS_PATTERN,\n `${CDK_LOCAL_URLS_PATTERN} => \\`LOCAL_CALLBACK_URLS.concat($rest)\\``,\n ],\n [\n CDK_CALL_PATTERN,\n `${CDK_CALL_PATTERN} => \\`this.createWebAcl($id, this.userPool, LOCAL_CALLBACK_URLS.length > 0)\\``,\n ],\n [\n CDK_SIGNATURE_PATTERN,\n `${CDK_SIGNATURE_PATTERN} => \\`private createWebAcl = (\n $id: string,\n $pool: UserPool,\n allowsLocalCallback: boolean\n ) => $body\\``,\n ],\n [\n CDK_STATEMENT_PATTERN,\n `${CDK_STATEMENT_PATTERN} => \\`managedRuleGroupStatement: {\n name: 'AWSManagedRulesCommonRuleSet',\n vendorName: 'AWS',\n // ${SSRF_RULE_NAME} treats the loopback redirect_uri the\n // Hosted UI receives during local sign-in as an SSRF attempt. Counted\n // only while a local callback URL is allowed; every other rule blocks.\n ruleActionOverrides: allowsLocalCallback\n ? [\n {\n name: '${SSRF_RULE_NAME}',\n actionToUse: { count: {} },\n },\n ]\n : undefined,\n }\\``,\n ],\n];\n\nconst TERRAFORM_DATA_SOURCES_PATTERN = [\n 'language hcl',\n '`data \"aws_region\" \"current\" {}`',\n].join('\\n');\n\nconst TERRAFORM_EDITS: Array<[string, string]> = [\n // Local callback URLs become a local the override can key off.\n [\n TERRAFORM_DATA_SOURCES_PATTERN,\n [\n 'language hcl',\n '`data \"aws_region\" \"current\" {}` => `data \"aws_region\" \"current\" {}',\n '',\n 'locals {',\n ' # Local dev server origins permitted to complete the sign-in redirect',\n ' local_callback_urls = [',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ]',\n '}`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`callback_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.callback_urls)`',\n ].join('\\n'),\n [\n 'language hcl',\n '`callback_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.callback_urls)` => `callback_urls = concat(local.local_callback_urls, var.callback_urls)`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`logout_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.logout_urls)`',\n ].join('\\n'),\n [\n 'language hcl',\n '`logout_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.logout_urls)` => `logout_urls = concat(local.local_callback_urls, var.logout_urls)`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n ' }`',\n ].join('\\n'),\n [\n 'language hcl',\n '`managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n ' }` => `managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n '',\n ` # ${SSRF_RULE_NAME} treats the loopback redirect_uri the`,\n ' # Hosted UI receives during local sign-in as an SSRF attempt. Counted',\n ' # only while a local callback URL is allowed; every other rule blocks.',\n ' dynamic \"rule_action_override\" {',\n ' for_each = length(local.local_callback_urls) > 0 ? [1] : []',\n '',\n ' content {',\n ` name = \"${SSRF_RULE_NAME}\"`,\n '',\n ' action_to_use {',\n ' count {}',\n ' }',\n ' }',\n ' }',\n ' }`',\n ].join('\\n'),\n ],\n];\n\nconst divergedNextStep = (filePath: string) =>\n `${filePath}: the UserIdentity Web ACL has diverged from the generated shape - left untouched. To sign in against a local dev server, override ${SSRF_RULE_NAME} in the AWSManagedRulesCommonRuleSet rule group to Count, otherwise the Cognito Hosted UI returns 403 for localhost redirect URIs.`;\n\nconst migratedNextStep = (filePath: string) =>\n `${filePath}: ${SSRF_RULE_NAME} is now counted rather than blocked while a local callback URL is allowed, so signing in against a local dev server works. Redeploy to apply it.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const [filePath, migratedPattern, edits] of [\n [CDK_USER_IDENTITY_FILE, CDK_MIGRATED_PATTERN, CDK_EDITS],\n [TERRAFORM_IDENTITY_FILE, TERRAFORM_MIGRATED_PATTERN, TERRAFORM_EDITS],\n ] as const) {\n if (!tree.exists(filePath)) {\n // This workspace doesn't use this IaC provider, or has no UserIdentity.\n continue;\n }\n\n if (await matchGritQL(tree, filePath, migratedPattern)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n continue;\n }\n\n // Confirm every edit site is present before writing any of them, so a file\n // that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n edits.map(([match]) => matchGritQL(tree, filePath, match)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(divergedNextStep(filePath));\n continue;\n }\n\n for (const [, rewrite] of edits) {\n await applyGritQL(tree, filePath, rewrite);\n }\n nextSteps.push(migratedNextStep(filePath));\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["applyGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","CDK_USER_IDENTITY_FILE","TERRAFORM_IDENTITY_FILE","SSRF_RULE_NAME","CDK_MIGRATED_PATTERN","TERRAFORM_MIGRATED_PATTERN","CDK_CONSTANT_ANCHOR_PATTERN","CDK_LOCAL_URLS_PATTERN","CDK_CALL_PATTERN","CDK_SIGNATURE_PATTERN","CDK_STATEMENT_PATTERN","CDK_EDITS","TERRAFORM_DATA_SOURCES_PATTERN","join","TERRAFORM_EDITS","divergedNextStep","filePath","migratedNextStep","migration","tree","nextSteps","migratedPattern","edits","exists","allSitesPresent","Promise","all","map","match","every","Boolean","push","rewrite"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/user-identity-waf-allow-localhost-callback/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport { applyGritQL, matchGritQL } from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Count the EC2MetaDataSSRF_QUERYARGUMENTS WAF rule on the UserIdentity Web ACL\n *\n * `AWSManagedRulesCommonRuleSet` treats the loopback `redirect_uri` the Cognito\n * Hosted UI receives during local sign-in as an SSRF attempt, so sign-in against\n * a local dev server fails with an opaque 403 before the login page renders. The\n * rule is overridden to Count while a local callback URL is allowed; every other\n * rule in the group still blocks.\n *\n * The local callback URLs are lifted into a named constant so the override can be\n * derived from them rather than restating the condition.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CDK_USER_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/user-identity.ts`;\nconst TERRAFORM_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/identity/identity.tf`;\n\nconst SSRF_RULE_NAME = 'EC2MetaDataSSRF_QUERYARGUMENTS';\n\n// Guards each file: present only once the override has been added.\nconst CDK_MIGRATED_PATTERN = `\\`ruleActionOverrides: allowsLocalCallback ? $_ : undefined\\``;\nconst TERRAFORM_MIGRATED_PATTERN = `language hcl\\n\\`name = \"${SSRF_RULE_NAME}\"\\``;\n\n// Every CDK edit site, matched structurally so formatting doesn't affect whether\n// the construct is recognised.\nconst CDK_CONSTANT_ANCHOR_PATTERN = \"`const WEB_CLIENT_ID = 'WebClient'`\";\n// Scoped to the `.concat` callsite so it can't also rewrite the array literal\n// inside the constant this migration inserts.\nconst CDK_LOCAL_URLS_PATTERN =\n \"`['http://localhost:4200', 'http://localhost:4300'].concat($rest)`\";\nconst CDK_CALL_PATTERN = '`this.createWebAcl($id, this.userPool)`';\nconst CDK_SIGNATURE_PATTERN =\n '`private createWebAcl = ($id: string, $pool: UserPool) => $body`';\nconst CDK_STATEMENT_PATTERN =\n \"`managedRuleGroupStatement: { name: 'AWSManagedRulesCommonRuleSet', vendorName: 'AWS' }`\";\n\nconst CDK_EDITS: Array<[string, string]> = [\n // Local callback URLs become a named constant the override can key off.\n [\n CDK_CONSTANT_ANCHOR_PATTERN,\n `${CDK_CONSTANT_ANCHOR_PATTERN} => \\`const WEB_CLIENT_ID = 'WebClient';\n\n/** Local dev server origins permitted to complete the sign-in redirect */\nconst LOCAL_CALLBACK_URLS = ['http://localhost:4200', 'http://localhost:4300']\\``,\n ],\n [\n CDK_LOCAL_URLS_PATTERN,\n `${CDK_LOCAL_URLS_PATTERN} => \\`LOCAL_CALLBACK_URLS.concat($rest)\\``,\n ],\n [\n CDK_CALL_PATTERN,\n `${CDK_CALL_PATTERN} => \\`this.createWebAcl($id, this.userPool, LOCAL_CALLBACK_URLS.length > 0)\\``,\n ],\n [\n CDK_SIGNATURE_PATTERN,\n `${CDK_SIGNATURE_PATTERN} => \\`private createWebAcl = (\n $id: string,\n $pool: UserPool,\n allowsLocalCallback: boolean\n ) => $body\\``,\n ],\n [\n CDK_STATEMENT_PATTERN,\n `${CDK_STATEMENT_PATTERN} => \\`managedRuleGroupStatement: {\n name: 'AWSManagedRulesCommonRuleSet',\n vendorName: 'AWS',\n // ${SSRF_RULE_NAME} treats the loopback redirect_uri the\n // Hosted UI receives during local sign-in as an SSRF attempt. Counted\n // only while a local callback URL is allowed; every other rule blocks.\n ruleActionOverrides: allowsLocalCallback\n ? [\n {\n name: '${SSRF_RULE_NAME}',\n actionToUse: { count: {} },\n },\n ]\n : undefined,\n }\\``,\n ],\n];\n\nconst TERRAFORM_DATA_SOURCES_PATTERN = [\n 'language hcl',\n '`data \"aws_region\" \"current\" {}`',\n].join('\\n');\n\nconst TERRAFORM_EDITS: Array<[string, string]> = [\n // Local callback URLs become a local the override can key off.\n [\n TERRAFORM_DATA_SOURCES_PATTERN,\n [\n 'language hcl',\n '`data \"aws_region\" \"current\" {}` => `data \"aws_region\" \"current\" {}',\n '',\n 'locals {',\n ' # Local dev server origins permitted to complete the sign-in redirect',\n ' local_callback_urls = [',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ]',\n '}`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`callback_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.callback_urls)`',\n ].join('\\n'),\n [\n 'language hcl',\n '`callback_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.callback_urls)` => `callback_urls = concat(local.local_callback_urls, var.callback_urls)`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`logout_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.logout_urls)`',\n ].join('\\n'),\n [\n 'language hcl',\n '`logout_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.logout_urls)` => `logout_urls = concat(local.local_callback_urls, var.logout_urls)`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n ' }`',\n ].join('\\n'),\n [\n 'language hcl',\n '`managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n ' }` => `managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n '',\n ` # ${SSRF_RULE_NAME} treats the loopback redirect_uri the`,\n ' # Hosted UI receives during local sign-in as an SSRF attempt. Counted',\n ' # only while a local callback URL is allowed; every other rule blocks.',\n ' dynamic \"rule_action_override\" {',\n ' for_each = length(local.local_callback_urls) > 0 ? [1] : []',\n '',\n ' content {',\n ` name = \"${SSRF_RULE_NAME}\"`,\n '',\n ' action_to_use {',\n ' count {}',\n ' }',\n ' }',\n ' }',\n ' }`',\n ].join('\\n'),\n ],\n];\n\nconst divergedNextStep = (filePath: string) =>\n `${filePath}: the UserIdentity Web ACL has diverged from the generated shape - left untouched. To sign in against a local dev server, override ${SSRF_RULE_NAME} in the AWSManagedRulesCommonRuleSet rule group to Count, otherwise the Cognito Hosted UI returns 403 for localhost redirect URIs.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const [filePath, migratedPattern, edits] of [\n [CDK_USER_IDENTITY_FILE, CDK_MIGRATED_PATTERN, CDK_EDITS],\n [TERRAFORM_IDENTITY_FILE, TERRAFORM_MIGRATED_PATTERN, TERRAFORM_EDITS],\n ] as const) {\n if (!tree.exists(filePath)) {\n // This workspace doesn't use this IaC provider, or has no UserIdentity.\n continue;\n }\n\n if (await matchGritQL(tree, filePath, migratedPattern)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n continue;\n }\n\n // Confirm every edit site is present before writing any of them, so a file\n // that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n edits.map(([match]) => matchGritQL(tree, filePath, match)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(divergedNextStep(filePath));\n continue;\n }\n\n for (const [, rewrite] of edits) {\n await applyGritQL(tree, filePath, rewrite);\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["applyGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","CDK_USER_IDENTITY_FILE","TERRAFORM_IDENTITY_FILE","SSRF_RULE_NAME","CDK_MIGRATED_PATTERN","TERRAFORM_MIGRATED_PATTERN","CDK_CONSTANT_ANCHOR_PATTERN","CDK_LOCAL_URLS_PATTERN","CDK_CALL_PATTERN","CDK_SIGNATURE_PATTERN","CDK_STATEMENT_PATTERN","CDK_EDITS","TERRAFORM_DATA_SOURCES_PATTERN","join","TERRAFORM_EDITS","divergedNextStep","filePath","migration","tree","nextSteps","migratedPattern","edits","exists","allSitesPresent","Promise","all","map","match","every","Boolean","push","rewrite"],"mappings":"AAAA;;;CAGC,GAED,SAASA,WAAW,EAAEC,WAAW,QAAQ,wBAAqB;AAC9D,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SACEC,YAAY,EACZC,qBAAqB,EACrBC,oBAAoB,QACf,gDAA6C;AAEpD;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,MAAMC,yBAAyB,GAAGH,aAAa,CAAC,EAAEC,sBAAsB,0BAA0B,CAAC;AACnG,MAAMG,0BAA0B,GAAGJ,aAAa,CAAC,EAAEE,qBAAqB,4CAA4C,CAAC;AAErH,MAAMG,iBAAiB;AAEvB,mEAAmE;AACnE,MAAMC,uBAAuB,CAAC,6DAA6D,CAAC;AAC5F,MAAMC,6BAA6B,CAAC,wBAAwB,EAAEF,eAAe,GAAG,CAAC;AAEjF,iFAAiF;AACjF,+BAA+B;AAC/B,MAAMG,8BAA8B;AACpC,8EAA8E;AAC9E,8CAA8C;AAC9C,MAAMC,yBACJ;AACF,MAAMC,mBAAmB;AACzB,MAAMC,wBACJ;AACF,MAAMC,wBACJ;AAEF,MAAMC,YAAqC;IACzC,wEAAwE;IACxE;QACEL;QACA,GAAGA,4BAA4B;;;gFAG6C,CAAC;KAC9E;IACD;QACEC;QACA,GAAGA,uBAAuB,yCAAyC,CAAC;KACrE;IACD;QACEC;QACA,GAAGA,iBAAiB,6EAA6E,CAAC;KACnG;IACD;QACEC;QACA,GAAGA,sBAAsB;;;;cAIf,CAAC;KACZ;IACD;QACEC;QACA,GAAGA,sBAAsB;;;iBAGZ,EAAEP,eAAe;;;;;;6BAML,EAAEA,eAAe;;;;;eAK/B,CAAC;KACb;CACF;AAED,MAAMS,iCAAiC;IACrC;IACA;CACD,CAACC,IAAI,CAAC;AAEP,MAAMC,kBAA2C;IAC/C,+DAA+D;IAC/D;QACEF;QACA;YACE;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD,CAACC,IAAI,CAAC;KACR;IACD;QACE;YACE;YACA;YACA;YACA;YACA;SACD,CAACA,IAAI,CAAC;QACP;YACE;YACA;YACA;YACA;YACA;SACD,CAACA,IAAI,CAAC;KACR;IACD;QACE;YACE;YACA;YACA;YACA;YACA;SACD,CAACA,IAAI,CAAC;QACP;YACE;YACA;YACA;YACA;YACA;SACD,CAACA,IAAI,CAAC;KACR;IACD;QACE;YACE;YACA;YACA;YACA;YACA;SACD,CAACA,IAAI,CAAC;QACP;YACE;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA,CAAC,UAAU,EAAEV,eAAe,qCAAqC,CAAC;YAClE;YACA;YACA;YACA;YACA;YACA;YACA,CAAC,oBAAoB,EAAEA,eAAe,CAAC,CAAC;YACxC;YACA;YACA;YACA;YACA;YACA;YACA;SACD,CAACU,IAAI,CAAC;KACR;CACF;AAED,MAAME,mBAAmB,CAACC,WACxB,GAAGA,SAAS,mIAAmI,EAAEb,eAAe,kIAAkI,CAAC;AAErS,eAAe,eAAec,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,KAAK,MAAM,CAACH,UAAUI,iBAAiBC,MAAM,IAAI;QAC/C;YAACpB;YAAwBG;YAAsBO;SAAU;QACzD;YAACT;YAAyBG;YAA4BS;SAAgB;KACvE,CAAW;QACV,IAAI,CAACI,KAAKI,MAAM,CAACN,WAAW;YAE1B;QACF;QAEA,IAAI,MAAMpB,YAAYsB,MAAMF,UAAUI,kBAAkB;YAEtD;QACF;QAEA,2EAA2E;QAC3E,kEAAkE;QAClE,MAAMG,kBAAkB,AACtB,CAAA,MAAMC,QAAQC,GAAG,CACfJ,MAAMK,GAAG,CAAC,CAAC,CAACC,MAAM,GAAK/B,YAAYsB,MAAMF,UAAUW,QACrD,EACAC,KAAK,CAACC;QAER,IAAI,CAACN,iBAAiB;YACpBJ,UAAUW,IAAI,CAACf,iBAAiBC;YAChC;QACF;QAEA,KAAK,MAAM,GAAGe,QAAQ,IAAIV,MAAO;YAC/B,MAAM1B,YAAYuB,MAAMF,UAAUe;QACpC;IACF;IAEA,MAAMlC,qBAAqBqB;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
@@ -565,7 +565,6 @@ for (;;) {
|
|
|
565
565
|
'./data',
|
|
566
566
|
'-port',
|
|
567
567
|
\`\${port}\`,
|
|
568
|
-
'-optimizeDbBeforeStartup',
|
|
569
568
|
];
|
|
570
569
|
const create = spawnSync(containerEngine, runArgs, { stdio: 'pipe' });
|
|
571
570
|
if (create.status === 0) {
|
|
@@ -1281,7 +1280,6 @@ for (;;) {
|
|
|
1281
1280
|
'./data',
|
|
1282
1281
|
'-port',
|
|
1283
1282
|
\`\${port}\`,
|
|
1284
|
-
'-optimizeDbBeforeStartup',
|
|
1285
1283
|
];
|
|
1286
1284
|
const create = spawnSync(containerEngine, runArgs, { stdio: 'pipe' });
|
|
1287
1285
|
if (create.status === 0) {
|
|
@@ -565,7 +565,6 @@ for (;;) {
|
|
|
565
565
|
'./data',
|
|
566
566
|
'-port',
|
|
567
567
|
\`\${port}\`,
|
|
568
|
-
'-optimizeDbBeforeStartup',
|
|
569
568
|
];
|
|
570
569
|
const create = spawnSync(containerEngine, runArgs, { stdio: 'pipe' });
|
|
571
570
|
if (create.status === 0) {
|
|
@@ -1213,7 +1212,6 @@ for (;;) {
|
|
|
1213
1212
|
'./data',
|
|
1214
1213
|
'-port',
|
|
1215
1214
|
\`\${port}\`,
|
|
1216
|
-
'-optimizeDbBeforeStartup',
|
|
1217
1215
|
];
|
|
1218
1216
|
const create = spawnSync(containerEngine, runArgs, { stdio: 'pipe' });
|
|
1219
1217
|
if (create.status === 0) {
|
|
@@ -19,6 +19,8 @@ import { formatFilesInSubtree } from '<%- formatImportPath %>';
|
|
|
19
19
|
* - Pattern-match before writing: skip files that have diverged from the shape
|
|
20
20
|
* your generators produce, and report them via `agentContext` so the prompt
|
|
21
21
|
* can pick them up, rather than clobbering the user's changes.
|
|
22
|
+
* - `nextSteps` is for work left for the user to do by hand, not a log of the
|
|
23
|
+
* edits you made: an edit that applied cleanly needs no entry.
|
|
22
24
|
* - Idempotent: re-running must be a no-op.
|
|
23
25
|
* - Format what you write: finish with `formatFilesInSubtree` so the files your
|
|
24
26
|
* migration wrote are formatted correctly.
|
|
@@ -36,6 +38,8 @@ import { formatFilesInSubtree } from '<%- formatImportPath %>';
|
|
|
36
38
|
* - Pattern-match before writing: skip files that have diverged from the shape
|
|
37
39
|
* your generators produce and report them via `nextSteps`, or consider a
|
|
38
40
|
* hybrid migration, rather than clobbering the user's changes.
|
|
41
|
+
* - `nextSteps` is for work left for the user to do by hand, not a log of the
|
|
42
|
+
* edits you made: an edit that applied cleanly needs no entry.
|
|
39
43
|
* - Idempotent: re-running must be a no-op.
|
|
40
44
|
* - Format what you write: finish with `formatFilesInSubtree` so the files your
|
|
41
45
|
* migration wrote are formatted correctly.
|
|
@@ -18,7 +18,8 @@ import { compare } from 'semver';
|
|
|
18
18
|
* - The weekly `update-versions` PR backfills the version of the release that
|
|
19
19
|
* shipped each migration, moving and re-keying it accordingly
|
|
20
20
|
* (`scripts/backfill-migration-versions.ts`), so source converges on the
|
|
21
|
-
* versions of everything already released.
|
|
21
|
+
* versions of everything already released. `packageJsonUpdates` entries are
|
|
22
|
+
* dated in place there too, since their keys never change.
|
|
22
23
|
*
|
|
23
24
|
* A version already in source always wins, so backfilled entries are stable.
|
|
24
25
|
*
|
|
@@ -32,8 +33,9 @@ export interface MigrationsJson {
|
|
|
32
33
|
} & Record<string, unknown>>;
|
|
33
34
|
/**
|
|
34
35
|
* Declarative dependency bumps `nx migrate` applies to the root manifest.
|
|
35
|
-
* Keyed
|
|
36
|
-
*
|
|
36
|
+
* Keyed by what the bump targets rather than the release that ships it, so an
|
|
37
|
+
* entry is unique from the moment it is written and one release's bump can sit
|
|
38
|
+
* beside the next; nx itself gates on the entry's `version`.
|
|
37
39
|
*/
|
|
38
40
|
packageJsonUpdates?: Record<string, {
|
|
39
41
|
version: string;
|
|
@@ -44,15 +46,19 @@ export declare const compareVersions: typeof compare;
|
|
|
44
46
|
/** Whether a string is an exact semver version. */
|
|
45
47
|
export declare const isValidVersion: (version: string) => boolean;
|
|
46
48
|
/**
|
|
47
|
-
* Map of
|
|
48
|
-
*
|
|
49
|
-
* in source wins, so its history doesn't
|
|
50
|
-
* been released is absent.
|
|
49
|
+
* Map of entry key -> version of the earliest release that registers it, across
|
|
50
|
+
* both `generators` and `packageJsonUpdates`. Resolved only for the entries still
|
|
51
|
+
* missing a version (one already recorded in source wins, so its history doesn't
|
|
52
|
+
* need reading). An entry that hasn't been released is absent.
|
|
51
53
|
*
|
|
52
54
|
* Walks releases newest first and stops as soon as every entry is resolved: an
|
|
53
55
|
* entry that has disappeared from a release's `migrations.json` was first
|
|
54
56
|
* registered by the release after it, which is the one that shipped it.
|
|
55
57
|
*
|
|
58
|
+
* Both sections resolve the same way and share the walk, so the release doesn't
|
|
59
|
+
* read tag history twice. Keys can't collide: a migration is keyed by its own
|
|
60
|
+
* name, an nx bump by the nx version it moves to.
|
|
61
|
+
*
|
|
56
62
|
* @param migrations parsed source migrations.json
|
|
57
63
|
* @param versions released versions in descending semver order
|
|
58
64
|
* @param readReleasedMigrations reads the `migrations.json` published by a given
|
|
@@ -61,9 +67,8 @@ export declare const isValidVersion: (version: string) => boolean;
|
|
|
61
67
|
export declare const readShippedMigrationVersions: (migrations: MigrationsJson, versions: string[], readReleasedMigrations: (version: string) => MigrationsJson | undefined) => Record<string, string>;
|
|
62
68
|
/**
|
|
63
69
|
* Return a copy of the migrations collection with a `version` stamped onto
|
|
64
|
-
* every generator entry, preserving any already present, and
|
|
65
|
-
* `packageJsonUpdates` entry still
|
|
66
|
-
* ships under.
|
|
70
|
+
* every generator entry, preserving any already present, and the pending version
|
|
71
|
+
* recorded on any `packageJsonUpdates` entry still holding `latest`.
|
|
67
72
|
*
|
|
68
73
|
* @param migrations parsed migrations.json to stamp
|
|
69
74
|
* @param shippedVersions migration key -> version of the earliest release
|
|
@@ -5,27 +5,40 @@
|
|
|
5
5
|
/** Ascending semver comparator for `Array.prototype.sort`. */ export const compareVersions = compare;
|
|
6
6
|
/** Whether a string is an exact semver version. */ export const isValidVersion = (version)=>valid(version) !== null;
|
|
7
7
|
/**
|
|
8
|
-
* Map of
|
|
9
|
-
*
|
|
10
|
-
* in source wins, so its history doesn't
|
|
11
|
-
* been released is absent.
|
|
8
|
+
* Map of entry key -> version of the earliest release that registers it, across
|
|
9
|
+
* both `generators` and `packageJsonUpdates`. Resolved only for the entries still
|
|
10
|
+
* missing a version (one already recorded in source wins, so its history doesn't
|
|
11
|
+
* need reading). An entry that hasn't been released is absent.
|
|
12
12
|
*
|
|
13
13
|
* Walks releases newest first and stops as soon as every entry is resolved: an
|
|
14
14
|
* entry that has disappeared from a release's `migrations.json` was first
|
|
15
15
|
* registered by the release after it, which is the one that shipped it.
|
|
16
16
|
*
|
|
17
|
+
* Both sections resolve the same way and share the walk, so the release doesn't
|
|
18
|
+
* read tag history twice. Keys can't collide: a migration is keyed by its own
|
|
19
|
+
* name, an nx bump by the nx version it moves to.
|
|
20
|
+
*
|
|
17
21
|
* @param migrations parsed source migrations.json
|
|
18
22
|
* @param versions released versions in descending semver order
|
|
19
23
|
* @param readReleasedMigrations reads the `migrations.json` published by a given
|
|
20
24
|
* version, or undefined if it predates the manifest
|
|
21
25
|
*/ export const readShippedMigrationVersions = (migrations, versions, readReleasedMigrations)=>{
|
|
22
26
|
const shippedVersions = {};
|
|
23
|
-
|
|
27
|
+
// A `packageJsonUpdates` entry records `latest` rather than omitting its
|
|
28
|
+
// version, since nx requires the field.
|
|
29
|
+
let unresolved = [
|
|
30
|
+
...Object.entries(migrations.generators ?? {}).filter(([, entry])=>!entry.version).map(([key])=>key),
|
|
31
|
+
...Object.entries(migrations.packageJsonUpdates ?? {}).filter(([, entry])=>entry.version === LATEST_MIGRATIONS_DIR).map(([key])=>key)
|
|
32
|
+
];
|
|
24
33
|
for (const version of versions){
|
|
25
34
|
if (unresolved.length === 0) {
|
|
26
35
|
break;
|
|
27
36
|
}
|
|
28
|
-
const
|
|
37
|
+
const released = readReleasedMigrations(version);
|
|
38
|
+
const registered = new Set([
|
|
39
|
+
...Object.keys(released?.generators ?? {}),
|
|
40
|
+
...Object.keys(released?.packageJsonUpdates ?? {})
|
|
41
|
+
]);
|
|
29
42
|
// Entries gone at this release keep the version recorded from the release
|
|
30
43
|
// after it (if any) and stop being looked up.
|
|
31
44
|
unresolved = unresolved.filter((key)=>registered.has(key));
|
|
@@ -37,9 +50,8 @@
|
|
|
37
50
|
};
|
|
38
51
|
/**
|
|
39
52
|
* Return a copy of the migrations collection with a `version` stamped onto
|
|
40
|
-
* every generator entry, preserving any already present, and
|
|
41
|
-
* `packageJsonUpdates` entry still
|
|
42
|
-
* ships under.
|
|
53
|
+
* every generator entry, preserving any already present, and the pending version
|
|
54
|
+
* recorded on any `packageJsonUpdates` entry still holding `latest`.
|
|
43
55
|
*
|
|
44
56
|
* @param migrations parsed migrations.json to stamp
|
|
45
57
|
* @param shippedVersions migration key -> version of the earliest release
|
|
@@ -69,19 +81,9 @@
|
|
|
69
81
|
}
|
|
70
82
|
});
|
|
71
83
|
/**
|
|
72
|
-
* Version any
|
|
73
|
-
*
|
|
74
|
-
*/ const stampPackageJsonUpdates = (packageJsonUpdates, pendingVersion)=>
|
|
75
|
-
reKeyToVersion(key, pendingVersion),
|
|
76
|
-
{
|
|
77
|
-
...entry,
|
|
78
|
-
version: pendingVersion
|
|
79
|
-
}
|
|
80
|
-
] : [
|
|
81
|
-
key,
|
|
82
|
-
entry
|
|
83
|
-
]));
|
|
84
|
-
/** Swap a `latest-<name>` key for the release that ships it. */ const reKeyToVersion = (key, version)=>key.startsWith(LATEST_KEY_PREFIX) ? migrationKey(versionMigrationsDir(version), key.slice(LATEST_KEY_PREFIX.length)) : key;
|
|
84
|
+
* Version any `packageJsonUpdates` entry still holding `latest` with the release
|
|
85
|
+
* about to publish it, so the nx bump ships under a version `nx migrate` gates on.
|
|
86
|
+
*/ const stampPackageJsonUpdates = (packageJsonUpdates, pendingVersion)=>resolvePackageJsonUpdateVersions(packageJsonUpdates, ()=>pendingVersion);
|
|
85
87
|
/** Directory a newly scaffolded migration lands in, before a release claims it. */ export const LATEST_MIGRATIONS_DIR = 'latest';
|
|
86
88
|
/** Directory holding the migrations shipped by a given release. */ export const versionMigrationsDir = (version)=>`v${version}`;
|
|
87
89
|
/**
|
|
@@ -106,6 +108,9 @@ const LATEST_KEY_PREFIX = `${LATEST_MIGRATIONS_DIR}-`;
|
|
|
106
108
|
const generators = {};
|
|
107
109
|
const backfilled = [];
|
|
108
110
|
const moves = [];
|
|
111
|
+
// Reported alongside the migrations so the update's PR says an nx bump was
|
|
112
|
+
// dated, and so the caller knows there is something to commit.
|
|
113
|
+
const backfilledUpdates = Object.entries(migrations.packageJsonUpdates ?? {}).filter(([key, entry])=>entry.version === LATEST_MIGRATIONS_DIR && shippedVersions[key]).map(([key])=>key);
|
|
109
114
|
for (const [key, entry] of Object.entries(migrations.generators ?? {})){
|
|
110
115
|
const version = shippedVersions[key];
|
|
111
116
|
// Pinning an every-migration entry would stop it running on later upgrades.
|
|
@@ -149,26 +154,33 @@ const LATEST_KEY_PREFIX = `${LATEST_MIGRATIONS_DIR}-`;
|
|
|
149
154
|
...migrations,
|
|
150
155
|
generators,
|
|
151
156
|
...migrations.packageJsonUpdates && {
|
|
152
|
-
packageJsonUpdates:
|
|
153
|
-
backfilled.map((key)=>shippedVersions[key]).sort(compareVersions)[0])
|
|
157
|
+
packageJsonUpdates: resolvePackageJsonUpdateVersions(migrations.packageJsonUpdates, (key)=>shippedVersions[key])
|
|
154
158
|
}
|
|
155
159
|
},
|
|
156
|
-
backfilled
|
|
160
|
+
backfilled: [
|
|
161
|
+
...backfilled,
|
|
162
|
+
...backfilledUpdates
|
|
163
|
+
],
|
|
157
164
|
moves
|
|
158
165
|
};
|
|
159
166
|
};
|
|
160
167
|
/**
|
|
161
|
-
* Record the
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
* Record the version each `packageJsonUpdates` entry ships under, for those still
|
|
169
|
+
* holding `latest`.
|
|
170
|
+
*
|
|
171
|
+
* The key already identifies the entry — an nx bump is named for the nx version
|
|
172
|
+
* it moves to — so only the `version` field changes. Nothing is re-keyed, which
|
|
173
|
+
* is what lets one release's bump sit alongside the next without either being
|
|
174
|
+
* overwritten before it ships.
|
|
175
|
+
*/ const resolvePackageJsonUpdateVersions = (packageJsonUpdates, resolve)=>Object.fromEntries(Object.entries(packageJsonUpdates).map(([key, entry])=>{
|
|
176
|
+
const version = entry.version === LATEST_MIGRATIONS_DIR ? resolve(key) : undefined;
|
|
177
|
+
return [
|
|
170
178
|
key,
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
version ? {
|
|
180
|
+
...entry,
|
|
181
|
+
version
|
|
182
|
+
} : entry
|
|
183
|
+
];
|
|
184
|
+
}));
|
|
173
185
|
|
|
174
186
|
//# sourceMappingURL=migration-versions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../packages/nx-plugin/src/utils/migration-versions.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { compare, valid } from 'semver';\n\n/**\n * Version stamping for migrations.\n *\n * A new migration is committed with no `version` — releases are calculated from\n * conventional commits and written only to `dist/` and a git tag, so versions\n * reach the published `migrations.json` two ways:\n *\n * - At release time (`scripts/stamp-migrations.ts`) entries still missing a\n * version are stamped into the compiled `migrations.json`: an already shipped\n * one gets the earliest release tag registering it, and a net-new one gets the\n * version the release is about to publish, which the release job passes in\n * after `nx release version` writes it to the dist manifests.\n * - The weekly `update-versions` PR backfills the version of the release that\n * shipped each migration, moving and re-keying it accordingly\n * (`scripts/backfill-migration-versions.ts`), so source converges on the\n * versions of everything already released.\n *\n * A version already in source always wins, so backfilled entries are stable.\n *\n * An entry marked `everyMigration: true` is never backfilled or pinned, and is\n * re-stamped with each pending version, so it runs on every upgrade.\n */\n\nexport interface MigrationsJson {\n generators?: Record<\n string,\n { version?: string; everyMigration?: boolean } & Record<string, unknown>\n >;\n /**\n * Declarative dependency bumps `nx migrate` applies to the root manifest.\n * Keyed `<dir>-<name>` like the migrations so entries from different releases\n * can't collide; nx itself gates on the entry's `version`.\n */\n packageJsonUpdates?: Record<\n string,\n { version: string } & Record<string, unknown>\n >;\n}\n\n/** Ascending semver comparator for `Array.prototype.sort`. */\nexport const compareVersions = compare;\n\n/** Whether a string is an exact semver version. */\nexport const isValidVersion = (version: string): boolean =>\n valid(version) !== null;\n\n/**\n * Map of migration key -> version of the earliest release that registers it,\n * resolved only for the entries still missing a `version` (one already recorded\n * in source wins, so its history doesn't need reading). A migration that hasn't\n * been released is absent.\n *\n * Walks releases newest first and stops as soon as every entry is resolved: an\n * entry that has disappeared from a release's `migrations.json` was first\n * registered by the release after it, which is the one that shipped it.\n *\n * @param migrations parsed source migrations.json\n * @param versions released versions in descending semver order\n * @param readReleasedMigrations reads the `migrations.json` published by a given\n * version, or undefined if it predates the manifest\n */\nexport const readShippedMigrationVersions = (\n migrations: MigrationsJson,\n versions: string[],\n readReleasedMigrations: (version: string) => MigrationsJson | undefined,\n): Record<string, string> => {\n const shippedVersions: Record<string, string> = {};\n let unresolved = Object.entries(migrations.generators ?? {})\n .filter(([, entry]) => !entry.version)\n .map(([key]) => key);\n\n for (const version of versions) {\n if (unresolved.length === 0) {\n break;\n }\n const registered = new Set(\n Object.keys(readReleasedMigrations(version)?.generators ?? {}),\n );\n // Entries gone at this release keep the version recorded from the release\n // after it (if any) and stop being looked up.\n unresolved = unresolved.filter((key) => registered.has(key));\n for (const key of unresolved) {\n shippedVersions[key] = version;\n }\n }\n\n return shippedVersions;\n};\n\n/**\n * Return a copy of the migrations collection with a `version` stamped onto\n * every generator entry, preserving any already present, and any\n * `packageJsonUpdates` entry still keyed `latest` re-keyed to the version it\n * ships under.\n *\n * @param migrations parsed migrations.json to stamp\n * @param shippedVersions migration key -> version of the earliest release\n * tag that registers it (absent for migrations that haven't shipped)\n * @param pendingVersion version the release is about to publish, stamped onto\n * unshipped migrations so their version is one that really shipped\n */\nexport const stampMigrationVersions = (\n migrations: MigrationsJson,\n shippedVersions: Record<string, string>,\n pendingVersion: string,\n): MigrationsJson => ({\n ...migrations,\n generators: Object.fromEntries(\n // `nx migrate` sorts the run ascending by version and preserves the\n // manifest's order among equal versions, so emitting the every-migration\n // entries last is what makes them run after the release's own migrations.\n Object.entries(migrations.generators ?? {})\n .sort(\n ([, a], [, b]) =>\n Number(a.everyMigration ?? false) - Number(b.everyMigration ?? false),\n )\n .map(([name, entry]) => {\n // Source-only marker, stripped from what nx reads.\n const { everyMigration, version: sourceVersion, ...published } = entry;\n // Overrides any source version so it stays ahead of what is installed.\n const version = everyMigration\n ? pendingVersion\n : (sourceVersion ?? shippedVersions[name] ?? pendingVersion);\n return [name, { version, ...published }];\n }),\n ),\n ...(migrations.packageJsonUpdates && {\n packageJsonUpdates: stampPackageJsonUpdates(\n migrations.packageJsonUpdates,\n pendingVersion,\n ),\n }),\n});\n\n/**\n * Version any unreleased `packageJsonUpdates` entry and re-key it out of\n * `latest`, so the nx bump ships under the same version as its migrations.\n */\nconst stampPackageJsonUpdates = (\n packageJsonUpdates: NonNullable<MigrationsJson['packageJsonUpdates']>,\n pendingVersion: string,\n): NonNullable<MigrationsJson['packageJsonUpdates']> =>\n Object.fromEntries(\n Object.entries(packageJsonUpdates).map(([key, entry]) =>\n entry.version === LATEST_MIGRATIONS_DIR\n ? [\n reKeyToVersion(key, pendingVersion),\n { ...entry, version: pendingVersion },\n ]\n : [key, entry],\n ),\n );\n\n/** Swap a `latest-<name>` key for the release that ships it. */\nconst reKeyToVersion = (key: string, version: string): string =>\n key.startsWith(LATEST_KEY_PREFIX)\n ? migrationKey(\n versionMigrationsDir(version),\n key.slice(LATEST_KEY_PREFIX.length),\n )\n : key;\n\n/** Directory a newly scaffolded migration lands in, before a release claims it. */\nexport const LATEST_MIGRATIONS_DIR = 'latest';\n\n/** Directory holding the migrations shipped by a given release. */\nexport const versionMigrationsDir = (version: string) => `v${version}`;\n\n/**\n * Key a migration is registered under in `migrations.json`. Prefixed with its\n * directory so reusing a name in a later release can't silently overwrite the\n * one that already shipped.\n */\nexport const migrationKey = (dir: string, name: string) => `${dir}-${name}`;\n\nconst LATEST_KEY_PREFIX = `${LATEST_MIGRATIONS_DIR}-`;\n\n/** A migration directory move the caller needs to make on disk. */\nexport interface MigrationDirMove {\n name: string;\n version: string;\n from: string;\n to: string;\n}\n\n/**\n * Record the release that shipped each migration on entries without a version,\n * re-keying them and re-pointing their paths at that release's folder, and\n * return the collection alongside the keys that changed and the directory moves\n * to make on disk.\n *\n * Unlike `stampMigrationVersions` this only records already-released versions,\n * leaving the release to decide what a net-new migration gets.\n *\n * @param migrations parsed migrations.json to backfill\n * @param shippedVersions migration key -> version of the earliest release tag\n * that registers it (absent for migrations that haven't shipped)\n */\nexport const backfillMigrationVersions = (\n migrations: MigrationsJson,\n shippedVersions: Record<string, string>,\n): {\n migrations: MigrationsJson;\n backfilled: string[];\n moves: MigrationDirMove[];\n} => {\n const generators: NonNullable<MigrationsJson['generators']> = {};\n const backfilled: string[] = [];\n const moves: MigrationDirMove[] = [];\n\n for (const [key, entry] of Object.entries(migrations.generators ?? {})) {\n const version = shippedVersions[key];\n // Pinning an every-migration entry would stop it running on later upgrades.\n if (entry.version || !version || entry.everyMigration) {\n generators[key] = entry;\n continue;\n }\n\n const name = key.startsWith(LATEST_KEY_PREFIX)\n ? key.slice(LATEST_KEY_PREFIX.length)\n : key;\n const versionDir = versionMigrationsDir(version);\n const latestSegment = `/${LATEST_MIGRATIONS_DIR}/${name}/`;\n const versionSegment = `/${versionDir}/${name}/`;\n\n // Re-point each path field at the version folder, recording the move the\n // caller needs to make on disk the first time one is found.\n const repointed: Record<string, unknown> = {};\n let moved = false;\n for (const [field, value] of Object.entries(entry)) {\n if (typeof value !== 'string' || !value.includes(latestSegment)) {\n repointed[field] = value;\n continue;\n }\n repointed[field] = value.replace(latestSegment, versionSegment);\n if (!moved) {\n moved = true;\n const dir = value.slice(0, value.indexOf(latestSegment));\n moves.push({\n name,\n version,\n from: `${dir}${latestSegment}`.replace(/^\\.\\/|\\/$/g, ''),\n to: `${dir}${versionSegment}`.replace(/^\\.\\/|\\/$/g, ''),\n });\n }\n }\n\n generators[migrationKey(versionDir, name)] = { version, ...repointed };\n backfilled.push(key);\n }\n\n return {\n migrations: {\n ...migrations,\n generators,\n ...(migrations.packageJsonUpdates && {\n packageJsonUpdates: backfillPackageJsonUpdates(\n migrations.packageJsonUpdates,\n // Re-keyed with the earliest release it shipped alongside.\n backfilled\n .map((key) => shippedVersions[key])\n .sort(compareVersions)[0],\n ),\n }),\n },\n backfilled,\n moves,\n };\n};\n\n/**\n * Record the release that shipped an unreleased `packageJsonUpdates` entry,\n * re-keying it out of `latest`, and leave it alone until one has.\n */\nconst backfillPackageJsonUpdates = (\n packageJsonUpdates: NonNullable<MigrationsJson['packageJsonUpdates']>,\n shippedVersion: string | undefined,\n): NonNullable<MigrationsJson['packageJsonUpdates']> =>\n Object.fromEntries(\n Object.entries(packageJsonUpdates).map(([key, entry]) =>\n entry.version === LATEST_MIGRATIONS_DIR && shippedVersion\n ? [\n reKeyToVersion(key, shippedVersion),\n { ...entry, version: shippedVersion },\n ]\n : [key, entry],\n ),\n );\n"],"names":["compare","valid","compareVersions","isValidVersion","version","readShippedMigrationVersions","migrations","versions","readReleasedMigrations","shippedVersions","unresolved","Object","entries","generators","filter","entry","map","key","length","registered","Set","keys","has","stampMigrationVersions","pendingVersion","fromEntries","sort","a","b","Number","everyMigration","name","sourceVersion","published","packageJsonUpdates","stampPackageJsonUpdates","LATEST_MIGRATIONS_DIR","reKeyToVersion","startsWith","LATEST_KEY_PREFIX","migrationKey","versionMigrationsDir","slice","dir","backfillMigrationVersions","backfilled","moves","versionDir","latestSegment","versionSegment","repointed","moved","field","value","includes","replace","indexOf","push","from","to","backfillPackageJsonUpdates","shippedVersion"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../packages/nx-plugin/src/utils/migration-versions.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { compare, valid } from 'semver';\n\n/**\n * Version stamping for migrations.\n *\n * A new migration is committed with no `version` — releases are calculated from\n * conventional commits and written only to `dist/` and a git tag, so versions\n * reach the published `migrations.json` two ways:\n *\n * - At release time (`scripts/stamp-migrations.ts`) entries still missing a\n * version are stamped into the compiled `migrations.json`: an already shipped\n * one gets the earliest release tag registering it, and a net-new one gets the\n * version the release is about to publish, which the release job passes in\n * after `nx release version` writes it to the dist manifests.\n * - The weekly `update-versions` PR backfills the version of the release that\n * shipped each migration, moving and re-keying it accordingly\n * (`scripts/backfill-migration-versions.ts`), so source converges on the\n * versions of everything already released. `packageJsonUpdates` entries are\n * dated in place there too, since their keys never change.\n *\n * A version already in source always wins, so backfilled entries are stable.\n *\n * An entry marked `everyMigration: true` is never backfilled or pinned, and is\n * re-stamped with each pending version, so it runs on every upgrade.\n */\n\nexport interface MigrationsJson {\n generators?: Record<\n string,\n { version?: string; everyMigration?: boolean } & Record<string, unknown>\n >;\n /**\n * Declarative dependency bumps `nx migrate` applies to the root manifest.\n * Keyed by what the bump targets rather than the release that ships it, so an\n * entry is unique from the moment it is written and one release's bump can sit\n * beside the next; nx itself gates on the entry's `version`.\n */\n packageJsonUpdates?: Record<\n string,\n { version: string } & Record<string, unknown>\n >;\n}\n\n/** Ascending semver comparator for `Array.prototype.sort`. */\nexport const compareVersions = compare;\n\n/** Whether a string is an exact semver version. */\nexport const isValidVersion = (version: string): boolean =>\n valid(version) !== null;\n\n/**\n * Map of entry key -> version of the earliest release that registers it, across\n * both `generators` and `packageJsonUpdates`. Resolved only for the entries still\n * missing a version (one already recorded in source wins, so its history doesn't\n * need reading). An entry that hasn't been released is absent.\n *\n * Walks releases newest first and stops as soon as every entry is resolved: an\n * entry that has disappeared from a release's `migrations.json` was first\n * registered by the release after it, which is the one that shipped it.\n *\n * Both sections resolve the same way and share the walk, so the release doesn't\n * read tag history twice. Keys can't collide: a migration is keyed by its own\n * name, an nx bump by the nx version it moves to.\n *\n * @param migrations parsed source migrations.json\n * @param versions released versions in descending semver order\n * @param readReleasedMigrations reads the `migrations.json` published by a given\n * version, or undefined if it predates the manifest\n */\nexport const readShippedMigrationVersions = (\n migrations: MigrationsJson,\n versions: string[],\n readReleasedMigrations: (version: string) => MigrationsJson | undefined,\n): Record<string, string> => {\n const shippedVersions: Record<string, string> = {};\n // A `packageJsonUpdates` entry records `latest` rather than omitting its\n // version, since nx requires the field.\n let unresolved = [\n ...Object.entries(migrations.generators ?? {})\n .filter(([, entry]) => !entry.version)\n .map(([key]) => key),\n ...Object.entries(migrations.packageJsonUpdates ?? {})\n .filter(([, entry]) => entry.version === LATEST_MIGRATIONS_DIR)\n .map(([key]) => key),\n ];\n\n for (const version of versions) {\n if (unresolved.length === 0) {\n break;\n }\n const released = readReleasedMigrations(version);\n const registered = new Set([\n ...Object.keys(released?.generators ?? {}),\n ...Object.keys(released?.packageJsonUpdates ?? {}),\n ]);\n // Entries gone at this release keep the version recorded from the release\n // after it (if any) and stop being looked up.\n unresolved = unresolved.filter((key) => registered.has(key));\n for (const key of unresolved) {\n shippedVersions[key] = version;\n }\n }\n\n return shippedVersions;\n};\n\n/**\n * Return a copy of the migrations collection with a `version` stamped onto\n * every generator entry, preserving any already present, and the pending version\n * recorded on any `packageJsonUpdates` entry still holding `latest`.\n *\n * @param migrations parsed migrations.json to stamp\n * @param shippedVersions migration key -> version of the earliest release\n * tag that registers it (absent for migrations that haven't shipped)\n * @param pendingVersion version the release is about to publish, stamped onto\n * unshipped migrations so their version is one that really shipped\n */\nexport const stampMigrationVersions = (\n migrations: MigrationsJson,\n shippedVersions: Record<string, string>,\n pendingVersion: string,\n): MigrationsJson => ({\n ...migrations,\n generators: Object.fromEntries(\n // `nx migrate` sorts the run ascending by version and preserves the\n // manifest's order among equal versions, so emitting the every-migration\n // entries last is what makes them run after the release's own migrations.\n Object.entries(migrations.generators ?? {})\n .sort(\n ([, a], [, b]) =>\n Number(a.everyMigration ?? false) - Number(b.everyMigration ?? false),\n )\n .map(([name, entry]) => {\n // Source-only marker, stripped from what nx reads.\n const { everyMigration, version: sourceVersion, ...published } = entry;\n // Overrides any source version so it stays ahead of what is installed.\n const version = everyMigration\n ? pendingVersion\n : (sourceVersion ?? shippedVersions[name] ?? pendingVersion);\n return [name, { version, ...published }];\n }),\n ),\n ...(migrations.packageJsonUpdates && {\n packageJsonUpdates: stampPackageJsonUpdates(\n migrations.packageJsonUpdates,\n pendingVersion,\n ),\n }),\n});\n\n/**\n * Version any `packageJsonUpdates` entry still holding `latest` with the release\n * about to publish it, so the nx bump ships under a version `nx migrate` gates on.\n */\nconst stampPackageJsonUpdates = (\n packageJsonUpdates: NonNullable<MigrationsJson['packageJsonUpdates']>,\n pendingVersion: string,\n): NonNullable<MigrationsJson['packageJsonUpdates']> =>\n resolvePackageJsonUpdateVersions(packageJsonUpdates, () => pendingVersion);\n\n/** Directory a newly scaffolded migration lands in, before a release claims it. */\nexport const LATEST_MIGRATIONS_DIR = 'latest';\n\n/** Directory holding the migrations shipped by a given release. */\nexport const versionMigrationsDir = (version: string) => `v${version}`;\n\n/**\n * Key a migration is registered under in `migrations.json`. Prefixed with its\n * directory so reusing a name in a later release can't silently overwrite the\n * one that already shipped.\n */\nexport const migrationKey = (dir: string, name: string) => `${dir}-${name}`;\n\nconst LATEST_KEY_PREFIX = `${LATEST_MIGRATIONS_DIR}-`;\n\n/** A migration directory move the caller needs to make on disk. */\nexport interface MigrationDirMove {\n name: string;\n version: string;\n from: string;\n to: string;\n}\n\n/**\n * Record the release that shipped each migration on entries without a version,\n * re-keying them and re-pointing their paths at that release's folder, and\n * return the collection alongside the keys that changed and the directory moves\n * to make on disk.\n *\n * Unlike `stampMigrationVersions` this only records already-released versions,\n * leaving the release to decide what a net-new migration gets.\n *\n * @param migrations parsed migrations.json to backfill\n * @param shippedVersions migration key -> version of the earliest release tag\n * that registers it (absent for migrations that haven't shipped)\n */\nexport const backfillMigrationVersions = (\n migrations: MigrationsJson,\n shippedVersions: Record<string, string>,\n): {\n migrations: MigrationsJson;\n backfilled: string[];\n moves: MigrationDirMove[];\n} => {\n const generators: NonNullable<MigrationsJson['generators']> = {};\n const backfilled: string[] = [];\n const moves: MigrationDirMove[] = [];\n // Reported alongside the migrations so the update's PR says an nx bump was\n // dated, and so the caller knows there is something to commit.\n const backfilledUpdates = Object.entries(migrations.packageJsonUpdates ?? {})\n .filter(\n ([key, entry]) =>\n entry.version === LATEST_MIGRATIONS_DIR && shippedVersions[key],\n )\n .map(([key]) => key);\n\n for (const [key, entry] of Object.entries(migrations.generators ?? {})) {\n const version = shippedVersions[key];\n // Pinning an every-migration entry would stop it running on later upgrades.\n if (entry.version || !version || entry.everyMigration) {\n generators[key] = entry;\n continue;\n }\n\n const name = key.startsWith(LATEST_KEY_PREFIX)\n ? key.slice(LATEST_KEY_PREFIX.length)\n : key;\n const versionDir = versionMigrationsDir(version);\n const latestSegment = `/${LATEST_MIGRATIONS_DIR}/${name}/`;\n const versionSegment = `/${versionDir}/${name}/`;\n\n // Re-point each path field at the version folder, recording the move the\n // caller needs to make on disk the first time one is found.\n const repointed: Record<string, unknown> = {};\n let moved = false;\n for (const [field, value] of Object.entries(entry)) {\n if (typeof value !== 'string' || !value.includes(latestSegment)) {\n repointed[field] = value;\n continue;\n }\n repointed[field] = value.replace(latestSegment, versionSegment);\n if (!moved) {\n moved = true;\n const dir = value.slice(0, value.indexOf(latestSegment));\n moves.push({\n name,\n version,\n from: `${dir}${latestSegment}`.replace(/^\\.\\/|\\/$/g, ''),\n to: `${dir}${versionSegment}`.replace(/^\\.\\/|\\/$/g, ''),\n });\n }\n }\n\n generators[migrationKey(versionDir, name)] = { version, ...repointed };\n backfilled.push(key);\n }\n\n return {\n migrations: {\n ...migrations,\n generators,\n ...(migrations.packageJsonUpdates && {\n packageJsonUpdates: resolvePackageJsonUpdateVersions(\n migrations.packageJsonUpdates,\n (key) => shippedVersions[key],\n ),\n }),\n },\n backfilled: [...backfilled, ...backfilledUpdates],\n moves,\n };\n};\n\n/**\n * Record the version each `packageJsonUpdates` entry ships under, for those still\n * holding `latest`.\n *\n * The key already identifies the entry — an nx bump is named for the nx version\n * it moves to — so only the `version` field changes. Nothing is re-keyed, which\n * is what lets one release's bump sit alongside the next without either being\n * overwritten before it ships.\n */\nconst resolvePackageJsonUpdateVersions = (\n packageJsonUpdates: NonNullable<MigrationsJson['packageJsonUpdates']>,\n resolve: (key: string) => string | undefined,\n): NonNullable<MigrationsJson['packageJsonUpdates']> =>\n Object.fromEntries(\n Object.entries(packageJsonUpdates).map(([key, entry]) => {\n const version =\n entry.version === LATEST_MIGRATIONS_DIR ? resolve(key) : undefined;\n return [key, version ? { ...entry, version } : entry];\n }),\n );\n"],"names":["compare","valid","compareVersions","isValidVersion","version","readShippedMigrationVersions","migrations","versions","readReleasedMigrations","shippedVersions","unresolved","Object","entries","generators","filter","entry","map","key","packageJsonUpdates","LATEST_MIGRATIONS_DIR","length","released","registered","Set","keys","has","stampMigrationVersions","pendingVersion","fromEntries","sort","a","b","Number","everyMigration","name","sourceVersion","published","stampPackageJsonUpdates","resolvePackageJsonUpdateVersions","versionMigrationsDir","migrationKey","dir","LATEST_KEY_PREFIX","backfillMigrationVersions","backfilled","moves","backfilledUpdates","startsWith","slice","versionDir","latestSegment","versionSegment","repointed","moved","field","value","includes","replace","indexOf","push","from","to","resolve","undefined"],"mappings":"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"}
|
|
@@ -22,14 +22,24 @@ export interface PackageJsonUpdate extends Record<string, unknown> {
|
|
|
22
22
|
}
|
|
23
23
|
export type PackageJsonUpdates = Record<string, PackageJsonUpdate>;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Key an nx bump is registered under, named for the nx version it moves to.
|
|
26
|
+
*
|
|
27
|
+
* The plugin version the bump ships under isn't known when it is written — the
|
|
28
|
+
* weekly update writes it, and only the release that publishes it can say which
|
|
29
|
+
* version that is. The nx version, though, is exactly what the entry is *for*,
|
|
30
|
+
* and two bumps to the same nx version would be the same bump — so it keys the
|
|
31
|
+
* entry uniquely from the moment it is written, with no re-keying later.
|
|
26
32
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
33
|
+
* That matters because each release's bump has to stay behind as the next is
|
|
34
|
+
* written: a workspace several releases behind gets every nx hop in turn rather
|
|
35
|
+
* than only the newest. A key that had to be rewritten once the plugin version
|
|
36
|
+
* was known would let a second bump land on the first before either shipped.
|
|
37
|
+
*/
|
|
38
|
+
export declare const nxPackageUpdatesKey: (nxVersion: string) => string;
|
|
39
|
+
/**
|
|
40
|
+
* `alwaysAddToPackageJson: false` so only packages already present are updated.
|
|
32
41
|
*
|
|
33
|
-
* @param version version `nx migrate` gates the bump on
|
|
42
|
+
* @param version plugin version `nx migrate` gates the bump on, or
|
|
43
|
+
* {@link LATEST_MIGRATIONS_DIR} while it is still waiting for a release
|
|
34
44
|
*/
|
|
35
|
-
export declare const nxPackageJsonUpdates: (
|
|
45
|
+
export declare const nxPackageJsonUpdates: (version: string, nxVersion?: string) => PackageJsonUpdates;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/ import {
|
|
5
|
-
import { NX_PACKAGES, NX_VERSION } from "../versions.js";
|
|
4
|
+
*/ import { NX_PACKAGES, NX_VERSION } from "../versions.js";
|
|
6
5
|
/**
|
|
7
6
|
* `packageJsonUpdates` for the nx packages a generated workspace pins.
|
|
8
7
|
*
|
|
@@ -17,17 +16,26 @@ import { NX_PACKAGES, NX_VERSION } from "../versions.js";
|
|
|
17
16
|
*/ export const NX_PACKAGE_UPDATES_NAME = 'nx-packages';
|
|
18
17
|
export const isNxPackage = (name)=>NX_PACKAGES.includes(name);
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
19
|
+
* Key an nx bump is registered under, named for the nx version it moves to.
|
|
20
|
+
*
|
|
21
|
+
* The plugin version the bump ships under isn't known when it is written — the
|
|
22
|
+
* weekly update writes it, and only the release that publishes it can say which
|
|
23
|
+
* version that is. The nx version, though, is exactly what the entry is *for*,
|
|
24
|
+
* and two bumps to the same nx version would be the same bump — so it keys the
|
|
25
|
+
* entry uniquely from the moment it is written, with no re-keying later.
|
|
21
26
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
+
* That matters because each release's bump has to stay behind as the next is
|
|
28
|
+
* written: a workspace several releases behind gets every nx hop in turn rather
|
|
29
|
+
* than only the newest. A key that had to be rewritten once the plugin version
|
|
30
|
+
* was known would let a second bump land on the first before either shipped.
|
|
31
|
+
*/ export const nxPackageUpdatesKey = (nxVersion)=>`nx-${nxVersion}-${NX_PACKAGE_UPDATES_NAME}`;
|
|
32
|
+
/**
|
|
33
|
+
* `alwaysAddToPackageJson: false` so only packages already present are updated.
|
|
27
34
|
*
|
|
28
|
-
* @param version version `nx migrate` gates the bump on
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
* @param version plugin version `nx migrate` gates the bump on, or
|
|
36
|
+
* {@link LATEST_MIGRATIONS_DIR} while it is still waiting for a release
|
|
37
|
+
*/ export const nxPackageJsonUpdates = (version, nxVersion = NX_VERSION)=>({
|
|
38
|
+
[nxPackageUpdatesKey(nxVersion)]: {
|
|
31
39
|
version,
|
|
32
40
|
packages: Object.fromEntries(NX_PACKAGES.map((name)=>[
|
|
33
41
|
name,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/version-upgrade-migration/nx-package-updates.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { migrationKey } from '../migration-versions';\nimport { NX_PACKAGES, NX_VERSION } from '../versions';\n\n/**\n * `packageJsonUpdates` for the nx packages a generated workspace pins.\n *\n * A migration cannot bump these: `nx migrate` builds its migration list from the\n * packages it is itself bumping, so an nx version only rewritten by our migration\n * silently skips Nx's own migrations for that hop. Declaring the bump here pulls\n * them in.\n *\n * It also keeps every nx pin moving together. A workspace nx even a patch apart\n * from the plugin's `@nx/*` packages hoists a second nested nx, and the two\n * deadlock `nx sync`.\n */\n\nexport const NX_PACKAGE_UPDATES_NAME = 'nx-packages';\n\nexport const isNxPackage = (name: string): boolean =>\n (NX_PACKAGES as readonly string[]).includes(name);\n\n/** Indexed to satisfy the open shape `migrations.json` entries carry. */\nexport interface PackageJsonUpdate extends Record<string, unknown> {\n version: string;\n packages: Record<string, { version: string; alwaysAddToPackageJson: false }>;\n}\n\nexport type PackageJsonUpdates = Record<string, PackageJsonUpdate>;\n\n/**\n *
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/version-upgrade-migration/nx-package-updates.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { migrationKey } from '../migration-versions';\nimport { NX_PACKAGES, NX_VERSION } from '../versions';\n\n/**\n * `packageJsonUpdates` for the nx packages a generated workspace pins.\n *\n * A migration cannot bump these: `nx migrate` builds its migration list from the\n * packages it is itself bumping, so an nx version only rewritten by our migration\n * silently skips Nx's own migrations for that hop. Declaring the bump here pulls\n * them in.\n *\n * It also keeps every nx pin moving together. A workspace nx even a patch apart\n * from the plugin's `@nx/*` packages hoists a second nested nx, and the two\n * deadlock `nx sync`.\n */\n\nexport const NX_PACKAGE_UPDATES_NAME = 'nx-packages';\n\nexport const isNxPackage = (name: string): boolean =>\n (NX_PACKAGES as readonly string[]).includes(name);\n\n/** Indexed to satisfy the open shape `migrations.json` entries carry. */\nexport interface PackageJsonUpdate extends Record<string, unknown> {\n version: string;\n packages: Record<string, { version: string; alwaysAddToPackageJson: false }>;\n}\n\nexport type PackageJsonUpdates = Record<string, PackageJsonUpdate>;\n\n/**\n * Key an nx bump is registered under, named for the nx version it moves to.\n *\n * The plugin version the bump ships under isn't known when it is written — the\n * weekly update writes it, and only the release that publishes it can say which\n * version that is. The nx version, though, is exactly what the entry is *for*,\n * and two bumps to the same nx version would be the same bump — so it keys the\n * entry uniquely from the moment it is written, with no re-keying later.\n *\n * That matters because each release's bump has to stay behind as the next is\n * written: a workspace several releases behind gets every nx hop in turn rather\n * than only the newest. A key that had to be rewritten once the plugin version\n * was known would let a second bump land on the first before either shipped.\n */\nexport const nxPackageUpdatesKey = (nxVersion: string) =>\n `nx-${nxVersion}-${NX_PACKAGE_UPDATES_NAME}`;\n\n/**\n * `alwaysAddToPackageJson: false` so only packages already present are updated.\n *\n * @param version plugin version `nx migrate` gates the bump on, or\n * {@link LATEST_MIGRATIONS_DIR} while it is still waiting for a release\n */\nexport const nxPackageJsonUpdates = (\n version: string,\n nxVersion: string = NX_VERSION,\n): PackageJsonUpdates => ({\n [nxPackageUpdatesKey(nxVersion)]: {\n version,\n packages: Object.fromEntries(\n NX_PACKAGES.map((name) => [\n name,\n { version: nxVersion, alwaysAddToPackageJson: false as const },\n ]),\n ),\n },\n});\n"],"names":["NX_PACKAGES","NX_VERSION","NX_PACKAGE_UPDATES_NAME","isNxPackage","name","includes","nxPackageUpdatesKey","nxVersion","nxPackageJsonUpdates","version","packages","Object","fromEntries","map","alwaysAddToPackageJson"],"mappings":"AAAA;;;CAGC,GAED,SAASA,WAAW,EAAEC,UAAU,QAAQ,iBAAc;AAEtD;;;;;;;;;;;CAWC,GAED,OAAO,MAAMC,0BAA0B,cAAc;AAErD,OAAO,MAAMC,cAAc,CAACC,OAC1B,AAACJ,YAAkCK,QAAQ,CAACD,MAAM;AAUpD;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAME,sBAAsB,CAACC,YAClC,CAAC,GAAG,EAAEA,UAAU,CAAC,EAAEL,yBAAyB,CAAC;AAE/C;;;;;CAKC,GACD,OAAO,MAAMM,uBAAuB,CAClCC,SACAF,YAAoBN,UAAU,GACN,CAAA;QACxB,CAACK,oBAAoBC,WAAW,EAAE;YAChCE;YACAC,UAAUC,OAAOC,WAAW,CAC1BZ,YAAYa,GAAG,CAAC,CAACT,OAAS;oBACxBA;oBACA;wBAAEK,SAASF;wBAAWO,wBAAwB;oBAAe;iBAC9D;QAEL;IACF,CAAA,EAAG"}
|
|
@@ -11,7 +11,12 @@ import { type Tree } from '@nx/devkit';
|
|
|
11
11
|
* only the nx packages need registering per update: they go through
|
|
12
12
|
* `packageJsonUpdates` rather than a migration (see `nx-package-updates.ts`).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* A bump already dated by a release stays — a workspace several releases behind
|
|
15
|
+
* needs each hop in turn. One still waiting for a release is dropped: this bump
|
|
16
|
+
* supersedes it, and both would otherwise ship under the same version, leaving
|
|
17
|
+
* which nx a workspace lands on down to the order nx happens to apply them in.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: re-running before a release replaces the pending entry with itself.
|
|
15
20
|
*
|
|
16
21
|
* @returns paths written, for the update report
|
|
17
22
|
*/
|
|
@@ -13,7 +13,12 @@ const MIGRATIONS_JSON_PATH = 'packages/nx-plugin/migrations.json';
|
|
|
13
13
|
* only the nx packages need registering per update: they go through
|
|
14
14
|
* `packageJsonUpdates` rather than a migration (see `nx-package-updates.ts`).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* A bump already dated by a release stays — a workspace several releases behind
|
|
17
|
+
* needs each hop in turn. One still waiting for a release is dropped: this bump
|
|
18
|
+
* supersedes it, and both would otherwise ship under the same version, leaving
|
|
19
|
+
* which nx a workspace lands on down to the order nx happens to apply them in.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent: re-running before a release replaces the pending entry with itself.
|
|
17
22
|
*
|
|
18
23
|
* @returns paths written, for the update report
|
|
19
24
|
*/ export const registerNxPackageUpdates = (tree)=>{
|
|
@@ -21,8 +26,8 @@ const MIGRATIONS_JSON_PATH = 'packages/nx-plugin/migrations.json';
|
|
|
21
26
|
...migrations,
|
|
22
27
|
// Recorded under `latest` until stamping resolves the version it ships with.
|
|
23
28
|
packageJsonUpdates: {
|
|
24
|
-
...migrations.packageJsonUpdates,
|
|
25
|
-
...nxPackageJsonUpdates(LATEST_MIGRATIONS_DIR
|
|
29
|
+
...Object.fromEntries(Object.entries(migrations.packageJsonUpdates ?? {}).filter(([, entry])=>entry.version !== LATEST_MIGRATIONS_DIR)),
|
|
30
|
+
...nxPackageJsonUpdates(LATEST_MIGRATIONS_DIR)
|
|
26
31
|
}
|
|
27
32
|
}));
|
|
28
33
|
return [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/version-upgrade-migration/register.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { type Tree, updateJson } from '@nx/devkit';\nimport {\n LATEST_MIGRATIONS_DIR,\n type MigrationsJson,\n} from '../migration-versions';\nimport { nxPackageJsonUpdates } from './nx-package-updates';\n\nconst MIGRATIONS_JSON_PATH = 'packages/nx-plugin/migrations.json';\n\n/**\n * Register the nx bump a version update needs, in `migrations.json`. Call only\n * when an nx package actually moved.\n *\n * The version sync migration itself is a committed `everyMigration` entry, so\n * only the nx packages need registering per update: they go through\n * `packageJsonUpdates` rather than a migration (see `nx-package-updates.ts`).\n *\n * Idempotent: re-running before a release
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/version-upgrade-migration/register.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { type Tree, updateJson } from '@nx/devkit';\nimport {\n LATEST_MIGRATIONS_DIR,\n type MigrationsJson,\n} from '../migration-versions';\nimport { nxPackageJsonUpdates } from './nx-package-updates';\n\nconst MIGRATIONS_JSON_PATH = 'packages/nx-plugin/migrations.json';\n\n/**\n * Register the nx bump a version update needs, in `migrations.json`. Call only\n * when an nx package actually moved.\n *\n * The version sync migration itself is a committed `everyMigration` entry, so\n * only the nx packages need registering per update: they go through\n * `packageJsonUpdates` rather than a migration (see `nx-package-updates.ts`).\n *\n * A bump already dated by a release stays — a workspace several releases behind\n * needs each hop in turn. One still waiting for a release is dropped: this bump\n * supersedes it, and both would otherwise ship under the same version, leaving\n * which nx a workspace lands on down to the order nx happens to apply them in.\n *\n * Idempotent: re-running before a release replaces the pending entry with itself.\n *\n * @returns paths written, for the update report\n */\nexport const registerNxPackageUpdates = (tree: Tree): string[] => {\n updateJson<MigrationsJson>(tree, MIGRATIONS_JSON_PATH, (migrations) => ({\n ...migrations,\n // Recorded under `latest` until stamping resolves the version it ships with.\n packageJsonUpdates: {\n ...Object.fromEntries(\n Object.entries(migrations.packageJsonUpdates ?? {}).filter(\n ([, entry]) => entry.version !== LATEST_MIGRATIONS_DIR,\n ),\n ),\n ...nxPackageJsonUpdates(LATEST_MIGRATIONS_DIR),\n },\n }));\n\n return [MIGRATIONS_JSON_PATH];\n};\n"],"names":["updateJson","LATEST_MIGRATIONS_DIR","nxPackageJsonUpdates","MIGRATIONS_JSON_PATH","registerNxPackageUpdates","tree","migrations","packageJsonUpdates","Object","fromEntries","entries","filter","entry","version"],"mappings":"AAAA;;;CAGC,GACD,SAAoBA,UAAU,QAAQ,aAAa;AACnD,SACEC,qBAAqB,QAEhB,2BAAwB;AAC/B,SAASC,oBAAoB,QAAQ,0BAAuB;AAE5D,MAAMC,uBAAuB;AAE7B;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,2BAA2B,CAACC;IACvCL,WAA2BK,MAAMF,sBAAsB,CAACG,aAAgB,CAAA;YACtE,GAAGA,UAAU;YACb,6EAA6E;YAC7EC,oBAAoB;gBAClB,GAAGC,OAAOC,WAAW,CACnBD,OAAOE,OAAO,CAACJ,WAAWC,kBAAkB,IAAI,CAAC,GAAGI,MAAM,CACxD,CAAC,GAAGC,MAAM,GAAKA,MAAMC,OAAO,KAAKZ,uBAEpC;gBACD,GAAGC,qBAAqBD,sBAAsB;YAChD;QACF,CAAA;IAEA,OAAO;QAACE;KAAqB;AAC/B,EAAE"}
|