@intentius/chant 0.0.1

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 (271) hide show
  1. package/README.md +365 -0
  2. package/package.json +22 -0
  3. package/src/attrref.test.ts +148 -0
  4. package/src/attrref.ts +50 -0
  5. package/src/barrel.test.ts +157 -0
  6. package/src/barrel.ts +101 -0
  7. package/src/bench.test.ts +227 -0
  8. package/src/build.test.ts +437 -0
  9. package/src/build.ts +425 -0
  10. package/src/builder.test.ts +312 -0
  11. package/src/builder.ts +56 -0
  12. package/src/child-project.ts +44 -0
  13. package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
  14. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
  15. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
  16. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
  17. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
  18. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
  19. package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
  20. package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
  21. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
  22. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
  23. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
  24. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
  25. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
  26. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
  27. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
  28. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
  29. package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
  30. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
  31. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
  32. package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
  33. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
  35. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
  36. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
  37. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
  38. package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
  39. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
  40. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
  41. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
  42. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
  43. package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
  44. package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
  45. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
  46. package/src/cli/commands/build.test.ts +149 -0
  47. package/src/cli/commands/build.ts +344 -0
  48. package/src/cli/commands/diff.test.ts +148 -0
  49. package/src/cli/commands/diff.ts +221 -0
  50. package/src/cli/commands/doctor.test.ts +239 -0
  51. package/src/cli/commands/doctor.ts +224 -0
  52. package/src/cli/commands/import.test.ts +379 -0
  53. package/src/cli/commands/import.ts +335 -0
  54. package/src/cli/commands/init-lexicon.test.ts +297 -0
  55. package/src/cli/commands/init-lexicon.ts +993 -0
  56. package/src/cli/commands/init.test.ts +317 -0
  57. package/src/cli/commands/init.ts +505 -0
  58. package/src/cli/commands/licenses.ts +165 -0
  59. package/src/cli/commands/lint.test.ts +332 -0
  60. package/src/cli/commands/lint.ts +408 -0
  61. package/src/cli/commands/list.test.ts +100 -0
  62. package/src/cli/commands/list.ts +108 -0
  63. package/src/cli/commands/update.test.ts +38 -0
  64. package/src/cli/commands/update.ts +207 -0
  65. package/src/cli/conflict-check.test.ts +255 -0
  66. package/src/cli/conflict-check.ts +89 -0
  67. package/src/cli/debug.ts +8 -0
  68. package/src/cli/format.test.ts +140 -0
  69. package/src/cli/format.ts +133 -0
  70. package/src/cli/handlers/build.ts +58 -0
  71. package/src/cli/handlers/dev.ts +38 -0
  72. package/src/cli/handlers/init.ts +46 -0
  73. package/src/cli/handlers/lint.ts +36 -0
  74. package/src/cli/handlers/misc.ts +57 -0
  75. package/src/cli/handlers/serve.ts +26 -0
  76. package/src/cli/index.ts +3 -0
  77. package/src/cli/lsp/capabilities.ts +46 -0
  78. package/src/cli/lsp/diagnostics.ts +52 -0
  79. package/src/cli/lsp/server.test.ts +618 -0
  80. package/src/cli/lsp/server.ts +393 -0
  81. package/src/cli/main.test.ts +257 -0
  82. package/src/cli/main.ts +224 -0
  83. package/src/cli/mcp/resources/context.ts +59 -0
  84. package/src/cli/mcp/server.test.ts +747 -0
  85. package/src/cli/mcp/server.ts +402 -0
  86. package/src/cli/mcp/tools/build.ts +117 -0
  87. package/src/cli/mcp/tools/import.ts +48 -0
  88. package/src/cli/mcp/tools/lint.ts +45 -0
  89. package/src/cli/plugins.test.ts +31 -0
  90. package/src/cli/plugins.ts +94 -0
  91. package/src/cli/registry.ts +73 -0
  92. package/src/cli/reporters/stylish.test.ts +282 -0
  93. package/src/cli/reporters/stylish.ts +186 -0
  94. package/src/cli/watch.test.ts +81 -0
  95. package/src/cli/watch.ts +101 -0
  96. package/src/codegen/case.test.ts +30 -0
  97. package/src/codegen/case.ts +11 -0
  98. package/src/codegen/coverage.ts +167 -0
  99. package/src/codegen/docs.ts +634 -0
  100. package/src/codegen/fetch.test.ts +119 -0
  101. package/src/codegen/fetch.ts +261 -0
  102. package/src/codegen/generate-registry.test.ts +118 -0
  103. package/src/codegen/generate-registry.ts +107 -0
  104. package/src/codegen/generate-runtime-index.test.ts +81 -0
  105. package/src/codegen/generate-runtime-index.ts +99 -0
  106. package/src/codegen/generate-typescript.test.ts +146 -0
  107. package/src/codegen/generate-typescript.ts +161 -0
  108. package/src/codegen/generate.ts +206 -0
  109. package/src/codegen/json-patch.test.ts +113 -0
  110. package/src/codegen/json-patch.ts +151 -0
  111. package/src/codegen/json-schema.test.ts +196 -0
  112. package/src/codegen/json-schema.ts +209 -0
  113. package/src/codegen/naming.ts +201 -0
  114. package/src/codegen/package.ts +161 -0
  115. package/src/codegen/rollback.test.ts +92 -0
  116. package/src/codegen/rollback.ts +115 -0
  117. package/src/codegen/topo-sort.test.ts +69 -0
  118. package/src/codegen/topo-sort.ts +46 -0
  119. package/src/codegen/typecheck.test.ts +37 -0
  120. package/src/codegen/typecheck.ts +74 -0
  121. package/src/codegen/validate.test.ts +86 -0
  122. package/src/codegen/validate.ts +143 -0
  123. package/src/composite.test.ts +426 -0
  124. package/src/composite.ts +243 -0
  125. package/src/config.test.ts +91 -0
  126. package/src/config.ts +87 -0
  127. package/src/declarable.test.ts +160 -0
  128. package/src/declarable.ts +47 -0
  129. package/src/detectLexicon.test.ts +236 -0
  130. package/src/detectLexicon.ts +37 -0
  131. package/src/discovery/cache.test.ts +78 -0
  132. package/src/discovery/cache.ts +86 -0
  133. package/src/discovery/collect.test.ts +269 -0
  134. package/src/discovery/collect.ts +51 -0
  135. package/src/discovery/cycles.test.ts +238 -0
  136. package/src/discovery/cycles.ts +107 -0
  137. package/src/discovery/files.test.ts +154 -0
  138. package/src/discovery/files.ts +61 -0
  139. package/src/discovery/graph.test.ts +476 -0
  140. package/src/discovery/graph.ts +150 -0
  141. package/src/discovery/import.test.ts +199 -0
  142. package/src/discovery/import.ts +20 -0
  143. package/src/discovery/index.test.ts +272 -0
  144. package/src/discovery/index.ts +132 -0
  145. package/src/discovery/resolve.test.ts +267 -0
  146. package/src/discovery/resolve.ts +54 -0
  147. package/src/errors.test.ts +138 -0
  148. package/src/errors.ts +86 -0
  149. package/src/import/base-parser.test.ts +67 -0
  150. package/src/import/base-parser.ts +48 -0
  151. package/src/import/generator.ts +21 -0
  152. package/src/import/ir-utils.test.ts +103 -0
  153. package/src/import/ir-utils.ts +87 -0
  154. package/src/import/parser.ts +41 -0
  155. package/src/index.ts +60 -0
  156. package/src/intrinsic-interpolation.test.ts +91 -0
  157. package/src/intrinsic-interpolation.ts +89 -0
  158. package/src/intrinsic.test.ts +69 -0
  159. package/src/intrinsic.ts +43 -0
  160. package/src/lexicon-integrity.test.ts +94 -0
  161. package/src/lexicon-integrity.ts +69 -0
  162. package/src/lexicon-manifest.test.ts +101 -0
  163. package/src/lexicon-manifest.ts +71 -0
  164. package/src/lexicon-output.test.ts +182 -0
  165. package/src/lexicon-output.ts +82 -0
  166. package/src/lexicon-schema.test.ts +239 -0
  167. package/src/lexicon-schema.ts +144 -0
  168. package/src/lexicon.ts +212 -0
  169. package/src/lint/config-overrides.test.ts +254 -0
  170. package/src/lint/config.test.ts +644 -0
  171. package/src/lint/config.ts +375 -0
  172. package/src/lint/declarative.test.ts +256 -0
  173. package/src/lint/declarative.ts +187 -0
  174. package/src/lint/engine.test.ts +465 -0
  175. package/src/lint/engine.ts +172 -0
  176. package/src/lint/named-checks.test.ts +37 -0
  177. package/src/lint/named-checks.ts +33 -0
  178. package/src/lint/parser.test.ts +129 -0
  179. package/src/lint/parser.ts +42 -0
  180. package/src/lint/post-synth.test.ts +113 -0
  181. package/src/lint/post-synth.ts +76 -0
  182. package/src/lint/presets/relaxed.json +19 -0
  183. package/src/lint/presets/strict.json +19 -0
  184. package/src/lint/rule-loader.test.ts +67 -0
  185. package/src/lint/rule-loader.ts +67 -0
  186. package/src/lint/rule-options.test.ts +141 -0
  187. package/src/lint/rule.test.ts +196 -0
  188. package/src/lint/rule.ts +98 -0
  189. package/src/lint/rules/barrel-import-style.test.ts +80 -0
  190. package/src/lint/rules/barrel-import-style.ts +59 -0
  191. package/src/lint/rules/composite-scope.ts +55 -0
  192. package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
  193. package/src/lint/rules/cor017-composite-name-match.ts +108 -0
  194. package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
  195. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
  196. package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
  197. package/src/lint/rules/declarable-naming-convention.ts +70 -0
  198. package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
  199. package/src/lint/rules/enforce-barrel-import.ts +81 -0
  200. package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
  201. package/src/lint/rules/enforce-barrel-ref.ts +75 -0
  202. package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
  203. package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
  204. package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
  205. package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
  206. package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
  207. package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
  208. package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
  209. package/src/lint/rules/evl004-spread-non-const.ts +111 -0
  210. package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
  211. package/src/lint/rules/evl005-resource-block-body.ts +49 -0
  212. package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
  213. package/src/lint/rules/evl006-barrel-usage.ts +95 -0
  214. package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
  215. package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
  216. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
  217. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
  218. package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
  219. package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
  220. package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
  221. package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
  222. package/src/lint/rules/export-required.test.ts +213 -0
  223. package/src/lint/rules/export-required.ts +158 -0
  224. package/src/lint/rules/file-declarable-limit.test.ts +148 -0
  225. package/src/lint/rules/file-declarable-limit.ts +96 -0
  226. package/src/lint/rules/flat-declarations.test.ts +210 -0
  227. package/src/lint/rules/flat-declarations.ts +70 -0
  228. package/src/lint/rules/index.ts +99 -0
  229. package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
  230. package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
  231. package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
  232. package/src/lint/rules/no-redundant-type-import.ts +85 -0
  233. package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
  234. package/src/lint/rules/no-redundant-value-cast.ts +46 -0
  235. package/src/lint/rules/no-string-ref.test.ts +100 -0
  236. package/src/lint/rules/no-string-ref.ts +66 -0
  237. package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
  238. package/src/lint/rules/no-unused-declarable-import.ts +103 -0
  239. package/src/lint/rules/no-unused-declarable.test.ts +134 -0
  240. package/src/lint/rules/no-unused-declarable.ts +118 -0
  241. package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
  242. package/src/lint/rules/prefer-namespace-import.ts +63 -0
  243. package/src/lint/rules/single-concern-file.test.ts +156 -0
  244. package/src/lint/rules/single-concern-file.ts +98 -0
  245. package/src/lint/rules/stale-barrel-types.ts +60 -0
  246. package/src/lint/selectors.test.ts +113 -0
  247. package/src/lint/selectors.ts +188 -0
  248. package/src/lsp/lexicon-providers.ts +191 -0
  249. package/src/lsp/types.ts +79 -0
  250. package/src/mcp/types.ts +22 -0
  251. package/src/project/scan.test.ts +178 -0
  252. package/src/project/scan.ts +182 -0
  253. package/src/project/sync.test.ts +87 -0
  254. package/src/project/sync.ts +46 -0
  255. package/src/project-validation.test.ts +64 -0
  256. package/src/project-validation.ts +79 -0
  257. package/src/pseudo-parameter.test.ts +39 -0
  258. package/src/pseudo-parameter.ts +47 -0
  259. package/src/runtime.ts +68 -0
  260. package/src/serializer-walker.test.ts +124 -0
  261. package/src/serializer-walker.ts +83 -0
  262. package/src/serializer.ts +42 -0
  263. package/src/sort.test.ts +290 -0
  264. package/src/sort.ts +58 -0
  265. package/src/stack-output.ts +82 -0
  266. package/src/types.test.ts +307 -0
  267. package/src/types.ts +46 -0
  268. package/src/utils.test.ts +195 -0
  269. package/src/utils.ts +46 -0
  270. package/src/validation.test.ts +308 -0
  271. package/src/validation.ts +50 -0
