@intentius/chant-lexicon-aws 0.1.1 → 0.1.8

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 (90) hide show
  1. package/dist/integrity.json +40 -34
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +3755 -690
  4. package/dist/rules/waw032.ts +52 -0
  5. package/dist/rules/waw033.ts +86 -0
  6. package/dist/rules/waw034.ts +63 -0
  7. package/dist/rules/waw035.ts +71 -0
  8. package/dist/rules/waw036.ts +88 -0
  9. package/dist/rules/waw037.ts +81 -0
  10. package/dist/types/index.d.ts +5043 -483
  11. package/package.json +7 -7
  12. package/src/actions/actions.test.ts +1 -1
  13. package/src/codegen/__snapshots__/snapshot.test.ts.snap +45 -45
  14. package/src/codegen/docs-links.test.ts +3 -3
  15. package/src/codegen/docs.ts +15 -10
  16. package/src/codegen/generate-cli.ts +2 -2
  17. package/src/codegen/generate.test.ts +1 -1
  18. package/src/codegen/idempotency.test.ts +1 -1
  19. package/src/codegen/package.test.ts +1 -1
  20. package/src/codegen/package.ts +2 -7
  21. package/src/codegen/snapshot.test.ts +1 -1
  22. package/src/codegen/typecheck.test.ts +1 -1
  23. package/src/composites/composites.test.ts +66 -1
  24. package/src/composites/ec2-instance-role.ts +39 -0
  25. package/src/composites/fargate-service.ts +9 -0
  26. package/src/composites/index.ts +6 -0
  27. package/src/composites/lambda-function.ts +2 -1
  28. package/src/composites/minimal-vpc.ts +71 -0
  29. package/src/composites/solr-fargate-service.ts +42 -0
  30. package/src/coverage.test.ts +1 -1
  31. package/src/default-tags.test.ts +1 -1
  32. package/src/generated/index.d.ts +5043 -483
  33. package/src/generated/index.ts +392 -46
  34. package/src/generated/lexicon-aws.json +3755 -690
  35. package/src/import/generator.test.ts +1 -1
  36. package/src/import/generator.ts +1 -1
  37. package/src/import/parser.test.ts +1 -1
  38. package/src/import/roundtrip-fixtures.test.ts +4 -4
  39. package/src/import/roundtrip.test.ts +1 -1
  40. package/src/index.ts +2 -0
  41. package/src/integration.test.ts +1 -1
  42. package/src/intrinsics.test.ts +1 -1
  43. package/src/lint/post-synth/ext001.test.ts +1 -1
  44. package/src/lint/post-synth/post-synth.test.ts +1 -1
  45. package/src/lint/post-synth/waw013.test.ts +1 -1
  46. package/src/lint/post-synth/waw014.test.ts +1 -1
  47. package/src/lint/post-synth/waw015.test.ts +1 -1
  48. package/src/lint/post-synth/waw016.test.ts +1 -1
  49. package/src/lint/post-synth/waw017.test.ts +1 -1
  50. package/src/lint/post-synth/waw018.test.ts +1 -1
  51. package/src/lint/post-synth/waw019.test.ts +1 -1
  52. package/src/lint/post-synth/waw020.test.ts +1 -1
  53. package/src/lint/post-synth/waw021.test.ts +1 -1
  54. package/src/lint/post-synth/waw022.test.ts +1 -1
  55. package/src/lint/post-synth/waw023.test.ts +1 -1
  56. package/src/lint/post-synth/waw024.test.ts +1 -1
  57. package/src/lint/post-synth/waw025.test.ts +1 -1
  58. package/src/lint/post-synth/waw026.test.ts +1 -1
  59. package/src/lint/post-synth/waw027.test.ts +1 -1
  60. package/src/lint/post-synth/waw028.test.ts +1 -1
  61. package/src/lint/post-synth/waw029.test.ts +1 -1
  62. package/src/lint/post-synth/waw030.test.ts +1 -1
  63. package/src/lint/post-synth/waw031.test.ts +1 -1
  64. package/src/lint/post-synth/waw032.test.ts +83 -0
  65. package/src/lint/post-synth/waw032.ts +52 -0
  66. package/src/lint/post-synth/waw033.test.ts +68 -0
  67. package/src/lint/post-synth/waw033.ts +86 -0
  68. package/src/lint/post-synth/waw034.test.ts +54 -0
  69. package/src/lint/post-synth/waw034.ts +63 -0
  70. package/src/lint/post-synth/waw035.test.ts +74 -0
  71. package/src/lint/post-synth/waw035.ts +71 -0
  72. package/src/lint/post-synth/waw036.test.ts +217 -0
  73. package/src/lint/post-synth/waw036.ts +88 -0
  74. package/src/lint/post-synth/waw037.test.ts +155 -0
  75. package/src/lint/post-synth/waw037.ts +81 -0
  76. package/src/lint/rules/rules.test.ts +1 -1
  77. package/src/lsp/completions.test.ts +1 -1
  78. package/src/lsp/hover.test.ts +1 -1
  79. package/src/nested-stack-integration.test.ts +2 -2
  80. package/src/nested-stack.test.ts +1 -1
  81. package/src/package-cli.ts +3 -3
  82. package/src/plugin.test.ts +5 -5
  83. package/src/plugin.ts +6 -6
  84. package/src/pseudo.test.ts +1 -1
  85. package/src/serializer.test.ts +1 -1
  86. package/src/spec/fetch.test.ts +1 -1
  87. package/src/spec/parse.test.ts +1 -1
  88. package/src/spec/parse.ts +6 -0
  89. package/src/validate-cli.ts +2 -2
  90. package/src/validate.test.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.1.1",
