@intentius/chant-lexicon-aws 0.0.6 → 0.0.9

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 (128) hide show
  1. package/dist/integrity.json +25 -10
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +9444 -4597
  4. package/dist/rules/cf-refs.ts +99 -0
  5. package/dist/rules/ext001.ts +32 -25
  6. package/dist/rules/hardcoded-region.ts +1 -0
  7. package/dist/rules/iam-wildcard.ts +1 -0
  8. package/dist/rules/s3-encryption.ts +3 -3
  9. package/dist/rules/waw016.ts +86 -0
  10. package/dist/rules/waw017.ts +53 -0
  11. package/dist/rules/waw018.ts +71 -0
  12. package/dist/rules/waw019.ts +82 -0
  13. package/dist/rules/waw020.ts +64 -0
  14. package/dist/rules/waw021.ts +53 -0
  15. package/dist/rules/waw022.ts +43 -0
  16. package/dist/rules/waw023.ts +47 -0
  17. package/dist/rules/waw024.ts +54 -0
  18. package/dist/rules/waw025.ts +43 -0
  19. package/dist/rules/waw026.ts +46 -0
  20. package/dist/rules/waw027.ts +50 -0
  21. package/dist/rules/waw028.ts +47 -0
  22. package/dist/rules/waw029.ts +62 -0
  23. package/dist/rules/waw030.ts +246 -0
  24. package/dist/skills/chant-aws.md +430 -0
  25. package/dist/types/index.d.ts +58525 -58501
  26. package/package.json +2 -2
  27. package/src/actions/actions.test.ts +75 -0
  28. package/src/actions/dynamodb.ts +36 -0
  29. package/src/actions/ecr.ts +9 -0
  30. package/src/actions/ecs.ts +5 -0
  31. package/src/actions/iam.ts +3 -0
  32. package/src/actions/index.ts +9 -0
  33. package/src/actions/lambda.ts +11 -0
  34. package/src/actions/logs.ts +4 -0
  35. package/src/actions/s3.ts +34 -0
  36. package/src/actions/sns.ts +5 -0
  37. package/src/actions/sqs.ts +15 -0
  38. package/src/codegen/__snapshots__/snapshot.test.ts.snap +20 -20
  39. package/src/codegen/docs-links.test.ts +143 -0
  40. package/src/codegen/docs.ts +294 -124
  41. package/src/codegen/generate-lexicon.ts +8 -0
  42. package/src/codegen/generate-typescript.ts +25 -1
  43. package/src/codegen/generate.ts +1 -13
  44. package/src/codegen/package.ts +2 -0
  45. package/src/codegen/typecheck.test.ts +1 -1
  46. package/src/composites/composites.test.ts +442 -0
  47. package/src/composites/fargate-alb.ts +253 -0
  48. package/src/composites/index.ts +20 -0
  49. package/src/composites/lambda-api.ts +20 -0
  50. package/src/composites/lambda-dynamodb.ts +64 -0
  51. package/src/composites/lambda-eventbridge.ts +36 -0
  52. package/src/composites/lambda-function.ts +76 -0
  53. package/src/composites/lambda-s3.ts +72 -0
  54. package/src/composites/lambda-sns.ts +30 -0
  55. package/src/composites/lambda-sqs.ts +44 -0
  56. package/src/composites/scheduled-lambda.ts +37 -0
  57. package/src/composites/vpc-default.ts +148 -0
  58. package/src/default-tags.test.ts +38 -0
  59. package/src/default-tags.ts +77 -0
  60. package/src/generated/index.d.ts +58525 -58501
  61. package/src/generated/index.ts +1351 -1351
  62. package/src/generated/lexicon-aws.json +9444 -4597
  63. package/src/import/generator.test.ts +5 -5
  64. package/src/import/generator.ts +4 -4
  65. package/src/import/roundtrip-fixtures.test.ts +2 -1
  66. package/src/import/roundtrip.test.ts +5 -5
  67. package/src/index.ts +21 -0
  68. package/src/integration.test.ts +92 -21
  69. package/src/intrinsics.ts +24 -13
  70. package/src/lint/post-synth/cf-refs.ts +99 -0
  71. package/src/lint/post-synth/ext001.test.ts +214 -31
  72. package/src/lint/post-synth/ext001.ts +32 -25
  73. package/src/lint/post-synth/waw013.test.ts +120 -0
  74. package/src/lint/post-synth/waw014.test.ts +121 -0
  75. package/src/lint/post-synth/waw015.test.ts +147 -0
  76. package/src/lint/post-synth/waw016.test.ts +141 -0
  77. package/src/lint/post-synth/waw016.ts +86 -0
  78. package/src/lint/post-synth/waw017.test.ts +130 -0
  79. package/src/lint/post-synth/waw017.ts +53 -0
  80. package/src/lint/post-synth/waw018.test.ts +109 -0
  81. package/src/lint/post-synth/waw018.ts +71 -0
  82. package/src/lint/post-synth/waw019.test.ts +138 -0
  83. package/src/lint/post-synth/waw019.ts +82 -0
  84. package/src/lint/post-synth/waw020.test.ts +125 -0
  85. package/src/lint/post-synth/waw020.ts +64 -0
  86. package/src/lint/post-synth/waw021.test.ts +81 -0
  87. package/src/lint/post-synth/waw021.ts +53 -0
  88. package/src/lint/post-synth/waw022.test.ts +54 -0
  89. package/src/lint/post-synth/waw022.ts +43 -0
  90. package/src/lint/post-synth/waw023.test.ts +53 -0
  91. package/src/lint/post-synth/waw023.ts +47 -0
  92. package/src/lint/post-synth/waw024.test.ts +64 -0
  93. package/src/lint/post-synth/waw024.ts +54 -0
  94. package/src/lint/post-synth/waw025.test.ts +42 -0
  95. package/src/lint/post-synth/waw025.ts +43 -0
  96. package/src/lint/post-synth/waw026.test.ts +54 -0
  97. package/src/lint/post-synth/waw026.ts +46 -0
  98. package/src/lint/post-synth/waw027.test.ts +63 -0
  99. package/src/lint/post-synth/waw027.ts +50 -0
  100. package/src/lint/post-synth/waw028.test.ts +68 -0
  101. package/src/lint/post-synth/waw028.ts +47 -0
  102. package/src/lint/post-synth/waw029.test.ts +179 -0
  103. package/src/lint/post-synth/waw029.ts +62 -0
  104. package/src/lint/post-synth/waw030.test.ts +800 -0
  105. package/src/lint/post-synth/waw030.ts +246 -0
  106. package/src/lint/rules/hardcoded-region.ts +1 -0
  107. package/src/lint/rules/iam-wildcard.ts +1 -0
  108. package/src/lint/rules/rules.test.ts +8 -8
  109. package/src/lint/rules/s3-encryption.ts +3 -3
  110. package/src/lsp/completions.ts +2 -0
  111. package/src/lsp/hover.ts +17 -0
  112. package/src/nested-stack-integration.test.ts +100 -0
  113. package/src/nested-stack.ts +2 -2
  114. package/src/plugin.test.ts +13 -15
  115. package/src/plugin.ts +552 -114
  116. package/src/serializer.test.ts +370 -43
  117. package/src/serializer.ts +69 -17
  118. package/src/spec/fetch.ts +10 -0
  119. package/src/spec/parse.test.ts +141 -0
  120. package/src/spec/parse.ts +40 -0
  121. package/src/taggable.ts +44 -0
  122. package/src/testdata/nested-stacks/app.ts +26 -0
  123. package/src/testdata/nested-stacks/network/outputs.ts +17 -0
  124. package/src/testdata/nested-stacks/network/security.ts +17 -0
  125. package/src/testdata/nested-stacks/network/vpc.ts +54 -0
  126. package/dist/skills/aws-cloudformation.md +0 -41
  127. package/src/codegen/rollback.test.ts +0 -80
  128. package/src/codegen/rollback.ts +0 -20
