@intentius/chant-lexicon-aws 0.0.3 → 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.
- package/dist/integrity.json +10 -10
- package/dist/manifest.json +7 -1
- package/dist/meta.json +329 -370
- package/dist/types/index.d.ts +11 -76
- package/package.json +3 -2
- package/src/codegen/docs.ts +32 -125
- package/src/generated/index.d.ts +11 -76
- package/src/generated/index.ts +1 -2
- package/src/generated/lexicon-aws.json +329 -370
- package/src/import/generator.test.ts +117 -6
- package/src/import/generator.ts +178 -62
- package/src/import/parser.ts +1 -1
- package/src/import/roundtrip-fixtures.test.ts +87 -5
- package/src/import/roundtrip.test.ts +6 -5
- package/src/index.ts +8 -1
- package/src/intrinsics.ts +27 -0
- package/src/parameter.ts +16 -0
- package/src/plugin.test.ts +1 -1
- package/src/plugin.ts +1 -0
- package/src/serializer.test.ts +4 -19
|
@@ -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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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("
|
|
55
|
-
expect(files[0].content).toContain("bucketName:
|
|
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("
|
|
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
|
|
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
|
+
}
|
package/src/parameter.ts
ADDED
|
@@ -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
|
+
}
|
package/src/plugin.test.ts
CHANGED
|
@@ -36,7 +36,7 @@ describe("awsPlugin", () => {
|
|
|
36
36
|
|
|
37
37
|
test("returns intrinsics", () => {
|
|
38
38
|
const intrinsics = awsPlugin.intrinsics!();
|
|
39
|
-
expect(intrinsics.length).toBe(
|
|
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
|
|
package/src/serializer.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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);
|