@intentius/chant-lexicon-aws 0.0.6 → 0.0.8

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/src/plugin.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { createRequire } from "module";
1
2
  import type { LexiconPlugin, IntrinsicDef, SkillDefinition } from "@intentius/chant/lexicon";
3
+ const require = createRequire(import.meta.url);
2
4
  import type { LintRule } from "@intentius/chant/lint/rule";
3
5
  import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
4
6
  import type { TemplateParser } from "@intentius/chant/import/parser";
@@ -58,34 +60,34 @@ export const awsPlugin: LexiconPlugin = {
58
60
  * Shared bucket configuration — encryption, versioning, public access
59
61
  */
60
62
 
61
- import * as aws from "@intentius/chant-lexicon-aws";
63
+ import { ServerSideEncryptionByDefault, ServerSideEncryptionRule, BucketEncryption, PublicAccessBlockConfiguration, VersioningConfiguration } from "@intentius/chant-lexicon-aws";
62
64
 
63
65
  // Encryption default — AES256 server-side encryption
64
- export const encryptionDefault = new aws.ServerSideEncryptionByDefault({
65
- sseAlgorithm: "AES256",
66
+ export const encryptionDefault = new ServerSideEncryptionByDefault({
67
+ SSEAlgorithm: "AES256",
66
68
  });
67
69
 
68
70
  // Encryption rule wrapping the default
69
- export const encryptionRule = new aws.ServerSideEncryptionRule({
70
- serverSideEncryptionByDefault: encryptionDefault,
71
+ export const encryptionRule = new ServerSideEncryptionRule({
72
+ ServerSideEncryptionByDefault: encryptionDefault,
71
73
  });
72
74
 
73
75
  // Bucket encryption configuration
74
- export const bucketEncryption = new aws.BucketEncryption({
75
- serverSideEncryptionConfiguration: [encryptionRule],
76
+ export const bucketEncryption = new BucketEncryption({
77
+ ServerSideEncryptionConfiguration: [encryptionRule],
76
78
  });
77
79
 
78
80
  // Public access block — deny all public access
79
- export const publicAccessBlock = new aws.PublicAccessBlockConfiguration({
80
- blockPublicAcls: true,
81
- blockPublicPolicy: true,
82
- ignorePublicAcls: true,
83
- restrictPublicBuckets: true,
81
+ export const publicAccessBlock = new PublicAccessBlockConfiguration({
82
+ BlockPublicAcls: true,
83
+ BlockPublicPolicy: true,
84
+ IgnorePublicAcls: true,
85
+ RestrictPublicBuckets: true,
84
86
  });
85
87
 
86
88
  // Versioning — enabled
87
- export const versioningEnabled = new aws.VersioningConfiguration({
88
- status: "Enabled",
89
+ export const versioningEnabled = new VersioningConfiguration({
90
+ Status: "Enabled",
89
91
  });
90
92
  `,
91
93
  "data-bucket.ts": `/**
@@ -96,10 +98,10 @@ import { Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
96
98
  import { versioningEnabled, bucketEncryption, publicAccessBlock } from "./config";
97
99
 
98
100
  export const dataBucket = new Bucket({
99
- bucketName: Sub\`\${AWS.StackName}-data\`,
100
- versioningConfiguration: versioningEnabled,
101
- bucketEncryption: bucketEncryption,
102
- publicAccessBlockConfiguration: publicAccessBlock,
101
+ BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-data\`,
102
+ VersioningConfiguration: versioningEnabled,
103
+ BucketEncryption: bucketEncryption,
104
+ PublicAccessBlockConfiguration: publicAccessBlock,
103
105
  });
104
106
  `,
105
107
  "logs-bucket.ts": `/**
@@ -110,11 +112,11 @@ import { Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
110
112
  import { versioningEnabled, bucketEncryption, publicAccessBlock } from "./config";
111
113
 
112
114
  export const logsBucket = new Bucket({
113
- bucketName: Sub\`\${AWS.StackName}-logs\`,
114
- accessControl: "LogDeliveryWrite",
115
- versioningConfiguration: versioningEnabled,
116
- bucketEncryption: bucketEncryption,
117
- publicAccessBlockConfiguration: publicAccessBlock,
115
+ BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-logs\`,
116
+ AccessControl: "LogDeliveryWrite",
117
+ VersioningConfiguration: versioningEnabled,
118
+ BucketEncryption: bucketEncryption,
119
+ PublicAccessBlockConfiguration: publicAccessBlock,
118
120
  });
119
121
  `,
120
122
  };
@@ -229,44 +231,17 @@ export const logsBucket = new Bucket({
229
231
 
230
232
  console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
231
233
 
232
- // Produce .tgz via bun pm pack
233
- const packProc = Bun.spawn(["bun", "pm", "pack"], {
234
- cwd: pkgDir,
235
- stdout: "pipe",
236
- stderr: "pipe",
237
- });
238
- const packOut = await new Response(packProc.stdout).text();
239
- const packErr = await new Response(packProc.stderr).text();
240
- const packExit = await packProc.exited;
234
+ // Produce .tgz via pack command
235
+ const { getRuntime } = await import("@intentius/chant/runtime-adapter");
236
+ const rt = getRuntime();
237
+ const { stdout: packOut, stderr: packErr, exitCode: packExit } = await rt.spawn(
238
+ rt.commands.packCmd,
239
+ { cwd: pkgDir },
240
+ );
241
241
  if (packExit === 0) {
242
242
  console.error(`Tarball: ${packOut.trim()}`);
243
243
  } else {
244
- console.error(`bun pm pack failed: ${packErr}`);
245
- }
246
- },
247
-
248
- async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
249
- const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
250
- const { join, dirname } = await import("path");
251
- const { fileURLToPath } = await import("url");
252
-
253
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
254
- const snapshotsDir = join(pkgDir, ".snapshots");
255
-
256
- if (options?.restore) {
257
- const generatedDir = join(pkgDir, "src", "generated");
258
- restoreSnapshot(String(options.restore), generatedDir);
259
- console.error(`Restored snapshot: ${options.restore}`);
260
- } else {
261
- const snapshots = listSnapshots(snapshotsDir);
262
- if (snapshots.length === 0) {
263
- console.error("No snapshots available.");
264
- } else {
265
- console.error(`Available snapshots (${snapshots.length}):`);
266
- for (const s of snapshots) {
267
- console.error(` ${s.timestamp} ${s.resourceCount} resources ${s.path}`);
268
- }
269
- }
244
+ console.error(`${rt.commands.packCmd.join(" ")} failed: ${packErr}`);
270
245
  }
271
246
  },
272
247
 
@@ -278,49 +253,80 @@ export const logsBucket = new Bucket({
278
253
  skills(): SkillDefinition[] {
279
254
  return [
280
255
  {
281
- name: "aws-cloudformation",
282
- description: "AWS CloudFormation best practices and common patterns",
256
+ name: "chant-aws",
257
+ description: "AWS CloudFormation template management workflows, patterns, and troubleshooting",
283
258
  content: `---
284
- name: aws-cloudformation
285
- description: AWS CloudFormation best practices and common patterns
259
+ skill: chant-aws
260
+ description: Build, validate, and deploy CloudFormation templates from a chant project
261
+ user-invocable: true
286
262
  ---
287
263
 
288
- # AWS CloudFormation with Chant
264
+ # Deploying CloudFormation from Chant
289
265
 
290
- ## Common Resource Types
266
+ This project defines CloudFormation resources as TypeScript in \`src/\`. Use these steps to build, validate, and deploy.
291
267
 
292
- - \`AWS::S3::Bucket\` Object storage
293
- - \`AWS::Lambda::Function\` — Serverless compute
294
- - \`AWS::DynamoDB::Table\` — NoSQL database
295
- - \`AWS::IAM::Role\` — Identity and access management
296
- - \`AWS::SNS::Topic\` — Pub/sub messaging
297
- - \`AWS::SQS::Queue\` — Message queue
298
- - \`AWS::EC2::SecurityGroup\` — Network firewall rules
268
+ ## Build the template
299
269
 
300
- ## Intrinsic Functions
270
+ \`\`\`bash
271
+ chant build src/ --output stack.json
272
+ \`\`\`
301
273
 
302
- - \`Sub\` String interpolation with \`\${}\` syntax
303
- - \`Ref\` — Reference a resource or parameter
304
- - \`GetAtt\` — Get a resource attribute (e.g. ARN)
305
- - \`If\` — Conditional value based on a condition
306
- - \`Join\` — Join strings with a delimiter
307
- - \`Select\` — Pick an item from a list by index
274
+ ## Validate before deploying
308
275
 
309
- ## Pseudo Parameters
276
+ \`\`\`bash
277
+ chant lint src/
278
+ aws cloudformation validate-template --template-body file://stack.json
279
+ \`\`\`
310
280
 
311
- - \`AWS::StackName\` Name of the current stack
312
- - \`AWS::Region\` — Current deployment region
313
- - \`AWS::AccountId\` — Current AWS account ID
314
- - \`AWS::Partition\` — Partition (aws, aws-cn, aws-us-gov)
281
+ ## Deploy a new stack
315
282
 
316
- ## Best Practices
283
+ \`\`\`bash
284
+ aws cloudformation deploy \\
285
+ --template-file stack.json \\
286
+ --stack-name <stack-name> \\
287
+ --capabilities CAPABILITY_NAMED_IAM
288
+ \`\`\`
317
289
 
318
- 1. **Always enable encryption** — Use \`BucketEncryption\` for S3, \`SSESpecification\` for DynamoDB
319
- 2. **Block public access** — Set \`PublicAccessBlockConfiguration\` on all S3 buckets
320
- 3. **Use least-privilege IAM** — Avoid \`*\` in IAM policy actions and resources
321
- 4. **Enable versioning** — Turn on \`VersioningConfiguration\` for data buckets
322
- 5. **Use Sub for dynamic names** — \`Sub\\\`\\\${AWS::StackName}-suffix\\\`\` for unique naming
323
- 6. **Share config via direct imports** — Put common settings in a config file and import directly
290
+ Add \`--parameter-overrides Key=Value\` if the template has parameters.
291
+
292
+ ## Update an existing stack
293
+
294
+ 1. Edit the TypeScript source
295
+ 2. Rebuild: \`chant build src/ --output stack.json\`
296
+ 3. Preview changes:
297
+ \`\`\`bash
298
+ aws cloudformation create-change-set \\
299
+ --stack-name <stack-name> \\
300
+ --template-body file://stack.json \\
301
+ --change-set-name update-$(date +%s) \\
302
+ --capabilities CAPABILITY_NAMED_IAM
303
+ aws cloudformation describe-change-set \\
304
+ --stack-name <stack-name> \\
305
+ --change-set-name update-<id>
306
+ \`\`\`
307
+ 4. Execute: \`aws cloudformation execute-change-set --stack-name <stack-name> --change-set-name update-<id>\`
308
+
309
+ Or deploy directly: \`aws cloudformation deploy --template-file stack.json --stack-name <stack-name> --capabilities CAPABILITY_NAMED_IAM\`
310
+
311
+ ## Delete a stack
312
+
313
+ \`\`\`bash
314
+ aws cloudformation delete-stack --stack-name <stack-name>
315
+ aws cloudformation wait stack-delete-complete --stack-name <stack-name>
316
+ \`\`\`
317
+
318
+ ## Check stack status
319
+
320
+ \`\`\`bash
321
+ aws cloudformation describe-stacks --stack-name <stack-name>
322
+ aws cloudformation describe-stack-events --stack-name <stack-name> --max-items 10
323
+ \`\`\`
324
+
325
+ ## Troubleshooting deploy failures
326
+
327
+ - Check events: \`aws cloudformation describe-stack-events --stack-name <stack-name>\`
328
+ - Rollback stuck: \`aws cloudformation continue-update-rollback --stack-name <stack-name>\`
329
+ - Drift: \`aws cloudformation detect-stack-drift --stack-name <stack-name>\`
324
330
  `,
325
331
  triggers: [
326
332
  { type: "file-pattern", value: "**/*.aws.ts" },
@@ -424,31 +430,31 @@ description: AWS CloudFormation best practices and common patterns
424
430
  description: "AWS S3 bucket with versioning and encryption",
425
431
  mimeType: "text/typescript",
426
432
  async handler(): Promise<string> {
427
- return `import * as aws from "@intentius/chant-lexicon-aws";
433
+ return `import { ServerSideEncryptionByDefault, ServerSideEncryptionRule, BucketEncryption, VersioningConfiguration, Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
428
434
 
429
435
  // Encryption configuration
430
- export const encryptionDefault = new aws.ServerSideEncryptionByDefault({
431
- sseAlgorithm: "AES256",
436
+ export const encryptionDefault = new ServerSideEncryptionByDefault({
437
+ SSEAlgorithm: "AES256",
432
438
  });
433
439
 
434
- export const encryptionRule = new aws.ServerSideEncryptionRule({
435
- serverSideEncryptionByDefault: encryptionDefault,
440
+ export const encryptionRule = new ServerSideEncryptionRule({
441
+ ServerSideEncryptionByDefault: encryptionDefault,
436
442
  });
437
443
 
438
- export const bucketEncryption = new aws.BucketEncryption({
439
- serverSideEncryptionConfiguration: [encryptionRule],
444
+ export const bucketEncryption = new BucketEncryption({
445
+ ServerSideEncryptionConfiguration: [encryptionRule],
440
446
  });
441
447
 
442
448
  // Versioning
443
- export const versioningEnabled = new aws.VersioningConfiguration({
444
- status: "Enabled",
449
+ export const versioningEnabled = new VersioningConfiguration({
450
+ Status: "Enabled",
445
451
  });
446
452
 
447
- // Create a versioned bucket with encryption
448
- export const dataBucket = new aws.Bucket({
449
- bucketName: aws.Sub\`\${aws.AWS.StackName}-data\`,
450
- versioningConfiguration: versioningEnabled,
451
- bucketEncryption: bucketEncryption,
453
+ // Create a versioned bucket with encryption (AccountId ensures global uniqueness)
454
+ export const dataBucket = new Bucket({
455
+ BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-data\`,
456
+ VersioningConfiguration: versioningEnabled,
457
+ BucketEncryption: bucketEncryption,
452
458
  });
453
459
  `;
454
460
  },
@@ -459,17 +465,17 @@ export const dataBucket = new aws.Bucket({
459
465
  description: "Using AttrRefs for cross-resource references",
460
466
  mimeType: "text/typescript",
461
467
  async handler(): Promise<string> {
462
- return `import * as aws from "@intentius/chant-lexicon-aws";
468
+ return `import { Bucket, VersioningConfiguration, Role } from "@intentius/chant-lexicon-aws";
463
469
 
464
470
  // Create a bucket
465
- export const dataBucket = new aws.Bucket({
466
- bucketName: "my-data-bucket",
467
- versioningConfiguration: new aws.VersioningConfiguration({ status: "Enabled" }),
471
+ export const dataBucket = new Bucket({
472
+ BucketName: "my-data-bucket",
473
+ VersioningConfiguration: new VersioningConfiguration({ Status: "Enabled" }),
468
474
  });
469
475
 
470
476
  // Create a role that references the bucket's ARN
471
- export const role = new aws.Role({
472
- assumeRolePolicyDocument: {
477
+ export const role = new Role({
478
+ AssumeRolePolicyDocument: {
473
479
  Version: "2012-10-17",
474
480
  Statement: [{
475
481
  Effect: "Allow",
@@ -18,9 +18,9 @@ class MockBucket implements Declarable {
18
18
  readonly lexicon = "aws";
19
19
  readonly entityType = "AWS::S3::Bucket";
20
20
  readonly arn: AttrRef;
21
- readonly props: { bucketName?: string; versioningConfiguration?: { status: string } };
21
+ readonly props: { BucketName?: string; VersioningConfiguration?: { Status: string } };
22
22
 
23
- constructor(props: { bucketName?: string; versioningConfiguration?: { status: string } } = {}) {
23
+ constructor(props: { BucketName?: string; VersioningConfiguration?: { Status: string } } = {}) {
24
24
  this.props = props;
25
25
  this.arn = new AttrRef(this, "Arn");
26
26
  }
@@ -57,7 +57,7 @@ describe("awsSerializer.serialize", () => {
57
57
 
58
58
  test("serializes resources", () => {
59
59
  const entities = new Map<string, Declarable>();
60
- entities.set("MyBucket", new MockBucket({ bucketName: "my-bucket" }));
60
+ entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
61
61
 
62
62
  const output = awsSerializer.serialize(entities);
63
63
  const template = JSON.parse(output);
@@ -86,8 +86,8 @@ describe("awsSerializer.serialize", () => {
86
86
  test("serializes nested properties", () => {
87
87
  const entities = new Map<string, Declarable>();
88
88
  entities.set("MyBucket", new MockBucket({
89
- bucketName: "my-bucket",
90
- versioningConfiguration: { status: "Enabled" },
89
+ BucketName: "my-bucket",
90
+ VersioningConfiguration: { Status: "Enabled" },
91
91
  }));
92
92
 
93
93
  const output = awsSerializer.serialize(entities);
@@ -98,21 +98,20 @@ describe("awsSerializer.serialize", () => {
98
98
  });
99
99
  });
100
100
 
101
- test("converts property names to PascalCase", () => {
101
+ test("passes through property names verbatim", () => {
102
102
  const entities = new Map<string, Declarable>();
103
- entities.set("MyBucket", new MockBucket({ bucketName: "test" }));
103
+ entities.set("MyBucket", new MockBucket({ BucketName: "test" }));
104
104
 
105
105
  const output = awsSerializer.serialize(entities);
106
106
  const template = JSON.parse(output);
107
107
 
108
108
  expect(template.Resources.MyBucket.Properties.BucketName).toBeDefined();
109
- expect(template.Resources.MyBucket.Properties.bucketName).toBeUndefined();
110
109
  });
111
110
 
112
111
  test("handles multiple resources", () => {
113
112
  const entities = new Map<string, Declarable>();
114
- entities.set("DataBucket", new MockBucket({ bucketName: "data-bucket" }));
115
- entities.set("LogsBucket", new MockBucket({ bucketName: "logs-bucket" }));
113
+ entities.set("DataBucket", new MockBucket({ BucketName: "data-bucket" }));
114
+ entities.set("LogsBucket", new MockBucket({ BucketName: "logs-bucket" }));
116
115
 
117
116
  const output = awsSerializer.serialize(entities);
118
117
  const template = JSON.parse(output);
@@ -125,7 +124,7 @@ describe("awsSerializer.serialize", () => {
125
124
  test("handles resources and parameters together", () => {
126
125
  const entities = new Map<string, Declarable>();
127
126
  entities.set("Env", new Parameter("String"));
128
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
127
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
129
128
 
130
129
  const output = awsSerializer.serialize(entities);
131
130
  const template = JSON.parse(output);
@@ -154,9 +153,9 @@ class MockEncryption implements Declarable {
154
153
  readonly lexicon = "aws";
155
154
  readonly entityType = "AWS::S3::Bucket.BucketEncryption";
156
155
  readonly kind = "property" as const;
157
- readonly props: { serverSideEncryptionConfiguration: unknown[] };
156
+ readonly props: { ServerSideEncryptionConfiguration: unknown[] };
158
157
 
159
- constructor(props: { serverSideEncryptionConfiguration: unknown[] }) {
158
+ constructor(props: { ServerSideEncryptionConfiguration: unknown[] }) {
160
159
  this.props = props;
161
160
  }
162
161
  }
@@ -164,14 +163,14 @@ class MockEncryption implements Declarable {
164
163
  describe("property-kind Declarables", () => {
165
164
  test("property-kind Declarables are inlined into parent properties", () => {
166
165
  const encryption = new MockEncryption({
167
- serverSideEncryptionConfiguration: [
168
- { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
166
+ ServerSideEncryptionConfiguration: [
167
+ { ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
169
168
  ],
170
169
  });
171
170
 
172
- const bucket = new MockBucket({ bucketName: "my-bucket" });
171
+ const bucket = new MockBucket({ BucketName: "my-bucket" });
173
172
  // Manually set encryption as a prop
174
- (bucket.props as Record<string, unknown>).encryption = encryption;
173
+ (bucket.props as Record<string, unknown>).BucketEncryption = encryption;
175
174
 
176
175
  const entities = new Map<string, Declarable>();
177
176
  entities.set("DataEncryption", encryption);
@@ -181,23 +180,23 @@ describe("property-kind Declarables", () => {
181
180
  const template = JSON.parse(output);
182
181
 
183
182
  // Encryption should be inlined, not a Ref
184
- expect(template.Resources.MyBucket.Properties.Encryption).toEqual({
183
+ expect(template.Resources.MyBucket.Properties.BucketEncryption).toEqual({
185
184
  ServerSideEncryptionConfiguration: [
186
- { ServerSideEncryptionByDefault: { SseAlgorithm: "AES256" } },
185
+ { ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
187
186
  ],
188
187
  });
189
188
  });
190
189
 
191
190
  test("property-kind Declarables do NOT appear as standalone Resources", () => {
192
191
  const encryption = new MockEncryption({
193
- serverSideEncryptionConfiguration: [
194
- { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
192
+ ServerSideEncryptionConfiguration: [
193
+ { ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
195
194
  ],
196
195
  });
197
196
 
198
197
  const entities = new Map<string, Declarable>();
199
198
  entities.set("DataEncryption", encryption);
200
- entities.set("MyBucket", new MockBucket({ bucketName: "my-bucket" }));
199
+ entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
201
200
 
202
201
  const output = awsSerializer.serialize(entities);
203
202
  const template = JSON.parse(output);
@@ -207,16 +206,16 @@ describe("property-kind Declarables", () => {
207
206
  });
208
207
 
209
208
  test("resource-kind Declarables still emit Ref when referenced", () => {
210
- const sourceBucket = new MockBucket({ bucketName: "source" });
209
+ const sourceBucket = new MockBucket({ BucketName: "source" });
211
210
 
212
211
  class MockConfig implements Declarable {
213
212
  readonly [DECLARABLE_MARKER] = true as const;
214
213
  readonly lexicon = "aws";
215
214
  readonly entityType = "AWS::S3::ReplicationDestination";
216
- readonly props: { bucket: Declarable };
215
+ readonly props: { Bucket: Declarable };
217
216
 
218
- constructor(bucket: Declarable) {
219
- this.props = { bucket };
217
+ constructor(Bucket: Declarable) {
218
+ this.props = { Bucket };
220
219
  }
221
220
  }
222
221
 
@@ -233,7 +232,7 @@ describe("property-kind Declarables", () => {
233
232
 
234
233
  describe("intrinsic serialization", () => {
235
234
  test("handles AttrRef in properties", () => {
236
- const source = new MockBucket({ bucketName: "source" });
235
+ const source = new MockBucket({ BucketName: "source" });
237
236
  // Set the logical name on the AttrRef before using it
238
237
  (source.arn as Record<string, unknown>)._setLogicalName("SourceBucket");
239
238
 
@@ -241,10 +240,10 @@ describe("intrinsic serialization", () => {
241
240
  readonly [DECLARABLE_MARKER] = true as const;
242
241
  readonly lexicon = "aws";
243
242
  readonly entityType = "AWS::S3::ReplicationConfiguration";
244
- readonly props: { sourceArn: AttrRef };
243
+ readonly props: { SourceArn: AttrRef };
245
244
 
246
- constructor(sourceArn: AttrRef) {
247
- this.props = { sourceArn };
245
+ constructor(SourceArn: AttrRef) {
246
+ this.props = { SourceArn };
248
247
  }
249
248
  }
250
249
 
@@ -256,14 +255,14 @@ describe("intrinsic serialization", () => {
256
255
  const template = JSON.parse(output);
257
256
 
258
257
  expect(template.Resources.Replication.Properties.SourceArn).toEqual({
259
- "Fn::GetAttr": ["SourceBucket", "Arn"],
258
+ "Fn::GetAtt": ["SourceBucket", "Arn"],
260
259
  });
261
260
  });
262
261
  });
263
262
 
264
263
  describe("LexiconOutput serialization", () => {
265
264
  test("generates CF Outputs section for LexiconOutputs", () => {
266
- const bucket = new MockBucket({ bucketName: "data-bucket" });
265
+ const bucket = new MockBucket({ BucketName: "data-bucket" });
267
266
  const lexiconOutput = new LexiconOutput(bucket.arn, "DataBucketArn");
268
267
  lexiconOutput._setSourceEntity("dataBucket");
269
268
 
@@ -275,13 +274,13 @@ describe("LexiconOutput serialization", () => {
275
274
 
276
275
  expect(template.Outputs).toBeDefined();
277
276
  expect(template.Outputs.DataBucketArn).toEqual({
278
- Value: { "Fn::GetAttr": ["dataBucket", "Arn"] },
277
+ Value: { "Fn::GetAtt": ["dataBucket", "Arn"] },
279
278
  });
280
279
  });
281
280
 
282
281
  test("generates multiple CF Outputs", () => {
283
- const dataBucket = new MockBucket({ bucketName: "data-bucket" });
284
- const logsBucket = new MockBucket({ bucketName: "logs-bucket" });
282
+ const dataBucket = new MockBucket({ BucketName: "data-bucket" });
283
+ const logsBucket = new MockBucket({ BucketName: "logs-bucket" });
285
284
 
286
285
  const dataOutput = new LexiconOutput(dataBucket.arn, "DataBucketArn");
287
286
  dataOutput._setSourceEntity("dataBucket");
@@ -298,16 +297,16 @@ describe("LexiconOutput serialization", () => {
298
297
  expect(template.Outputs).toBeDefined();
299
298
  expect(Object.keys(template.Outputs)).toHaveLength(2);
300
299
  expect(template.Outputs.DataBucketArn.Value).toEqual({
301
- "Fn::GetAttr": ["dataBucket", "Arn"],
300
+ "Fn::GetAtt": ["dataBucket", "Arn"],
302
301
  });
303
302
  expect(template.Outputs.LogsBucketArn.Value).toEqual({
304
- "Fn::GetAttr": ["logsBucket", "Arn"],
303
+ "Fn::GetAtt": ["logsBucket", "Arn"],
305
304
  });
306
305
  });
307
306
 
308
307
  test("omits Outputs section when no LexiconOutputs provided", () => {
309
308
  const entities = new Map<string, Declarable>();
310
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
309
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
311
310
 
312
311
  const result = awsSerializer.serialize(entities);
313
312
  const template = JSON.parse(result);
@@ -317,7 +316,7 @@ describe("LexiconOutput serialization", () => {
317
316
 
318
317
  test("omits Outputs section when empty LexiconOutputs array", () => {
319
318
  const entities = new Map<string, Declarable>();
320
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
319
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
321
320
 
322
321
  const result = awsSerializer.serialize(entities, []);
323
322
  const template = JSON.parse(result as string);
@@ -430,7 +429,7 @@ describe("nested stack serialization", () => {
430
429
  Resources: {},
431
430
  });
432
431
 
433
- const bucket = new MockBucket({ bucketName: "data" });
432
+ const bucket = new MockBucket({ BucketName: "data" });
434
433
 
435
434
  const entities = new Map<string, Declarable>();
436
435
  entities.set("network", stack as unknown as Declarable);
@@ -446,7 +445,7 @@ describe("nested stack serialization", () => {
446
445
 
447
446
  test("without nested stacks returns plain string", () => {
448
447
  const entities = new Map<string, Declarable>();
449
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
448
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
450
449
 
451
450
  const result = awsSerializer.serialize(entities);
452
451
  expect(typeof result).toBe("string");
@@ -460,7 +459,7 @@ describe("nested stack serialization", () => {
460
459
  subnet: { Type: "AWS::EC2::Subnet" },
461
460
  },
462
461
  Outputs: {
463
- subnetId: { Value: { "Fn::GetAttr": ["subnet", "SubnetId"] } },
462
+ subnetId: { Value: { "Fn::GetAtt": ["subnet", "SubnetId"] } },
464
463
  },
465
464
  });
466
465
 
@@ -472,7 +471,7 @@ describe("nested stack serialization", () => {
472
471
  entityType: "AWS::Lambda::Function",
473
472
  kind: "resource" as const,
474
473
  props: {
475
- vpcConfig: { subnetIds: [subnetRef] },
474
+ VpcConfig: { SubnetIds: [subnetRef] },
476
475
  },
477
476
  } as unknown as Declarable;
478
477
 
package/src/serializer.ts CHANGED
@@ -3,7 +3,6 @@ import { isPropertyDeclarable } from "@intentius/chant/declarable";
3
3
  import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
4
4
  import type { LexiconOutput } from "@intentius/chant/lexicon-output";
5
5
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
6
- import { toPascalCase } from "@intentius/chant/codegen/case";
7
6
  import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
8
7
  import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
9
8
 
@@ -68,7 +67,7 @@ interface CFOutput {
68
67
  */
69
68
  function cfnVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
70
69
  return {
71
- attrRef: (name, attr) => ({ "Fn::GetAttr": [name, attr] }),
70
+ attrRef: (name, attr) => ({ "Fn::GetAtt": [name, attr] }),
72
71
  resourceRef: (name) => ({ Ref: name }),
73
72
  propertyDeclarable: (entity, walk) => {
74
73
  if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
@@ -78,26 +77,19 @@ function cfnVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
78
77
  const cfProps: Record<string, unknown> = {};
79
78
  for (const [key, value] of Object.entries(props)) {
80
79
  if (value !== undefined) {
81
- const cfKey = toPascalCase(key);
82
- cfProps[cfKey] = walk(value);
80
+ cfProps[key] = walk(value);
83
81
  }
84
82
  }
85
83
  return Object.keys(cfProps).length > 0 ? cfProps : undefined;
86
84
  },
87
- transformKey: toPascalCase,
88
85
  };
89
86
  }
90
87
 
91
88
  /**
92
89
  * Convert a value to CF-compatible JSON using the generic walker.
93
90
  */
94
- function toCFValue(value: unknown, entityNames: Map<Declarable, string>, convertKeys = false): unknown {
95
- const visitor = cfnVisitor(entityNames);
96
- if (!convertKeys) {
97
- // When not converting keys, use a visitor without transformKey
98
- return walkValue(value, entityNames, { ...visitor, transformKey: undefined });
99
- }
100
- return walkValue(value, entityNames, visitor);
91
+ function toCFValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
92
+ return walkValue(value, entityNames, cfnVisitor(entityNames));
101
93
  }
102
94
 
103
95
  /**
@@ -116,8 +108,7 @@ function toProperties(
116
108
 
117
109
  for (const [key, value] of Object.entries(props)) {
118
110
  if (value !== undefined) {
119
- const cfKey = toPascalCase(key);
120
- cfProps[cfKey] = toCFValue(value, entityNames, true);
111
+ cfProps[key] = toCFValue(value, entityNames);
121
112
  }
122
113
  }
123
114
 
@@ -231,7 +222,7 @@ function serializeToTemplate(
231
222
  const logicalName = ref.getLogicalName();
232
223
  if (logicalName) {
233
224
  const output: CFOutput = {
234
- Value: { "Fn::GetAttr": [logicalName, ref.attribute] },
225
+ Value: { "Fn::GetAtt": [logicalName, ref.attribute] },
235
226
  };
236
227
  if (stackOutput.description) {
237
228
  output.Description = stackOutput.description;
@@ -247,7 +238,7 @@ function serializeToTemplate(
247
238
  for (const output of outputs) {
248
239
  template.Outputs[output.outputName] = {
249
240
  Value: {
250
- "Fn::GetAttr": [output.sourceEntity, output.sourceAttribute],
241
+ "Fn::GetAtt": [output.sourceEntity, output.sourceAttribute],
251
242
  },
252
243
  };
253
244
  }