3
+ "version": "0.1.8",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -36,18 +36,18 @@
36
36
  "./types": "./dist/types/index.d.ts"
37
37
  },
38
38
  "scripts": {
39
- "generate": "bun run src/codegen/generate-cli.ts",
40
- "bundle": "bun run src/package-cli.ts",
41
- "validate": "bun run src/validate-cli.ts",
42
- "docs": "bun run src/codegen/docs-cli.ts",
43
- "prepack": "bun run generate && bun run bundle && bun run validate"
39
+ "generate": "tsx src/codegen/generate-cli.ts",
40
+ "bundle": "tsx src/package-cli.ts",
41
+ "validate": "tsx src/validate-cli.ts",
42
+ "docs": "tsx src/codegen/docs-cli.ts",
43
+ "prepack": "npm run generate && npm run bundle && npm run validate"
44
44
  },
45
45
  "dependencies": {
46
46
  "fflate": "^0.8.2",
47
47
  "js-yaml": "^4.1.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@intentius/chant": "0.1.1",
50
+ "@intentius/chant": "*",
51
51
  "typescript": "^5.9.3"
52
52
  },
53
53
  "peerDependencies": {
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { S3Actions } from "./s3";
3
3
  import { LambdaActions } from "./lambda";
4
4
  import { DynamoDBActions } from "./dynamodb";
@@ -1,6 +1,24 @@
1
- // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`snapshot tests Bucket lexicon entry 1`] = `
3
+ exports[`snapshot tests > Bucket .d.ts class declaration 1`] = `
4
+ "export declare class Bucket {
5
+ constructor(props: {
6
+ /** A name for the bucket. */
7
+ BucketName: string;
8
+ /** The Amazon Resource Name (ARN) of the bucket. */
9
+ Arn?: string;
10
+ /** Specifies default encryption for a bucket. */
11
+ BucketEncryption?: Bucket_BucketEncryption;
12
+ /** An arbitrary set of tags (key-value pairs) for this S3 bucket. */
13
+ Tags?: Bucket_Tag[];
14
+ /** Enables multiple versions of all objects in this bucket. */
15
+ VersioningConfiguration?: Bucket_VersioningConfiguration;
16
+ }, attributes?: CFResourceAttributes);
17
+ readonly Arn: string;
18
+ }"
19
+ `;
20
+
21
+ exports[`snapshot tests > Bucket lexicon entry 1`] = `
4
22
  {
5
23
  "attrs": {
6
24
  "Arn": "Arn",
@@ -24,7 +42,29 @@ exports[`snapshot tests Bucket lexicon entry 1`] = `
24
42
  }
