@aws/nx-plugin 1.0.0-rc.57 → 1.0.0-rc.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE-THIRD-PARTY +2917 -1295
  2. package/migrations.json +31 -26
  3. package/package.json +13 -13
  4. package/src/agentcore-gateway/generator.d.ts +16 -1
  5. package/src/agentcore-gateway/generator.js +3 -2
  6. package/src/agentcore-gateway/generator.js.map +1 -1
  7. package/src/agentcore-gateway/schema.json +1 -0
  8. package/src/connection/scaffold-catalog.d.ts +160 -0
  9. package/src/connection/scaffold-catalog.js +207 -0
  10. package/src/connection/scaffold-catalog.js.map +1 -0
  11. package/src/connection/schema-resolver.d.ts +5 -0
  12. package/src/connection/schema-resolver.js +27 -0
  13. package/src/connection/schema-resolver.js.map +1 -0
  14. package/src/infra/app/__snapshots__/generator.spec.ts.snap +7 -7
  15. package/src/migrations/latest/smithy-ssdk-bundle-pins/migration.js +78 -0
  16. package/src/migrations/latest/smithy-ssdk-bundle-pins/migration.js.map +1 -0
  17. package/src/migrations/v1.0.0-rc.50/modernize-function-props-cast/migration.js.map +1 -0
  18. package/src/migrations/v1.0.0-rc.50/restrict-cors-to-custom-domains/migration.js.map +1 -0
  19. package/src/migrations/v1.0.0-rc.50/terraform-bootstrap-adopt-existing-bucket/migration.js.map +1 -0
  20. package/src/migrations/v1.0.0-rc.52/user-identity-waf-allow-localhost-callback/migration.js.map +1 -0
  21. package/src/migrations/v1.0.0-rc.54/order-access-log-delivery-after-bucket-policy/migration.js.map +1 -0
  22. package/src/migrations/v1.0.0-rc.55/backfill-generator-metadata/migration.js.map +1 -0
  23. package/src/migrations/v1.0.0-rc.56/dynamodb-local-remove-optimize-flag/migration.js.map +1 -0
  24. package/src/migrations/v1.0.0-rc.57/ts-agent-session-management-support/migration.d.ts +6 -0
  25. package/src/migrations/v1.0.0-rc.57/ts-agent-session-management-support/migration.js.map +1 -0
  26. package/src/preset/__snapshots__/generator.spec.ts.snap +5 -5
  27. package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +8 -8
  28. package/src/py/agent/__snapshots__/generator.frameworks.spec.ts.snap +8 -8
  29. package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +2 -2
  30. package/src/py/agent/a2a-connection/generator.d.ts +1 -1
  31. package/src/py/agent/gateway-connection/generator.d.ts +1 -1
  32. package/src/py/agent/mcp-connection/generator.d.ts +1 -1
  33. package/src/py/agent/schema.json +1 -0
  34. package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
  35. package/src/py/dynamodb/schema.json +1 -0
  36. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +12 -12
  37. package/src/py/fast-api/generator.d.ts +5 -0
  38. package/src/py/fast-api/generator.js +3 -2
  39. package/src/py/fast-api/generator.js.map +1 -1
  40. package/src/py/fast-api/schema.json +1 -0
  41. package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
  42. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +5 -5
  43. package/src/py/mcp-server/schema.json +1 -0
  44. package/src/py/rdb/__snapshots__/generator.spec.ts.snap +2 -2
  45. package/src/py/rdb/schema.json +1 -0
  46. package/src/smithy/project/__snapshots__/generator.spec.ts.snap +4 -4
  47. package/src/smithy/project/files/service/build.Dockerfile.template +2 -2
  48. package/src/smithy/project/generator.d.ts +25 -0
  49. package/src/smithy/project/generator.js +24 -3
  50. package/src/smithy/project/generator.js.map +1 -1
  51. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +7 -7
  52. package/src/smithy/ts/api/generator.d.ts +6 -1
  53. package/src/smithy/ts/api/generator.js +3 -2
  54. package/src/smithy/ts/api/generator.js.map +1 -1
  55. package/src/smithy/ts/api/schema.json +1 -0
  56. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +12 -12
  57. package/src/trpc/backend/generator.d.ts +6 -1
  58. package/src/trpc/backend/generator.js +3 -2
  59. package/src/trpc/backend/generator.js.map +1 -1
  60. package/src/trpc/backend/schema.json +1 -0
  61. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +5 -5
  62. package/src/ts/agent/generator.d.ts +32 -1
  63. package/src/ts/agent/generator.js +7 -4
  64. package/src/ts/agent/generator.js.map +1 -1
  65. package/src/ts/agent/schema.json +1 -0
  66. package/src/ts/dcr-proxy/__snapshots__/generator.spec.ts.snap +1 -1
  67. package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
  68. package/src/ts/dynamodb/schema.json +1 -0
  69. package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
  70. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +2 -2
  71. package/src/ts/mcp-server/generator.d.ts +32 -1
  72. package/src/ts/mcp-server/generator.js +7 -4
  73. package/src/ts/mcp-server/generator.js.map +1 -1
  74. package/src/ts/mcp-server/schema.json +1 -0
  75. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +6 -6
  76. package/src/ts/rdb/generator.d.ts +8 -0
  77. package/src/ts/rdb/generator.js +3 -2
  78. package/src/ts/rdb/generator.js.map +1 -1
  79. package/src/ts/rdb/schema.json +1 -0
  80. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +61 -45
  81. package/src/ts/react-website/app/generator.d.ts +6 -1
  82. package/src/ts/react-website/app/generator.js +3 -2
  83. package/src/ts/react-website/app/generator.js.map +1 -1
  84. package/src/ts/react-website/app/schema.json +1 -0
  85. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +2 -2
  86. package/src/ts/react-website/cognito-auth/generator.d.ts +6 -1
  87. package/src/ts/react-website/cognito-auth/generator.js +3 -2
  88. package/src/ts/react-website/cognito-auth/generator.js.map +1 -1
  89. package/src/utils/agent-core-constructs/agent-core-constructs.d.ts +18 -0
  90. package/src/utils/agent-core-constructs/agent-core-constructs.js +22 -1
  91. package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
  92. package/src/utils/api-constructs/api-constructs.d.ts +9 -0
  93. package/src/utils/api-constructs/api-constructs.js +11 -1
  94. package/src/utils/api-constructs/api-constructs.js.map +1 -1
  95. package/src/utils/docker.d.ts +27 -2
  96. package/src/utils/docker.js +31 -2
  97. package/src/utils/docker.js.map +1 -1
  98. package/src/utils/identity-constructs/identity-constructs.d.ts +10 -0
  99. package/src/utils/identity-constructs/identity-constructs.js +11 -1
  100. package/src/utils/identity-constructs/identity-constructs.js.map +1 -1
  101. package/src/utils/shared-constructs-constants.d.ts +6 -0
  102. package/src/utils/shared-constructs-constants.js +5 -0
  103. package/src/utils/shared-constructs-constants.js.map +1 -1
  104. package/src/utils/version-upgrade-migration/owned-dependencies.d.ts +26 -0
  105. package/src/utils/version-upgrade-migration/owned-dependencies.js +52 -4
  106. package/src/utils/version-upgrade-migration/owned-dependencies.js.map +1 -1
  107. package/src/utils/version-upgrade-migration/register.d.ts +5 -1
  108. package/src/utils/version-upgrade-migration/register.js +6 -2
  109. package/src/utils/version-upgrade-migration/register.js.map +1 -1
  110. package/src/utils/version-upgrade-migration/sync-embedded-versions.d.ts +26 -0
  111. package/src/utils/version-upgrade-migration/sync-embedded-versions.js +304 -0
  112. package/src/utils/version-upgrade-migration/sync-embedded-versions.js.map +1 -0
  113. package/src/utils/version-upgrade-migration/sync-vended-versions.js +14 -0
  114. package/src/utils/version-upgrade-migration/sync-vended-versions.js.map +1 -1
  115. package/src/utils/versions.d.ts +90 -77
  116. package/src/utils/versions.js +86 -75
  117. package/src/utils/versions.js.map +1 -1
  118. package/src/utils/website-constructs/website-constructs.d.ts +11 -0
  119. package/src/utils/website-constructs/website-constructs.js +12 -1
  120. package/src/utils/website-constructs/website-constructs.js.map +1 -1
  121. package/src/migrations/latest/backfill-generator-metadata/migration.js.map +0 -1
  122. package/src/migrations/latest/dynamodb-local-remove-optimize-flag/migration.js.map +0 -1
  123. package/src/migrations/latest/modernize-function-props-cast/migration.js.map +0 -1
  124. package/src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration.js.map +0 -1
  125. package/src/migrations/latest/restrict-cors-to-custom-domains/migration.js.map +0 -1
  126. package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.js.map +0 -1
  127. package/src/migrations/latest/ts-agent-session-management-support/migration.js.map +0 -1
  128. package/src/migrations/latest/user-identity-waf-allow-localhost-callback/migration.js.map +0 -1
  129. /package/src/migrations/latest/{modernize-function-props-cast → smithy-ssdk-bundle-pins}/migration.d.ts +0 -0
  130. /package/src/migrations/{latest/restrict-cors-to-custom-domains → v1.0.0-rc.50/modernize-function-props-cast}/migration.d.ts +0 -0
  131. /package/src/migrations/{latest → v1.0.0-rc.50}/modernize-function-props-cast/migration.js +0 -0
  132. /package/src/migrations/{latest/terraform-bootstrap-adopt-existing-bucket → v1.0.0-rc.50/restrict-cors-to-custom-domains}/migration.d.ts +0 -0
  133. /package/src/migrations/{latest → v1.0.0-rc.50}/restrict-cors-to-custom-domains/migration.js +0 -0
  134. /package/src/migrations/{latest/ts-agent-session-management-support → v1.0.0-rc.50/terraform-bootstrap-adopt-existing-bucket}/migration.d.ts +0 -0
  135. /package/src/migrations/{latest → v1.0.0-rc.50}/terraform-bootstrap-adopt-existing-bucket/migration.js +0 -0
  136. /package/src/migrations/{latest/dynamodb-local-remove-optimize-flag → v1.0.0-rc.52/user-identity-waf-allow-localhost-callback}/migration.d.ts +0 -0
  137. /package/src/migrations/{latest → v1.0.0-rc.52}/user-identity-waf-allow-localhost-callback/migration.js +0 -0
  138. /package/src/migrations/{latest → v1.0.0-rc.54}/order-access-log-delivery-after-bucket-policy/migration.d.ts +0 -0
  139. /package/src/migrations/{latest → v1.0.0-rc.54}/order-access-log-delivery-after-bucket-policy/migration.js +0 -0
  140. /package/src/migrations/{latest → v1.0.0-rc.55}/backfill-generator-metadata/migration.d.ts +0 -0
  141. /package/src/migrations/{latest → v1.0.0-rc.55}/backfill-generator-metadata/migration.js +0 -0
  142. /package/src/migrations/{latest/user-identity-waf-allow-localhost-callback → v1.0.0-rc.56/dynamodb-local-remove-optimize-flag}/migration.d.ts +0 -0
  143. /package/src/migrations/{latest → v1.0.0-rc.56}/dynamodb-local-remove-optimize-flag/migration.js +0 -0
  144. /package/src/migrations/{latest → v1.0.0-rc.57}/ts-agent-session-management-support/migration.js +0 -0
