@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,78 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { DiscoveryCache } from "./cache";
3
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+
7
+ describe("DiscoveryCache", () => {
8
+ test("hasFileChanged returns true for unknown files", () => {
9
+ const cache = new DiscoveryCache();
10
+ expect(cache.hasFileChanged("/nonexistent/file.ts")).toBe(true);
11
+ });
12
+
13
+ test("trackFileImport and hasFileChanged detect changes", async () => {
14
+ const cache = new DiscoveryCache();
15
+ const dir = join(tmpdir(), `chant-cache-test-${Date.now()}`);
16
+ mkdirSync(dir, { recursive: true });
17
+
18
+ const file = join(dir, "test.ts");
19
+ writeFileSync(file, "export const a = 1;");
20
+
21
+ const { statSync: stat } = require("fs");
22
+ const { mtimeMs } = stat(file);
23
+ cache.trackFileImport(file, mtimeMs, ["a"]);
24
+
25
+ // File hasn't changed
26
+ expect(cache.hasFileChanged(file)).toBe(false);
27
+
28
+ // Wait briefly to ensure mtime differs, then modify
29
+ await new Promise((r) => setTimeout(r, 50));
30
+ writeFileSync(file, "export const a = 2;");
31
+
32
+ // File has changed
33
+ expect(cache.hasFileChanged(file)).toBe(true);
34
+
35
+ rmSync(dir, { recursive: true, force: true });
36
+ });
37
+
38
+ test("invalidate removes entries and returns entity names", () => {
39
+ const cache = new DiscoveryCache();
40
+ cache.trackFileImport("/a.ts", 100, ["bucket", "role"]);
41
+ cache.trackFileImport("/b.ts", 200, ["table"]);
42
+
43
+ const invalidated = cache.invalidate(["/a.ts"]);
44
+ expect(invalidated.has("bucket")).toBe(true);
45
+ expect(invalidated.has("role")).toBe(true);
46
+ expect(invalidated.has("table")).toBe(false);
47
+
48
+ // a.ts is now unknown
49
+ expect(cache.hasFileChanged("/a.ts")).toBe(true);
50
+ });
51
+
52
+ test("invalidate with unknown file returns empty set", () => {
53
+ const cache = new DiscoveryCache();
54
+ const invalidated = cache.invalidate(["/unknown.ts"]);
55
+ expect(invalidated.size).toBe(0);
56
+ });
57
+
58
+ test("trackedFiles returns all tracked file paths", () => {
59
+ const cache = new DiscoveryCache();
60
+ cache.trackFileImport("/a.ts", 100, ["x"]);
61
+ cache.trackFileImport("/b.ts", 200, ["y"]);
62
+ expect(cache.trackedFiles().sort()).toEqual(["/a.ts", "/b.ts"]);
63
+ });
64
+
65
+ test("getFileEntities returns entity names for tracked file", () => {
66
+ const cache = new DiscoveryCache();
67
+ cache.trackFileImport("/a.ts", 100, ["bucket", "role"]);
68
+ expect(cache.getFileEntities("/a.ts")).toEqual(["bucket", "role"]);
69
+ expect(cache.getFileEntities("/unknown.ts")).toBeUndefined();
70
+ });
71
+
72
+ test("clear removes all entries", () => {
73
+ const cache = new DiscoveryCache();
74
+ cache.trackFileImport("/a.ts", 100, ["x"]);
75
+ cache.clear();
76
+ expect(cache.trackedFiles()).toEqual([]);
77
+ });
78
+ });
@@ -0,0 +1,86 @@
1
+ import { statSync } from "fs";
2
+
3
+ /**
4
+ * Cache entry for a single file
5
+ */
6
+ interface FileCacheEntry {
7
+ /** Last modified time in milliseconds */
8
+ mtime: number;
9
+ /** Entity names exported by this file */
10
+ entityNames: string[];
11
+ }
12
+
13
+ /**
14
+ * Discovery cache for incremental rebuilds.
15
+ *
16
+ * Tracks file mtimes and file→entity mappings so that only changed files
17
+ * need to be re-imported during watch-mode rebuilds.
18
+ */
19
+ export class DiscoveryCache {
20
+ private files = new Map<string, FileCacheEntry>();
21
+
22
+ /**
23
+ * Check if a file has changed since it was last tracked.
24
+ * Returns true if the file is new or its mtime has changed.
25
+ */
26
+ hasFileChanged(filePath: string): boolean {
27
+ const entry = this.files.get(filePath);
28
+ if (!entry) return true;
29
+
30
+ try {
31
+ const stat = statSync(filePath);
32
+ return stat.mtimeMs !== entry.mtime;
33
+ } catch {
34
+ // File may have been deleted
35
+ return true;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Record a successful import, storing the file's mtime and entity names.
41
+ */
42
+ trackFileImport(filePath: string, mtime: number, entityNames: string[]): void {
43
+ this.files.set(filePath, { mtime, entityNames });
44
+ }
45
+
46
+ /**
47
+ * Invalidate cache entries for the given files.
48
+ * Returns the set of entity names that were invalidated.
49
+ */
50
+ invalidate(changedFiles: string[]): Set<string> {
51
+ const invalidatedEntities = new Set<string>();
52
+
53
+ for (const file of changedFiles) {
54
+ const entry = this.files.get(file);
55
+ if (entry) {
56
+ for (const name of entry.entityNames) {
57
+ invalidatedEntities.add(name);
58
+ }
59
+ this.files.delete(file);
60
+ }
61
+ }
62
+
63
+ return invalidatedEntities;
64
+ }
65
+
66
+ /**
67
+ * Get all tracked file paths.
68
+ */
69
+ trackedFiles(): string[] {
70
+ return [...this.files.keys()];
71
+ }
72
+
73
+ /**
74
+ * Get entity names for a tracked file.
75
+ */
76
+ getFileEntities(filePath: string): string[] | undefined {
77
+ return this.files.get(filePath)?.entityNames;
78
+ }
79
+
80
+ /**
81
+ * Clear all cached data.
82
+ */
83
+ clear(): void {
84
+ this.files.clear();
85
+ }
86
+ }
@@ -0,0 +1,269 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import { collectEntities } from "./collect";
3
+ import { DECLARABLE_MARKER } from "../declarable";
4
+ import { DiscoveryError } from "../errors";
5
+ import { Composite, CompositeRegistry } from "../composite";
6
+ import { createMockEntity, expectToThrow } from "@intentius/chant-test-utils";
7
+
8
+ describe("collectEntities", () => {
9
+ test("collects declarable entities from single module", () => {
10
+ const entity1 = createMockEntity("test");
11
+
12
+ const modules = [
13
+ {
14
+ file: "test.ts",
15
+ exports: {
16
+ entity1,
17
+ notDeclarable: "value",
18
+ },
19
+ },
20
+ ];
21
+
22
+ const result = collectEntities(modules);
23
+ expect(result.size).toBe(1);
24
+ expect(result.get("entity1")).toBe(entity1);
25
+ });
26
+
27
+ test("collects declarable entities from multiple modules", () => {
28
+ const entity1 = createMockEntity("type1");
29
+ const entity2 = createMockEntity("type2");
30
+ const entity3 = createMockEntity("type3");
31
+
32
+ const modules = [
33
+ {
34
+ file: "file1.ts",
35
+ exports: {
36
+ entity1,
37
+ helper: "function",
38
+ },
39
+ },
40
+ {
41
+ file: "file2.ts",
42
+ exports: {
43
+ entity2,
44
+ entity3,
45
+ util: 42,
46
+ },
47
+ },
48
+ ];
49
+
50
+ const result = collectEntities(modules);
51
+ expect(result.size).toBe(3);
52
+ expect(result.get("entity1")).toBe(entity1);
53
+ expect(result.get("entity2")).toBe(entity2);
54
+ expect(result.get("entity3")).toBe(entity3);
55
+ });
56
+
57
+ test("ignores non-declarable exports", () => {
58
+ const entity = createMockEntity("test");
59
+
60
+ const modules = [
61
+ {
62
+ file: "mixed.ts",
63
+ exports: {
64
+ entity,
65
+ string: "value",
66
+ number: 123,
67
+ object: { key: "value" },
68
+ array: [1, 2, 3],
69
+ func: () => "test",
70
+ nullValue: null,
71
+ undefinedValue: undefined,
72
+ boolValue: true,
73
+ },
74
+ },
75
+ ];
76
+
77
+ const result = collectEntities(modules);
78
+ expect(result.size).toBe(1);
79
+ expect(result.get("entity")).toBe(entity);
80
+ });
81
+
82
+ test("returns empty map when no declarables found", () => {
83
+ const modules = [
84
+ {
85
+ file: "empty.ts",
86
+ exports: {
87
+ value1: "string",
88
+ value2: 42,
89
+ value3: { data: "object" },
90
+ },
91
+ },
92
+ ];
93
+
94
+ const result = collectEntities(modules);
95
+ expect(result.size).toBe(0);
96
+ });
97
+
98
+ test("returns empty map when modules array is empty", () => {
99
+ const result = collectEntities([]);
100
+ expect(result.size).toBe(0);
101
+ });
102
+
103
+ test("throws DiscoveryError with type 'resolution' on duplicate export names", async () => {
104
+ const entity1 = createMockEntity("type1");
105
+ const entity2 = createMockEntity("type2");
106
+
107
+ const modules = [
108
+ {
109
+ file: "file1.ts",
110
+ exports: { myEntity: entity1 },
111
+ },
112
+ {
113
+ file: "file2.ts",
114
+ exports: { myEntity: entity2 },
115
+ },
116
+ ];
117
+
118
+ await expectToThrow(
119
+ () => collectEntities(modules),
120
+ DiscoveryError,
121
+ (error) => {
122
+ expect(error.type).toBe("resolution");
123
+ expect(error.file).toBe("file2.ts");
124
+ expect(error.message).toContain("Duplicate");
125
+ expect(error.message).toContain("myEntity");
126
+ }
127
+ );
128
+ });
129
+
130
+ test("throws DiscoveryError with detailed message for duplicates", async () => {
131
+ const entity1 = createMockEntity("test");
132
+ const entity2 = createMockEntity("test");
133
+
134
+ const modules = [
135
+ {
136
+ file: "a.ts",
137
+ exports: { duplicateName: entity1 },
138
+ },
139
+ {
140
+ file: "b.ts",
141
+ exports: { duplicateName: entity2 },
142
+ },
143
+ ];
144
+
145
+ await expectToThrow(
146
+ () => collectEntities(modules),
147
+ DiscoveryError,
148
+ (error) => {
149
+ expect(error.type).toBe("resolution");
150
+ expect(error.message).toBe('Duplicate export name "duplicateName" found');
151
+ }
152
+ );
153
+ });
154
+
155
+ test("allows same export name if not declarable in one module", () => {
156
+ const entity = createMockEntity("test");
157
+
158
+ const modules = [
159
+ {
160
+ file: "file1.ts",
161
+ exports: { name: "not declarable" },
162
+ },
163
+ {
164
+ file: "file2.ts",
165
+ exports: { name: entity },
166
+ },
167
+ ];
168
+
169
+ const result = collectEntities(modules);
170
+ expect(result.size).toBe(1);
171
+ expect(result.get("name")).toBe(entity);
172
+ });
173
+
174
+ test("preserves entity type information", () => {
175
+ const entity1 = createMockEntity("parameter");
176
+ const entity2 = createMockEntity("output");
177
+
178
+ const modules = [
179
+ {
180
+ file: "test.ts",
181
+ exports: { entity1, entity2 },
182
+ },
183
+ ];
184
+
185
+ const result = collectEntities(modules);
186
+ expect(result.get("entity1")?.entityType).toBe("parameter");
187
+ expect(result.get("entity2")?.entityType).toBe("output");
188
+ });
189
+
190
+ test("correctly identifies objects without DECLARABLE_MARKER as non-declarable", () => {
191
+ const fakeDeclarable = {
192
+ entityType: "fake",
193
+ // Missing DECLARABLE_MARKER
194
+ };
195
+
196
+ const modules = [
197
+ {
198
+ file: "test.ts",
199
+ exports: { fakeDeclarable },
200
+ },
201
+ ];
202
+
203
+ const result = collectEntities(modules);
204
+ expect(result.size).toBe(0);
205
+ });
206
+
207
+ test("correctly identifies objects with wrong marker value as non-declarable", () => {
208
+ const fakeDeclarable = {
209
+ entityType: "fake",
210
+ [DECLARABLE_MARKER]: false, // Wrong value
211
+ };
212
+
213
+ const modules = [
214
+ {
215
+ file: "test.ts",
216
+ exports: { fakeDeclarable },
217
+ },
218
+ ];
219
+
220
+ const result = collectEntities(modules);
221
+ expect(result.size).toBe(0);
222
+ });
223
+
224
+ test("error serializes to JSON correctly", async () => {
225
+ const modules = [
226
+ {
227
+ file: "first.ts",
228
+ exports: { dup: createMockEntity("test") },
229
+ },
230
+ {
231
+ file: "second.ts",
232
+ exports: { dup: createMockEntity("test") },
233
+ },
234
+ ];
235
+
236
+ const error = await expectToThrow(
237
+ () => collectEntities(modules),
238
+ DiscoveryError
239
+ );
240
+
241
+ const json = error.toJSON();
242
+ expect(json.name).toBe("DiscoveryError");
243
+ expect(json.file).toBe("second.ts");
244
+ expect(json.type).toBe("resolution");
245
+ expect(json.message).toBeDefined();
246
+ });
247
+ });
248
+
249
+ describe("collectEntities with composites", () => {
250
+ beforeEach(() => {
251
+ CompositeRegistry.clear();
252
+ });
253
+
254
+ test("composite is expanded into individual entities", () => {
255
+ const Comp = Composite(() => ({
256
+ a: createMockEntity("TestA"),
257
+ b: createMockEntity("TestB"),
258
+ }));
259
+ const instance = Comp({});
260
+
261
+ const entities = collectEntities([
262
+ { file: "test.ts", exports: { myComp: instance } },
263
+ ]);
264
+
265
+ expect(entities.has("myComp_a")).toBe(true);
266
+ expect(entities.has("myComp_b")).toBe(true);
267
+ expect(entities.has("myComp")).toBe(false);
268
+ });
269
+ });
@@ -0,0 +1,51 @@
1
+ import { isDeclarable, type Declarable } from "../declarable";
2
+ import { isCompositeInstance, expandComposite } from "../composite";
3
+ import { DiscoveryError } from "../errors";
4
+
5
+ /**
6
+ * Collects all declarable entities from imported modules.
7
+ * CompositeInstance exports are expanded into individual entities
8
+ * with `{exportName}_{memberName}` naming.
9
+ *
10
+ * @param modules - Array of module records with their exports
11
+ * @returns Map of export name to Declarable entity
12
+ * @throws {DiscoveryError} with type "resolution" if duplicate export names are found
13
+ */
14
+ export function collectEntities(
15
+ modules: Array<{ file: string; exports: Record<string, unknown> }>
16
+ ): Map<string, Declarable> {
17
+ const entities = new Map<string, Declarable>();
18
+
19
+ for (const { file, exports } of modules) {
20
+ for (const [name, value] of Object.entries(exports)) {
21
+ if (isDeclarable(value)) {
22
+ if (entities.has(name)) {
23
+ // Same object re-exported from multiple files (e.g. barrel re-exports) is fine
24
+ if (entities.get(name) !== value) {
25
+ throw new DiscoveryError(
26
+ file,
27
+ `Duplicate export name "${name}" found`,
28
+ "resolution"
29
+ );
30
+ }
31
+ } else {
32
+ entities.set(name, value);
33
+ }
34
+ } else if (isCompositeInstance(value)) {
35
+ const expanded = expandComposite(name, value);
36
+ for (const [expandedName, entity] of expanded) {
37
+ if (entities.has(expandedName)) {
38
+ throw new DiscoveryError(
39
+ file,
40
+ `Duplicate entity name "${expandedName}" from composite expansion of "${name}"`,
41
+ "resolution",
42
+ );
43
+ }
44
+ entities.set(expandedName, entity);
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ return entities;
51
+ }