@intentius/chant-lexicon-aws 0.0.2

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.
Files changed (94) hide show
  1. package/README.md +438 -0
  2. package/package.json +30 -0
  3. package/src/codegen/__snapshots__/snapshot.test.ts.snap +197 -0
  4. package/src/codegen/docs-cli.ts +3 -0
  5. package/src/codegen/docs.ts +1206 -0
  6. package/src/codegen/extensions.ts +171 -0
  7. package/src/codegen/fallback.ts +33 -0
  8. package/src/codegen/generate-cli.ts +17 -0
  9. package/src/codegen/generate-lexicon.ts +98 -0
  10. package/src/codegen/generate-typescript.ts +257 -0
  11. package/src/codegen/generate.test.ts +125 -0
  12. package/src/codegen/generate.ts +226 -0
  13. package/src/codegen/idempotency.test.ts +28 -0
  14. package/src/codegen/naming.ts +120 -0
  15. package/src/codegen/package.test.ts +60 -0
  16. package/src/codegen/package.ts +84 -0
  17. package/src/codegen/patches.ts +98 -0
  18. package/src/codegen/rollback.test.ts +80 -0
  19. package/src/codegen/rollback.ts +20 -0
  20. package/src/codegen/sam.ts +387 -0
  21. package/src/codegen/snapshot.test.ts +84 -0
  22. package/src/codegen/typecheck.test.ts +50 -0
  23. package/src/codegen/typecheck.ts +4 -0
  24. package/src/codegen/versions.ts +37 -0
  25. package/src/coverage.ts +14 -0
  26. package/src/generated/index.d.ts +160753 -0
  27. package/src/generated/index.ts +14396 -0
  28. package/src/generated/lexicon-aws.json +114563 -0
  29. package/src/generated/runtime.ts +4 -0
  30. package/src/import/generator.test.ts +181 -0
  31. package/src/import/generator.ts +349 -0
  32. package/src/import/parser.test.ts +200 -0
  33. package/src/import/parser.ts +350 -0
  34. package/src/import/roundtrip-fixtures.test.ts +78 -0
  35. package/src/import/roundtrip.test.ts +195 -0
  36. package/src/index.ts +63 -0
  37. package/src/integration.test.ts +129 -0
  38. package/src/intrinsics.test.ts +167 -0
  39. package/src/intrinsics.ts +223 -0
  40. package/src/lint/post-synth/cf-refs.ts +91 -0
  41. package/src/lint/post-synth/cor020.ts +72 -0
  42. package/src/lint/post-synth/ext001.test.ts +68 -0
  43. package/src/lint/post-synth/ext001.ts +222 -0
  44. package/src/lint/post-synth/post-synth.test.ts +280 -0
  45. package/src/lint/post-synth/waw010.ts +49 -0
  46. package/src/lint/post-synth/waw011.ts +49 -0
  47. package/src/lint/post-synth/waw013.ts +45 -0
  48. package/src/lint/post-synth/waw014.ts +50 -0
  49. package/src/lint/post-synth/waw015.ts +100 -0
  50. package/src/lint/rules/hardcoded-region.ts +43 -0
  51. package/src/lint/rules/iam-wildcard.ts +66 -0
  52. package/src/lint/rules/index.ts +7 -0
  53. package/src/lint/rules/rules.test.ts +175 -0
  54. package/src/lint/rules/s3-encryption.ts +69 -0
  55. package/src/lsp/completions.test.ts +72 -0
  56. package/src/lsp/completions.ts +18 -0
  57. package/src/lsp/hover.test.ts +53 -0
  58. package/src/lsp/hover.ts +53 -0
  59. package/src/nested-stack.test.ts +83 -0
  60. package/src/nested-stack.ts +125 -0
  61. package/src/plugin.test.ts +316 -0
  62. package/src/plugin.ts +514 -0
  63. package/src/pseudo.test.ts +55 -0
  64. package/src/pseudo.ts +29 -0
  65. package/src/serializer.test.ts +507 -0
  66. package/src/serializer.ts +333 -0
  67. package/src/spec/fetch.test.ts +27 -0
  68. package/src/spec/fetch.ts +107 -0
  69. package/src/spec/parse.test.ts +153 -0
  70. package/src/spec/parse.ts +202 -0
  71. package/src/testdata/load-fixtures.ts +17 -0
  72. package/src/testdata/roundtrip/conditions.json +21 -0
  73. package/src/testdata/roundtrip/intrinsic-calls.json +31 -0
  74. package/src/testdata/roundtrip/intrinsics.json +18 -0
  75. package/src/testdata/roundtrip/multi-resource.json +37 -0
  76. package/src/testdata/roundtrip/parameters.json +23 -0
  77. package/src/testdata/roundtrip/simple.json +12 -0
  78. package/src/testdata/sam-fixtures/api.yaml +14 -0
  79. package/src/testdata/sam-fixtures/application.yaml +13 -0
  80. package/src/testdata/sam-fixtures/function.yaml +22 -0
  81. package/src/testdata/sam-fixtures/graphql-api.yaml +13 -0
  82. package/src/testdata/sam-fixtures/http-api.yaml +15 -0
  83. package/src/testdata/sam-fixtures/layer-version.yaml +15 -0
  84. package/src/testdata/sam-fixtures/multi-type-a.yaml +23 -0
  85. package/src/testdata/sam-fixtures/multi-type-b.yaml +29 -0
  86. package/src/testdata/sam-fixtures/simple-table.yaml +12 -0
  87. package/src/testdata/sam-fixtures/state-machine.yaml +14 -0
  88. package/src/testdata/schemas/aws-dynamodb-table.json +126 -0
  89. package/src/testdata/schemas/aws-iam-role.json +85 -0
  90. package/src/testdata/schemas/aws-lambda-function.json +90 -0
  91. package/src/testdata/schemas/aws-s3-bucket.json +83 -0
  92. package/src/testdata/schemas/aws-sns-topic.json +71 -0
  93. package/src/validate-cli.ts +19 -0
  94. package/src/validate.ts +34 -0