@@ -6,6 +6,7 @@ import { getProjects } from "@nx/devkit";
6
6
  import { AWS_NX_PLUGIN_CONFIG_FILE_NAME } from "../config/utils.js";
7
7
  import { ownedDependencyEntries } from "../declared-dependencies.js";
8
8
  import { buildGeneratorInfoList } from "../generators.js";
9
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../shared-constructs-constants.js";
9
10
  /** Directory holding `generators.json`, which maps ids to their modules. */ export const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..', '..', '..');
10
11
  /**
11
12
  * Every recorded run of a generator in this workspace, from the metadata
@@ -29,14 +30,16 @@ import { buildGeneratorInfoList } from "../generators.js";
29
30
  const { components, ...ownMetadata } = metadata;
30
31
  occurrences.push({
31
32
  id: metadata.generator,
32
- metadata: ownMetadata
33
+ metadata: ownMetadata,
34
+ projectRoot: project.root
33
35
  });
34
36
  }
35
37
  for (const component of metadata?.components ?? []){
36
38
  if (component.generator) {
37
39
  occurrences.push({
38
40
  id: component.generator,
39
- metadata: component
41
+ metadata: component,
42
+ projectRoot: project.root
40
43
  });
41
44
  }
42
45
  }
@@ -59,6 +62,7 @@ import { buildGeneratorInfoList } from "../generators.js";
59
62
  const occurrences = generatorOccurrences(tree);
60
63
  const ts = new Set();
61
64
  const py = new Set();
65
+ const byProject = new Map();
62
66
  for (const info of buildGeneratorInfoList(PLUGIN_ROOT)){
63
67
  const matching = occurrences.filter((occurrence)=>occurrence.id === info.id);
64
68
  if (matching.length === 0) {
@@ -68,20 +72,64 @@ import { buildGeneratorInfoList } from "../generators.js";
68
72
  if (!declaration) {
69
73
  continue;
70
74
  }
71
- for (const { metadata } of matching){
75
+ for (const { metadata, projectRoot } of matching){
76
+ const scoped = projectRoot ? byProject.get(projectRoot) ?? byProject.set(projectRoot, {
77
+ ts: new Set(),
78
+ py: new Set()
79
+ }).get(projectRoot) : undefined;
72
80
  for (const entry of ownedDependencyEntries(declaration.ts, metadata)){
73
81
  ts.add(entry.name);
82
+ scoped?.ts.add(entry.name);
74
83
  }
75
84
  for (const entry of ownedDependencyEntries(declaration.py, metadata)){
76
85
  py.add(entry.name);
86
+ scoped?.py.add(entry.name);
77
87
  }
78
88
  }
79
89
  }
80
90
  return {
81
91
  ts,
82
- py
92
+ py,
93
+ byProject
83
94
  };
84
95
  };
96
+ /**
97
+ * Roots of the projects every generator contributes into, rather than one
98
+ * generator owning outright.
99
+ *
100
+ * `sharedConstructsGenerator` creates these and each infra generator writes its
101
+ * own module into them — the Terraform modules carrying the `uv run --with` pins
102
+ * come from the agent-core, api, identity and website helpers alike. Both are
103
+ * registered projects, so scoping a file here to the generator that created the
104
+ * project would strand every pin the others put there.
105
+ */ const SHARED_PROJECT_ROOTS = [
106
+ `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}`,
107
+ `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}`
108
+ ];
109
+ /**
110
+ * The dependencies owned where a file sits: those of the project it belongs to,
111
+ * or the workspace-wide union for a file in a shared project or under none.
112
+ *
113
+ * Used for the pins embedded in a generated file, so a version is only rewritten
114
+ * where the generator that pins it actually ran: a `Dockerfile` under one
115
+ * project's root is not upgraded because a sibling project owns the package.
116
+ *
117
+ * The longest matching root wins, since a nested project's root is a prefix of
118
+ * nothing else but its own files.
119
+ */ export const ownedForFile = (owned, filePath)=>{
120
+ if (SHARED_PROJECT_ROOTS.some((root)=>filePath === root || filePath.startsWith(`${root}/`))) {
121
+ return owned;
122
+ }
123
+ let best;
124
+ let bestLength = -1;
125
+ for (const [root, scoped] of owned.byProject){
126
+ if ((filePath === root || filePath.startsWith(`${root}/`)) && root.length > bestLength) {
127
+ best = scoped;
128
+ bestLength = root.length;
129
+ }
130
+ }
131
+ return best ?? owned;
132
+ };
85
133
  /**
86
134
  * A generator's `DEPENDENCIES`, or undefined when it declares none.
87
135
  * A generator that fails to load must not fail the upgrade, so its dependencies
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/version-upgrade-migration/owned-dependencies.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport * as path from 'node:path';\nimport { getProjects, type Tree } from '@nx/devkit';\nimport { AWS_NX_PLUGIN_CONFIG_FILE_NAME } from '../config/utils';\nimport {\n type DependencyDeclaration,\n type DependencyMetadata,\n ownedDependencyEntries,\n} from '../declared-dependencies';\nimport { buildGeneratorInfoList } from '../generators';\n\n/** Directory holding `generators.json`, which maps ids to their modules. */\nexport const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..', '..', '..');\n\n/**\n * Which vended dependencies this workspace's generators are responsible for.\n *\n * Only these are synced, so a package the user added themselves keeps the version\n * they chose. Ownership is derived from the generators the workspace has run and\n * the dependencies each declares — nothing extra is recorded in the workspace.\n */\nexport interface OwnedDependencies {\n readonly ts: ReadonlySet<string>;\n readonly py: ReadonlySet<string>;\n}\n\n/**\n * One recorded run of a generator, with the metadata it wrote.\n *\n * The metadata carries the values the generator's dependency conditions read, so\n * a declaration filters down to the branch this occurrence actually took.\n */\nexport interface GeneratorOccurrence {\n readonly id: string;\n readonly metadata: DependencyMetadata;\n}\n\n/**\n * Every recorded run of a generator in this workspace, from the metadata\n * generators record on the projects (and components) they create.\n *\n * A generator appears once per project or component it created, since each may\n * have been run with different options.\n *\n * `init` creates no project, so its config file stands in for it.\n */\nexport const generatorOccurrences = (\n tree: Tree,\n): readonly GeneratorOccurrence[] => {\n const occurrences: GeneratorOccurrence[] = [];\n if (tree.exists(AWS_NX_PLUGIN_CONFIG_FILE_NAME)) {\n occurrences.push({ id: 'init', metadata: {} });\n }\n for (const [, project] of getProjects(tree)) {\n const metadata = project.metadata as\n | {\n generator?: string;\n components?: ({ generator?: string } & DependencyMetadata)[];\n }\n | undefined;\n if (metadata?.generator) {\n const { components, ...ownMetadata } = metadata;\n occurrences.push({ id: metadata.generator, metadata: ownMetadata });\n }\n for (const component of metadata?.components ?? []) {\n if (component.generator) {\n occurrences.push({ id: component.generator, metadata: component });\n }\n }\n }\n return occurrences;\n};\n\n/** Generator ids this workspace has run. */\nexport const generatorsRun = (tree: Tree): ReadonlySet<string> =>\n new Set(generatorOccurrences(tree).map((occurrence) => occurrence.id));\n\n/**\n * Union of the dependencies declared by the generators this workspace has run,\n * narrowed per occurrence to those whose conditions its recorded metadata\n * satisfies — a project generated with one protocol does not own another's\n * packages.\n *\n * A `versionOnly` entry is owned like any other: it is declared precisely so the\n * sync keeps its pinned version current wherever the workspace already holds it.\n *\n * Each generator module exports its declaration, so reading one is an import\n * rather than a run — the generators themselves are never invoked.\n */\nexport const ownedDependencies = async (\n tree: Tree,\n): Promise<OwnedDependencies> => {\n const occurrences = generatorOccurrences(tree);\n const ts = new Set<string>();\n const py = new Set<string>();\n\n for (const info of buildGeneratorInfoList(PLUGIN_ROOT)) {\n const matching = occurrences.filter(\n (occurrence) => occurrence.id === info.id,\n );\n if (matching.length === 0) {\n continue;\n }\n const declaration = await readDeclaration(info.resolvedFactoryPath);\n if (!declaration) {\n continue;\n }\n for (const { metadata } of matching) {\n for (const entry of ownedDependencyEntries(declaration.ts, metadata)) {\n ts.add(entry.name as string);\n }\n for (const entry of ownedDependencyEntries(declaration.py, metadata)) {\n py.add(entry.name as string);\n }\n }\n }\n\n return { ts, py };\n};\n\n/**\n * A generator's `DEPENDENCIES`, or undefined when it declares none.\n * A generator that fails to load must not fail the upgrade, so its dependencies\n * are treated as unowned and left as they are.\n */\nconst readDeclaration = async (\n factoryPath: string,\n): Promise<DependencyDeclaration | undefined> => {\n try {\n const module = await import(`${factoryPath}.js`);\n return module.DEPENDENCIES;\n } catch {\n return undefined;\n }\n};\n"],"names":["path","getProjects","AWS_NX_PLUGIN_CONFIG_FILE_NAME","ownedDependencyEntries","buildGeneratorInfoList","PLUGIN_ROOT","resolve","dirname","generatorOccurrences","tree","occurrences","exists","push","id","metadata","project","generator","components","ownMetadata","component","generatorsRun","Set","map","occurrence","ownedDependencies","ts","py","info","matching","filter","length","declaration","readDeclaration","resolvedFactoryPath","entry","add","name","factoryPath","module","DEPENDENCIES","undefined"],"mappings":"AAAA;;;CAGC,GACD,YAAYA,UAAU,YAAY;AAClC,SAASC,WAAW,QAAmB,aAAa;AACpD,SAASC,8BAA8B,QAAQ,qBAAkB;AACjE,SAGEC,sBAAsB,QACjB,8BAA2B;AAClC,SAASC,sBAAsB,QAAQ,mBAAgB;AAEvD,0EAA0E,GAC1E,OAAO,MAAMC,cAAcL,KAAKM,OAAO,CAAC,YAAYC,OAAO,EAAE,MAAM,MAAM,MAAM;AAyB/E;;;;;;;;CAQC,GACD,OAAO,MAAMC,uBAAuB,CAClCC;IAEA,MAAMC,cAAqC,EAAE;IAC7C,IAAID,KAAKE,MAAM,CAACT,iCAAiC;QAC/CQ,YAAYE,IAAI,CAAC;YAAEC,IAAI;YAAQC,UAAU,CAAC;QAAE;IAC9C;IACA,KAAK,MAAM,GAAGC,QAAQ,IAAId,YAAYQ,MAAO;QAC3C,MAAMK,WAAWC,QAAQD,QAAQ;QAMjC,IAAIA,UAAUE,WAAW;YACvB,MAAM,EAAEC,UAAU,EAAE,GAAGC,aAAa,GAAGJ;YACvCJ,YAAYE,IAAI,CAAC;gBAAEC,IAAIC,SAASE,SAAS;gBAAEF,UAAUI;YAAY;QACnE;QACA,KAAK,MAAMC,aAAaL,UAAUG,cAAc,EAAE,CAAE;YAClD,IAAIE,UAAUH,SAAS,EAAE;gBACvBN,YAAYE,IAAI,CAAC;oBAAEC,IAAIM,UAAUH,SAAS;oBAAEF,UAAUK;gBAAU;YAClE;QACF;IACF;IACA,OAAOT;AACT,EAAE;AAEF,0CAA0C,GAC1C,OAAO,MAAMU,gBAAgB,CAACX,OAC5B,IAAIY,IAAIb,qBAAqBC,MAAMa,GAAG,CAAC,CAACC,aAAeA,WAAWV,EAAE,GAAG;AAEzE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMW,oBAAoB,OAC/Bf;IAEA,MAAMC,cAAcF,qBAAqBC;IACzC,MAAMgB,KAAK,IAAIJ;IACf,MAAMK,KAAK,IAAIL;IAEf,KAAK,MAAMM,QAAQvB,uBAAuBC,aAAc;QACtD,MAAMuB,WAAWlB,YAAYmB,MAAM,CACjC,CAACN,aAAeA,WAAWV,EAAE,KAAKc,KAAKd,EAAE;QAE3C,IAAIe,SAASE,MAAM,KAAK,GAAG;YACzB;QACF;QACA,MAAMC,cAAc,MAAMC,gBAAgBL,KAAKM,mBAAmB;QAClE,IAAI,CAACF,aAAa;YAChB;QACF;QACA,KAAK,MAAM,EAAEjB,QAAQ,EAAE,IAAIc,SAAU;YACnC,KAAK,MAAMM,SAAS/B,uBAAuB4B,YAAYN,EAAE,EAAEX,UAAW;gBACpEW,GAAGU,GAAG,CAACD,MAAME,IAAI;YACnB;YACA,KAAK,MAAMF,SAAS/B,uBAAuB4B,YAAYL,EAAE,EAAEZ,UAAW;gBACpEY,GAAGS,GAAG,CAACD,MAAME,IAAI;YACnB;QACF;IACF;IAEA,OAAO;QAAEX;QAAIC;IAAG;AAClB,EAAE;AAEF;;;;CAIC,GACD,MAAMM,kBAAkB,OACtBK;IAEA,IAAI;QACF,MAAMC,SAAS,MAAM,MAAM,CAAC,GAAGD,YAAY,GAAG,CAAC;QAC/C,OAAOC,OAAOC,YAAY;IAC5B,EAAE,OAAM;QACN,OAAOC;IACT;AACF"}
1
+ {"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/version-upgrade-migration/owned-dependencies.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport * as path from 'node:path';\nimport { getProjects, type Tree } from '@nx/devkit';\nimport { AWS_NX_PLUGIN_CONFIG_FILE_NAME } from '../config/utils';\nimport {\n type DependencyDeclaration,\n type DependencyMetadata,\n ownedDependencyEntries,\n} from '../declared-dependencies';\nimport { buildGeneratorInfoList } from '../generators';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../shared-constructs-constants';\n\n/** Directory holding `generators.json`, which maps ids to their modules. */\nexport const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..', '..', '..');\n\n/**\n * Which vended dependencies this workspace's generators are responsible for.\n *\n * Only these are synced, so a package the user added themselves keeps the version\n * they chose. Ownership is derived from the generators the workspace has run and\n * the dependencies each declares — nothing extra is recorded in the workspace.\n */\nexport interface OwnedDependencies {\n readonly ts: ReadonlySet<string>;\n readonly py: ReadonlySet<string>;\n /**\n * The same, split by the root of the project that owns it, for scoping a pin\n * embedded in a generated file to the project it sits in. Keyed by project\n * root; read through {@link ownedForFile}.\n */\n readonly byProject: ReadonlyMap<\n string,\n { readonly ts: ReadonlySet<string>; readonly py: ReadonlySet<string> }\n >;\n}\n\n/**\n * One recorded run of a generator, with the metadata it wrote.\n *\n * The metadata carries the values the generator's dependency conditions read, so\n * a declaration filters down to the branch this occurrence actually took.\n */\nexport interface GeneratorOccurrence {\n readonly id: string;\n readonly metadata: DependencyMetadata;\n /** Root of the project this ran against, for scoping what it owns to it. */\n readonly projectRoot?: string;\n}\n\n/**\n * Every recorded run of a generator in this workspace, from the metadata\n * generators record on the projects (and components) they create.\n *\n * A generator appears once per project or component it created, since each may\n * have been run with different options.\n *\n * `init` creates no project, so its config file stands in for it.\n */\nexport const generatorOccurrences = (\n tree: Tree,\n): readonly GeneratorOccurrence[] => {\n const occurrences: GeneratorOccurrence[] = [];\n if (tree.exists(AWS_NX_PLUGIN_CONFIG_FILE_NAME)) {\n occurrences.push({ id: 'init', metadata: {} });\n }\n for (const [, project] of getProjects(tree)) {\n const metadata = project.metadata as\n | {\n generator?: string;\n components?: ({ generator?: string } & DependencyMetadata)[];\n }\n | undefined;\n if (metadata?.generator) {\n const { components, ...ownMetadata } = metadata;\n occurrences.push({\n id: metadata.generator,\n metadata: ownMetadata,\n projectRoot: project.root,\n });\n }\n for (const component of metadata?.components ?? []) {\n if (component.generator) {\n occurrences.push({\n id: component.generator,\n metadata: component,\n projectRoot: project.root,\n });\n }\n }\n }\n return occurrences;\n};\n\n/** Generator ids this workspace has run. */\nexport const generatorsRun = (tree: Tree): ReadonlySet<string> =>\n new Set(generatorOccurrences(tree).map((occurrence) => occurrence.id));\n\n/**\n * Union of the dependencies declared by the generators this workspace has run,\n * narrowed per occurrence to those whose conditions its recorded metadata\n * satisfies — a project generated with one protocol does not own another's\n * packages.\n *\n * A `versionOnly` entry is owned like any other: it is declared precisely so the\n * sync keeps its pinned version current wherever the workspace already holds it.\n *\n * Each generator module exports its declaration, so reading one is an import\n * rather than a run — the generators themselves are never invoked.\n */\nexport const ownedDependencies = async (\n tree: Tree,\n): Promise<OwnedDependencies> => {\n const occurrences = generatorOccurrences(tree);\n const ts = new Set<string>();\n const py = new Set<string>();\n const byProject = new Map<string, { ts: Set<string>; py: Set<string> }>();\n\n for (const info of buildGeneratorInfoList(PLUGIN_ROOT)) {\n const matching = occurrences.filter(\n (occurrence) => occurrence.id === info.id,\n );\n if (matching.length === 0) {\n continue;\n }\n const declaration = await readDeclaration(info.resolvedFactoryPath);\n if (!declaration) {\n continue;\n }\n for (const { metadata, projectRoot } of matching) {\n const scoped = projectRoot\n ? (byProject.get(projectRoot) ??\n byProject\n .set(projectRoot, { ts: new Set(), py: new Set() })\n .get(projectRoot)!)\n : undefined;\n for (const entry of ownedDependencyEntries(declaration.ts, metadata)) {\n ts.add(entry.name as string);\n scoped?.ts.add(entry.name as string);\n }\n for (const entry of ownedDependencyEntries(declaration.py, metadata)) {\n py.add(entry.name as string);\n scoped?.py.add(entry.name as string);\n }\n }\n }\n\n return { ts, py, byProject };\n};\n\n/**\n * Roots of the projects every generator contributes into, rather than one\n * generator owning outright.\n *\n * `sharedConstructsGenerator` creates these and each infra generator writes its\n * own module into them — the Terraform modules carrying the `uv run --with` pins\n * come from the agent-core, api, identity and website helpers alike. Both are\n * registered projects, so scoping a file here to the generator that created the\n * project would strand every pin the others put there.\n */\nconst SHARED_PROJECT_ROOTS = [\n `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}`,\n `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}`,\n];\n\n/**\n * The dependencies owned where a file sits: those of the project it belongs to,\n * or the workspace-wide union for a file in a shared project or under none.\n *\n * Used for the pins embedded in a generated file, so a version is only rewritten\n * where the generator that pins it actually ran: a `Dockerfile` under one\n * project's root is not upgraded because a sibling project owns the package.\n *\n * The longest matching root wins, since a nested project's root is a prefix of\n * nothing else but its own files.\n */\nexport const ownedForFile = (\n owned: OwnedDependencies,\n filePath: string,\n): { ts: ReadonlySet<string>; py: ReadonlySet<string> } => {\n if (\n SHARED_PROJECT_ROOTS.some(\n (root) => filePath === root || filePath.startsWith(`${root}/`),\n )\n ) {\n return owned;\n }\n let best: { ts: ReadonlySet<string>; py: ReadonlySet<string> } | undefined;\n let bestLength = -1;\n for (const [root, scoped] of owned.byProject) {\n if (\n (filePath === root || filePath.startsWith(`${root}/`)) &&\n root.length > bestLength\n ) {\n best = scoped;\n bestLength = root.length;\n }\n }\n return best ?? owned;\n};\n\n/**\n * A generator's `DEPENDENCIES`, or undefined when it declares none.\n * A generator that fails to load must not fail the upgrade, so its dependencies\n * are treated as unowned and left as they are.\n */\nconst readDeclaration = async (\n factoryPath: string,\n): Promise<DependencyDeclaration | undefined> => {\n try {\n const module = await import(`${factoryPath}.js`);\n return module.DEPENDENCIES;\n } catch {\n return undefined;\n }\n};\n"],"names":["path","getProjects","AWS_NX_PLUGIN_CONFIG_FILE_NAME","ownedDependencyEntries","buildGeneratorInfoList","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","PLUGIN_ROOT","resolve","dirname","generatorOccurrences","tree","occurrences","exists","push","id","metadata","project","generator","components","ownMetadata","projectRoot","root","component","generatorsRun","Set","map","occurrence","ownedDependencies","ts","py","byProject","Map","info","matching","filter","length","declaration","readDeclaration","resolvedFactoryPath","scoped","get","set","undefined","entry","add","name","SHARED_PROJECT_ROOTS","ownedForFile","owned","filePath","some","startsWith","best","bestLength","factoryPath","module","DEPENDENCIES"],"mappings":"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"}
@@ -18,6 +18,10 @@ import { type Tree } from '@nx/devkit';
18
18
  *
