@intentius/chant-lexicon-aws 0.0.4 → 0.0.5

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.
@@ -1,8 +1,11 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { readdirSync, readFileSync } from "fs";
2
+ import { readdirSync, readFileSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { CFParser } from "./parser";
5
5
  import { CFGenerator } from "./generator";
6
+ import { build } from "@intentius/chant/build";
7
+ import { awsSerializer } from "../serializer";
8
+ import * as awsLexicon from "../index";
6
9
 
7
10
  const parser = new CFParser();
8
11
  const generator = new CFGenerator();
@@ -10,6 +13,15 @@ const generator = new CFGenerator();
10
13
  const roundtripDir = join(import.meta.dir, "../testdata/roundtrip");
11
14
  const samDir = join(import.meta.dir, "../testdata/sam-fixtures");
12
15
 
16
+ /** Extract imported symbols from `import { A, B } from "..."` statements */
17
+ function extractImportedSymbols(code: string): string[] {
18
+ const match = code.match(/import\s*\{([^}]+)\}\s*from\s*/);
19
+ if (!match) return [];
20
+ return match[1].split(",").map((s) => s.trim()).filter(Boolean);
21
+ }
22
+
23
+ const lexiconExports = new Set(Object.keys(awsLexicon));
24
+
13
25
  describe("CF roundtrip fixtures", () => {
14
26
  const fixtures = readdirSync(roundtripDir).filter((f) => f.endsWith(".json"));
15
27
 
@@ -36,20 +48,90 @@ describe("CF roundtrip fixtures", () => {
36
48
  expect(mainFile).toBeDefined();
37
49
 
38
50
  for (const name of resourceNames) {
39
- const varName = name.charAt(0).toLowerCase() + name.slice(1);
40
- expect(mainFile!.content).toContain(varName);
51
+ expect(mainFile!.content).toContain(name);
41
52
  }
42
53
 
43
54
  // If parameters exist, verify they appear in the output
44
55
  const paramNames = Object.keys(template.Parameters ?? {});
45
56
  for (const name of paramNames) {
46
- const varName = name.charAt(0).toLowerCase() + name.slice(1);
47
- expect(mainFile!.content).toContain(varName);
57
+ expect(mainFile!.content).toContain(name);
58
+ }
59
+
60
+ // Verify all imported symbols actually exist as exports from the lexicon
61
+ const importedSymbols = extractImportedSymbols(mainFile!.content);
62
+ for (const sym of importedSymbols) {
63
+ expect(lexiconExports.has(sym)).toBe(true);
48
64
  }
49
65
  });
50
66
  }
51
67
  });
52
68
 
