@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,125 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generate, writeGeneratedFiles } from "./generate";
|
|
3
|
+
import { NamingStrategy } from "./naming";
|
|
4
|
+
import { samResources } from "./sam";
|
|
5
|
+
import { fallbackResources } from "./fallback";
|
|
6
|
+
import { parseCFNSchema } from "../spec/parse";
|
|
7
|
+
|
|
8
|
+
describe("generate pipeline components", () => {
|
|
9
|
+
test("exports generate function", () => {
|
|
10
|
+
expect(typeof generate).toBe("function");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("exports writeGeneratedFiles function", () => {
|
|
14
|
+
expect(typeof writeGeneratedFiles).toBe("function");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Integration test - requires network, skip by default
|
|
18
|
+
test.skip("generates full pipeline (integration)", async () => {
|
|
19
|
+
const result = await generate({ verbose: false });
|
|
20
|
+
|
|
21
|
+
expect(result.resources).toBeGreaterThan(500);
|
|
22
|
+
expect(result.properties).toBeGreaterThan(0);
|
|
23
|
+
expect(result.lexiconJSON).toBeTruthy();
|
|
24
|
+
expect(result.typesDTS).toBeTruthy();
|
|
25
|
+
expect(result.indexTS).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("NamingStrategy", () => {
|
|
30
|
+
test("assigns priority names", () => {
|
|
31
|
+
const results = [
|
|
32
|
+
parseCFNSchema(JSON.stringify({ typeName: "AWS::S3::Bucket", properties: {}, additionalProperties: false })),
|
|
33
|
+
parseCFNSchema(JSON.stringify({ typeName: "AWS::Lambda::Function", properties: {}, additionalProperties: false })),
|
|
34
|
+
parseCFNSchema(JSON.stringify({ typeName: "AWS::IAM::Role", properties: {}, additionalProperties: false })),
|
|
35
|
+
];
|
|
36
|
+
const ns = new NamingStrategy(results);
|
|
37
|
+
|
|
38
|
+
expect(ns.resolve("AWS::S3::Bucket")).toBe("Bucket");
|
|
39
|
+
expect(ns.resolve("AWS::Lambda::Function")).toBe("Function");
|
|
40
|
+
expect(ns.resolve("AWS::IAM::Role")).toBe("Role");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("handles collisions with service prefix", () => {
|
|
44
|
+
const results = [
|
|
45
|
+
parseCFNSchema(JSON.stringify({ typeName: "AWS::IAM::Policy", properties: {}, additionalProperties: false })),
|
|
46
|
+
parseCFNSchema(JSON.stringify({ typeName: "AWS::S3::BucketPolicy", properties: {}, additionalProperties: false })),
|
|
47
|
+
];
|
|
48
|
+
const ns = new NamingStrategy(results);
|
|
49
|
+
|
|
50
|
+
// IAM Policy has priority name "Policy" and alias "IamPolicy"
|
|
51
|
+
expect(ns.resolve("AWS::IAM::Policy")).toBe("Policy");
|
|
52
|
+
expect(ns.aliases("AWS::IAM::Policy")).toContain("IamPolicy");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("handles unknown resources with short names", () => {
|
|
56
|
+
const results = [
|
|
57
|
+
parseCFNSchema(JSON.stringify({ typeName: "AWS::SomeService::SomeResource", properties: {}, additionalProperties: false })),
|
|
58
|
+
];
|
|
59
|
+
const ns = new NamingStrategy(results);
|
|
60
|
+
|
|
61
|
+
expect(ns.resolve("AWS::SomeService::SomeResource")).toBe("SomeResource");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("SAM resources", () => {
|
|
66
|
+
test("provides 9 SAM resource definitions", () => {
|
|
67
|
+
const resources = samResources();
|
|
68
|
+
|
|
69
|
+
expect(resources.length).toBe(9);
|
|
70
|
+
|
|
71
|
+
const typeNames = resources.map((r) => r.resource.typeName);
|
|
72
|
+
expect(typeNames).toContain("AWS::Serverless::Function");
|
|
73
|
+
expect(typeNames).toContain("AWS::Serverless::Api");
|
|
74
|
+
expect(typeNames).toContain("AWS::Serverless::HttpApi");
|
|
75
|
+
expect(typeNames).toContain("AWS::Serverless::SimpleTable");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("fallback resources", () => {
|
|
80
|
+
test("provides LogGroup fallback", () => {
|
|
81
|
+
const resources = fallbackResources();
|
|
82
|
+
|
|
83
|
+
expect(resources.length).toBeGreaterThan(0);
|
|
84
|
+
|
|
85
|
+
const typeNames = resources.map((r) => r.resource.typeName);
|
|
86
|
+
expect(typeNames).toContain("AWS::Logs::LogGroup");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("offline fixture pipeline", () => {
|
|
91
|
+
test("generates from fixtures", async () => {
|
|
92
|
+
const { loadSchemaFixtures } = await import("../testdata/load-fixtures");
|
|
93
|
+
const fixtures = loadSchemaFixtures();
|
|
94
|
+
const result = await generate({ schemaSource: fixtures });
|
|
95
|
+
|
|
96
|
+
expect(result.resources).toBeGreaterThanOrEqual(5);
|
|
97
|
+
expect(result.lexiconJSON).toBeTruthy();
|
|
98
|
+
expect(result.typesDTS).toBeTruthy();
|
|
99
|
+
expect(result.indexTS).toBeTruthy();
|
|
100
|
+
|
|
101
|
+
// Verify known resource types are present in lexicon JSON
|
|
102
|
+
const lexicon = JSON.parse(result.lexiconJSON);
|
|
103
|
+
expect(lexicon["Bucket"]).toBeDefined();
|
|
104
|
+
expect(lexicon["Function"]).toBeDefined();
|
|
105
|
+
expect(lexicon["Role"]).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("parseCFNSchema", () => {
|
|
110
|
+
test("parses a minimal schema", () => {
|
|
111
|
+
const result = parseCFNSchema(JSON.stringify({
|
|
112
|
+
typeName: "AWS::Test::Resource",
|
|
113
|
+
properties: {
|
|
114
|
+
Name: { type: "string" },
|
|
115
|
+
},
|
|
116
|
+
required: ["Name"],
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
expect(result.resource.typeName).toBe("AWS::Test::Resource");
|
|
121
|
+
expect(result.resource.properties.length).toBe(1);
|
|
122
|
+
expect(result.resource.properties[0].name).toBe("Name");
|
|
123
|
+
expect(result.resource.properties[0].required).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS CloudFormation generation pipeline — uses core generatePipeline
|
|
3
|
+
* with AWS-specific fetch, parse, naming, and generation callbacks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import {
|
|
9
|
+
generatePipeline,
|
|
10
|
+
writeGeneratedArtifacts,
|
|
11
|
+
type GenerateOptions,
|
|
12
|
+
type GenerateResult,
|
|
13
|
+
type GeneratePipelineConfig,
|
|
14
|
+
type AugmentResult,
|
|
15
|
+
} from "@intentius/chant/codegen/generate";
|
|
16
|
+
import { fetchSchemaZip } from "../spec/fetch";
|
|
17
|
+
import { parseCFNSchema, cfnShortName, type SchemaParseResult } from "../spec/parse";
|
|
18
|
+
import { fetchCfnLintPatches, applyPatches } from "./patches";
|
|
19
|
+
import { fetchCfnLintExtensions, loadExtensionSchemas, type ExtensionConstraint } from "./extensions";
|
|
20
|
+
import { samResources } from "./sam";
|
|
21
|
+
import { fallbackResources } from "./fallback";
|
|
22
|
+
import { NamingStrategy, propertyTypeName, extractDefName } from "./naming";
|
|
23
|
+
import { generateLexiconJSON, lambdaRuntimeDeprecations } from "./generate-lexicon";
|
|
24
|
+
import { generateTypeScriptDeclarations } from "./generate-typescript";
|
|
25
|
+
import {
|
|
26
|
+
generateRuntimeIndex as coreGenerateRuntimeIndex,
|
|
27
|
+
type RuntimeIndexEntry,
|
|
28
|
+
type RuntimeIndexPropertyEntry,
|
|
29
|
+
} from "@intentius/chant/codegen/generate-runtime-index";
|
|
30
|
+
|
|
31
|
+
export type { GenerateOptions, GenerateResult };
|
|
32
|
+
|
|
33
|
+
// AWS-specific state shared between pipeline callbacks
|
|
34
|
+
let awsConstraints = new Map<string, ExtensionConstraint[]>();
|
|
35
|
+
|
|
36
|
+
const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
|
|
37
|
+
fetchSchemas: async (opts) => {
|
|
38
|
+
return fetchSchemaZip(opts.force);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
parseSchema: (_typeName, data) => {
|
|
42
|
+
const result = parseCFNSchema(data);
|
|
43
|
+
if (!result.resource.typeName) return null;
|
|
44
|
+
return result;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
createNaming: (results) => new NamingStrategy(results),
|
|
48
|
+
|
|
49
|
+
augmentSchemas: async (schemas, opts, log) => {
|
|
50
|
+
const warnings: Array<{ file: string; error: string }> = [];
|
|
51
|
+
|
|
52
|
+
// Fetch and apply cfn-lint patches
|
|
53
|
+
try {
|
|
54
|
+
log("Fetching cfn-lint patches...");
|
|
55
|
+
const patchesDir = await fetchCfnLintPatches(opts.force);
|
|
56
|
+
const { schemas: patched, stats } = applyPatches(schemas, patchesDir);
|
|
57
|
+
schemas = patched;
|
|
58
|
+
log(`Applied ${stats.patchesApplied} patches across ${stats.resourcesFixed} resources`);
|
|
59
|
+
for (const w of stats.warnings) {
|
|
60
|
+
warnings.push({ file: `${w.typeName}/${w.patchFile}`, error: w.error.message });
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
log(`WARNING: failed to fetch cfn-lint patches: ${err}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fetch extension constraints (stored for use in generateRegistry)
|
|
67
|
+
try {
|
|
68
|
+
log("Fetching cfn-lint extensions...");
|
|
69
|
+
const extensionsDir = await fetchCfnLintExtensions(opts.force);
|
|
70
|
+
// We'll apply these later in generateRegistry — store them
|
|
71
|
+
// The parsedTypes set isn't available yet, so we load all and filter later
|
|
72
|
+
awsConstraints = loadExtensionSchemas(extensionsDir, new Set(schemas.keys()));
|
|
73
|
+
log(`Loaded extension constraints for ${awsConstraints.size} resource types`);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log(`WARNING: failed to fetch cfn-lint extensions: ${err}`);
|
|
76
|
+
awsConstraints = new Map();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { schemas, warnings };
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
augmentResults: (results, _opts, log) => {
|
|
83
|
+
const warnings: Array<{ file: string; error: string }> = [];
|
|
84
|
+
|
|
85
|
+
// Add SAM resources
|
|
86
|
+
results.push(...samResources());
|
|
87
|
+
log("Added SAM resources");
|
|
88
|
+
|
|
89
|
+
// Add fallback resources for missing priority types
|
|
90
|
+
const parsedTypes = new Set(results.map((r) => r.resource.typeName));
|
|
91
|
+
for (const fb of fallbackResources()) {
|
|
92
|
+
if (!parsedTypes.has(fb.resource.typeName)) {
|
|
93
|
+
results.push(fb);
|
|
94
|
+
parsedTypes.add(fb.resource.typeName);
|
|
95
|
+
warnings.push({
|
|
96
|
+
file: fb.resource.typeName,
|
|
97
|
+
error: `Priority resource missing from schema zip; using fallback`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Re-filter extension constraints now that we have the full type set
|
|
103
|
+
// (augmentSchemas loaded them before parsing, so we already have them)
|
|
104
|
+
|
|
105
|
+
log(`Total: ${results.length} resource schemas`);
|
|
106
|
+
return { results, warnings };
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
generateRegistry: (results, naming) => {
|
|
110
|
+
const runtimeDeprecations = lambdaRuntimeDeprecations();
|
|
111
|
+
return generateLexiconJSON(
|
|
112
|
+
results,
|
|
113
|
+
naming as NamingStrategy,
|
|
114
|
+
awsConstraints,
|
|
115
|
+
runtimeDeprecations,
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
generateTypes: (results, naming) => {
|
|
120
|
+
return generateTypeScriptDeclarations(results, naming as NamingStrategy);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
generateRuntimeIndex: (results, naming) => {
|
|
124
|
+
return generateRuntimeIndex(results, naming as NamingStrategy);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run the full AWS generation pipeline.
|
|
130
|
+
*/
|
|
131
|
+
export async function generate(opts: GenerateOptions = {}): Promise<GenerateResult> {
|
|
132
|
+
// Reset shared state
|
|
133
|
+
awsConstraints = new Map();
|
|
134
|
+
return generatePipeline(awsPipelineConfig, opts);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Write generated artifacts to disk.
|
|
139
|
+
*/
|
|
140
|
+
export function writeGeneratedFiles(result: GenerateResult, baseDir: string): void {
|
|
141
|
+
writeGeneratedArtifacts({
|
|
142
|
+
baseDir,
|
|
143
|
+
files: {
|
|
144
|
+
"lexicon-aws.json": result.lexiconJSON,
|
|
145
|
+
"index.d.ts": result.typesDTS,
|
|
146
|
+
"index.ts": result.indexTS,
|
|
147
|
+
"runtime.ts": [
|
|
148
|
+
"/**",
|
|
149
|
+
" * Runtime factory constructors — re-exported from core.",
|
|
150
|
+
" */",
|
|
151
|
+
'export { createResource, createProperty } from "@intentius/chant/runtime";',
|
|
152
|
+
"",
|
|
153
|
+
].join("\n"),
|
|
154
|
+
},
|
|
155
|
+
snapshot: (generatedDir) => {
|
|
156
|
+
const { snapshotArtifacts, saveSnapshot } = require("./rollback");
|
|
157
|
+
const lexiconPath = join(generatedDir, "lexicon-aws.json");
|
|
158
|
+
if (existsSync(lexiconPath)) {
|
|
159
|
+
const snapshot = snapshotArtifacts(generatedDir);
|
|
160
|
+
if (Object.keys(snapshot.files).length > 0) {
|
|
161
|
+
const snapshotsDir = join(baseDir, ".snapshots");
|
|
162
|
+
saveSnapshot(snapshot, snapshotsDir);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate the runtime index.ts with factory constructor exports.
|
|
171
|
+
*/
|
|
172
|
+
function generateRuntimeIndex(
|
|
173
|
+
results: SchemaParseResult[],
|
|
174
|
+
naming: NamingStrategy,
|
|
175
|
+
): string {
|
|
176
|
+
const resourceEntries: RuntimeIndexEntry[] = [];
|
|
177
|
+
const propertyEntries: RuntimeIndexPropertyEntry[] = [];
|
|
178
|
+
|
|
179
|
+
for (const r of results) {
|
|
180
|
+
const cfnType = r.resource.typeName;
|
|
181
|
+
const tsName = naming.resolve(cfnType);
|
|
182
|
+
if (!tsName) continue;
|
|
183
|
+
|
|
184
|
+
// Build attrs map
|
|
185
|
+
const attrs: Record<string, string> = {};
|
|
186
|
+
for (const a of r.resource.attributes) {
|
|
187
|
+
const camelName = a.name.charAt(0).toLowerCase() + a.name.slice(1);
|
|
188
|
+
attrs[camelName] = a.name;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
resourceEntries.push({ tsName, resourceType: cfnType, attrs });
|
|
192
|
+
|
|
193
|
+
for (const alias of naming.aliases(cfnType)) {
|
|
194
|
+
resourceEntries.push({ tsName: alias, resourceType: cfnType, attrs });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Property types
|
|
198
|
+
const shortName = cfnShortName(cfnType);
|
|
199
|
+
const ptAliases = naming.propertyTypeAliases(cfnType);
|
|
200
|
+
for (const pt of r.propertyTypes) {
|
|
201
|
+
const defName = extractDefName(pt.name, shortName);
|
|
202
|
+
const ptName = propertyTypeName(tsName, defName);
|
|
203
|
+
const ptCfnType = `${cfnType}.${pt.cfnType}`;
|
|
204
|
+
propertyEntries.push({ tsName: ptName, resourceType: ptCfnType });
|
|
205
|
+
|
|
206
|
+
if (ptAliases) {
|
|
207
|
+
const aliasName = ptAliases.get(defName);
|
|
208
|
+
if (aliasName) {
|
|
209
|
+
propertyEntries.push({ tsName: aliasName, resourceType: ptCfnType });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return coreGenerateRuntimeIndex(resourceEntries, propertyEntries, {
|
|
216
|
+
lexiconName: "aws",
|
|
217
|
+
intrinsicReExports: {
|
|
218
|
+
names: ["Sub", "Ref", "GetAtt", "If", "Join", "Select", "Split", "Base64"],
|
|
219
|
+
from: "../intrinsics",
|
|
220
|
+
},
|
|
221
|
+
pseudoReExports: {
|
|
222
|
+
names: ["AWS", "StackName", "Region", "AccountId", "StackId", "URLSuffix", "NoValue", "NotificationARNs", "Partition"],
|
|
223
|
+
from: "../pseudo",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generate } from "./generate";
|
|
3
|
+
import { loadSchemaFixtures } from "../testdata/load-fixtures";
|
|
4
|
+
|
|
5
|
+
describe("generation idempotency", () => {
|
|
6
|
+
test("two generations from same fixtures produce identical content", async () => {
|
|
7
|
+
const fixtures = loadSchemaFixtures();
|
|
8
|
+
|
|
9
|
+
const result1 = await generate({ schemaSource: fixtures });
|
|
10
|
+
const result2 = await generate({ schemaSource: fixtures });
|
|
11
|
+
|
|
12
|
+
// Content should be byte-identical
|
|
13
|
+
expect(result1.lexiconJSON).toBe(result2.lexiconJSON);
|
|
14
|
+
expect(result1.typesDTS).toBe(result2.typesDTS);
|
|
15
|
+
expect(result1.indexTS).toBe(result2.indexTS);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("resource counts match between runs", async () => {
|
|
19
|
+
const fixtures = loadSchemaFixtures();
|
|
20
|
+
|
|
21
|
+
const result1 = await generate({ schemaSource: fixtures });
|
|
22
|
+
const result2 = await generate({ schemaSource: fixtures });
|
|
23
|
+
|
|
24
|
+
expect(result1.resources).toBe(result2.resources);
|
|
25
|
+
expect(result1.properties).toBe(result2.properties);
|
|
26
|
+
expect(result1.enums).toBe(result2.enums);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS-specific naming configuration for the core NamingStrategy.
|
|
3
|
+
*
|
|
4
|
+
* The algorithm lives in core; only the data tables and helper functions
|
|
5
|
+
* for extracting names from AWS CloudFormation types are defined here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
NamingStrategy as CoreNamingStrategy,
|
|
10
|
+
type NamingConfig,
|
|
11
|
+
type NamingInput,
|
|
12
|
+
} from "@intentius/chant/codegen/naming";
|
|
13
|
+
|
|
14
|
+
export { propertyTypeName, extractDefName } from "@intentius/chant/codegen/naming";
|
|
15
|
+
|
|
16
|
+
import type { SchemaParseResult } from "../spec/parse";
|
|
17
|
+
import { cfnShortName, cfnServiceName } from "../spec/parse";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fixed TypeScript class names for backward compatibility.
|
|
21
|
+
*/
|
|
22
|
+
const priorityNames: Record<string, string> = {
|
|
23
|
+
"AWS::S3::Bucket": "Bucket",
|
|
24
|
+
"AWS::Lambda::Function": "Function",
|
|
25
|
+
"AWS::IAM::Role": "Role",
|
|
26
|
+
"AWS::IAM::Policy": "Policy",
|
|
27
|
+
"AWS::DynamoDB::Table": "Table",
|
|
28
|
+
"AWS::SQS::Queue": "Queue",
|
|
29
|
+
"AWS::SNS::Topic": "Topic",
|
|
30
|
+
"AWS::EC2::Instance": "Instance",
|
|
31
|
+
"AWS::EC2::SecurityGroup": "SecurityGroup",
|
|
32
|
+
"AWS::EC2::VPC": "Vpc",
|
|
33
|
+
"AWS::EC2::Subnet": "Subnet",
|
|
34
|
+
"AWS::CloudFormation::Stack": "Stack",
|
|
35
|
+
"AWS::CloudWatch::Alarm": "Alarm",
|
|
36
|
+
"AWS::Events::Rule": "EventRule",
|
|
37
|
+
"AWS::Logs::LogGroup": "LogGroup",
|
|
38
|
+
"AWS::ApiGateway::RestApi": "RestApi",
|
|
39
|
+
"AWS::ApiGatewayV2::Api": "HttpApi",
|
|
40
|
+
"AWS::ECS::Cluster": "EcsCluster",
|
|
41
|
+
"AWS::ECS::Service": "EcsService",
|
|
42
|
+
"AWS::ECS::TaskDefinition": "TaskDefinition",
|
|
43
|
+
"AWS::ElasticLoadBalancingV2::LoadBalancer": "LoadBalancer",
|
|
44
|
+
"AWS::ElasticLoadBalancingV2::TargetGroup": "TargetGroup",
|
|
45
|
+
"AWS::ElasticLoadBalancingV2::Listener": "Listener",
|
|
46
|
+
"AWS::RDS::DBInstance": "DbInstance",
|
|
47
|
+
"AWS::RDS::DBCluster": "DbCluster",
|
|
48
|
+
"AWS::StepFunctions::StateMachine": "StateMachine",
|
|
49
|
+
"AWS::KMS::Key": "KmsKey",
|
|
50
|
+
"AWS::SecretsManager::Secret": "Secret",
|
|
51
|
+
"AWS::SSM::Parameter": "SsmParameter",
|
|
52
|
+
"AWS::Lambda::Permission": "Permission",
|
|
53
|
+
"AWS::Lambda::LayerVersion": "LayerVersion",
|
|
54
|
+
"AWS::IAM::ManagedPolicy": "ManagedPolicy",
|
|
55
|
+
"AWS::IAM::InstanceProfile": "InstanceProfile",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Additional TypeScript names beyond the primary priority name.
|
|
60
|
+
*/
|
|
61
|
+
const priorityAliases: Record<string, string[]> = {
|
|
62
|
+
"AWS::IAM::Policy": ["IamPolicy"],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Property type aliases that must always be emitted for backward compat.
|
|
67
|
+
*/
|
|
68
|
+
const priorityPropertyAliases: Record<string, Record<string, string>> = {
|
|
69
|
+
"AWS::S3::Bucket": {
|
|
70
|
+
ServerSideEncryptionByDefault: "ServerSideEncryptionByDefault",
|
|
71
|
+
ServerSideEncryptionRule: "ServerSideEncryptionRule",
|
|
72
|
+
BucketEncryption: "BucketEncryption",
|
|
73
|
+
VersioningConfiguration: "VersioningConfiguration",
|
|
74
|
+
PublicAccessBlockConfiguration: "PublicAccessBlockConfiguration",
|
|
75
|
+
LoggingConfiguration: "LoggingConfiguration",
|
|
76
|
+
},
|
|
77
|
+
"AWS::Lambda::Function": {
|
|
78
|
+
Code: "Code",
|
|
79
|
+
Environment: "Environment",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Service name abbreviations for collision-resolved names.
|
|
85
|
+
*/
|
|
86
|
+
const serviceAbbreviations: Record<string, string> = {
|
|
87
|
+
ElasticLoadBalancingV2: "Elbv2",
|
|
88
|
+
CloudFormation: "Cfn",
|
|
89
|
+
CloudWatch: "Cw",
|
|
90
|
+
ApiGateway: "Apigw",
|
|
91
|
+
ApiGatewayV2: "Apigwv2",
|
|
92
|
+
SecretsManager: "Sm",
|
|
93
|
+
StepFunctions: "Sfn",
|
|
94
|
+
CertificateManager: "Acm",
|
|
95
|
+
ElasticLoadBalancing: "Elb",
|
|
96
|
+
CloudFront: "Cf",
|
|
97
|
+
ElastiCache: "Ec",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const awsNamingConfig: NamingConfig = {
|
|
101
|
+
priorityNames,
|
|
102
|
+
priorityAliases,
|
|
103
|
+
priorityPropertyAliases,
|
|
104
|
+
serviceAbbreviations,
|
|
105
|
+
shortName: cfnShortName,
|
|
106
|
+
serviceName: cfnServiceName,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* AWS-specific NamingStrategy — wraps the core algorithm with AWS data tables.
|
|
111
|
+
*/
|
|
112
|
+
export class NamingStrategy extends CoreNamingStrategy {
|
|
113
|
+
constructor(results: SchemaParseResult[]) {
|
|
114
|
+
const inputs: NamingInput[] = results.map((r) => ({
|
|
115
|
+
typeName: r.resource.typeName,
|
|
116
|
+
propertyTypes: r.propertyTypes,
|
|
117
|
+
}));
|
|
118
|
+
super(inputs, awsNamingConfig);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { packageLexicon } from "./package";
|
|
3
|
+
import { validateManifest } from "@intentius/chant/lexicon-schema";
|
|
4
|
+
|
|
5
|
+
describe("packageLexicon", () => {
|
|
6
|
+
test("produces valid BundleSpec with all artifacts", async () => {
|
|
7
|
+
const { spec, stats } = await packageLexicon();
|
|
8
|
+
|
|
9
|
+
// Manifest fields
|
|
10
|
+
expect(spec.manifest.name).toBe("aws");
|
|
11
|
+
expect(spec.manifest.version).toBeTruthy();
|
|
12
|
+
expect(spec.manifest.chantVersion).toBe(">=0.1.0");
|
|
13
|
+
expect(spec.manifest.namespace).toBe("AWS");
|
|
14
|
+
|
|
15
|
+
// Intrinsics
|
|
16
|
+
expect(spec.manifest.intrinsics).toBeDefined();
|
|
17
|
+
expect(spec.manifest.intrinsics!.length).toBeGreaterThan(0);
|
|
18
|
+
const sub = spec.manifest.intrinsics!.find((i) => i.name === "Sub");
|
|
19
|
+
expect(sub).toBeDefined();
|
|
20
|
+
expect(sub!.outputKey).toBe("Fn::Sub");
|
|
21
|
+
expect(sub!.isTag).toBe(true);
|
|
22
|
+
const ref = spec.manifest.intrinsics!.find((i) => i.name === "Ref");
|
|
23
|
+
expect(ref).toBeDefined();
|
|
24
|
+
expect(ref!.outputKey).toBe("Ref");
|
|
25
|
+
|
|
26
|
+
// Pseudo-parameters
|
|
27
|
+
expect(spec.manifest.pseudoParameters).toBeDefined();
|
|
28
|
+
expect(spec.manifest.pseudoParameters!.StackName).toBe("AWS::StackName");
|
|
29
|
+
expect(spec.manifest.pseudoParameters!.Region).toBe("AWS::Region");
|
|
30
|
+
expect(spec.manifest.pseudoParameters!.AccountId).toBe("AWS::AccountId");
|
|
31
|
+
|
|
32
|
+
// Registry (lexicon JSON)
|
|
33
|
+
expect(spec.registry.length).toBeGreaterThan(0);
|
|
34
|
+
const registry = JSON.parse(spec.registry);
|
|
35
|
+
expect(Object.keys(registry).length).toBeGreaterThan(100);
|
|
36
|
+
|
|
37
|
+
// Types
|
|
38
|
+
expect(spec.typesDTS.length).toBeGreaterThan(0);
|
|
39
|
+
expect(spec.typesDTS).toContain("export declare class");
|
|
40
|
+
|
|
41
|
+
// Rules
|
|
42
|
+
expect(spec.rules.size).toBeGreaterThan(0);
|
|
43
|
+
expect(spec.rules.has("s3-encryption.ts")).toBe(true);
|
|
44
|
+
expect(spec.rules.has("iam-wildcard.ts")).toBe(true);
|
|
45
|
+
|
|
46
|
+
// Skills
|
|
47
|
+
expect(spec.skills.size).toBeGreaterThan(0);
|
|
48
|
+
|
|
49
|
+
// Stats
|
|
50
|
+
expect(stats.resources).toBeGreaterThan(100);
|
|
51
|
+
expect(stats.ruleCount).toBeGreaterThan(0);
|
|
52
|
+
}, 120_000);
|
|
53
|
+
|
|
54
|
+
test("manifest passes validation", async () => {
|
|
55
|
+
const { spec } = await packageLexicon();
|
|
56
|
+
const validated = validateManifest(spec.manifest);
|
|
57
|
+
expect(validated.name).toBe("aws");
|
|
58
|
+
expect(validated.version).toBeTruthy();
|
|
59
|
+
}, 120_000);
|
|
60
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS lexicon packaging — delegates to core packagePipeline
|
|
3
|
+
* with AWS-specific manifest building and skill collection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import type { IntrinsicDef } from "@intentius/chant/lexicon";
|
|
10
|
+
import {
|
|
11
|
+
packagePipeline,
|
|
12
|
+
collectSkills,
|
|
13
|
+
type PackageOptions,
|
|
14
|
+
type PackageResult,
|
|
15
|
+
} from "@intentius/chant/codegen/package";
|
|
16
|
+
import { generate } from "./generate";
|
|
17
|
+
|
|
18
|
+
export type { PackageOptions, PackageResult };
|
|
19
|
+
|
|
20
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Map intrinsic name to its CloudFormation output key.
|
|
24
|
+
*/
|
|
25
|
+
function intrinsicOutputKey(name: string): string {
|
|
26
|
+
switch (name) {
|
|
27
|
+
case "Ref": return "Ref";
|
|
28
|
+
default: return `Fn::${name}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Package the AWS lexicon into a distributable BundleSpec.
|
|
34
|
+
*/
|
|
35
|
+
export async function packageLexicon(opts: PackageOptions = {}): Promise<PackageResult> {
|
|
36
|
+
const pkgJson = JSON.parse(readFileSync(join(pkgDir, "..", "package.json"), "utf-8"));
|
|
37
|
+
|
|
38
|
+
return packagePipeline(
|
|
39
|
+
{
|
|
40
|
+
generate: (genOpts) => generate({ verbose: genOpts.verbose, force: genOpts.force }),
|
|
41
|
+
|
|
42
|
+
buildManifest: (_genResult) => {
|
|
43
|
+
// Lazy-import to avoid circular dependency
|
|
44
|
+
const { awsPlugin } = require("../plugin");
|
|
45
|
+
|
|
46
|
+
const intrinsics: IntrinsicDef[] = (awsPlugin.intrinsics?.() ?? []).map(
|
|
47
|
+
(i: { name: string; description: string }) => ({
|
|
48
|
+
name: i.name,
|
|
49
|
+
description: i.description,
|
|
50
|
+
outputKey: intrinsicOutputKey(i.name),
|
|
51
|
+
isTag: i.name === "Sub",
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const pseudoParams: string[] = awsPlugin.pseudoParameters?.() ?? [];
|
|
56
|
+
const pseudoParameters: Record<string, string> = {};
|
|
57
|
+
for (const p of pseudoParams) {
|
|
58
|
+
const shortName = p.split("::").pop()!;
|
|
59
|
+
pseudoParameters[shortName] = p;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: "aws",
|
|
64
|
+
version: pkgJson.version ?? "0.0.0",
|
|
65
|
+
chantVersion: ">=0.1.0",
|
|
66
|
+
namespace: "AWS",
|
|
67
|
+
intrinsics,
|
|
68
|
+
pseudoParameters,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
srcDir: pkgDir,
|
|
73
|
+
|
|
74
|
+
collectSkills: () => {
|
|
75
|
+
const { awsPlugin } = require("../plugin");
|
|
76
|
+
const skillDefs = awsPlugin.skills?.() ?? [];
|
|
77
|
+
return collectSkills(skillDefs);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
version: pkgJson.version ?? "0.0.0",
|
|
81
|
+
},
|
|
82
|
+
opts,
|
|
83
|
+
);
|
|
84
|
+
}
|