@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.
- package/README.md +438 -0
- package/package.json +30 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +197 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1206 -0
- package/src/codegen/extensions.ts +171 -0
- package/src/codegen/fallback.ts +33 -0
- package/src/codegen/generate-cli.ts +17 -0
- package/src/codegen/generate-lexicon.ts +98 -0
- package/src/codegen/generate-typescript.ts +257 -0
- package/src/codegen/generate.test.ts +125 -0
- package/src/codegen/generate.ts +226 -0
- package/src/codegen/idempotency.test.ts +28 -0
- package/src/codegen/naming.ts +120 -0
- package/src/codegen/package.test.ts +60 -0
- package/src/codegen/package.ts +84 -0
- package/src/codegen/patches.ts +98 -0
- package/src/codegen/rollback.test.ts +80 -0
- package/src/codegen/rollback.ts +20 -0
- package/src/codegen/sam.ts +387 -0
- package/src/codegen/snapshot.test.ts +84 -0
- package/src/codegen/typecheck.test.ts +50 -0
- package/src/codegen/typecheck.ts +4 -0
- package/src/codegen/versions.ts +37 -0
- package/src/coverage.ts +14 -0
- package/src/generated/index.d.ts +160753 -0
- package/src/generated/index.ts +14396 -0
- package/src/generated/lexicon-aws.json +114563 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +181 -0
- package/src/import/generator.ts +349 -0
- package/src/import/parser.test.ts +200 -0
- package/src/import/parser.ts +350 -0
- package/src/import/roundtrip-fixtures.test.ts +78 -0
- package/src/import/roundtrip.test.ts +195 -0
- package/src/index.ts +63 -0
- package/src/integration.test.ts +129 -0
- package/src/intrinsics.test.ts +167 -0
- package/src/intrinsics.ts +223 -0
- package/src/lint/post-synth/cf-refs.ts +91 -0
- package/src/lint/post-synth/cor020.ts +72 -0
- package/src/lint/post-synth/ext001.test.ts +68 -0
- package/src/lint/post-synth/ext001.ts +222 -0
- package/src/lint/post-synth/post-synth.test.ts +280 -0
- package/src/lint/post-synth/waw010.ts +49 -0
- package/src/lint/post-synth/waw011.ts +49 -0
- package/src/lint/post-synth/waw013.ts +45 -0
- package/src/lint/post-synth/waw014.ts +50 -0
- package/src/lint/post-synth/waw015.ts +100 -0
- package/src/lint/rules/hardcoded-region.ts +43 -0
- package/src/lint/rules/iam-wildcard.ts +66 -0
- package/src/lint/rules/index.ts +7 -0
- package/src/lint/rules/rules.test.ts +175 -0
- package/src/lint/rules/s3-encryption.ts +69 -0
- package/src/lsp/completions.test.ts +72 -0
- package/src/lsp/completions.ts +18 -0
- package/src/lsp/hover.test.ts +53 -0
- package/src/lsp/hover.ts +53 -0
- package/src/nested-stack.test.ts +83 -0
- package/src/nested-stack.ts +125 -0
- package/src/plugin.test.ts +316 -0
- package/src/plugin.ts +514 -0
- package/src/pseudo.test.ts +55 -0
- package/src/pseudo.ts +29 -0
- package/src/serializer.test.ts +507 -0
- package/src/serializer.ts +333 -0
- package/src/spec/fetch.test.ts +27 -0
- package/src/spec/fetch.ts +107 -0
- package/src/spec/parse.test.ts +153 -0
- package/src/spec/parse.ts +202 -0
- package/src/testdata/load-fixtures.ts +17 -0
- package/src/testdata/roundtrip/conditions.json +21 -0
- package/src/testdata/roundtrip/intrinsic-calls.json +31 -0
- package/src/testdata/roundtrip/intrinsics.json +18 -0
- package/src/testdata/roundtrip/multi-resource.json +37 -0
- package/src/testdata/roundtrip/parameters.json +23 -0
- package/src/testdata/roundtrip/simple.json +12 -0
- package/src/testdata/sam-fixtures/api.yaml +14 -0
- package/src/testdata/sam-fixtures/application.yaml +13 -0
- package/src/testdata/sam-fixtures/function.yaml +22 -0
- package/src/testdata/sam-fixtures/graphql-api.yaml +13 -0
- package/src/testdata/sam-fixtures/http-api.yaml +15 -0
- package/src/testdata/sam-fixtures/layer-version.yaml +15 -0
- package/src/testdata/sam-fixtures/multi-type-a.yaml +23 -0
- package/src/testdata/sam-fixtures/multi-type-b.yaml +29 -0
- package/src/testdata/sam-fixtures/simple-table.yaml +12 -0
- package/src/testdata/sam-fixtures/state-machine.yaml +14 -0
- package/src/testdata/schemas/aws-dynamodb-table.json +126 -0
- package/src/testdata/schemas/aws-iam-role.json +85 -0
- package/src/testdata/schemas/aws-lambda-function.json +90 -0
- package/src/testdata/schemas/aws-s3-bucket.json +83 -0
- package/src/testdata/schemas/aws-sns-topic.json +71 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.ts +34 -0
|
@@ -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
|
+
}
|