@intentius/chant-lexicon-aws 0.0.3 → 0.0.5

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.
@@ -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 myBucket = new Bucket({");
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 myFunction = new Function({");
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: 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: sourceBucket.arn");
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("sourceBucket");
177
- const depPos = content.indexOf("dependentBucket");
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
  });
@@ -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 needsSub = irUsesIntrinsic(ir, "Sub");
58
- const needsRef = irUsesIntrinsic(ir, "Ref");
59
- const needsIf = irUsesIntrinsic(ir, "If");
60
- const needsJoin = irUsesIntrinsic(ir, "Join");
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 { service, resourceClass } = this.parseResourceType(resource.type);
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
- // AWS::S3::Bucket -> { service: "s3", resourceClass: "Bucket" }
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: parts[2] ?? "Unknown",
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) => [...collectDependencies(r.properties, (obj) => {
160
- if (obj.__intrinsic === "Ref") {
161
- const name = obj.name as string;
162
- return name.startsWith("AWS::") ? null : name;
163
- }
164
- if (obj.__intrinsic === "GetAtt") {
165
- return obj.logicalId as string;
166
- }
167
- return null;
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.toVariableName(param.name);
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 varName = this.toVariableName(resource.logicalId);
185
- const { resourceClass } = this.parseResourceType(resource.type);
186
- const propsStr = this.generateProps(resource.properties, ir);
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
- return this.toVariableName(name);
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.toVariableName(logicalId);
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
- // Regular object
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
- return `${key}: ${this.generateValue(val, ir)}`;
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 [logicalId, attr] = expr.split(".");
300
- const varName = this.toVariableName(logicalId);
301
- const attrName = this.toPropName(attr);
302
- expressions.push(`${varName}.${attrName}`);
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.toVariableName(expr));
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 logical name to a valid TypeScript variable name
338
- */
339
- private toVariableName(name: string): string {
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);
@@ -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
  }