@@ -0,0 +1,124 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { walkValue, type SerializerVisitor } from "./serializer-walker";
3
+ import { DECLARABLE_MARKER, type Declarable } from "./declarable";
4
+ import { INTRINSIC_MARKER } from "./intrinsic";
5
+ import { AttrRef } from "./attrref";
6
+
7
+ function makeDeclarable(type: string, kind: "resource" | "property" = "resource", props?: Record<string, unknown>): Declarable & { props?: Record<string, unknown> } {
8
+ const d: Declarable & { props?: Record<string, unknown> } = {
9
+ lexicon: "test",
10
+ entityType: type,
11
+ kind,
12
+ [DECLARABLE_MARKER]: true as const,
13
+ };
14
+ if (props) d.props = props;
15
+ return d;
16
+ }
17
+
18
+ const mockVisitor: SerializerVisitor = {
19
+ attrRef: (name, attr) => ({ __getAtt: [name, attr] }),
20
+ resourceRef: (name) => ({ __ref: name }),
21
+ propertyDeclarable: (entity, walk) => {
22
+ if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
23
+ return undefined;
24
+ }
25
+ const props = entity.props as Record<string, unknown>;
26
+ const result: Record<string, unknown> = {};
27
+ for (const [k, v] of Object.entries(props)) {
28
+ if (v !== undefined) result[k] = walk(v);
29
+ }
30
+ return Object.keys(result).length > 0 ? result : undefined;
31
+ },
32
+ };
33
+
34
+ describe("walkValue", () => {
35
+ test("returns null and undefined as-is", () => {
36
+ const names = new Map<Declarable, string>();
37
+ expect(walkValue(null, names, mockVisitor)).toBe(null);
38
+ expect(walkValue(undefined, names, mockVisitor)).toBe(undefined);
39
+ });
40
+
41
+ test("returns primitives as-is", () => {
42
+ const names = new Map<Declarable, string>();
43
+ expect(walkValue(42, names, mockVisitor)).toBe(42);
44
+ expect(walkValue("hello", names, mockVisitor)).toBe("hello");
45
+ expect(walkValue(true, names, mockVisitor)).toBe(true);
46
+ });
47
+
48
+ test("handles AttrRef", () => {
49
+ const parent = makeDeclarable("Test::Resource");
50
+ const ref = new AttrRef(parent, "arn");
51
+ ref._setLogicalName("MyResource");
52
+
53
+ const names = new Map<Declarable, string>([[parent, "MyResource"]]);
54
+ expect(walkValue(ref, names, mockVisitor)).toEqual({ __getAtt: ["MyResource", "arn"] });
55
+ });
56
+
57
+ test("throws for AttrRef without logical name", () => {
58
+ const parent = makeDeclarable("Test::Resource");
59
+ const ref = new AttrRef(parent, "arn");
60
+
61
+ const names = new Map<Declarable, string>();
62
+ expect(() => walkValue(ref, names, mockVisitor)).toThrow("logical name not set");
63
+ });
64
+
65
+ test("handles intrinsic with toJSON", () => {
66
+ const intrinsic = {
67
+ [INTRINSIC_MARKER]: true as const,
68
+ toJSON: () => ({ MyIntrinsic: "value" }),
69
+ };
70
+ const names = new Map<Declarable, string>();
71
+ expect(walkValue(intrinsic, names, mockVisitor)).toEqual({ MyIntrinsic: "value" });
72
+ });
73
+
74
+ test("handles resource Declarable via resourceRef", () => {
75
+ const resource = makeDeclarable("Test::Bucket");
76
+ const names = new Map<Declarable, string>([[resource, "MyBucket"]]);
77
+ expect(walkValue(resource, names, mockVisitor)).toEqual({ __ref: "MyBucket" });
78
+ });
79
+
80
+ test("handles property Declarable via propertyDeclarable", () => {
81
+ const prop = makeDeclarable("Test::Config", "property", { key: "value" });
82
+ const names = new Map<Declarable, string>();
83
+ expect(walkValue(prop, names, mockVisitor)).toEqual({ key: "value" });
84
+ });
85
+
86
+ test("recurses into arrays", () => {
87
+ const names = new Map<Declarable, string>();
88
+ expect(walkValue([1, "two", [3]], names, mockVisitor)).toEqual([1, "two", [3]]);
89
+ });
90
+
91
+ test("recurses into objects", () => {
92
+ const names = new Map<Declarable, string>();
93
+ expect(walkValue({ a: 1, b: { c: 2 } }, names, mockVisitor)).toEqual({ a: 1, b: { c: 2 } });
94
+ });
95
+
96
+ test("applies transformKey when provided", () => {
97
+ const visitor: SerializerVisitor = {
98
+ ...mockVisitor,
99
+ transformKey: (k) => k.toUpperCase(),
100
+ };
101
+ const names = new Map<Declarable, string>();
102
+ expect(walkValue({ foo: 1, bar: 2 }, names, visitor)).toEqual({ FOO: 1, BAR: 2 });
103
+ });
104
+
105
+ test("complex nested structure", () => {
106
+ const resource = makeDeclarable("Test::Role");
107
+ const ref = new AttrRef(resource, "arn");
108
+ ref._setLogicalName("MyRole");
109
+
110
+ const names = new Map<Declarable, string>([[resource, "MyRole"]]);
111
+ const value = {
112
+ config: {
113
+ role: resource,
114
+ items: [ref, "static"],
115
+ },
116
+ };
117
+ expect(walkValue(value, names, mockVisitor)).toEqual({
118
+ config: {
119
+ role: { __ref: "MyRole" },
120
+ items: [{ __getAtt: ["MyRole", "arn"] }, "static"],
121
+ },
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Generic recursive value walker for lexicon serializers.
3
+ *
4
+ * Implements the dispatch chain: null → AttrRef → Intrinsic → Declarable → Array → Object,
5
+ * delegating format-specific behavior to a SerializerVisitor.
6
+ */
7
+
8
+ import type { Declarable } from "./declarable";
9
+ import { isPropertyDeclarable } from "./declarable";
10
+ import { INTRINSIC_MARKER } from "./intrinsic";
11
+ import { AttrRef } from "./attrref";
12
+
13
+ export interface SerializerVisitor {
14
+ /** Format an attribute reference (e.g. CFN Fn::GetAttr). */
15
+ attrRef(logicalName: string, attribute: string): unknown;
16
+ /** Format a resource-level Declarable reference (e.g. CFN Ref). */
17
+ resourceRef(logicalName: string): unknown;
18
+ /** Format a property-level Declarable by walking its props. */
19
+ propertyDeclarable(entity: Declarable, walk: (v: unknown) => unknown): unknown;
20
+ /** Optional key transformation (e.g. camelCase → PascalCase). */
21
+ transformKey?(key: string): string;
22
+ }
23
+
24
+ /**
25
+ * Recursively walk a value, converting AttrRefs, Intrinsics, Declarables,
26
+ * arrays, and objects using the provided visitor.
27
+ */
28
+ export function walkValue(
29
+ value: unknown,
30
+ entityNames: Map<Declarable, string>,
31
+ visitor: SerializerVisitor,
32
+ ): unknown {
33
+ if (value === null || value === undefined) {
34
+ return value;
35
+ }
36
+
37
+ // Handle AttrRef
38
+ if (value instanceof AttrRef) {
39
+ const name = value.getLogicalName();
40
+ if (!name) {
41
+ throw new Error(
42
+ `Cannot serialize AttrRef for attribute "${value.attribute}": logical name not set`
43
+ );
44
+ }
45
+ return visitor.attrRef(name, value.attribute);
46
+ }
47
+
48
+ // Handle Intrinsics
49
+ if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
50
+ if ("toJSON" in value && typeof value.toJSON === "function") {
51
+ return value.toJSON();
52
+ }
53
+ }
54
+
55
+ // Handle Declarable references
56
+ if (typeof value === "object" && value !== null && "entityType" in value) {
57
+ const decl = value as Declarable;
58
+ if (isPropertyDeclarable(decl)) {
59
+ return visitor.propertyDeclarable(decl, (v) => walkValue(v, entityNames, visitor));
60
+ }
61
+ const name = entityNames.get(decl);
62
+ if (name) {
63
+ return visitor.resourceRef(name);
64
+ }
65
+ }
66
+
67
+ // Handle arrays
68
+ if (Array.isArray(value)) {
69
+ return value.map((item) => walkValue(item, entityNames, visitor));
70
+ }
71
+
72
+ // Handle objects
73
+ if (typeof value === "object") {
74
+ const result: Record<string, unknown> = {};
75
+ for (const [key, val] of Object.entries(value)) {
76
+ const outKey = visitor.transformKey ? visitor.transformKey(key) : key;
77
+ result[outKey] = walkValue(val, entityNames, visitor);
78
+ }
79
+ return result;
80
+ }
81
+
82
+ return value;
83
+ }
@@ -0,0 +1,42 @@
1
+ import type { Declarable } from "./declarable";
2
+ import type { LexiconOutput } from "./lexicon-output";
3
+
4
+ /**
5
+ * Result of serialization that may include additional files (e.g. nested stack templates).
6
+ */
7
+ export interface SerializerResult {
8
+ /** Primary template content */
9
+ primary: string;
10
+ /** Additional files keyed by filename (e.g. "network.template.json" → content) */
11
+ files?: Record<string, string>;
12
+ }
13
+
14
+ /**
15
+ * Serializer interface for chant specifications
16
+ */
17
+ export interface Serializer {
18
+ /**
19
+ * Name of the lexicon
20
+ */
21
+ name: string;
22
+
23
+ /**
24
+ * Prefix used for rules in this lexicon
25
+ */
26
+ rulePrefix: string;
27
+
28
+ /**
29
+ * Serializes the entities to a string representation
30
+ * @param entities - Map of entity name to Declarable entity
31
+ * @param outputs - Optional array of LexiconOutputs produced by this lexicon
32
+ */
33
+ serialize(entities: Map<string, Declarable>, outputs?: LexiconOutput[]): string | SerializerResult;
34
+
35
+ /**
36
+ * Serialize a cross-lexicon reference to a foreign output.
37
+ * Called when this lexicon consumes an output produced by another lexicon.
38
+ * @param output - The LexiconOutput being referenced
39
+ * @returns Lexicon-specific reference representation
40
+ */
41
+ serializeCrossRef?(output: LexiconOutput): unknown;
42
+ }
@@ -0,0 +1,290 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { topologicalSort } from "./sort";
3
+ import { BuildError } from "./errors";
4
+
5
+ describe("topologicalSort", () => {
6
+ test("returns empty array for empty dependencies", () => {
7
+ const dependencies = {};
8
+ const result = topologicalSort(dependencies);
9
+ expect(result).toEqual([]);
10
+ });
11
+
12
+ test("returns single node with no dependencies", () => {
13
+ const dependencies = {
14
+ A: [],
15
+ };
16
+ const result = topologicalSort(dependencies);
17
+ expect(result).toEqual(["A"]);
18
+ });
19
+
20
+ test("sorts linear dependency chain", () => {
21
+ const dependencies = {
22
+ A: [],
23
+ B: ["A"],
24
+ C: ["B"],
25
+ D: ["C"],
26
+ };
27
+ const result = topologicalSort(dependencies);
28
+ expect(result).toEqual(["A", "B", "C", "D"]);
29
+ });
30
+
31
+ test("sorts diamond dependency pattern", () => {
32
+ const dependencies = {
33
+ A: [],
34
+ B: ["A"],
35
+ C: ["A"],
36
+ D: ["B", "C"],
37
+ };
38
+ const result = topologicalSort(dependencies);
39
+
40
+ // A must come first, D must come last
41
+ expect(result[0]).toBe("A");
42
+ expect(result[3]).toBe("D");
43
+
44
+ // B and C must come before D but after A
45
+ const bIndex = result.indexOf("B");
46
+ const cIndex = result.indexOf("C");
47
+ const dIndex = result.indexOf("D");
48
+ expect(bIndex).toBeLessThan(dIndex);
49
+ expect(cIndex).toBeLessThan(dIndex);
50
+ });
51
+
52
+ test("sorts multiple independent nodes", () => {
53
+ const dependencies = {
54
+ A: [],
55
+ B: [],
56
+ C: [],
57
+ };
58
+ const result = topologicalSort(dependencies);
59
+ expect(result).toHaveLength(3);
60
+ expect(result).toContain("A");
61
+ expect(result).toContain("B");
62
+ expect(result).toContain("C");
63
+ });
64
+
65
+ test("sorts complex graph with multiple levels", () => {
66
+ const dependencies = {
67
+ A: [],
68
+ B: [],
69
+ C: ["A"],
70
+ D: ["A", "B"],
71
+ E: ["C", "D"],
72
+ };
73
+ const result = topologicalSort(dependencies);
74
+
75
+ // A and B must come before C, D, E
76
+ const aIndex = result.indexOf("A");
77
+ const bIndex = result.indexOf("B");
78
+ const cIndex = result.indexOf("C");
79
+ const dIndex = result.indexOf("D");
80
+ const eIndex = result.indexOf("E");
81
+
82
+ expect(aIndex).toBeLessThan(cIndex);
83
+ expect(aIndex).toBeLessThan(dIndex);
84
+ expect(aIndex).toBeLessThan(eIndex);
85
+ expect(bIndex).toBeLessThan(dIndex);
86
+ expect(bIndex).toBeLessThan(eIndex);
87
+ expect(cIndex).toBeLessThan(eIndex);
88
+ expect(dIndex).toBeLessThan(eIndex);
89
+ });
90
+
91
+ test("handles node with multiple dependencies", () => {
92
+ const dependencies = {
93
+ A: [],
94
+ B: [],
95
+ C: [],
96
+ D: ["A", "B", "C"],
97
+ };
98
+ const result = topologicalSort(dependencies);
99
+
100
+ // A, B, C must all come before D
101
+ const dIndex = result.indexOf("D");
102
+ expect(result.indexOf("A")).toBeLessThan(dIndex);
103
+ expect(result.indexOf("B")).toBeLessThan(dIndex);
104
+ expect(result.indexOf("C")).toBeLessThan(dIndex);
105
+ });
106
+
107
+ test("throws BuildError for self-loop", () => {
108
+ const dependencies = {
109
+ A: ["A"],
110
+ };
111
+
112
+ expect(() => topologicalSort(dependencies)).toThrow(BuildError);
113
+ expect(() => topologicalSort(dependencies)).toThrow(/Circular dependency detected/);
114
+ });
115
+
116
+ test("throws BuildError for two-node cycle", () => {
117
+ const dependencies = {
118
+ A: ["B"],
119
+ B: ["A"],
120
+ };
121
+
122
+ expect(() => topologicalSort(dependencies)).toThrow(BuildError);
123
+ expect(() => topologicalSort(dependencies)).toThrow(/Circular dependency detected/);
124
+ });
125
+
126
+ test("throws BuildError for three-node cycle", () => {
127
+ const dependencies = {
128
+ A: ["B"],
129
+ B: ["C"],
130
+ C: ["A"],
131
+ };
132
+
133
+ expect(() => topologicalSort(dependencies)).toThrow(BuildError);
134
+ expect(() => topologicalSort(dependencies)).toThrow(/Circular dependency detected/);
135
+ });
136
+
137
+ test("throws BuildError for cycle in complex graph", () => {
138
+ const dependencies = {
139
+ A: [],
140
+ B: ["A"],
141
+ C: ["B"],
142
+ D: ["C"],
143
+ E: ["D", "B"],
144
+ F: ["E"],
145
+ G: ["F", "C"],
146
+ // Create cycle: G -> C -> B, but also E -> B
147
+ H: ["G"],
148
+ I: ["H", "A"],
149
+ J: ["I"],
150
+ // Add the cycle
151
+ K: ["J"],
152
+ L: ["K"],
153
+ M: ["L", "K"],
154
+ };
155
+
156
+ // Add actual cycle
157
+ dependencies.B = ["M"];
158
+
159
+ expect(() => topologicalSort(dependencies)).toThrow(BuildError);
160
+ });
161
+
162
+ test("BuildError contains entity name from cycle", () => {
163
+ const dependencies = {
164
+ A: ["B"],
165
+ B: ["C"],
166
+ C: ["A"],
167
+ };
168
+
169
+ try {
170
+ topologicalSort(dependencies);
171
+ expect(true).toBe(false); // Should not reach here
172
+ } catch (error) {
173
+ expect(error).toBeInstanceOf(BuildError);
174
+ if (error instanceof BuildError) {
175
+ // Entity name should be one of the nodes in the cycle
176
+ expect(["A", "B", "C"]).toContain(error.entityName);
177
+ }
178
+ }
179
+ });
180
+
181
+ test("BuildError message includes cycle path", () => {
182
+ const dependencies = {
183
+ A: ["B"],
184
+ B: ["C"],
185
+ C: ["A"],
186
+ };
187
+
188
+ try {
189
+ topologicalSort(dependencies);
190
+ expect(true).toBe(false); // Should not reach here
191
+ } catch (error) {
192
+ expect(error).toBeInstanceOf(BuildError);
193
+ if (error instanceof BuildError) {
194
+ expect(error.message).toContain("Circular dependency detected");
195
+ expect(error.message).toContain("->");
196
+ }
197
+ }
198
+ });
199
+
200
+ test("sorts disconnected components", () => {
201
+ const dependencies = {
202
+ A: [],
203
+ B: ["A"],
204
+ C: [],
205
+ D: ["C"],
206
+ };
207
+ const result = topologicalSort(dependencies);
208
+
209
+ // Check ordering within each component
210
+ const aIndex = result.indexOf("A");
211
+ const bIndex = result.indexOf("B");
212
+ const cIndex = result.indexOf("C");
213
+ const dIndex = result.indexOf("D");
214
+
215
+ expect(aIndex).toBeLessThan(bIndex);
216
+ expect(cIndex).toBeLessThan(dIndex);
217
+ expect(result).toHaveLength(4);
218
+ });
219
+
220
+ test("handles nodes with empty dependency arrays", () => {
221
+ const dependencies = {
222
+ A: [],
223
+ B: [],
224
+ C: ["A", "B"],
225
+ };
226
+ const result = topologicalSort(dependencies);
227
+
228
+ const cIndex = result.indexOf("C");
229
+ expect(result.indexOf("A")).toBeLessThan(cIndex);
230
+ expect(result.indexOf("B")).toBeLessThan(cIndex);
231
+ });
232
+
233
+ test("preserves all nodes in result", () => {
234
+ const dependencies = {
235
+ A: [],
236
+ B: ["A"],
237
+ C: ["A"],
238
+ D: ["B", "C"],
239
+ E: [],
240
+ F: ["E"],
241
+ };
242
+ const result = topologicalSort(dependencies);
243
+
244
+ expect(result).toHaveLength(6);
245
+ expect(new Set(result).size).toBe(6); // No duplicates
246
+ expect(result).toContain("A");
247
+ expect(result).toContain("B");
248
+ expect(result).toContain("C");
249
+ expect(result).toContain("D");
250
+ expect(result).toContain("E");
251
+ expect(result).toContain("F");
252
+ });
253
+
254
+ test("handles dependencies on non-existent nodes", () => {
255
+ const dependencies = {
256
+ A: ["B"], // B doesn't exist as a key
257
+ C: [],
258
+ };
259
+
260
+ // Should handle gracefully - B is referenced but not defined
261
+ const result = topologicalSort(dependencies);
262
+ expect(result).toContain("A");
263
+ expect(result).toContain("C");
264
+ });
265
+
266
+ test("handles realistic CloudFormation-style resource dependencies", () => {
267
+ const dependencies = {
268
+ MyVPC: [],
269
+ MySubnet: ["MyVPC"],
270
+ MySecurityGroup: ["MyVPC"],
271
+ MyInstance: ["MySubnet", "MySecurityGroup"],
272
+ MyEIP: ["MyInstance"],
273
+ };
274
+ const result = topologicalSort(dependencies);
275
+
276
+ // VPC must be first
277
+ expect(result[0]).toBe("MyVPC");
278
+
279
+ // EIP must be last
280
+ expect(result[result.length - 1]).toBe("MyEIP");
281
+
282
+ // Subnet and SecurityGroup must come before Instance
283
+ const subnetIndex = result.indexOf("MySubnet");
284
+ const sgIndex = result.indexOf("MySecurityGroup");
285
+ const instanceIndex = result.indexOf("MyInstance");
286
+
287
+ expect(subnetIndex).toBeLessThan(instanceIndex);
288
+ expect(sgIndex).toBeLessThan(instanceIndex);
289
+ });
290
+ });
package/src/sort.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { BuildError } from "./errors";
2
+ import { detectCycles } from "./discovery/cycles";
3
+
4
+ /**
5
+ * Performs a topological sort on a dependency graph
6
+ * @param dependencies - Record where keys are entity names and values are arrays of their dependencies
7
+ * @returns Array of entity names in topological order (dependencies appear before dependents)
8
+ * @throws {BuildError} If a cycle is detected in the dependency graph
9
+ */
10
+ export function topologicalSort(
11
+ dependencies: Record<string, string[]>
12
+ ): string[] {
13
+ // Check for cycles first
14
+ const cycles = detectCycles(dependencies);
15
+ if (cycles.length > 0) {
16
+ const cycleStr = cycles[0].join(" -> ");
17
+ throw new BuildError(
18
+ cycles[0][0],
19
+ `Circular dependency detected: ${cycleStr}`
20
+ );
21
+ }
22
+
23
+ const sorted: string[] = [];
24
+ const visited = new Set<string>();
25
+ const visiting = new Set<string>();
26
+
27
+ /**
28
+ * DFS helper for topological sort
29
+ * @param node - Current node being visited
30
+ */
31
+ function visit(node: string): void {
32
+ if (visited.has(node)) {
33
+ return;
34
+ }
35
+
36
+ visiting.add(node);
37
+
38
+ const deps = dependencies[node] || [];
39
+ for (const dep of deps) {
40
+ if (!visited.has(dep)) {
41
+ visit(dep);
42
+ }
43
+ }
44
+
45
+ visiting.delete(node);
46
+ visited.add(node);
47
+ sorted.push(node);
48
+ }
49
+
50
+ // Visit all nodes
51
+ for (const node of Object.keys(dependencies)) {
52
+ if (!visited.has(node)) {
53
+ visit(node);
54
+ }
55
+ }
56
+
57
+ return sorted;
58
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Stack Output — marks a value for cross-stack export.
3
+ *
4
+ * When a child project declares `stackOutput(ref)`, the serializer emits
5
+ * it into the template's Outputs section. The parent can then reference
6
+ * it via `nestedStack().outputs.name`.
7
+ */
8
+
9
+ import { DECLARABLE_MARKER, type Declarable } from "./declarable";
10
+ import type { AttrRef } from "./attrref";
11
+
12
+ /**
13
+ * Marker symbol for stack output identification.
14
+ */
15
+ export const STACK_OUTPUT_MARKER = Symbol.for("chant.stackOutput");
16
+
17
+ /**
18
+ * A stack output declaration — wraps an AttrRef into a Declarable
19
+ * that serializers emit as a template Output.
20
+ */
21
+ export interface StackOutput extends Declarable {
22
+ readonly [STACK_OUTPUT_MARKER]: true;
23
+ readonly [DECLARABLE_MARKER]: true;
24
+ readonly lexicon: string;
25
+ readonly entityType: string;
26
+ readonly kind: "output";
27
+ readonly sourceRef: AttrRef;
28
+ readonly description?: string;
29
+ }
30
+
31
+ /**
32
+ * Type guard for StackOutput.
33
+ */
34
+ export function isStackOutput(value: unknown): value is StackOutput {
35
+ return (
36
+ typeof value === "object" &&
37
+ value !== null &&
38
+ STACK_OUTPUT_MARKER in value &&
39
+ (value as Record<symbol, unknown>)[STACK_OUTPUT_MARKER] === true
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Create a stack output that exports an attribute reference for cross-stack use.
45
+ *
46
+ * @param ref - The AttrRef to export (e.g. `vpc.vpcId`)
47
+ * @param options - Optional description for the output
48
+ * @returns A StackOutput Declarable
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { stackOutput } from "@intentius/chant";
53
+ * import * as _ from "./_";
54
+ *
55
+ * export const vpcId = stackOutput(_.$.vpc.vpcId);
56
+ * export const subnetId = stackOutput(_.$.subnet.subnetId, {
57
+ * description: "Primary subnet ID",
58
+ * });
59
+ * ```
60
+ */
61
+ export function stackOutput(
62
+ ref: AttrRef,
63
+ options?: { description?: string },
64
+ ): StackOutput {
65
+ // Derive lexicon from the AttrRef's parent entity
66
+ const parent = ref.parent.deref();
67
+ const lexicon = parent && typeof (parent as any).lexicon === "string"
68
+ ? (parent as any).lexicon
69
+ : "unknown";
70
+
71
+ const output: StackOutput = {
72
+ [STACK_OUTPUT_MARKER]: true,
73
+ [DECLARABLE_MARKER]: true,
74
+ lexicon,
75
+ entityType: "chant:output",
76
+ kind: "output",
77
+ sourceRef: ref,
78
+ description: options?.description,
79
+ };
80
+
81
+ return output;
82
+ }