@intentius/chant-lexicon-aws 0.55.0 → 0.57.0

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 (45) hide show
  1. package/dist/condition.d.ts +27 -0
  2. package/dist/condition.d.ts.map +1 -0
  3. package/dist/generated/index.d.ts +1 -1
  4. package/dist/generated/index.d.ts.map +1 -1
  5. package/dist/import/generator.d.ts +19 -0
  6. package/dist/import/generator.d.ts.map +1 -1
  7. package/dist/import/parser.d.ts +19 -0
  8. package/dist/import/parser.d.ts.map +1 -1
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/integrity.json +4 -3
  12. package/dist/intrinsics.d.ts +77 -3
  13. package/dist/intrinsics.d.ts.map +1 -1
  14. package/dist/lint/audit-catalog.d.ts.map +1 -1
  15. package/dist/lint/post-synth/index.d.ts.map +1 -1
  16. package/dist/lint/post-synth/waw069.d.ts +15 -0
  17. package/dist/lint/post-synth/waw069.d.ts.map +1 -0
  18. package/dist/manifest.json +29 -1
  19. package/dist/okf/index.md +1 -0
  20. package/dist/okf/rules/WAW069.md +11 -0
  21. package/dist/plugin.d.ts.map +1 -1
  22. package/dist/rules/waw069.ts +0 -0
  23. package/dist/serializer.d.ts.map +1 -1
  24. package/dist/types/index.d.ts +3 -3
  25. package/package.json +2 -2
  26. package/src/codegen/generate-typescript.ts +3 -3
  27. package/src/codegen/generate.ts +1 -1
  28. package/src/condition.test.ts +129 -0
  29. package/src/condition.ts +40 -0
  30. package/src/generated/index.d.ts +3 -3
  31. package/src/generated/index.ts +1 -1
  32. package/src/import/generator.ts +131 -50
  33. package/src/import/parser.test.ts +96 -0
  34. package/src/import/parser.ts +86 -1
  35. package/src/import/roundtrip-fixtures.test.ts +50 -0
  36. package/src/index.ts +12 -0
  37. package/src/intrinsics.ts +130 -5
  38. package/src/lint/audit-catalog.ts +1 -0
  39. package/src/lint/post-synth/index.ts +2 -0
  40. package/src/lint/post-synth/waw069.test.ts +94 -0
  41. package/src/lint/post-synth/waw069.ts +0 -0
  42. package/src/plugin.test.ts +5 -1
  43. package/src/plugin.ts +4 -0
  44. package/src/serializer.ts +24 -3
  45. package/src/testdata/roundtrip/with-conditions.json +16 -0
