@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,99 @@
1
+ /**
2
+ * Generic runtime index generator for lexicon packages.
3
+ *
4
+ * Produces the `index.ts` file containing factory constructor exports
5
+ * (`createResource`, `createProperty`) and collision-safe re-exports.
6
+ */
7
+
8
+ export interface RuntimeIndexConfig {
9
+ /** Lexicon identifier: "aws", "terraform", etc. */
10
+ lexiconName: string;
11
+ /** Optional re-exports of intrinsic functions. */
12
+ intrinsicReExports?: {
13
+ names: string[];
14
+ from: string;
15
+ };
16
+ /** Optional re-exports of pseudo-parameters. */
17
+ pseudoReExports?: {
18
+ names: string[];
19
+ from: string;
20
+ };
21
+ }
22
+
23
+ export interface RuntimeIndexEntry {
24
+ tsName: string;
25
+ resourceType: string;
26
+ attrs: Record<string, string>;
27
+ }
28
+
29
+ export interface RuntimeIndexPropertyEntry {
30
+ tsName: string;
31
+ resourceType: string;
32
+ }
33
+
34
+ /**
35
+ * Generate the runtime index.ts content with factory constructor exports.
36
+ *
37
+ * Phases:
38
+ * 1. Banner + import createResource/createProperty
39
+ * 2. Sort and emit resource exports
40
+ * 3. Sort and emit property exports
41
+ * 4. Collision-safe re-exports of intrinsics and pseudo-parameters
42
+ */
43
+ export function generateRuntimeIndex(
44
+ resources: RuntimeIndexEntry[],
45
+ properties: RuntimeIndexPropertyEntry[],
46
+ config: RuntimeIndexConfig,
47
+ ): string {
48
+ const lines: string[] = [];
49
+ lines.push("// Code generated by chant generate. DO NOT EDIT.");
50
+ lines.push('import { createResource, createProperty } from "./runtime";');
51
+ lines.push("");
52
+
53
+ // Sort and emit resource exports
54
+ const sortedResources = [...resources].sort((a, b) => a.tsName.localeCompare(b.tsName));
55
+ for (const { tsName, resourceType, attrs } of sortedResources) {
56
+ const attrsStr = JSON.stringify(attrs);
57
+ lines.push(
58
+ `export const ${tsName} = createResource(${JSON.stringify(resourceType)}, ${JSON.stringify(config.lexiconName)}, ${attrsStr});`,
59
+ );
60
+ }
61
+
62
+ lines.push("");
63
+
64
+ // Sort and emit property exports
65
+ const sortedProperties = [...properties].sort((a, b) => a.tsName.localeCompare(b.tsName));
66
+ for (const { tsName, resourceType } of sortedProperties) {
67
+ lines.push(
68
+ `export const ${tsName} = createProperty(${JSON.stringify(resourceType)}, ${JSON.stringify(config.lexiconName)});`,
69
+ );
70
+ }
71
+
72
+ lines.push("");
73
+
74
+ // Collision-safe re-exports
75
+ const allExportNames = new Set([
76
+ ...resources.map((e) => e.tsName),
77
+ ...properties.map((e) => e.tsName),
78
+ ]);
79
+
80
+ lines.push("// Re-exports for convenience");
81
+
82
+ if (config.intrinsicReExports) {
83
+ const safe = config.intrinsicReExports.names.filter((n) => !allExportNames.has(n));
84
+ if (safe.length > 0) {
85
+ lines.push(`export { ${safe.join(", ")} } from ${JSON.stringify(config.intrinsicReExports.from)};`);
86
+ }
87
+ }
88
+
89
+ if (config.pseudoReExports) {
90
+ const safe = config.pseudoReExports.names.filter((n) => !allExportNames.has(n));
91
+ if (safe.length > 0) {
92
+ lines.push(`export { ${safe.join(", ")} } from ${JSON.stringify(config.pseudoReExports.from)};`);
93
+ }
94
+ }
95
+
96
+ lines.push("");
97
+
98
+ return lines.join("\n");
99
+ }
@@ -0,0 +1,146 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ writeResourceClass,
4
+ writePropertyClass,
5
+ writeConstructor,
6
+ writeEnumType,
7
+ resolveConstructorType,
8
+ } from "./generate-typescript";
9
+
10
+ describe("writeResourceClass", () => {
11
+ test("generates class with constructor and attributes", () => {
12
+ const lines: string[] = [];
13
+ writeResourceClass(
14
+ lines,
15
+ "Bucket",
16
+ [{ name: "BucketName", type: "string", required: true }],
17
+ [{ name: "Arn", type: "string" }],
18
+ );
19
+ const output = lines.join("\n");
20
+ expect(output).toContain("export declare class Bucket {");
21
+ expect(output).toContain("bucketName: string;");
22
+ expect(output).toContain("readonly arn: string;");
23
+ expect(output).toContain("}");
24
+ });
25
+
26
+ test("applies remap to attribute types", () => {
27
+ const lines: string[] = [];
28
+ const remap = new Map([["CustomType", "BucketConfig"]]);
29
+ writeResourceClass(
30
+ lines,
31
+ "Bucket",
32
+ [],
33
+ [{ name: "Config", type: "CustomType" }],
34
+ remap,
35
+ );
36
+ const output = lines.join("\n");
37
+ expect(output).toContain("readonly config: BucketConfig;");
38
+ });
39
+ });
40
+
41
+ describe("writePropertyClass", () => {
42
+ test("generates class with constructor only", () => {
43
+ const lines: string[] = [];
44
+ writePropertyClass(
45
+ lines,
46
+ "BucketConfig",
47
+ [{ name: "Enabled", type: "boolean", required: false }],
48
+ );
49
+ const output = lines.join("\n");
50
+ expect(output).toContain("export declare class BucketConfig {");
51
+ expect(output).toContain("enabled?: boolean;");
52
+ expect(output).toContain("}");
53
+ });
54
+ });
55
+
56
+ describe("writeConstructor", () => {
57
+ test("empty props produces Record<string, unknown> constructor", () => {
58
+ const lines: string[] = [];
59
+ writeConstructor(lines, [], undefined);
60
+ expect(lines.join("\n")).toContain("constructor(props: Record<string, unknown>);");
61
+ });
62
+
63
+ test("sorts required before optional", () => {
64
+ const lines: string[] = [];
65
+ writeConstructor(
66
+ lines,
67
+ [
68
+ { name: "Optional", type: "string", required: false },
69
+ { name: "Required", type: "string", required: true },
70
+ ],
71
+ undefined,
72
+ );
73
+ const output = lines.join("\n");
74
+ const reqIdx = output.indexOf("required:");
75
+ const optIdx = output.indexOf("optional?:");
76
+ expect(reqIdx).toBeLessThan(optIdx);
77
+ });
78
+
79
+ test("includes description as JSDoc", () => {
80
+ const lines: string[] = [];
81
+ writeConstructor(
82
+ lines,
83
+ [{ name: "Name", type: "string", required: true, description: "The bucket name" }],
84
+ undefined,
85
+ );
86
+ const output = lines.join("\n");
87
+ expect(output).toContain("/** The bucket name */");
88
+ });
89
+ });
90
+
91
+ describe("writeEnumType", () => {
92
+ test("writes single-line for short enum", () => {
93
+ const lines: string[] = [];
94
+ writeEnumType(lines, "Status", ["active", "inactive"]);
95
+ const output = lines.join("\n");
96
+ expect(output).toContain('export type Status = "active" | "inactive";');
97
+ });
98
+
99
+ test("writes multi-line for long enum", () => {
100
+ const lines: string[] = [];
101
+ writeEnumType(lines, "VeryLongTypeName", [
102
+ "value-one-is-very-long",
103
+ "value-two-is-very-long",
104
+ "value-three-is-very-long",
105
+ "value-four-is-very-long",
106
+ ]);
107
+ const output = lines.join("\n");
108
+ expect(output).toContain("export type VeryLongTypeName =");
109
+ expect(output).toContain(' | "value-one-is-very-long"');
110
+ });
111
+
112
+ test("sorts values alphabetically", () => {
113
+ const lines: string[] = [];
114
+ writeEnumType(lines, "Order", ["z", "a", "m"]);
115
+ const output = lines.join("\n");
116
+ expect(output).toContain('"a" | "m" | "z"');
117
+ });
118
+ });
119
+
120
+ describe("resolveConstructorType", () => {
121
+ test("passes through primitives", () => {
122
+ expect(resolveConstructorType("string", undefined)).toBe("string");
123
+ expect(resolveConstructorType("number", undefined)).toBe("number");
124
+ expect(resolveConstructorType("boolean", undefined)).toBe("boolean");
125
+ expect(resolveConstructorType("any", undefined)).toBe("any");
126
+ });
127
+
128
+ test("normalizes Record<string, any>", () => {
129
+ expect(resolveConstructorType("Record<string, any>", undefined)).toBe("Record<string, unknown>");
130
+ });
131
+
132
+ test("handles array types recursively", () => {
133
+ const remap = new Map([["Foo", "Bar"]]);
134
+ expect(resolveConstructorType("Foo[]", remap)).toBe("Bar[]");
135
+ expect(resolveConstructorType("string[]", undefined)).toBe("string[]");
136
+ });
137
+
138
+ test("applies remap", () => {
139
+ const remap = new Map([["InternalName", "PublicName"]]);
140
+ expect(resolveConstructorType("InternalName", remap)).toBe("PublicName");
141
+ });
142
+
143
+ test("returns type as-is when no remap match", () => {
144
+ expect(resolveConstructorType("UnknownType", new Map())).toBe("UnknownType");
145
+ });
146
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Generic TypeScript declaration (.d.ts) generation utilities.
3
+ *
4
+ * Provides functions for writing resource classes, property classes,
5
+ * constructors, and enum types. Lexicon-specific generators use these
6
+ * building blocks with their own orchestration logic.
7
+ */
8
+
9
+ // ── Minimal input interfaces ────────────────────────────────────────
10
+
11
+ export interface DtsProperty {
12
+ name: string;
13
+ type: string;
14
+ required: boolean;
15
+ description?: string;
16
+ }
17
+
18
+ export interface DtsAttribute {
19
+ name: string;
20
+ type: string;
21
+ }
22
+
23
+ export interface DtsPropertyType {
24
+ name: string;
25
+ properties: DtsProperty[];
26
+ }
27
+
28
+ export interface DtsResource {
29
+ typeName: string;
30
+ properties: DtsProperty[];
31
+ attributes: DtsAttribute[];
32
+ }
33
+
34
+ // ── Writers ─────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Write a resource class declaration with constructor and readonly attributes.
38
+ */
39
+ export function writeResourceClass(
40
+ lines: string[],
41
+ tsName: string,
42
+ properties: DtsProperty[],
43
+ attributes: DtsAttribute[],
44
+ remap?: Map<string, string>,
45
+ ): void {
46
+ lines.push("");
47
+ lines.push(`export declare class ${tsName} {`);
48
+ writeConstructor(lines, properties, remap);
49
+
50
+ // Attributes as readonly properties (sorted)
51
+ const attrs = [...attributes].sort((a, b) => a.name.localeCompare(b.name));
52
+ for (const a of attrs) {
53
+ const attrType = resolveConstructorType(a.type, remap);
54
+ lines.push(` readonly ${toCamelCase(a.name)}: ${attrType};`);
55
+ }
56
+
57
+ lines.push("}");
58
+ }
59
+
60
+ /**
61
+ * Write a property class declaration with constructor.
62
+ */
63
+ export function writePropertyClass(
64
+ lines: string[],
65
+ tsName: string,
66
+ properties: DtsProperty[],
67
+ remap?: Map<string, string>,
68
+ ): void {
69
+ lines.push("");
70
+ lines.push(`export declare class ${tsName} {`);
71
+ writeConstructor(lines, properties, remap);
72
+ lines.push("}");
73
+ }
74
+
75
+ /**
76
+ * Write a constructor with typed props parameter.
77
+ */
78
+ export function writeConstructor(
79
+ lines: string[],
80
+ props: DtsProperty[],
81
+ remap: Map<string, string> | undefined,
82
+ ): void {
83
+ if (props.length === 0) {
84
+ lines.push(" constructor(props: Record<string, unknown>);");
85
+ return;
86
+ }
87
+
88
+ // Sort: required first, then optional, each group alphabetically
89
+ const sorted = [...props].sort((a, b) => {
90
+ if (a.required !== b.required) return a.required ? -1 : 1;
91
+ return a.name.localeCompare(b.name);
92
+ });
93
+
94
+ lines.push(" constructor(props: {");
95
+ for (const p of sorted) {
96
+ const optional = p.required ? "" : "?";
97
+ const tsType = resolveConstructorType(p.type, remap);
98
+ if (p.description) {
99
+ lines.push(` /** ${p.description} */`);
100
+ }
101
+ lines.push(` ${toCamelCase(p.name)}${optional}: ${tsType};`);
102
+ }
103
+ lines.push(" });");
104
+ }
105
+
106
+ /**
107
+ * Write an enum union type.
108
+ */
109
+ export function writeEnumType(lines: string[], tsName: string, values: string[]): void {
110
+ const sorted = [...values].sort();
111
+ const quoted = sorted.map((v) => JSON.stringify(v));
112
+
113
+ const singleLine = `\nexport type ${tsName} = ${quoted.join(" | ")};`;
114
+ if (singleLine.length <= 100) {
115
+ lines.push(singleLine);
116
+ } else {
117
+ lines.push("");
118
+ lines.push(`export type ${tsName} =`);
119
+ for (let i = 0; i < quoted.length; i++) {
120
+ const suffix = i < quoted.length - 1 ? "" : ";";
121
+ lines.push(` | ${quoted[i]}${suffix}`);
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Resolve a type string through the remap table, handling arrays and
128
+ * common type normalization.
129
+ */
130
+ export function resolveConstructorType(tsType: string, remap: Map<string, string> | undefined): string {
131
+ // Handle array types
132
+ if (tsType.endsWith("[]")) {
133
+ const inner = tsType.slice(0, -2);
134
+ return resolveConstructorType(inner, remap) + "[]";
135
+ }
136
+
137
+ // Primitive and pass-through types
138
+ switch (tsType) {
139
+ case "string":
140
+ case "number":
141
+ case "boolean":
142
+ case "any":
143
+ return tsType;
144
+ case "Record<string, any>":
145
+ return "Record<string, unknown>";
146
+ case "Record<string, unknown>":
147
+ return tsType;
148
+ }
149
+
150
+ // Remap parser property type names to resolved names
151
+ if (remap) {
152
+ const resolved = remap.get(tsType);
153
+ if (resolved) return resolved;
154
+ }
155
+
156
+ return tsType;
157
+ }
158
+
159
+ function toCamelCase(name: string): string {
160
+ return name.charAt(0).toLowerCase() + name.slice(1);
161
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Generic generation pipeline orchestration.
3
+ *
4
+ * Provides the step sequencing, logging, warning collection, stats counting,
5
+ * and artifact writing pattern. Individual steps (fetch, parse, etc.) are
6
+ * supplied by the lexicon via callbacks.
7
+ */
8
+
9
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
10
+ import { join } from "path";
11
+ import type { NamingStrategy } from "./naming";
12
+
13
+ // ── Types ──────────────────────────────────────────────────────────
14
+
15
+ export interface GenerateOptions {
16
+ force?: boolean;
17
+ verbose?: boolean;
18
+ dryRun?: boolean;
19
+ schemaSource?: Map<string, Buffer>;
20
+ }
21
+
22
+ export interface GenerateResult {
23
+ lexiconJSON: string;
24
+ typesDTS: string;
25
+ indexTS: string;
26
+ resources: number;
27
+ properties: number;
28
+ enums: number;
29
+ warnings: Array<{ file: string; error: string }>;
30
+ }
31
+
32
+ /**
33
+ * A parsed result with enough structure for the pipeline to count stats.
34
+ * Lexicons extend this with their own fields.
35
+ */
36
+ export interface ParsedResult {
37
+ propertyTypes: Array<{ name: string }>;
38
+ enums: Array<unknown>;
39
+ }
40
+
41
+ export interface AugmentResult<T> {
42
+ schemas: Map<string, Buffer>;
43
+ extraResults?: T[];
44
+ warnings?: Array<{ file: string; error: string }>;
45
+ }
46
+
47
+ export interface GeneratePipelineConfig<T extends ParsedResult> {
48
+ /** Fetch or provide raw schema data. */
49
+ fetchSchemas: (opts: { force?: boolean }) => Promise<Map<string, Buffer>>;
50
+
51
+ /** Parse a single schema buffer into a result. Returns null to skip. */
52
+ parseSchema: (typeName: string, data: Buffer) => T | null;
53
+
54
+ /** Create a naming strategy from the parsed results. */
55
+ createNaming: (results: T[]) => NamingStrategy;
56
+
57
+ /** Generate lexicon JSON from results + naming. */
58
+ generateRegistry: (results: T[], naming: NamingStrategy) => string;
59
+
60
+ /** Generate TypeScript declarations. */
61
+ generateTypes: (results: T[], naming: NamingStrategy) => string;
62
+
63
+ /** Generate runtime index with factory exports. */
64
+ generateRuntimeIndex: (results: T[], naming: NamingStrategy) => string;
65
+
66
+ /** Optional pre-parse hook (patches, overlays, extra resources, etc.). */
67
+ augmentSchemas?: (
68
+ schemas: Map<string, Buffer>,
69
+ opts: GenerateOptions,
70
+ log: (msg: string) => void,
71
+ ) => Promise<AugmentResult<T>>;
72
+
73
+ /** Optional post-parse hook (add synthetic resources, fallbacks, etc.). */
74
+ augmentResults?: (
75
+ results: T[],
76
+ opts: GenerateOptions,
77
+ log: (msg: string) => void,
78
+ ) => { results: T[]; warnings?: Array<{ file: string; error: string }> };
79
+ }
80
+
81
+ // ── Pipeline ───────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Run a generation pipeline with the supplied config callbacks.
85
+ */
86
+ export async function generatePipeline<T extends ParsedResult>(
87
+ config: GeneratePipelineConfig<T>,
88
+ opts: GenerateOptions = {},
89
+ ): Promise<GenerateResult> {
90
+ const log = opts.verbose
91
+ ? (msg: string) => console.error(msg)
92
+ : (_msg: string) => {};
93
+
94
+ const warnings: Array<{ file: string; error: string }> = [];
95
+
96
+ // Step 1: Fetch schemas (or use provided source)
97
+ let schemas: Map<string, Buffer>;
98
+ if (opts.schemaSource) {
99
+ schemas = opts.schemaSource;
100
+ log(`Using provided schema source with ${schemas.size} schemas`);
101
+ } else {
102
+ log("Fetching schemas...");
103
+ schemas = await config.fetchSchemas({ force: opts.force });
104
+ log(`Fetched ${schemas.size} schemas`);
105
+ }
106
+
107
+ // Step 2: Augment schemas (patches, overlays, etc.)
108
+ let extraResults: T[] = [];
109
+ if (config.augmentSchemas && !opts.schemaSource) {
110
+ const augment = await config.augmentSchemas(schemas, opts, log);
111
+ schemas = augment.schemas;
112
+ if (augment.extraResults) extraResults = augment.extraResults;
113
+ if (augment.warnings) warnings.push(...augment.warnings);
114
+ }
115
+
116
+ // Step 3: Parse each schema
117
+ log("Parsing schemas...");
118
+ const results: T[] = [];
119
+ for (const [typeName, data] of schemas) {
120
+ try {
121
+ const result = config.parseSchema(typeName, data);
122
+ if (result) results.push(result);
123
+ } catch (err) {
124
+ warnings.push({
125
+ file: typeName,
126
+ error: err instanceof Error ? err.message : String(err),
127
+ });
128
+ }
129
+ }
130
+ results.push(...extraResults);
131
+ log(`Parsed ${results.length} schemas`);
132
+
133
+ // Step 4: Augment results (fallbacks, synthetic resources, etc.)
134
+ if (config.augmentResults) {
135
+ const augment = config.augmentResults(results, opts, log);
136
+ // augmentResults may mutate results in-place or return new ones
137
+ if (augment.warnings) warnings.push(...augment.warnings);
138
+ }
139
+
140
+ // Step 5: Naming strategy
141
+ const naming = config.createNaming(results);
142
+
143
+ // Step 6: Generate artifacts
144
+ log("Generating lexicon JSON...");
145
+ const lexiconJSON = config.generateRegistry(results, naming);
146
+
147
+ log("Generating TypeScript declarations...");
148
+ const typesDTS = config.generateTypes(results, naming);
149
+
150
+ log("Generating runtime index...");
151
+ const indexTS = config.generateRuntimeIndex(results, naming);
152
+
153
+ // Count stats
154
+ let resourceCount = 0;
155
+ let propertyCount = 0;
156
+ let enumCount = 0;
157
+ for (const r of results) {
158
+ resourceCount++;
159
+ propertyCount += r.propertyTypes.length;
160
+ enumCount += r.enums.length;
161
+ }
162
+
163
+ return {
164
+ lexiconJSON,
165
+ typesDTS,
166
+ indexTS,
167
+ resources: resourceCount,
168
+ properties: propertyCount,
169
+ enums: enumCount,
170
+ warnings,
171
+ };
172
+ }
173
+
174
+ // ── Artifact writing ───────────────────────────────────────────────
175
+
176
+ export interface WriteConfig {
177
+ /** Base directory of the lexicon package. */
178
+ baseDir: string;
179
+ /** Subdirectory for generated files (default: "src/generated"). */
180
+ generatedSubdir?: string;
181
+ /** Map of filename → content to write. */
182
+ files: Record<string, string>;
183
+ /** Optional snapshot function called before overwriting. */
184
+ snapshot?: (generatedDir: string) => void;
185
+ }
186
+
187
+ /**
188
+ * Write generated artifacts to disk with optional auto-snapshot.
189
+ */
190
+ export function writeGeneratedArtifacts(config: WriteConfig): void {
191
+ const generatedDir = join(config.baseDir, config.generatedSubdir ?? "src/generated");
192
+ mkdirSync(generatedDir, { recursive: true });
193
+
194
+ // Auto-snapshot before overwriting
195
+ if (config.snapshot) {
196
+ try {
197
+ config.snapshot(generatedDir);
198
+ } catch {
199
+ // Best effort — don't fail generation if snapshot fails
200
+ }
201
+ }
202
+
203
+ for (const [filename, content] of Object.entries(config.files)) {
204
+ writeFileSync(join(generatedDir, filename), content);
205
+ }
206
+ }