19
19
  * Idempotent: re-running before a release replaces the pending entry with itself.
20
20
  *
21
+ * @param nxVersion nx version being moved to. The update script rewrites
22
+ * `versions.ts` on the tree, which cannot change the `NX_VERSION` this process
23
+ * imported at load — so the caller passes the version it just wrote, or the
24
+ * entry would record the one being replaced.
21
25
  * @returns paths written, for the update report
22
26
  */
23
- export declare const registerNxPackageUpdates: (tree: Tree) => string[];
27
+ export declare const registerNxPackageUpdates: (tree: Tree, nxVersion?: string) => string[];
@@ -20,14 +20,18 @@ const MIGRATIONS_JSON_PATH = 'packages/nx-plugin/migrations.json';
20
20
  *
21
21
  * Idempotent: re-running before a release replaces the pending entry with itself.
22
22
  *
23
+ * @param nxVersion nx version being moved to. The update script rewrites
24
+ * `versions.ts` on the tree, which cannot change the `NX_VERSION` this process
25
+ * imported at load — so the caller passes the version it just wrote, or the
26
+ * entry would record the one being replaced.
23
27
  * @returns paths written, for the update report
24
- */ export const registerNxPackageUpdates = (tree)=>{
28
+ */ export const registerNxPackageUpdates = (tree, nxVersion)=>{
25
29
  updateJson(tree, MIGRATIONS_JSON_PATH, (migrations)=>({
26
30
  ...migrations,
27
31
  // Recorded under `latest` until stamping resolves the version it ships with.
28
32
  packageJsonUpdates: {
29
33
  ...Object.fromEntries(Object.entries(migrations.packageJsonUpdates ?? {}).filter(([, entry])=>entry.version !== LATEST_MIGRATIONS_DIR)),
30
- ...nxPackageJsonUpdates(LATEST_MIGRATIONS_DIR)
34
+ ...nxPackageJsonUpdates(LATEST_MIGRATIONS_DIR, nxVersion)
31
35
  }
32
36
  }));
33
37
  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 * 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"}
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 * @param nxVersion nx version being moved to. The update script rewrites\n * `versions.ts` on the tree, which cannot change the `NX_VERSION` this process\n * imported at load — so the caller passes the version it just wrote, or the\n * entry would record the one being replaced.\n * @returns paths written, for the update report\n */\nexport const registerNxPackageUpdates = (\n tree: Tree,\n nxVersion?: string,\n): 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, nxVersion),\n },\n }));\n\n return [MIGRATIONS_JSON_PATH];\n};\n"],"names":["updateJson","LATEST_MIGRATIONS_DIR","nxPackageJsonUpdates","MIGRATIONS_JSON_PATH","registerNxPackageUpdates","tree","nxVersion","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;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMC,2BAA2B,CACtCC,MACAC;IAEAN,WAA2BK,MAAMF,sBAAsB,CAACI,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,KAAKb,uBAEpC;gBACD,GAAGC,qBAAqBD,uBAAuBK,UAAU;YAC3D;QACF,CAAA;IAEA,OAAO;QAACH;KAAqB;AAC/B,EAAE"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { Tree } from '@nx/devkit';
6
+ import { type OwnedDependencies } from './owned-dependencies';
7
+ /**
8
+ * Whether a file is a Dockerfile: exactly `Dockerfile`, or the `<name>.Dockerfile`
9
+ * form the smithy templates vend as `build.Dockerfile`.
10
+ *
11
+ * Deliberately exact rather than a prefix match, which would also claim a
12
+ * `Dockerfile.bak` or a `Dockerfile.md` the user keeps beside a real one.
13
+ */
14
+ export declare const isDockerfile: (filePath: string) => boolean;
15
+ /**
16
+ * Sync the vended versions baked into generated file bodies: `Dockerfile` pins,
17
+ * the Python pins in Terraform inline scripts, and the tooling images
18
+ * `project.json` target commands run.
19
+ *
20
+ * Reports only the files left for the user: a pin that takes effect the next time
21
+ * the thing holding it runs needs no follow-up, but one this skipped for holding
22
+ * more occurrences than it will rewrite at once does.
23
+ *
24
+ * @returns paths whose pins were left as they were
25
+ */
26
+ export declare const syncEmbeddedVersions: (tree: Tree, owned: OwnedDependencies) => Promise<string[]>;
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { visitNotIgnoredFiles } from "@nx/devkit";
5
+ import { applyGritQL } from "../ast.js";
6
+ import { BASE_IMAGES, CONTAINER_REPOSITORIES, CONTAINER_VERSIONS, PY_VERSIONS, TS_VERSIONS } from "../versions.js";
7
+ import { ownedForFile } from "./owned-dependencies.js";
8
+ import { isVendedUpgrade } from "./vended-upgrade.js";
9
+ /** Escape a package name or image reference for use inside a regex. */ const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ /**
11
+ * Characters that end a pinned version: whitespace, the backslash continuing a
12
+ * shell line, and the quote closing the string the pin sits in.
13
+ *
14
+ * The quote is escaped because the pattern is itself a double-quoted GritQL
15
+ * regex literal, which a bare quote would terminate.
16
+ */ const VERSION_TERMINATORS = '\\s\\\\\\"';
17
+ /**
18
+ * Rewrite the capture group of an anchored regex to the vended version.
19
+ *
20
+ * The leading and trailing `[\s\S]*` anchor the match to the whole file, which
21
+ * is what lets a capture group be rewritten on its own: GritQL binds the group
22
+ * to a node it can replace, leaving every other byte untouched. Without the
23
+ * anchors nothing matches.
24
+ */ const rewriteCapture = (pattern, vended)=>`r"[\\s\\S]*${pattern}[\\s\\S]*"($version) where { $version => \`${vended}\` }`;
25
+ /**
26
+ * Versions a pin's regex currently matches in the file.
27
+ *
28
+ * Read before rewriting so a version the user raised past what this release
29
+ * vends is left alone, matching how every other surface behaves.
30
+ */ const declaredVersions = (contents, pattern)=>{
31
+ const matches = contents.matchAll(new RegExp(pattern, 'g'));
32
+ return [
33
+ ...matches
34
+ ].flatMap((match)=>match[1] ? [
35
+ match[1]
36
+ ] : []);
37
+ };
38
+ /**
39
+ * Occurrences of one pin a single rewrite will handle.
40
+ *
41
+ * GritQL rewrites each match separately, so its cost grows with how many a
42
+ * pattern matches — ~10ms an occurrence, which a hand-authored file with hundreds
43
+ * of `RUN npm install` lines turns into minutes of an unattended `nx migrate`.
44
+ * Past this the file is left alone and reported, since a wrong version the user
45
+ * can see and fix beats an upgrade that appears to hang.
46
+ */ const MAX_OCCURRENCES_PER_PIN = 100;
47
+ /**
48
+ * Apply the pins whose declared version this release moves forward.
49
+ *
50
+ * A single rewrite covers every occurrence of a pin in the file, so each pin is
51
+ * applied once. Reports whether anything changed, and whether a pin was left
52
+ * alone for exceeding {@link MAX_OCCURRENCES_PER_PIN}.
53
+ */ const applyPins = async (tree, path, pins)=>{
54
+ // Read once and re-read only after a rewrite: a rewrite parses the whole file,
55
+ // and there is a pin per owned package, so re-reading per pin would scale the
56
+ // cost of a large file with the size of the owned set.
57
+ let contents = tree.read(path, 'utf-8') ?? '';
58
+ let changed = false;
59
+ let skipped = false;
60
+ for (const { pattern, vended, kind = 'version' } of pins){
61
+ const declared = declaredVersions(contents, pattern);
62
+ if (declared.length === 0) {
63
+ continue;
64
+ }
65
+ if (declared.length > MAX_OCCURRENCES_PER_PIN) {
66
+ skipped = true;
67
+ continue;
68
+ }
69
+ const outOfDate = kind === 'tag' ? (tag)=>tag !== vended : (version)=>isVendedUpgrade(vended, version);
70
+ // A rewrite replaces every occurrence at once, so this must hold for all of
71
+ // them — a pin already at or beyond the vended version is left as it is.
72
+ if (!declared.every(outOfDate)) {
73
+ continue;
74
+ }
75
+ if (await applyGritQL(tree, path, rewriteCapture(pattern, vended))) {
76
+ contents = tree.read(path, 'utf-8') ?? '';
77
+ changed = true;
78
+ }
79
+ }
80
+ return {
81
+ changed,
82
+ skipped
83
+ };
84
+ };
85
+ /**
86
+ * The version this release vends for a package, or undefined for one it doesn't
87
+ * vend at all.
88
+ *
89
+ * Ownership is applied by the caller, which only ever passes a name drawn from
90
+ * the owned set — so a package the user pinned themselves never reaches here and
91
+ * keeps the version they chose.
92
+ */ const vendedTsVersion = (name)=>TS_VERSIONS[name];
93
+ /** `PY_VERSIONS` records the `==` operator; a pin holds the bare version. */ const vendedPyVersion = (name)=>PY_VERSIONS[name]?.replace(/^==/, '');
94
+ /**
95
+ * The ways a generated `Dockerfile` pins a TypeScript package's version, as
96
+ * regex fragments taking the escaped package name.
97
+ *
98
+ * The name is required to run to its delimiter, so a package whose name merely
99
+ * ends with another's — `some-npm` against `npm` — cannot match, and an
100
+ * installed version must start with a digit so a `@latest` style tag is left
101
+ * alone.
102
+ */ const DOCKERFILE_PIN_SHAPES = [
103
+ // Installed by the image build. `install` and its `i` alias are both used, as
104
+ // are `-g` and several packages in one command, so the name only has to follow
105
+ // an `npm` subcommand rather than sit right after it. The whitespace before it
106
+ // is matched rather than asserted, since GritQL's regex engine has no
107
+ // lookbehind — which also keeps `some-npm` from matching `npm`, and unlike a
108
+ // `\b` boundary still reaches a `@scope/name`.
109
+ (name)=>`npm (?:install|i|add)[^\\n]*?\\s${name}@([0-9][^${VERSION_TERMINATORS}]*)`,
110
+ // Held at a fixed version through an `npm pkg set` override, to clear a known
111
+ // vulnerability in a transitive dependency.
112
+ (name)=>`overrides\\.${name}=([^${VERSION_TERMINATORS}]+)`
113
+ ];
114
+ /**
115
+ * Every pin a generated `Dockerfile` may carry, for the packages owned where it
116
+ * sits.
117
+ *
118
+ * Driven off the owned packages rather than a list of the ones that happen to be
119
+ * pinned today: a pin added to a template in a later release is then covered
120
+ * without this module changing, which is the whole point — the versions baked
121
+ * into image builds are pinned precisely because they clear a known
122
+ * vulnerability.
123
+ *
124
+ * A package the user pinned themselves is not owned, so it is never matched.
125
+ */ const dockerfilePins = (owned)=>{
126
+ const pins = [];
127
+ for (const name of owned.ts){
128
+ const vended = vendedTsVersion(name);
129
+ if (!vended) {
130
+ continue;
131
+ }
132
+ for (const shape of DOCKERFILE_PIN_SHAPES){
133
+ pins.push({
134
+ pattern: shape(escapeRegExp(name)),
135
+ vended
136
+ });
137
+ }
138
+ }
139
+ for (const image of Object.values(BASE_IMAGES)){
140
+ const [repository, tag] = splitImageReference(image);
141
+ pins.push({
142
+ // Only a `FROM` that is the whole instruction. A build stage names itself
143
+ // — `FROM <repo>:24 AS builder` — and picks its tag for what that stage has
144
+ // to run: the smithy builder needs curl and unzip, which the slim tag this
145
+ // release vends for a runtime image does not carry. Rewriting it would
146
+ // break the build, and the two share a repository, so the instruction
147
+ // ending is what separates them.
148
+ pattern: `FROM ${escapeRegExp(repository)}:([^\\s]+)\\n`,
149
+ vended: tag,
150
+ kind: 'tag'
151
+ });
152
+ }
153
+ return pins;
154
+ };
155
+ /**
156
+ * The repository and tag of a pinned image reference. The tag is the part after
157
+ * the last `:`, which a registry port would otherwise be mistaken for — every
158
+ * reference here carries one.
159
+ */ const splitImageReference = (image)=>{
160
+ const at = image.lastIndexOf(':');
161
+ return [
162
+ image.slice(0, at),
163
+ image.slice(at + 1)
164
+ ];
165
+ };
166
+ /**
167
+ * Whether a file is a Dockerfile: exactly `Dockerfile`, or the `<name>.Dockerfile`
168
+ * form the smithy templates vend as `build.Dockerfile`.
169
+ *
170
+ * Deliberately exact rather than a prefix match, which would also claim a
171
+ * `Dockerfile.bak` or a `Dockerfile.md` the user keeps beside a real one.
172
+ */ export const isDockerfile = (filePath)=>{
173
+ const name = filePath.split('/').pop() ?? '';
174
+ return name === 'Dockerfile' || name.endsWith('.Dockerfile');
175
+ };
176
+ /**
177
+ * Sync the pins in every generated Dockerfile.
178
+ *
179
+ * Ownership is resolved per file, so a Dockerfile is only rewritten for the
180
+ * packages the project it belongs to owns — a sibling project owning `prisma`
181
+ * does not license rewriting a `prisma` pin here.
182
+ */ const syncDockerfiles = async (tree, owned)=>{
183
+ const dockerfiles = [];
184
+ visitNotIgnoredFiles(tree, '.', (path)=>{
185
+ if (isDockerfile(path)) {
186
+ dockerfiles.push(path);
187
+ }
188
+ });
189
+ const skipped = [];
190
+ for (const path of dockerfiles){
191
+ const result = await applyPins(tree, path, dockerfilePins(ownedForFile(owned, path)));
192
+ if (result.skipped) {
193
+ skipped.push(path);
194
+ }
195
+ }
196
+ return skipped;
197
+ };
198
+ /** The `uv run --with` pins to apply, for the Python packages owned where a file sits. */ const terraformScriptPins = (owned)=>[
199
+ ...owned.py
200
+ ].flatMap((name)=>{
201
+ const vended = vendedPyVersion(name);
202
+ return vended ? [
203
+ {
204
+ pattern: `--with ${escapeRegExp(name)}==([^${VERSION_TERMINATORS}]+)`,
205
+ vended
206
+ }
207
+ ] : [];
208
+ });
209
+ /**
210
+ * Sync the Python pins a generated Terraform inline script runs through
211
+ * `uv run --with <package>==<version>`.
212
+ *
213
+ * These are the plugin's own pins, in a file the Terraform provider sync already
214
+ * visits but only reads `required_providers` from. Driven off the owned Python
215
+ * packages, so a pin added to a template later is covered without a change here.
216
+ *
217
+ * Scoped per file like the Dockerfiles. The modules carrying these live in the
218
+ * shared Terraform project, which no single generator owns — a file there belongs
219
+ * to no project, so it takes the workspace-wide union, which is the set of
220
+ * generators that legitimately write into it.
221
+ */ const syncTerraformScriptPins = async (tree, owned)=>{
222
+ const terraformFiles = [];
223
+ visitNotIgnoredFiles(tree, '.', (path)=>{
224
+ if (path.endsWith('.tf')) {
225
+ terraformFiles.push(path);
226
+ }
227
+ });
228
+ const skipped = [];
229
+ for (const path of terraformFiles){
230
+ const result = await applyPins(tree, path, terraformScriptPins(ownedForFile(owned, path)));
231
+ if (result.skipped) {
232
+ skipped.push(path);
233
+ }
234
+ }
235
+ return skipped;
236
+ };
237
+ /**
238
+ * The ways a `uvx` invocation pins a Python version: the tool it runs, and any
239
+ * package it adds to that tool's environment.
240
+ *
241
+ * A command string is JSON here, so a quote ends a version too.
242
+ */ const UVX_PIN_SHAPES = [
243
+ (name)=>`uvx --from ${name}==`,
244
+ (name)=>`--with ${name}==`
245
+ ];
246
+ /**
247
+ * Sync the pinned tools a `project.json` target command runs: a container image,
248
+ * and the Python versions a `uvx` invocation pins.
249
+ *
250
+ * Both are built into the command string rather than declared as a dependency, so
251
+ * nothing else reaches them. The images are driven off `CONTAINER_REPOSITORIES`
252
+ * and the Python versions off `PY_VERSIONS`, so a tool pinned in a target command
253
+ * later is covered without a change here — the same property the Dockerfile and
254
+ * Terraform paths get from the owned set.
255
+ *
256
+ * Unscoped, unlike the file-body pins: a `uvx` invocation installs nothing into
257
+ * the project, so there is no dependency for a generator to own. A version is only
258
+ * rewritten where this release vends that exact package at a higher one.
259
+ */ const syncTargetToolPins = async (tree)=>{
260
+ const projectJsons = [];
261
+ visitNotIgnoredFiles(tree, '.', (path)=>{
262
+ if (path.endsWith('project.json')) {
263
+ projectJsons.push(path);
264
+ }
265
+ });
266
+ const pins = [
267
+ ...Object.entries(CONTAINER_REPOSITORIES).map(([tool, repository])=>({
268
+ // A command string is JSON here, so a quote ends the reference too.
269
+ pattern: `${escapeRegExp(repository)}:([^${VERSION_TERMINATORS}']+)`,
270
+ vended: CONTAINER_VERSIONS[tool]
271
+ })),
272
+ ...Object.keys(PY_VERSIONS).flatMap((name)=>{
273
+ const vended = vendedPyVersion(name);
274
+ return vended ? UVX_PIN_SHAPES.map((shape)=>({
275
+ pattern: `${shape(escapeRegExp(name))}([^${VERSION_TERMINATORS}']+)`,
276
+ vended
277
+ })) : [];
278
+ })
279
+ ];
280
+ const skipped = [];
281
+ for (const path of projectJsons){
282
+ if ((await applyPins(tree, path, pins)).skipped) {
283
+ skipped.push(path);
284
+ }
285
+ }
286
+ return skipped;
287
+ };
288
+ /**
289
+ * Sync the vended versions baked into generated file bodies: `Dockerfile` pins,
290
+ * the Python pins in Terraform inline scripts, and the tooling images
291
+ * `project.json` target commands run.
292
+ *
293
+ * Reports only the files left for the user: a pin that takes effect the next time
294
+ * the thing holding it runs needs no follow-up, but one this skipped for holding
295
+ * more occurrences than it will rewrite at once does.
296
+ *
297
+ * @returns paths whose pins were left as they were
298
+ */ export const syncEmbeddedVersions = async (tree, owned)=>[
299
+ ...await syncDockerfiles(tree, owned),
300
+ ...await syncTerraformScriptPins(tree, owned),
301
+ ...await syncTargetToolPins(tree)
302
+ ];
303
+
304
+ //# sourceMappingURL=sync-embedded-versions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/version-upgrade-migration/sync-embedded-versions.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { Tree } from '@nx/devkit';\nimport { visitNotIgnoredFiles } from '@nx/devkit';\nimport { applyGritQL } from '../ast';\nimport {\n BASE_IMAGES,\n CONTAINER_REPOSITORIES,\n CONTAINER_VERSIONS,\n PY_VERSIONS,\n TS_VERSIONS,\n} from '../versions';\nimport { type OwnedDependencies, ownedForFile } from './owned-dependencies';\nimport { isVendedUpgrade } from './vended-upgrade';\n\n/**\n * Sync the vended versions generators bake into the body of a file rather than\n * into a manifest: the container image pins in a `Dockerfile`, the Python pins\n * in a Terraform inline script, and the tooling images a `project.json` target\n * command runs.\n *\n * Every version here is pinned to clear a known HIGH/CRITICAL vulnerability, or\n * to keep a generated image reproducible, so leaving them behind strands a\n * workspace on exactly the versions most in need of an upgrade.\n *\n * These sit inside shell commands, which GritQL has no grammar for — so instead\n * of pattern-matching the surrounding syntax, each pin is located by an anchored\n * regex whose single capture group is the version, and only that capture is\n * rewritten. The file's own text around it is never reconstructed, so a\n * reformatted or hand-edited file syncs the same way a freshly generated one\n * does.\n */\n\n/** A pin found in a file body, and the version this release vends for it. */\ninterface EmbeddedPin {\n /** Regex matching the pin, with the version as its only capture group. */\n readonly pattern: string;\n readonly vended: string;\n /**\n * Whether the declared value is a version to move forward, or a tag to replace\n * whenever it differs. A tag — `lts-slim`, say — has no ordering to compare, so\n * one that isn't what this release vends is simply out of date.\n */\n readonly kind?: 'version' | 'tag';\n}\n\n/** Escape a package name or image reference for use inside a regex. */\nconst escapeRegExp = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Characters that end a pinned version: whitespace, the backslash continuing a\n * shell line, and the quote closing the string the pin sits in.\n *\n * The quote is escaped because the pattern is itself a double-quoted GritQL\n * regex literal, which a bare quote would terminate.\n */\nconst VERSION_TERMINATORS = '\\\\s\\\\\\\\\\\\\"';\n\n/**\n * Rewrite the capture group of an anchored regex to the vended version.\n *\n * The leading and trailing `[\\s\\S]*` anchor the match to the whole file, which\n * is what lets a capture group be rewritten on its own: GritQL binds the group\n * to a node it can replace, leaving every other byte untouched. Without the\n * anchors nothing matches.\n */\nconst rewriteCapture = (pattern: string, vended: string): string =>\n `r\"[\\\\s\\\\S]*${pattern}[\\\\s\\\\S]*\"($version) where { $version => \\`${vended}\\` }`;\n\n/**\n * Versions a pin's regex currently matches in the file.\n *\n * Read before rewriting so a version the user raised past what this release\n * vends is left alone, matching how every other surface behaves.\n */\nconst declaredVersions = (contents: string, pattern: string): string[] => {\n const matches = contents.matchAll(new RegExp(pattern, 'g'));\n return [...matches].flatMap((match) => (match[1] ? [match[1]] : []));\n};\n\n/**\n * Occurrences of one pin a single rewrite will handle.\n *\n * GritQL rewrites each match separately, so its cost grows with how many a\n * pattern matches — ~10ms an occurrence, which a hand-authored file with hundreds\n * of `RUN npm install` lines turns into minutes of an unattended `nx migrate`.\n * Past this the file is left alone and reported, since a wrong version the user\n * can see and fix beats an upgrade that appears to hang.\n */\nconst MAX_OCCURRENCES_PER_PIN = 100;\n\n/**\n * Apply the pins whose declared version this release moves forward.\n *\n * A single rewrite covers every occurrence of a pin in the file, so each pin is\n * applied once. Reports whether anything changed, and whether a pin was left\n * alone for exceeding {@link MAX_OCCURRENCES_PER_PIN}.\n */\nconst applyPins = async (\n tree: Tree,\n path: string,\n pins: readonly EmbeddedPin[],\n): Promise<{ changed: boolean; skipped: boolean }> => {\n // Read once and re-read only after a rewrite: a rewrite parses the whole file,\n // and there is a pin per owned package, so re-reading per pin would scale the\n // cost of a large file with the size of the owned set.\n let contents = tree.read(path, 'utf-8') ?? '';\n let changed = false;\n let skipped = false;\n\n for (const { pattern, vended, kind = 'version' } of pins) {\n const declared = declaredVersions(contents, pattern);\n if (declared.length === 0) {\n continue;\n }\n if (declared.length > MAX_OCCURRENCES_PER_PIN) {\n skipped = true;\n continue;\n }\n const outOfDate =\n kind === 'tag'\n ? (tag: string) => tag !== vended\n : (version: string) => isVendedUpgrade(vended, version);\n // A rewrite replaces every occurrence at once, so this must hold for all of\n // them — a pin already at or beyond the vended version is left as it is.\n if (!declared.every(outOfDate)) {\n continue;\n }\n if (await applyGritQL(tree, path, rewriteCapture(pattern, vended))) {\n contents = tree.read(path, 'utf-8') ?? '';\n changed = true;\n }\n }\n\n return { changed, skipped };\n};\n\n/**\n * The version this release vends for a package, or undefined for one it doesn't\n * vend at all.\n *\n * Ownership is applied by the caller, which only ever passes a name drawn from\n * the owned set — so a package the user pinned themselves never reaches here and\n * keeps the version they chose.\n */\nconst vendedTsVersion = (name: string): string | undefined =>\n TS_VERSIONS[name as keyof typeof TS_VERSIONS];\n\n/** `PY_VERSIONS` records the `==` operator; a pin holds the bare version. */\nconst vendedPyVersion = (name: string): string | undefined =>\n PY_VERSIONS[name as keyof typeof PY_VERSIONS]?.replace(/^==/, '');\n\n/**\n * The ways a generated `Dockerfile` pins a TypeScript package's version, as\n * regex fragments taking the escaped package name.\n *\n * The name is required to run to its delimiter, so a package whose name merely\n * ends with another's — `some-npm` against `npm` — cannot match, and an\n * installed version must start with a digit so a `@latest` style tag is left\n * alone.\n */\nconst DOCKERFILE_PIN_SHAPES: readonly ((name: string) => string)[] = [\n // Installed by the image build. `install` and its `i` alias are both used, as\n // are `-g` and several packages in one command, so the name only has to follow\n // an `npm` subcommand rather than sit right after it. The whitespace before it\n // is matched rather than asserted, since GritQL's regex engine has no\n // lookbehind — which also keeps `some-npm` from matching `npm`, and unlike a\n // `\\b` boundary still reaches a `@scope/name`.\n (name) =>\n `npm (?:install|i|add)[^\\\\n]*?\\\\s${name}@([0-9][^${VERSION_TERMINATORS}]*)`,\n // Held at a fixed version through an `npm pkg set` override, to clear a known\n // vulnerability in a transitive dependency.\n (name) => `overrides\\\\.${name}=([^${VERSION_TERMINATORS}]+)`,\n];\n\n/**\n * Every pin a generated `Dockerfile` may carry, for the packages owned where it\n * sits.\n *\n * Driven off the owned packages rather than a list of the ones that happen to be\n * pinned today: a pin added to a template in a later release is then covered\n * without this module changing, which is the whole point — the versions baked\n * into image builds are pinned precisely because they clear a known\n * vulnerability.\n *\n * A package the user pinned themselves is not owned, so it is never matched.\n */\nconst dockerfilePins = (owned: {\n readonly ts: ReadonlySet<string>;\n}): EmbeddedPin[] => {\n const pins: EmbeddedPin[] = [];\n\n for (const name of owned.ts) {\n const vended = vendedTsVersion(name);\n if (!vended) {\n continue;\n }\n for (const shape of DOCKERFILE_PIN_SHAPES) {\n pins.push({ pattern: shape(escapeRegExp(name)), vended });\n }\n }\n\n for (const image of Object.values(BASE_IMAGES)) {\n const [repository, tag] = splitImageReference(image);\n pins.push({\n // Only a `FROM` that is the whole instruction. A build stage names itself\n // — `FROM <repo>:24 AS builder` — and picks its tag for what that stage has\n // to run: the smithy builder needs curl and unzip, which the slim tag this\n // release vends for a runtime image does not carry. Rewriting it would\n // break the build, and the two share a repository, so the instruction\n // ending is what separates them.\n pattern: `FROM ${escapeRegExp(repository)}:([^\\\\s]+)\\\\n`,\n vended: tag,\n kind: 'tag',\n });\n }\n\n return pins;\n};\n\n/**\n * The repository and tag of a pinned image reference. The tag is the part after\n * the last `:`, which a registry port would otherwise be mistaken for — every\n * reference here carries one.\n */\nconst splitImageReference = (image: string): [string, string] => {\n const at = image.lastIndexOf(':');\n return [image.slice(0, at), image.slice(at + 1)];\n};\n\n/**\n * Whether a file is a Dockerfile: exactly `Dockerfile`, or the `<name>.Dockerfile`\n * form the smithy templates vend as `build.Dockerfile`.\n *\n * Deliberately exact rather than a prefix match, which would also claim a\n * `Dockerfile.bak` or a `Dockerfile.md` the user keeps beside a real one.\n */\nexport const isDockerfile = (filePath: string): boolean => {\n const name = filePath.split('/').pop() ?? '';\n return name === 'Dockerfile' || name.endsWith('.Dockerfile');\n};\n\n/**\n * Sync the pins in every generated Dockerfile.\n *\n * Ownership is resolved per file, so a Dockerfile is only rewritten for the\n * packages the project it belongs to owns — a sibling project owning `prisma`\n * does not license rewriting a `prisma` pin here.\n */\nconst syncDockerfiles = async (\n tree: Tree,\n owned: OwnedDependencies,\n): Promise<string[]> => {\n const dockerfiles: string[] = [];\n\n visitNotIgnoredFiles(tree, '.', (path) => {\n if (isDockerfile(path)) {\n dockerfiles.push(path);\n }\n });\n\n const skipped: string[] = [];\n for (const path of dockerfiles) {\n const result = await applyPins(\n tree,\n path,\n dockerfilePins(ownedForFile(owned, path)),\n );\n if (result.skipped) {\n skipped.push(path);\n }\n }\n return skipped;\n};\n\n/** The `uv run --with` pins to apply, for the Python packages owned where a file sits. */\nconst terraformScriptPins = (owned: {\n readonly py: ReadonlySet<string>;\n}): EmbeddedPin[] =>\n [...owned.py].flatMap((name) => {\n const vended = vendedPyVersion(name);\n return vended\n ? [\n {\n pattern: `--with ${escapeRegExp(name)}==([^${VERSION_TERMINATORS}]+)`,\n vended,\n },\n ]\n : [];\n });\n\n/**\n * Sync the Python pins a generated Terraform inline script runs through\n * `uv run --with <package>==<version>`.\n *\n * These are the plugin's own pins, in a file the Terraform provider sync already\n * visits but only reads `required_providers` from. Driven off the owned Python\n * packages, so a pin added to a template later is covered without a change here.\n *\n * Scoped per file like the Dockerfiles. The modules carrying these live in the\n * shared Terraform project, which no single generator owns — a file there belongs\n * to no project, so it takes the workspace-wide union, which is the set of\n * generators that legitimately write into it.\n */\nconst syncTerraformScriptPins = async (\n tree: Tree,\n owned: OwnedDependencies,\n): Promise<string[]> => {\n const terraformFiles: string[] = [];\n\n visitNotIgnoredFiles(tree, '.', (path) => {\n if (path.endsWith('.tf')) {\n terraformFiles.push(path);\n }\n });\n\n const skipped: string[] = [];\n for (const path of terraformFiles) {\n const result = await applyPins(\n tree,\n path,\n terraformScriptPins(ownedForFile(owned, path)),\n );\n if (result.skipped) {\n skipped.push(path);\n }\n }\n return skipped;\n};\n\n/**\n * The ways a `uvx` invocation pins a Python version: the tool it runs, and any\n * package it adds to that tool's environment.\n *\n * A command string is JSON here, so a quote ends a version too.\n */\nconst UVX_PIN_SHAPES: readonly ((name: string) => string)[] = [\n (name) => `uvx --from ${name}==`,\n (name) => `--with ${name}==`,\n];\n\n/**\n * Sync the pinned tools a `project.json` target command runs: a container image,\n * and the Python versions a `uvx` invocation pins.\n *\n * Both are built into the command string rather than declared as a dependency, so\n * nothing else reaches them. The images are driven off `CONTAINER_REPOSITORIES`\n * and the Python versions off `PY_VERSIONS`, so a tool pinned in a target command\n * later is covered without a change here — the same property the Dockerfile and\n * Terraform paths get from the owned set.\n *\n * Unscoped, unlike the file-body pins: a `uvx` invocation installs nothing into\n * the project, so there is no dependency for a generator to own. A version is only\n * rewritten where this release vends that exact package at a higher one.\n */\nconst syncTargetToolPins = async (tree: Tree): Promise<string[]> => {\n const projectJsons: string[] = [];\n\n visitNotIgnoredFiles(tree, '.', (path) => {\n if (path.endsWith('project.json')) {\n projectJsons.push(path);\n }\n });\n\n const pins: EmbeddedPin[] = [\n ...Object.entries(CONTAINER_REPOSITORIES).map(([tool, repository]) => ({\n // A command string is JSON here, so a quote ends the reference too.\n pattern: `${escapeRegExp(repository)}:([^${VERSION_TERMINATORS}']+)`,\n vended: CONTAINER_VERSIONS[tool as keyof typeof CONTAINER_VERSIONS],\n })),\n ...Object.keys(PY_VERSIONS).flatMap((name) => {\n const vended = vendedPyVersion(name);\n return vended\n ? UVX_PIN_SHAPES.map((shape) => ({\n pattern: `${shape(escapeRegExp(name))}([^${VERSION_TERMINATORS}']+)`,\n vended,\n }))\n : [];\n }),\n ];\n\n const skipped: string[] = [];\n for (const path of projectJsons) {\n if ((await applyPins(tree, path, pins)).skipped) {\n skipped.push(path);\n }\n }\n return skipped;\n};\n\n/**\n * Sync the vended versions baked into generated file bodies: `Dockerfile` pins,\n * the Python pins in Terraform inline scripts, and the tooling images\n * `project.json` target commands run.\n *\n * Reports only the files left for the user: a pin that takes effect the next time\n * the thing holding it runs needs no follow-up, but one this skipped for holding\n * more occurrences than it will rewrite at once does.\n *\n * @returns paths whose pins were left as they were\n */\nexport const syncEmbeddedVersions = async (\n tree: Tree,\n owned: OwnedDependencies,\n): Promise<string[]> => [\n ...(await syncDockerfiles(tree, owned)),\n ...(await syncTerraformScriptPins(tree, owned)),\n ...(await syncTargetToolPins(tree)),\n];\n"],"names":["visitNotIgnoredFiles","applyGritQL","BASE_IMAGES","CONTAINER_REPOSITORIES","CONTAINER_VERSIONS","PY_VERSIONS","TS_VERSIONS","ownedForFile","isVendedUpgrade","escapeRegExp","value","replace","VERSION_TERMINATORS","rewriteCapture","pattern","vended","declaredVersions","contents","matches","matchAll","RegExp","flatMap","match","MAX_OCCURRENCES_PER_PIN","applyPins","tree","path","pins","read","changed","skipped","kind","declared","length","outOfDate","tag","version","every","vendedTsVersion","name","vendedPyVersion","DOCKERFILE_PIN_SHAPES","dockerfilePins","owned","ts","shape","push","image","Object","values","repository","splitImageReference","at","lastIndexOf","slice","isDockerfile","filePath","split","pop","endsWith","syncDockerfiles","dockerfiles","result","terraformScriptPins","py","syncTerraformScriptPins","terraformFiles","UVX_PIN_SHAPES","syncTargetToolPins","projectJsons","entries","map","tool","keys","syncEmbeddedVersions"],"mappings":"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"}
@@ -13,6 +13,7 @@ import { updateToml } from "../toml.js";
13
13
  import { PY_VERSIONS, TERRAFORM_VERSIONS, TS_VERSIONS } from "../versions.js";
