@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,507 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { awsSerializer } from "./serializer";
3
+ import { AttrRef } from "@intentius/chant/attrref";
4
+ import { DECLARABLE_MARKER, type Declarable, type CoreParameter } from "@intentius/chant/declarable";
5
+ import { LexiconOutput } from "@intentius/chant/lexicon-output";
6
+ import { Sub } from "./intrinsics";
7
+ import { AWS } from "./pseudo";
8
+ import { nestedStack, NestedStackOutputRef } from "./nested-stack";
9
+ import { stackOutput } from "@intentius/chant/stack-output";
10
+ import { createResource } from "@intentius/chant/runtime";
11
+ import type { SerializerResult } from "@intentius/chant/serializer";
12
+ import type { BuildResult } from "@intentius/chant/build";
13
+
14
+ // Mock S3 Bucket for testing
15
+ class MockBucket implements Declarable {
16
+ readonly [DECLARABLE_MARKER] = true as const;
17
+ readonly lexicon = "aws";
18
+ readonly entityType = "AWS::S3::Bucket";
19
+ readonly arn: AttrRef;
20
+ readonly props: { bucketName?: string; versioningConfiguration?: { status: string } };
21
+
22
+ constructor(props: { bucketName?: string; versioningConfiguration?: { status: string } } = {}) {
23
+ this.props = props;
24
+ this.arn = new AttrRef(this, "Arn");
25
+ }
26
+ }
27
+
28
+ // Mock Parameter for testing
29
+ class MockParameter implements CoreParameter {
30
+ readonly [DECLARABLE_MARKER] = true as const;
31
+ readonly lexicon = "aws";
32
+ readonly entityType = "AWS::CloudFormation::Parameter";
33
+ readonly parameterType: string;
34
+ readonly description?: string;
35
+ readonly defaultValue?: unknown;
36
+
37
+ constructor(type: string, options: { description?: string; defaultValue?: unknown } = {}) {
38
+ this.parameterType = type;
39
+ this.description = options.description;
40
+ this.defaultValue = options.defaultValue;
41
+ }
42
+ }
43
+
44
+ describe("awsSerializer", () => {
45
+ test("has correct name", () => {
46
+ expect(awsSerializer.name).toBe("aws");
47
+ });
48
+
49
+ test("has correct rulePrefix", () => {
50
+ expect(awsSerializer.rulePrefix).toBe("WAW");
51
+ });
52
+ });
53
+
54
+ describe("awsSerializer.serialize", () => {
55
+ test("produces valid CF template structure", () => {
56
+ const entities = new Map<string, Declarable>();
57
+ const output = awsSerializer.serialize(entities);
58
+ const template = JSON.parse(output);
59
+
60
+ expect(template.AWSTemplateFormatVersion).toBe("2010-09-09");
61
+ expect(template.Resources).toBeDefined();
62
+ });
63
+
64
+ test("serializes empty entities", () => {
65
+ const entities = new Map<string, Declarable>();
66
+ const output = awsSerializer.serialize(entities);
67
+ const template = JSON.parse(output);
68
+
69
+ expect(template.Resources).toEqual({});
70
+ expect(template.Parameters).toBeUndefined();
71
+ });
72
+
73
+ test("serializes resources", () => {
74
+ const entities = new Map<string, Declarable>();
75
+ entities.set("MyBucket", new MockBucket({ bucketName: "my-bucket" }));
76
+
77
+ const output = awsSerializer.serialize(entities);
78
+ const template = JSON.parse(output);
79
+
80
+ expect(template.Resources.MyBucket).toBeDefined();
81
+ expect(template.Resources.MyBucket.Type).toBe("AWS::S3::Bucket");
82
+ expect(template.Resources.MyBucket.Properties.BucketName).toBe("my-bucket");
83
+ });
84
+
85
+ test("serializes parameters", () => {
86
+ const entities = new Map<string, Declarable>();
87
+ entities.set("Environment", new MockParameter("String", {
88
+ description: "Environment name",
89
+ defaultValue: "dev",
90
+ }));
91
+
92
+ const output = awsSerializer.serialize(entities);
93
+ const template = JSON.parse(output);
94
+
95
+ expect(template.Parameters).toBeDefined();
96
+ expect(template.Parameters.Environment.Type).toBe("String");
97
+ expect(template.Parameters.Environment.Description).toBe("Environment name");
98
+ expect(template.Parameters.Environment.Default).toBe("dev");
99
+ });
100
+
101
+ test("serializes nested properties", () => {
102
+ const entities = new Map<string, Declarable>();
103
+ entities.set("MyBucket", new MockBucket({
104
+ bucketName: "my-bucket",
105
+ versioningConfiguration: { status: "Enabled" },
106
+ }));
107
+
108
+ const output = awsSerializer.serialize(entities);
109
+ const template = JSON.parse(output);
110
+
111
+ expect(template.Resources.MyBucket.Properties.VersioningConfiguration).toEqual({
112
+ Status: "Enabled",
113
+ });
114
+ });
115
+
116
+ test("converts property names to PascalCase", () => {
117
+ const entities = new Map<string, Declarable>();
118
+ entities.set("MyBucket", new MockBucket({ bucketName: "test" }));
119
+
120
+ const output = awsSerializer.serialize(entities);
121
+ const template = JSON.parse(output);
122
+
123
+ expect(template.Resources.MyBucket.Properties.BucketName).toBeDefined();
124
+ expect(template.Resources.MyBucket.Properties.bucketName).toBeUndefined();
125
+ });
126
+
127
+ test("handles multiple resources", () => {
128
+ const entities = new Map<string, Declarable>();
129
+ entities.set("DataBucket", new MockBucket({ bucketName: "data-bucket" }));
130
+ entities.set("LogsBucket", new MockBucket({ bucketName: "logs-bucket" }));
131
+
132
+ const output = awsSerializer.serialize(entities);
133
+ const template = JSON.parse(output);
134
+
135
+ expect(Object.keys(template.Resources)).toHaveLength(2);
136
+ expect(template.Resources.DataBucket).toBeDefined();
137
+ expect(template.Resources.LogsBucket).toBeDefined();
138
+ });
139
+
140
+ test("handles resources and parameters together", () => {
141
+ const entities = new Map<string, Declarable>();
142
+ entities.set("Env", new MockParameter("String"));
143
+ entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
144
+
145
+ const output = awsSerializer.serialize(entities);
146
+ const template = JSON.parse(output);
147
+
148
+ expect(template.Parameters).toBeDefined();
149
+ expect(template.Resources).toBeDefined();
150
+ expect(Object.keys(template.Parameters)).toHaveLength(1);
151
+ expect(Object.keys(template.Resources)).toHaveLength(1);
152
+ });
153
+
154
+ test("omits undefined properties", () => {
155
+ const entities = new Map<string, Declarable>();
156
+ entities.set("MyBucket", new MockBucket({})); // No properties set
157
+
158
+ const output = awsSerializer.serialize(entities);
159
+ const template = JSON.parse(output);
160
+
161
+ // Should have no Properties key or empty properties
162
+ expect(template.Resources.MyBucket.Properties).toBeUndefined();
163
+ });
164
+ });
165
+
166
+ // Mock property-kind Declarable for testing
167
+ class MockEncryption implements Declarable {
168
+ readonly [DECLARABLE_MARKER] = true as const;
169
+ readonly lexicon = "aws";
170
+ readonly entityType = "AWS::S3::Bucket.BucketEncryption";
171
+ readonly kind = "property" as const;
172
+ readonly props: { serverSideEncryptionConfiguration: unknown[] };
173
+
174
+ constructor(props: { serverSideEncryptionConfiguration: unknown[] }) {
175
+ this.props = props;
176
+ }
177
+ }
178
+
179
+ describe("property-kind Declarables", () => {
180
+ test("property-kind Declarables are inlined into parent properties", () => {
181
+ const encryption = new MockEncryption({
182
+ serverSideEncryptionConfiguration: [
183
+ { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
184
+ ],
185
+ });
186
+
187
+ const bucket = new MockBucket({ bucketName: "my-bucket" });
188
+ // Manually set encryption as a prop
189
+ (bucket.props as Record<string, unknown>).encryption = encryption;
190
+
191
+ const entities = new Map<string, Declarable>();
192
+ entities.set("DataEncryption", encryption);
193
+ entities.set("MyBucket", bucket);
194
+
195
+ const output = awsSerializer.serialize(entities);
196
+ const template = JSON.parse(output);
197
+
198
+ // Encryption should be inlined, not a Ref
199
+ expect(template.Resources.MyBucket.Properties.Encryption).toEqual({
200
+ ServerSideEncryptionConfiguration: [
201
+ { ServerSideEncryptionByDefault: { SseAlgorithm: "AES256" } },
202
+ ],
203
+ });
204
+ });
205
+
206
+ test("property-kind Declarables do NOT appear as standalone Resources", () => {
207
+ const encryption = new MockEncryption({
208
+ serverSideEncryptionConfiguration: [
209
+ { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
210
+ ],
211
+ });
212
+
213
+ const entities = new Map<string, Declarable>();
214
+ entities.set("DataEncryption", encryption);
215
+ entities.set("MyBucket", new MockBucket({ bucketName: "my-bucket" }));
216
+
217
+ const output = awsSerializer.serialize(entities);
218
+ const template = JSON.parse(output);
219
+
220
+ expect(template.Resources.DataEncryption).toBeUndefined();
221
+ expect(template.Resources.MyBucket).toBeDefined();
222
+ });
223
+
224
+ test("resource-kind Declarables still emit Ref when referenced", () => {
225
+ const sourceBucket = new MockBucket({ bucketName: "source" });
226
+
227
+ class MockConfig implements Declarable {
228
+ readonly [DECLARABLE_MARKER] = true as const;
229
+ readonly lexicon = "aws";
230
+ readonly entityType = "AWS::S3::ReplicationDestination";
231
+ readonly props: { bucket: Declarable };
232
+
233
+ constructor(bucket: Declarable) {
234
+ this.props = { bucket };
235
+ }
236
+ }
237
+
238
+ const entities = new Map<string, Declarable>();
239
+ entities.set("SourceBucket", sourceBucket);
240
+ entities.set("Config", new MockConfig(sourceBucket));
241
+
242
+ const output = awsSerializer.serialize(entities);
243
+ const template = JSON.parse(output);
244
+
245
+ expect(template.Resources.Config.Properties.Bucket).toEqual({ Ref: "SourceBucket" });
246
+ });
247
+ });
248
+
249
+ describe("intrinsic serialization", () => {
250
+ test("handles AttrRef in properties", () => {
251
+ const source = new MockBucket({ bucketName: "source" });
252
+ // Set the logical name on the AttrRef before using it
253
+ (source.arn as Record<string, unknown>)._setLogicalName("SourceBucket");
254
+
255
+ class MockReplication implements Declarable {
256
+ readonly [DECLARABLE_MARKER] = true as const;
257
+ readonly lexicon = "aws";
258
+ readonly entityType = "AWS::S3::ReplicationConfiguration";
259
+ readonly props: { sourceArn: AttrRef };
260
+
261
+ constructor(sourceArn: AttrRef) {
262
+ this.props = { sourceArn };
263
+ }
264
+ }
265
+
266
+ const entities = new Map<string, Declarable>();
267
+ entities.set("SourceBucket", source);
268
+ entities.set("Replication", new MockReplication(source.arn));
269
+
270
+ const output = awsSerializer.serialize(entities);
271
+ const template = JSON.parse(output);
272
+
273
+ expect(template.Resources.Replication.Properties.SourceArn).toEqual({
274
+ "Fn::GetAttr": ["SourceBucket", "Arn"],
275
+ });
276
+ });
277
+ });
278
+
279
+ describe("LexiconOutput serialization", () => {
280
+ test("generates CF Outputs section for LexiconOutputs", () => {
281
+ const bucket = new MockBucket({ bucketName: "data-bucket" });
282
+ const lexiconOutput = new LexiconOutput(bucket.arn, "DataBucketArn");
283
+ lexiconOutput._setSourceEntity("dataBucket");
284
+
285
+ const entities = new Map<string, Declarable>();
286
+ entities.set("dataBucket", bucket);
287
+
288
+ const result = awsSerializer.serialize(entities, [lexiconOutput]);
289
+ const template = JSON.parse(result);
290
+
291
+ expect(template.Outputs).toBeDefined();
292
+ expect(template.Outputs.DataBucketArn).toEqual({
293
+ Value: { "Fn::GetAttr": ["dataBucket", "Arn"] },
294
+ });
295
+ });
296
+
297
+ test("generates multiple CF Outputs", () => {
298
+ const dataBucket = new MockBucket({ bucketName: "data-bucket" });
299
+ const logsBucket = new MockBucket({ bucketName: "logs-bucket" });
300
+
301
+ const dataOutput = new LexiconOutput(dataBucket.arn, "DataBucketArn");
302
+ dataOutput._setSourceEntity("dataBucket");
303
+ const logsOutput = new LexiconOutput(logsBucket.arn, "LogsBucketArn");
304
+ logsOutput._setSourceEntity("logsBucket");
305
+
306
+ const entities = new Map<string, Declarable>();
307
+ entities.set("dataBucket", dataBucket);
308
+ entities.set("logsBucket", logsBucket);
309
+
310
+ const result = awsSerializer.serialize(entities, [dataOutput, logsOutput]);
311
+ const template = JSON.parse(result);
312
+
313
+ expect(template.Outputs).toBeDefined();
314
+ expect(Object.keys(template.Outputs)).toHaveLength(2);
315
+ expect(template.Outputs.DataBucketArn.Value).toEqual({
316
+ "Fn::GetAttr": ["dataBucket", "Arn"],
317
+ });
318
+ expect(template.Outputs.LogsBucketArn.Value).toEqual({
319
+ "Fn::GetAttr": ["logsBucket", "Arn"],
320
+ });
321
+ });
322
+
323
+ test("omits Outputs section when no LexiconOutputs provided", () => {
324
+ const entities = new Map<string, Declarable>();
325
+ entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
326
+
327
+ const result = awsSerializer.serialize(entities);
328
+ const template = JSON.parse(result);
329
+
330
+ expect(template.Outputs).toBeUndefined();
331
+ });
332
+
333
+ test("omits Outputs section when empty LexiconOutputs array", () => {
334
+ const entities = new Map<string, Declarable>();
335
+ entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
336
+
337
+ const result = awsSerializer.serialize(entities, []);
338
+ const template = JSON.parse(result as string);
339
+
340
+ expect(template.Outputs).toBeUndefined();
341
+ });
342
+ });
343
+
344
+ // ── Nested Stack Serialization ──────────────────────────
345
+
346
+ function mockChildBuildResult(childTemplate: object): BuildResult {
347
+ return {
348
+ outputs: new Map([["aws", JSON.stringify(childTemplate, null, 2)]]),
349
+ entities: new Map(),
350
+ warnings: [],
351
+ errors: [],
352
+ manifest: { lexicons: ["aws"], outputs: {}, deployOrder: ["aws"] },
353
+ sourceFileCount: 1,
354
+ };
355
+ }
356
+
357
+ describe("nested stack serialization", () => {
358
+ test("returns SerializerResult with child template file", () => {
359
+ const stack = nestedStack("network", "/path/to/network");
360
+ stack.buildResult = mockChildBuildResult({
361
+ AWSTemplateFormatVersion: "2010-09-09",
362
+ Resources: {
363
+ vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
364
+ },
365
+ });
366
+
367
+ const entities = new Map<string, Declarable>();
368
+ entities.set("network", stack as unknown as Declarable);
369
+
370
+ const result = awsSerializer.serialize(entities);
371
+
372
+ // Should be a SerializerResult, not a plain string
373
+ expect(typeof result).toBe("object");
374
+ const sr = result as SerializerResult;
375
+ expect(sr.primary).toBeDefined();
376
+ expect(sr.files).toBeDefined();
377
+ expect(sr.files!["network.template.json"]).toBeDefined();
378
+ });
379
+
380
+ test("parent template has TemplateBasePath parameter", () => {
381
+ const stack = nestedStack("network", "/path/to/network");
382
+ stack.buildResult = mockChildBuildResult({
383
+ AWSTemplateFormatVersion: "2010-09-09",
384
+ Resources: {},
385
+ });
386
+
387
+ const entities = new Map<string, Declarable>();
388
+ entities.set("network", stack as unknown as Declarable);
389
+
390
+ const result = awsSerializer.serialize(entities) as SerializerResult;
391
+ const parent = JSON.parse(result.primary);
392
+
393
+ expect(parent.Parameters).toBeDefined();
394
+ expect(parent.Parameters.TemplateBasePath).toBeDefined();
395
+ expect(parent.Parameters.TemplateBasePath.Type).toBe("String");
396
+ expect(parent.Parameters.TemplateBasePath.Default).toBe(".");
397
+ });
398
+
399
+ test("parent template has AWS::CloudFormation::Stack resource", () => {
400
+ const stack = nestedStack("network", "/path/to/network");
401
+ stack.buildResult = mockChildBuildResult({
402
+ AWSTemplateFormatVersion: "2010-09-09",
403
+ Resources: {},
404
+ });
405
+
406
+ const entities = new Map<string, Declarable>();
407
+ entities.set("network", stack as unknown as Declarable);
408
+
409
+ const result = awsSerializer.serialize(entities) as SerializerResult;
410
+ const parent = JSON.parse(result.primary);
411
+
412
+ expect(parent.Resources.network).toBeDefined();
413
+ expect(parent.Resources.network.Type).toBe("AWS::CloudFormation::Stack");
414
+ expect(parent.Resources.network.Properties.TemplateURL).toEqual({
415
+ "Fn::Sub": "${TemplateBasePath}/network.template.json",
416
+ });
417
+ });
418
+
419
+ test("explicit parameters are passed to child stack", () => {
420
+ const stack = nestedStack("network", "/path/to/network", {
421
+ parameters: { Environment: "prod" },
422
+ });
423
+ stack.buildResult = mockChildBuildResult({
424
+ AWSTemplateFormatVersion: "2010-09-09",
425
+ Resources: {},
426
+ });
427
+
428
+ const entities = new Map<string, Declarable>();
429
+ entities.set("network", stack as unknown as Declarable);
430
+
431
+ const result = awsSerializer.serialize(entities) as SerializerResult;
432
+ const parent = JSON.parse(result.primary);
433
+
434
+ // Parameters should be passed to the stack resource
435
+ expect(parent.Resources.network.Properties.Parameters.Environment).toBe("prod");
436
+ expect(parent.Resources.network.Properties.Parameters.TemplateBasePath).toEqual({
437
+ Ref: "TemplateBasePath",
438
+ });
439
+ });
440
+
441
+ test("mixed nested and regular resources work together", () => {
442
+ const stack = nestedStack("network", "/path/to/network");
443
+ stack.buildResult = mockChildBuildResult({
444
+ AWSTemplateFormatVersion: "2010-09-09",
445
+ Resources: {},
446
+ });
447
+
448
+ const bucket = new MockBucket({ bucketName: "data" });
449
+
450
+ const entities = new Map<string, Declarable>();
451
+ entities.set("network", stack as unknown as Declarable);
452
+ entities.set("dataBucket", bucket);
453
+
454
+ const result = awsSerializer.serialize(entities) as SerializerResult;
455
+ const parent = JSON.parse(result.primary);
456
+
457
+ // Both should be in parent
458
+ expect(parent.Resources.network.Type).toBe("AWS::CloudFormation::Stack");
459
+ expect(parent.Resources.dataBucket.Type).toBe("AWS::S3::Bucket");
460
+ });
461
+
462
+ test("without nested stacks returns plain string", () => {
463
+ const entities = new Map<string, Declarable>();
464
+ entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
465
+
466
+ const result = awsSerializer.serialize(entities);
467
+ expect(typeof result).toBe("string");
468
+ });
469
+
470
+ test("cross-stack ref via NestedStackOutputRef serializes correctly in parent", () => {
471
+ const stack = nestedStack("network", "/path/to/network");
472
+ stack.buildResult = mockChildBuildResult({
473
+ AWSTemplateFormatVersion: "2010-09-09",
474
+ Resources: {
475
+ subnet: { Type: "AWS::EC2::Subnet" },
476
+ },
477
+ Outputs: {
478
+ subnetId: { Value: { "Fn::GetAttr": ["subnet", "SubnetId"] } },
479
+ },
480
+ });
481
+
482
+ // Create a function that uses network.outputs.subnetId
483
+ const subnetRef = new NestedStackOutputRef("network", "subnetId");
484
+ const fn = {
485
+ [DECLARABLE_MARKER]: true,
486
+ lexicon: "aws",
487
+ entityType: "AWS::Lambda::Function",
488
+ kind: "resource" as const,
489
+ props: {
490
+ vpcConfig: { subnetIds: [subnetRef] },
491
+ },
492
+ } as unknown as Declarable;
493
+
494
+ const entities = new Map<string, Declarable>();
495
+ entities.set("network", stack as unknown as Declarable);
496
+ entities.set("handler", fn);
497
+
498
+ const result = awsSerializer.serialize(entities) as SerializerResult;
499
+ const parent = JSON.parse(result.primary);
500
+
501
+ // The NestedStackOutputRef should serialize via toJSON()
502
+ const vpcConfig = parent.Resources.handler.Properties.VpcConfig;
503
+ expect(vpcConfig.SubnetIds[0]).toEqual({
504
+ "Fn::GetAtt": ["network", "Outputs.subnetId"],
505
+ });
506
+ });
507
+ });