@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,4 @@
1
+ /**
2
+ * Runtime factory constructors — re-exported from core.
3
+ */
4
+ export { createResource, createProperty } from "@intentius/chant/runtime";
@@ -0,0 +1,181 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { CFGenerator } from "./generator";
3
+ import type { TemplateIR } from "@intentius/chant/import/parser";
4
+
5
+ describe("CFGenerator", () => {
6
+ const generator = new CFGenerator();
7
+
8
+ test("generates empty template", () => {
9
+ const ir: TemplateIR = {
10
+ parameters: [],
11
+ resources: [],
12
+ };
13
+
14
+ const files = generator.generate(ir);
15
+
16
+ expect(files).toHaveLength(1);
17
+ expect(files[0].path).toBe("main.ts");
18
+ });
19
+
20
+ test("generates S3 Bucket", () => {
21
+ const ir: TemplateIR = {
22
+ parameters: [],
23
+ resources: [
24
+ {
25
+ logicalId: "MyBucket",
26
+ type: "AWS::S3::Bucket",
27
+ properties: {
28
+ BucketName: "my-bucket",
29
+ },
30
+ },
31
+ ],
32
+ };
33
+
34
+ const files = generator.generate(ir);
35
+
36
+ expect(files[0].content).toContain("import { Bucket }");
37
+ expect(files[0].content).toContain("export const myBucket = new Bucket({");
38
+ expect(files[0].content).toContain('bucketName: "my-bucket"');
39
+ });
40
+
41
+ test("generates Lambda Function", () => {
42
+ const ir: TemplateIR = {
43
+ parameters: [],
44
+ resources: [
45
+ {
46
+ logicalId: "MyFunction",
47
+ type: "AWS::Lambda::Function",
48
+ properties: {
49
+ FunctionName: "my-function",
50
+ Runtime: "nodejs18.x",
51
+ Handler: "index.handler",
52
+ },
53
+ },
54
+ ],
55
+ };
56
+
57
+ const files = generator.generate(ir);
58
+
59
+ expect(files[0].content).toContain("import { Function }");
60
+ expect(files[0].content).toContain("export const myFunction = new Function({");
61
+ expect(files[0].content).toContain('functionName: "my-function"');
62
+ expect(files[0].content).toContain('runtime: "nodejs18.x"');
63
+ });
64
+
65
+ test("generates Ref as variable reference", () => {
66
+ const ir: TemplateIR = {
67
+ parameters: [{ name: "BucketName", type: "String" }],
68
+ resources: [
69
+ {
70
+ logicalId: "MyBucket",
71
+ type: "AWS::S3::Bucket",
72
+ properties: {
73
+ BucketName: { __intrinsic: "Ref", name: "BucketName" },
74
+ },
75
+ },
76
+ ],
77
+ };
78
+
79
+ const files = generator.generate(ir);
80
+
81
+ expect(files[0].content).toContain("bucketName: bucketName");
82
+ });
83
+
84
+ test("generates GetAtt as property access", () => {
85
+ const ir: TemplateIR = {
86
+ parameters: [],
87
+ resources: [
88
+ {
89
+ logicalId: "SourceBucket",
90
+ type: "AWS::S3::Bucket",
91
+ properties: {},
92
+ },
93
+ {
94
+ logicalId: "DestBucket",
95
+ type: "AWS::S3::Bucket",
96
+ properties: {
97
+ SourceArn: { __intrinsic: "GetAtt", logicalId: "SourceBucket", attribute: "Arn" },
98
+ },
99
+ },
100
+ ],
101
+ };
102
+
103
+ const files = generator.generate(ir);
104
+
105
+ expect(files[0].content).toContain("sourceArn: sourceBucket.arn");
106
+ });
107
+
108
+ test("generates Sub as tagged template", () => {
109
+ const ir: TemplateIR = {
110
+ parameters: [],
111
+ resources: [
112
+ {
113
+ logicalId: "MyBucket",
114
+ type: "AWS::S3::Bucket",
115
+ properties: {
116
+ BucketName: { __intrinsic: "Sub", template: "${AWS::StackName}-bucket" },
117
+ },
118
+ },
119
+ ],
120
+ };
121
+
122
+ const files = generator.generate(ir);
123
+
124
+ expect(files[0].content).toContain("Sub");
125
+ expect(files[0].content).toContain("AWS.StackName");
126
+ });
127
+
128
+ test("generates If intrinsic", () => {
129
+ const ir: TemplateIR = {
130
+ parameters: [],
131
+ resources: [
132
+ {
133
+ logicalId: "MyBucket",
134
+ type: "AWS::S3::Bucket",
135
+ properties: {
136
+ BucketName: {
137
+ __intrinsic: "If",
138
+ condition: "CreateProd",
139
+ valueIfTrue: "prod-bucket",
140
+ valueIfFalse: "dev-bucket",
141
+ },
142
+ },
143
+ },
144
+ ],
145
+ };
146
+
147
+ const files = generator.generate(ir);
148
+
149
+ expect(files[0].content).toContain('If("CreateProd"');
150
+ expect(files[0].content).toContain('"prod-bucket"');
151
+ expect(files[0].content).toContain('"dev-bucket"');
152
+ });
153
+
154
+ test("orders resources by dependencies", () => {
155
+ const ir: TemplateIR = {
156
+ parameters: [],
157
+ resources: [
158
+ {
159
+ logicalId: "DependentBucket",
160
+ type: "AWS::S3::Bucket",
161
+ properties: {
162
+ SourceArn: { __intrinsic: "GetAtt", logicalId: "SourceBucket", attribute: "Arn" },
163
+ },
164
+ },
165
+ {
166
+ logicalId: "SourceBucket",
167
+ type: "AWS::S3::Bucket",
168
+ properties: {},
169
+ },
170
+ ],
171
+ };
172
+
173
+ const files = generator.generate(ir);
174
+ const content = files[0].content;
175
+
176
+ const sourcePos = content.indexOf("sourceBucket");
177
+ const depPos = content.indexOf("dependentBucket");
178
+
179
+ expect(sourcePos).toBeLessThan(depPos);
180
+ });
181
+ });
@@ -0,0 +1,349 @@
1
+ import type { TemplateIR, ResourceIR, ParameterIR } from "@intentius/chant/import/parser";
2
+ import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
3
+ import { topoSort } from "@intentius/chant/codegen/topo-sort";
4
+ import { hasIntrinsicInValue, irUsesIntrinsic, collectDependencies } from "@intentius/chant/import/ir-utils";
5
+
6
+ /**
7
+ * TypeScript code generator for CloudFormation templates
8
+ */
9
+ export class CFGenerator implements TypeScriptGenerator {
10
+ /**
11
+ * Generate TypeScript files from intermediate representation
12
+ */
13
+ generate(ir: TemplateIR): GeneratedFile[] {
14
+ const lines: string[] = [];
15
+
16
+ // Generate imports
17
+ lines.push(this.generateImports(ir));
18
+ lines.push("");
19
+
20
+ // Generate parameters
21
+ for (const param of ir.parameters) {
22
+ lines.push(this.generateParameter(param));
23
+ }
24
+
25
+ if (ir.parameters.length > 0) {
26
+ lines.push("");
27
+ }
28
+
29
+ // Generate resources in dependency order
30
+ const sortedResources = this.sortByDependencies(ir.resources);
31
+ for (const resource of sortedResources) {
32
+ lines.push(this.generateResource(resource, ir));
33
+ }
34
+
35
+ return [
36
+ {
37
+ path: "main.ts",
38
+ content: lines.join("\n") + "\n",
39
+ },
40
+ ];
41
+ }
42
+
43
+ /**
44
+ * Generate import statements
45
+ */
46
+ private generateImports(ir: TemplateIR): string {
47
+ const imports: Set<string> = new Set();
48
+ const serviceImports: Map<string, Set<string>> = new Map();
49
+
50
+ // Collect what we need to import
51
+ const needsParameter = ir.parameters.length > 0;
52
+ if (needsParameter) {
53
+ imports.add("Parameter");
54
+ }
55
+
56
+ // 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");
66
+
67
+ // Check for AWS pseudo-parameters
68
+ if (this.needsAWSPseudo(ir)) {
69
+ imports.add("AWS");
70
+ }
71
+
72
+ // Collect service imports
73
+ for (const resource of ir.resources) {
74
+ const { service, resourceClass } = this.parseResourceType(resource.type);
75
+ if (!serviceImports.has(service)) {
76
+ serviceImports.set(service, new Set());
77
+ }
78
+ serviceImports.get(service)!.add(resourceClass);
79
+ }
80
+
81
+ // Build import lines
82
+ const importLines: string[] = [];
83
+
84
+ // Merge service resource imports into the core imports set
85
+ for (const [_service, resources] of serviceImports) {
86
+ for (const r of resources) {
87
+ imports.add(r);
88
+ }
89
+ }
90
+
91
+ // All imports come from the flat @intentius/chant-lexicon-aws package
92
+ const allImports = [...imports];
93
+ if (allImports.length > 0) {
94
+ importLines.push(`import { ${allImports.join(", ")} } from "@intentius/chant-lexicon-aws";`);
95
+ }
96
+
97
+ return importLines.join("\n");
98
+ }
99
+
100
+ /**
101
+ * Parse AWS resource type into service and class names
102
+ */
103
+ private parseResourceType(type: string): { service: string; resourceClass: string } {
104
+ // AWS::S3::Bucket -> { service: "s3", resourceClass: "Bucket" }
105
+ const parts = type.split("::");
106
+ return {
107
+ service: parts[1]?.toLowerCase() ?? "unknown",
108
+ resourceClass: parts[2] ?? "Unknown",
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Check if AWS pseudo-parameters are used
114
+ */
115
+ private needsAWSPseudo(ir: TemplateIR): boolean {
116
+ for (const resource of ir.resources) {
117
+ if (this.hasAWSPseudo(resource.properties)) {
118
+ return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
124
+ /**
125
+ * Recursively check for AWS pseudo-parameter references
126
+ */
127
+ private hasAWSPseudo(value: unknown): boolean {
128
+ if (value === null || value === undefined) {
129
+ return false;
130
+ }
131
+
132
+ if (Array.isArray(value)) {
133
+ return value.some((item) => this.hasAWSPseudo(item));
134
+ }
135
+
136
+ if (typeof value === "object") {
137
+ const obj = value as Record<string, unknown>;
138
+ if (obj.__intrinsic === "Ref") {
139
+ const name = obj.name as string;
140
+ return name.startsWith("AWS::");
141
+ }
142
+ if (obj.__intrinsic === "Sub") {
143
+ const template = obj.template as string;
144
+ return template.includes("${AWS::");
145
+ }
146
+ return Object.values(obj).some((v) => this.hasAWSPseudo(v));
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * Sort resources by dependencies
154
+ */
155
+ private sortByDependencies(resources: ResourceIR[]): ResourceIR[] {
156
+ return topoSort(
157
+ resources,
158
+ (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
+ })],
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Generate a parameter declaration
174
+ */
175
+ private generateParameter(param: ParameterIR): string {
176
+ const varName = this.toVariableName(param.name);
177
+ return `export const ${varName} = new Parameter("${param.type}");`;
178
+ }
179
+
180
+ /**
181
+ * Generate a resource declaration
182
+ */
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);
187
+
188
+ if (propsStr === "{}") {
189
+ return `export const ${varName} = new ${resourceClass}();`;
190
+ }
191
+
192
+ return `export const ${varName} = new ${resourceClass}(${propsStr});`;
193
+ }
194
+
195
+ /**
196
+ * Generate property object as TypeScript
197
+ */
198
+ private generateProps(props: Record<string, unknown>, ir: TemplateIR): string {
199
+ if (Object.keys(props).length === 0) {
200
+ return "{}";
201
+ }
202
+
203
+ const entries = Object.entries(props).map(([key, value]) => {
204
+ const propName = this.toPropName(key);
205
+ const valueStr = this.generateValue(value, ir);
206
+ return ` ${propName}: ${valueStr}`;
207
+ });
208
+
209
+ return `{\n${entries.join(",\n")},\n}`;
210
+ }
211
+
212
+ /**
213
+ * Generate a value as TypeScript
214
+ */
215
+ private generateValue(value: unknown, ir: TemplateIR): string {
216
+ if (value === null || value === undefined) {
217
+ return "undefined";
218
+ }
219
+
220
+ if (typeof value === "string") {
221
+ return JSON.stringify(value);
222
+ }
223
+
224
+ if (typeof value === "number" || typeof value === "boolean") {
225
+ return String(value);
226
+ }
227
+
228
+ if (Array.isArray(value)) {
229
+ const items = value.map((item) => this.generateValue(item, ir));
230
+ return `[${items.join(", ")}]`;
231
+ }
232
+
233
+ if (typeof value === "object") {
234
+ const obj = value as Record<string, unknown>;
235
+
236
+ // Handle intrinsic functions
237
+ if (obj.__intrinsic === "Ref") {
238
+ const name = obj.name as string;
239
+ if (name.startsWith("AWS::")) {
240
+ return `AWS.${this.pseudoParamName(name)}`;
241
+ }
242
+ return this.toVariableName(name);
243
+ }
244
+
245
+ if (obj.__intrinsic === "GetAtt") {
246
+ const logicalId = obj.logicalId as string;
247
+ const attribute = obj.attribute as string;
248
+ const varName = this.toVariableName(logicalId);
249
+ const attrName = this.toPropName(attribute);
250
+ return `${varName}.${attrName}`;
251
+ }
252
+
253
+ if (obj.__intrinsic === "Sub") {
254
+ return this.generateSubIntrinsic(obj.template as string, ir);
255
+ }
256
+
257
+ if (obj.__intrinsic === "If") {
258
+ const condition = obj.condition as string;
259
+ const trueVal = this.generateValue(obj.valueIfTrue, ir);
260
+ const falseVal = this.generateValue(obj.valueIfFalse, ir);
261
+ return `If("${condition}", ${trueVal}, ${falseVal})`;
262
+ }
263
+
264
+ if (obj.__intrinsic === "Join") {
265
+ const delimiter = JSON.stringify(obj.delimiter);
266
+ const values = (obj.values as unknown[]).map((v) => this.generateValue(v, ir));
267
+ return `Join(${delimiter}, [${values.join(", ")}])`;
268
+ }
269
+
270
+ // Regular object
271
+ const entries = Object.entries(obj).map(([key, val]) => {
272
+ return `${key}: ${this.generateValue(val, ir)}`;
273
+ });
274
+ return `{ ${entries.join(", ")} }`;
275
+ }
276
+
277
+ return String(value);
278
+ }
279
+
280
+ /**
281
+ * Generate Sub intrinsic as tagged template literal
282
+ */
283
+ private generateSubIntrinsic(template: string, ir: TemplateIR): string {
284
+ // Parse ${...} interpolations from the Sub template
285
+ const parts: string[] = [];
286
+ const expressions: string[] = [];
287
+
288
+ let currentPos = 0;
289
+ const regex = /\$\{([^}]+)\}/g;
290
+ let match;
291
+
292
+ while ((match = regex.exec(template)) !== null) {
293
+ parts.push(template.slice(currentPos, match.index));
294
+
295
+ const expr = match[1];
296
+ if (expr.startsWith("AWS::")) {
297
+ expressions.push(`AWS.${this.pseudoParamName(expr)}`);
298
+ } 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}`);
303
+ } else {
304
+ expressions.push(this.toVariableName(expr));
305
+ }
306
+
307
+ currentPos = match.index + match[0].length;
308
+ }
309
+
310
+ parts.push(template.slice(currentPos));
311
+
312
+ if (expressions.length === 0) {
313
+ return `Sub\`${template}\``;
314
+ }
315
+
316
+ let result = "Sub`";
317
+ for (let i = 0; i < parts.length; i++) {
318
+ result += parts[i];
319
+ if (i < expressions.length) {
320
+ result += `\${${expressions[i]}}`;
321
+ }
322
+ }
323
+ result += "`";
324
+
325
+ return result;
326
+ }
327
+
328
+ /**
329
+ * Convert AWS pseudo-parameter name to TypeScript
330
+ */
331
+ private pseudoParamName(awsName: string): string {
332
+ // AWS::StackName -> StackName
333
+ return awsName.replace("AWS::", "");
334
+ }
335
+
336
+ /**
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
345
+ */
346
+ private toPropName(name: string): string {
347
+ return name.charAt(0).toLowerCase() + name.slice(1);
348
+ }
349
+ }