@intentius/chant-lexicon-aws 0.109.0 → 0.110.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 +12 -1
- package/dist/condition.d.ts.map +1 -1
- package/dist/import/generator.d.ts +69 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- package/dist/intrinsics.d.ts +58 -6
- package/dist/intrinsics.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/waw042.d.ts.map +1 -1
- package/dist/manifest.json +22 -1
- package/dist/mapping.d.ts +27 -0
- package/dist/mapping.d.ts.map +1 -0
- package/dist/parameter.d.ts +16 -1
- package/dist/parameter.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/cf-refs.ts +12 -2
- package/dist/rules/waw042.ts +25 -4
- package/dist/serializer.d.ts.map +1 -1
- package/dist/template-sections.d.ts +44 -0
- package/dist/template-sections.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/condition.test.ts +31 -0
- package/src/condition.ts +15 -1
- package/src/import/generator.test.ts +24 -3
- package/src/import/generator.ts +322 -49
- package/src/import/parser.test.ts +0 -1
- package/src/import/parser.ts +74 -21
- package/src/import/roundtrip-build.test.ts +322 -0
- package/src/import/roundtrip-fixtures.test.ts +3 -3
- package/src/index.ts +10 -0
- package/src/intrinsics.test.ts +7 -2
- package/src/intrinsics.ts +102 -13
- package/src/lint/post-synth/cf-refs.ts +12 -2
- package/src/lint/post-synth/waw019.test.ts +32 -0
- package/src/lint/post-synth/waw042.test.ts +28 -0
- package/src/lint/post-synth/waw042.ts +25 -4
- package/src/mapping.test.ts +64 -0
- package/src/mapping.ts +41 -0
- package/src/parameter.ts +22 -1
- package/src/plugin.test.ts +2 -1
- package/src/plugin.ts +3 -0
- package/src/serializer.ts +79 -12
- package/src/template-sections.ts +71 -0
package/src/condition.test.ts
CHANGED
|
@@ -127,3 +127,34 @@ describe("serializer Conditions section (#2068)", () => {
|
|
|
127
127
|
expect(template.Outputs.RuleNameByName.Condition).toBe("DoCutover");
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
describe("a name that is not the export name (#3604)", () => {
|
|
132
|
+
test("a Condition's name option is its key in Conditions and in every reference to it", () => {
|
|
133
|
+
const param = new Parameter("String");
|
|
134
|
+
const cond = new Condition(Equals(Ref(param), "true"), { name: "EnableReadReplica" });
|
|
135
|
+
const group = new LogGroup({ LogGroupName: If(cond, "a", "b") }, { Condition: cond });
|
|
136
|
+
const out = stackOutput(Ref(group), { condition: cond, lexicon: "aws" });
|
|
137
|
+
const template = serialize(
|
|
138
|
+
new Map<string, Declarable>([
|
|
139
|
+
["EnableReadReplica", param],
|
|
140
|
+
["EnableReadReplicaCondition", cond],
|
|
141
|
+
["Group", group],
|
|
142
|
+
["Out", out],
|
|
143
|
+
]),
|
|
144
|
+
);
|
|
145
|
+
expect(template.Parameters.EnableReadReplica).toEqual({ Type: "String" });
|
|
146
|
+
expect(template.Conditions).toEqual({ EnableReadReplica: { "Fn::Equals": [{ Ref: "EnableReadReplica" }, "true"] } });
|
|
147
|
+
expect(template.Resources.Group.Condition).toBe("EnableReadReplica");
|
|
148
|
+
expect(template.Resources.Group.Properties.LogGroupName).toEqual({ "Fn::If": ["EnableReadReplica", "a", "b"] });
|
|
149
|
+
expect(template.Outputs.Out.Condition).toBe("EnableReadReplica");
|
|
150
|
+
expect(JSON.parse(JSON.stringify(Not(cond)))).toEqual({ "Fn::Not": [{ Condition: "EnableReadReplica" }] });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a stackOutput's name option is its key in Outputs", () => {
|
|
154
|
+
const group = new LogGroup({});
|
|
155
|
+
const out = stackOutput(Ref(group), { name: "QueueName", lexicon: "aws" });
|
|
156
|
+
const template = serialize(new Map<string, Declarable>([["QueueName", group], ["QueueNameOutput", out]]));
|
|
157
|
+
expect(Object.keys(template.Resources)).toEqual(["QueueName"]);
|
|
158
|
+
expect(template.Outputs).toEqual({ QueueName: { Value: { Ref: "QueueName" } } });
|
|
159
|
+
});
|
|
160
|
+
});
|
package/src/condition.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { DECLARABLE_MARKER, isDeclarable, type Declarable } from "@intentius/chant/declarable";
|
|
12
12
|
import { isIntrinsic, type Intrinsic } from "@intentius/chant/intrinsic";
|
|
13
|
+
import { getLogicalName } from "@intentius/chant/utils";
|
|
13
14
|
|
|
14
15
|
export const CONDITION_ENTITY_TYPE = "AWS::CloudFormation::Condition";
|
|
15
16
|
|
|
@@ -19,17 +20,30 @@ export class Condition implements Declarable {
|
|
|
19
20
|
readonly entityType = CONDITION_ENTITY_TYPE;
|
|
20
21
|
/** The boolean expression: an `Equals`/`And`/`Or`/`Not` intrinsic. */
|
|
21
22
|
readonly expression: Intrinsic;
|
|
23
|
+
/** The condition's key in the template's `Conditions`, when it is not the
|
|
24
|
+
* export name. A template may use one name for a condition and a
|
|
25
|
+
* parameter; a module cannot export both under that name (#3604). */
|
|
26
|
+
readonly name?: string;
|
|
22
27
|
|
|
23
|
-
constructor(expression: Intrinsic) {
|
|
28
|
+
constructor(expression: Intrinsic, options?: { name?: string }) {
|
|
24
29
|
if (!isIntrinsic(expression)) {
|
|
25
30
|
throw new Error(
|
|
26
31
|
"new Condition(expression): expression must be a condition intrinsic (Equals, And, Or, Not)",
|
|
27
32
|
);
|
|
28
33
|
}
|
|
29
34
|
this.expression = expression;
|
|
35
|
+
if (options?.name !== undefined) this.name = options.name;
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The name a condition has in the template: its `name` option, else the
|
|
41
|
+
* logical name discovery gave it (the export name).
|
|
42
|
+
*/
|
|
43
|
+
export function conditionName(condition: Condition, entityNames?: Map<unknown, string>): string {
|
|
44
|
+
return condition.name ?? entityNames?.get(condition) ?? getLogicalName(condition);
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/**
|
|
34
48
|
* Type guard for the `Condition` declarable. Duck-typed on `entityType`
|
|
35
49
|
* rather than `instanceof` so a lexicon built against a separate copy of
|
|
@@ -265,7 +265,7 @@ describe("CFGenerator", () => {
|
|
|
265
265
|
expect(files[0].content).toContain("\\`field-name\\`");
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
-
test("generates
|
|
268
|
+
test("generates a dotted GetAtt attribute as its underscored property", () => {
|
|
269
269
|
const ir: TemplateIR = {
|
|
270
270
|
parameters: [],
|
|
271
271
|
resources: [
|
|
@@ -286,7 +286,28 @@ describe("CFGenerator", () => {
|
|
|
286
286
|
|
|
287
287
|
const files = generator.generate(ir);
|
|
288
288
|
|
|
289
|
-
expect(files[0].content).toContain(
|
|
290
|
-
expect(files[0].content).toContain("
|
|
289
|
+
expect(files[0].content).toContain("BucketName: ELB.SourceSecurityGroup_OwnerAlias");
|
|
290
|
+
expect(files[0].content).not.toContain("GetAtt");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("generates GetAtt by name for an attribute the class does not expose", () => {
|
|
294
|
+
const ir: TemplateIR = {
|
|
295
|
+
parameters: [],
|
|
296
|
+
resources: [
|
|
297
|
+
{ logicalId: "MyQueue", type: "AWS::SQS::Queue", properties: {} },
|
|
298
|
+
{
|
|
299
|
+
logicalId: "MyBucket",
|
|
300
|
+
type: "AWS::S3::Bucket",
|
|
301
|
+
properties: {
|
|
302
|
+
BucketName: { __intrinsic: "GetAtt", logicalId: "MyQueue", attribute: "QueueName" },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const files = generator.generate(ir);
|
|
309
|
+
|
|
310
|
+
expect(files[0].content).toContain('BucketName: GetAtt("MyQueue", "QueueName")');
|
|
311
|
+
expect(files[0].content).toMatch(/import \{[^}]*\bGetAtt\b/);
|
|
291
312
|
});
|
|
292
313
|
});
|
package/src/import/generator.ts
CHANGED
|
@@ -11,8 +11,20 @@ import { join } from "path";
|
|
|
11
11
|
* TypeScript code generator for CloudFormation templates
|
|
12
12
|
*/
|
|
13
13
|
export class CFGenerator implements TypeScriptGenerator {
|
|
14
|
+
/** One `main.ts` holds the whole template. Split files would each need the
|
|
15
|
+
* parameters, conditions and resources the others declare. */
|
|
16
|
+
readonly ownsLayout = true;
|
|
14
17
|
private typeToClass: Map<string, string>;
|
|
15
18
|
private allClassNames: Set<string>;
|
|
19
|
+
/** resourceType -> (property name on the class -> CloudFormation attribute name) */
|
|
20
|
+
private attrsByType: Map<string, Record<string, string>>;
|
|
21
|
+
/** Resource classes imported under another name, because a logical id in
|
|
22
|
+
* the template is the class's own name (`InternetGateway`). */
|
|
23
|
+
private classAliases: Map<string, string> = new Map();
|
|
24
|
+
/** Variable names for mappings ("m:<name>"), conditions ("c:<name>") and
|
|
25
|
+
* outputs ("o:<name>") whose template name is already a parameter's or
|
|
26
|
+
* resource's (#3604). */
|
|
27
|
+
private renames: Map<string, string> = new Map();
|
|
16
28
|
|
|
17
29
|
constructor() {
|
|
18
30
|
// Reverse lookup from the generated registry: resourceType → className.
|
|
@@ -21,10 +33,13 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
21
33
|
const meta = loadLexiconRegistry(join(import.meta.dirname, "../.."), "aws");
|
|
22
34
|
this.typeToClass = new Map();
|
|
23
35
|
this.allClassNames = new Set();
|
|
36
|
+
this.attrsByType = new Map();
|
|
24
37
|
for (const [className, entry] of Object.entries(meta)) {
|
|
25
38
|
if (entry.kind === "resource" && !className.includes("_")) {
|
|
26
39
|
this.typeToClass.set(entry.resourceType, className);
|
|
27
40
|
this.allClassNames.add(className);
|
|
41
|
+
// The aws registry also carries each class's attribute map.
|
|
42
|
+
this.attrsByType.set(entry.resourceType, (entry as { attrs?: Record<string, string> }).attrs ?? {});
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
45
|
}
|
|
@@ -35,13 +50,21 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
35
50
|
generate(ir: TemplateIR): GeneratedFile[] {
|
|
36
51
|
const lines: string[] = [];
|
|
37
52
|
|
|
53
|
+
this.classAliases = this.computeClassAliases(ir);
|
|
38
54
|
// Collect the set of imported class names so we can detect variable name conflicts
|
|
39
55
|
const importedSymbols = this.collectImportedSymbols(ir);
|
|
56
|
+
this.renames = this.collisionRenames(ir, importedSymbols);
|
|
40
57
|
|
|
41
58
|
// Generate imports
|
|
42
59
|
lines.push(this.generateImports(ir));
|
|
43
60
|
lines.push("");
|
|
44
61
|
|
|
62
|
+
// Template-level Description and Metadata
|
|
63
|
+
const sections = this.generateTemplateSections(ir, importedSymbols);
|
|
64
|
+
if (sections.length > 0) {
|
|
65
|
+
lines.push(...sections, "");
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
// Generate parameters
|
|
46
69
|
for (const param of ir.parameters) {
|
|
47
70
|
lines.push(this.generateParameter(param, importedSymbols));
|
|
@@ -51,6 +74,15 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
51
74
|
lines.push("");
|
|
52
75
|
}
|
|
53
76
|
|
|
77
|
+
// Generate mappings
|
|
78
|
+
const mappings = this.mappings(ir);
|
|
79
|
+
for (const [name, map] of Object.entries(mappings)) {
|
|
80
|
+
lines.push(this.generateMapping(name, map, importedSymbols));
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(mappings).length > 0) {
|
|
83
|
+
lines.push("");
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
// Generate conditions in dependency order ({ Condition: ... } references
|
|
55
87
|
// point at earlier declarations) — #2069
|
|
56
88
|
const conditions = ir.conditions ?? [];
|
|
@@ -92,19 +124,27 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
92
124
|
private collectImportedSymbols(ir: TemplateIR): Set<string> {
|
|
93
125
|
const symbols = new Set<string>();
|
|
94
126
|
if (ir.parameters.length > 0) symbols.add("Parameter");
|
|
127
|
+
if (Object.keys(this.mappings(ir)).length > 0) symbols.add("Mapping");
|
|
95
128
|
if ((ir.conditions ?? []).length > 0) symbols.add("Condition");
|
|
96
129
|
if ((ir.outputs ?? []).length > 0) symbols.add("stackOutput");
|
|
97
|
-
const intrinsics = ["Sub", "Ref", "If", "Join", "Select", "Split", "Base64", "GetAZs", "
|
|
130
|
+
const intrinsics = ["Sub", "Ref", "If", "Join", "Select", "Split", "Base64", "GetAZs", "Equals", "And", "Or", "Not", "FindInMap", "ImportValue", "Cidr"] as const;
|
|
98
131
|
for (const name of intrinsics) {
|
|
99
132
|
if (irUsesIntrinsic(ir, name)) symbols.add(name);
|
|
100
133
|
}
|
|
101
134
|
// Outputs whose value is a Ref envelope render as Ref(<var>) (#2069)
|
|
102
135
|
if ((ir.outputs ?? []).some((o) => hasIntrinsicInValue(o.value, "Ref"))) symbols.add("Ref");
|
|
136
|
+
// A Sub that names a parameter or resource renders it as Ref(<var>), and an
|
|
137
|
+
// attribute the class does not expose renders as GetAtt("<id>", "<attr>").
|
|
138
|
+
const uses = this.scanGeneratedIntrinsics(ir);
|
|
139
|
+
if (uses.ref) symbols.add("Ref");
|
|
140
|
+
if (uses.getAtt) symbols.add("GetAtt");
|
|
103
141
|
if (this.needsAWSPseudo(ir)) symbols.add("AWS");
|
|
104
142
|
for (const resource of ir.resources) {
|
|
105
143
|
const parsed = this.parseResourceType(resource.type);
|
|
106
|
-
if (parsed) symbols.add(parsed.resourceClass);
|
|
144
|
+
if (parsed) symbols.add(this.classAliases.get(parsed.resourceClass) ?? parsed.resourceClass);
|
|
107
145
|
}
|
|
146
|
+
if (typeof ir.metadata?.description === "string") symbols.add("templateDescription");
|
|
147
|
+
if (ir.metadata?.templateMetadata !== undefined) symbols.add("templateMetadata");
|
|
108
148
|
return symbols;
|
|
109
149
|
}
|
|
110
150
|
|
|
@@ -115,6 +155,169 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
115
155
|
return importedSymbols.has(name) ? name + "_" : name;
|
|
116
156
|
}
|
|
117
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The property on the generated class that reads `attribute` of the
|
|
160
|
+
* resource `logicalId`, or undefined when the class has none. A dotted
|
|
161
|
+
* CloudFormation attribute (`Endpoint.Address`) maps to its underscored
|
|
162
|
+
* property (`Endpoint_Address`).
|
|
163
|
+
*/
|
|
164
|
+
private attrProperty(logicalId: string, attribute: string, ir: TemplateIR): string | undefined {
|
|
165
|
+
const resource = ir.resources.find((r) => r.logicalId === logicalId);
|
|
166
|
+
if (!resource || !this.parseResourceType(resource.type)) return undefined;
|
|
167
|
+
const attrs = this.attrsByType.get(resource.type) ?? {};
|
|
168
|
+
const key = Object.keys(attrs).find((k) => attrs[k] === attribute);
|
|
169
|
+
return key !== undefined && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* An attribute read: `<var>.<property>` when the class exposes the
|
|
174
|
+
* attribute, else `GetAtt("<id>", "<attr>")`. CloudFormation accepts
|
|
175
|
+
* GetAtt on some writable properties (`AWS::SQS::Queue` `QueueName`) that
|
|
176
|
+
* the class does not list, and reading those as a property gives
|
|
177
|
+
* undefined.
|
|
178
|
+
*/
|
|
179
|
+
private attrExpr(logicalId: string, attribute: string, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
180
|
+
const prop = this.attrProperty(logicalId, attribute, ir);
|
|
181
|
+
if (prop !== undefined) return `${this.safeVarName(logicalId, importedSymbols)}.${prop}`;
|
|
182
|
+
return `GetAtt(${JSON.stringify(logicalId)}, ${JSON.stringify(attribute)})`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Whether `name` is a parameter or resource the template declares. */
|
|
186
|
+
private isDeclaredName(name: string, ir: TemplateIR): boolean {
|
|
187
|
+
return ir.parameters.some((p) => p.name === name) || ir.resources.some((r) => r.logicalId === name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Which of Ref and GetAtt the generated source calls beyond the IR's own
|
|
192
|
+
* Ref and GetAtt envelopes: Sub interpolations of a declared name, and
|
|
193
|
+
* attribute reads the class does not expose.
|
|
194
|
+
*/
|
|
195
|
+
private scanGeneratedIntrinsics(ir: TemplateIR): { ref: boolean; getAtt: boolean } {
|
|
196
|
+
const uses = { ref: false, getAtt: false };
|
|
197
|
+
const visit = (value: unknown): void => {
|
|
198
|
+
if (value === null || typeof value !== "object") return;
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
for (const item of value) visit(item);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const obj = value as Record<string, unknown>;
|
|
204
|
+
if (obj.__intrinsic === "GetAtt") {
|
|
205
|
+
if (this.attrProperty(obj.logicalId as string, obj.attribute as string, ir) === undefined) uses.getAtt = true;
|
|
206
|
+
} else if (obj.__intrinsic === "Sub") {
|
|
207
|
+
const variables = (obj.variables ?? {}) as Record<string, unknown>;
|
|
208
|
+
for (const m of (obj.template as string).matchAll(/\$\{([^}]+)\}/g)) {
|
|
209
|
+
const expr = m[1];
|
|
210
|
+
if (expr.startsWith("AWS::") || expr.startsWith("!") || expr in variables) continue;
|
|
211
|
+
const dot = expr.indexOf(".");
|
|
212
|
+
if (dot < 0) uses.ref = true;
|
|
213
|
+
else if (this.attrProperty(expr.slice(0, dot), expr.slice(dot + 1), ir) === undefined) uses.getAtt = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const v of Object.values(obj)) visit(v);
|
|
217
|
+
};
|
|
218
|
+
for (const v of this.generatedValues(ir)) visit(v);
|
|
219
|
+
return uses;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Every name the template declares: parameters, resources, conditions, outputs. */
|
|
223
|
+
private declaredNames(ir: TemplateIR): Set<string> {
|
|
224
|
+
return new Set([
|
|
225
|
+
...ir.parameters.map((p) => p.name),
|
|
226
|
+
...ir.resources.map((r) => r.logicalId),
|
|
227
|
+
...(ir.conditions ?? []).map((c) => c.name),
|
|
228
|
+
...(ir.outputs ?? []).map((o) => o.name),
|
|
229
|
+
...Object.keys(this.mappings(ir)),
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The template's Mappings section, as the parser carried it. */
|
|
234
|
+
private mappings(ir: TemplateIR): Record<string, Record<string, Record<string, unknown>>> {
|
|
235
|
+
return (ir.metadata?.mappings ?? {}) as Record<string, Record<string, Record<string, unknown>>>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The variable a declared mapping is exported as. */
|
|
239
|
+
private mappingVar(name: string, importedSymbols: Set<string>): string {
|
|
240
|
+
return this.renames.get(`m:${name}`) ?? this.safeVarName(name, importedSymbols);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A mapping declaration. */
|
|
244
|
+
private generateMapping(name: string, map: Record<string, Record<string, unknown>>, importedSymbols: Set<string>): string {
|
|
245
|
+
const varName = this.mappingVar(name, importedSymbols);
|
|
246
|
+
const nameOpt = this.renames.has(`m:${name}`) ? `, { name: ${JSON.stringify(name)} }` : "";
|
|
247
|
+
return `export const ${varName} = new Mapping(${JSON.stringify(map, null, 2)}${nameOpt});`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* A resource class whose name the template also uses for something it
|
|
252
|
+
* declares is imported under an alias (`InternetGateway as
|
|
253
|
+
* InternetGatewayResource`), so the declaration keeps its name. Suffixing
|
|
254
|
+
* the declaration instead would change its logical id.
|
|
255
|
+
*/
|
|
256
|
+
private computeClassAliases(ir: TemplateIR): Map<string, string> {
|
|
257
|
+
const declared = this.declaredNames(ir);
|
|
258
|
+
const aliases = new Map<string, string>();
|
|
259
|
+
for (const resource of ir.resources) {
|
|
260
|
+
const cls = this.parseResourceType(resource.type)?.resourceClass;
|
|
261
|
+
if (!cls || !declared.has(cls) || aliases.has(cls)) continue;
|
|
262
|
+
let alias = `${cls}Resource`;
|
|
263
|
+
for (let i = 2; declared.has(alias) || this.allClassNames.has(alias); i++) alias = `${cls}Resource${i}`;
|
|
264
|
+
aliases.set(cls, alias);
|
|
265
|
+
}
|
|
266
|
+
return aliases;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** `templateDescription(...)` and `templateMetadata(...)` for the template's own sections. */
|
|
270
|
+
private generateTemplateSections(ir: TemplateIR, importedSymbols: Set<string>): string[] {
|
|
271
|
+
const declared = this.declaredNames(ir);
|
|
272
|
+
const freeName = (base: string) => {
|
|
273
|
+
let v = base;
|
|
274
|
+
for (let i = 2; declared.has(v) || importedSymbols.has(v); i++) v = `${base}${i}`;
|
|
275
|
+
declared.add(v);
|
|
276
|
+
return v;
|
|
277
|
+
};
|
|
278
|
+
const lines: string[] = [];
|
|
279
|
+
if (typeof ir.metadata?.description === "string") {
|
|
280
|
+
lines.push(`export const ${freeName("description")} = templateDescription(${JSON.stringify(ir.metadata.description)});`);
|
|
281
|
+
}
|
|
282
|
+
if (ir.metadata?.templateMetadata !== undefined) {
|
|
283
|
+
const value = this.generateValue(ir.metadata.templateMetadata, ir, importedSymbols);
|
|
284
|
+
lines.push(`export const ${freeName("metadata")} = templateMetadata(${value});`);
|
|
285
|
+
}
|
|
286
|
+
return lines;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A condition or output whose template name is a parameter's or resource's
|
|
291
|
+
* cannot be exported under that name too. It gets a suffixed variable
|
|
292
|
+
* (`EnableReadReplicaCondition`, `QueueNameOutput`), and the generated code
|
|
293
|
+
* passes the template name as its `name` option.
|
|
294
|
+
*/
|
|
295
|
+
private collisionRenames(ir: TemplateIR, importedSymbols: Set<string>): Map<string, string> {
|
|
296
|
+
const renames = new Map<string, string>();
|
|
297
|
+
const taken = new Set<string>();
|
|
298
|
+
const varOf = (name: string) => this.safeVarName(name, importedSymbols);
|
|
299
|
+
for (const p of ir.parameters) taken.add(varOf(p.name));
|
|
300
|
+
for (const r of ir.resources) taken.add(varOf(r.logicalId));
|
|
301
|
+
const claim = (key: string, name: string, suffix: string) => {
|
|
302
|
+
let v = varOf(name);
|
|
303
|
+
if (taken.has(v)) {
|
|
304
|
+
v = `${name}${suffix}`;
|
|
305
|
+
for (let i = 2; taken.has(v); i++) v = `${name}${suffix}${i}`;
|
|
306
|
+
renames.set(key, v);
|
|
307
|
+
}
|
|
308
|
+
taken.add(v);
|
|
309
|
+
};
|
|
310
|
+
for (const m of Object.keys(this.mappings(ir))) claim(`m:${m}`, m, "Mapping");
|
|
311
|
+
for (const c of ir.conditions ?? []) claim(`c:${c.name}`, c.name, "Condition");
|
|
312
|
+
for (const o of ir.outputs ?? []) claim(`o:${o.name}`, o.name, "Output");
|
|
313
|
+
return renames;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The variable a declared condition is exported as. */
|
|
317
|
+
private conditionVar(name: string, importedSymbols: Set<string>): string {
|
|
318
|
+
return this.renames.get(`c:${name}`) ?? this.safeVarName(name, importedSymbols);
|
|
319
|
+
}
|
|
320
|
+
|
|
118
321
|
/**
|
|
119
322
|
* Generate import statements
|
|
120
323
|
*/
|
|
@@ -122,7 +325,8 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
122
325
|
// Everything comes from the flat @intentius/chant-lexicon-aws package,
|
|
123
326
|
// and the needed symbol set is exactly what collectImportedSymbols
|
|
124
327
|
// computes for variable-name conflict detection.
|
|
125
|
-
const
|
|
328
|
+
const aliasOf = new Map([...this.classAliases].map(([cls, alias]) => [alias, cls]));
|
|
329
|
+
const allImports = [...this.collectImportedSymbols(ir)].map((s) => (aliasOf.has(s) ? `${aliasOf.get(s)} as ${s}` : s));
|
|
126
330
|
if (allImports.length === 0) {
|
|
127
331
|
return "";
|
|
128
332
|
}
|
|
@@ -147,12 +351,17 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
147
351
|
* Check if AWS pseudo-parameters are used
|
|
148
352
|
*/
|
|
149
353
|
private needsAWSPseudo(ir: TemplateIR): boolean {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return
|
|
354
|
+
return this.generatedValues(ir).some((v) => this.hasAWSPseudo(v));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Every IR value the generator writes as source. */
|
|
358
|
+
private generatedValues(ir: TemplateIR): unknown[] {
|
|
359
|
+
return [
|
|
360
|
+
...ir.resources.flatMap((r) => [r.properties, r.metadata, r.attributes]),
|
|
361
|
+
...(ir.conditions ?? []).map((c) => c.expression),
|
|
362
|
+
...(ir.outputs ?? []).flatMap((o) => [o.value, o.exportName]),
|
|
363
|
+
ir.metadata?.templateMetadata,
|
|
364
|
+
];
|
|
156
365
|
}
|
|
157
366
|
|
|
158
367
|
/**
|
|
@@ -193,7 +402,12 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
193
402
|
(r) => r.logicalId,
|
|
194
403
|
(r) => {
|
|
195
404
|
const extraDeps = new Set<string>();
|
|
196
|
-
|
|
405
|
+
// DependsOn names a resource that is generated as a variable reference.
|
|
406
|
+
const dependsOn = r.attributes?.DependsOn;
|
|
407
|
+
for (const d of Array.isArray(dependsOn) ? dependsOn : dependsOn !== undefined ? [dependsOn] : []) {
|
|
408
|
+
if (typeof d === "string" && resourceIds.has(d)) extraDeps.add(d);
|
|
409
|
+
}
|
|
410
|
+
const deps = collectDependencies({ properties: r.properties, metadata: r.metadata, attributes: r.attributes }, (obj) => {
|
|
197
411
|
if (obj.__intrinsic === "Ref") {
|
|
198
412
|
const name = obj.name as string;
|
|
199
413
|
return name.startsWith("AWS::") ? null : name;
|
|
@@ -245,15 +459,18 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
245
459
|
*/
|
|
246
460
|
private conditionVarRef(name: string, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
247
461
|
const declared = (ir.conditions ?? []).some((c) => c.name === name);
|
|
248
|
-
return declared ? this.
|
|
462
|
+
return declared ? this.conditionVar(name, importedSymbols) : JSON.stringify(name);
|
|
249
463
|
}
|
|
250
464
|
|
|
251
465
|
/**
|
|
252
466
|
* Generate a condition declaration (#2069)
|
|
253
467
|
*/
|
|
254
468
|
private generateCondition(condition: ConditionIR, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
255
|
-
const varName = this.
|
|
469
|
+
const varName = this.conditionVar(condition.name, importedSymbols);
|
|
256
470
|
const exprStr = this.generateValue(condition.expression, ir, importedSymbols);
|
|
471
|
+
if (this.renames.has(`c:${condition.name}`)) {
|
|
472
|
+
return `export const ${varName} = new Condition(${exprStr}, { name: ${JSON.stringify(condition.name)} });`;
|
|
473
|
+
}
|
|
257
474
|
return `export const ${varName} = new Condition(${exprStr});`;
|
|
258
475
|
}
|
|
259
476
|
|
|
@@ -261,7 +478,7 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
261
478
|
* Generate an output declaration as stackOutput(...) (#2069)
|
|
262
479
|
*/
|
|
263
480
|
private generateOutput(output: OutputIR, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
264
|
-
const varName = this.safeVarName(output.name, importedSymbols);
|
|
481
|
+
const varName = this.renames.get(`o:${output.name}`) ?? this.safeVarName(output.name, importedSymbols);
|
|
265
482
|
|
|
266
483
|
// A bare Ref envelope becomes Ref(<var>) — stackOutput takes an intrinsic
|
|
267
484
|
// or attribute reference, not the resource/parameter object itself.
|
|
@@ -281,15 +498,13 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
281
498
|
if (output.condition) {
|
|
282
499
|
opts.push(`condition: ${this.conditionVarRef(output.condition, ir, importedSymbols)}`);
|
|
283
500
|
}
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
501
|
+
// Every output names its lexicon. A literal, or a GetAtt written by name,
|
|
502
|
+
// has no entity to derive it from, and an output without one is dropped
|
|
503
|
+
// at serialization.
|
|
504
|
+
opts.push(`lexicon: "aws"`);
|
|
505
|
+
if (this.renames.has(`o:${output.name}`)) opts.push(`name: ${JSON.stringify(output.name)}`);
|
|
288
506
|
|
|
289
|
-
|
|
290
|
-
return `export const ${varName} = stackOutput(${valueStr}, { ${opts.join(", ")} });`;
|
|
291
|
-
}
|
|
292
|
-
return `export const ${varName} = stackOutput(${valueStr});`;
|
|
507
|
+
return `export const ${varName} = stackOutput(${valueStr}, { ${opts.join(", ")} });`;
|
|
293
508
|
}
|
|
294
509
|
|
|
295
510
|
/**
|
|
@@ -300,6 +515,9 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
300
515
|
const opts: string[] = [];
|
|
301
516
|
if (param.description) opts.push(`description: ${JSON.stringify(param.description)}`);
|
|
302
517
|
if (param.defaultValue !== undefined) opts.push(`defaultValue: ${JSON.stringify(param.defaultValue)}`);
|
|
518
|
+
for (const [key, value] of Object.entries(param.constraints ?? {})) {
|
|
519
|
+
opts.push(`${key.charAt(0).toLowerCase()}${key.slice(1)}: ${JSON.stringify(value)}`);
|
|
520
|
+
}
|
|
303
521
|
if (opts.length > 0) {
|
|
304
522
|
return `export const ${varName} = new Parameter("${param.type}", { ${opts.join(", ")} });`;
|
|
305
523
|
}
|
|
@@ -316,13 +534,26 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
316
534
|
return `// Unsupported type: ${resource.type}\nexport const ${varName} = "${resource.logicalId}";`;
|
|
317
535
|
}
|
|
318
536
|
const varName = this.safeVarName(resource.logicalId, importedSymbols);
|
|
319
|
-
const
|
|
537
|
+
const resourceClass = this.classAliases.get(parsed.resourceClass) ?? parsed.resourceClass;
|
|
320
538
|
const propsStr = this.generateProps(resource.properties, ir, importedSymbols);
|
|
321
539
|
|
|
322
|
-
// Resource
|
|
540
|
+
// Resource attributes -> the attributes argument: Condition (#2069),
|
|
541
|
+
// DependsOn, the policies, and Metadata.
|
|
542
|
+
const attrs: string[] = [];
|
|
323
543
|
if (resource.condition) {
|
|
324
|
-
|
|
325
|
-
|
|
544
|
+
attrs.push(`Condition: ${this.conditionVarRef(resource.condition, ir, importedSymbols)}`);
|
|
545
|
+
}
|
|
546
|
+
for (const [key, value] of Object.entries(resource.attributes ?? {})) {
|
|
547
|
+
const valueStr = key === "DependsOn"
|
|
548
|
+
? this.generateDependsOn(value, ir, importedSymbols)
|
|
549
|
+
: this.generateValue(value, ir, importedSymbols);
|
|
550
|
+
attrs.push(`${key}: ${valueStr}`);
|
|
551
|
+
}
|
|
552
|
+
if (resource.metadata !== undefined) {
|
|
553
|
+
attrs.push(`Metadata: ${this.generateValue(resource.metadata, ir, importedSymbols)}`);
|
|
554
|
+
}
|
|
555
|
+
if (attrs.length > 0) {
|
|
556
|
+
return `export const ${varName} = new ${resourceClass}(${propsStr}, { ${attrs.join(", ")} });`;
|
|
326
557
|
}
|
|
327
558
|
|
|
328
559
|
if (propsStr === "{}") {
|
|
@@ -332,6 +563,19 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
332
563
|
return `export const ${varName} = new ${resourceClass}(${propsStr});`;
|
|
333
564
|
}
|
|
334
565
|
|
|
566
|
+
/**
|
|
567
|
+
* DependsOn as the variables of the resources it names, so the generated
|
|
568
|
+
* source references them. A name the template does not declare as a
|
|
569
|
+
* supported resource stays a string.
|
|
570
|
+
*/
|
|
571
|
+
private generateDependsOn(value: unknown, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
572
|
+
const one = (d: unknown) =>
|
|
573
|
+
typeof d === "string" && ir.resources.some((r) => r.logicalId === d && this.parseResourceType(r.type))
|
|
574
|
+
? this.safeVarName(d, importedSymbols)
|
|
575
|
+
: this.generateValue(d, ir, importedSymbols);
|
|
576
|
+
return Array.isArray(value) ? `[${value.map(one).join(", ")}]` : one(value);
|
|
577
|
+
}
|
|
578
|
+
|
|
335
579
|
/**
|
|
336
580
|
* Generate property object as TypeScript
|
|
337
581
|
*/
|
|
@@ -389,14 +633,7 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
389
633
|
}
|
|
390
634
|
|
|
391
635
|
if (obj.__intrinsic === "GetAtt") {
|
|
392
|
-
|
|
393
|
-
const attribute = obj.attribute as string;
|
|
394
|
-
const varName = this.safeVarName(logicalId, importedSymbols);
|
|
395
|
-
if (attribute.includes(".")) {
|
|
396
|
-
return `GetAtt(${varName}, "${attribute}")`;
|
|
397
|
-
}
|
|
398
|
-
const attrName = this.toPropName(attribute);
|
|
399
|
-
return `${varName}.${attrName}`;
|
|
636
|
+
return this.attrExpr(obj.logicalId as string, obj.attribute as string, ir, importedSymbols);
|
|
400
637
|
}
|
|
401
638
|
|
|
402
639
|
if (obj.__intrinsic === "Sub") {
|
|
@@ -437,9 +674,12 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
437
674
|
}
|
|
438
675
|
|
|
439
676
|
if (obj.__intrinsic === "Select") {
|
|
440
|
-
const index = obj.index
|
|
441
|
-
|
|
442
|
-
|
|
677
|
+
const index = JSON.stringify(obj.index);
|
|
678
|
+
if (Array.isArray(obj.values)) {
|
|
679
|
+
const values = obj.values.map((v) => this.generateValue(v, ir, importedSymbols));
|
|
680
|
+
return `Select(${index}, [${values.join(", ")}])`;
|
|
681
|
+
}
|
|
682
|
+
return `Select(${index}, ${this.generateValue(obj.source, ir, importedSymbols)})`;
|
|
443
683
|
}
|
|
444
684
|
|
|
445
685
|
if (obj.__intrinsic === "Split") {
|
|
@@ -461,6 +701,34 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
461
701
|
return `GetAZs(${this.generateValue(region, ir, importedSymbols)})`;
|
|
462
702
|
}
|
|
463
703
|
|
|
704
|
+
if (obj.__intrinsic === "FindInMap") {
|
|
705
|
+
const mapName = obj.mapName;
|
|
706
|
+
const map = typeof mapName === "string" && mapName in this.mappings(ir)
|
|
707
|
+
? this.mappingVar(mapName, importedSymbols)
|
|
708
|
+
: this.generateValue(mapName, ir, importedSymbols);
|
|
709
|
+
const first = this.generateValue(obj.firstKey, ir, importedSymbols);
|
|
710
|
+
const second = this.generateValue(obj.secondKey, ir, importedSymbols);
|
|
711
|
+
return `FindInMap(${map}, ${first}, ${second})`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (obj.__intrinsic === "ImportValue") {
|
|
715
|
+
return `ImportValue(${this.generateValue(obj.value, ir, importedSymbols)})`;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (obj.__intrinsic === "Cidr") {
|
|
719
|
+
const args = [obj.ipBlock, obj.count, obj.cidrBits].map((v) => this.generateValue(v, ir, importedSymbols));
|
|
720
|
+
return `Cidr(${args.join(", ")})`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// An intrinsic the parser read but the generator has no form for
|
|
724
|
+
// (Fn::Transform) is refused. Written as an object literal, it would
|
|
725
|
+
// build into a template CloudFormation cannot read.
|
|
726
|
+
if (typeof obj.__intrinsic === "string") {
|
|
727
|
+
throw new Error(
|
|
728
|
+
`chant import cannot generate Fn::${obj.__intrinsic} yet; the template uses it, so it is not imported`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
464
732
|
// Regular object — quote keys that aren't valid JS identifiers
|
|
465
733
|
const entries = Object.entries(obj).map(([key, val]) => {
|
|
466
734
|
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
@@ -486,33 +754,38 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
486
754
|
const regex = /\$\{([^}]+)\}/g;
|
|
487
755
|
let match;
|
|
488
756
|
|
|
757
|
+
let literal = "";
|
|
489
758
|
while ((match = regex.exec(template)) !== null) {
|
|
490
|
-
parts.push(template.slice(currentPos, match.index));
|
|
491
|
-
|
|
492
759
|
const expr = match[1];
|
|
760
|
+
// `${!Literal}` is CloudFormation's escape for a literal `${Literal}`;
|
|
761
|
+
// it stays in the template text.
|
|
762
|
+
if (expr.startsWith("!")) {
|
|
763
|
+
literal += template.slice(currentPos, match.index + match[0].length);
|
|
764
|
+
currentPos = match.index + match[0].length;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
parts.push(literal + template.slice(currentPos, match.index));
|
|
768
|
+
literal = "";
|
|
769
|
+
|
|
493
770
|
if (expr.startsWith("AWS::")) {
|
|
494
771
|
expressions.push(`AWS.${this.pseudoParamName(expr)}`);
|
|
495
772
|
} else if (variables && expr in variables) {
|
|
496
773
|
expressions.push(this.generateValue(variables[expr], ir, importedSymbols));
|
|
497
774
|
} else if (expr.includes(".")) {
|
|
498
775
|
const dotIdx = expr.indexOf(".");
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
} else {
|
|
505
|
-
const attrName = this.toPropName(attr);
|
|
506
|
-
expressions.push(`${varName}.${attrName}`);
|
|
507
|
-
}
|
|
776
|
+
expressions.push(this.attrExpr(expr.slice(0, dotIdx), expr.slice(dotIdx + 1), ir, importedSymbols));
|
|
777
|
+
} else if (this.isDeclaredName(expr, ir)) {
|
|
778
|
+
// A Declarable cannot be interpolated into Sub; Ref(<var>) serializes
|
|
779
|
+
// as `${Name}`.
|
|
780
|
+
expressions.push(`Ref(${this.safeVarName(expr, importedSymbols)})`);
|
|
508
781
|
} else {
|
|
509
|
-
expressions.push(
|
|
782
|
+
expressions.push(`Ref(${JSON.stringify(expr)})`);
|
|
510
783
|
}
|
|
511
784
|
|
|
512
785
|
currentPos = match.index + match[0].length;
|
|
513
786
|
}
|
|
514
787
|
|
|
515
|
-
parts.push(template.slice(currentPos));
|
|
788
|
+
parts.push(literal + template.slice(currentPos));
|
|
516
789
|
|
|
517
790
|
if (expressions.length === 0) {
|
|
518
791
|
return `Sub\`${escapePart(template)}\``;
|
|
@@ -284,7 +284,6 @@ describe("CFParser conditions and outputs (#2069)", () => {
|
|
|
284
284
|
}),
|
|
285
285
|
);
|
|
286
286
|
expect(ir.warnings).toEqual([
|
|
287
|
-
'Template section "Mappings" is not carried by import — it is dropped from the generated source',
|
|
288
287
|
'Template section "Rules" is not carried by import — it is dropped from the generated source',
|
|
289
288
|
]);
|
|
290
289
|
});
|