@@ -10765,5 +10765,5 @@ export const ZonalShiftConfig = createProperty("AWS::EKS::Cluster.ZonalShiftConf
10765
10765
  export const ZookeeperAccess = createProperty("AWS::MSK::Cluster.ZookeeperAccess", "aws");
10766
10766
 
10767
10767
  // Re-exports for convenience
10768
- export { Sub, Ref, GetAtt, If, Join, Select, Split, Base64 } from "../intrinsics";
10768
+ export { Sub, Ref, GetAtt, If, Join, Select, Split, Base64, Equals, Not } from "../intrinsics";
10769
10769
  export { AWS, StackName, Region, AccountId, StackId, URLSuffix, NoValue, NotificationARNs } from "../pseudo";
@@ -1,6 +1,6 @@
1
1
  import { loadLexiconRegistry } from "@intentius/chant/codegen/registry";
2
2
  import { createRequire } from "module";
3
- import type { TemplateIR, ResourceIR, ParameterIR } from "@intentius/chant/import/parser";
3
+ import type { TemplateIR, ResourceIR, ParameterIR, ConditionIR, OutputIR } from "@intentius/chant/import/parser";
4
4
  const require = createRequire(import.meta.url);
5
5
  import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
6
6
  import { topoSort } from "@intentius/chant/codegen/topo-sort";
@@ -51,12 +51,33 @@ export class CFGenerator implements TypeScriptGenerator {
51
51
  lines.push("");
52
52
  }
53
53
 
54
+ // Generate conditions in dependency order ({ Condition: ... } references
55
+ // point at earlier declarations) — #2069
56
+ const conditions = ir.conditions ?? [];
57
+ const sortedConditions = this.sortConditions(conditions);
58
+ for (const condition of sortedConditions) {
59
+ lines.push(this.generateCondition(condition, ir, importedSymbols));
60
+ }
61
+
62
+ if (conditions.length > 0) {
63
+ lines.push("");
64
+ }
65
+
54
66
  // Generate resources in dependency order
55
67
  const sortedResources = this.sortByDependencies(ir.resources);
56
68
  for (const resource of sortedResources) {
57
69
  lines.push(this.generateResource(resource, ir, importedSymbols));
58
70
  }
59
71
 
72
+ // Generate outputs (#2069)
73
+ const outputs = ir.outputs ?? [];
74
+ if (outputs.length > 0) {
75
+ lines.push("");
76
+ for (const output of outputs) {
77
+ lines.push(this.generateOutput(output, ir, importedSymbols));
78
+ }
79
+ }
80
+
60
81
  return [
61
82
  {
62
83
  path: "main.ts",
@@ -71,10 +92,14 @@ export class CFGenerator implements TypeScriptGenerator {
71
92
  private collectImportedSymbols(ir: TemplateIR): Set<string> {
72
93
  const symbols = new Set<string>();
73
94
  if (ir.parameters.length > 0) symbols.add("Parameter");
74
- const intrinsics = ["Sub", "Ref", "If", "Join", "Select", "Split", "Base64", "GetAZs", "GetAtt"] as const;
95
+ if ((ir.conditions ?? []).length > 0) symbols.add("Condition");
96
+ if ((ir.outputs ?? []).length > 0) symbols.add("stackOutput");
97
+ const intrinsics = ["Sub", "Ref", "If", "Join", "Select", "Split", "Base64", "GetAZs", "GetAtt", "Equals", "And", "Or", "Not"] as const;
75
98
  for (const name of intrinsics) {
76
99
  if (irUsesIntrinsic(ir, name)) symbols.add(name);
77
100
  }
101
+ // Outputs whose value is a Ref envelope render as Ref(<var>) (#2069)
102
+ if ((ir.outputs ?? []).some((o) => hasIntrinsicInValue(o.value, "Ref"))) symbols.add("Ref");
78
103
  if (this.needsAWSPseudo(ir)) symbols.add("AWS");
79
104
  for (const resource of ir.resources) {
80
105
  const parsed = this.parseResourceType(resource.type);
@@ -94,54 +119,14 @@ export class CFGenerator implements TypeScriptGenerator {
94
119
  * Generate import statements
95
120
  */
96
121
  private generateImports(ir: TemplateIR): string {
97
- const imports: Set<string> = new Set();
98
- const serviceImports: Map<string, Set<string>> = new Map();
99
-
100
- // Collect what we need to import
101
- const needsParameter = ir.parameters.length > 0;
102
- if (needsParameter) {
103
- imports.add("Parameter");
104
- }
105
-
106
- // Check for intrinsics
107
- const intrinsics = ["Sub", "Ref", "If", "Join", "Select", "Split", "Base64", "GetAZs", "GetAtt"] as const;
108
- for (const name of intrinsics) {
109
- if (irUsesIntrinsic(ir, name)) imports.add(name);
110
- }
111
-
112
- // Check for AWS pseudo-parameters
113
- if (this.needsAWSPseudo(ir)) {
114
- imports.add("AWS");
115
- }
116
-
117
- // Collect service imports (skip unknown resource types)
118
- for (const resource of ir.resources) {
119
- const parsed = this.parseResourceType(resource.type);
120
- if (!parsed) continue;
121
- const { service, resourceClass } = parsed;
122
- if (!serviceImports.has(service)) {
123
- serviceImports.set(service, new Set());
124
- }
125
- serviceImports.get(service)!.add(resourceClass);
126
- }
127
-
128
- // Build import lines
129
- const importLines: string[] = [];
130
-
131
- // Merge service resource imports into the core imports set
132
- for (const [_service, resources] of serviceImports) {
133
- for (const r of resources) {
134
- imports.add(r);
135
- }
122
+ // Everything comes from the flat @intentius/chant-lexicon-aws package,
123
+ // and the needed symbol set is exactly what collectImportedSymbols
124
+ // computes for variable-name conflict detection.
125
+ const allImports = [...this.collectImportedSymbols(ir)];
126
+ if (allImports.length === 0) {
127
+ return "";
136
128
  }
137
-
138
- // All imports come from the flat @intentius/chant-lexicon-aws package
139
- const allImports = [...imports];
140
- if (allImports.length > 0) {
141
- importLines.push(`import { ${allImports.join(", ")} } from "@intentius/chant-lexicon-aws";`);
142
- }
143
-
144
- return importLines.join("\n");
129
+ return `import { ${allImports.join(", ")} } from "@intentius/chant-lexicon-aws";`;
145
130
  }
146
131
 
147
132
  /**
@@ -237,6 +222,76 @@ export class CFGenerator implements TypeScriptGenerator {
237
222
  );
238
223
  }
239
224
 
225
+ /**
226
+ * Sort conditions so `{ Condition: ... }` references point at earlier
227
+ * declarations (#2069).
228
+ */
229
+ private sortConditions(conditions: ConditionIR[]): ConditionIR[] {
230
+ return topoSort(
231
+ conditions,
232
+ (c) => c.name,
233
+ (c) => [
234
+ ...collectDependencies(c.expression, (obj) =>
235
+ obj.__intrinsic === "ConditionRef" ? (obj.name as string) : null,
236
+ ),
237
+ ],
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Render a condition reference as the condition's variable when the
243
+ * template declares it, or as a literal name string when it doesn't (an
244
+ * undeclared reference stays visible rather than breaking generation).
245
+ */
246
+ private conditionVarRef(name: string, ir: TemplateIR, importedSymbols: Set<string>): string {
247
+ const declared = (ir.conditions ?? []).some((c) => c.name === name);
248
+ return declared ? this.safeVarName(name, importedSymbols) : JSON.stringify(name);
249
+ }
250
+
251
+ /**
252
+ * Generate a condition declaration (#2069)
253
+ */
254
+ private generateCondition(condition: ConditionIR, ir: TemplateIR, importedSymbols: Set<string>): string {
255
+ const varName = this.safeVarName(condition.name, importedSymbols);
256
+ const exprStr = this.generateValue(condition.expression, ir, importedSymbols);
257
+ return `export const ${varName} = new Condition(${exprStr});`;
258
+ }
259
+
260
+ /**
261
+ * Generate an output declaration as stackOutput(...) (#2069)
262
+ */
263
+ private generateOutput(output: OutputIR, ir: TemplateIR, importedSymbols: Set<string>): string {
264
+ const varName = this.safeVarName(output.name, importedSymbols);
265
+
266
+ // A bare Ref envelope becomes Ref(<var>) — stackOutput takes an intrinsic
267
+ // or attribute reference, not the resource/parameter object itself.
268
+ let valueStr: string;
269
+ const value = output.value as Record<string, unknown> | null;
270
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && value.__intrinsic === "Ref" && !(value.name as string).startsWith("AWS::")) {
271
+ valueStr = `Ref(${this.safeVarName(value.name as string, importedSymbols)})`;
272
+ } else {
273
+ valueStr = this.generateValue(output.value, ir, importedSymbols);
274
+ }
275
+
276
+ const opts: string[] = [];
277
+ if (output.description) opts.push(`description: ${JSON.stringify(output.description)}`);
278
+ if (output.exportName !== undefined) {
279
+ opts.push(`exportName: ${this.generateValue(output.exportName, ir, importedSymbols)}`);
280
+ }
281
+ if (output.condition) {
282
+ opts.push(`condition: ${this.conditionVarRef(output.condition, ir, importedSymbols)}`);
283
+ }
284
+ // A literal output has no entity to derive its lexicon from.
285
+ if (typeof output.value === "string") {
286
+ opts.push(`lexicon: "aws"`);
287
+ }
288
+
289
+ if (opts.length > 0) {
290
+ return `export const ${varName} = stackOutput(${valueStr}, { ${opts.join(", ")} });`;
291
+ }
292
+ return `export const ${varName} = stackOutput(${valueStr});`;
293
+ }
294
+
240
295
  /**
241
296
  * Generate a parameter declaration
242
297
  */
@@ -264,6 +319,12 @@ export class CFGenerator implements TypeScriptGenerator {
264
319
  const { resourceClass } = parsed;
265
320
  const propsStr = this.generateProps(resource.properties, ir, importedSymbols);
266
321
 
322
+ // Resource-level Condition key → the attributes argument (#2069)
323
+ if (resource.condition) {
324
+ const condRef = this.conditionVarRef(resource.condition, ir, importedSymbols);
325
+ return `export const ${varName} = new ${resourceClass}(${propsStr}, { Condition: ${condRef} });`;
326
+ }
327
+
267
328
  if (propsStr === "{}") {
268
329
  return `export const ${varName} = new ${resourceClass}();`;
269
330
  }
@@ -344,9 +405,29 @@ export class CFGenerator implements TypeScriptGenerator {
344
405
 
345
406
  if (obj.__intrinsic === "If") {
346
407
  const condition = obj.condition as string;
408
+ const condRef = this.conditionVarRef(condition, ir, importedSymbols);
347
409
  const trueVal = this.generateValue(obj.valueIfTrue, ir, importedSymbols);
348
410
  const falseVal = this.generateValue(obj.valueIfFalse, ir, importedSymbols);
349
- return `If("${condition}", ${trueVal}, ${falseVal})`;
411
+ return `If(${condRef}, ${trueVal}, ${falseVal})`;
412
+ }
413
+
414
+ if (obj.__intrinsic === "Equals") {
415
+ const left = this.generateValue(obj.left, ir, importedSymbols);
416
+ const right = this.generateValue(obj.right, ir, importedSymbols);
417
+ return `Equals(${left}, ${right})`;
418
+ }
419
+
420
+ if (obj.__intrinsic === "And" || obj.__intrinsic === "Or") {
421
+ const operands = (obj.conditions as unknown[]).map((c) => this.generateValue(c, ir, importedSymbols));
422
+ return `${obj.__intrinsic}(${operands.join(", ")})`;
423
+ }
424
+
425
+ if (obj.__intrinsic === "Not") {
426
+ return `Not(${this.generateValue(obj.condition, ir, importedSymbols)})`;
427
+ }
428
+
429
+ if (obj.__intrinsic === "ConditionRef") {
430
+ return this.conditionVarRef(obj.name as string, ir, importedSymbols);
350
431
  }
351
432
 
352
433
  if (obj.__intrinsic === "Join") {
@@ -198,3 +198,99 @@ describe("CFParser", () => {
198
198
  expect(vars.KEY).toBe("value");
199
199
  });
200
200
  });
201
+
202
+ describe("CFParser conditions and outputs (#2069)", () => {
203
+ const parser = new CFParser();
204
+
205
+ const template = JSON.stringify({
206
+ AWSTemplateFormatVersion: "2010-09-09",
207
+ Parameters: { Cutover: { Type: "String", Default: "false" } },
208
+ Conditions: {
209
+ DoCutover: { "Fn::Equals": [{ Ref: "Cutover" }, "true"] },
210
+ NoCutover: { "Fn::Not": [{ Condition: "DoCutover" }] },
211
+ },
212
+ Resources: {
213
+ Rule: {
214
+ Type: "AWS::Logs::LogGroup",
215
+ Condition: "DoCutover",
216
+ Properties: { LogGroupName: { "Fn::If": ["DoCutover", "/on", "/off"] } },
217
+ },
218
+ },
219
+ Outputs: {
220
+ RuleName: { Condition: "DoCutover", Value: { Ref: "Rule" }, Description: "d", Export: { Name: "rule-name" } },
221
+ Plain: { Value: "literal" },
222
+ },
223
+ });
224
+
225
+ test("parses the Conditions section, Condition references included", () => {
226
+ const ir = parser.parse(template);
227
+ expect(ir.conditions).toHaveLength(2);
228
+ expect(ir.conditions![0]).toEqual({
229
+ name: "DoCutover",
230
+ expression: { __intrinsic: "Equals", left: { __intrinsic: "Ref", name: "Cutover" }, right: "true" },
231
+ });
232
+ expect(ir.conditions![1]).toEqual({
233
+ name: "NoCutover",
234
+ expression: { __intrinsic: "Not", condition: { __intrinsic: "ConditionRef", name: "DoCutover" } },
235
+ });
236
+ });
237
+
238
+ test("carries the resource-level Condition key", () => {
239
+ const ir = parser.parse(template);
240
+ expect(ir.resources[0].condition).toBe("DoCutover");
241
+ });
242
+
243
+ test("parses Outputs with Condition, Description, and Export", () => {
244
+ const ir = parser.parse(template);
245
+ expect(ir.outputs).toHaveLength(2);
246
+ expect(ir.outputs![0]).toEqual({
247
+ name: "RuleName",
248
+ value: { __intrinsic: "Ref", name: "Rule" },
249
+ description: "d",
250
+ exportName: "rule-name",
251
+ condition: "DoCutover",
252
+ });
253
+ expect(ir.outputs![1]).toEqual({
254
+ name: "Plain",
255
+ value: "literal",
256
+ description: undefined,
257
+ exportName: undefined,
258
+ condition: undefined,
259
+ });
260
+ });
261
+
262
+ test("a single-key Condition object in resource properties is NOT a condition reference", () => {
263
+ const ir = parser.parse(
264
+ JSON.stringify({
265
+ Resources: {
266
+ Role: {
267
+ Type: "AWS::IAM::Role",
268
+ Properties: {
269
+ Policy: { Condition: "not-a-ref" },
270
+ },
271
+ },
272
+ },
273
+ }),
274
+ );
275
+ expect(ir.resources[0].properties.Policy).toEqual({ Condition: "not-a-ref" });
276
+ });
277
+
278
+ test("names sections import cannot carry instead of dropping them silently", () => {
279
+ const ir = parser.parse(
280
+ JSON.stringify({
281
+ Resources: { R: { Type: "AWS::S3::Bucket" } },
282
+ Mappings: { M: {} },
283
+ Rules: { R1: {} },
284
+ }),
285
+ );
286
+ expect(ir.warnings).toEqual([
287
+ 'Template section "Mappings" is not carried by import — it is dropped from the generated source',
288
+ 'Template section "Rules" is not carried by import — it is dropped from the generated source',
289
+ ]);
290
+ });
291
+
292
+ test("no warnings for fully-carried templates", () => {
293
+ const ir = parser.parse(template);
294
+ expect(ir.warnings).toBeUndefined();
295
+ });
296
+ });
@@ -3,6 +3,8 @@ import type {
3
3
  TemplateIR,
4
4
  ResourceIR,
5
5
  ParameterIR,
6
+ ConditionIR,
7
+ OutputIR,
6
8
  } from "@intentius/chant/import/parser";
7
9
  import { BaseValueParser } from "@intentius/chant/import/base-parser";
8
10
  import yaml from "js-yaml";
@@ -114,8 +116,19 @@ interface CFTemplate {
114
116
  AWSTemplateFormatVersion?: string;
115
117
  Description?: string;
116
118
  Parameters?: Record<string, CFParameter>;
119
+ Conditions?: Record<string, unknown>;
117
120
  Resources?: Record<string, CFResource>;
118
- Outputs?: Record<string, unknown>;
121
+ Outputs?: Record<string, CFOutput>;
122
+ }
123
+
124
+ /**
125
+ * CloudFormation output
126
+ */
127
+ interface CFOutput {
128
+ Value: unknown;
129
+ Description?: string;
130
+ Export?: { Name?: unknown };
131
+ Condition?: string;
119
132
  }
120
133
 
121
134
  /**
@@ -135,6 +148,7 @@ interface CFResource {
135
148
  Properties?: Record<string, unknown>;
136
149
  Metadata?: Record<string, unknown>;
137
150
  DependsOn?: string | string[];
151
+ Condition?: string;
138
152
  }
139
153
 
140
154
  /**
@@ -156,11 +170,17 @@ export class CFParser extends BaseValueParser implements TemplateParser {
156
170
  const template = parsed as CFTemplate;
157
171
 
158
172
  const parameters = this.parseParameters(template.Parameters ?? {});
173
+ const conditions = this.parseConditions(template.Conditions ?? {});
159
174
  const resources = this.parseResources(template.Resources ?? {});
175
+ const outputs = this.parseOutputs(template.Outputs ?? {});
176
+ const warnings = this.collectDroppedSectionWarnings(template as unknown as Record<string, unknown>);
160
177
 
161
178
  return {
162
179
  parameters,
180
+ conditions: conditions.length > 0 ? conditions : undefined,
163
181
  resources,
182
+ outputs: outputs.length > 0 ? outputs : undefined,
183
+ warnings: warnings.length > 0 ? warnings : undefined,
164
184
  metadata: {
165
185
  version: template.AWSTemplateFormatVersion ?? "2010-09-09",
166
186
  description: template.Description,
@@ -168,6 +188,64 @@ export class CFParser extends BaseValueParser implements TemplateParser {
168
188
  };
169
189
  }
170
190
 
191
+ /**
192
+ * Template sections import carries. Anything else is named in a warning
193
+ * rather than dropped silently (#2069).
194
+ */
195
+ private static readonly CARRIED_SECTIONS = new Set([
196
+ "AWSTemplateFormatVersion",
197
+ "Description",
198
+ "Parameters",
199
+ "Conditions",
200
+ "Resources",
201
+ "Outputs",
202
+ ]);
203
+
204
+ private collectDroppedSectionWarnings(template: Record<string, unknown>): string[] {
205
+ const warnings: string[] = [];
206
+ for (const key of Object.keys(template)) {
207
+ if (!CFParser.CARRIED_SECTIONS.has(key)) {
208
+ warnings.push(`Template section "${key}" is not carried by import — it is dropped from the generated source`);
209
+ }
210
+ }
211
+ return warnings;
212
+ }
213
+
214
+ /**
215
+ * Parse the Conditions section (#2069). Inside a condition expression the
216
+ * single-key `{ "Condition": "<name>" }` form references another declared
217
+ * condition; `inConditionExpression` scopes that dispatch to this section
218
+ * so a resource property that happens to hold a single-key `Condition`
219
+ * object is left alone.
220
+ */
221
+ private inConditionExpression = false;
222
+
223
+ private parseConditions(conditions: Record<string, unknown>): ConditionIR[] {
224
+ return Object.entries(conditions).map(([name, expression]) => {
225
+ this.inConditionExpression = true;
226
+ try {
227
+ return { name, expression: this.parseValue(expression) };
228
+ } finally {
229
+ this.inConditionExpression = false;
230
+ }
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Parse the Outputs section (#2069).
236
+ */
237
+ private parseOutputs(outputs: Record<string, CFOutput>): OutputIR[] {
238
+ return Object.entries(outputs)
239
+ .filter(([_, output]) => typeof output === "object" && output !== null)
240
+ .map(([name, output]) => ({
241
+ name,
242
+ value: this.parseValue(output.Value),
243
+ description: output.Description,
244
+ exportName: output.Export?.Name !== undefined ? this.parseValue(output.Export.Name) : undefined,
245
+ condition: typeof output.Condition === "string" ? output.Condition : undefined,
246
+ }));
247
+ }
248
+
171
249
  /**
172
250
  * Parse parameters section
173
251
  */
@@ -192,6 +270,7 @@ export class CFParser extends BaseValueParser implements TemplateParser {
192
270
  type: resource.Type,
193
271
  properties: this.parseProperties(resource.Properties ?? {}),
194
272
  metadata: resource.Metadata,
273
+ condition: typeof resource.Condition === "string" ? resource.Condition : undefined,
195
274
  }));
196
275
  }
197
276
 
@@ -212,6 +291,12 @@ export class CFParser extends BaseValueParser implements TemplateParser {
212
291
  * CFN-specific intrinsic dispatch table.
213
292
  */
214
293
  protected dispatchIntrinsic(key: string, value: unknown, _obj: Record<string, unknown>): unknown | null {
294
+ // `{ "Condition": "<name>" }` — only valid inside the Conditions section
295
+ // (#2069); see parseConditions for the scoping.
296
+ if (key === "Condition" && this.inConditionExpression && typeof value === "string") {
297
+ return { __intrinsic: "ConditionRef", name: value };
298
+ }
299
+
215
300
  if (key === "Ref") {
216
301
  return { __intrinsic: "Ref", name: value };
217
302
  }
@@ -112,6 +112,56 @@ describe("parameters.json build roundtrip", () => {
112
112
  });
113
113
  });
114
114
 
115
+ describe("with-conditions.json build roundtrip (#2069)", () => {
116
+ test("Conditions, Condition keys, and the conditioned output round-trip exactly", async () => {
117
+ const content = readFileSync(join(roundtripDir, "with-conditions.json"), "utf-8");
118
+ const source = JSON.parse(content);
119
+
120
+ const ir = parser.parse(content);
121
+ const files = generator.generate(ir);
122
+ const mainFile = files.find((f) => f.path === "main.ts")!;
123
+
124
+ // The generated source declares both conditions and keeps the references
125
+ expect(mainFile.content).toContain("new Condition(Equals(Ref(Cutover), \"true\"))");
126
+ expect(mainFile.content).toContain("new Condition(Not(DoCutover))");
127
+ expect(mainFile.content).toContain("{ Condition: DoCutover }");
128
+ expect(mainFile.content).toContain("stackOutput(Ref(Rule), { condition: DoCutover })");
129
+
130
+ const dir = mkdtempSync(join(import.meta.dirname, "../../.roundtrip-tmp-"));
131
+ try {
132
+ const srcDir = join(dir, "src");
133
+ mkdirSync(srcDir);
134
+ writeFileSync(join(srcDir, "main.ts"), mainFile.content);
135
+
136
+ const result = await build(srcDir, [awsSerializer]);
137
+ expect(result.errors).toHaveLength(0);
138
+
139
+ const template = JSON.parse(result.outputs.get("aws") as string);
140
+
141
+ // The Conditions section round-trips exactly, {Condition: ...} ref included
142
+ expect(template.Conditions).toEqual(source.Conditions);
143
+ // The resource keeps its Condition key and its Fn::If
144
+ expect(template.Resources.Rule.Condition).toBe("DoCutover");
145
+ expect(template.Resources.Rule.Properties.LogGroupName).toEqual(
146
+ source.Resources.Rule.Properties.LogGroupName,
147
+ );
148
+ // The conditioned output round-trips exactly
149
+ expect(template.Outputs).toEqual(source.Outputs);
150
+ } finally {
151
+ rmSync(dir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ test("a section import cannot carry is named in a warning, never dropped silently", () => {
156
+ const template = JSON.parse(readFileSync(join(roundtripDir, "with-conditions.json"), "utf-8"));
157
+ template.Mappings = { RegionMap: { "us-east-1": { AMI: "ami-123" } } };
158
+ const ir = parser.parse(JSON.stringify(template));
159
+ expect(ir.warnings).toEqual([
160
+ 'Template section "Mappings" is not carried by import — it is dropped from the generated source',
161
+ ]);
162
+ });
163
+ });
164
+
115
165
  describe("SAM roundtrip fixtures", () => {
116
166
  const fixtures = readdirSync(samDir).filter(
117
167
  (f) => f.endsWith(".yaml") || f.endsWith(".yml"),
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  // Parameter
2
2
  export { Parameter } from "./parameter";
3
3
 
4
+ // Condition (#2068) — the Conditions-section declarable
5
+ export { Condition, isCondition, CONDITION_ENTITY_TYPE } from "./condition";
6
+
4
7
  // Default Tags
5
8
  export { defaultTags, isDefaultTags, DEFAULT_TAGS_MARKER } from "./default-tags";
6
9
  export type { DefaultTags, TagEntry } from "./default-tags";
@@ -105,6 +108,10 @@ export {
105
108
  Split,
106
109
  Base64,
107
110
  GetAZs,
111
+ Equals,
112
+ And,
113
+ Or,
114
+ Not,
108
115
  SubIntrinsic,
109
116
  RefIntrinsic,
110
117
  GetAttIntrinsic,
@@ -114,7 +121,12 @@ export {
114
121
  SplitIntrinsic,
115
122
  Base64Intrinsic,
116
123
  GetAZsIntrinsic,
124
+ EqualsIntrinsic,
125
+ AndIntrinsic,
126
+ OrIntrinsic,
127
+ NotIntrinsic,
117
128
  } from "./intrinsics";
129
+ export type { ConditionOperand } from "./intrinsics";
118
130
 
119
131
  // Pseudo-parameters
120
132
  export {