69
+ describe("parameters.json build roundtrip", () => {
70
+ test("generated code builds and produces correct CF Parameters", async () => {
71
+ const content = readFileSync(join(roundtripDir, "parameters.json"), "utf-8");
72
+ const source = JSON.parse(content);
73
+
74
+ const ir = parser.parse(content);
75
+ const files = generator.generate(ir);
76
+ const mainFile = files.find((f) => f.path === "main.ts")!;
77
+
78
+ // Write generated code to a temp directory inside the monorepo (so workspace packages resolve)
79
+ const dir = mkdtempSync(join(import.meta.dir, "../../.roundtrip-tmp-"));
80
+ try {
81
+ const srcDir = join(dir, "src");
82
+ mkdirSync(srcDir);
83
+
84
+ writeFileSync(join(srcDir, "_.ts"), [
85
+ `export * from "@intentius/chant-lexicon-aws";`,
86
+ `import * as core from "@intentius/chant";`,
87
+ `export const $ = core.barrel(import.meta.dir);`,
88
+ ].join("\n"));
89
+
90
+ // Rewrite the import to use the barrel
91
+ const rewritten = mainFile.content.replace(
92
+ /import \{[^}]+\} from "@intentius\/chant-lexicon-aws";/,
93
+ `import * as _ from "./_";`,
94
+ );
95
+ // Replace bare symbol references with _.Symbol
96
+ const symbols = extractImportedSymbols(mainFile.content);
97
+ let code = rewritten;
98
+ for (const sym of symbols) {
99
+ code = code.replace(new RegExp(`\\bnew ${sym}\\(`, "g"), `new _.${sym}(`);
100
+ code = code.replace(new RegExp(`(?<!\\.)\\b${sym}\``, "g"), `_.${sym}\``);
101
+ code = code.replace(new RegExp(`(?<!\\.)\\b${sym}\\(`, "g"), `_.${sym}(`);
102
+ code = code.replace(new RegExp(`(?<!\\.)\\b${sym}\\.`, "g"), `_.${sym}.`);
103
+ }
104
+ writeFileSync(join(srcDir, "main.ts"), code);
105
+
106
+ // Build and verify
107
+ const result = await build(srcDir, [awsSerializer]);
108
+ expect(result.errors).toHaveLength(0);
109
+
110
+ const template = JSON.parse(result.outputs.get("aws")!);
111
+
112
+ // Verify parameters round-tripped correctly (names preserved as-is)
113
+ for (const [name, param] of Object.entries(source.Parameters ?? {})) {
114
+ const p = param as Record<string, unknown>;
115
+ expect(template.Parameters[name]).toBeDefined();
116
+ expect(template.Parameters[name].Type).toBe(p.Type);
117
+ if (p.Description) {
118
+ expect(template.Parameters[name].Description).toBe(p.Description);
119
+ }
120
+ if (p.Default !== undefined) {
121
+ expect(template.Parameters[name].Default).toBe(p.Default);
122
+ }
123
+ }
124
+
125
+ // Verify resources round-tripped (names preserved as-is)
126
+ for (const name of Object.keys(source.Resources ?? {})) {
127
+ expect(template.Resources[name]).toBeDefined();
128
+ }
129
+ } finally {
130
+ rmSync(dir, { recursive: true, force: true });
131
+ }
132
+ });
133
+ });
134
+
53
135
  describe("SAM roundtrip fixtures", () => {
54
136
  const fixtures = readdirSync(samDir).filter(
55
137
  (f) => f.endsWith(".yaml") || f.endsWith(".yml"),
@@ -25,6 +25,7 @@ describe("CloudFormation round-trip", () => {
25
25
 
26
26
  expect(files[0].content).toContain("Bucket");
27
27
  expect(files[0].content).toContain('bucketName: "my-bucket"');
28
+ expect(files[0].content).toContain("export const MyBucket");
28
29
  });
29
30
 
30
31
  test("round-trips template with parameters", () => {
@@ -51,8 +52,8 @@ describe("CloudFormation round-trip", () => {
51
52
  const files = generator.generate(ir);
52
53
 
53
54
  expect(files[0].content).toContain("Parameter");
54
- expect(files[0].content).toContain("environment");
55
- expect(files[0].content).toContain("bucketName: environment");
55
+ expect(files[0].content).toContain("Environment");
56
+ expect(files[0].content).toContain("bucketName: Ref(Environment)");
56
57
  });
57
58
 
58
59
  test("round-trips template with Fn::Sub", () => {
@@ -114,7 +115,7 @@ describe("CloudFormation round-trip", () => {
114
115
 
115
116
  expect(files[0].content).toContain("Role");
116
117
  expect(files[0].content).toContain("Function");
117
- expect(files[0].content).toContain("lambdaRole.arn");
118
+ expect(files[0].content).toContain("LambdaRole.arn");
118
119
  });
119
120
 
120
121
  test("round-trips complex nested properties", () => {
@@ -147,8 +148,8 @@ describe("CloudFormation round-trip", () => {
147
148
  const files = generator.generate(ir);
148
149
 
149
150
  expect(files[0].content).toContain("Function");
150
- expect(files[0].content).toContain("environment");
151
- expect(files[0].content).toContain("vpcConfig");
151
+ expect(files[0].content).toContain("environment:");
152
+ expect(files[0].content).toContain("vpcConfig:");
152
153
  });
153
154
 
154
155
  test("round-trips empty template", () => {
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // Parameter
2
+ export { Parameter } from "./parameter";
3
+
1
4
  // Serializer
2
5
  export { awsSerializer } from "./serializer";
3
6
 
@@ -5,11 +8,13 @@ export { awsSerializer } from "./serializer";
5
8
  export { nestedStack, isNestedStackInstance, NestedStackOutputRef, isNestedStackOutputRef, NESTED_STACK_MARKER } from "./nested-stack";
6
9
  export type { NestedStackOptions, NestedStackInstance } from "./nested-stack";
7
10
 
8
- // Re-export core child project and stack output primitives
11
+ // Re-export core child project, stack output, and lexicon output primitives
9
12
  export { isChildProject, CHILD_PROJECT_MARKER } from "@intentius/chant/child-project";
10
13
  export type { ChildProjectInstance } from "@intentius/chant/child-project";
11
14
  export { stackOutput, isStackOutput, STACK_OUTPUT_MARKER } from "@intentius/chant/stack-output";
12
15
  export type { StackOutput } from "@intentius/chant/stack-output";
16
+ export { output, isLexiconOutput } from "@intentius/chant/lexicon-output";
17
+ export type { LexiconOutput } from "@intentius/chant/lexicon-output";
13
18
 
14
19
  // Plugin
15
20
  export { awsPlugin } from "./plugin";
@@ -24,6 +29,7 @@ export {
24
29
  Select,
25
30
  Split,
26
31
  Base64,
32
+ GetAZs,
27
33
  SubIntrinsic,
28
34
  RefIntrinsic,
29
35
  GetAttIntrinsic,
@@ -32,6 +38,7 @@ export {
32
38
  SelectIntrinsic,
33
39
  SplitIntrinsic,
34
40
  Base64Intrinsic,
41
+ GetAZsIntrinsic,
35
42
  } from "./intrinsics";
36
43
 
37
44
  // Pseudo-parameters
package/src/intrinsics.ts CHANGED
@@ -221,3 +221,30 @@ export class Base64Intrinsic implements Intrinsic {
221
221
  export function Base64(value: string | Intrinsic): Base64Intrinsic {
222
222
  return new Base64Intrinsic(value);
223
223
  }
224
+
225
+ /**
226
+ * Fn::GetAZs intrinsic function
227
+ * Returns a list of Availability Zones for a region
228
+ */
229
+ export class GetAZsIntrinsic implements Intrinsic {
230
+ readonly [INTRINSIC_MARKER] = true as const;
231
+ private region: string | Intrinsic;
232
+
233
+ constructor(region: string | Intrinsic = "") {
234
+ this.region = region;
235
+ }
236
+
237
+ toJSON(): { "Fn::GetAZs": unknown } {
238
+ const regionValue = typeof this.region === "string"
239
+ ? this.region
240
+ : (this.region as Intrinsic & { toJSON(): unknown }).toJSON();
241
+ return { "Fn::GetAZs": regionValue };
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Create a GetAZs intrinsic
247
+ */
248
+ export function GetAZs(region?: string | Intrinsic): GetAZsIntrinsic {
249
+ return new GetAZsIntrinsic(region);
250
+ }
@@ -0,0 +1,16 @@
1
+ import { DECLARABLE_MARKER, type CoreParameter } from "@intentius/chant/declarable";
2
+
3
+ export class Parameter implements CoreParameter {
4
+ readonly [DECLARABLE_MARKER] = true as const;
5
+ readonly lexicon = "aws";
6
+ readonly entityType = "AWS::CloudFormation::Parameter";
7
+ readonly parameterType: string;
8
+ readonly description?: string;
9
+ readonly defaultValue?: unknown;
10
+
11
+ constructor(type: string, options?: { description?: string; defaultValue?: unknown }) {
12
+ this.parameterType = type;
13
+ this.description = options?.description;
14
+ this.defaultValue = options?.defaultValue;
15
+ }
16
+ }
@@ -36,7 +36,7 @@ describe("awsPlugin", () => {
36
36
 
37
37
  test("returns intrinsics", () => {
38
38
  const intrinsics = awsPlugin.intrinsics!();
39
- expect(intrinsics.length).toBe(8);
39
+ expect(intrinsics.length).toBe(9);
40
40
  const names = intrinsics.map((i) => i.name);
41
41
  expect(names).toContain("Sub");
42
42
  expect(names).toContain("Ref");
package/src/plugin.ts CHANGED
@@ -35,6 +35,7 @@ export const awsPlugin: LexiconPlugin = {
35
35
  { name: "Select", description: "Fn::Select — select value by index" },
36
36
  { name: "Split", description: "Fn::Split — split string by delimiter" },
37
37
  { name: "Base64", description: "Fn::Base64 — encode to Base64" },
38
+ { name: "GetAZs", description: "Fn::GetAZs — list Availability Zones" },
38
39
  ];
39
40
  },
40
41
 
@@ -1,7 +1,7 @@
1
1
  import { describe, test, expect } from "bun:test";
2
2
  import { awsSerializer } from "./serializer";
3
3
  import { AttrRef } from "@intentius/chant/attrref";
4
- import { DECLARABLE_MARKER, type Declarable, type CoreParameter } from "@intentius/chant/declarable";
4
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
5
5
  import { LexiconOutput } from "@intentius/chant/lexicon-output";
6
6
  import { Sub } from "./intrinsics";
7
7
  import { AWS } from "./pseudo";
@@ -10,6 +10,7 @@ import { stackOutput } from "@intentius/chant/stack-output";
10
10
  import { createResource } from "@intentius/chant/runtime";
11
11
  import type { SerializerResult } from "@intentius/chant/serializer";
12
12
  import type { BuildResult } from "@intentius/chant/build";
13
+ import { Parameter } from "./parameter";
13
14
 
14
15
  // Mock S3 Bucket for testing
15
16
  class MockBucket implements Declarable {
@@ -25,22 +26,6 @@ class MockBucket implements Declarable {
25
26
  }
26
27
  }
27
28
 
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
29
  describe("awsSerializer", () => {
45
30
  test("has correct name", () => {
46
31
  expect(awsSerializer.name).toBe("aws");
@@ -84,7 +69,7 @@ describe("awsSerializer.serialize", () => {
84
69
 
85
70
  test("serializes parameters", () => {
86
71
  const entities = new Map<string, Declarable>();
87
- entities.set("Environment", new MockParameter("String", {
72
+ entities.set("Environment", new Parameter("String", {
88
73
  description: "Environment name",
89
74
  defaultValue: "dev",
90
75
  }));
@@ -139,7 +124,7 @@ describe("awsSerializer.serialize", () => {
139
124
 
140
125
  test("handles resources and parameters together", () => {
141
126
  const entities = new Map<string, Declarable>();
142
- entities.set("Env", new MockParameter("String"));
127
+ entities.set("Env", new Parameter("String"));
143
128
  entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
144
129
 
145
130
  const output = awsSerializer.serialize(entities);