@@ -0,0 +1,1206 @@
1
+ /**
2
+ * AWS CloudFormation documentation generator.
3
+ *
4
+ * Calls the core docsPipeline with AWS-specific config:
5
+ * service grouping, resource type URLs, and overview content.
6
+ *
7
+ * Produces a standalone Starlight docs site at lexicons/aws/docs/.
8
+ */
9
+
10
+ import { dirname, join } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";
13
+
14
+ const __dirname_ = dirname(fileURLToPath(import.meta.url));
15
+ const pkgDir = join(__dirname_, "..", "..");
16
+
17
+ /**
18
+ * Extract the AWS service name from a CloudFormation resource type.
19
+ * e.g. "AWS::S3::Bucket" → "S3", "AWS::Lambda::Function" → "Lambda"
20
+ */
21
+ function serviceFromType(resourceType: string): string {
22
+ const parts = resourceType.split("::");
23
+ return parts.length >= 2 ? parts[1] : "Other";
24
+ }
25
+
26
+ const overview = `The **AWS CloudFormation** lexicon provides full support for defining AWS infrastructure using chant's declarative TypeScript syntax. Resources are serialized to CloudFormation JSON templates.
27
+
28
+ This lexicon is generated from the official [CloudFormation Resource Provider Schemas](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) and includes coverage for all publicly available resource types.
29
+
30
+ Install it with:
31
+
32
+ \`\`\`bash
33
+ npm install --save-dev @intentius/chant-lexicon-aws
34
+ \`\`\``;
35
+
36
+ const outputFormat = `The AWS lexicon serializes resources into **CloudFormation JSON templates**.
37
+
38
+ ## Building
39
+
40
+ Run \`chant build\` to produce a CloudFormation template from your declarations:
41
+
42
+ \`\`\`bash
43
+ chant build
44
+ # Writes dist/template.json
45
+ \`\`\`
46
+
47
+ The generated template includes:
48
+
49
+ - \`AWSTemplateFormatVersion\` header
50
+ - \`Parameters\` section (if any parameters are declared)
51
+ - \`Resources\` section with typed resource definitions
52
+ - \`Outputs\` section for exported values
53
+ - Full support for intrinsic functions (\`Fn::Sub\`, \`Ref\`, \`Fn::GetAtt\`, etc.)
54
+
55
+ ## Deploying
56
+
57
+ The output is standard CloudFormation JSON. Deploy with any CF-compatible tool:
58
+
59
+ \`\`\`bash
60
+ # AWS CLI
61
+ aws cloudformation deploy \\
62
+ --template-file dist/template.json \\
63
+ --stack-name my-stack \\
64
+ --capabilities CAPABILITY_IAM
65
+
66
+ # Rain (faster, with diff preview)
67
+ rain deploy dist/template.json my-stack
68
+
69
+ # SAM CLI (if using serverless transforms)
70
+ sam deploy --template-file dist/template.json --stack-name my-stack
71
+ \`\`\`
72
+
73
+ ## Multi-file output (nested stacks)
74
+
75
+ When your project uses [nested stacks](./nested-stacks), \`chant build\` produces multiple template files:
76
+
77
+ \`\`\`bash
78
+ chant build -o template.json
79
+ # Produces:
80
+ # template.json — parent template
81
+ # network.template.json — child template (one per nestedStack)
82
+ \`\`\`
83
+
84
+ The parent template includes a \`TemplateBasePath\` parameter that controls where CloudFormation looks for child templates. Override it at deploy time to point to an S3 bucket:
85
+
86
+ \`\`\`bash
87
+ aws cloudformation deploy \\
88
+ --template-file template.json \\
89
+ --stack-name my-stack \\
90
+ --parameter-overrides TemplateBasePath=https://my-bucket.s3.amazonaws.com/templates
91
+ \`\`\`
92
+
93
+ All child template files must be uploaded alongside the parent template (or to the S3 path specified by \`TemplateBasePath\`).
94
+
95
+ ## Compatibility
96
+
97
+ The output is compatible with:
98
+ - AWS CloudFormation service (direct deployment)
99
+ - AWS SAM CLI
100
+ - AWS CDK (as an escape hatch via \`CfnInclude\`)
101
+ - Rain and other CloudFormation tooling
102
+ - Any tool that accepts CloudFormation JSON templates`;
103
+
104
+ /**
105
+ * Generate AWS lexicon documentation as a standalone Starlight site.
106
+ */
107
+ export async function generateDocs(options?: { verbose?: boolean }): Promise<void> {
108
+ const log = options?.verbose
109
+ ? (msg: string) => console.error(msg)
110
+ : (_msg: string) => {};
111
+
112
+ const distDir = join(pkgDir, "dist");
113
+ const srcDir = join(pkgDir, "src");
114
+ const outDir = join(pkgDir, "docs");
115
+
116
+ const config: DocsConfig = {
117
+ name: "aws",
118
+ basePath: process.env.DOCS_BASE_PATH ?? "/lexicons/aws/",
119
+ displayName: "AWS CloudFormation",
120
+ description: "AWS CloudFormation lexicon for chant — resource types, intrinsics, and lint rules",
121
+ distDir,
122
+ outDir,
123
+ srcDir,
124
+ overview,
125
+ outputFormat,
126
+ serviceFromType,
127
+ suppressPages: ["pseudo-parameters", "intrinsics", "rules"],
128
+ extraPages: [
129
+ {
130
+ slug: "cloudformation",
131
+ title: "CloudFormation Concepts",
132
+ description: "Templates, resources, parameters, outputs, dependencies, and tagging in the AWS CloudFormation lexicon",
133
+ content: `Every exported resource declaration becomes a logical resource in a CloudFormation template. The serializer handles the translation automatically:
134
+
135
+ - Wraps output in \`AWSTemplateFormatVersion: "2010-09-09"\`
136
+ - Converts camelCase property names to PascalCase (CloudFormation convention)
137
+ - Resolves \`AttrRef\` references to \`Fn::GetAtt\`
138
+ - Resolves resource references to \`Ref\` intrinsics
139
+
140
+ \`\`\`typescript
141
+ // This chant declaration...
142
+ export const dataBucket = new Bucket({
143
+ bucketName: Sub\`\${AWS.StackName}-data\`,
144
+ versioningConfiguration: $.versioningEnabled,
145
+ });
146
+ \`\`\`
147
+
148
+ Produces this CloudFormation resource:
149
+
150
+ \`\`\`json
151
+ "DataBucket": {
152
+ "Type": "AWS::S3::Bucket",
153
+ "Properties": {
154
+ "BucketName": { "Fn::Sub": "\${AWS::StackName}-data" },
155
+ "VersioningConfiguration": { "Status": "Enabled" }
156
+ }
157
+ }
158
+ \`\`\`
159
+
160
+ Notice how \`dataBucket\` becomes \`DataBucket\` (PascalCase logical ID), and \`bucketName\` becomes \`BucketName\`. This mapping is automatic.
161
+
162
+ ## Resource types and naming
163
+
164
+ CloudFormation resource types like \`AWS::S3::Bucket\` are mapped to short TypeScript class names. The lexicon uses a naming strategy that prioritizes readability:
165
+
166
+ | CloudFormation Type | Chant Class | Rule |
167
+ |---|---|---|
168
+ | \`AWS::S3::Bucket\` | \`Bucket\` | Priority name (common resource) |
169
+ | \`AWS::Lambda::Function\` | \`Function\` | Priority name |
170
+ | \`AWS::IAM::Role\` | \`Role\` | Priority name |
171
+ | \`AWS::EC2::Instance\` | \`Instance\` | Short name (last segment) |
172
+ | \`AWS::EC2::SecurityGroup\` | \`SecurityGroup\` | Short name |
173
+ | \`AWS::ECS::Service\` | \`EcsService\` | Service-prefixed (avoids collision with \`AWS::AppRunner::Service\`) |
174
+
175
+ Common resources get fixed short names for stability. When two services define the same resource name (e.g. both ECS and AppRunner have \`Service\`), the less common one gets a service prefix.
176
+
177
+ **Discovering available resources:** Your editor's autocomplete is the best tool — every resource is a named export from the lexicon. You can also run \`chant list\` to see all resource types, or browse the generated TypeScript types.
178
+
179
+ ## The barrel file
180
+
181
+ Every chant project has a barrel file (conventionally \`_.ts\`) that re-exports the lexicon and provides cross-file references:
182
+
183
+ \`\`\`typescript
184
+ // _.ts — the barrel file
185
+ export * from "@intentius/chant-lexicon-aws";
186
+ import * as core from "@intentius/chant";
187
+ export const $ = core.barrel(import.meta.dir);
188
+ \`\`\`
189
+
190
+ Other files import the barrel and use \`$\` to reference sibling exports:
191
+
192
+ \`\`\`typescript
193
+ // data-bucket.ts
194
+ import * as _ from "./_";
195
+
196
+ export const dataBucket = new _.Bucket({
197
+ bucketName: _.Sub\`\${_.AWS.StackName}-data\`,
198
+ serverSideEncryptionConfiguration: _.$.encryptionDefault, // from defaults.ts
199
+ });
200
+ \`\`\`
201
+
202
+ The \`$\` proxy lazily resolves exports from other files in the same directory. When the serializer encounters \`_.$.encryptionDefault\`, it resolves to the actual exported value and serializes the reference as \`Fn::GetAtt\` or \`Ref\` as appropriate. This is how cross-file references work without circular imports.
203
+
204
+ ## Parameters
205
+
206
+ CloudFormation parameters let you customize a stack at deploy time. Export a \`Parameter\` to add it to the template's \`Parameters\` section:
207
+
208
+ \`\`\`typescript
209
+ import { Parameter } from "@intentius/chant-lexicon-aws";
210
+
211
+ export const environment = new Parameter("String", {
212
+ description: "Deployment environment",
213
+ defaultValue: "dev",
214
+ });
215
+ \`\`\`
216
+
217
+ Produces:
218
+
219
+ \`\`\`json
220
+ "Parameters": {
221
+ "Environment": {
222
+ "Type": "String",
223
+ "Description": "Deployment environment",
224
+ "Default": "dev"
225
+ }
226
+ }
227
+ \`\`\`
228
+
229
+ Reference parameters with \`Ref\`:
230
+
231
+ \`\`\`typescript
232
+ import { Ref } from "@intentius/chant-lexicon-aws";
233
+
234
+ export const bucket = new Bucket({
235
+ bucketName: Sub\`\${Ref("Environment")}-data\`,
236
+ });
237
+ \`\`\`
238
+
239
+ ## Outputs
240
+
241
+ Use \`output()\` to create explicit stack outputs. Cross-resource \`AttrRef\` usage is also auto-detected and promoted to outputs when needed.
242
+
243
+ \`\`\`typescript
244
+ import { output } from "@intentius/chant";
245
+
246
+ export const bucketArn = output(dataBucket.arn, "DataBucketArn");
247
+ \`\`\`
248
+
249
+ Produces:
250
+
251
+ \`\`\`json
252
+ "Outputs": {
253
+ "DataBucketArn": {
254
+ "Value": { "Fn::GetAtt": ["DataBucket", "Arn"] }
255
+ }
256
+ }
257
+ \`\`\`
258
+
259
+ ## Pseudo-parameters
260
+
261
+ Runtime context values available in every template, accessed via the \`AWS\` namespace:
262
+
263
+ \`\`\`typescript
264
+ import { AWS, Sub } from "@intentius/chant-lexicon-aws";
265
+
266
+ const endpoint = Sub\`https://s3.\${AWS.Region}.\${AWS.URLSuffix}\`;
267
+ \`\`\`
268
+
269
+ | Pseudo-parameter | Description |
270
+ |---|---|
271
+ | \`AWS.StackName\` | Name of the stack |
272
+ | \`AWS.Region\` | AWS region |
273
+ | \`AWS.AccountId\` | AWS account ID |
274
+ | \`AWS.StackId\` | Stack ID |
275
+ | \`AWS.URLSuffix\` | Domain suffix (usually \`amazonaws.com\`) |
276
+ | \`AWS.Partition\` | Partition (\`aws\`, \`aws-cn\`, \`aws-us-gov\`) |
277
+ | \`AWS.NotificationARNs\` | Notification ARNs |
278
+ | \`AWS.NoValue\` | Removes property when used with \`Fn::If\` |
279
+
280
+ ## Intrinsic functions
281
+
282
+ The lexicon provides 8 intrinsic functions (\`Sub\`, \`Ref\`, \`GetAtt\`, \`If\`, \`Join\`, \`Select\`, \`Split\`, \`Base64\`) that map directly to CloudFormation \`Fn::\` calls. See [Intrinsic Functions](./intrinsics) for full usage examples.
283
+
284
+ ## Dependencies
285
+
286
+ CloudFormation automatically creates dependencies between resources when you use \`Ref\` or \`Fn::GetAtt\`. Chant leverages this — when you reference \`$.myBucket.arn\`, the serializer emits \`Fn::GetAtt\` and CloudFormation infers the dependency.
287
+
288
+ For cases where you need an explicit dependency without a property reference, set \`dependsOn\`:
289
+
290
+ \`\`\`typescript
291
+ export const appServer = new Instance({
292
+ imageId: "ami-12345678",
293
+ instanceType: "t3.micro",
294
+ dependsOn: ["DatabaseCluster"],
295
+ });
296
+ \`\`\`
297
+
298
+ The \`WAW010\` post-synth check warns if a \`DependsOn\` target is already referenced via \`Ref\` or \`Fn::GetAtt\` in properties — in that case the explicit dependency is redundant.
299
+
300
+ ## Policy documents
301
+
302
+ IAM policy documents appear on many AWS resources — \`Role.assumeRolePolicyDocument\`, \`ManagedPolicy.policyDocument\`, \`BucketPolicy.policyDocument\`, and others. These properties are typed as \`PolicyDocument\`, giving you autocomplete for the IAM JSON Policy Language.
303
+
304
+ The \`PolicyDocument\` interface and its supporting types:
305
+
306
+ | Type | Fields |
307
+ |------|--------|
308
+ | \`PolicyDocument\` | \`Version?\` (\`"2012-10-17"\` \\| \`"2008-10-17"\`), \`Id?\`, \`Statement\` |
309
+ | \`IamPolicyStatement\` | \`Effect\` (\`"Allow"\` \\| \`"Deny"\`), \`Action?\`, \`Resource?\`, \`Principal?\`, \`Condition?\`, and their \`Not\` variants |
310
+ | \`IamPolicyPrincipal\` | \`"*"\` or \`{ AWS?, Service?, Federated? }\` |
311
+
312
+ Policy documents use **PascalCase keys** (\`Effect\`, \`Action\`, \`Resource\`) because they follow the IAM JSON Policy Language spec — CloudFormation passes them through to IAM as-is, unlike resource properties which are automatically converted from camelCase.
313
+
314
+ The recommended pattern is to extract policies into your \`defaults.ts\` and reference them via the barrel:
315
+
316
+ \`\`\`typescript
317
+ // defaults.ts — shared trust policies and permission policies
318
+ import { Sub, AWS, type PolicyDocument } from "@intentius/chant-lexicon-aws";
319
+
320
+ export const lambdaTrustPolicy: PolicyDocument = {
321
+ Version: "2012-10-17",
322
+ Statement: [{
323
+ Effect: "Allow",
324
+ Principal: { Service: "lambda.amazonaws.com" },
325
+ Action: "sts:AssumeRole",
326
+ }],
327
+ };
328
+
329
+ export const s3ReadPolicy: PolicyDocument = {
330
+ Statement: [{
331
+ Effect: "Allow",
332
+ Action: ["s3:GetObject", "s3:ListBucket"],
333
+ Resource: "*",
334
+ }],
335
+ };
336
+ \`\`\`
337
+
338
+ Then reference them from resource files:
339
+
340
+ \`\`\`typescript
341
+ // role.ts
342
+ import * as _ from "./_";
343
+
344
+ export const functionRole = new _.Role({
345
+ assumeRolePolicyDocument: _.$.lambdaTrustPolicy,
346
+ });
347
+
348
+ export const readPolicy = new _.ManagedPolicy({
349
+ policyDocument: _.$.s3ReadPolicy,
350
+ roles: [_.$.functionRole],
351
+ });
352
+ \`\`\`
353
+
354
+ For scoped resource ARNs, use \`Sub\` in the policy constant:
355
+
356
+ \`\`\`typescript
357
+ // defaults.ts
358
+ export const bucketWritePolicy: PolicyDocument = {
359
+ Statement: [{
360
+ Effect: "Allow",
361
+ Action: ["s3:PutObject"],
362
+ Resource: Sub\`arn:aws:s3:::\${AWS.StackName}-data/*\`,
363
+ }],
364
+ };
365
+ \`\`\`
366
+
367
+ The \`IamPolicyPrincipal\` type supports all principal forms — wildcard (\`"*"\`), AWS accounts, services, and federated providers:
368
+
369
+ \`\`\`typescript
370
+ // Wildcard principal
371
+ Principal: "*",
372
+
373
+ // Service principal
374
+ Principal: { Service: "lambda.amazonaws.com" },
375
+
376
+ // Cross-account
377
+ Principal: { AWS: "arn:aws:iam::123456789012:root" },
378
+
379
+ // Multiple services
380
+ Principal: { Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] },
381
+ \`\`\`
382
+
383
+ ## Conditions
384
+
385
+ Use the \`If\` intrinsic for conditional values within resource properties:
386
+
387
+ \`\`\`typescript
388
+ import { If } from "@intentius/chant-lexicon-aws";
389
+
390
+ export const bucket = new Bucket({
391
+ bucketName: If("IsProduction", "prod-data", "dev-data"),
392
+ });
393
+ \`\`\`
394
+
395
+ CloudFormation \`Conditions\` blocks are recognized by the serializer when importing existing templates. For new stacks, use TypeScript logic for build-time decisions and \`If\` for deploy-time decisions.
396
+
397
+ ## Mappings
398
+
399
+ CloudFormation Mappings are a static lookup mechanism. In chant, use TypeScript objects instead — they're evaluated at build time and produce the same result:
400
+
401
+ \`\`\`typescript
402
+ const regionAMIs: Record<string, string> = {
403
+ "us-east-1": "ami-12345678",
404
+ "us-west-2": "ami-87654321",
405
+ "eu-west-1": "ami-abcdef01",
406
+ };
407
+
408
+ // Use directly in resource properties
409
+ export const server = new Instance({
410
+ imageId: regionAMIs["us-east-1"],
411
+ instanceType: "t3.micro",
412
+ });
413
+ \`\`\`
414
+
415
+ For deploy-time region lookups, combine \`AWS.Region\` with \`If\` or use \`Fn::Sub\` with SSM parameter store references.
416
+
417
+ ## Nested stacks
418
+
419
+ CloudFormation nested stacks (\`AWS::CloudFormation::Stack\`) let you decompose large templates into smaller, reusable child templates. Use \`nestedStack()\` to reference a child project directory — a subdirectory with its own barrel file that builds independently:
420
+
421
+ \`\`\`typescript
422
+ // Child project declares outputs with stackOutput()
423
+ // src/network/outputs.ts
424
+ export const vpcId = stackOutput(_.$.vpc.vpcId);
425
+ export const subnetId = stackOutput(_.$.subnet.subnetId);
426
+ export const lambdaSgId = stackOutput(_.$.lambdaSg.groupId);
427
+
428
+ // Parent references the child project
429
+ // src/app.ts
430
+ const network = _.nestedStack("network", import.meta.dir + "/network");
431
+
432
+ export const handler = new _.Function({
433
+ vpcConfig: {
434
+ subnetIds: [network.outputs.subnetId], // cross-stack ref
435
+ securityGroupIds: [network.outputs.lambdaSgId],
436
+ },
437
+ });
438
+ \`\`\`
439
+
440
+ chant handles the wiring: child template gets an \`Outputs\` section, parent uses \`Fn::GetAtt\` on the stack resource. A \`TemplateBasePath\` parameter lets you configure child template URLs per environment.
441
+
442
+ See [Nested Stacks](./nested-stacks) for the full guide.
443
+
444
+ ## Tagging
445
+
446
+ Tags are standard CloudFormation \`Key\`/\`Value\` arrays. Pass them on any resource that supports tagging:
447
+
448
+ \`\`\`typescript
449
+ export const bucket = new Bucket({
450
+ bucketName: "my-bucket",
451
+ tags: [
452
+ { key: "Environment", value: "production" },
453
+ { key: "Team", value: "platform" },
454
+ ],
455
+ });
456
+ \`\`\`
457
+
458
+ To apply tags across all members of a composite, use [\`propagate\`](./composites#propagate--shared-properties):
459
+
460
+ \`\`\`typescript
461
+ import { propagate } from "@intentius/chant";
462
+
463
+ export const api = propagate(
464
+ LambdaApi({ name: "myApi", code: lambdaCode }),
465
+ { tags: [{ key: "env", value: "prod" }] },
466
+ );
467
+ \`\`\``,
468
+ },
469
+ {
470
+ slug: "intrinsics",
471
+ title: "Intrinsic Functions",
472
+ description: "CloudFormation intrinsic functions and their chant syntax",
473
+ content: `CloudFormation intrinsic functions are available as imports from the lexicon. They produce the corresponding \`Fn::\` calls in the serialized template.
474
+
475
+ \`\`\`typescript
476
+ import { Sub, Ref, GetAtt, If, Join, Select, Split, Base64, AWS } from "@intentius/chant-lexicon-aws";
477
+ \`\`\`
478
+
479
+ ## \`Sub\` — string substitution
480
+
481
+ Tagged template literal that produces \`Fn::Sub\`. The most common intrinsic — use it for dynamic naming with pseudo-parameters and attribute references:
482
+
483
+ \`\`\`typescript
484
+ // Simple pseudo-parameter substitution
485
+ const bucketName = Sub\`\${AWS.StackName}-data\`;
486
+ // → { "Fn::Sub": "\${AWS::StackName}-data" }
487
+
488
+ // Multiple pseudo-parameters
489
+ const arn = Sub\`arn:aws:s3:::\${AWS.AccountId}:\${AWS.Region}:*\`;
490
+
491
+ // With resource attribute references
492
+ const url = Sub\`https://\${bucket.domainName}/path\`;
493
+ // → { "Fn::Sub": "https://\${DataBucket.DomainName}/path" }
494
+ \`\`\`
495
+
496
+ \`Sub\` is a tagged template — use it with backticks, not as a function call.
497
+
498
+ ## \`Ref\` — resource and parameter references
499
+
500
+ References a resource's physical ID or a parameter's value:
501
+
502
+ \`\`\`typescript
503
+ // Reference a parameter
504
+ const envRef = Ref("Environment");
505
+ // → { "Ref": "Environment" }
506
+
507
+ // Reference a resource (returns its physical ID)
508
+ const bucketRef = Ref("DataBucket");
509
+ // → { "Ref": "DataBucket" }
510
+ \`\`\`
511
+
512
+ In most cases you don't need \`Ref\` directly — the serializer automatically generates \`Ref\` when you reference a resource via the barrel (e.g. \`_.$.dataBucket\`).
513
+
514
+ ## \`GetAtt\` — resource attributes
515
+
516
+ Retrieves an attribute from a resource:
517
+
518
+ \`\`\`typescript
519
+ // Explicit GetAtt
520
+ const bucketArn = GetAtt("DataBucket", "Arn");
521
+ // → { "Fn::GetAtt": ["DataBucket", "Arn"] }
522
+ \`\`\`
523
+
524
+ **Preferred:** Use AttrRef directly via the resource's typed properties. When you write \`$.dataBucket.arn\`, the serializer automatically emits \`Fn::GetAtt\`. Explicit \`GetAtt\` is only needed for dynamic or imported resource names.
525
+
526
+ ## \`If\` — conditional values
527
+
528
+ Returns one of two values based on a condition:
529
+
530
+ \`\`\`typescript
531
+ const value = If("IsProduction", "prod-value", "dev-value");
532
+ // → { "Fn::If": ["IsProduction", "prod-value", "dev-value"] }
533
+ \`\`\`
534
+
535
+ Use with \`AWS.NoValue\` to conditionally omit a property:
536
+
537
+ \`\`\`typescript
538
+ export const bucket = new Bucket({
539
+ bucketName: "my-bucket",
540
+ accelerateConfiguration: If("EnableAcceleration",
541
+ { accelerationStatus: "Enabled" },
542
+ AWS.NoValue,
543
+ ),
544
+ });
545
+ \`\`\`
546
+
547
+ ## \`Join\` — join values
548
+
549
+ Joins values with a delimiter:
550
+
551
+ \`\`\`typescript
552
+ const joined = Join("-", ["prefix", AWS.StackName, "suffix"]);
553
+ // → { "Fn::Join": ["-", ["prefix", { "Ref": "AWS::StackName" }, "suffix"]] }
554
+ \`\`\`
555
+
556
+ ## \`Select\` — select by index
557
+
558
+ Selects a value from a list by index:
559
+
560
+ \`\`\`typescript
561
+ const first = Select(0, Split(",", "a,b,c"));
562
+ // → { "Fn::Select": [0, { "Fn::Split": [",", "a,b,c"] }] }
563
+ \`\`\`
564
+
565
+ ## \`Split\` — split string
566
+
567
+ Splits a string by a delimiter:
568
+
569
+ \`\`\`typescript
570
+ const parts = Split(",", "a,b,c");
571
+ // → { "Fn::Split": [",", "a,b,c"] }
572
+ \`\`\`
573
+
574
+ ## \`Base64\` — encode to Base64
575
+
576
+ Encodes a string to Base64, commonly used for EC2 user data:
577
+
578
+ \`\`\`typescript
579
+ const userData = Base64(Sub\`#!/bin/bash
580
+ echo "Stack: \${AWS.StackName}"
581
+ yum update -y
582
+ \`);
583
+ // → { "Fn::Base64": { "Fn::Sub": "..." } }
584
+ \`\`\``,
585
+ },
586
+ {
587
+ slug: "composites",
588
+ title: "Composites",
589
+ description: "Composite resources, withDefaults presets, and propagate in the AWS CloudFormation lexicon",
590
+ content: `Composites group related resources into reusable factories. See also the core [Composite Resources](/guide/composite-resources/) guide.
591
+
592
+ \`\`\`typescript
593
+ import * as _ from "./_";
594
+
595
+ export const LambdaApi = _.Composite<LambdaApiProps>((props) => {
596
+ const role = new _.Role({
597
+ assumeRolePolicyDocument: _.$.lambdaTrustPolicy,
598
+ managedPolicyArns: [_.$.lambdaBasicExecutionArn],
599
+ policies: props.policies,
600
+ });
601
+
602
+ const func = new _.Function({
603
+ functionName: props.name,
604
+ runtime: props.runtime,
605
+ handler: props.handler,
606
+ code: props.code,
607
+ role: role.arn,
608
+ timeout: props.timeout,
609
+ memorySize: props.memorySize,
610
+ });
611
+
612
+ const permission = new _.Permission({
613
+ functionName: func.arn,
614
+ action: "lambda:InvokeFunction",
615
+ principal: "apigateway.amazonaws.com",
616
+ });
617
+
618
+ return { role, func, permission };
619
+ }, "LambdaApi");
620
+ \`\`\`
621
+
622
+ Instantiate and export:
623
+
624
+ \`\`\`typescript
625
+ export const healthApi = LambdaApi({
626
+ name: Sub\`\${AWS.StackName}-health\`,
627
+ runtime: "nodejs20.x",
628
+ handler: "index.handler",
629
+ code: { zipFile: \`exports.handler = async () => ({ statusCode: 200 });\` },
630
+ });
631
+ \`\`\`
632
+
633
+ During build, composites expand to flat CloudFormation resources: \`healthApi_role\` → \`HealthApiRole\`, \`healthApi_func\` → \`HealthApiFunc\`, \`healthApi_permission\` → \`HealthApiPermission\`.
634
+
635
+ ## \`withDefaults\` — composite presets
636
+
637
+ Wrap a composite with pre-applied defaults. Defaulted props become optional:
638
+
639
+ \`\`\`typescript
640
+ import { withDefaults } from "@intentius/chant";
641
+
642
+ const SecureApi = withDefaults(LambdaApi, {
643
+ runtime: "nodejs20.x",
644
+ handler: "index.handler",
645
+ timeout: 10,
646
+ memorySize: 256,
647
+ });
648
+
649
+ // Only name and code are required now
650
+ export const healthApi = SecureApi({
651
+ name: Sub\`\${AWS.StackName}-health\`,
652
+ code: { zipFile: \`exports.handler = async () => ({ statusCode: 200 });\` },
653
+ });
654
+
655
+ // Composable — stack defaults on top of defaults
656
+ const HighMemoryApi = withDefaults(SecureApi, { memorySize: 2048, timeout: 25 });
657
+ \`\`\`
658
+
659
+ \`withDefaults\` preserves the original composite's identity — same \`_id\` and \`compositeName\`, no new registry entry.
660
+
661
+ ## \`propagate\` — shared properties
662
+
663
+ Attach properties that merge into every member during expansion:
664
+
665
+ \`\`\`typescript
666
+ import { propagate } from "@intentius/chant";
667
+
668
+ export const api = propagate(
669
+ LambdaApi({ name: "myApi", code: lambdaCode }),
670
+ { tags: [{ key: "env", value: "prod" }] },
671
+ );
672
+ // role, func, and permission all receive the env tag
673
+ \`\`\`
674
+
675
+ Merge semantics:
676
+ - **Scalars** — member-specific value wins over shared
677
+ - **Arrays** (e.g. tags) — shared values prepended, member values appended
678
+ - **\`undefined\`** — stripped from shared props, never overwrites
679
+
680
+ ## Nested stacks
681
+
682
+ When resources should produce a separate CloudFormation template instead of expanding into the parent, use a **child project** — a subdirectory with its own barrel file (\`_.ts\`) that builds independently. The parent references it with \`nestedStack()\`:
683
+
684
+ \`\`\`typescript
685
+ // src/app.ts — parent references child project directory
686
+ const network = _.nestedStack("network", import.meta.dir + "/network");
687
+
688
+ // Cross-stack reference via outputs proxy
689
+ export const handler = new _.Function({
690
+ vpcConfig: { subnetIds: [network.outputs.subnetId] },
691
+ });
692
+ \`\`\`
693
+
694
+ See [Nested Stacks](./nested-stacks) for the full guide.`,
695
+ },
696
+ {
697
+ slug: "nested-stacks",
698
+ title: "Nested Stacks",
699
+ description: "Splitting resources into child CloudFormation templates with automatic cross-stack reference wiring",
700
+ content: `CloudFormation nested stacks (\`AWS::CloudFormation::Stack\`) let you decompose large templates into smaller, reusable child templates. The AWS lexicon's \`nestedStack()\` function references a **child project directory** — a subdirectory with its own barrel file that builds independently to a valid CloudFormation template.
701
+
702
+ ## Project structure
703
+
704
+ A nested stack is a child project — a subdirectory with its own \`_.ts\` barrel, resource files, and explicit \`stackOutput()\` declarations:
705
+
706
+ \`\`\`
707
+ src/
708
+ _.ts # parent barrel
709
+ app.ts # parent resources
710
+ network/ # ← child project (nested stack)
711
+ _.ts # its own barrel
712
+ vpc.ts # VPC, subnet, internet gateway, routing
713
+ security.ts # security group for Lambda
714
+ outputs.ts # declares cross-stack outputs
715
+ \`\`\`
716
+
717
+ ## Declaring outputs in the child
718
+
719
+ Use \`stackOutput()\` to mark values that the parent can reference. Each \`stackOutput()\` becomes an entry in the child template's \`Outputs\` section:
720
+
721
+ \`\`\`typescript
722
+ // src/network/outputs.ts
723
+ import * as _ from "./_";
724
+ import { stackOutput } from "@intentius/chant";
725
+
726
+ export const vpcId = stackOutput(_.$.vpc.vpcId, { description: "VPC ID" });
727
+ export const subnetId = stackOutput(_.$.subnet.subnetId, { description: "Public subnet ID" });
728
+ export const lambdaSgId = stackOutput(_.$.lambdaSg.groupId, { description: "Lambda security group ID" });
729
+ \`\`\`
730
+
731
+ The child can be built independently:
732
+
733
+ \`\`\`bash
734
+ chant build src/network/ -o network.json
735
+ # Produces a standalone, valid CloudFormation template with Outputs
736
+ \`\`\`
737
+
738
+ ## Referencing from the parent
739
+
740
+ Use \`nestedStack()\` in the parent to reference a child project directory. It returns an object with an \`outputs\` proxy for cross-stack references:
741
+
742
+ \`\`\`typescript
743
+ // src/app.ts
744
+ import * as _ from "./_";
745
+
746
+ const network = _.nestedStack("network", import.meta.dir + "/network");
747
+
748
+ export const handler = new _.Function({
749
+ functionName: _.Sub\`\${_.AWS.StackName}-handler\`,
750
+ runtime: "nodejs20.x",
751
+ handler: "index.handler",
752
+ role: _.Ref("LambdaExecutionRole"),
753
+ code: { zipFile: "exports.handler = async () => ({ statusCode: 200 });" },
754
+ vpcConfig: {
755
+ subnetIds: [network.outputs.subnetId],
756
+ securityGroupIds: [network.outputs.lambdaSgId],
757
+ },
758
+ });
759
+ \`\`\`
760
+
761
+ \`network.outputs.subnetId\` produces a \`NestedStackOutputRef\` that serializes to \`{ "Fn::GetAtt": ["Network", "Outputs.SubnetId"] }\`.
762
+
763
+ ## Build output
764
+
765
+ \`chant build\` produces multiple template files:
766
+
767
+ \`\`\`bash
768
+ chant build -o template.json
769
+ # Produces:
770
+ # template.json — parent template
771
+ # network.template.json — child template
772
+ \`\`\`
773
+
774
+ The parent template includes an \`AWS::CloudFormation::Stack\` resource pointing to the child:
775
+
776
+ \`\`\`json
777
+ "Network": {
778
+ "Type": "AWS::CloudFormation::Stack",
779
+ "Properties": {
780
+ "TemplateURL": { "Fn::Sub": "\${TemplateBasePath}/network.template.json" }
781
+ }
782
+ }
783
+ \`\`\`
784
+
785
+ ## \`TemplateBasePath\` parameter
786
+
787
+ Every parent template gets a \`TemplateBasePath\` parameter (default \`"."\`) that controls where CloudFormation looks for child templates:
788
+
789
+ \`\`\`bash
790
+ # Local dev — default "." works with rain and similar tools
791
+ chant build -o template.json
792
+
793
+ # Production — override with S3 URL
794
+ aws cloudformation deploy \\
795
+ --template-file template.json \\
796
+ --stack-name my-stack \\
797
+ --parameter-overrides TemplateBasePath=https://my-bucket.s3.amazonaws.com/templates
798
+ \`\`\`
799
+
800
+ Child templates also receive the \`TemplateBasePath\` parameter so it propagates through all nesting levels.
801
+
802
+ ## Explicit parameters
803
+
804
+ Pass CloudFormation Parameters to child stacks with the \`parameters\` option:
805
+
806
+ \`\`\`typescript
807
+ const network = _.nestedStack("network", import.meta.dir + "/network", {
808
+ parameters: { Environment: "prod", CidrBlock: "10.0.0.0/16" },
809
+ });
810
+ \`\`\`
811
+
812
+ ## Recursive nesting
813
+
814
+ Child projects can themselves reference grandchild projects. Each level produces its own template file:
815
+
816
+ \`\`\`
817
+ src/
818
+ _.ts
819
+ app.ts
820
+ infra/
821
+ _.ts
822
+ network/
823
+ _.ts
824
+ vpc.ts
825
+ outputs.ts
826
+ database/
827
+ _.ts
828
+ cluster.ts
829
+ outputs.ts
830
+ \`\`\`
831
+
832
+ The build pipeline detects circular references and reports an error if child A references child B which references child A.
833
+
834
+ ## Lint rules
835
+
836
+ Three lint rules help catch common nested stack issues:
837
+
838
+ | Rule | Severity | Description |
839
+ |------|----------|-------------|
840
+ | **WAW013** | error | Child project has no \`stackOutput()\` exports — parent can't reference anything |
841
+ | **WAW014** | warning | \`nestedStack()\` outputs never referenced from parent — could be a separate build |
842
+ | **WAW015** | error | Circular project references |
843
+
844
+ ## When to use nested stacks
845
+
846
+ **Use nested stacks when:**
847
+ - Your template exceeds CloudFormation's 500-resource limit
848
+ - You want to reuse a group of resources across multiple parent stacks
849
+ - You need independent update/rollback boundaries for parts of your infrastructure
850
+
851
+ **Use flat composites when:**
852
+ - Resources are tightly coupled and always deploy together
853
+ - You don't need independent update boundaries
854
+ - Your template is within resource limits
855
+
856
+ See [Composites](./composites) for the flat composite approach, and [Examples](./examples#nested-stacks) for a runnable nested stack example.`,
857
+ },
858
+ {
859
+ slug: "lint-rules",
860
+ title: "Lint Rules",
861
+ description: "Built-in lint rules and post-synth checks for AWS CloudFormation",
862
+ content: `The AWS lexicon ships lint rules that run during \`chant lint\` and post-synth checks that validate the serialized CloudFormation output after \`chant build\`.
863
+
864
+ ## Lint rules
865
+
866
+ Lint rules analyze your TypeScript source code before build.
867
+
868
+ ### WAW001 — Hardcoded Region
869
+
870
+ **Severity:** warning | **Category:** security
871
+
872
+ Flags hardcoded AWS region strings like \`us-east-1\`. Use \`AWS.Region\` instead so templates are portable across regions.
873
+
874
+ \`\`\`typescript
875
+ // Triggers WAW001
876
+ const endpoint = "s3.us-east-1.amazonaws.com";
877
+
878
+ // Fixed
879
+ const endpoint = Sub\`s3.\${AWS.Region}.amazonaws.com\`;
880
+ \`\`\`
881
+
882
+ ### WAW006 — S3 Bucket Encryption
883
+
884
+ **Severity:** warning | **Category:** security
885
+
886
+ Flags S3 buckets that don't configure server-side encryption. AWS recommends enabling encryption on all buckets.
887
+
888
+ \`\`\`typescript
889
+ // Triggers WAW006
890
+ export const bucket = new Bucket({ bucketName: "my-bucket" });
891
+
892
+ // Fixed — add encryption configuration
893
+ export const bucket = new Bucket({
894
+ bucketName: "my-bucket",
895
+ bucketEncryption: {
896
+ serverSideEncryptionConfiguration: [{
897
+ serverSideEncryptionByDefault: { sseAlgorithm: "AES256" },
898
+ }],
899
+ },
900
+ });
901
+ \`\`\`
902
+
903
+ ### WAW009 — IAM Wildcard Resource
904
+
905
+ **Severity:** warning | **Category:** security
906
+
907
+ Flags IAM policy statements that use \`"Resource": "*"\`. Prefer scoped resource ARNs following the principle of least privilege.
908
+
909
+ \`\`\`typescript
910
+ // Triggers WAW009
911
+ { Effect: "Allow", Action: ["s3:GetObject"], Resource: "*" }
912
+
913
+ // Fixed — scope to a specific bucket
914
+ { Effect: "Allow", Action: ["s3:GetObject"], Resource: Sub\`arn:aws:s3:::\${AWS.StackName}-data/*\` }
915
+ \`\`\`
916
+
917
+ IAM policy documents use PascalCase keys (\`Effect\`, \`Action\`, \`Resource\`) matching the IAM JSON Policy Language spec. The \`PolicyDocument\` and \`IamPolicyStatement\` types provide full autocomplete for these fields.
918
+
919
+ ## Post-synth checks
920
+
921
+ Post-synth checks run against the serialized CloudFormation JSON after build. They catch issues that are only visible in the final template.
922
+
923
+ ### COR020 — Circular Resource Dependency
924
+
925
+ Detects cycles in the resource dependency graph built from \`Ref\`, \`Fn::GetAtt\`, and \`DependsOn\` entries. Circular dependencies cause CloudFormation deployments to fail.
926
+
927
+ ### EXT001 — Extension Constraint Violation
928
+
929
+ Validates cross-property constraints from CloudFormation's cfn-lint extension schemas. For example, an EC2 instance might require \`SubnetId\` when \`NetworkInterfaces\` is not set.
930
+
931
+ ### WAW010 — Redundant DependsOn
932
+
933
+ Flags \`DependsOn\` entries where the target resource is already referenced via \`Ref\` or \`Fn::GetAtt\` in the resource's properties. CloudFormation automatically creates dependencies for these references, making the explicit \`DependsOn\` unnecessary.
934
+
935
+ ### WAW011 — Deprecated Lambda Runtime
936
+
937
+ Flags Lambda functions using deprecated or approaching-end-of-life runtimes (e.g. \`nodejs16.x\`, \`python3.8\`). Using deprecated runtimes prevents function updates and may cause deployment failures.
938
+
939
+ ### WAW013 — No Stack Outputs
940
+
941
+ **Severity:** error | **Category:** correctness
942
+
943
+ Flags child projects (nested stacks) that have no \`stackOutput()\` exports. Without outputs, the parent stack can't reference any values from the child — either add \`stackOutput()\` declarations or remove the \`nestedStack()\` reference.
944
+
945
+ ### WAW014 — Unreferenced Stack Outputs
946
+
947
+ **Severity:** warning | **Category:** style
948
+
949
+ Flags \`nestedStack()\` references whose outputs are never used from the parent. If no cross-stack references exist, the child project could just be built and deployed independently.
950
+
951
+ ### WAW015 — Circular Project References
952
+
953
+ **Severity:** error | **Category:** correctness
954
+
955
+ Detects circular references between child projects (e.g. project A references project B which references project A). Circular project dependencies cause infinite build recursion.
956
+
957
+ ## Running lint
958
+
959
+ \`\`\`bash
960
+ # Lint your chant project
961
+ chant lint
962
+
963
+ # Lint with auto-fix where supported
964
+ chant lint --fix
965
+ \`\`\`
966
+
967
+ To suppress a rule on a specific line:
968
+
969
+ \`\`\`typescript
970
+ // chant-disable-next-line WAW001
971
+ const endpoint = "s3.us-east-1.amazonaws.com";
972
+ \`\`\`
973
+
974
+ To suppress globally in \`chant.config.ts\`:
975
+
976
+ \`\`\`typescript
977
+ export default {
978
+ lint: {
979
+ rules: {
980
+ WAW001: "off",
981
+ },
982
+ },
983
+ };
984
+ \`\`\`
985
+
986
+ See also [Custom Lint Rules](./custom-rules) for writing project-specific rules.`,
987
+ },
988
+ {
989
+ slug: "custom-rules",
990
+ title: "Custom Lint Rules",
991
+ description: "Writing and registering project-specific lint rules for AWS CloudFormation",
992
+ content: `Chant's lint engine runs TypeScript AST visitors. Write project-specific rules that enforce domain conventions.
993
+
994
+ ## Anatomy of a lint rule
995
+
996
+ \`\`\`typescript
997
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
998
+ import * as ts from "typescript";
999
+
1000
+ export const apiTimeoutRule: LintRule = {
1001
+ id: "WAW012", // unique ID (WAW = AWS-specific prefix)
1002
+ severity: "error", // "error" | "warning"
1003
+ category: "correctness", // "correctness" | "style" | "security"
1004
+
1005
+ check(context: LintContext): LintDiagnostic[] {
1006
+ const { sourceFile } = context;
1007
+ const diagnostics: LintDiagnostic[] = [];
1008
+
1009
+ function visit(node: ts.Node): void {
1010
+ // Walk the AST looking for violations
1011
+ if (ts.isCallExpression(node)) {
1012
+ // Inspect arguments, report diagnostics
1013
+ }
1014
+ ts.forEachChild(node, visit);
1015
+ }
1016
+
1017
+ visit(sourceFile);
1018
+ return diagnostics;
1019
+ },
1020
+ };
1021
+ \`\`\`
1022
+
1023
+ ## Example: API Gateway timeout (WAW012)
1024
+
1025
+ The advanced example includes a rule that flags Lambda API composites with \`timeout > 29\` — API Gateway's synchronous limit:
1026
+
1027
+ \`\`\`typescript
1028
+ const API_FACTORIES = new Set(["LambdaApi", "SecureApi", "HighMemoryApi"]);
1029
+
1030
+ export const apiTimeoutRule: LintRule = {
1031
+ id: "WAW012",
1032
+ severity: "error",
1033
+ category: "correctness",
1034
+
1035
+ check(context: LintContext): LintDiagnostic[] {
1036
+ // Walks AST for calls to API factory functions,
1037
+ // inspects the timeout property, reports if > 29
1038
+ },
1039
+ };
1040
+ \`\`\`
1041
+
1042
+ ## Registering custom rules
1043
+
1044
+ Add a \`chant.config.ts\` to your project:
1045
+
1046
+ \`\`\`typescript
1047
+ export default {
1048
+ lint: {
1049
+ extends: ["@intentius/chant/lint/presets/strict"],
1050
+ rules: {
1051
+ COR004: "off", // disable a built-in rule
1052
+ },
1053
+ plugins: ["./lint/api-timeout.ts"], // load custom rules
1054
+ },
1055
+ };
1056
+ \`\`\`
1057
+
1058
+ The \`plugins\` array accepts relative paths. Each plugin module should export a \`LintRule\` object.`,
1059
+ },
1060
+ {
1061
+ slug: "examples",
1062
+ title: "Examples",
1063
+ description: "Walkthrough of the getting-started and advanced AWS CloudFormation examples",
1064
+ content: `Two runnable examples live in the lexicon's \`examples/\` directory. Clone the repo and try them:
1065
+
1066
+ \`\`\`bash
1067
+ cd examples/getting-started
1068
+ bun install
1069
+ chant build # produces CloudFormation JSON
1070
+ chant lint # runs lint rules
1071
+ bun test # runs the example's tests
1072
+ \`\`\`
1073
+
1074
+ ## Getting Started
1075
+
1076
+ \`examples/getting-started/\` — 4 resources across separate files: two S3 buckets, an IAM role, and a Lambda function.
1077
+
1078
+ \`\`\`
1079
+ src/
1080
+ ├── _.ts # Barrel — re-exports lexicon + auto-discovers siblings
1081
+ ├── defaults.ts # Shared config: encryption, versioning, public access block
1082
+ ├── data-bucket.ts # S3 bucket using barrel defaults
1083
+ ├── logs-bucket.ts # S3 bucket for access logs
1084
+ ├── role.ts # IAM role with Lambda assume-role policy
1085
+ └── handler.ts # Lambda function referencing role and bucket
1086
+ \`\`\`
1087
+
1088
+ **Patterns demonstrated:**
1089
+
1090
+ 1. **Barrel file** — \`_.ts\` re-exports the AWS lexicon and creates the \`$\` proxy for cross-file references
1091
+ 2. **Shared defaults** — \`defaults.ts\` exports reusable property objects (\`encryptionDefault\`, \`publicAccessBlock\`) that other files reference via \`_.$\`
1092
+ 3. **Cross-resource references** — \`_.$.dataBucket.arn\` in \`handler.ts\` serializes to \`Fn::GetAtt\` in the template
1093
+ 4. **Intrinsics** — \`Sub\` tagged templates with pseudo-parameters for dynamic naming
1094
+
1095
+ \`\`\`typescript
1096
+ // handler.ts — Lambda function referencing other resources
1097
+ import * as _ from "./_";
1098
+
1099
+ const lambdaCode = { zipFile: "exports.handler = async () => ({ statusCode: 200 });" };
1100
+
1101
+ export const handler = new _.Function({
1102
+ functionName: _.Sub\`\${_.AWS.StackName}-handler\`,
1103
+ handler: "index.handler",
1104
+ runtime: "nodejs20.x",
1105
+ role: _.$.functionRole.arn, // → Fn::GetAtt
1106
+ code: lambdaCode,
1107
+ environment: {
1108
+ variables: { BUCKET_ARN: _.$.dataBucket.arn }, // → Fn::GetAtt
1109
+ },
1110
+ });
1111
+ \`\`\`
1112
+
1113
+ ## Advanced
1114
+
1115
+ \`examples/advanced/\` — builds on getting-started with composites, presets, inline IAM policies, and a custom lint rule.
1116
+
1117
+ \`\`\`
1118
+ src/
1119
+ ├── _.ts # Barrel + re-exports Composite from core
1120
+ ├── chant.config.ts # Lint config: strict preset + custom plugin
1121
+ ├── defaults.ts # Encryption, versioning, access block, Lambda trust policy
1122
+ ├── data-bucket.ts # S3 bucket
1123
+ ├── lambda-api.ts # Composite factory + SecureApi/HighMemoryApi presets
1124
+ ├── health-api.ts # SecureApi — minimal health check
1125
+ ├── upload-api.ts # SecureApi + S3 PutObject policy
1126
+ ├── process-api.ts # HighMemoryApi + S3 read/write policy
1127
+ └── lint/
1128
+ └── api-timeout.ts # Custom WAW012 rule
1129
+ \`\`\`
1130
+
1131
+ **What it adds:**
1132
+
1133
+ - **Composites** — \`LambdaApi\` groups Role + Function + Permission into a reusable unit (see [Composites](./composites))
1134
+ - **Composite presets** — \`SecureApi\` (low memory, short timeout) and \`HighMemoryApi\` (high memory, longer timeout) created with \`withDefaults\`
1135
+ - **Inline IAM policies** — \`upload-api.ts\` and \`process-api.ts\` attach \`Role_Policy\` objects for scoped S3 access
1136
+ - **Custom lint rule** — \`api-timeout.ts\` enforces API Gateway's 29-second timeout limit (see [Custom Lint Rules](./custom-rules))
1137
+ - **Lint config** — \`chant.config.ts\` extends the strict preset and loads the custom plugin
1138
+
1139
+ The example produces 10 CloudFormation resources: 1 S3 bucket + 3 composites × 3 members each.
1140
+
1141
+ ## Nested Stacks
1142
+
1143
+ \`examples/nested-stacks/\` — demonstrates child projects for splitting resources into child CloudFormation templates with automatic cross-stack reference wiring.
1144
+
1145
+ \`\`\`
1146
+ src/
1147
+ ├── _.ts # Parent barrel
1148
+ ├── app.ts # Lambda function (references network outputs)
1149
+ └── network/ # Child project (nested stack)
1150
+ ├── _.ts # Child barrel
1151
+ ├── vpc.ts # VPC, subnet, internet gateway, route table
1152
+ ├── security.ts # Security group for Lambda
1153
+ └── outputs.ts # stackOutput() declarations
1154
+ \`\`\`
1155
+
1156
+ **Patterns demonstrated:**
1157
+
1158
+ 1. **Child project** — \`network/\` is a separate project directory with its own barrel, resources, and \`stackOutput()\` exports
1159
+ 2. **Cross-stack references** — \`app.ts\` accesses \`network.outputs.subnetId\` and \`network.outputs.lambdaSgId\`, which serialize to \`Fn::GetAtt\` on the parent's \`AWS::CloudFormation::Stack\` resource
1160
+ 3. **Multi-file output** — build produces \`template.json\` (parent) and \`network.template.json\` (child)
1161
+ 4. **TemplateBasePath** — auto-generated parameter for configuring child template URLs per environment
1162
+
1163
+ \`\`\`typescript
1164
+ // network/outputs.ts — child declares what the parent can reference
1165
+ import * as _ from "./_";
1166
+ import { stackOutput } from "@intentius/chant";
1167
+
1168
+ export const vpcId = stackOutput(_.$.vpc.vpcId, { description: "VPC ID" });
1169
+ export const subnetId = stackOutput(_.$.subnet.subnetId, { description: "Public subnet ID" });
1170
+ export const lambdaSgId = stackOutput(_.$.lambdaSg.groupId, { description: "Lambda security group ID" });
1171
+ \`\`\`
1172
+
1173
+ \`\`\`typescript
1174
+ // app.ts — parent references child project
1175
+ import * as _ from "./_";
1176
+
1177
+ const network = _.nestedStack("network", import.meta.dir + "/network");
1178
+
1179
+ export const handler = new _.Function({
1180
+ functionName: _.Sub\`\${_.AWS.StackName}-handler\`,
1181
+ runtime: "nodejs20.x",
1182
+ handler: "index.handler",
1183
+ role: _.Ref("LambdaExecutionRole"),
1184
+ code: { zipFile: "exports.handler = async () => ({ statusCode: 200 });" },
1185
+ vpcConfig: {
1186
+ subnetIds: [network.outputs.subnetId],
1187
+ securityGroupIds: [network.outputs.lambdaSgId],
1188
+ },
1189
+ });
1190
+ \`\`\`
1191
+
1192
+ See [Nested Stacks](./nested-stacks) for the full guide.`,
1193
+ },
1194
+ ],
1195
+ };
1196
+
1197
+ log("Generating AWS documentation...");
1198
+ const result = docsPipeline(config);
1199
+
1200
+ log(`Writing standalone docs site to ${outDir}`);
1201
+ writeDocsSite(config, result);
1202
+
1203
+ console.error(
1204
+ `Docs generated: ${result.stats.resources} resources, ${result.stats.services} services, ${result.stats.rules} rules`,
1205
+ );
1206
+ }