@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,195 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { CFParser } from "./parser";
3
+ import { CFGenerator } from "./generator";
4
+
5
+ describe("CloudFormation round-trip", () => {
6
+ const parser = new CFParser();
7
+ const generator = new CFGenerator();
8
+
9
+ test("round-trips simple S3 bucket", () => {
10
+ const original = {
11
+ AWSTemplateFormatVersion: "2010-09-09",
12
+ Resources: {
13
+ MyBucket: {
14
+ Type: "AWS::S3::Bucket",
15
+ Properties: {
16
+ BucketName: "my-bucket",
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ const content = JSON.stringify(original);
23
+ const ir = parser.parse(content);
24
+ const files = generator.generate(ir);
25
+
26
+ expect(files[0].content).toContain("Bucket");
27
+ expect(files[0].content).toContain('bucketName: "my-bucket"');
28
+ });
29
+
30
+ test("round-trips template with parameters", () => {
31
+ const original = {
32
+ AWSTemplateFormatVersion: "2010-09-09",
33
+ Parameters: {
34
+ Environment: {
35
+ Type: "String",
36
+ Default: "dev",
37
+ },
38
+ },
39
+ Resources: {
40
+ MyBucket: {
41
+ Type: "AWS::S3::Bucket",
42
+ Properties: {
43
+ BucketName: { Ref: "Environment" },
44
+ },
45
+ },
46
+ },
47
+ };
48
+
49
+ const content = JSON.stringify(original);
50
+ const ir = parser.parse(content);
51
+ const files = generator.generate(ir);
52
+
53
+ expect(files[0].content).toContain("Parameter");
54
+ expect(files[0].content).toContain("environment");
55
+ expect(files[0].content).toContain("bucketName: environment");
56
+ });
57
+
58
+ test("round-trips template with Fn::Sub", () => {
59
+ const original = {
60
+ AWSTemplateFormatVersion: "2010-09-09",
61
+ Resources: {
62
+ MyBucket: {
63
+ Type: "AWS::S3::Bucket",
64
+ Properties: {
65
+ BucketName: { "Fn::Sub": "${AWS::StackName}-data" },
66
+ },
67
+ },
68
+ },
69
+ };
70
+
71
+ const content = JSON.stringify(original);
72
+ const ir = parser.parse(content);
73
+ const files = generator.generate(ir);
74
+
75
+ expect(files[0].content).toContain("Sub");
76
+ expect(files[0].content).toContain("AWS.StackName");
77
+ });
78
+
79
+ test("round-trips Lambda with IAM Role", () => {
80
+ const original = {
81
+ AWSTemplateFormatVersion: "2010-09-09",
82
+ Resources: {
83
+ LambdaRole: {
84
+ Type: "AWS::IAM::Role",
85
+ Properties: {
86
+ RoleName: "lambda-role",
87
+ AssumeRolePolicyDocument: {
88
+ Version: "2012-10-17",
89
+ Statement: [
90
+ {
91
+ Effect: "Allow",
92
+ Principal: { Service: "lambda.amazonaws.com" },
93
+ Action: "sts:AssumeRole",
94
+ },
95
+ ],
96
+ },
97
+ },
98
+ },
99
+ MyFunction: {
100
+ Type: "AWS::Lambda::Function",
101
+ Properties: {
102
+ FunctionName: "my-function",
103
+ Runtime: "nodejs18.x",
104
+ Handler: "index.handler",
105
+ Role: { "Fn::GetAtt": ["LambdaRole", "Arn"] },
106
+ },
107
+ },
108
+ },
109
+ };
110
+
111
+ const content = JSON.stringify(original);
112
+ const ir = parser.parse(content);
113
+ const files = generator.generate(ir);
114
+
115
+ expect(files[0].content).toContain("Role");
116
+ expect(files[0].content).toContain("Function");
117
+ expect(files[0].content).toContain("lambdaRole.arn");
118
+ });
119
+
120
+ test("round-trips complex nested properties", () => {
121
+ const original = {
122
+ AWSTemplateFormatVersion: "2010-09-09",
123
+ Resources: {
124
+ MyFunction: {
125
+ Type: "AWS::Lambda::Function",
126
+ Properties: {
127
+ FunctionName: "my-function",
128
+ Runtime: "nodejs18.x",
129
+ Handler: "index.handler",
130
+ Environment: {
131
+ Variables: {
132
+ NODE_ENV: "production",
133
+ LOG_LEVEL: "info",
134
+ },
135
+ },
136
+ VpcConfig: {
137
+ SecurityGroupIds: ["sg-12345"],
138
+ SubnetIds: ["subnet-abc", "subnet-def"],
139
+ },
140
+ },
141
+ },
142
+ },
143
+ };
144
+
145
+ const content = JSON.stringify(original);
146
+ const ir = parser.parse(content);
147
+ const files = generator.generate(ir);
148
+
149
+ expect(files[0].content).toContain("Function");
150
+ expect(files[0].content).toContain("environment");
151
+ expect(files[0].content).toContain("vpcConfig");
152
+ });
153
+
154
+ test("round-trips empty template", () => {
155
+ const original = {
156
+ AWSTemplateFormatVersion: "2010-09-09",
157
+ Resources: {},
158
+ };
159
+
160
+ const content = JSON.stringify(original);
161
+ const ir = parser.parse(content);
162
+ const files = generator.generate(ir);
163
+
164
+ expect(files).toHaveLength(1);
165
+ expect(files[0].path).toBe("main.ts");
166
+ });
167
+
168
+ test("handles multiple resource types", () => {
169
+ const original = {
170
+ AWSTemplateFormatVersion: "2010-09-09",
171
+ Resources: {
172
+ MyBucket: {
173
+ Type: "AWS::S3::Bucket",
174
+ Properties: { BucketName: "bucket" },
175
+ },
176
+ MyQueue: {
177
+ Type: "AWS::SQS::Queue",
178
+ Properties: { QueueName: "queue" },
179
+ },
180
+ MyTopic: {
181
+ Type: "AWS::SNS::Topic",
182
+ Properties: { TopicName: "topic" },
183
+ },
184
+ },
185
+ };
186
+
187
+ const content = JSON.stringify(original);
188
+ const ir = parser.parse(content);
189
+ const files = generator.generate(ir);
190
+
191
+ expect(files[0].content).toContain("Bucket");
192
+ expect(files[0].content).toContain("Queue");
193
+ expect(files[0].content).toContain("Topic");
194
+ });
195
+ });
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ // Serializer
2
+ export { awsSerializer } from "./serializer";
3
+
4
+ // Nested Stacks
5
+ export { nestedStack, isNestedStackInstance, NestedStackOutputRef, isNestedStackOutputRef, NESTED_STACK_MARKER } from "./nested-stack";
6
+ export type { NestedStackOptions, NestedStackInstance } from "./nested-stack";
7
+
8
+ // Re-export core child project and stack output primitives
9
+ export { isChildProject, CHILD_PROJECT_MARKER } from "@intentius/chant/child-project";
10
+ export type { ChildProjectInstance } from "@intentius/chant/child-project";
11
+ export { stackOutput, isStackOutput, STACK_OUTPUT_MARKER } from "@intentius/chant/stack-output";
12
+ export type { StackOutput } from "@intentius/chant/stack-output";
13
+
14
+ // Plugin
15
+ export { awsPlugin } from "./plugin";
16
+
17
+ // Intrinsics
18
+ export {
19
+ Sub,
20
+ Ref,
21
+ GetAtt,
22
+ If,
23
+ Join,
24
+ Select,
25
+ Split,
26
+ Base64,
27
+ SubIntrinsic,
28
+ RefIntrinsic,
29
+ GetAttIntrinsic,
30
+ IfIntrinsic,
31
+ JoinIntrinsic,
32
+ SelectIntrinsic,
33
+ SplitIntrinsic,
34
+ Base64Intrinsic,
35
+ } from "./intrinsics";
36
+
37
+ // Pseudo-parameters
38
+ export {
39
+ AWS,
40
+ StackName,
41
+ Region,
42
+ AccountId,
43
+ StackId,
44
+ URLSuffix,
45
+ NoValue,
46
+ NotificationARNs,
47
+ Partition,
48
+ } from "./pseudo";
49
+
50
+ // Generated resources — export everything from generated index
51
+ // After running `chant generate`, this re-exports all 1000+ resource classes
52
+ export * from "./generated/index";
53
+
54
+ // Spec utilities (for tooling)
55
+ export { fetchSchemaZip } from "./spec/fetch";
56
+ export type { CFNSchema, SchemaProperty, SchemaDefinition } from "./spec/fetch";
57
+ export { parseCFNSchema, cfnShortName, cfnServiceName } from "./spec/parse";
58
+ export type { SchemaParseResult, ParsedResource, ParsedProperty, ParsedAttribute, ParsedPropertyType, ParsedEnum, PropertyConstraints } from "./spec/parse";
59
+
60
+ // Code generation pipeline
61
+ export { generate, writeGeneratedFiles } from "./codegen/generate";
62
+ export { packageLexicon } from "./codegen/package";
63
+ export type { PackageOptions, PackageResult } from "./codegen/package";
@@ -0,0 +1,129 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { awsSerializer } from "./serializer";
3
+ import { Sub, If, Join, AWS } from "./index";
4
+ import { Bucket, Function, Role } from "./generated/index";
5
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
6
+
7
+ describe("AWS Integration", () => {
8
+ describe("Serializer", () => {
9
+ test("produces valid CF JSON structure", () => {
10
+ const entities = new Map<string, Declarable>();
11
+ const output = awsSerializer.serialize(entities);
12
+ const template = JSON.parse(output);
13
+
14
+ expect(template.AWSTemplateFormatVersion).toBe("2010-09-09");
15
+ expect(template.Resources).toBeDefined();
16
+ });
17
+
18
+ test("serializes S3 bucket with properties", () => {
19
+ const bucket = new (Bucket as any)({
20
+ bucketName: "my-bucket",
21
+ });
22
+
23
+ const entities = new Map<string, Declarable>();
24
+ entities.set("MyBucket", bucket);
25
+
26
+ const output = awsSerializer.serialize(entities);
27
+ const template = JSON.parse(output);
28
+
29
+ expect(template.Resources.MyBucket.Type).toBe("AWS::S3::Bucket");
30
+ expect(template.Resources.MyBucket.Properties.BucketName).toBe("my-bucket");
31
+ });
32
+ });
33
+
34
+ describe("Intrinsics", () => {
35
+ test("Sub with pseudo-parameters", () => {
36
+ const result = Sub`${AWS.StackName}-bucket`;
37
+ expect(result.toJSON()).toEqual({ "Fn::Sub": "${AWS::StackName}-bucket" });
38
+ });
39
+
40
+ test("Sub with multiple interpolations", () => {
41
+ const result = Sub`${AWS.StackName}-${AWS.Region}-bucket`;
42
+ expect(result.toJSON()).toEqual({
43
+ "Fn::Sub": "${AWS::StackName}-${AWS::Region}-bucket",
44
+ });
45
+ });
46
+
47
+ test("If with string values", () => {
48
+ const result = If("IsProduction", "prod-bucket", "dev-bucket");
49
+ expect(result.toJSON()).toEqual({
50
+ "Fn::If": ["IsProduction", "prod-bucket", "dev-bucket"],
51
+ });
52
+ });
53
+
54
+ test("Join with array", () => {
55
+ const result = Join("-", ["my", "bucket", "name"]);
56
+ expect(result.toJSON()).toEqual({
57
+ "Fn::Join": ["-", ["my", "bucket", "name"]],
58
+ });
59
+ });
60
+ });
61
+
62
+ describe("Cross-resource references", () => {
63
+ test("GetAtt for bucket ARN", () => {
64
+ const bucket = new (Bucket as any)({ bucketName: "source" });
65
+ // Set logical name for the AttrRef
66
+ (bucket.arn as Record<string, unknown>)._setLogicalName("SourceBucket");
67
+
68
+ expect(bucket.arn.getLogicalName()).toBe("SourceBucket");
69
+ expect(bucket.arn.attribute).toBe("Arn");
70
+ });
71
+ });
72
+
73
+ describe("Resource types", () => {
74
+ test("Bucket has correct entity type", () => {
75
+ const bucket = new (Bucket as any)({});
76
+ expect(bucket.entityType).toBe("AWS::S3::Bucket");
77
+ expect(bucket[DECLARABLE_MARKER]).toBe(true);
78
+ });
79
+
80
+ test("Lambda Function has correct entity type", () => {
81
+ const fn = new (Function as any)({
82
+ runtime: "nodejs18.x",
83
+ handler: "index.handler",
84
+ code: { s3Bucket: "my-bucket", s3Key: "code.zip" },
85
+ role: "arn:aws:iam::123456789012:role/lambda-role",
86
+ });
87
+ expect(fn.entityType).toBe("AWS::Lambda::Function");
88
+ expect(fn[DECLARABLE_MARKER]).toBe(true);
89
+ });
90
+
91
+ test("IAM Role has correct entity type", () => {
92
+ const role = new (Role as any)({
93
+ assumeRolePolicyDocument: {
94
+ Version: "2012-10-17",
95
+ Statement: [],
96
+ },
97
+ });
98
+ expect(role.entityType).toBe("AWS::IAM::Role");
99
+ expect(role[DECLARABLE_MARKER]).toBe(true);
100
+ });
101
+ });
102
+
103
+ describe("AttrRefs", () => {
104
+ test("Bucket has expected AttrRefs", () => {
105
+ const bucket = new (Bucket as any)({});
106
+ expect(bucket.arn).toBeDefined();
107
+ expect(bucket.domainName).toBeDefined();
108
+ expect(bucket.websiteURL).toBeDefined();
109
+ });
110
+
111
+ test("Lambda Function has expected AttrRefs", () => {
112
+ const fn = new (Function as any)({
113
+ runtime: "nodejs18.x",
114
+ handler: "index.handler",
115
+ code: { s3Bucket: "bucket", s3Key: "key" },
116
+ role: "role-arn",
117
+ });
118
+ expect(fn.arn).toBeDefined();
119
+ });
120
+
121
+ test("IAM Role has expected AttrRefs", () => {
122
+ const role = new (Role as any)({
123
+ assumeRolePolicyDocument: {},
124
+ });
125
+ expect(role.arn).toBeDefined();
126
+ expect(role.roleId).toBeDefined();
127
+ });
128
+ });
129
+ });
@@ -0,0 +1,167 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { Sub, Ref, GetAtt, If, Join, Select, Split, Base64 } from "./intrinsics";
3
+ import { AWS } from "./pseudo";
4
+ import { AttrRef } from "@intentius/chant/attrref";
5
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
6
+
7
+ // Mock declarable for testing
8
+ class MockBucket implements Declarable {
9
+ readonly [DECLARABLE_MARKER] = true as const;
10
+ readonly lexicon = "aws";
11
+ readonly entityType = "AWS::S3::Bucket";
12
+ readonly arn: AttrRef;
13
+ readonly props = {};
14
+
15
+ constructor() {
16
+ this.arn = new AttrRef(this, "Arn");
17
+ }
18
+ }
19
+
20
+ describe("Sub intrinsic", () => {
21
+ test("creates simple template", () => {
22
+ const result = Sub`my-bucket`;
23
+ expect(result.toJSON()).toEqual({ "Fn::Sub": "my-bucket" });
24
+ });
25
+
26
+ test("interpolates string values", () => {
27
+ const env = "prod";
28
+ const result = Sub`my-bucket-${env}`;
29
+ expect(result.toJSON()).toEqual({ "Fn::Sub": "my-bucket-prod" });
30
+ });
31
+
32
+ test("interpolates pseudo-parameters", () => {
33
+ const result = Sub`${AWS.StackName}-bucket`;
34
+ expect(result.toJSON()).toEqual({ "Fn::Sub": "${AWS::StackName}-bucket" });
35
+ });
36
+
37
+ test("interpolates AttrRef", () => {
38
+ const bucket = new MockBucket();
39
+ // Set the logical name on the AttrRef
40
+ (bucket.arn as Record<string, unknown>)._setLogicalName("MyBucket");
41
+
42
+ const result = Sub`arn:aws:s3:::${bucket.arn}/*`;
43
+ expect(result.toJSON()["Fn::Sub"]).toContain("${MyBucket.Arn}");
44
+ });
45
+
46
+ test("handles multiple interpolations", () => {
47
+ const result = Sub`${AWS.StackName}-${AWS.Region}-bucket`;
48
+ expect(result.toJSON()).toEqual({
49
+ "Fn::Sub": "${AWS::StackName}-${AWS::Region}-bucket",
50
+ });
51
+ });
52
+
53
+ test("throws for direct Declarable when serializing", () => {
54
+ const bucket = new MockBucket();
55
+ const sub = Sub`${bucket as unknown}`;
56
+ expect(() => sub.toJSON()).toThrow("Cannot embed Declarable directly");
57
+ });
58
+ });
59
+
60
+ describe("Ref intrinsic", () => {
61
+ test("creates Ref for resource name", () => {
62
+ const result = Ref("MyBucket");
63
+ expect(result.toJSON()).toEqual({ Ref: "MyBucket" });
64
+ });
65
+
66
+ test("creates Ref for parameter", () => {
67
+ const result = Ref("Environment");
68
+ expect(result.toJSON()).toEqual({ Ref: "Environment" });
69
+ });
70
+ });
71
+
72
+ describe("GetAtt intrinsic", () => {
73
+ test("creates GetAtt", () => {
74
+ const result = GetAtt("MyBucket", "Arn");
75
+ expect(result.toJSON()).toEqual({ "Fn::GetAtt": ["MyBucket", "Arn"] });
76
+ });
77
+
78
+ test("handles dotted attribute names", () => {
79
+ const result = GetAtt("MyFunction", "SnapStartResponse.ApplyOn");
80
+ expect(result.toJSON()).toEqual({
81
+ "Fn::GetAtt": ["MyFunction", "SnapStartResponse.ApplyOn"],
82
+ });
83
+ });
84
+ });
85
+
86
+ describe("If intrinsic", () => {
87
+ test("creates If with values", () => {
88
+ const result = If("CreateProd", "prod-bucket", "dev-bucket");
89
+ expect(result.toJSON()).toEqual({
90
+ "Fn::If": ["CreateProd", "prod-bucket", "dev-bucket"],
91
+ });
92
+ });
93
+
94
+ test("handles nested intrinsics", () => {
95
+ const result = If("CreateProd", Ref("ProdBucket"), Ref("DevBucket"));
96
+ expect(result.toJSON()).toEqual({
97
+ "Fn::If": ["CreateProd", { Ref: "ProdBucket" }, { Ref: "DevBucket" }],
98
+ });
99
+ });
100
+ });
101
+
102
+ describe("Join intrinsic", () => {
103
+ test("joins strings", () => {
104
+ const result = Join("-", ["a", "b", "c"]);
105
+ expect(result.toJSON()).toEqual({ "Fn::Join": ["-", ["a", "b", "c"]] });
106
+ });
107
+
108
+ test("joins with empty delimiter", () => {
109
+ const result = Join("", ["hello", "world"]);
110
+ expect(result.toJSON()).toEqual({ "Fn::Join": ["", ["hello", "world"]] });
111
+ });
112
+
113
+ test("handles intrinsic values", () => {
114
+ const result = Join("-", [Ref("Prefix"), "bucket"]);
115
+ expect(result.toJSON()).toEqual({
116
+ "Fn::Join": ["-", [{ Ref: "Prefix" }, "bucket"]],
117
+ });
118
+ });
119
+ });
120
+
121
+ describe("Select intrinsic", () => {
122
+ test("selects from array", () => {
123
+ const result = Select(0, ["a", "b", "c"]);
124
+ expect(result.toJSON()).toEqual({ "Fn::Select": ["0", ["a", "b", "c"]] });
125
+ });
126
+
127
+ test("selects with intrinsic array", () => {
128
+ const result = Select(1, [Ref("A"), Ref("B")]);
129
+ expect(result.toJSON()).toEqual({
130
+ "Fn::Select": ["1", [{ Ref: "A" }, { Ref: "B" }]],
131
+ });
132
+ });
133
+ });
134
+
135
+ describe("Split intrinsic", () => {
136
+ test("splits string", () => {
137
+ const result = Split(",", "a,b,c");
138
+ expect(result.toJSON()).toEqual({ "Fn::Split": [",", "a,b,c"] });
139
+ });
140
+
141
+ test("splits with intrinsic source", () => {
142
+ const result = Split(",", Ref("CommaSeparatedList"));
143
+ expect(result.toJSON()).toEqual({
144
+ "Fn::Split": [",", { Ref: "CommaSeparatedList" }],
145
+ });
146
+ });
147
+ });
148
+
149
+ describe("Base64 intrinsic", () => {
150
+ test("encodes string", () => {
151
+ const result = Base64("Hello World");
152
+ expect(result.toJSON()).toEqual({ "Fn::Base64": "Hello World" });
153
+ });
154
+
155
+ test("encodes intrinsic result", () => {
156
+ const result = Base64(Ref("UserData"));
157
+ expect(result.toJSON()).toEqual({ "Fn::Base64": { Ref: "UserData" } });
158
+ });
159
+
160
+ test("handles nested Sub", () => {
161
+ const result = Base64(Sub`#!/bin/bash\necho ${AWS.StackName}`);
162
+ const json = result.toJSON();
163
+ expect(json["Fn::Base64"]).toEqual({
164
+ "Fn::Sub": "#!/bin/bash\necho ${AWS::StackName}",
165
+ });
166
+ });
167
+ });