@intentius/chant-lexicon-terraform 0.124.52 → 0.124.54

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 (336) hide show
  1. package/README.md +7 -6
  2. package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
  3. package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
  4. package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
  5. package/dist/__fixtures__/report/generate.d.ts +26 -0
  6. package/dist/__fixtures__/report/generate.d.ts.map +1 -0
  7. package/dist/__fixtures__/report/generate.js +58 -0
  8. package/dist/applied-state.d.ts +27 -0
  9. package/dist/applied-state.d.ts.map +1 -0
  10. package/dist/applied-state.js +44 -0
  11. package/dist/backend-config.d.ts +53 -0
  12. package/dist/backend-config.d.ts.map +1 -0
  13. package/dist/backend-config.js +136 -0
  14. package/dist/behaviour/kinds.d.ts.map +1 -1
  15. package/dist/behaviour/kinds.js +4 -0
  16. package/dist/behaviour/predict.d.ts.map +1 -1
  17. package/dist/behaviour/predict.js +6 -2
  18. package/dist/behaviour/reference-catalog.d.ts +16 -5
  19. package/dist/behaviour/reference-catalog.d.ts.map +1 -1
  20. package/dist/behaviour/reference-catalog.js +16 -5
  21. package/dist/behaviour/request.d.ts.map +1 -1
  22. package/dist/behaviour/request.js +3 -2
  23. package/dist/changed-roots.d.ts +6 -4
  24. package/dist/changed-roots.d.ts.map +1 -1
  25. package/dist/changed-roots.js +14 -18
  26. package/dist/commands.d.ts.map +1 -1
  27. package/dist/commands.js +4 -3
  28. package/dist/components/capability-plugin.d.ts +65 -3
  29. package/dist/components/capability-plugin.d.ts.map +1 -1
  30. package/dist/components/capability-plugin.js +197 -13
  31. package/dist/components/compose-group.d.ts +145 -0
  32. package/dist/components/compose-group.d.ts.map +1 -0
  33. package/dist/components/compose-group.js +407 -0
  34. package/dist/components/placeholder-preview.d.ts +78 -0
  35. package/dist/components/placeholder-preview.d.ts.map +1 -0
  36. package/dist/components/placeholder-preview.js +254 -0
  37. package/dist/compose/apply-events.d.ts +46 -0
  38. package/dist/compose/apply-events.d.ts.map +1 -0
  39. package/dist/compose/apply-events.js +77 -0
  40. package/dist/compose/estate.d.ts +194 -0
  41. package/dist/compose/estate.d.ts.map +1 -0
  42. package/dist/compose/estate.js +363 -0
  43. package/dist/compose/hcl.d.ts +99 -0
  44. package/dist/compose/hcl.d.ts.map +1 -0
  45. package/dist/compose/hcl.js +302 -0
  46. package/dist/compose/module-address.d.ts +24 -0
  47. package/dist/compose/module-address.d.ts.map +1 -0
  48. package/dist/compose/module-address.js +43 -0
  49. package/dist/compose/root.d.ts +127 -0
  50. package/dist/compose/root.d.ts.map +1 -0
  51. package/dist/compose/root.js +586 -0
  52. package/dist/compose/run.d.ts +183 -0
  53. package/dist/compose/run.d.ts.map +1 -0
  54. package/dist/compose/run.js +447 -0
  55. package/dist/compose/slice.d.ts +102 -0
  56. package/dist/compose/slice.d.ts.map +1 -0
  57. package/dist/compose/slice.js +249 -0
  58. package/dist/compose/state-address.d.ts +28 -0
  59. package/dist/compose/state-address.d.ts.map +1 -0
  60. package/dist/compose/state-address.js +154 -0
  61. package/dist/compose/state-merge.d.ts +52 -0
  62. package/dist/compose/state-merge.d.ts.map +1 -0
  63. package/dist/compose/state-merge.js +135 -0
  64. package/dist/compose.d.ts +179 -0
  65. package/dist/compose.d.ts.map +1 -0
  66. package/dist/compose.js +482 -0
  67. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  68. package/dist/composites/terraform-apply-op.js +7 -3
  69. package/dist/config.d.ts +59 -13
  70. package/dist/config.d.ts.map +1 -1
  71. package/dist/config.js +43 -9
  72. package/dist/describe-resources.d.ts +74 -12
  73. package/dist/describe-resources.d.ts.map +1 -1
  74. package/dist/describe-resources.js +374 -66
  75. package/dist/drift-read.d.ts +33 -0
  76. package/dist/drift-read.d.ts.map +1 -0
  77. package/dist/drift-read.js +243 -0
  78. package/dist/drift-record.d.ts +147 -0
  79. package/dist/drift-record.d.ts.map +1 -0
  80. package/dist/drift-record.js +383 -0
  81. package/dist/hcl/edges.d.ts.map +1 -1
  82. package/dist/hcl/edges.js +24 -1
  83. package/dist/hcl/files.d.ts +61 -0
  84. package/dist/hcl/files.d.ts.map +1 -0
  85. package/dist/hcl/files.js +221 -0
  86. package/dist/hcl/instances.d.ts +82 -0
  87. package/dist/hcl/instances.d.ts.map +1 -0
  88. package/dist/hcl/instances.js +390 -0
  89. package/dist/hcl/parse.d.ts +81 -9
  90. package/dist/hcl/parse.d.ts.map +1 -1
  91. package/dist/hcl/parse.js +114 -16
  92. package/dist/hcl/roots.d.ts.map +1 -1
  93. package/dist/hcl/roots.js +5 -0
  94. package/dist/hcl/suppressions.d.ts.map +1 -1
  95. package/dist/hcl/suppressions.js +8 -2
  96. package/dist/index.d.ts +11 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +21 -2
  99. package/dist/integrity.json +3 -3
  100. package/dist/live-plan-reasons.d.ts +21 -0
  101. package/dist/live-plan-reasons.d.ts.map +1 -0
  102. package/dist/live-plan-reasons.js +47 -0
  103. package/dist/lsp/option-keys.d.ts.map +1 -1
  104. package/dist/lsp/option-keys.js +2 -0
  105. package/dist/manifest.json +1 -1
  106. package/dist/op/activities/index.d.ts +6 -2
  107. package/dist/op/activities/index.d.ts.map +1 -1
  108. package/dist/op/activities/index.js +5 -1
  109. package/dist/op/activities/live-detect.d.ts +4 -3
  110. package/dist/op/activities/live-detect.d.ts.map +1 -1
  111. package/dist/op/activities/live-detect.js +57 -32
  112. package/dist/op/activities/pin-rollout.d.ts +2 -0
  113. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  114. package/dist/op/activities/pin-rollout.js +1 -0
  115. package/dist/op/activities/terraform.d.ts +153 -5
  116. package/dist/op/activities/terraform.d.ts.map +1 -1
  117. package/dist/op/activities/terraform.js +205 -20
  118. package/dist/op/activity-contracts.d.ts +27 -0
  119. package/dist/op/activity-contracts.d.ts.map +1 -1
  120. package/dist/op/activity-contracts.js +19 -1
  121. package/dist/op/builders.d.ts +7 -1
  122. package/dist/op/builders.d.ts.map +1 -1
  123. package/dist/op/builders.js +9 -0
  124. package/dist/pin/edit.d.ts.map +1 -1
  125. package/dist/pin/edit.js +35 -0
  126. package/dist/pin/rollout.d.ts +2 -0
  127. package/dist/pin/rollout.d.ts.map +1 -1
  128. package/dist/pin/rollout.js +14 -11
  129. package/dist/plan-shape.d.ts +30 -0
  130. package/dist/plan-shape.d.ts.map +1 -0
  131. package/dist/plan-shape.js +125 -0
  132. package/dist/plugin.d.ts.map +1 -1
  133. package/dist/plugin.js +32 -18
  134. package/dist/report.d.ts +15 -6
  135. package/dist/report.d.ts.map +1 -1
  136. package/dist/report.js +28 -55
  137. package/dist/skills/chant-terraform.md +1 -1
  138. package/dist/state/azure-blob.d.ts +68 -0
  139. package/dist/state/azure-blob.d.ts.map +1 -0
  140. package/dist/state/azure-blob.js +201 -0
  141. package/dist/state/backend.d.ts +158 -0
  142. package/dist/state/backend.d.ts.map +1 -0
  143. package/dist/state/backend.js +406 -0
  144. package/dist/state/exec.d.ts +39 -0
  145. package/dist/state/exec.d.ts.map +1 -0
  146. package/dist/state/exec.js +80 -0
  147. package/dist/state/gcs.d.ts +62 -0
  148. package/dist/state/gcs.d.ts.map +1 -0
  149. package/dist/state/gcs.js +224 -0
  150. package/dist/state/index.d.ts +15 -0
  151. package/dist/state/index.d.ts.map +1 -0
  152. package/dist/state/index.js +14 -0
  153. package/dist/state/locks.d.ts +105 -0
  154. package/dist/state/locks.d.ts.map +1 -0
  155. package/dist/state/locks.js +130 -0
  156. package/dist/state/object-store.d.ts +37 -0
  157. package/dist/state/object-store.d.ts.map +1 -0
  158. package/dist/state/object-store.js +20 -0
  159. package/dist/state/root-states.d.ts +131 -0
  160. package/dist/state/root-states.d.ts.map +1 -0
  161. package/dist/state/root-states.js +241 -0
  162. package/dist/state/s3.d.ts +68 -0
  163. package/dist/state/s3.d.ts.map +1 -0
  164. package/dist/state/s3.js +170 -0
  165. package/dist/state/store.d.ts +51 -0
  166. package/dist/state/store.d.ts.map +1 -0
  167. package/dist/state/store.js +185 -0
  168. package/dist/terragrunt/affected.d.ts.map +1 -1
  169. package/dist/terragrunt/affected.js +2 -1
  170. package/package.json +7 -3
  171. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  172. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  173. package/src/__fixtures__/compose/state-app.json +66 -0
  174. package/src/__fixtures__/compose/state-edge.json +40 -0
  175. package/src/__fixtures__/compose/state-net.json +58 -0
  176. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  177. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  178. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  179. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  180. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  181. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  182. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  183. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  184. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  185. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  186. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  187. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  188. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  189. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  190. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  191. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  192. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  193. package/src/__fixtures__/drift-record/README.md +11 -0
  194. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  195. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  196. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  197. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  198. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  199. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  200. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  201. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  202. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  203. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  204. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  205. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  206. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  207. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  208. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  209. package/src/__fixtures__/pr-loop/README.md +50 -0
  210. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  211. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  212. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  213. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  214. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  215. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  216. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  217. package/src/__fixtures__/report/applied.show.json +51 -0
  218. package/src/__fixtures__/report/generate.ts +63 -0
  219. package/src/__fixtures__/report/plan.show.json +200 -0
  220. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  221. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  222. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  223. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  224. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  225. package/src/applied-state.ts +52 -0
  226. package/src/backend-config.test.ts +105 -0
  227. package/src/backend-config.ts +133 -0
  228. package/src/behaviour/kinds.ts +4 -0
  229. package/src/behaviour/predict.ts +5 -2
  230. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  231. package/src/behaviour/reference-catalog.ts +16 -5
  232. package/src/behaviour/request.ts +3 -2
  233. package/src/change-set-instances.test.ts +98 -0
  234. package/src/changed-roots.ts +14 -16
  235. package/src/commands.ts +4 -3
  236. package/src/components/capability-plugin.test.ts +172 -5
  237. package/src/components/capability-plugin.ts +212 -11
  238. package/src/components/change-set-graph.e2e.test.ts +11 -5
  239. package/src/components/compose-group.test.ts +486 -0
  240. package/src/components/compose-group.ts +489 -0
  241. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  242. package/src/components/member-outputs.test.ts +76 -0
  243. package/src/components/placeholder-preview.test.ts +147 -0
  244. package/src/components/placeholder-preview.ts +257 -0
  245. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  246. package/src/components/pr-loop.tofu.test.ts +112 -56
  247. package/src/compose/apply-events.test.ts +62 -0
  248. package/src/compose/apply-events.ts +100 -0
  249. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  250. package/src/compose/estate.test.ts +421 -0
  251. package/src/compose/estate.ts +443 -0
  252. package/src/compose/hcl.ts +311 -0
  253. package/src/compose/module-address.ts +49 -0
  254. package/src/compose/root.test.ts +416 -0
  255. package/src/compose/root.ts +592 -0
  256. package/src/compose/run.test.ts +440 -0
  257. package/src/compose/run.tofu.test.ts +176 -0
  258. package/src/compose/run.ts +527 -0
  259. package/src/compose/slice.test.ts +184 -0
  260. package/src/compose/slice.ts +290 -0
  261. package/src/compose/state-address.ts +157 -0
  262. package/src/compose/state-merge.test.ts +131 -0
  263. package/src/compose/state-merge.ts +148 -0
  264. package/src/compose.test.ts +248 -0
  265. package/src/compose.ts +627 -0
  266. package/src/composites/terraform-apply-op.test.ts +9 -0
  267. package/src/composites/terraform-apply-op.ts +7 -3
  268. package/src/config-file-listers.test.ts +265 -0
  269. package/src/config.ts +44 -9
  270. package/src/describe-resources.engine-facts.test.ts +412 -0
  271. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  272. package/src/describe-resources.ts +416 -70
  273. package/src/drift-read.ts +284 -0
  274. package/src/drift-record.test.ts +223 -0
  275. package/src/drift-record.ts +455 -0
  276. package/src/hcl/config-blocks.test.ts +174 -0
  277. package/src/hcl/edges.ts +21 -1
  278. package/src/hcl/files.ts +234 -0
  279. package/src/hcl/instance-forms.test.ts +94 -0
  280. package/src/hcl/instances.test.ts +137 -0
  281. package/src/hcl/instances.ts +422 -0
  282. package/src/hcl/parse.test.ts +139 -1
  283. package/src/hcl/parse.ts +149 -16
  284. package/src/hcl/roots.ts +6 -0
  285. package/src/hcl/suppressions.ts +8 -2
  286. package/src/index.ts +76 -0
  287. package/src/live-plan-reasons.ts +54 -0
  288. package/src/lsp/completions.test.ts +1 -1
  289. package/src/lsp/option-keys.ts +2 -0
  290. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  291. package/src/op/activities/index.ts +9 -0
  292. package/src/op/activities/live-detect.ts +55 -26
  293. package/src/op/activities/pin-rollout.ts +3 -0
  294. package/src/op/activities/registry.test.ts +2 -1
  295. package/src/op/activities/terraform.test.ts +55 -0
  296. package/src/op/activities/terraform.ts +266 -22
  297. package/src/op/activity-contracts.test.ts +1 -0
  298. package/src/op/activity-contracts.ts +24 -1
  299. package/src/op/builders.ts +14 -0
  300. package/src/pin/bundle.test.ts +2 -0
  301. package/src/pin/edit.ts +30 -0
  302. package/src/pin/rollout.ts +16 -11
  303. package/src/plan-shape.test.ts +148 -0
  304. package/src/plan-shape.ts +128 -0
  305. package/src/plugin.ts +33 -19
  306. package/src/report.test.ts +82 -1
  307. package/src/report.ts +31 -54
  308. package/src/signal-only-root.test.ts +72 -0
  309. package/src/skills/chant-terraform.md +1 -1
  310. package/src/state/azure-blob.ts +223 -0
  311. package/src/state/backend-verdict.test.ts +187 -0
  312. package/src/state/backend.test.ts +127 -0
  313. package/src/state/backend.ts +488 -0
  314. package/src/state/exec.ts +111 -0
  315. package/src/state/gcs.ts +254 -0
  316. package/src/state/index.ts +15 -0
  317. package/src/state/locks.test.ts +84 -0
  318. package/src/state/locks.ts +201 -0
  319. package/src/state/object-store.ts +35 -0
  320. package/src/state/root-states-floci.e2e.test.ts +66 -0
  321. package/src/state/root-states.test.ts +462 -0
  322. package/src/state/root-states.ts +309 -0
  323. package/src/state/s3.ts +193 -0
  324. package/src/state/store.ts +225 -0
  325. package/src/terragrunt/affected.ts +2 -1
  326. package/src/workspace-kinds.test.ts +4 -17
  327. package/workspace-kinds.json +1 -24
  328. package/dist/workspace-member.d.ts +0 -30
  329. package/dist/workspace-member.d.ts.map +0 -1
  330. package/dist/workspace-member.js +0 -91
  331. package/schemas/pulumi.schema.json +0 -34
  332. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  333. package/src/testdata/pulumi-member/stack-export.json +0 -53
  334. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  335. package/src/workspace-member.test.ts +0 -89
  336. package/src/workspace-member.ts +0 -126
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Which files of a directory are a root's configuration, and how a JSON-syntax
3
+ * one reads (chant#4457). Every lister in the lexicon asks here, so the parser,
4
+ * change detection, the backend readers, the report and the rollout pin all
5
+ * see the same file set for one root.
6
+ *
7
+ * The set follows the root's binary. Terraform reads `.tf` and `.tf.json`.
8
+ * OpenTofu (and choudoufu, which is OpenTofu) also reads `.tofu` and
9
+ * `.tofu.json`, and when `x.tofu` and `x.tf` both exist it reads `x.tofu`
10
+ * only (likewise `x.tofu.json` over `x.tf.json`). A binary that is not given
11
+ * is `terraform`, the config's default.
12
+ *
13
+ * `parse.ts` re-exports all of this; the module is separate so the readers
14
+ * that never parse HCL (`state/backend.ts`, `changed-roots.ts`) do not load
15
+ * the parser's dependencies.
16
+ */
17
+
18
+ import { readdirSync, statSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ /** The configuration file suffixes Terraform reads. */
22
+ export const TERRAFORM_SUFFIXES: readonly string[] = [".tf", ".tf.json"];
23
+ /** The ones OpenTofu reads as well. */
24
+ export const TOFU_SUFFIXES: readonly string[] = [".tofu", ".tofu.json"];
25
+
26
+ /** Whether a root run by `binary` reads `.tofu` files: `tofu` and `choudoufu` do (by name or path), `terraform` and an unset binary do not. */
27
+ export function readsTofuFiles(binary?: string): boolean {
28
+ const base = (binary ?? "terraform").split(/[\\/]/).pop() ?? "";
29
+ return base.startsWith("tofu") || base.startsWith("choudoufu");
30
+ }
31
+
32
+ /** Whether `name` is a configuration file in Terraform's JSON syntax (`.tf.json`, `.tofu.json`). */
33
+ export function isJsonConfigFile(name: string): boolean {
34
+ return name.endsWith(".tf.json") || name.endsWith(".tofu.json");
35
+ }
36
+
37
+ /** Whether `name` is a `.tofu` or `.tofu.json` file. */
38
+ export function isTofuConfigFile(name: string): boolean {
39
+ return TOFU_SUFFIXES.some((s) => name.endsWith(s));
40
+ }
41
+
42
+ /** Whether `name` is a configuration file a root run by `binary` reads, before the `.tofu`-over-`.tf` rule. */
43
+ export function isConfigFileFor(name: string, binary?: string): boolean {
44
+ if (TERRAFORM_SUFFIXES.some((s) => name.endsWith(s))) return true;
45
+ return readsTofuFiles(binary) && isTofuConfigFile(name);
46
+ }
47
+
48
+ /**
49
+ * The configuration files among `names` a root run by `binary` reads, in
50
+ * filename order. For a tofu or choudoufu root, `x.tf` is dropped when
51
+ * `x.tofu` is present and `x.tf.json` when `x.tofu.json` is, which is
52
+ * OpenTofu's own rule. Names may carry a directory; the rule compares names
53
+ * in the same directory.
54
+ */
55
+ export function selectConfigFiles(names: Iterable<string>, binary?: string): string[] {
56
+ const kept = [...names].filter((n) => isConfigFileFor(n, binary));
57
+ const present = new Set(kept);
58
+ return kept
59
+ .filter((n) => {
60
+ if (n.endsWith(".tf")) return !present.has(`${n.slice(0, -".tf".length)}.tofu`);
61
+ if (n.endsWith(".tf.json")) return !present.has(`${n.slice(0, -".tf.json".length)}.tofu.json`);
62
+ return true;
63
+ })
64
+ .sort();
65
+ }
66
+
67
+ /**
68
+ * The configuration files directly under `dir` a root run by `binary` reads,
69
+ * by name, in filename order. Non-recursive, matching Terraform's module
70
+ * scoping; directories named like a file are left out. Throws when `dir`
71
+ * cannot be read.
72
+ */
73
+ export function listConfigFiles(dir: string, binary?: string): string[] {
74
+ const files = readdirSync(dir).filter((n) => {
75
+ if (!isConfigFileFor(n, binary)) return false;
76
+ try {
77
+ return statSync(join(dir, n)).isFile();
78
+ } catch {
79
+ return false;
80
+ }
81
+ });
82
+ return selectConfigFiles(files, binary);
83
+ }
84
+
85
+ /** {@link listConfigFiles}, empty when `dir` cannot be read. */
86
+ export function listConfigFilesSafe(dir: string, binary?: string): string[] {
87
+ try {
88
+ return listConfigFiles(dir, binary);
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
94
+ /**
95
+ * The nested blocks a JSON-syntax body can carry that hcl2json would give as
96
+ * arrays, by block type: each entry is the nested block's label count and its
97
+ * own nested blocks. Anything not named here is an attribute and is kept as
98
+ * written, since JSON syntax cannot tell a nested block from an object
99
+ * attribute without the provider's schema.
100
+ */
101
+ interface BlockShape {
102
+ labels: number;
103
+ nested?: Record<string, BlockShape>;
104
+ /**
105
+ * Attributes JSON syntax writes as a bare expression in a string (a type,
106
+ * a reference), where HCL writes the expression itself and hcl2json gives
107
+ * it as `"${...}"`. Their strings are wrapped the same way.
108
+ */
109
+ exprs?: readonly string[];
110
+ }
111
+
112
+ const LIFECYCLE: BlockShape = { labels: 0, exprs: ["ignore_changes", "replace_triggered_by"] };
113
+ const RESOURCE_NESTED: Record<string, BlockShape> = {
114
+ lifecycle: LIFECYCLE,
115
+ provisioner: { labels: 1, nested: { connection: { labels: 0 } } },
116
+ connection: { labels: 0 },
117
+ };
118
+ const REFERS = ["provider", "depends_on"];
119
+
120
+ const TOP_LEVEL: Record<string, BlockShape> = {
121
+ terraform: {
122
+ labels: 0,
123
+ nested: {
124
+ backend: { labels: 1 },
125
+ cloud: { labels: 0, nested: { workspaces: { labels: 0 } } },
126
+ required_providers: { labels: 0 },
127
+ provider_meta: { labels: 1 },
128
+ live: { labels: 0 },
129
+ },
130
+ },
131
+ locals: { labels: 0 },
132
+ provider: { labels: 1 },
133
+ module: { labels: 1, exprs: ["depends_on", "providers"] },
134
+ variable: { labels: 1, nested: { validation: { labels: 0 } }, exprs: ["type"] },
135
+ output: { labels: 1, nested: { precondition: { labels: 0 } }, exprs: ["depends_on"] },
136
+ resource: { labels: 2, nested: RESOURCE_NESTED, exprs: REFERS },
137
+ data: { labels: 2, nested: { lifecycle: LIFECYCLE }, exprs: REFERS },
138
+ moved: { labels: 0, exprs: ["from", "to"] },
139
+ import: { labels: 0, exprs: ["to", "provider"] },
140
+ removed: { labels: 0, nested: { lifecycle: LIFECYCLE, provisioner: RESOURCE_NESTED.provisioner! }, exprs: ["from"] },
141
+ check: { labels: 1, nested: { data: { labels: 2 }, assert: { labels: 0 } } },
142
+ };
143
+
144
+ const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
145
+
146
+ /** A bare expression string as hcl2json writes the expression: `aws.west` becomes `${aws.west}`; a template is kept. */
147
+ function asExpression(v: unknown): unknown {
148
+ if (typeof v === "string") return v.includes("${") ? v : `\${${v}}`;
149
+ if (Array.isArray(v)) return v.map(asExpression);
150
+ if (isObject(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, asExpression(x)]));
151
+ return v;
152
+ }
153
+
154
+ /** A JSON body with its `"//"` comment keys dropped (Terraform ignores them), its known nested blocks wrapped and its expression attributes written as hcl2json writes them. */
155
+ function normaliseBody(body: unknown, shape: BlockShape): Record<string, unknown> {
156
+ const out: Record<string, unknown> = {};
157
+ if (!isObject(body)) return out;
158
+ for (const [k, v] of Object.entries(body)) {
159
+ if (k === "//") continue;
160
+ const inner = shape.nested?.[k];
161
+ out[k] = inner ? wrapBlock(v, inner) : shape.exprs?.includes(k) ? asExpression(v) : v;
162
+ }
163
+ return out;
164
+ }
165
+
166
+ /**
167
+ * A block in JSON syntax as hcl2json gives the same block in HCL: one object
168
+ * level per label, then an array of bodies. JSON syntax writes a single body
169
+ * as an object or several as an array, and allows an array of objects at a
170
+ * label level to be merged, so both are read.
171
+ */
172
+ function wrapBlock(value: unknown, shape: BlockShape, labels = shape.labels): unknown {
173
+ if (labels === 0) return (Array.isArray(value) ? value : [value]).filter(isObject).map((b) => normaliseBody(b, shape));
174
+ const out: Record<string, unknown> = {};
175
+ for (const part of Array.isArray(value) ? value : [value]) {
176
+ if (!isObject(part)) continue;
177
+ for (const [label, inner] of Object.entries(part)) {
178
+ if (label === "//") continue;
179
+ const wrapped = wrapBlock(inner, shape, labels - 1);
180
+ const had = out[label];
181
+ if (Array.isArray(had) && Array.isArray(wrapped)) out[label] = [...had, ...wrapped];
182
+ else if (isObject(had) && isObject(wrapped)) out[label] = mergeLabelled(had, wrapped);
183
+ else out[label] = wrapped;
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function mergeLabelled(a: Record<string, unknown>, b: Record<string, unknown>): Record<string, unknown> {
190
+ const out: Record<string, unknown> = { ...a };
191
+ for (const [k, v] of Object.entries(b)) {
192
+ const had = out[k];
193
+ if (Array.isArray(had) && Array.isArray(v)) out[k] = [...had, ...v];
194
+ else if (isObject(had) && isObject(v)) out[k] = mergeLabelled(had, v);
195
+ else out[k] = v;
196
+ }
197
+ return out;
198
+ }
199
+
200
+ /**
201
+ * A `.tf.json` or `.tofu.json` document in the tree shape hcl2json gives an
202
+ * HCL file, so `blocksToEntities` reads both the same way. Top-level keys it
203
+ * does not know are kept as written. Throws on malformed JSON, as hcl2json
204
+ * throws on malformed HCL.
205
+ */
206
+ export function jsonConfigTree(source: string): Record<string, unknown> {
207
+ const doc: unknown = JSON.parse(source);
208
+ if (!isObject(doc)) throw new Error("a JSON configuration file must hold an object");
209
+ const out: Record<string, unknown> = {};
210
+ for (const [k, v] of Object.entries(doc)) {
211
+ if (k === "//") continue;
212
+ const shape = TOP_LEVEL[k];
213
+ out[k] = shape ? wrapBlock(v, shape) : v;
214
+ }
215
+ return out;
216
+ }
217
+
218
+ /**
219
+ * The bodies of the `live` blocks nested in the `terraform` blocks of a
220
+ * `.tf.json` or `.tofu.json` document, in the order written (chant#4504).
221
+ * The fast live readers (`report.ts`'s `estateOfDir`, `op/activities/live-detect.ts`)
222
+ * read HCL by text and a JSON file through this. Malformed JSON reads as no
223
+ * `live` block, as those readers treat any file they cannot read.
224
+ */
225
+ export function jsonLiveBlocks(source: string): Record<string, unknown>[] {
226
+ let tree: Record<string, unknown>;
227
+ try {
228
+ tree = jsonConfigTree(source);
229
+ } catch {
230
+ return [];
231
+ }
232
+ const terraform = Array.isArray(tree.terraform) ? tree.terraform : [];
233
+ return terraform.flatMap((t) => (isObject(t) && Array.isArray(t.live) ? t.live.filter(isObject) : []));
234
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The expansion forms #4489 counts, and the rollup reason on every block the
3
+ * declared graph still leaves as one node. The fixture is
4
+ * `src/__fixtures__/instance-forms/app/`, built as `chant graph --format ir`
5
+ * builds a terraform root's graph.
6
+ */
7
+
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { beforeAll, describe, expect, it } from "vitest";
11
+ import { buildGraphIr, withInstanceNodes, type GraphIR, type IRNode } from "@intentius/chant/graph-ir";
12
+ import { INSTANCE_ROLLUP_CODES } from "@intentius/chant/instance-nodes";
13
+ import type { Declarable } from "@intentius/chant/declarable";
14
+ import { renderTerraformRoots } from "./roots";
15
+
16
+ const TREE = join(dirname(fileURLToPath(import.meta.url)), "..", "__fixtures__", "instance-forms");
17
+
18
+ let entities: Map<string, Declarable>;
19
+ let ir: GraphIR;
20
+
21
+ beforeAll(async () => {
22
+ const rendered = await renderTerraformRoots({ projectRoot: TREE, roots: { estate: { dir: "./app" } } });
23
+ entities = rendered.entities;
24
+ ir = withInstanceNodes(buildGraphIr(entities), entities);
25
+ });
26
+
27
+ function instancesOf(block: string): string[] {
28
+ return ir.nodes.filter((n) => n.instanceOf === block).map((n) => n.id).sort();
29
+ }
30
+
31
+ function node(id: string): IRNode {
32
+ const n = ir.nodes.find((x) => x.id === id);
33
+ if (!n) throw new Error(`no node ${id}`);
34
+ return n;
35
+ }
36
+
37
+ describe("forms counted since #4489", () => {
38
+ it("counts `count = local.x` over a literal local", () => {
39
+ expect(instancesOf("estate/aws_eip.local_count")).toEqual(["estate/aws_eip.local_count[0]", "estate/aws_eip.local_count[1]"]);
40
+ expect(node("estate/aws_eip.local_count").instanceRollup).toBeUndefined();
41
+ });
42
+
43
+ it("counts `count = length(local.x)`", () => {
44
+ expect(instancesOf("estate/aws_sqs_queue.local_length")).toHaveLength(3);
45
+ });
46
+
47
+ it("keys `for_each = toset(local.l)` and `for_each = local.m`", () => {
48
+ expect(instancesOf("estate/aws_s3_bucket.local_set")).toEqual(['estate/aws_s3_bucket.local_set["p"]', 'estate/aws_s3_bucket.local_set["q"]']);
49
+ expect(instancesOf("estate/aws_lb_listener.local_map")).toEqual(['estate/aws_lb_listener.local_map["http"]', 'estate/aws_lb_listener.local_map["https"]']);
50
+ });
51
+
52
+ it("counts `var.on ? 1 : 0` over a known bool, and a local that reads one", () => {
53
+ expect(instancesOf("estate/aws_sns_topic.enabled")).toEqual(["estate/aws_sns_topic.enabled[0]"]);
54
+ expect(instancesOf("estate/aws_sns_topic.disabled")).toEqual([]);
55
+ expect(node("estate/aws_sns_topic.disabled").instanceRollup).toBeUndefined();
56
+ expect(instancesOf("estate/aws_sns_topic.local_flag")).toHaveLength(2);
57
+ });
58
+ });
59
+
60
+ describe("forms still rolled up, with the node's reason", () => {
61
+ it.each([
62
+ ["estate/aws_sns_topic.flag_unknown", "instances-value-unknown", "count", "var.flag ? 1 : 0"],
63
+ ["estate/aws_sns_topic.local_unknown", "instances-value-unknown", "count", "local.computed"],
64
+ ["estate/aws_iam_group.each", "instances-each-value", "for_each", "each.value"],
65
+ ["estate/aws_iam_policy.arith", "instances-form-unread", "count", "local.replicas + 1"],
66
+ ["estate/aws_eip.too_many", "instances-over-limit", "count", "300"],
67
+ ["estate/module.svc", "instances-value-unknown", "count", "var.unknown"],
68
+ ])("%s is one node with %s", (id, code, argument, expression) => {
69
+ expect(instancesOf(id)).toEqual([]);
70
+ expect(node(id).instanceRollup).toEqual({ code, argument, expression });
71
+ });
72
+
73
+ it("names merge() and a for expression", () => {
74
+ expect(node("estate/aws_iam_user.merged").instanceRollup).toMatchObject({ code: "instances-merge", argument: "for_each" });
75
+ expect(node("estate/aws_iam_user.merged").instanceRollup?.expression).toMatch(/^merge\(/);
76
+ expect(node("estate/aws_iam_role.for_expr").instanceRollup).toMatchObject({ code: "instances-form-unread", argument: "for_each" });
77
+ });
78
+
79
+ it("names the uncounted module call on a block under it", () => {
80
+ expect(instancesOf("estate/module.svc/aws_iam_role.runner")).toEqual([]);
81
+ expect(node("estate/module.svc/aws_iam_role.runner").instanceRollup).toEqual({
82
+ code: "instances-module-uncounted",
83
+ argument: "count",
84
+ expression: "var.unknown",
85
+ call: "module.svc",
86
+ });
87
+ });
88
+
89
+ it("uses only the listed codes", () => {
90
+ for (const n of ir.nodes) {
91
+ if (n.instanceRollup) expect(INSTANCE_ROLLUP_CODES).toContain(n.instanceRollup.code);
92
+ }
93
+ });
94
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Instance nodes counted from variables, and under expanded module calls
3
+ * (chant #4486).
4
+ *
5
+ * The fixture is `src/__fixtures__/instance-vars/app/`, rendered through
6
+ * `renderTerraformRoots`, then core's `buildGraphIr` and `withInstanceNodes`,
7
+ * which is how `chant graph --format ir` builds a terraform root's graph.
8
+ */
9
+
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { beforeAll, describe, expect, it } from "vitest";
13
+ import { buildGraphIr, withInstanceNodes, type GraphIR } from "@intentius/chant/graph-ir";
14
+ import { graphNodeFor } from "@intentius/chant/change-set-graph";
15
+ import { terraformInstancePath } from "@intentius/chant/instance-nodes";
16
+ import type { Declarable } from "@intentius/chant/declarable";
17
+ import { renderTerraformRoots } from "./roots";
18
+
19
+ const TREE = join(dirname(fileURLToPath(import.meta.url)), "..", "__fixtures__", "instance-vars");
20
+
21
+ let entities: Map<string, Declarable>;
22
+ let ir: GraphIR;
23
+
24
+ beforeAll(async () => {
25
+ const rendered = await renderTerraformRoots({ projectRoot: TREE, roots: { estate: { dir: "./app" } } });
26
+ expect(rendered.warnings).toEqual([]);
27
+ entities = rendered.entities;
28
+ ir = withInstanceNodes(buildGraphIr(entities), entities);
29
+ });
30
+
31
+ /** The instance nodes under one block node, as `[id, instanceKey, instanceScope]`. */
32
+ function instancesOf(block: string): Array<[string, string | null, string | null]> {
33
+ return ir.nodes
34
+ .filter((n) => n.instanceOf === block)
35
+ .map((n) => [n.id, n.instanceKey ?? null, n.instanceScope ?? null] as [string, string | null, string | null])
36
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
37
+ }
38
+
39
+ describe("count and for_each resolved from variables", () => {
40
+ it("counts `count = var.n` from terraform.tfvars over the default", () => {
41
+ expect(instancesOf("estate/aws_eip.pool")).toEqual([
42
+ ["estate/aws_eip.pool[0]", "[0]", null],
43
+ ["estate/aws_eip.pool[1]", "[1]", null],
44
+ ["estate/aws_eip.pool[2]", "[2]", null],
45
+ ]);
46
+ });
47
+
48
+ it("keys `for_each = var.names` over a set variable's default", () => {
49
+ expect(instancesOf("estate/aws_s3_bucket.named").map((i) => i[0])).toEqual([
50
+ 'estate/aws_s3_bucket.named["a"]',
51
+ 'estate/aws_s3_bucket.named["b"]',
52
+ ]);
53
+ });
54
+
55
+ it("counts `count = length(var.zones)`", () => {
56
+ expect(instancesOf("estate/aws_sqs_queue.zoned").map((i) => i[0])).toEqual([
57
+ "estate/aws_sqs_queue.zoned[0]",
58
+ "estate/aws_sqs_queue.zoned[1]",
59
+ ]);
60
+ });
61
+
62
+ it("leaves a block whose variable has no known value as one node", () => {
63
+ expect(instancesOf("estate/aws_sns_topic.uncounted")).toEqual([]);
64
+ expect(ir.nodes.some((n) => n.id === "estate/aws_sns_topic.uncounted")).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe("instances under an expanded module call", () => {
69
+ it("gives the module call its own instance nodes", () => {
70
+ expect(instancesOf("estate/module.app")).toEqual([
71
+ ["estate/module.app[0]", "[0]", null],
72
+ ["estate/module.app[1]", "[1]", null],
73
+ ]);
74
+ });
75
+
76
+ it("puts the module key on the call segment and the block key at the end", () => {
77
+ // `size = var.n` passes the root's 3 into the child's `count = var.size`.
78
+ expect(instancesOf("estate/module.app/aws_instance.web")).toEqual([
79
+ ["estate/module.app[0]/aws_instance.web[0]", "[0]", "module.app[0]"],
80
+ ["estate/module.app[0]/aws_instance.web[1]", "[1]", "module.app[0]"],
81
+ ["estate/module.app[0]/aws_instance.web[2]", "[2]", "module.app[0]"],
82
+ ["estate/module.app[1]/aws_instance.web[0]", "[0]", "module.app[1]"],
83
+ ["estate/module.app[1]/aws_instance.web[1]", "[1]", "module.app[1]"],
84
+ ["estate/module.app[1]/aws_instance.web[2]", "[2]", "module.app[1]"],
85
+ ]);
86
+ });
87
+
88
+ it("gives a block that is not expanded itself one instance per module instance, with no instanceKey", () => {
89
+ expect(instancesOf("estate/module.app/aws_iam_role.runner")).toEqual([
90
+ ["estate/module.app[0]/aws_iam_role.runner", null, "module.app[0]"],
91
+ ["estate/module.app[1]/aws_iam_role.runner", null, "module.app[1]"],
92
+ ]);
93
+ expect(instancesOf("estate/module.edge/aws_route53_record.front")).toEqual([
94
+ ['estate/module.edge["east"]/aws_route53_record.front', null, 'module.edge["east"]'],
95
+ ['estate/module.edge["west"]/aws_route53_record.front', null, 'module.edge["west"]'],
96
+ ]);
97
+ });
98
+
99
+ it("keeps the block node as the rollup, in the same groups", () => {
100
+ expect(ir.nodes.some((n) => n.id === "estate/module.app/aws_instance.web" && !n.instanceOf)).toBe(true);
101
+ expect(ir.groups.byStack?.estate).toEqual(
102
+ expect.arrayContaining(["estate/module.app/aws_instance.web", "estate/module.app[1]/aws_instance.web[2]"]),
103
+ );
104
+ });
105
+
106
+ it("places a plan address on the same id the graph lists", () => {
107
+ const graph = { member: null, at: null, ids: ir.nodes.map((n) => n.id) };
108
+ expect(graphNodeFor({ member: "estate", planner: "terraform", address: "module.app[1].aws_instance.web[2]" }, graph)).toEqual({
109
+ node: "estate/module.app[1]/aws_instance.web[2]",
110
+ instanceOf: "estate/module.app/aws_instance.web",
111
+ });
112
+ expect(graphNodeFor({ member: "estate", planner: "tofu", address: 'module.edge["east"].aws_route53_record.front' }, graph)).toEqual({
113
+ node: 'estate/module.edge["east"]/aws_route53_record.front',
114
+ instanceOf: "estate/module.edge/aws_route53_record.front",
115
+ });
116
+ for (const n of ir.nodes.filter((x) => x.instanceOf)) expect(graph.ids).toContain(n.id);
117
+ });
118
+ });
119
+
120
+ describe("terraformInstancePath", () => {
121
+ it("maps an address to its block key and instance key", () => {
122
+ expect(terraformInstancePath("aws_eip.pool[1]")).toEqual({ block: "aws_eip.pool", instance: "aws_eip.pool[1]", key: "[1]" });
123
+ expect(terraformInstancePath("aws_eip.pool")).toBeUndefined();
124
+ expect(terraformInstancePath("module.app[0].aws_eip.pool[1]")).toEqual({
125
+ block: "module.app/aws_eip.pool",
126
+ instance: "module.app[0]/aws_eip.pool[1]",
127
+ key: "[1]",
128
+ scope: "module.app[0]",
129
+ });
130
+ expect(terraformInstancePath('module.a["x.y"].module.b.data.http.site')).toEqual({
131
+ block: "module.a/module.b/data.http.site",
132
+ instance: 'module.a["x.y"]/module.b/data.http.site',
133
+ scope: 'module.a["x.y"].module.b',
134
+ });
135
+ expect(terraformInstancePath("module.app[0]")).toEqual({ block: "module.app", instance: "module.app[0]", key: "[0]" });
136
+ });
137
+ });