@intentius/chant-lexicon-aws 0.56.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.
- package/dist/condition.d.ts +27 -0
- package/dist/condition.d.ts.map +1 -0
- package/dist/generated/index.d.ts +1 -1
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/generator.d.ts +19 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +19 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -3
- package/dist/intrinsics.d.ts +77 -3
- package/dist/intrinsics.d.ts.map +1 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw069.d.ts +15 -0
- package/dist/lint/post-synth/waw069.d.ts.map +1 -0
- package/dist/manifest.json +29 -1
- package/dist/okf/index.md +1 -0
- package/dist/okf/rules/WAW069.md +11 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/waw069.ts +0 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/package.json +2 -2
- package/src/codegen/generate-typescript.ts +3 -3
- package/src/codegen/generate.ts +1 -1
- package/src/condition.test.ts +129 -0
- package/src/condition.ts +40 -0
- package/src/generated/index.d.ts +3 -3
- package/src/generated/index.ts +1 -1
- package/src/import/generator.ts +131 -50
- package/src/import/parser.test.ts +96 -0
- package/src/import/parser.ts +86 -1
- package/src/import/roundtrip-fixtures.test.ts +50 -0
- package/src/index.ts +12 -0
- package/src/intrinsics.ts +130 -5
- package/src/lint/audit-catalog.ts +1 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/waw069.test.ts +94 -0
- package/src/lint/post-synth/waw069.ts +0 -0
- package/src/plugin.test.ts +5 -1
- package/src/plugin.ts +4 -0
- package/src/serializer.ts +24 -3
- package/src/testdata/roundtrip/with-conditions.json +16 -0
package/src/generated/index.ts
CHANGED
|
@@ -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";
|
package/src/import/generator.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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(
|
|
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
|
+
});
|
package/src/import/parser.ts
CHANGED
|
@@ -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,
|
|
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 {
|