25
43
  `;
26
44
 
27
- exports[`snapshot tests Function lexicon entry 1`] = `
45
+ exports[`snapshot tests > Function .d.ts class declaration 1`] = `
46
+ "export declare class Function {
47
+ constructor(props: {
48
+ /** The code for the function. */
49
+ Code: Function_Code;
50
+ /** The ARN of the function's execution role. */
51
+ Role: string;
52
+ /** The ARN of the function. */
53
+ Arn?: string;
54
+ /** The name of the Lambda function. */
55
+ FunctionName?: string;
56
+ /** The name of the method within your code that Lambda calls to run your function. */
57
+ Handler?: string;
58
+ /** The amount of memory available to the function at runtime. */
59
+ MemorySize?: number;
60
+ /** The identifier of the function's runtime. */
61
+ Runtime?: "java17" | "java21" | "nodejs18.x" | "nodejs20.x" | "python3.11" | "python3.12";
62
+ }, attributes?: CFResourceAttributes);
63
+ readonly Arn: string;
64
+ }"
65
+ `;
66
+
67
+ exports[`snapshot tests > Function lexicon entry 1`] = `
28
68
  {
29
69
  "attrs": {
30
70
  "Arn": "Arn",
@@ -76,7 +116,7 @@ exports[`snapshot tests Function lexicon entry 1`] = `
76
116
  }
77
117
  `;
78
118
 
79
- exports[`snapshot tests Role lexicon entry 1`] = `
119
+ exports[`snapshot tests > Role lexicon entry 1`] = `
80
120
  {
81
121
  "attrs": {
82
122
  "Arn": "Arn",
@@ -98,7 +138,7 @@ exports[`snapshot tests Role lexicon entry 1`] = `
98
138
  }
99
139
  `;
100
140
 
101
- exports[`snapshot tests generated resource names 1`] = `
141
+ exports[`snapshot tests > generated resource names 1`] = `
102
142
  [
103
143
  "Api",
104
144
  "Api_Auth",
@@ -155,43 +195,3 @@ exports[`snapshot tests generated resource names 1`] = `
155
195
  "VpcConfig",
156
196
  ]
