@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,182 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join, basename, relative } from "node:path";
3
+ import ts from "typescript";
4
+
5
+ export interface ProjectExport {
6
+ name: string;
7
+ file: string;
8
+ className: string;
9
+ }
10
+
11
+ export interface ProjectScan {
12
+ barrelPath: string;
13
+ lexiconPackage: string;
14
+ exports: ProjectExport[];
15
+ }
16
+
17
+ /**
18
+ * Extract the lexicon package from export * declarations in the barrel file.
19
+ * Finds the first `export * from "..."` where the module specifier is not a relative path.
20
+ */
21
+ function extractLexiconPackage(sourceFile: ts.SourceFile): string {
22
+ for (const stmt of sourceFile.statements) {
23
+ if (
24
+ ts.isExportDeclaration(stmt) &&
25
+ !stmt.exportClause &&
26
+ stmt.moduleSpecifier &&
27
+ ts.isStringLiteral(stmt.moduleSpecifier)
28
+ ) {
29
+ const spec = stmt.moduleSpecifier.text;
30
+ if (!spec.startsWith(".")) {
31
+ return spec;
32
+ }
33
+ }
34
+ }
35
+ return "";
36
+ }
37
+
38
+ /**
39
+ * Infer class name from a variable declaration's initializer or type annotation.
40
+ *
41
+ * Patterns:
42
+ * - `export const x = new aws.Bucket(...)` → "Bucket"
43
+ * - `export const x = new _.Bucket(...)` → "Bucket"
44
+ * - `export const x = new Bucket(...)` → "Bucket"
45
+ * - `export const x: aws.Code = ...` → "Code"
46
+ * - `export const x: _.Code = ...` → "Code"
47
+ */
48
+ function inferClassName(decl: ts.VariableDeclaration): string {
49
+ // Check initializer: new X.ClassName(...) or new ClassName(...)
50
+ if (decl.initializer && ts.isNewExpression(decl.initializer)) {
51
+ const expr = decl.initializer.expression;
52
+ if (ts.isPropertyAccessExpression(expr)) {
53
+ return expr.name.text;
54
+ }
55
+ if (ts.isIdentifier(expr)) {
56
+ return expr.text;
57
+ }
58
+ }
59
+
60
+ // Check type annotation: X.ClassName or ClassName
61
+ if (decl.type) {
62
+ if (ts.isTypeReferenceNode(decl.type)) {
63
+ const typeName = decl.type.typeName;
64
+ if (ts.isQualifiedName(typeName)) {
65
+ return typeName.right.text;
66
+ }
67
+ if (ts.isIdentifier(typeName)) {
68
+ return typeName.text;
69
+ }
70
+ }
71
+ }
72
+
73
+ return "";
74
+ }
75
+
76
+ /**
77
+ * Extract named exports from a source file.
78
+ */
79
+ function extractExports(
80
+ sourceFile: ts.SourceFile,
81
+ filePath: string,
82
+ ): ProjectExport[] {
83
+ const results: ProjectExport[] = [];
84
+
85
+ for (const stmt of sourceFile.statements) {
86
+ // export const x = ...
87
+ if (
88
+ ts.isVariableStatement(stmt) &&
89
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
90
+ ) {
91
+ for (const decl of stmt.declarationList.declarations) {
92
+ if (ts.isIdentifier(decl.name)) {
93
+ results.push({
94
+ name: decl.name.text,
95
+ file: filePath,
96
+ className: inferClassName(decl),
97
+ });
98
+ }
99
+ }
100
+ }
101
+
102
+ // export function x() {}
103
+ if (
104
+ ts.isFunctionDeclaration(stmt) &&
105
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&
106
+ stmt.name
107
+ ) {
108
+ results.push({
109
+ name: stmt.name.text,
110
+ file: filePath,
111
+ className: "",
112
+ });
113
+ }
114
+
115
+ // export class X {}
116
+ if (
117
+ ts.isClassDeclaration(stmt) &&
118
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&
119
+ stmt.name
120
+ ) {
121
+ results.push({
122
+ name: stmt.name.text,
123
+ file: filePath,
124
+ className: stmt.name.text,
125
+ });
126
+ }
127
+ }
128
+
129
+ return results;
130
+ }
131
+
132
+ /**
133
+ * Scan a project directory to identify the barrel file, lexicon package,
134
+ * and all exports with their inferred types.
135
+ */
136
+ export function scanProject(dir: string): ProjectScan {
137
+ const barrelPath = join(dir, "_.ts");
138
+
139
+ let barrelContent: string;
140
+ try {
141
+ barrelContent = readFileSync(barrelPath, "utf-8");
142
+ } catch {
143
+ throw new Error(`No barrel file found at ${barrelPath}`);
144
+ }
145
+
146
+ const barrelSource = ts.createSourceFile(
147
+ "_.ts",
148
+ barrelContent,
149
+ ts.ScriptTarget.Latest,
150
+ true,
151
+ );
152
+
153
+ const lexiconPackage = extractLexiconPackage(barrelSource);
154
+
155
+ // Scan sibling .ts files
156
+ const entries = readdirSync(dir, { withFileTypes: true });
157
+ const exports: ProjectExport[] = [];
158
+
159
+ for (const entry of entries) {
160
+ if (!entry.isFile()) continue;
161
+ if (!entry.name.endsWith(".ts")) continue;
162
+ if (entry.name === "_.ts") continue;
163
+ if (entry.name.startsWith("_")) continue;
164
+ if (entry.name.endsWith(".test.ts") || entry.name.endsWith(".spec.ts"))
165
+ continue;
166
+ if (entry.name.endsWith(".d.ts")) continue;
167
+
168
+ const filePath = "./" + entry.name.replace(/\.ts$/, "");
169
+ const fullPath = join(dir, entry.name);
170
+ const content = readFileSync(fullPath, "utf-8");
171
+ const sourceFile = ts.createSourceFile(
172
+ entry.name,
173
+ content,
174
+ ts.ScriptTarget.Latest,
175
+ true,
176
+ );
177
+
178
+ exports.push(...extractExports(sourceFile, filePath));
179
+ }
180
+
181
+ return { barrelPath, lexiconPackage, exports };
182
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateBarrelTypes, syncProject } from "./sync";
3
+ import { withTestDir } from "@intentius/chant-test-utils";
4
+ import { writeFile, readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import type { ProjectScan } from "./scan";
7
+
8
+ describe("generateBarrelTypes", () => {
9
+ test("generates valid .d.ts with import and typed $", () => {
10
+ const scan: ProjectScan = {
11
+ barrelPath: "/project/_.ts",
12
+ lexiconPackage: "@intentius/chant-lexicon-testdom",
13
+ exports: [
14
+ { name: "dataBucket", file: "./data-bucket", className: "Bucket" },
15
+ { name: "logsBucket", file: "./logs-bucket", className: "Bucket" },
16
+ { name: "functionRole", file: "./iam", className: "Role" },
17
+ ],
18
+ };
19
+
20
+ const result = generateBarrelTypes(scan);
21
+ expect(result).toContain(
22
+ 'import type * as _lexicon from "@intentius/chant-lexicon-testdom";',
23
+ );
24
+ expect(result).toContain("dataBucket: _lexicon.Bucket;");
25
+ expect(result).toContain("logsBucket: _lexicon.Bucket;");
26
+ expect(result).toContain("functionRole: _lexicon.Role;");
27
+ expect(result).toContain("export declare const $: typeof _barrel;");
28
+ });
29
+
30
+ test("uses unknown for unresolvable types", () => {
31
+ const scan: ProjectScan = {
32
+ barrelPath: "/project/_.ts",
33
+ lexiconPackage: "@intentius/chant-lexicon-testdom",
34
+ exports: [
35
+ { name: "dataBucket", file: "./data-bucket", className: "Bucket" },
36
+ { name: "greeting", file: "./utils", className: "" },
37
+ ],
38
+ };
39
+
40
+ const result = generateBarrelTypes(scan);
41
+ expect(result).toContain("dataBucket: _lexicon.Bucket;");
42
+ expect(result).toContain("greeting: unknown;");
43
+ });
44
+
45
+ test("handles scan with no lexicon package", () => {
46
+ const scan: ProjectScan = {
47
+ barrelPath: "/project/_.ts",
48
+ lexiconPackage: "",
49
+ exports: [
50
+ { name: "bucket", file: "./storage", className: "Bucket" },
51
+ ],
52
+ };
53
+
54
+ const result = generateBarrelTypes(scan);
55
+ expect(result).not.toContain("import type");
56
+ expect(result).toContain("bucket: Bucket;");
57
+ });
58
+ });
59
+
60
+ describe("syncProject", () => {
61
+ test("writes _.d.ts to directory", async () => {
62
+ await withTestDir(async (dir) => {
63
+ await writeFile(
64
+ join(dir, "_.ts"),
65
+ `export * from "@intentius/chant-lexicon-testdom";\n`,
66
+ );
67
+ await writeFile(
68
+ join(dir, "storage.ts"),
69
+ [
70
+ `import * as td from "@intentius/chant-lexicon-testdom";`,
71
+ `export const dataBucket = new td.Bucket({});`,
72
+ `export const logsBucket = new td.Bucket({});`,
73
+ ].join("\n"),
74
+ );
75
+
76
+ syncProject(dir);
77
+
78
+ const dtsContent = await readFile(join(dir, "_.d.ts"), "utf-8");
79
+ expect(dtsContent).toContain(
80
+ 'import type * as _lexicon from "@intentius/chant-lexicon-testdom";',
81
+ );
82
+ expect(dtsContent).toContain("dataBucket: _lexicon.Bucket;");
83
+ expect(dtsContent).toContain("logsBucket: _lexicon.Bucket;");
84
+ expect(dtsContent).toContain("export declare const $: typeof _barrel;");
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,46 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { scanProject, type ProjectScan } from "./scan";
4
+
5
+ /**
6
+ * Generate a _.d.ts file content from a project scan.
7
+ * Produces typed declarations for the barrel's $ proxy.
8
+ */
9
+ export function generateBarrelTypes(scan: ProjectScan): string {
10
+ const lines: string[] = [];
11
+
12
+ lines.push("// _.d.ts (auto-generated by chant lint)");
13
+
14
+ if (scan.lexiconPackage) {
15
+ lines.push(`import type * as _lexicon from "${scan.lexiconPackage}";`);
16
+ }
17
+
18
+ lines.push("declare const _barrel: {");
19
+
20
+ for (const exp of scan.exports) {
21
+ let typeName: string;
22
+ if (!exp.className) {
23
+ typeName = "unknown";
24
+ } else if (scan.lexiconPackage) {
25
+ typeName = `_lexicon.${exp.className}`;
26
+ } else {
27
+ typeName = exp.className;
28
+ }
29
+ lines.push(` ${exp.name}: ${typeName};`);
30
+ }
31
+
32
+ lines.push("};");
33
+ lines.push("export declare const $: typeof _barrel;");
34
+ lines.push("");
35
+
36
+ return lines.join("\n");
37
+ }
38
+
39
+ /**
40
+ * Scan a project directory and write the _.d.ts file.
41
+ */
42
+ export function syncProject(dir: string): void {
43
+ const scan = scanProject(dir);
44
+ const content = generateBarrelTypes(scan);
45
+ writeFileSync(join(dir, "_.d.ts"), content, "utf-8");
46
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { validateProjectStructure } from "./project-validation";
3
+ import { mkdirSync, writeFileSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const TEST_DIR = join(import.meta.dir, "__test_project_validation__");
7
+
8
+ beforeEach(() => {
9
+ mkdirSync(TEST_DIR, { recursive: true });
10
+ });
11
+
12
+ afterEach(() => {
13
+ rmSync(TEST_DIR, { recursive: true, force: true });
14
+ });
15
+
16
+ describe("validateProjectStructure", () => {
17
+ test("warns on empty directory", () => {
18
+ const result = validateProjectStructure(TEST_DIR);
19
+ expect(result.valid).toBe(true); // warnings don't block
20
+ expect(result.issues.length).toBeGreaterThan(0);
21
+ expect(result.issues.some((i) => i.message.includes("config"))).toBe(true);
22
+ expect(result.issues.some((i) => i.message.includes("src/"))).toBe(true);
23
+ });
24
+
25
+ test("no config warning when chant.config.ts exists", () => {
26
+ writeFileSync(join(TEST_DIR, "chant.config.ts"), "export default {};");
27
+ mkdirSync(join(TEST_DIR, "src"));
28
+ const result = validateProjectStructure(TEST_DIR);
29
+ expect(result.issues.some((i) => i.message.includes("config"))).toBe(false);
30
+ });
31
+
32
+ test("no config warning when chant.config.json exists", () => {
33
+ writeFileSync(join(TEST_DIR, "chant.config.json"), "{}");
34
+ mkdirSync(join(TEST_DIR, "src"));
35
+ const result = validateProjectStructure(TEST_DIR);
36
+ expect(result.issues.some((i) => i.message.includes("config"))).toBe(false);
37
+ });
38
+
39
+ test("warns on missing core types", () => {
40
+ writeFileSync(join(TEST_DIR, "chant.config.ts"), "export default {};");
41
+ mkdirSync(join(TEST_DIR, "src"));
42
+ const result = validateProjectStructure(TEST_DIR);
43
+ expect(result.issues.some((i) => i.message.includes("core"))).toBe(true);
44
+ });
45
+
46
+ test("warns on missing lexicon types", () => {
47
+ writeFileSync(join(TEST_DIR, "chant.config.ts"), "export default {};");
48
+ mkdirSync(join(TEST_DIR, "src"));
49
+ const result = validateProjectStructure(TEST_DIR, ["testdom"]);
50
+ expect(result.issues.some((i) => i.message.includes('"testdom"'))).toBe(true);
51
+ });
52
+
53
+ test("passes with complete structure", () => {
54
+ writeFileSync(join(TEST_DIR, "chant.config.ts"), "export default {};");
55
+ mkdirSync(join(TEST_DIR, "src"));
56
+ mkdirSync(join(TEST_DIR, ".chant", "types", "core"), { recursive: true });
57
+ writeFileSync(join(TEST_DIR, ".chant", "types", "core", "index.d.ts"), "export {};");
58
+ mkdirSync(join(TEST_DIR, ".chant", "types", "lexicon-testdom"), { recursive: true });
59
+
60
+ const result = validateProjectStructure(TEST_DIR, ["testdom"]);
61
+ expect(result.valid).toBe(true);
62
+ expect(result.issues).toHaveLength(0);
63
+ });
64
+ });
@@ -0,0 +1,79 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ /**
5
+ * Issue found during project structure check
6
+ */
7
+ export interface ProjectIssue {
8
+ /** Severity: "error" blocks commands, "warning" is advisory */
9
+ severity: "error" | "warning";
10
+ /** Human-readable message */
11
+ message: string;
12
+ }
13
+
14
+ /**
15
+ * Result of project structure validation
16
+ */
17
+ export interface ProjectValidation {
18
+ /** Whether the project structure is valid (no errors, warnings OK) */
19
+ valid: boolean;
20
+ /** Issues found */
21
+ issues: ProjectIssue[];
22
+ }
23
+
24
+ /**
25
+ * Validate project structure.
26
+ *
27
+ * Checks that required files and directories exist for a chant project.
28
+ *
29
+ * @param projectDir - Root directory of the chant project
30
+ * @param lexicons - Lexicon names from config (e.g. ["aws"])
31
+ */
32
+ export function validateProjectStructure(
33
+ projectDir: string,
34
+ lexicons: string[] = [],
35
+ ): ProjectValidation {
36
+ const issues: ProjectIssue[] = [];
37
+
38
+ // Check for config file
39
+ const hasTsConfig = existsSync(join(projectDir, "chant.config.ts"));
40
+ const hasJsonConfig = existsSync(join(projectDir, "chant.config.json"));
41
+ if (!hasTsConfig && !hasJsonConfig) {
42
+ issues.push({
43
+ severity: "warning",
44
+ message: "No chant config found. Run \"chant init\" to create one.",
45
+ });
46
+ }
47
+
48
+ // Check for src directory
49
+ if (!existsSync(join(projectDir, "src"))) {
50
+ issues.push({
51
+ severity: "warning",
52
+ message: "No src/ directory found.",
53
+ });
54
+ }
55
+
56
+ // Check for .chant/types/core/
57
+ if (!existsSync(join(projectDir, ".chant", "types", "core", "index.d.ts"))) {
58
+ issues.push({
59
+ severity: "warning",
60
+ message: "Core types not installed in .chant/types/core/. Run \"chant update\" to sync.",
61
+ });
62
+ }
63
+
64
+ // Check for lexicon type stubs
65
+ for (const lexicon of lexicons) {
66
+ const lexiconDir = join(projectDir, ".chant", "types", `lexicon-${lexicon}`);
67
+ if (!existsSync(lexiconDir)) {
68
+ issues.push({
69
+ severity: "warning",
70
+ message: `Lexicon types not installed for "${lexicon}". Run "chant update" to sync.`,
71
+ });
72
+ }
73
+ }
74
+
75
+ return {
76
+ valid: issues.every((i) => i.severity !== "error"),
77
+ issues,
78
+ };
79
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { PseudoParameter, createPseudoParameters } from "./pseudo-parameter";
3
+ import { INTRINSIC_MARKER } from "./intrinsic";
4
+
5
+ describe("PseudoParameter", () => {
6
+ test("toJSON returns Ref object", () => {
7
+ const p = new PseudoParameter("AWS::StackName");
8
+ expect(p.toJSON()).toEqual({ Ref: "AWS::StackName" });
9
+ });
10
+
11
+ test("toString returns interpolation syntax", () => {
12
+ const p = new PseudoParameter("AWS::Region");
13
+ expect(p.toString()).toBe("${AWS::Region}");
14
+ });
15
+
16
+ test("has INTRINSIC_MARKER", () => {
17
+ const p = new PseudoParameter("AWS::AccountId");
18
+ expect(p[INTRINSIC_MARKER]).toBe(true);
19
+ });
20
+ });
21
+
22
+ describe("createPseudoParameters", () => {
23
+ test("creates typed namespace from name map", () => {
24
+ const params = createPseudoParameters({
25
+ Region: "AWS::Region",
26
+ StackName: "AWS::StackName",
27
+ });
28
+
29
+ expect(params.Region).toBeInstanceOf(PseudoParameter);
30
+ expect(params.StackName).toBeInstanceOf(PseudoParameter);
31
+ expect(params.Region.toJSON()).toEqual({ Ref: "AWS::Region" });
32
+ expect(params.StackName.toJSON()).toEqual({ Ref: "AWS::StackName" });
33
+ });
34
+
35
+ test("works with empty map", () => {
36
+ const params = createPseudoParameters({});
37
+ expect(Object.keys(params)).toHaveLength(0);
38
+ });
39
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Generic pseudo-parameter class for lexicon-provided global named references.
3
+ *
4
+ * AWS uses "AWS::StackName", Terraform has "path.module", Azure has
5
+ * "subscription().id" — every format needs the same class that serializes
6
+ * to a Ref-like structure and has the INTRINSIC_MARKER.
7
+ */
8
+
9
+ import { INTRINSIC_MARKER, type Intrinsic } from "./intrinsic";
10
+
11
+ export class PseudoParameter implements Intrinsic {
12
+ readonly [INTRINSIC_MARKER] = true as const;
13
+ private refName: string;
14
+
15
+ constructor(refName: string) {
16
+ this.refName = refName;
17
+ }
18
+
19
+ toJSON(): { Ref: string } {
20
+ return { Ref: this.refName };
21
+ }
22
+
23
+ toString(): string {
24
+ return `\${${this.refName}}`;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Create a namespace of pseudo-parameters from a name map.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * export const { StackName, Region } = createPseudoParameters({
34
+ * StackName: "AWS::StackName",
35
+ * Region: "AWS::Region",
36
+ * });
37
+ * ```
38
+ */
39
+ export function createPseudoParameters<T extends Record<string, string>>(
40
+ nameMap: T,
41
+ ): { [K in keyof T]: PseudoParameter } {
42
+ const result = {} as Record<string, PseudoParameter>;
43
+ for (const [key, refName] of Object.entries(nameMap)) {
44
+ result[key] = new PseudoParameter(refName);
45
+ }
46
+ return result as { [K in keyof T]: PseudoParameter };
47
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Runtime factory constructors for generated resource and property classes.
3
+ *
4
+ * Creates classes that implement the Declarable interface with DECLARABLE_MARKER,
5
+ * lexicon, entityType, kind, and attribute references.
6
+ */
7
+
8
+ import { DECLARABLE_MARKER } from "./declarable";
9
+ import { AttrRef } from "./attrref";
10
+
11
+ /**
12
+ * Create a resource class for a given type.
13
+ * The returned constructor creates Declarable objects with:
14
+ * - DECLARABLE_MARKER for type identification
15
+ * - lexicon identifier
16
+ * - entityType set to the resource type
17
+ * - kind "resource"
18
+ * - props stored from constructor argument
19
+ * - AttrRef instances for each attribute
20
+ */
21
+ export function createResource(
22
+ type: string,
23
+ lexicon: string,
24
+ attrMap: Record<string, string>,
25
+ ): new (props: Record<string, unknown>) => Record<string, unknown> {
26
+ const ResourceClass = function (this: Record<string, unknown>, props: Record<string, unknown>) {
27
+ Object.defineProperty(this, DECLARABLE_MARKER, { value: true, enumerable: false });
28
+ Object.defineProperty(this, "lexicon", { value: lexicon, enumerable: false });
29
+ Object.defineProperty(this, "entityType", { value: type, enumerable: false });
30
+ Object.defineProperty(this, "kind", { value: "resource", enumerable: false });
31
+ Object.defineProperty(this, "props", { value: props ?? {}, enumerable: false, configurable: true });
32
+
33
+ // Create AttrRef instances for each attribute
34
+ // Must be enumerable so getAttributes() can discover them for resolveAttrRefs()
35
+ for (const [camelName, attrName] of Object.entries(attrMap)) {
36
+ Object.defineProperty(this, camelName, {
37
+ value: new AttrRef(this, attrName),
38
+ enumerable: true,
39
+ writable: false,
40
+ });
41
+ }
42
+ } as unknown as new (props: Record<string, unknown>) => Record<string, unknown>;
43
+
44
+ // Set the constructor name for debugging
45
+ Object.defineProperty(ResourceClass, "name", { value: type.split("::").pop() ?? type });
46
+
47
+ return ResourceClass;
48
+ }
49
+
50
+ /**
51
+ * Create a property-kind class for a given property type.
52
+ */
53
+ export function createProperty(
54
+ type: string,
55
+ lexicon: string,
56
+ ): new (props: Record<string, unknown>) => Record<string, unknown> {
57
+ const PropertyClass = function (this: Record<string, unknown>, props: Record<string, unknown>) {
58
+ Object.defineProperty(this, DECLARABLE_MARKER, { value: true, enumerable: false });
59
+ Object.defineProperty(this, "lexicon", { value: lexicon, enumerable: false });
60
+ Object.defineProperty(this, "entityType", { value: type, enumerable: false });
61
+ Object.defineProperty(this, "kind", { value: "property", enumerable: false });
62
+ Object.defineProperty(this, "props", { value: props ?? {}, enumerable: false, configurable: true });
63
+ } as unknown as new (props: Record<string, unknown>) => Record<string, unknown>;
64
+
65
+ Object.defineProperty(PropertyClass, "name", { value: type.split(".").pop() ?? type });
66
+
67
+ return PropertyClass;
68
+ }