package/src/serializer.ts CHANGED
@@ -3,9 +3,11 @@ import { isPropertyDeclarable } from "@intentius/chant/declarable";
3
3
  import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
4
4
  import type { LexiconOutput } from "@intentius/chant/lexicon-output";
5
5
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
6
- import { toPascalCase } from "@intentius/chant/codegen/case";
7
6
  import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
8
7
  import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
8
+ import { resolveDependsOn } from "@intentius/chant/resource-attributes";
9
+ import { isDefaultTags, type TagEntry } from "./default-tags";
10
+ import { loadTaggableResources } from "./taggable";
9
11
 
10
12
  /**
11
13
  * Check if a declarable is a CoreParameter
@@ -50,6 +52,10 @@ interface CFResource {
50
52
  Properties?: Record<string, unknown>;
51
53
  DependsOn?: string | string[];
52
54
  Condition?: string;
55
+ DeletionPolicy?: string;
56
+ UpdateReplacePolicy?: string;
57
+ UpdatePolicy?: unknown;
58
+ CreationPolicy?: unknown;
53
59
  Metadata?: Record<string, unknown>;
54
60
  }
55
61
 
@@ -68,7 +74,7 @@ interface CFOutput {
68
74
  */
69
75
  function cfnVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
70
76
  return {
71
- attrRef: (name, attr) => ({ "Fn::GetAttr": [name, attr] }),
77
+ attrRef: (name, attr) => ({ "Fn::GetAtt": [name, attr] }),
72
78
  resourceRef: (name) => ({ Ref: name }),
73
79
  propertyDeclarable: (entity, walk) => {
74
80
  if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
@@ -78,26 +84,19 @@ function cfnVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
78
84
  const cfProps: Record<string, unknown> = {};
79
85
  for (const [key, value] of Object.entries(props)) {
80
86
  if (value !== undefined) {
81
- const cfKey = toPascalCase(key);
82
- cfProps[cfKey] = walk(value);
87
+ cfProps[key] = walk(value);
83
88
  }
84
89
  }
85
90
  return Object.keys(cfProps).length > 0 ? cfProps : undefined;
86
91
  },
87
- transformKey: toPascalCase,
88
92
  };