14
14
  import { isNxPackage } from "./nx-package-updates.js";
15
15
  import { ownedDependencies } from "./owned-dependencies.js";
16
+ import { syncEmbeddedVersions } from "./sync-embedded-versions.js";
16
17
  import { syncMetricsVersion } from "./sync-metrics-version.js";
17
18
  import { isVendedUpgrade } from "./vended-upgrade.js";
18
19
  /**
@@ -39,6 +40,12 @@ import { isVendedUpgrade } from "./vended-upgrade.js";
39
40
  * It ships none for bun, and without one it rewrites the reference to a literal
40
41
  * version, severing the catalog — so bun's references are synced directly.
41
42
  */ const isSyncableSpecifier = (tree, declared)=>{
43
+ // A manifest is the user's file and may hold anything. A version that isn't a
44
+ // string is not one this can reason about, and throwing here would abort the
45
+ // whole `nx migrate` over a file the sync only meant to skip.
46
+ if (typeof declared !== 'string') {
47
+ return false;
48
+ }
42
49
  if (REFERENCE_PROTOCOL.test(declared)) {
43
50
  return false;
44
51
  }
@@ -482,6 +489,7 @@ const syncOverrides = (tree, owned)=>{
482
489
  const overrides = syncOverrides(tree, owned);
483
490
  const pyProjects = syncPyProjects(tree, owned);
484
491
  const terraformFiles = await syncTerraformProviders(tree);
492
+ const skippedEmbedded = await syncEmbeddedVersions(tree, owned);
485
493
  await syncMetricsVersion(tree);
486
494
  // Lock files are left to the user: `nx migrate` only installs when the root
487
495
  // manifest changes, and never runs `uv` or `terraform`.
@@ -494,6 +502,12 @@ const syncOverrides = (tree, owned)=>{
494
502
  if (terraformFiles.length > 0) {
495
503
  nextSteps.push(`Terraform provider versions were updated in ${terraformFiles.length} file(s). Run \`terraform init -upgrade\` in each Terraform project to update its lock file.`);
496
504
  }
505
+ // A synced embedded pin needs no next step — it is read the next time the thing
506
+ // holding it runs. A file the sync left alone does: its versions are still the
507
+ // old ones, and only the user can reconcile that.
508
+ if (skippedEmbedded.length > 0) {
509
+ nextSteps.push(`Version pins were left as they are in ${skippedEmbedded.join(', ')}, which repeat a pin more times than this migration rewrites at once. Update them by hand.`);
510
+ }
497
511
  // Without this the workspace's own format check fails on the migrated files.
498
512
  await formatFilesInSubtree(tree);
499
513
  return {