@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,119 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { extractFromTar } from "./fetch";
3
+
4
+ /**
5
+ * Build a minimal valid tar buffer with a single file entry.
6
+ * Tar format: 512-byte header + data padded to 512-byte boundary + 1024 zero bytes (end marker).
7
+ */
8
+ function buildTar(entries: Array<{ name: string; content: string; typeFlag?: string }>): Uint8Array {
9
+ const blocks: Uint8Array[] = [];
10
+
11
+ for (const entry of entries) {
12
+ const header = new Uint8Array(512);
13
+ const content = new TextEncoder().encode(entry.content);
14
+
15
+ // Name (bytes 0-99)
16
+ const nameBytes = new TextEncoder().encode(entry.name);
17
+ header.set(nameBytes.slice(0, 100), 0);
18
+
19
+ // Mode (bytes 100-107): "0000644\0"
20
+ header.set(new TextEncoder().encode("0000644\0"), 100);
21
+
22
+ // UID (bytes 108-115): "0000000\0"
23
+ header.set(new TextEncoder().encode("0000000\0"), 108);
24
+
25
+ // GID (bytes 116-123): "0000000\0"
26
+ header.set(new TextEncoder().encode("0000000\0"), 116);
27
+
28
+ // Size (bytes 124-135): octal, 11 chars + null
29
+ const sizeOctal = content.length.toString(8).padStart(11, "0") + "\0";
30
+ header.set(new TextEncoder().encode(sizeOctal), 124);
31
+
32
+ // Mtime (bytes 136-147): "00000000000\0"
33
+ header.set(new TextEncoder().encode("00000000000\0"), 136);
34
+
35
+ // Type flag (byte 156): '0' for regular file
36
+ header[156] = (entry.typeFlag ?? "0").charCodeAt(0);
37
+
38
+ // Checksum (bytes 148-155): calculate sum of all header bytes with checksum field as spaces
39
+ header.set(new TextEncoder().encode(" "), 148); // 8 spaces
40
+ let sum = 0;
41
+ for (let i = 0; i < 512; i++) sum += header[i];
42
+ const checksumStr = sum.toString(8).padStart(6, "0") + "\0 ";
43
+ header.set(new TextEncoder().encode(checksumStr), 148);
44
+
45
+ blocks.push(header);
46
+
47
+ // Data blocks
48
+ const dataBlocks = Math.ceil(content.length / 512);
49
+ const dataBuffer = new Uint8Array(dataBlocks * 512);
50
+ dataBuffer.set(content);
51
+ blocks.push(dataBuffer);
52
+ }
53
+
54
+ // End-of-archive marker (two zero blocks)
55
+ blocks.push(new Uint8Array(1024));
56
+
57
+ const totalLength = blocks.reduce((sum, b) => sum + b.length, 0);
58
+ const result = new Uint8Array(totalLength);
59
+ let offset = 0;
60
+ for (const block of blocks) {
61
+ result.set(block, offset);
62
+ offset += block.length;
63
+ }
64
+ return result;
65
+ }
66
+
67
+ describe("extractFromTar", () => {
68
+ test("extracts all regular files", () => {
69
+ const tar = buildTar([
70
+ { name: "file1.txt", content: "hello" },
71
+ { name: "dir/file2.txt", content: "world" },
72
+ ]);
73
+
74
+ const files = extractFromTar(tar);
75
+ expect(files.size).toBe(2);
76
+ expect(files.get("file1.txt")!.toString()).toBe("hello");
77
+ expect(files.get("dir/file2.txt")!.toString()).toBe("world");
78
+ });
79
+
80
+ test("applies filter", () => {
81
+ const tar = buildTar([
82
+ { name: "a.json", content: '{"a":1}' },
83
+ { name: "b.txt", content: "text" },
84
+ { name: "c.json", content: '{"c":3}' },
85
+ ]);
86
+
87
+ const files = extractFromTar(tar, (path) => path.endsWith(".json"));
88
+ expect(files.size).toBe(2);
89
+ expect(files.has("a.json")).toBe(true);
90
+ expect(files.has("c.json")).toBe(true);
91
+ expect(files.has("b.txt")).toBe(false);
92
+ });
93
+
94
+ test("skips directory entries", () => {
95
+ const tar = buildTar([
96
+ { name: "dir/", content: "", typeFlag: "5" },
97
+ { name: "dir/file.txt", content: "content" },
98
+ ]);
99
+
100
+ const files = extractFromTar(tar);
101
+ expect(files.size).toBe(1);
102
+ expect(files.has("dir/file.txt")).toBe(true);
103
+ });
104
+
105
+ test("returns empty map for empty tar", () => {
106
+ // Just end-of-archive marker
107
+ const tar = new Uint8Array(1024);
108
+ const files = extractFromTar(tar);
109
+ expect(files.size).toBe(0);
110
+ });
111
+
112
+ test("handles files with various content", () => {
113
+ const content = "abc\ndef\n";
114
+ const tar = buildTar([{ name: "multi.txt", content }]);
115
+ const files = extractFromTar(tar);
116
+ expect(files.size).toBe(1);
117
+ expect(files.get("multi.txt")!.toString()).toBe(content);
118
+ });
119
+ });
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Generic HTTP fetch with caching and zip extraction utilities.
3
+ *
4
+ * Provides the download + cache + zip pattern used by lexicon codegen
5
+ * pipelines that fetch schemas from remote sources.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, unlinkSync, rmSync } from "fs";
9
+ import { dirname, join } from "path";
10
+ import { debug } from "../cli/debug";
11
+
12
+ // ── Types ──────────────────────────────────────────────────────────
13
+
14
+ export interface FetchConfig {
15
+ /** URL to fetch. */
16
+ url: string;
17
+ /** Local file path for caching the download. */
18
+ cacheFile: string;
19
+ /** Cache TTL in milliseconds (default: 24 hours). */
20
+ cacheTtlMs?: number;
21
+ }
22
+
23
+ // ── Fetch with cache ───────────────────────────────────────────────
24
+
25
+ const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
26
+
27
+ /**
28
+ * Fetch a URL with local file caching.
29
+ *
30
+ * Returns cached data if the cache file exists and is younger than cacheTtlMs.
31
+ * Otherwise downloads from the URL, caches the result, and returns it.
32
+ */
33
+ export async function fetchWithCache(config: FetchConfig, force = false): Promise<Buffer> {
34
+ const ttl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
35
+
36
+ if (!force) {
37
+ try {
38
+ if (existsSync(config.cacheFile)) {
39
+ const stat = statSync(config.cacheFile);
40
+ if (Date.now() - stat.mtimeMs < ttl) {
41
+ return readFileSync(config.cacheFile) as unknown as Buffer;
42
+ }
43
+ }
44
+ } catch (e) {
45
+ debug("cache read failed:", e);
46
+ }
47
+ }
48
+
49
+ const response = await fetch(config.url);
50
+ if (!response.ok) {
51
+ throw new Error(`Download from ${config.url} returned ${response.status}`);
52
+ }
53
+
54
+ const arrayBuffer = await response.arrayBuffer();
55
+ const data = Buffer.from(arrayBuffer);
56
+
57
+ try {
58
+ mkdirSync(dirname(config.cacheFile), { recursive: true });
59
+ writeFileSync(config.cacheFile, data as unknown as Uint8Array);
60
+ } catch (e) {
61
+ debug("cache write failed:", e);
62
+ }
63
+
64
+ return data;
65
+ }
66
+
67
+ // ── Zip extraction ─────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Extract files from a zip buffer using fflate.
71
+ *
72
+ * @param filter - Optional predicate to select which files to include.
73
+ * Receives the file name (path within the zip). Defaults to all files.
74
+ * @returns Map of filename → Buffer for each extracted file.
75
+ */
76
+ export async function extractFromZip(
77
+ zipData: Buffer,
78
+ filter?: (name: string) => boolean,
79
+ ): Promise<Map<string, Buffer>> {
80
+ const { unzipSync } = await import("fflate");
81
+ const files = unzipSync(new Uint8Array(zipData));
82
+
83
+ const result = new Map<string, Buffer>();
84
+ for (const [name, data] of Object.entries(files)) {
85
+ if (filter && !filter(name)) continue;
86
+ result.set(name, Buffer.from(data));
87
+ }
88
+ return result;
89
+ }
90
+
91
+ // ── Tar extraction ──────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Extract files from an uncompressed tar buffer.
95
+ *
96
+ * The caller handles gunzip (via `fflate.gunzipSync` or `zlib.gunzipSync`)
97
+ * and any prefix stripping. Returns `Map<path, Buffer>`.
98
+ *
99
+ * @param filter - Optional predicate to select which files to include.
100
+ * Receives the full file name (path within the tar). Defaults to all regular files.
101
+ */
102
+ export function extractFromTar(
103
+ tarData: Uint8Array,
104
+ filter?: (path: string) => boolean,
105
+ ): Map<string, Buffer> {
106
+ const result = new Map<string, Buffer>();
107
+ let offset = 0;
108
+ let longName: string | null = null;
109
+
110
+ while (offset < tarData.length - 512) {
111
+ const header = tarData.slice(offset, offset + 512);
112
+ offset += 512;
113
+
114
+ // Check for end-of-archive marker (all zeros)
115
+ if (header.every((b) => b === 0)) break;
116
+
117
+ // Parse file name (first 100 bytes)
118
+ const nameBytes = header.slice(0, 100);
119
+ let name = new TextDecoder().decode(nameBytes).replace(/\0+$/, "");
120
+
121
+ // Check type flag
122
+ const typeFlag = String.fromCharCode(header[156]);
123
+
124
+ // Parse file size (bytes 124-135, octal)
125
+ const sizeStr = new TextDecoder().decode(header.slice(124, 136)).replace(/\0+$/, "").trim();
126
+ const size = parseInt(sizeStr, 8) || 0;
127
+
128
+ // Calculate blocks to skip
129
+ const blocks = Math.ceil(size / 512);
130
+
131
+ if (typeFlag === "L") {
132
+ // GNU long name: read the name from the next data block
133
+ const longNameData = tarData.slice(offset, offset + size);
134
+ longName = new TextDecoder().decode(longNameData).replace(/\0+$/, "");
135
+ offset += blocks * 512;
136
+ continue;
137
+ }
138
+
139
+ // Apply long name from previous GNU 'L' entry
140
+ if (longName !== null) {
141
+ name = longName;
142
+ longName = null;
143
+ } else {
144
+ // Check prefix field (bytes 345-500) for USTAR format
145
+ const prefix = new TextDecoder().decode(header.slice(345, 500)).replace(/\0+$/, "");
146
+ if (prefix) {
147
+ name = prefix + "/" + name;
148
+ }
149
+ }
150
+
151
+ const fileData = tarData.slice(offset, offset + size);
152
+ offset += blocks * 512;
153
+
154
+ // Skip non-regular files
155
+ if (typeFlag !== "0" && typeFlag !== "\0") continue;
156
+
157
+ if (filter && !filter(name)) continue;
158
+ result.set(name, Buffer.from(fileData));
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ // ── Directory-level tar cache ───────────────────────────────────────
165
+
166
+ export interface FetchTarConfig {
167
+ /** URL of the gzipped tarball. */
168
+ url: string;
169
+ /** Local directory for extracted files. */
170
+ destDir: string;
171
+ /** Cache TTL in milliseconds (default: 7 days). */
172
+ cacheTtlMs?: number;
173
+ }
174
+
175
+ const DEFAULT_TAR_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
176
+
177
+ /**
178
+ * Fetch a gzipped tarball, extract files matching `tarPrefix`, and cache them
179
+ * in `destDir`. Returns `destDir`.
180
+ *
181
+ * Checks `destDir` mtime against TTL. If fresh, returns immediately.
182
+ * Otherwise downloads, gunzips, extracts matching files, and writes to `destDir`.
183
+ *
184
+ * @param tarPrefix - Only files whose path (after stripping the top-level directory)
185
+ * starts with this prefix will be extracted.
186
+ * @param force - If true, ignore cache and re-download.
187
+ */
188
+ export async function fetchAndExtractTar(
189
+ config: FetchTarConfig,
190
+ tarPrefix: string,
191
+ force = false,
192
+ ): Promise<string> {
193
+ const ttl = config.cacheTtlMs ?? DEFAULT_TAR_CACHE_TTL_MS;
194
+
195
+ if (!force) {
196
+ try {
197
+ if (existsSync(config.destDir)) {
198
+ const stat = statSync(config.destDir);
199
+ if (Date.now() - stat.mtimeMs < ttl) {
200
+ return config.destDir;
201
+ }
202
+ }
203
+ } catch (e) {
204
+ debug("tar cache check failed:", e);
205
+ }
206
+ }
207
+
208
+ const resp = await fetch(config.url);
209
+ if (!resp.ok) {
210
+ throw new Error(`Tarball download from ${config.url} returned ${resp.status}`);
211
+ }
212
+
213
+ const compressed = new Uint8Array(await resp.arrayBuffer());
214
+ const { gunzipSync } = await import("fflate");
215
+ const tarData = gunzipSync(compressed);
216
+
217
+ // Remove old cache
218
+ if (existsSync(config.destDir)) {
219
+ rmSync(config.destDir, { recursive: true });
220
+ }
221
+
222
+ // Extract files matching the prefix
223
+ let extracted = 0;
224
+ const files = extractFromTar(tarData);
225
+
226
+ for (const [name, data] of files) {
227
+ // Strip top-level directory (e.g. "cfn-lint-main/")
228
+ const slashIdx = name.indexOf("/");
229
+ if (slashIdx < 0) continue;
230
+ const relPath = name.slice(slashIdx + 1);
231
+
232
+ if (!relPath.startsWith(tarPrefix)) continue;
233
+ const localPath = relPath.slice(tarPrefix.length);
234
+ if (!localPath) continue;
235
+
236
+ const fullPath = join(config.destDir, localPath);
237
+ const dir = dirname(fullPath);
238
+ mkdirSync(dir, { recursive: true });
239
+ writeFileSync(fullPath, data as unknown as Uint8Array);
240
+ extracted++;
241
+ }
242
+
243
+ if (extracted === 0) {
244
+ throw new Error(`No files matching prefix "${tarPrefix}" found in tarball`);
245
+ }
246
+
247
+ return config.destDir;
248
+ }
249
+
250
+ // ── Cache utilities ────────────────────────────────────────────────
251
+
252
+ /**
253
+ * Clear a cache file. Ignores errors if the file doesn't exist.
254
+ */
255
+ export function clearCacheFile(cacheFile: string): void {
256
+ try {
257
+ if (existsSync(cacheFile)) unlinkSync(cacheFile);
258
+ } catch (e) {
259
+ debug("cache clear failed:", e);
260
+ }
261
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildRegistry, serializeRegistry, type RegistryResource, type RegistryConfig } from "./generate-registry";
3
+ import { NamingStrategy, type NamingInput, type NamingConfig } from "./naming";
4
+
5
+ interface TestEntry {
6
+ resourceType: string;
7
+ kind: "resource" | "property";
8
+ attrs?: Record<string, string>;
9
+ }
10
+
11
+ function makeNaming(results: RegistryResource[]): NamingStrategy {
12
+ const inputs: NamingInput[] = results.map((r) => ({
13
+ typeName: r.typeName,
14
+ propertyTypes: r.propertyTypes,
15
+ }));
16
+ const config: NamingConfig = {
17
+ priorityNames: {},
18
+ priorityAliases: {},
19
+ priorityPropertyAliases: {},
20
+ serviceAbbreviations: {},
21
+ shortName: (t) => t.split("::")[2] ?? t,
22
+ serviceName: (t) => t.split("::")[1] ?? t,
23
+ };
24
+ return new NamingStrategy(inputs, config);
25
+ }
26
+
27
+ const testConfig: RegistryConfig<TestEntry> = {
28
+ shortName: (t) => t.split("::")[2] ?? t,
29
+ buildEntry: (_resource, _tsName, attrs) => ({
30
+ resourceType: _resource.typeName,
31
+ kind: "resource",
32
+ ...(attrs && { attrs }),
33
+ }),
34
+ buildPropertyEntry: (resourceType, propertyType) => ({
35
+ resourceType: `${resourceType}.${propertyType}`,
36
+ kind: "property",
37
+ }),
38
+ };
39
+
40
+ describe("buildRegistry", () => {
41
+ test("builds entries with attrs map", () => {
42
+ const resources: RegistryResource[] = [
43
+ {
44
+ typeName: "Test::S3::Bucket",
45
+ attributes: [{ name: "Arn" }, { name: "DomainName" }],
46
+ properties: [],
47
+ propertyTypes: [],
48
+ },
49
+ ];
50
+ const naming = makeNaming(resources);
51
+ const entries = buildRegistry(resources, naming, testConfig);
52
+
53
+ expect(entries["Bucket"]).toBeDefined();
54
+ expect(entries["Bucket"].attrs).toEqual({ arn: "Arn", domainName: "DomainName" });
55
+ });
56
+
57
+ test("omits attrs when empty", () => {
58
+ const resources: RegistryResource[] = [
59
+ { typeName: "Test::S3::Bucket", attributes: [], properties: [], propertyTypes: [] },
60
+ ];
61
+ const naming = makeNaming(resources);
62
+ const entries = buildRegistry(resources, naming, testConfig);
63
+ expect(entries["Bucket"].attrs).toBeUndefined();
64
+ });
65
+
66
+ test("includes property type entries", () => {
67
+ const resources: RegistryResource[] = [
68
+ {
69
+ typeName: "Test::S3::Bucket",
70
+ attributes: [],
71
+ properties: [],
72
+ propertyTypes: [{ name: "Bucket_Versioning", cfnType: "Versioning" }],
73
+ },
74
+ ];
75
+ const naming = makeNaming(resources);
76
+ const entries = buildRegistry(resources, naming, testConfig);
77
+ expect(entries["Bucket_Versioning"]).toEqual({
78
+ resourceType: "Test::S3::Bucket.Versioning",
79
+ kind: "property",
80
+ });
81
+ });
82
+
83
+ test("filters empty constraints", () => {
84
+ const resources: RegistryResource[] = [
85
+ {
86
+ typeName: "Test::S3::Bucket",
87
+ attributes: [],
88
+ properties: [
89
+ { name: "Name", constraints: { minLength: 3 } },
90
+ { name: "Tag", constraints: {} },
91
+ ],
92
+ propertyTypes: [],
93
+ },
94
+ ];
95
+ const naming = makeNaming(resources);
96
+
97
+ let capturedConstraints: Record<string, unknown> | undefined;
98
+ const config: RegistryConfig<TestEntry> = {
99
+ ...testConfig,
100
+ buildEntry: (_r, _t, _a, propConstraints) => {
101
+ capturedConstraints = propConstraints;
102
+ return { resourceType: _r.typeName, kind: "resource" };
103
+ },
104
+ };
105
+
106
+ buildRegistry(resources, naming, config);
107
+ expect(capturedConstraints).toEqual({ Name: { minLength: 3 } });
108
+ });
109
+ });
110
+
111
+ describe("serializeRegistry", () => {
112
+ test("sorts keys deterministically", () => {
113
+ const entries = { Zebra: { a: 1 }, Apple: { b: 2 }, Mango: { c: 3 } };
114
+ const json = serializeRegistry(entries);
115
+ const parsed = JSON.parse(json);
116
+ expect(Object.keys(parsed)).toEqual(["Apple", "Mango", "Zebra"]);
117
+ });
118
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Generic lexicon registry builder.
3
+ *
4
+ * Implements the shared loop: iterate parsed results, resolve naming,
5
+ * build attrs/constraints maps, call format-specific entry builders,
6
+ * handle aliases and property type sub-entries.
7
+ */
8
+
9
+ import type { NamingStrategy } from "./naming";
10
+ import { propertyTypeName, extractDefName } from "./naming";
11
+ import { toCamelCase } from "./case";
12
+ import { constraintsIsEmpty, type PropertyConstraints } from "./json-schema";
13
+
14
+ export interface RegistryResource {
15
+ typeName: string;
16
+ attributes: { name: string }[];
17
+ properties: { name: string; constraints: PropertyConstraints }[];
18
+ propertyTypes: { name: string; cfnType: string }[];
19
+ }
20
+
21
+ export interface RegistryConfig<E> {
22
+ /** Short name extractor (e.g. cfnShortName). */
23
+ shortName: (typeName: string) => string;
24
+ /** Build a resource entry from parsed data. */
25
+ buildEntry: (
26
+ resource: RegistryResource,
27
+ tsName: string,
28
+ attrs: Record<string, string> | undefined,
29
+ propConstraints: Record<string, PropertyConstraints> | undefined,
30
+ ) => E;
31
+ /** Build a property type entry. */
32
+ buildPropertyEntry: (resourceType: string, propertyType: string) => E;
33
+ }
34
+
35
+ /**
36
+ * Build a registry of entries from parsed results using the naming strategy.
37
+ */
38
+ export function buildRegistry<E>(
39
+ results: RegistryResource[],
40
+ naming: NamingStrategy,
41
+ config: RegistryConfig<E>,
42
+ ): Record<string, E> {
43
+ const entries: Record<string, E> = {};
44
+
45
+ for (const r of results) {
46
+ const typeName = r.typeName;
47
+ const tsName = naming.resolve(typeName);
48
+ if (!tsName) continue;
49
+
50
+ // Build attrs map: camelCase → raw name
51
+ let attrs: Record<string, string> | undefined;
52
+ if (r.attributes.length > 0) {
53
+ attrs = {};
54
+ for (const a of r.attributes) {
55
+ attrs[toCamelCase(a.name)] = a.name;
56
+ }
57
+ }
58
+
59
+ // Build per-property constraints (skip empty)
60
+ let propConstraints: Record<string, PropertyConstraints> | undefined;
61
+ for (const p of r.properties) {
62
+ if (!constraintsIsEmpty(p.constraints)) {
63
+ if (!propConstraints) propConstraints = {};
64
+ propConstraints[p.name] = p.constraints;
65
+ }
66
+ }
67
+
68
+ const entry = config.buildEntry(r, tsName, attrs, propConstraints);
69
+ entries[tsName] = entry;
70
+
71
+ // Alias entries
72
+ for (const alias of naming.aliases(typeName)) {
73
+ entries[alias] = entry;
74
+ }
75
+
76
+ // Property type entries
77
+ const shortName = config.shortName(typeName);
78
+ const ptAliases = naming.propertyTypeAliases(typeName);
79
+
80
+ for (const pt of r.propertyTypes) {
81
+ const defName = extractDefName(pt.name, shortName);
82
+ const ptName = propertyTypeName(tsName, defName);
83
+ const ptEntry = config.buildPropertyEntry(typeName, pt.cfnType);
84
+ entries[ptName] = ptEntry;
85
+
86
+ if (ptAliases) {
87
+ const aliasName = ptAliases.get(defName);
88
+ if (aliasName) {
89
+ entries[aliasName] = ptEntry;
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ return entries;
96
+ }
97
+
98
+ /**
99
+ * Sort registry keys and serialize to JSON.
100
+ */
101
+ export function serializeRegistry(entries: Record<string, unknown>): string {
102
+ const sorted: Record<string, unknown> = {};
103
+ for (const key of Object.keys(entries).sort()) {
104
+ sorted[key] = entries[key];
105
+ }
106
+ return JSON.stringify(sorted, null, 2);
107
+ }
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ generateRuntimeIndex,
4
+ type RuntimeIndexEntry,
5
+ type RuntimeIndexPropertyEntry,
6
+ type RuntimeIndexConfig,
7
+ } from "./generate-runtime-index";
8
+
9
+ const baseConfig: RuntimeIndexConfig = {
10
+ lexiconName: "test",
11
+ intrinsicReExports: { names: ["Sub", "Ref"], from: "../intrinsics" },
12
+ pseudoReExports: { names: ["Region", "AccountId"], from: "../pseudo" },
13
+ };
14
+
15
+ describe("generateRuntimeIndex", () => {
16
+ test("emits resource and property exports in sorted order", () => {
17
+ const resources: RuntimeIndexEntry[] = [
18
+ { tsName: "Bucket", resourceType: "AWS::S3::Bucket", attrs: { arn: "Arn" } },
19
+ { tsName: "Alarm", resourceType: "AWS::CloudWatch::Alarm", attrs: {} },
20
+ ];
21
+ const properties: RuntimeIndexPropertyEntry[] = [
22
+ { tsName: "Bucket_Versioning", resourceType: "AWS::S3::Bucket.VersioningConfiguration" },
23
+ ];
24
+
25
+ const result = generateRuntimeIndex(resources, properties, baseConfig);
26
+
27
+ // Resources sorted: Alarm before Bucket
28
+ expect(result).toContain('export const Alarm = createResource("AWS::CloudWatch::Alarm", "test", {});');
29
+ expect(result).toContain('export const Bucket = createResource("AWS::S3::Bucket", "test", {"arn":"Arn"});');
30
+ expect(result).toContain('export const Bucket_Versioning = createProperty("AWS::S3::Bucket.VersioningConfiguration", "test");');
31
+
32
+ // Alarm should come before Bucket in the output
33
+ const alarmIdx = result.indexOf("Alarm");
34
+ const bucketIdx = result.indexOf("Bucket");
35
+ expect(alarmIdx).toBeLessThan(bucketIdx);
36
+ });
37
+
38
+ test("includes re-exports", () => {
39
+ const result = generateRuntimeIndex([], [], baseConfig);
40
+ expect(result).toContain('export { Sub, Ref } from "../intrinsics";');
41
+ expect(result).toContain('export { Region, AccountId } from "../pseudo";');
42
+ });
43
+
44
+ test("filters colliding re-exports", () => {
45
+ const resources: RuntimeIndexEntry[] = [
46
+ { tsName: "Sub", resourceType: "Test::Sub", attrs: {} },
47
+ { tsName: "Region", resourceType: "Test::Region", attrs: {} },
48
+ ];
49
+
50
+ const result = generateRuntimeIndex(resources, [], baseConfig);
51
+ // "Sub" and "Region" collide with generated exports — only non-colliding survive
52
+ expect(result).toContain('export { Ref } from "../intrinsics";');
53
+ expect(result).toContain('export { AccountId } from "../pseudo";');
54
+ expect(result).not.toContain("export { Sub, Ref }");
55
+ expect(result).not.toContain("export { Region, AccountId }");
56
+ });
57
+
58
+ test("omits re-export line when all names collide", () => {
59
+ const resources: RuntimeIndexEntry[] = [
60
+ { tsName: "Sub", resourceType: "T::Sub", attrs: {} },
61
+ { tsName: "Ref", resourceType: "T::Ref", attrs: {} },
62
+ ];
63
+
64
+ const result = generateRuntimeIndex(resources, [], baseConfig);
65
+ // Both intrinsic names collide, so no intrinsic re-export line
66
+ expect(result).not.toContain("../intrinsics");
67
+ });
68
+
69
+ test("works with no re-exports configured", () => {
70
+ const config: RuntimeIndexConfig = { lexiconName: "bare" };
71
+ const result = generateRuntimeIndex([], [], config);
72
+ expect(result).toContain("// Re-exports for convenience");
73
+ expect(result).not.toContain("export {");
74
+ });
75
+
76
+ test("includes banner and import", () => {
77
+ const result = generateRuntimeIndex([], [], baseConfig);
78
+ expect(result).toContain("// Code generated by chant generate. DO NOT EDIT.");
79
+ expect(result).toContain('import { createResource, createProperty } from "./runtime";');
80
+ });
81
+ });