157
197
  `;
158
-
159
- exports[`snapshot tests Bucket .d.ts class declaration 1`] = `
160
- "export declare class Bucket {
161
- constructor(props: {
162
- /** A name for the bucket. */
163
- BucketName: string;
164
- /** The Amazon Resource Name (ARN) of the bucket. */
165
- Arn?: string;
166
- /** Specifies default encryption for a bucket. */
167
- BucketEncryption?: Bucket_BucketEncryption;
168
- /** An arbitrary set of tags (key-value pairs) for this S3 bucket. */
169
- Tags?: Bucket_Tag[];
170
- /** Enables multiple versions of all objects in this bucket. */
171
- VersioningConfiguration?: Bucket_VersioningConfiguration;
172
- }, attributes?: CFResourceAttributes);
173
- readonly Arn: string;
174
- }"
175
- `;
176
-
177
- exports[`snapshot tests Function .d.ts class declaration 1`] = `
178
- "export declare class Function {
179
- constructor(props: {
180
- /** The code for the function. */
181
- Code: Function_Code;
182
- /** The ARN of the function's execution role. */
183
- Role: string;
184
- /** The ARN of the function. */
185
- Arn?: string;
186
- /** The name of the Lambda function. */
187
- FunctionName?: string;
188
- /** The name of the method within your code that Lambda calls to run your function. */
189
- Handler?: string;
190
- /** The amount of memory available to the function at runtime. */
191
- MemorySize?: number;
192
- /** The identifier of the function's runtime. */
193
- Runtime?: "java17" | "java21" | "nodejs18.x" | "nodejs20.x" | "python3.11" | "python3.12";
194
- }, attributes?: CFResourceAttributes);
195
- readonly Arn: string;
196
- }"
197
- `;
@@ -1,9 +1,9 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { existsSync, readdirSync, readFileSync } from "fs";
3
3
  import { join, basename } from "path";
4
4
 
5
- const docsDir = join(import.meta.dir, "..", "..", "docs", "src", "content", "docs");
6
- const docsSource = join(import.meta.dir, "docs.ts");
5
+ const docsDir = join(import.meta.dirname, "..", "..", "docs", "src", "content", "docs");
6
+ const docsSource = join(import.meta.dirname, "docs.ts");
7
7
  const docsExist = existsSync(docsDir);
8
8
 
9
9
  /**
@@ -488,12 +488,16 @@ When you need to split resources into a separate CloudFormation template, the le
488
488
  title: "Nested Stacks",
489
489
  sidebar: false,
490
490
  description: "Splitting resources into child CloudFormation templates with automatic cross-stack reference wiring",
491
- content: `CloudFormation nested stacks (\`AWS::CloudFormation::Stack\`) let you decompose large templates into smaller, reusable child templates. The AWS lexicon's \`nestedStack()\` function references a **child project directory** — a subdirectory that builds independently to a valid CloudFormation template.
491
+ content: `import Diagram from '../../components/Diagram.astro';
492
+
493
+ CloudFormation nested stacks (\`AWS::CloudFormation::Stack\`) let you decompose large templates into smaller, reusable child templates. The AWS lexicon's \`nestedStack()\` function references a **child project directory** — a subdirectory that builds independently to a valid CloudFormation template.
492
494
 
493
495
  :::caution[Consider alternatives first]
494
- Nested stacks add deployment complexity: child templates must be uploaded to S3, rollbacks are all-or-nothing at the parent level, drift detection doesn't recurse into children, and debugging failures requires drilling into child stack events. For most projects, [flat composites](../composites/) are simpler. Nested stacks are supported for specific cases — exceeding CloudFormation's 500-resource limit or packaging reusable infrastructure as a black box — but are not the recommended default.
496
+ Nested stacks add deployment complexity: child templates must be uploaded to S3, rollbacks are all-or-nothing at the parent level, drift detection doesn't recurse into children, and debugging failures requires drilling into child stack events. For splitting a large project into separately-deployed pieces, prefer [Multi-Stack Projects](/chant/guide/multi-stack/) each \`src/\` subdirectory becomes an independent stack with no parent orchestration and no S3 upload step. Nested stacks are supported for specific cases — exceeding CloudFormation's 500-resource limit inside a single deployable unit, or packaging reusable infrastructure for other teams to deploy as a black box — but are not the recommended default.
495
497
  :::
496
498
 
499
+ <Diagram name="nested-stacks" alt="Parent and child project source directories producing separate CloudFormation templates, both uploaded to S3" caption="Nested stack source layout and output" />
500
+
497
501
  ## Project structure
498
502
 
499
503
  A nested stack is a child project — a subdirectory with its own resource files and explicit \`stackOutput()\` declarations:
@@ -611,14 +615,15 @@ Three lint rules help catch common nested stack issues:
611
615
 
612
616
  ## When to use nested stacks
613
617
 
614
- **Prefer flat composites** for most projects. Composites expand into a single template, deploy atomically, and are simpler to debug.
618
+ Most splitting use cases are better served by other mechanisms:
615
619
 
616
- **Use nested stacks only when:**
617
- - Your template exceeds CloudFormation's 500-resource limit
618
- - You're packaging reusable infrastructure for other teams to deploy as a black box
619
- - You need independent update/rollback boundaries (rare — this usually means the resources should be separate stacks entirely)
620
+ - **Splitting a large project into separately-deployed pieces** → [Multi-Stack Projects](/chant/guide/multi-stack/). Organize \`src/\` into subdirectories — each becomes an independent stack, deployed on its own, with cross-stack references resolved via standard CloudFormation \`Export\`/\`ImportValue\`. No parent template, no S3 upload of children, no \`TemplateBasePath\` parameter.
621
+ - **Reusing resource patterns within a single template** [Composites](../composites/). Composites expand into the same template and deploy atomically — they're about DRY, not splitting.
620
622
 
621
- See [Composites](../composites/) for the flat composite approach.`,
623
+ **Use nested stacks only when:**
624
+ - A single deployable unit exceeds CloudFormation's 500-resource limit
625
+ - You're packaging reusable infrastructure for other teams to consume as a black box (the parent-child relationship is load-bearing)
626
+ - You need an orchestrator parent that coordinates child lifecycles via CloudFormation Parameters and Outputs specifically`,
622
627
  },
623
628
  {
624
629
  slug: "lint-rules",
@@ -902,10 +907,10 @@ The \`plugins\` array accepts relative paths. Each plugin module should export a
902
907
 
903
908
  \`\`\`bash
904
909
  cd examples/lambda-function
905
- bun install
910
+ npm install
906
911
  chant build # produces CloudFormation JSON
907
912
  chant lint # runs lint rules
908
- bun test # runs the example's tests
913
+ npx vitest run # run the tests
909
914
  \`\`\`
910
915
 
911
916
  ## Lambda Function
@@ -1,6 +1,6 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env tsx
2
2
  /**
3
- * Thin entry point for `bun run generate` in lexicon-aws.
3
+ * Thin entry point for `npm run generate` in lexicon-aws.
4
4
  */
5
5
  import { generate, writeGeneratedFiles } from "./generate";
6
6
  import { dirname } from "path";
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { generate, writeGeneratedFiles } from "./generate";
3
3
  import { NamingStrategy } from "./naming";
4
4
  import { samResources } from "./sam";
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { generate } from "./generate";
3
3
  import { loadSchemaFixtures } from "../testdata/load-fixtures";
4
4
 
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { packageLexicon } from "./package";
3
3
  import { validateManifest } from "@intentius/chant/lexicon-schema";
4
4
 
@@ -3,9 +3,8 @@
3
3
  * with AWS-specific manifest building and skill collection.
4
4
  */
5
5
 
6
- import { createRequire } from "module";
7
6
  import { readFileSync } from "fs";
8
- const require = createRequire(import.meta.url);
7
+ import { awsPlugin } from "../plugin";
9
8
  import { join, dirname } from "path";
10
9
  import { fileURLToPath } from "url";
11
10
  import type { IntrinsicDef } from "@intentius/chant/lexicon";
@@ -43,8 +42,6 @@ export async function packageLexicon(opts: PackageOptions = {}): Promise<Package
43
42
 
44
43
  buildManifest: (_genResult) => {
45
44
  // Lazy-import to avoid circular dependency
46
- const { awsPlugin } = require("../plugin");
47
-
48
45
  const intrinsics: IntrinsicDef[] = (awsPlugin.intrinsics?.() ?? []).map(
49
46
  (i: { name: string; description: string }) => ({
50
47
  name: i.name,
@@ -73,9 +70,7 @@ export async function packageLexicon(opts: PackageOptions = {}): Promise<Package
73
70
 
74
71
  srcDir: pkgDir,
75
72
 
76
- collectSkills: () => {
77
- const { awsPlugin } = require("../plugin");
78
- const skillDefs = awsPlugin.skills?.() ?? [];
73
+ collectSkills: () => { const skillDefs = awsPlugin.skills?.() ?? [];
79
74
  return collectSkills(skillDefs);
80
75
  },
81
76
 
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { generate } from "./generate";
3
3
  import { loadSchemaFixtures } from "../testdata/load-fixtures";
4
4
 
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { typecheckDTS } from "./typecheck";
3
3
 
4
4
  describe("typecheckDTS", () => {
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, beforeEach } from "bun:test";
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
2
  import { expandComposite, CompositeRegistry, isCompositeInstance } from "@intentius/chant";
3
3
  import { AttrRef } from "@intentius/chant/attrref";
4
4
  import { LambdaFunction, LambdaNode, LambdaPython, NodeLambda, PythonLambda } from "./lambda-function";
@@ -14,6 +14,8 @@ import { FargateAlb } from "./fargate-alb";
14
14
  import { AlbShared } from "./alb-shared";
15
15
  import { FargateService } from "./fargate-service";
16
16
  import { RdsInstance } from "./rds-instance";
17
+ import { Ec2InstanceRole } from "./ec2-instance-role";
18
+ import { MinimalVpc } from "./minimal-vpc";
17
19
 
18
20
  const baseProps = {
19
21
  name: "TestFunc",
@@ -865,3 +867,66 @@ describe("per-member defaults", () => {
865
867
  expect(funcPropsA.Timeout).toBe(funcPropsB.Timeout);
866
868
  });
867
869
  });
870
+
871
+ describe("Ec2InstanceRole", () => {
872
+ test("returns role and instanceProfile members", () => {
873
+ const instance = Ec2InstanceRole({});
874
+ expect(instance.role).toBeDefined();
875
+ expect(instance.instanceProfile).toBeDefined();
876
+ expect(Object.keys(instance.members)).toEqual(["role", "instanceProfile"]);
877
+ });
878
+
879
+ test("role has EC2 trust policy", () => {
880
+ const instance = Ec2InstanceRole({});
881
+ const roleProps = (instance.role as any).props;
882
+ expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("ec2.amazonaws.com");
883
+ });
884
+
885
+ test("ManagedPolicyArns are passed through", () => {
886
+ const arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore";
887
+ const instance = Ec2InstanceRole({ ManagedPolicyArns: [arn] });
888
+ const roleProps = (instance.role as any).props;
889
+ expect(roleProps.ManagedPolicyArns).toContain(arn);
890
+ });
891
+
892
+ test("expandComposite produces 2 entries", () => {
893
+ const expanded = expandComposite("myRole", Ec2InstanceRole({}));
894
+ expect(expanded.size).toBe(2);
895
+ expect(expanded.has("myRoleRole")).toBe(true);
896
+ expect(expanded.has("myRoleInstanceProfile")).toBe(true);
897
+ });
898
+ });
899
+
900
+ describe("MinimalVpc", () => {
901
+ test("returns 8 members", () => {
902
+ const instance = MinimalVpc({});
903
+ expect(Object.keys(instance.members)).toEqual([
904
+ "vpc", "subnet", "igw", "igwAttachment", "routeTable", "defaultRoute", "subnetRta", "securityGroup",
905
+ ]);
906
+ });
907
+
908
+ test("subnet uses Select/GetAZs for AZ (not a plain string)", () => {
909
+ const instance = MinimalVpc({});
910
+ const subnetProps = (instance.subnet as any).props;
911
+ expect(typeof subnetProps.AvailabilityZone).not.toBe("string");
912
+ });
913
+
914
+ test("vpc cidr defaults to 10.0.0.0/24", () => {
915
+ const instance = MinimalVpc({});
916
+ const vpcProps = (instance.vpc as any).props;
917
+ expect(vpcProps.CidrBlock).toBe("10.0.0.0/24");
918
+ });
919
+
920
+ test("custom cidr is respected", () => {
921
+ const instance = MinimalVpc({ cidr: "192.168.0.0/16", subnetCidr: "192.168.1.0/24" });
922
+ const vpcProps = (instance.vpc as any).props;
923
+ const subnetProps = (instance.subnet as any).props;
924
+ expect(vpcProps.CidrBlock).toBe("192.168.0.0/16");
925
+ expect(subnetProps.CidrBlock).toBe("192.168.1.0/24");
926
+ });
927
+
928
+ test("expandComposite produces 8 entries", () => {
929
+ const expanded = expandComposite("net", MinimalVpc({}));
930
+ expect(expanded.size).toBe(8);
931
+ });
932
+ });
@@ -0,0 +1,39 @@
1
+ import { Composite, mergeDefaults } from "@intentius/chant";
2
+ import { Role, InstanceProfile } from "../generated";
3
+ import { Ref } from "../intrinsics";
4
+
5
+ export interface Ec2InstanceRoleProps {
6
+ ManagedPolicyArns?: string[];
7
+ Policies?: ConstructorParameters<typeof Role>[0]["Policies"];
8
+ defaults?: {
9
+ role?: Partial<ConstructorParameters<typeof Role>[0]>;
10
+ instanceProfile?: Partial<ConstructorParameters<typeof InstanceProfile>[0]>;
11
+ };
12
+ }
13
+
14
+ const EC2_ASSUME_ROLE = {
15
+ Version: "2012-10-17" as const,
16
+ Statement: [
17
+ {
18
+ Effect: "Allow" as const,
19
+ Principal: { Service: "ec2.amazonaws.com" },
20
+ Action: "sts:AssumeRole",
21
+ },
22
+ ],
23
+ };
24
+
25
+ export const Ec2InstanceRole = Composite<Ec2InstanceRoleProps>((props) => {
26
+ const { defaults } = props;
27
+
28
+ const role = new Role(mergeDefaults({
29
+ AssumeRolePolicyDocument: EC2_ASSUME_ROLE,
30
+ ManagedPolicyArns: props.ManagedPolicyArns ?? [],
31
+ Policies: props.Policies ?? [],
32
+ }, defaults?.role));
33
+
34
+ const instanceProfile = new InstanceProfile(mergeDefaults({
35
+ Roles: [Ref(role)],
36
+ }, defaults?.instanceProfile));
37
+
38
+ return { role, instanceProfile };
39
+ }, "Ec2InstanceRole");
@@ -12,6 +12,7 @@ import {
12
12
  TaskDefinition_KeyValuePair,
13
13
  TaskDefinition_EFSVolumeConfiguration,
14
14
  TaskDefinition_Volume,
15
+ TaskDefinition_Ulimit,
15
16
  TargetGroup,
16
17
  ListenerRule,
17
18
  ListenerRule_Action,
@@ -74,6 +75,9 @@ export interface FargateServiceProps {
74
75
  privateSubnetIds: string[];
75
76
  healthCheckPath?: string;
76
77
 
78
+ // Ulimits (container-level)
79
+ ulimits?: Array<{ name: string; softLimit: number; hardLimit: number }>;
80
+
77
81
  // IAM
78
82
  ManagedPolicyArns?: string[];
79
83
  Policies?: InstanceType<typeof Role_Policy>[];
@@ -175,6 +179,11 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
175
179
  Environment: environmentVars.length > 0 ? environmentVars : undefined,
176
180
  Command: props.command,
177
181
  MountPoints: allMountPoints.length > 0 ? allMountPoints : undefined,
182
+ Ulimits: props.ulimits?.map(u => new TaskDefinition_Ulimit({
183
+ Name: u.name,
184
+ SoftLimit: u.softLimit,
185
+ HardLimit: u.hardLimit,
186
+ })),
178
187
  });
179
188
 
180
189
  // Task definition
@@ -26,3 +26,9 @@ export { RdsInstance, RdsInstance as RdsPostgres } from "./rds-instance";
26
26
  export type { RdsInstanceProps, RdsInstanceProps as RdsPostgresProps } from "./rds-instance";
27
27
  export { EfsWithAccessPoint } from "./efs-with-access-point";
28
28
  export type { EfsWithAccessPointProps } from "./efs-with-access-point";
29
+ export { SolrFargateService } from "./solr-fargate-service";
30
+ export type { SolrFargateServiceProps } from "./solr-fargate-service";
31
+ export { Ec2InstanceRole } from "./ec2-instance-role";
32
+ export type { Ec2InstanceRoleProps } from "./ec2-instance-role";
33
+ export { MinimalVpc } from "./minimal-vpc";
34
+ export type { MinimalVpcProps } from "./minimal-vpc";
@@ -1,5 +1,6 @@
1
1
  import { Composite, withDefaults, mergeDefaults } from "@intentius/chant";
2
2
  import { Role, Function, Function_VpcConfig, Role_Policy } from "../generated";
3
+ import { Sub } from "../intrinsics";
3
4
 
4
5
  const lambdaTrustPolicy = {
5
6
  Version: "2012-10-17" as const,
@@ -18,7 +19,7 @@ const VPC_ACCESS_ARN =
18
19
  "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole";
19
20
 
20
21
  export interface LambdaFunctionProps {
21
- name: string;
22
+ name: string | ReturnType<typeof Sub>;
22
23
  Runtime: string;
23
24
  Handler: string;
24
25
  Code: ConstructorParameters<typeof Function>[0]["Code"];
@@ -0,0 +1,71 @@
1
+ import { Composite, mergeDefaults } from "@intentius/chant";
2
+ import {
3
+ Vpc,
4
+ Subnet,
5
+ InternetGateway,
6
+ VPCGatewayAttachment,
7
+ RouteTable,
8
+ EC2Route,
9
+ SubnetRouteTableAssociation,
10
+ SecurityGroup,
11
+ } from "../generated";
12
+ import { Select, GetAZs } from "../intrinsics";
13
+
14
+ export interface MinimalVpcProps {
15
+ cidr?: string;
16
+ subnetCidr?: string;
17
+ defaults?: {
18
+ vpc?: Partial<ConstructorParameters<typeof Vpc>[0]>;
19
+ subnet?: Partial<ConstructorParameters<typeof Subnet>[0]>;
20
+ securityGroup?: Partial<ConstructorParameters<typeof SecurityGroup>[0]>;
21
+ };
22
+ }
23
+
24
+ export const MinimalVpc = Composite<MinimalVpcProps>((props) => {
25
+ const { defaults } = props;
26
+ const cidr = props.cidr ?? "10.0.0.0/24";
27
+ const subnetCidr = props.subnetCidr ?? "10.0.0.0/25";
28
+
29
+ const vpc = new Vpc(mergeDefaults({
30
+ CidrBlock: cidr,
31
+ EnableDnsHostnames: true,
32
+ EnableDnsSupport: true,
33
+ }, defaults?.vpc));
34
+
35
+ const subnet = new Subnet(mergeDefaults({
36
+ VpcId: vpc.VpcId,
37
+ CidrBlock: subnetCidr,
38
+ AvailabilityZone: Select(0, GetAZs("")),
39
+ MapPublicIpOnLaunch: true,
40
+ }, defaults?.subnet));
41
+
42
+ const igw = new InternetGateway({});
43
+
44
+ const igwAttachment = new VPCGatewayAttachment({
45
+ VpcId: vpc.VpcId,
46
+ InternetGatewayId: igw.InternetGatewayId,
47
+ });
48
+
49
+ const routeTable = new RouteTable({ VpcId: vpc.VpcId });
50
+
51
+ const defaultRoute = new EC2Route(
52
+ {
53
+ RouteTableId: routeTable.RouteTableId,
54
+ DestinationCidrBlock: "0.0.0.0/0",
55
+ GatewayId: igw.InternetGatewayId,
56
+ },
57
+ { DependsOn: [igwAttachment] },
58
+ );
59
+
60
+ const subnetRta = new SubnetRouteTableAssociation({
61
+ SubnetId: subnet.SubnetId,
62
+ RouteTableId: routeTable.RouteTableId,
63
+ });
64
+
65
+ const securityGroup = new SecurityGroup(mergeDefaults({
66
+ GroupDescription: "MinimalVpc default security group",
67
+ VpcId: vpc.VpcId,
68
+ }, defaults?.securityGroup));
69
+
70
+ return { vpc, subnet, igw, igwAttachment, routeTable, defaultRoute, subnetRta, securityGroup };
71
+ }, "MinimalVpc");
@@ -0,0 +1,42 @@
1
+ import { Composite } from "@intentius/chant";
2
+ import { FargateService, FargateServiceProps } from "./fargate-service";
3
+
4
+ export interface SolrFargateServiceProps extends FargateServiceProps {
5
+ /**
6
+ * JVM heap size passed as SOLR_HEAP. Defaults to 45% of task memory.
7
+ * Examples: "1843m", "4g". Must not exceed 50% of task memory.
8
+ */
9
+ solrHeap?: string;
10
+ /**
11
+ * GC tuning string passed as GC_TUNE.
12
+ * Default: "-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
13
+ */
14
+ gcOpts?: string;
15
+ }
16
+
17
+ export const SolrFargateService = Composite<SolrFargateServiceProps>((props) => {
18
+ const memoryMb = parseInt(props.memory ?? "4096");
19
+ const solrHeap = props.solrHeap ?? `${Math.floor(memoryMb * 0.45)}m`;
20
+ const gcOpts = props.gcOpts ?? "-XX:+UseG1GC -XX:MaxGCPauseMillis=200";
21
+
22
+ // Solr env defaults — user-supplied environment entries override these
23
+ const solrEnv: Record<string, string> = {
24
+ SOLR_HEAP: solrHeap,
25
+ GC_TUNE: gcOpts,
26
+ SOLR_OPTS: "-XX:-UseLargePages",
27
+ ...props.environment,
28
+ };
29
+
30
+ // Solr-specific ulimit default — user-supplied ulimits override
31
+ const solrUlimits = props.ulimits ?? [
32
+ { name: "nofile", softLimit: 65535, hardLimit: 65535 },
33
+ ];
34
+
35
+ return FargateService({
36
+ containerPort: 8983,
37
+ healthCheckPath: "/solr/admin/info/health",
38
+ ...props,
39
+ environment: solrEnv,
40
+ ulimits: solrUlimits,
41
+ });
42
+ }, "SolrFargateService");
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { existsSync } from "fs";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
3
3
  import { defaultTags, isDefaultTags, DEFAULT_TAGS_MARKER } from "./default-tags";
4
4