@intentius/chant-lexicon-aws 0.0.4 → 0.0.6
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/README.md +9 -425
- package/dist/integrity.json +11 -11
- package/dist/manifest.json +7 -1
- package/dist/meta.json +340 -320
- package/dist/skills/aws-cloudformation.md +1 -1
- package/dist/types/index.d.ts +39 -0
- package/package.json +2 -2
- package/src/codegen/docs.ts +80 -473
- package/src/codegen/generate.ts +1 -1
- package/src/codegen/sam.ts +11 -11
- package/src/generated/index.d.ts +39 -0
- package/src/generated/index.ts +4 -0
- package/src/generated/lexicon-aws.json +340 -320
- package/src/import/generator.test.ts +117 -6
- package/src/import/generator.ts +178 -62
- package/src/import/parser.ts +1 -1
- package/src/import/roundtrip-fixtures.test.ts +72 -11
- package/src/import/roundtrip.test.ts +6 -5
- package/src/index.ts +8 -1
- package/src/intrinsics.ts +27 -0
- package/src/parameter.ts +16 -0
- package/src/plugin.test.ts +1 -1
- package/src/plugin.ts +20 -48
- package/src/serializer.test.ts +4 -19
- package/src/spec/parse.ts +2 -2
|
@@ -34,7 +34,7 @@ describe("CFGenerator", () => {
|
|
|
34
34
|
const files = generator.generate(ir);
|
|
35
35
|
|
|
36
36
|
expect(files[0].content).toContain("import { Bucket }");
|
|
37
|
-
expect(files[0].content).toContain("export const
|
|
37
|
+
expect(files[0].content).toContain("export const MyBucket = new Bucket({");
|
|
38
38
|
expect(files[0].content).toContain('bucketName: "my-bucket"');
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -57,7 +57,7 @@ describe("CFGenerator", () => {
|
|
|
57
57
|
const files = generator.generate(ir);
|
|
58
58
|
|
|
59
59
|
expect(files[0].content).toContain("import { Function }");
|
|
60
|
-
expect(files[0].content).toContain("export const
|
|
60
|
+
expect(files[0].content).toContain("export const MyFunction = new Function({");
|
|
61
61
|
expect(files[0].content).toContain('functionName: "my-function"');
|
|
62
62
|
expect(files[0].content).toContain('runtime: "nodejs18.x"');
|
|
63
63
|
});
|
|
@@ -78,7 +78,7 @@ describe("CFGenerator", () => {
|
|
|
78
78
|
|
|
79
79
|
const files = generator.generate(ir);
|
|
80
80
|
|
|
81
|
-
expect(files[0].content).toContain("bucketName:
|
|
81
|
+
expect(files[0].content).toContain("bucketName: Ref(BucketName)");
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
test("generates GetAtt as property access", () => {
|
|
@@ -102,7 +102,7 @@ describe("CFGenerator", () => {
|
|
|
102
102
|
|
|
103
103
|
const files = generator.generate(ir);
|
|
104
104
|
|
|
105
|
-
expect(files[0].content).toContain("sourceArn:
|
|
105
|
+
expect(files[0].content).toContain("sourceArn: SourceBucket.arn");
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
test("generates Sub as tagged template", () => {
|
|
@@ -173,9 +173,120 @@ describe("CFGenerator", () => {
|
|
|
173
173
|
const files = generator.generate(ir);
|
|
174
174
|
const content = files[0].content;
|
|
175
175
|
|
|
176
|
-
const sourcePos = content.indexOf("
|
|
177
|
-
const depPos = content.indexOf("
|
|
176
|
+
const sourcePos = content.indexOf("SourceBucket");
|
|
177
|
+
const depPos = content.indexOf("DependentBucket");
|
|
178
178
|
|
|
179
179
|
expect(sourcePos).toBeLessThan(depPos);
|
|
180
180
|
});
|
|
181
|
+
|
|
182
|
+
test("Sub template refs create topo sort dependencies", () => {
|
|
183
|
+
const ir: TemplateIR = {
|
|
184
|
+
parameters: [],
|
|
185
|
+
resources: [
|
|
186
|
+
{
|
|
187
|
+
logicalId: "DependentResource",
|
|
188
|
+
type: "AWS::S3::Bucket",
|
|
189
|
+
properties: {
|
|
190
|
+
BucketName: { __intrinsic: "Sub", template: "${SourceBucket}-copy" },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
logicalId: "SourceBucket",
|
|
195
|
+
type: "AWS::S3::Bucket",
|
|
196
|
+
properties: {},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const files = generator.generate(ir);
|
|
202
|
+
const content = files[0].content;
|
|
203
|
+
|
|
204
|
+
const sourcePos = content.indexOf("SourceBucket");
|
|
205
|
+
const depPos = content.indexOf("DependentResource");
|
|
206
|
+
|
|
207
|
+
expect(sourcePos).toBeLessThan(depPos);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("generates GetAZs with no argument", () => {
|
|
211
|
+
const ir: TemplateIR = {
|
|
212
|
+
parameters: [],
|
|
213
|
+
resources: [
|
|
214
|
+
{
|
|
215
|
+
logicalId: "MyBucket",
|
|
216
|
+
type: "AWS::S3::Bucket",
|
|
217
|
+
properties: {
|
|
218
|
+
BucketName: { __intrinsic: "GetAZs", region: "" },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const files = generator.generate(ir);
|
|
225
|
+
|
|
226
|
+
expect(files[0].content).toContain("GetAZs()");
|
|
227
|
+
expect(files[0].content).toContain("import { GetAZs");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("generates GetAZs with region argument", () => {
|
|
231
|
+
const ir: TemplateIR = {
|
|
232
|
+
parameters: [],
|
|
233
|
+
resources: [
|
|
234
|
+
{
|
|
235
|
+
logicalId: "MyBucket",
|
|
236
|
+
type: "AWS::S3::Bucket",
|
|
237
|
+
properties: {
|
|
238
|
+
BucketName: { __intrinsic: "GetAZs", region: "us-east-1" },
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const files = generator.generate(ir);
|
|
245
|
+
|
|
246
|
+
expect(files[0].content).toContain('GetAZs("us-east-1")');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("escapes backticks in Sub template", () => {
|
|
250
|
+
const ir: TemplateIR = {
|
|
251
|
+
parameters: [],
|
|
252
|
+
resources: [
|
|
253
|
+
{
|
|
254
|
+
logicalId: "MyBucket",
|
|
255
|
+
type: "AWS::S3::Bucket",
|
|
256
|
+
properties: {
|
|
257
|
+
BucketName: { __intrinsic: "Sub", template: "stats `field-name` query" },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const files = generator.generate(ir);
|
|
264
|
+
|
|
265
|
+
expect(files[0].content).toContain("\\`field-name\\`");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("generates nested GetAtt attribute with GetAtt function call", () => {
|
|
269
|
+
const ir: TemplateIR = {
|
|
270
|
+
parameters: [],
|
|
271
|
+
resources: [
|
|
272
|
+
{
|
|
273
|
+
logicalId: "ELB",
|
|
274
|
+
type: "AWS::ElasticLoadBalancing::LoadBalancer",
|
|
275
|
+
properties: {},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
logicalId: "MyBucket",
|
|
279
|
+
type: "AWS::S3::Bucket",
|
|
280
|
+
properties: {
|
|
281
|
+
BucketName: { __intrinsic: "GetAtt", logicalId: "ELB", attribute: "SourceSecurityGroup.OwnerAlias" },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const files = generator.generate(ir);
|
|
288
|
+
|
|
289
|
+
expect(files[0].content).toContain('GetAtt(ELB, "SourceSecurityGroup.OwnerAlias")');
|
|
290
|
+
expect(files[0].content).toContain("import { GetAtt");
|
|
291
|
+
});
|
|
181
292
|
});
|
package/src/import/generator.ts
CHANGED
|
@@ -2,24 +2,46 @@ import type { TemplateIR, ResourceIR, ParameterIR } from "@intentius/chant/impor
|
|
|
2
2
|
import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
|
|
3
3
|
import { topoSort } from "@intentius/chant/codegen/topo-sort";
|
|
4
4
|
import { hasIntrinsicInValue, irUsesIntrinsic, collectDependencies } from "@intentius/chant/import/ir-utils";
|
|
5
|
+
import { join } from "path";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* TypeScript code generator for CloudFormation templates
|
|
8
9
|
*/
|
|
9
10
|
export class CFGenerator implements TypeScriptGenerator {
|
|
11
|
+
private typeToClass: Map<string, string>;
|
|
12
|
+
private allClassNames: Set<string>;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
// Build reverse lookup from dist/meta.json: resourceType → className
|
|
16
|
+
const metaPath = join(import.meta.dir, "../../dist/meta.json");
|
|
17
|
+
const meta: Record<string, { resourceType: string; kind: string }> =
|
|
18
|
+
require(metaPath);
|
|
19
|
+
this.typeToClass = new Map();
|
|
20
|
+
this.allClassNames = new Set();
|
|
21
|
+
for (const [className, entry] of Object.entries(meta)) {
|
|
22
|
+
if (entry.kind === "resource" && !className.includes("_")) {
|
|
23
|
+
this.typeToClass.set(entry.resourceType, className);
|
|
24
|
+
this.allClassNames.add(className);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
/**
|
|
11
30
|
* Generate TypeScript files from intermediate representation
|
|
12
31
|
*/
|
|
13
32
|
generate(ir: TemplateIR): GeneratedFile[] {
|
|
14
33
|
const lines: string[] = [];
|
|
15
34
|
|
|
35
|
+
// Collect the set of imported class names so we can detect variable name conflicts
|
|
36
|
+
const importedSymbols = this.collectImportedSymbols(ir);
|
|
37
|
+
|
|
16
38
|
// Generate imports
|
|
17
39
|
lines.push(this.generateImports(ir));
|
|
18
40
|
lines.push("");
|
|
19
41
|
|
|
20
42
|
// Generate parameters
|
|
21
43
|
for (const param of ir.parameters) {
|
|
22
|
-
lines.push(this.generateParameter(param));
|
|
44
|
+
lines.push(this.generateParameter(param, importedSymbols));
|
|
23
45
|
}
|
|
24
46
|
|
|
25
47
|
if (ir.parameters.length > 0) {
|
|
@@ -29,7 +51,7 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
29
51
|
// Generate resources in dependency order
|
|
30
52
|
const sortedResources = this.sortByDependencies(ir.resources);
|
|
31
53
|
for (const resource of sortedResources) {
|
|
32
|
-
lines.push(this.generateResource(resource, ir));
|
|
54
|
+
lines.push(this.generateResource(resource, ir, importedSymbols));
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
return [
|
|
@@ -40,6 +62,31 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
40
62
|
];
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Collect the set of symbols that will be imported (class names, intrinsics, etc.)
|
|
67
|
+
*/
|
|
68
|
+
private collectImportedSymbols(ir: TemplateIR): Set<string> {
|
|
69
|
+
const symbols = new Set<string>();
|
|
70
|
+
if (ir.parameters.length > 0) symbols.add("Parameter");
|
|
71
|
+
const intrinsics = ["Sub", "Ref", "If", "Join", "Select", "Split", "Base64", "GetAZs", "GetAtt"] as const;
|
|
72
|
+
for (const name of intrinsics) {
|
|
73
|
+
if (irUsesIntrinsic(ir, name)) symbols.add(name);
|
|
74
|
+
}
|
|
75
|
+
if (this.needsAWSPseudo(ir)) symbols.add("AWS");
|
|
76
|
+
for (const resource of ir.resources) {
|
|
77
|
+
const parsed = this.parseResourceType(resource.type);
|
|
78
|
+
if (parsed) symbols.add(parsed.resourceClass);
|
|
79
|
+
}
|
|
80
|
+
return symbols;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a logical ID to a safe variable name, suffixing with _ if it conflicts with an imported symbol
|
|
85
|
+
*/
|
|
86
|
+
private safeVarName(name: string, importedSymbols: Set<string>): string {
|
|
87
|
+
return importedSymbols.has(name) ? name + "_" : name;
|
|
88
|
+
}
|
|
89
|
+
|
|
43
90
|
/**
|
|
44
91
|
* Generate import statements
|
|
45
92
|
*/
|
|
@@ -54,24 +101,21 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
54
101
|
}
|
|
55
102
|
|
|
56
103
|
// Check for intrinsics
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (needsSub) imports.add("Sub");
|
|
63
|
-
if (needsRef) imports.add("Ref");
|
|
64
|
-
if (needsIf) imports.add("If");
|
|
65
|
-
if (needsJoin) imports.add("Join");
|
|
104
|
+
const intrinsics = ["Sub", "Ref", "If", "Join", "Select", "Split", "Base64", "GetAZs", "GetAtt"] as const;
|
|
105
|
+
for (const name of intrinsics) {
|
|
106
|
+
if (irUsesIntrinsic(ir, name)) imports.add(name);
|
|
107
|
+
}
|
|
66
108
|
|
|
67
109
|
// Check for AWS pseudo-parameters
|
|
68
110
|
if (this.needsAWSPseudo(ir)) {
|
|
69
111
|
imports.add("AWS");
|
|
70
112
|
}
|
|
71
113
|
|
|
72
|
-
// Collect service imports
|
|
114
|
+
// Collect service imports (skip unknown resource types)
|
|
73
115
|
for (const resource of ir.resources) {
|
|
74
|
-
const
|
|
116
|
+
const parsed = this.parseResourceType(resource.type);
|
|
117
|
+
if (!parsed) continue;
|
|
118
|
+
const { service, resourceClass } = parsed;
|
|
75
119
|
if (!serviceImports.has(service)) {
|
|
76
120
|
serviceImports.set(service, new Set());
|
|
77
121
|
}
|
|
@@ -98,14 +142,16 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
98
142
|
}
|
|
99
143
|
|
|
100
144
|
/**
|
|
101
|
-
* Parse AWS resource type into service and class names
|
|
145
|
+
* Parse AWS resource type into service and class names.
|
|
146
|
+
* Returns null for unknown/unsupported types (Custom::*, third-party, etc.)
|
|
102
147
|
*/
|
|
103
|
-
private parseResourceType(type: string): { service: string; resourceClass: string } {
|
|
104
|
-
|
|
148
|
+
private parseResourceType(type: string): { service: string; resourceClass: string } | null {
|
|
149
|
+
const className = this.typeToClass.get(type);
|
|
150
|
+
if (!className) return null;
|
|
105
151
|
const parts = type.split("::");
|
|
106
152
|
return {
|
|
107
153
|
service: parts[1]?.toLowerCase() ?? "unknown",
|
|
108
|
-
resourceClass:
|
|
154
|
+
resourceClass: className,
|
|
109
155
|
};
|
|
110
156
|
}
|
|
111
157
|
|
|
@@ -153,37 +199,67 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
153
199
|
* Sort resources by dependencies
|
|
154
200
|
*/
|
|
155
201
|
private sortByDependencies(resources: ResourceIR[]): ResourceIR[] {
|
|
202
|
+
const resourceIds = new Set(resources.map((r) => r.logicalId));
|
|
156
203
|
return topoSort(
|
|
157
204
|
resources,
|
|
158
205
|
(r) => r.logicalId,
|
|
159
|
-
(r) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
206
|
+
(r) => {
|
|
207
|
+
const extraDeps = new Set<string>();
|
|
208
|
+
const deps = collectDependencies(r.properties, (obj) => {
|
|
209
|
+
if (obj.__intrinsic === "Ref") {
|
|
210
|
+
const name = obj.name as string;
|
|
211
|
+
return name.startsWith("AWS::") ? null : name;
|
|
212
|
+
}
|
|
213
|
+
if (obj.__intrinsic === "GetAtt") {
|
|
214
|
+
return obj.logicalId as string;
|
|
215
|
+
}
|
|
216
|
+
if (obj.__intrinsic === "Sub") {
|
|
217
|
+
const tpl = obj.template as string;
|
|
218
|
+
const re = /\$\{([^}]+)\}/g;
|
|
219
|
+
let m;
|
|
220
|
+
while ((m = re.exec(tpl)) !== null) {
|
|
221
|
+
const expr = m[1];
|
|
222
|
+
if (!expr.startsWith("AWS::")) {
|
|
223
|
+
const id = expr.split(".")[0];
|
|
224
|
+
if (resourceIds.has(id)) extraDeps.add(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
});
|
|
231
|
+
for (const d of extraDeps) deps.add(d);
|
|
232
|
+
return [...deps];
|
|
233
|
+
},
|
|
169
234
|
);
|
|
170
235
|
}
|
|
171
236
|
|
|
172
237
|
/**
|
|
173
238
|
* Generate a parameter declaration
|
|
174
239
|
*/
|
|
175
|
-
private generateParameter(param: ParameterIR): string {
|
|
176
|
-
const varName = this.
|
|
240
|
+
private generateParameter(param: ParameterIR, importedSymbols: Set<string>): string {
|
|
241
|
+
const varName = this.safeVarName(param.name, importedSymbols);
|
|
242
|
+
const opts: string[] = [];
|
|
243
|
+
if (param.description) opts.push(`description: ${JSON.stringify(param.description)}`);
|
|
244
|
+
if (param.defaultValue !== undefined) opts.push(`defaultValue: ${JSON.stringify(param.defaultValue)}`);
|
|
245
|
+
if (opts.length > 0) {
|
|
246
|
+
return `export const ${varName} = new Parameter("${param.type}", { ${opts.join(", ")} });`;
|
|
247
|
+
}
|
|
177
248
|
return `export const ${varName} = new Parameter("${param.type}");`;
|
|
178
249
|
}
|
|
179
250
|
|
|
180
251
|
/**
|
|
181
|
-
* Generate a resource declaration
|
|
252
|
+
* Generate a resource declaration, or a comment if the type is unknown
|
|
182
253
|
*/
|
|
183
|
-
private generateResource(resource: ResourceIR, ir: TemplateIR): string {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
254
|
+
private generateResource(resource: ResourceIR, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
255
|
+
const parsed = this.parseResourceType(resource.type);
|
|
256
|
+
if (!parsed) {
|
|
257
|
+
const varName = this.safeVarName(resource.logicalId, importedSymbols);
|
|
258
|
+
return `// Unsupported type: ${resource.type}\nexport const ${varName} = "${resource.logicalId}";`;
|
|
259
|
+
}
|
|
260
|
+
const varName = this.safeVarName(resource.logicalId, importedSymbols);
|
|
261
|
+
const { resourceClass } = parsed;
|
|
262
|
+
const propsStr = this.generateProps(resource.properties, ir, importedSymbols);
|
|
187
263
|
|
|
188
264
|
if (propsStr === "{}") {
|
|
189
265
|
return `export const ${varName} = new ${resourceClass}();`;
|
|
@@ -195,14 +271,14 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
195
271
|
/**
|
|
196
272
|
* Generate property object as TypeScript
|
|
197
273
|
*/
|
|
198
|
-
private generateProps(props: Record<string, unknown>, ir: TemplateIR): string {
|
|
274
|
+
private generateProps(props: Record<string, unknown>, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
199
275
|
if (Object.keys(props).length === 0) {
|
|
200
276
|
return "{}";
|
|
201
277
|
}
|
|
202
278
|
|
|
203
279
|
const entries = Object.entries(props).map(([key, value]) => {
|
|
204
280
|
const propName = this.toPropName(key);
|
|
205
|
-
const valueStr = this.generateValue(value, ir);
|
|
281
|
+
const valueStr = this.generateValue(value, ir, importedSymbols);
|
|
206
282
|
return ` ${propName}: ${valueStr}`;
|
|
207
283
|
});
|
|
208
284
|
|
|
@@ -212,7 +288,7 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
212
288
|
/**
|
|
213
289
|
* Generate a value as TypeScript
|
|
214
290
|
*/
|
|
215
|
-
private generateValue(value: unknown, ir: TemplateIR): string {
|
|
291
|
+
private generateValue(value: unknown, ir: TemplateIR, importedSymbols: Set<string> = new Set()): string {
|
|
216
292
|
if (value === null || value === undefined) {
|
|
217
293
|
return "undefined";
|
|
218
294
|
}
|
|
@@ -226,7 +302,7 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
226
302
|
}
|
|
227
303
|
|
|
228
304
|
if (Array.isArray(value)) {
|
|
229
|
-
const items = value.map((item) => this.generateValue(item, ir));
|
|
305
|
+
const items = value.map((item) => this.generateValue(item, ir, importedSymbols));
|
|
230
306
|
return `[${items.join(", ")}]`;
|
|
231
307
|
}
|
|
232
308
|
|
|
@@ -239,37 +315,72 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
239
315
|
if (name.startsWith("AWS::")) {
|
|
240
316
|
return `AWS.${this.pseudoParamName(name)}`;
|
|
241
317
|
}
|
|
242
|
-
|
|
318
|
+
const varName = this.safeVarName(name, importedSymbols);
|
|
319
|
+
// Parameters need Ref() — bare variable would pass the Parameter object, not its value
|
|
320
|
+
const isParam = ir.parameters.some((p) => p.name === name);
|
|
321
|
+
if (isParam) {
|
|
322
|
+
return `Ref(${varName})`;
|
|
323
|
+
}
|
|
324
|
+
return varName;
|
|
243
325
|
}
|
|
244
326
|
|
|
245
327
|
if (obj.__intrinsic === "GetAtt") {
|
|
246
328
|
const logicalId = obj.logicalId as string;
|
|
247
329
|
const attribute = obj.attribute as string;
|
|
248
|
-
const varName = this.
|
|
330
|
+
const varName = this.safeVarName(logicalId, importedSymbols);
|
|
331
|
+
if (attribute.includes(".")) {
|
|
332
|
+
return `GetAtt(${varName}, "${attribute}")`;
|
|
333
|
+
}
|
|
249
334
|
const attrName = this.toPropName(attribute);
|
|
250
335
|
return `${varName}.${attrName}`;
|
|
251
336
|
}
|
|
252
337
|
|
|
253
338
|
if (obj.__intrinsic === "Sub") {
|
|
254
|
-
return this.generateSubIntrinsic(obj.template as string, ir);
|
|
339
|
+
return this.generateSubIntrinsic(obj.template as string, obj.variables as Record<string, unknown> | undefined, ir, importedSymbols);
|
|
255
340
|
}
|
|
256
341
|
|
|
257
342
|
if (obj.__intrinsic === "If") {
|
|
258
343
|
const condition = obj.condition as string;
|
|
259
|
-
const trueVal = this.generateValue(obj.valueIfTrue, ir);
|
|
260
|
-
const falseVal = this.generateValue(obj.valueIfFalse, ir);
|
|
344
|
+
const trueVal = this.generateValue(obj.valueIfTrue, ir, importedSymbols);
|
|
345
|
+
const falseVal = this.generateValue(obj.valueIfFalse, ir, importedSymbols);
|
|
261
346
|
return `If("${condition}", ${trueVal}, ${falseVal})`;
|
|
262
347
|
}
|
|
263
348
|
|
|
264
349
|
if (obj.__intrinsic === "Join") {
|
|
265
350
|
const delimiter = JSON.stringify(obj.delimiter);
|
|
266
|
-
const values = (obj.values as unknown[]).map((v) => this.generateValue(v, ir));
|
|
351
|
+
const values = (obj.values as unknown[]).map((v) => this.generateValue(v, ir, importedSymbols));
|
|
267
352
|
return `Join(${delimiter}, [${values.join(", ")}])`;
|
|
268
353
|
}
|
|
269
354
|
|
|
270
|
-
|
|
355
|
+
if (obj.__intrinsic === "Select") {
|
|
356
|
+
const index = obj.index as number;
|
|
357
|
+
const values = (obj.values as unknown[]).map((v) => this.generateValue(v, ir, importedSymbols));
|
|
358
|
+
return `Select(${index}, [${values.join(", ")}])`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (obj.__intrinsic === "Split") {
|
|
362
|
+
const delimiter = JSON.stringify(obj.delimiter);
|
|
363
|
+
const source = this.generateValue(obj.source, ir, importedSymbols);
|
|
364
|
+
return `Split(${delimiter}, ${source})`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (obj.__intrinsic === "Base64") {
|
|
368
|
+
const value = this.generateValue(obj.value, ir, importedSymbols);
|
|
369
|
+
return `Base64(${value})`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (obj.__intrinsic === "GetAZs") {
|
|
373
|
+
const region = obj.region;
|
|
374
|
+
if (region === undefined || region === "" || region === null) {
|
|
375
|
+
return "GetAZs()";
|
|
376
|
+
}
|
|
377
|
+
return `GetAZs(${this.generateValue(region, ir, importedSymbols)})`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Regular object — quote keys that aren't valid JS identifiers
|
|
271
381
|
const entries = Object.entries(obj).map(([key, val]) => {
|
|
272
|
-
|
|
382
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
383
|
+
return `${safeKey}: ${this.generateValue(val, ir, importedSymbols)}`;
|
|
273
384
|
});
|
|
274
385
|
return `{ ${entries.join(", ")} }`;
|
|
275
386
|
}
|
|
@@ -280,7 +391,9 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
280
391
|
/**
|
|
281
392
|
* Generate Sub intrinsic as tagged template literal
|
|
282
393
|
*/
|
|
283
|
-
private generateSubIntrinsic(template: string, ir: TemplateIR): string {
|
|
394
|
+
private generateSubIntrinsic(template: string, variables: Record<string, unknown> | undefined, ir: TemplateIR, importedSymbols: Set<string>): string {
|
|
395
|
+
const escapePart = (s: string) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
396
|
+
|
|
284
397
|
// Parse ${...} interpolations from the Sub template
|
|
285
398
|
const parts: string[] = [];
|
|
286
399
|
const expressions: string[] = [];
|
|
@@ -295,13 +408,21 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
295
408
|
const expr = match[1];
|
|
296
409
|
if (expr.startsWith("AWS::")) {
|
|
297
410
|
expressions.push(`AWS.${this.pseudoParamName(expr)}`);
|
|
411
|
+
} else if (variables && expr in variables) {
|
|
412
|
+
expressions.push(this.generateValue(variables[expr], ir, importedSymbols));
|
|
298
413
|
} else if (expr.includes(".")) {
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
414
|
+
const dotIdx = expr.indexOf(".");
|
|
415
|
+
const logicalId = expr.slice(0, dotIdx);
|
|
416
|
+
const attr = expr.slice(dotIdx + 1);
|
|
417
|
+
const varName = this.safeVarName(logicalId, importedSymbols);
|
|
418
|
+
if (attr.includes(".")) {
|
|
419
|
+
expressions.push(`GetAtt(${varName}, "${attr}")`);
|
|
420
|
+
} else {
|
|
421
|
+
const attrName = this.toPropName(attr);
|
|
422
|
+
expressions.push(`${varName}.${attrName}`);
|
|
423
|
+
}
|
|
303
424
|
} else {
|
|
304
|
-
expressions.push(this.
|
|
425
|
+
expressions.push(this.safeVarName(expr, importedSymbols));
|
|
305
426
|
}
|
|
306
427
|
|
|
307
428
|
currentPos = match.index + match[0].length;
|
|
@@ -310,12 +431,12 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
310
431
|
parts.push(template.slice(currentPos));
|
|
311
432
|
|
|
312
433
|
if (expressions.length === 0) {
|
|
313
|
-
return `Sub\`${template}\``;
|
|
434
|
+
return `Sub\`${escapePart(template)}\``;
|
|
314
435
|
}
|
|
315
436
|
|
|
316
437
|
let result = "Sub`";
|
|
317
438
|
for (let i = 0; i < parts.length; i++) {
|
|
318
|
-
result += parts[i];
|
|
439
|
+
result += escapePart(parts[i]);
|
|
319
440
|
if (i < expressions.length) {
|
|
320
441
|
result += `\${${expressions[i]}}`;
|
|
321
442
|
}
|
|
@@ -334,14 +455,9 @@ export class CFGenerator implements TypeScriptGenerator {
|
|
|
334
455
|
}
|
|
335
456
|
|
|
336
457
|
/**
|
|
337
|
-
* Convert a
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Convert a property name to camelCase
|
|
458
|
+
* Convert a property name to camelCase (lowercase first char).
|
|
459
|
+
* Used for resource property access (e.g., GetAtt attribute names)
|
|
460
|
+
* which matches chant's camelCase property convention.
|
|
345
461
|
*/
|
|
346
462
|
private toPropName(name: string): string {
|
|
347
463
|
return name.charAt(0).toLowerCase() + name.slice(1);
|
package/src/import/parser.ts
CHANGED
|
@@ -221,7 +221,7 @@ export class CFParser extends BaseValueParser implements TemplateParser {
|
|
|
221
221
|
return { __intrinsic: "GetAtt", logicalId: value[0], attribute: value[1] };
|
|
222
222
|
}
|
|
223
223
|
if (typeof value === "string") {
|
|
224
|
-
const [logicalId, attribute] = value.split(".");
|
|
224
|
+
const [logicalId, attribute] = value.split(".", 2);
|
|
225
225
|
return { __intrinsic: "GetAtt", logicalId, attribute };
|
|
226
226
|
}
|
|
227
227
|
}
|