89
93
  }
90
94
 
91
95
  /**
92
96
  * Convert a value to CF-compatible JSON using the generic walker.
93
97
  */
94
- function toCFValue(value: unknown, entityNames: Map<Declarable, string>, convertKeys = false): unknown {
95
- const visitor = cfnVisitor(entityNames);
96
- if (!convertKeys) {
97
- // When not converting keys, use a visitor without transformKey
98
- return walkValue(value, entityNames, { ...visitor, transformKey: undefined });
99
- }
100
- return walkValue(value, entityNames, visitor);
98
+ function toCFValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
99
+ return walkValue(value, entityNames, cfnVisitor(entityNames));
101
100
  }
102
101
 
103
102
  /**
@@ -116,8 +115,7 @@ function toProperties(
116
115
 
117
116
  for (const [key, value] of Object.entries(props)) {
118
117
  if (value !== undefined) {
119
- const cfKey = toPascalCase(key);
120
- cfProps[cfKey] = toCFValue(value, entityNames, true);
118
+ cfProps[key] = toCFValue(value, entityNames);
121
119
  }
122
120
  }
123
121
 
@@ -150,6 +148,14 @@ function serializeToTemplate(
150
148
  entityNames.set(entity, name);
151
149
  }
152
150
 
151
+ // Collect default tags
152
+ const defaultTagEntries: TagEntry[] = [];
153
+ for (const [, entity] of entities) {
154
+ if (isDefaultTags(entity)) {
155
+ defaultTagEntries.push(...entity.tags);
156
+ }
157
+ }
158
+
153
159
  // Process entities
154
160
  for (const [name, entity] of entities) {
155
161
  // Skip StackOutput entities — they go in the Outputs section
@@ -157,6 +163,11 @@ function serializeToTemplate(
157
163
  continue;
158
164
  }
159
165
 
166
+ // Skip DefaultTags entities — handled via tag injection below
167
+ if (isDefaultTags(entity)) {
168
+ continue;
169
+ }
170
+
160
171
  if (isCoreParameter(entity)) {
161
172
  if (!template.Parameters) {
162
173
  template.Parameters = {};
@@ -211,15 +222,56 @@ function serializeToTemplate(
211
222
  Type: entity.entityType,
212
223
  };
213
224
 
225
+ // Read resource-level attributes from the second constructor arg
226
+ const attrs = ("attributes" in entity && typeof entity.attributes === "object" && entity.attributes !== null)
227
+ ? entity.attributes as Record<string, unknown>
228
+ : undefined;
229
+
230
+ if (attrs) {
231
+ // DependsOn — resolve Declarable refs to logical names
232
+ if (attrs.DependsOn !== undefined) {
233
+ const resolved = resolveDependsOn(attrs.DependsOn, entityNames, name);
234
+ if (resolved.length > 0) {
235
+ resource.DependsOn = resolved.length === 1 ? resolved[0] : resolved;
236
+ }
237
+ }
238
+ // Pass-through attributes
239
+ if (attrs.Condition) resource.Condition = attrs.Condition as string;
240
+ if (attrs.DeletionPolicy) resource.DeletionPolicy = attrs.DeletionPolicy as string;
241
+ if (attrs.UpdateReplacePolicy) resource.UpdateReplacePolicy = attrs.UpdateReplacePolicy as string;
242
+ if (attrs.UpdatePolicy) resource.UpdatePolicy = attrs.UpdatePolicy;
243
+ if (attrs.CreationPolicy) resource.CreationPolicy = attrs.CreationPolicy;
244
+ if (attrs.Metadata) resource.Metadata = toCFValue(attrs.Metadata, entityNames) as Record<string, unknown>;
245
+ }
246
+
214
247
  const properties = toProperties(entity, entityNames);
215
248
  if (properties) {
216
- resource.Properties = properties;
249
+ if (Object.keys(properties).length > 0) {
250
+ resource.Properties = properties;
251
+ }
217
252
  }
218
253
 
219
254
  template.Resources[name] = resource;
220
255
  }
221
256
  }
222
257
 
258
+ // Inject default tags into taggable resources
259
+ if (defaultTagEntries.length > 0) {
260
+ const taggable = loadTaggableResources();
261
+ for (const [, resource] of Object.entries(template.Resources)) {
262
+ if (!taggable.has(resource.Type)) continue;
263
+ const resolved = defaultTagEntries.map(t => ({
264
+ Key: t.Key,
265
+ Value: toCFValue(t.Value, entityNames),
266
+ }));
267
+ const explicit = (resource.Properties?.Tags ?? []) as Array<{ Key: string }>;
268
+ const explicitKeys = new Set(explicit.map(t => t.Key));
269
+ const merged = [...resolved.filter(t => !explicitKeys.has(t.Key)), ...explicit];
270
+ if (!resource.Properties) resource.Properties = {};
271
+ resource.Properties.Tags = merged;
272
+ }
273
+ }
274
+
223
275
  // Emit StackOutput entities as CF Outputs
224
276
  for (const [name, entity] of entities) {
225
277
  if (isStackOutput(entity)) {
@@ -231,7 +283,7 @@ function serializeToTemplate(
231
283
  const logicalName = ref.getLogicalName();
232
284
  if (logicalName) {
233
285
  const output: CFOutput = {
234
- Value: { "Fn::GetAttr": [logicalName, ref.attribute] },
286
+ Value: { "Fn::GetAtt": [logicalName, ref.attribute] },
235
287
  };
236
288
  if (stackOutput.description) {
237
289
  output.Description = stackOutput.description;
@@ -247,7 +299,7 @@ function serializeToTemplate(
247
299
  for (const output of outputs) {
248
300
  template.Outputs[output.outputName] = {
249
301
  Value: {
250
- "Fn::GetAttr": [output.sourceEntity, output.sourceAttribute],
302
+ "Fn::GetAtt": [output.sourceEntity, output.sourceAttribute],
251
303
  },
252
304
  };
253
305
  }
package/src/spec/fetch.ts CHANGED
@@ -15,6 +15,16 @@ export interface CFNSchema {
15
15
  createOnlyProperties?: string[];
16
16
  writeOnlyProperties?: string[];
17
17
  primaryIdentifier?: string[];
18
+ deprecatedProperties?: string[];
19
+ conditionalCreateOnlyProperties?: string[];
20
+ replacementStrategy?: string;
21
+ tagging?: {
22
+ taggable?: boolean;
23
+ tagOnCreate?: boolean;
24
+ tagUpdatable?: boolean;
25
+ cloudFormationSystemTags?: boolean;
26
+ tagProperty?: string;
27
+ };
18
28
  additionalProperties?: boolean;
19
29
  }
20
30
 
@@ -20,6 +20,7 @@ const sampleBucketSchema = JSON.stringify({
20
20
  },
21
21
  AccessControl: {
22
22
  type: "string",
23
+ description: "This is a legacy property, and it is not recommended for most use cases.",
23
24
  enum: ["Private", "PublicRead", "PublicReadWrite"],
24
25
  },
25
26
  },
@@ -42,6 +43,7 @@ const sampleBucketSchema = JSON.stringify({
42
43
  additionalProperties: false,
43
44
  },
44
45
  },
46
+ deprecatedProperties: ["/properties/AccessControl"],
45
47
  readOnlyProperties: [
46
48
  "/properties/Arn",
47
49
  "/properties/DomainName",
@@ -134,6 +136,145 @@ describe("parseCFNSchema", () => {
134
136
  expect(result.resource.properties).toEqual([]);
135
137
  expect(result.resource.attributes).toEqual([]);
136
138
  });
139
+
140
+ // --- Deprecated properties ---
141
+
142
+ test("parses explicit deprecatedProperties", () => {
143
+ const result = parseCFNSchema(sampleBucketSchema);
144
+ expect(result.resource.deprecatedProperties).toContain("AccessControl");
145
+ });
146
+
147
+ test("mines deprecation from property description", () => {
148
+ const schema = JSON.stringify({
149
+ typeName: "AWS::Test::DescMined",
150
+ properties: {
151
+ OldProp: { type: "string", description: "This property is deprecated. Use NewProp instead." },
152
+ NewProp: { type: "string", description: "The replacement property" },
153
+ },
154
+ additionalProperties: false,
155
+ });
156
+ const result = parseCFNSchema(schema);
157
+ expect(result.resource.deprecatedProperties).toContain("OldProp");
158
+ expect(result.resource.deprecatedProperties).not.toContain("NewProp");
159
+ });
160
+
161
+ test("mines 'legacy' keyword from description", () => {
162
+ const schema = JSON.stringify({
163
+ typeName: "AWS::Test::Legacy",
164
+ properties: {
165
+ LegacyProp: { type: "string", description: "This is a legacy property, not recommended." },
166
+ },
167
+ additionalProperties: false,
168
+ });
169
+ const result = parseCFNSchema(schema);
170
+ expect(result.resource.deprecatedProperties).toContain("LegacyProp");
171
+ });
172
+
173
+ test("deduplicates when both explicit and description flag same property", () => {
174
+ // sampleBucketSchema has AccessControl in both deprecatedProperties array and description
175
+ const result = parseCFNSchema(sampleBucketSchema);
176
+ const count = result.resource.deprecatedProperties.filter((p) => p === "AccessControl").length;
177
+ expect(count).toBe(1);
178
+ });
179
+
180
+ test("empty deprecatedProperties when none found", () => {
181
+ const schema = JSON.stringify({
182
+ typeName: "AWS::Test::Clean",
183
+ properties: {
184
+ Name: { type: "string", description: "A normal property" },
185
+ },
186
+ additionalProperties: false,
187
+ });
188
+ const result = parseCFNSchema(schema);
189
+ expect(result.resource.deprecatedProperties).toEqual([]);
190
+ });
191
+
192
+ // --- Tagging metadata ---
193
+
194
+ test("parses tagging metadata when taggable", () => {
195
+ const schema = JSON.stringify({
196
+ typeName: "AWS::Test::Taggable",
197
+ properties: { Tags: { type: "array" } },
198
+ tagging: { taggable: true, tagOnCreate: true, tagUpdatable: true },
199
+ additionalProperties: false,
200
+ });
201
+ const result = parseCFNSchema(schema);
202
+ expect(result.resource.tagging).toEqual({
203
+ taggable: true,
204
+ tagOnCreate: true,
205
+ tagUpdatable: true,
206
+ });
207
+ });
208
+
209
+ test("omits tagging when not taggable", () => {
210
+ const schema = JSON.stringify({
211
+ typeName: "AWS::Test::NotTaggable",
212
+ properties: { Name: { type: "string" } },
213
+ tagging: { taggable: false },
214
+ additionalProperties: false,
215
+ });
216
+ const result = parseCFNSchema(schema);
217
+ expect(result.resource.tagging).toBeUndefined();
218
+ });
219
+
220
+ test("omits tagging when absent", () => {
221
+ const schema = JSON.stringify({
222
+ typeName: "AWS::Test::NoTagging",
223
+ properties: { Name: { type: "string" } },
224
+ additionalProperties: false,
225
+ });
226
+ const result = parseCFNSchema(schema);
227
+ expect(result.resource.tagging).toBeUndefined();
228
+ });
229
+
230
+ // --- Replacement strategy ---
231
+
232
+ test("parses replacementStrategy", () => {
233
+ const schema = JSON.stringify({
234
+ typeName: "AWS::Test::DeleteFirst",
235
+ properties: { Name: { type: "string" } },
236
+ replacementStrategy: "delete_then_create",
237
+ additionalProperties: false,
238
+ });
239
+ const result = parseCFNSchema(schema);
240
+ expect(result.resource.replacementStrategy).toBe("delete_then_create");
241
+ });
242
+
243
+ test("omits replacementStrategy when absent", () => {
244
+ const schema = JSON.stringify({
245
+ typeName: "AWS::Test::NoStrategy",
246
+ properties: { Name: { type: "string" } },
247
+ additionalProperties: false,
248
+ });
249
+ const result = parseCFNSchema(schema);
250
+ expect(result.resource.replacementStrategy).toBeUndefined();
251
+ });
252
+
253
+ // --- Conditional create-only ---
254
+
255
+ test("parses conditionalCreateOnlyProperties", () => {
256
+ const schema = JSON.stringify({
257
+ typeName: "AWS::Test::ConditionalCreate",
258
+ properties: {
259
+ Name: { type: "string" },
260
+ Engine: { type: "string" },
261
+ },
262
+ conditionalCreateOnlyProperties: ["/properties/Engine"],
263
+ additionalProperties: false,
264
+ });
265
+ const result = parseCFNSchema(schema);
266
+ expect(result.resource.conditionalCreateOnly).toContain("Engine");
267
+ });
268
+
269
+ test("empty conditionalCreateOnly when absent", () => {
270
+ const schema = JSON.stringify({
271
+ typeName: "AWS::Test::NoConditional",
272
+ properties: { Name: { type: "string" } },
273
+ additionalProperties: false,
274
+ });
275
+ const result = parseCFNSchema(schema);
276
+ expect(result.resource.conditionalCreateOnly).toEqual([]);
277
+ });
137
278
  });
138
279
 
139
280
  describe("cfnShortName", () => {
package/src/spec/parse.ts CHANGED
@@ -52,6 +52,10 @@ export interface ParsedResource {
52
52
  createOnly: string[];
53
53
  writeOnly: string[];
54
54
  primaryIdentifier: string[];
55
+ deprecatedProperties: string[];
56
+ conditionalCreateOnly: string[];
57
+ replacementStrategy?: "delete_then_create" | "create_then_delete";
58
+ tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
55
59
  }
56
60
 
57
61
  export interface SchemaParseResult {
@@ -137,6 +141,38 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
137
141
  }
138
142
  }
139
143
 
144
+ // --- Deprecated properties: explicit + description-mined ---
145
+ const deprecatedSet = new Set<string>(
146
+ stripPointerPaths(schema.deprecatedProperties ?? []),
147
+ );
148
+
149
+ const DEPRECATION_RE = /\bdeprecated\b|\blegacy\b|no longer (available|recommended|used|supported)|is not recommended|has been discontinued/i;
150
+
151
+ // Mine top-level property descriptions
152
+ if (schema.properties) {
153
+ for (const [name, prop] of Object.entries(schema.properties)) {
154
+ if (prop.description && DEPRECATION_RE.test(prop.description)) {
155
+ deprecatedSet.add(name);
156
+ }
157
+ }
158
+ }
159
+
160
+ // --- Tagging ---
161
+ let tagging: ParsedResource["tagging"];
162
+ if (schema.tagging && schema.tagging.taggable) {
163
+ tagging = {
164
+ taggable: true,
165
+ tagOnCreate: schema.tagging.tagOnCreate ?? false,
166
+ tagUpdatable: schema.tagging.tagUpdatable ?? false,
167
+ };
168
+ }
169
+
170
+ // --- Replacement strategy ---
171
+ let replacementStrategy: ParsedResource["replacementStrategy"];
172
+ if (schema.replacementStrategy === "delete_then_create" || schema.replacementStrategy === "create_then_delete") {
173
+ replacementStrategy = schema.replacementStrategy;
174
+ }
175
+
140
176
  return {
141
177
  resource: {
142
178
  typeName: schema.typeName,
@@ -145,6 +181,10 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
145
181
  createOnly: stripPointerPaths(schema.createOnlyProperties ?? []),
146
182
  writeOnly: stripPointerPaths(schema.writeOnlyProperties ?? []),
147
183
  primaryIdentifier: stripPointerPaths(schema.primaryIdentifier ?? []),
184
+ deprecatedProperties: [...deprecatedSet],
185
+ conditionalCreateOnly: stripPointerPaths(schema.conditionalCreateOnlyProperties ?? []),
186
+ ...(replacementStrategy && { replacementStrategy }),
187
+ ...(tagging && { tagging }),
148
188
  },
149
189
  propertyTypes,
150
190
  enums,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared taggable resource lookup — loads from the generated lexicon JSON.
3
+ *
4
+ * Lazy-loaded and cached for the lifetime of the process.
5
+ */
6
+
7
+ import { readFileSync } from "fs";
8
+ import { join } from "path";
9
+
10
+ interface LexiconEntry {
11
+ kind: string;
12
+ resourceType: string;
13
+ tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ let _cached: Set<string> | undefined;
18
+
19
+ /**
20
+ * Load taggable resource types from the lexicon JSON.
21
+ * Result is cached after first call.
22
+ */
23
+ export function loadTaggableResources(): Set<string> {
24
+ if (_cached) return _cached;
25
+
26
+ const set = new Set<string>();
27
+ try {
28
+ const pkgDir = join(__dirname, "..");
29
+ const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
30
+ const content = readFileSync(lexiconPath, "utf-8");
31
+ const data = JSON.parse(content) as Record<string, LexiconEntry>;
32
+
33
+ for (const [_name, entry] of Object.entries(data)) {
34
+ if (entry.kind === "resource" && entry.resourceType && entry.tagging?.taggable) {
35
+ set.add(entry.resourceType);
36
+ }
37
+ }
38
+ } catch {
39
+ // Lexicon not available — skip
40
+ }
41
+
42
+ _cached = set;
43
+ return set;
44
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * App layer — Lambda function in the parent template that references
3
+ * the network nested stack's outputs via cross-stack references
4
+ */
5
+
6
+ import { Function, Sub, AWS, Ref, nestedStack } from "@intentius/chant-lexicon-aws";
7
+
8
+ // nestedStack() references a child project directory
9
+ const network = nestedStack("network", import.meta.dirname + "/network", {
10
+ parameters: { Environment: "prod" },
11
+ });
12
+
13
+ export const handler = new Function({
14
+ FunctionName: Sub`${AWS.StackName}-handler`,
15
+ Runtime: "nodejs20.x",
16
+ Handler: "index.handler",
17
+ Role: Ref("LambdaExecutionRole"),
18
+ Code: { ZipFile: "exports.handler = async () => ({ statusCode: 200 });" },
19
+ VpcConfig: {
20
+ SubnetIds: [network.outputs.subnetId],
21
+ SecurityGroupIds: [network.outputs.lambdaSgId],
22
+ },
23
+ });
24
+
25
+ // Re-export so discovery picks it up as an entity
26
+ export { network };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Cross-stack outputs — values the parent can reference
3
+ */
4
+
5
+ import { stackOutput } from "@intentius/chant";
6
+ import { vpc, subnet } from "./vpc";
7
+ import { lambdaSg } from "./security";
8
+
9
+ export const vpcId = stackOutput(vpc.VpcId, {
10
+ description: "VPC ID",
11
+ });
12
+ export const subnetId = stackOutput(subnet.SubnetId, {
13
+ description: "Public subnet ID",
14
+ });
15
+ export const lambdaSgId = stackOutput(lambdaSg.GroupId, {
16
+ description: "Lambda security group ID",
17
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Security group for the Lambda function in the parent stack
3
+ */
4
+
5
+ import { SecurityGroup, Sub, AWS } from "@intentius/chant-lexicon-aws";
6
+ import { vpc } from "./vpc";
7
+
8
+ export const lambdaSg = new SecurityGroup({
9
+ GroupDescription: Sub`${AWS.StackName} Lambda security group`,
10
+ VpcId: vpc.VpcId,
11
+ SecurityGroupEgress: [{
12
+ IpProtocol: "-1",
13
+ CidrIp: "0.0.0.0/0",
14
+ Description: "Allow all outbound",
15
+ }],
16
+ Tags: [{ Key: "Name", Value: Sub`${AWS.StackName}-lambda-sg` }],
17
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Network resources — VPC, subnet, internet gateway, and routing
3
+ */
4
+
5
+ import {
6
+ Vpc,
7
+ Subnet,
8
+ InternetGateway,
9
+ VPCGatewayAttachment,
10
+ RouteTable,
11
+ EC2Route,
12
+ SubnetRouteTableAssociation,
13
+ Sub,
14
+ AWS,
15
+ } from "@intentius/chant-lexicon-aws";
16
+
17
+ export const vpc = new Vpc({
18
+ CidrBlock: "10.0.0.0/16",
19
+ EnableDnsSupport: true,
20
+ EnableDnsHostnames: true,
21
+ Tags: [{ Key: "Name", Value: Sub`${AWS.StackName}-vpc` }],
22
+ });
23
+
24
+ export const subnet = new Subnet({
25
+ VpcId: vpc.VpcId,
26
+ CidrBlock: "10.0.1.0/24",
27
+ MapPublicIpOnLaunch: true,
28
+ Tags: [{ Key: "Name", Value: Sub`${AWS.StackName}-public` }],
29
+ });
30
+
31
+ export const igw = new InternetGateway({
32
+ Tags: [{ Key: "Name", Value: Sub`${AWS.StackName}-igw` }],
33
+ });
34
+
35
+ export const igwAttachment = new VPCGatewayAttachment({
36
+ VpcId: vpc.VpcId,
37
+ InternetGatewayId: igw.InternetGatewayId,
38
+ });
39
+
40
+ export const routeTable = new RouteTable({
41
+ VpcId: vpc.VpcId,
42
+ Tags: [{ Key: "Name", Value: Sub`${AWS.StackName}-public-rt` }],
43
+ });
44
+
45
+ export const defaultRoute = new EC2Route({
46
+ RouteTableId: routeTable.RouteTableId,
47
+ DestinationCidrBlock: "0.0.0.0/0",
48
+ GatewayId: igw.InternetGatewayId,
49
+ });
50
+
51
+ export const subnetRouteTableAssoc = new SubnetRouteTableAssociation({
52
+ SubnetId: subnet.SubnetId,
53
+ RouteTableId: routeTable.RouteTableId,
54
+ });
@@ -1,41 +0,0 @@
1
- ---
2
- name: aws-cloudformation
3
- description: AWS CloudFormation best practices and common patterns
4
- ---
5
-
6
- # AWS CloudFormation with Chant
7
-
8
- ## Common Resource Types
9
-
10
- - `AWS::S3::Bucket` — Object storage
11
- - `AWS::Lambda::Function` — Serverless compute
12
- - `AWS::DynamoDB::Table` — NoSQL database
13
- - `AWS::IAM::Role` — Identity and access management
14
- - `AWS::SNS::Topic` — Pub/sub messaging
15
- - `AWS::SQS::Queue` — Message queue
16
- - `AWS::EC2::SecurityGroup` — Network firewall rules
17
-
18
- ## Intrinsic Functions
19
-
20
- - `Sub` — String interpolation with `${}` syntax
21
- - `Ref` — Reference a resource or parameter
22
- - `GetAtt` — Get a resource attribute (e.g. ARN)
23
- - `If` — Conditional value based on a condition
24
- - `Join` — Join strings with a delimiter
25
- - `Select` — Pick an item from a list by index
26
-
27
- ## Pseudo Parameters
28
-
29
- - `AWS::StackName` — Name of the current stack
30
- - `AWS::Region` — Current deployment region
31
- - `AWS::AccountId` — Current AWS account ID
32
- - `AWS::Partition` — Partition (aws, aws-cn, aws-us-gov)
33
-
34
- ## Best Practices
35
-
36
- 1. **Always enable encryption** — Use `BucketEncryption` for S3, `SSESpecification` for DynamoDB
37
- 2. **Block public access** — Set `PublicAccessBlockConfiguration` on all S3 buckets
38
- 3. **Use least-privilege IAM** — Avoid `*` in IAM policy actions and resources
39
- 4. **Enable versioning** — Turn on `VersioningConfiguration` for data buckets
40
- 5. **Use Sub for dynamic names** — `Sub\`\${AWS::StackName}-suffix\`` for unique naming
41
- 6. **Share config via direct imports** — Put common settings in a config file and import directly