@intentius/chant-lexicon-fly 0.16.0

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 (149) hide show
  1. package/README.md +91 -0
  2. package/dist/codegen/docs-cli.d.ts +3 -0
  3. package/dist/codegen/docs-cli.d.ts.map +1 -0
  4. package/dist/codegen/docs.d.ts +27 -0
  5. package/dist/codegen/docs.d.ts.map +1 -0
  6. package/dist/codegen/generate-cli.d.ts +6 -0
  7. package/dist/codegen/generate-cli.d.ts.map +1 -0
  8. package/dist/codegen/generate-lexicon.d.ts +18 -0
  9. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  10. package/dist/codegen/generate-typescript.d.ts +11 -0
  11. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  12. package/dist/codegen/generate.d.ts +15 -0
  13. package/dist/codegen/generate.d.ts.map +1 -0
  14. package/dist/codegen/naming.d.ts +14 -0
  15. package/dist/codegen/naming.d.ts.map +1 -0
  16. package/dist/codegen/package.d.ts +17 -0
  17. package/dist/codegen/package.d.ts.map +1 -0
  18. package/dist/composites/fly-deploy.d.ts +110 -0
  19. package/dist/composites/fly-deploy.d.ts.map +1 -0
  20. package/dist/describe-resources.d.ts +74 -0
  21. package/dist/describe-resources.d.ts.map +1 -0
  22. package/dist/detect.d.ts +22 -0
  23. package/dist/detect.d.ts.map +1 -0
  24. package/dist/export-resources.d.ts +25 -0
  25. package/dist/export-resources.d.ts.map +1 -0
  26. package/dist/generated/index.d.ts +42 -0
  27. package/dist/generated/index.d.ts.map +1 -0
  28. package/dist/generated/runtime.d.ts +5 -0
  29. package/dist/generated/runtime.d.ts.map +1 -0
  30. package/dist/import/generator.d.ts +21 -0
  31. package/dist/import/generator.d.ts.map +1 -0
  32. package/dist/import/live-export.d.ts +42 -0
  33. package/dist/import/live-export.d.ts.map +1 -0
  34. package/dist/import/parser.d.ts +40 -0
  35. package/dist/import/parser.d.ts.map +1 -0
  36. package/dist/index.d.ts +8 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/integrity.json +9 -0
  39. package/dist/lint/post-synth/fly-helpers.d.ts +15 -0
  40. package/dist/lint/post-synth/fly-helpers.d.ts.map +1 -0
  41. package/dist/lint/post-synth/fly010-machine-requires-image.d.ts +10 -0
  42. package/dist/lint/post-synth/fly010-machine-requires-image.d.ts.map +1 -0
  43. package/dist/lint/post-synth/fly011-mount-references-declared-volume.d.ts +12 -0
  44. package/dist/lint/post-synth/fly011-mount-references-declared-volume.d.ts.map +1 -0
  45. package/dist/lint/post-synth/index.d.ts +3 -0
  46. package/dist/lint/post-synth/index.d.ts.map +1 -0
  47. package/dist/lint/rules/guest-sizing.d.ts +9 -0
  48. package/dist/lint/rules/guest-sizing.d.ts.map +1 -0
  49. package/dist/lint/rules/index.d.ts +7 -0
  50. package/dist/lint/rules/index.d.ts.map +1 -0
  51. package/dist/lint/rules/no-secret-literals.d.ts +13 -0
  52. package/dist/lint/rules/no-secret-literals.d.ts.map +1 -0
  53. package/dist/lint/rules/valid-region.d.ts +9 -0
  54. package/dist/lint/rules/valid-region.d.ts.map +1 -0
  55. package/dist/lsp/completions.d.ts +9 -0
  56. package/dist/lsp/completions.d.ts.map +1 -0
  57. package/dist/lsp/hover.d.ts +9 -0
  58. package/dist/lsp/hover.d.ts.map +1 -0
  59. package/dist/manifest.json +4 -0
  60. package/dist/meta.json +285 -0
  61. package/dist/op/activities/flaps.d.ts +29 -0
  62. package/dist/op/activities/flaps.d.ts.map +1 -0
  63. package/dist/op/activities/fly-apply.d.ts +382 -0
  64. package/dist/op/activities/fly-apply.d.ts.map +1 -0
  65. package/dist/op/activities/index.d.ts +12 -0
  66. package/dist/op/activities/index.d.ts.map +1 -0
  67. package/dist/ownership.d.ts +27 -0
  68. package/dist/ownership.d.ts.map +1 -0
  69. package/dist/package-cli.d.ts +3 -0
  70. package/dist/package-cli.d.ts.map +1 -0
  71. package/dist/plugin.d.ts +8 -0
  72. package/dist/plugin.d.ts.map +1 -0
  73. package/dist/pseudo.d.ts +23 -0
  74. package/dist/pseudo.d.ts.map +1 -0
  75. package/dist/serializer.d.ts +36 -0
  76. package/dist/serializer.d.ts.map +1 -0
  77. package/dist/spec/fetch.d.ts +12 -0
  78. package/dist/spec/fetch.d.ts.map +1 -0
  79. package/dist/spec/parse.d.ts +58 -0
  80. package/dist/spec/parse.d.ts.map +1 -0
  81. package/dist/types/index.d.ts +590 -0
  82. package/dist/validate-cli.d.ts +3 -0
  83. package/dist/validate-cli.d.ts.map +1 -0
  84. package/dist/validate.d.ts +15 -0
  85. package/dist/validate.d.ts.map +1 -0
  86. package/package.json +76 -0
  87. package/src/codegen/docs-cli.ts +7 -0
  88. package/src/codegen/docs.ts +208 -0
  89. package/src/codegen/generate-cli.ts +36 -0
  90. package/src/codegen/generate-lexicon.ts +52 -0
  91. package/src/codegen/generate-typescript.ts +69 -0
  92. package/src/codegen/generate.ts +106 -0
  93. package/src/codegen/naming.ts +45 -0
  94. package/src/codegen/package.ts +25 -0
  95. package/src/composites/fly-deploy.ts +169 -0
  96. package/src/describe-resources.test.ts +188 -0
  97. package/src/describe-resources.ts +0 -0
  98. package/src/detect.ts +64 -0
  99. package/src/export-resources.ts +123 -0
  100. package/src/generated/.gitkeep +0 -0
  101. package/src/generated/index.d.ts +590 -0
  102. package/src/generated/index.ts +47 -0
  103. package/src/generated/lexicon-fly.json +285 -0
  104. package/src/generated/runtime.ts +4 -0
  105. package/src/import/generator.test.ts +123 -0
  106. package/src/import/generator.ts +242 -0
  107. package/src/import/live-export.test.ts +150 -0
  108. package/src/import/live-export.ts +107 -0
  109. package/src/import/parser.test.ts +132 -0
  110. package/src/import/parser.ts +185 -0
  111. package/src/import/roundtrip.test.ts +77 -0
  112. package/src/index.ts +23 -0
  113. package/src/lint/post-synth/fly-helpers.ts +25 -0
  114. package/src/lint/post-synth/fly010-machine-requires-image.test.ts +59 -0
  115. package/src/lint/post-synth/fly010-machine-requires-image.ts +39 -0
  116. package/src/lint/post-synth/fly011-mount-references-declared-volume.test.ts +92 -0
  117. package/src/lint/post-synth/fly011-mount-references-declared-volume.ts +61 -0
  118. package/src/lint/post-synth/index.ts +9 -0
  119. package/src/lint/rules/guest-sizing.ts +100 -0
  120. package/src/lint/rules/index.ts +15 -0
  121. package/src/lint/rules/no-secret-literals.ts +67 -0
  122. package/src/lint/rules/rules.test.ts +127 -0
  123. package/src/lint/rules/valid-region.ts +58 -0
  124. package/src/lsp/completions.test.ts +10 -0
  125. package/src/lsp/completions.ts +14 -0
  126. package/src/lsp/hover.test.ts +10 -0
  127. package/src/lsp/hover.ts +14 -0
  128. package/src/op/activities/describe-resources.integration.test.ts +105 -0
  129. package/src/op/activities/flaps.test.ts +29 -0
  130. package/src/op/activities/flaps.ts +44 -0
  131. package/src/op/activities/fly-apply.integration.test.ts +245 -0
  132. package/src/op/activities/fly-apply.test.ts +513 -0
  133. package/src/op/activities/fly-apply.ts +1014 -0
  134. package/src/op/activities/index.ts +48 -0
  135. package/src/ownership.ts +32 -0
  136. package/src/package-cli.ts +24 -0
  137. package/src/plugin.test.ts +17 -0
  138. package/src/plugin.ts +285 -0
  139. package/src/pseudo.test.ts +32 -0
  140. package/src/pseudo.ts +29 -0
  141. package/src/serializer.test.ts +232 -0
  142. package/src/serializer.ts +293 -0
  143. package/src/skills/chant-fly-ops.md +55 -0
  144. package/src/skills/chant-fly-patterns.md +71 -0
  145. package/src/skills/chant-fly.md +123 -0
  146. package/src/spec/fetch.ts +34 -0
  147. package/src/spec/parse.ts +323 -0
  148. package/src/validate-cli.ts +5 -0
  149. package/src/validate.ts +28 -0
@@ -0,0 +1,285 @@
1
+ {
2
+ "App": {
3
+ "resourceType": "Fly::Machines::App",
4
+ "kind": "resource",
5
+ "lexicon": "fly"
6
+ },
7
+ "Certificate": {
8
+ "resourceType": "Fly::Machines::Certificate",
9
+ "kind": "resource",
10
+ "lexicon": "fly"
11
+ },
12
+ "ContainerConfig": {
13
+ "resourceType": "Fly::Machines::ContainerConfig",
14
+ "kind": "property",
15
+ "lexicon": "fly"
16
+ },
17
+ "ContainerDependency": {
18
+ "resourceType": "Fly::Machines::ContainerDependency",
19
+ "kind": "property",
20
+ "lexicon": "fly"
21
+ },
22
+ "ContainerHealthcheck": {
23
+ "resourceType": "Fly::Machines::ContainerHealthcheck",
24
+ "kind": "property",
25
+ "lexicon": "fly"
26
+ },
27
+ "DNSConfig": {
28
+ "resourceType": "Fly::Machines::DNSConfig",
29
+ "kind": "property",
30
+ "lexicon": "fly"
31
+ },
32
+ "DnsForwardRule": {
33
+ "resourceType": "Fly::Machines::DnsForwardRule",
34
+ "kind": "property",
35
+ "lexicon": "fly"
36
+ },
37
+ "DnsOption": {
38
+ "resourceType": "Fly::Machines::DnsOption",
39
+ "kind": "property",
40
+ "lexicon": "fly"
41
+ },
42
+ "EnvFrom": {
43
+ "resourceType": "Fly::Machines::EnvFrom",
44
+ "kind": "property",
45
+ "lexicon": "fly",
46
+ "constraints": {
47
+ "field_ref": {
48
+ "enum": [
49
+ "id",
50
+ "version",
51
+ "app_name",
52
+ "private_ip",
53
+ "region",
54
+ "image"
55
+ ]
56
+ }
57
+ }
58
+ },
59
+ "ExecHealthcheck": {
60
+ "resourceType": "Fly::Machines::ExecHealthcheck",
61
+ "kind": "property",
62
+ "lexicon": "fly"
63
+ },
64
+ "File": {
65
+ "resourceType": "Fly::Machines::File",
66
+ "kind": "property",
67
+ "lexicon": "fly"
68
+ },
69
+ "HTTPHealthcheck": {
70
+ "resourceType": "Fly::Machines::HTTPHealthcheck",
71
+ "kind": "property",
72
+ "lexicon": "fly"
73
+ },
74
+ "HTTPOptions": {
75
+ "resourceType": "Fly::Machines::HTTPOptions",
76
+ "kind": "property",
77
+ "lexicon": "fly"
78
+ },
79
+ "HTTPResponseOptions": {
80
+ "resourceType": "Fly::Machines::HTTPResponseOptions",
81
+ "kind": "property",
82
+ "lexicon": "fly"
83
+ },
84
+ "IPAddress": {
85
+ "resourceType": "Fly::Machines::IPAddress",
86
+ "kind": "resource",
87
+ "lexicon": "fly"
88
+ },
89
+ "Machine": {
90
+ "resourceType": "Fly::Machines::Machine",
91
+ "kind": "resource",
92
+ "lexicon": "fly"
93
+ },
94
+ "MachineCacheDrive": {
95
+ "resourceType": "Fly::Machines::MachineCacheDrive",
96
+ "kind": "property",
97
+ "lexicon": "fly"
98
+ },
99
+ "MachineCheck": {
100
+ "resourceType": "Fly::Machines::MachineCheck",
101
+ "kind": "property",
102
+ "lexicon": "fly",
103
+ "constraints": {
104
+ "kind": {
105
+ "enum": [
106
+ "informational",
107
+ "readiness"
108
+ ]
109
+ }
110
+ }
111
+ },
112
+ "MachineConfig": {
113
+ "resourceType": "Fly::Machines::MachineConfig",
114
+ "kind": "property",
115
+ "lexicon": "fly"
116
+ },
117
+ "MachineGuest": {
118
+ "resourceType": "Fly::Machines::MachineGuest",
119
+ "kind": "property",
120
+ "lexicon": "fly",
121
+ "constraints": {
122
+ "persist_rootfs": {
123
+ "enum": [
124
+ "never",
125
+ "always",
126
+ "restart"
127
+ ]
128
+ }
129
+ }
130
+ },
131
+ "MachineHTTPHeader": {
132
+ "resourceType": "Fly::Machines::MachineHTTPHeader",
133
+ "kind": "property",
134
+ "lexicon": "fly"
135
+ },
136
+ "MachineInit": {
137
+ "resourceType": "Fly::Machines::MachineInit",
138
+ "kind": "property",
139
+ "lexicon": "fly"
140
+ },
141
+ "MachineMetrics": {
142
+ "resourceType": "Fly::Machines::MachineMetrics",
143
+ "kind": "property",
144
+ "lexicon": "fly"
145
+ },
146
+ "MachineMount": {
147
+ "resourceType": "Fly::Machines::MachineMount",
148
+ "kind": "property",
149
+ "lexicon": "fly"
150
+ },
151
+ "MachinePort": {
152
+ "resourceType": "Fly::Machines::MachinePort",
153
+ "kind": "property",
154
+ "lexicon": "fly"
155
+ },
156
+ "MachineProcess": {
157
+ "resourceType": "Fly::Machines::MachineProcess",
158
+ "kind": "property",
159
+ "lexicon": "fly"
160
+ },
161
+ "MachineRestart": {
162
+ "resourceType": "Fly::Machines::MachineRestart",
163
+ "kind": "property",
164
+ "lexicon": "fly",
165
+ "constraints": {
166
+ "policy": {
167
+ "enum": [
168
+ "no",
169
+ "always",
170
+ "on-failure",
171
+ "spot-price"
172
+ ]
173
+ }
174
+ }
175
+ },
176
+ "MachineRootfs": {
177
+ "resourceType": "Fly::Machines::MachineRootfs",
178
+ "kind": "property",
179
+ "lexicon": "fly",
180
+ "constraints": {
181
+ "persist": {
182
+ "enum": [
183
+ "never",
184
+ "always",
185
+ "restart"
186
+ ]
187
+ }
188
+ }
189
+ },
190
+ "MachineSecret": {
191
+ "resourceType": "Fly::Machines::MachineSecret",
192
+ "kind": "property",
193
+ "lexicon": "fly"
194
+ },
195
+ "MachineService": {
196
+ "resourceType": "Fly::Machines::MachineService",
197
+ "kind": "property",
198
+ "lexicon": "fly",
199
+ "constraints": {
200
+ "autostop": {
201
+ "enum": [
202
+ "off",
203
+ "stop",
204
+ "suspend"
205
+ ]
206
+ }
207
+ }
208
+ },
209
+ "MachineServiceCheck": {
210
+ "resourceType": "Fly::Machines::MachineServiceCheck",
211
+ "kind": "property",
212
+ "lexicon": "fly"
213
+ },
214
+ "MachineServiceConcurrency": {
215
+ "resourceType": "Fly::Machines::MachineServiceConcurrency",
216
+ "kind": "property",
217
+ "lexicon": "fly"
218
+ },
219
+ "MachineSpot": {
220
+ "resourceType": "Fly::Machines::MachineSpot",
221
+ "kind": "property",
222
+ "lexicon": "fly"
223
+ },
224
+ "ProxyProtoOptions": {
225
+ "resourceType": "Fly::Machines::ProxyProtoOptions",
226
+ "kind": "property",
227
+ "lexicon": "fly"
228
+ },
229
+ "ReplayCache": {
230
+ "resourceType": "Fly::Machines::ReplayCache",
231
+ "kind": "property",
232
+ "lexicon": "fly",
233
+ "constraints": {
234
+ "type": {
235
+ "enum": [
236
+ "cookie",
237
+ "header"
238
+ ]
239
+ }
240
+ }
241
+ },
242
+ "Secret": {
243
+ "resourceType": "Fly::Machines::Secret",
244
+ "kind": "resource",
245
+ "lexicon": "fly"
246
+ },
247
+ "Static": {
248
+ "resourceType": "Fly::Machines::Static",
249
+ "kind": "property",
250
+ "lexicon": "fly"
251
+ },
252
+ "StopConfig": {
253
+ "resourceType": "Fly::Machines::StopConfig",
254
+ "kind": "property",
255
+ "lexicon": "fly",
256
+ "constraints": {
257
+ "signal": {
258
+ "enum": [
259
+ "SIGHUP",
260
+ "SIGINT",
261
+ "SIGQUIT",
262
+ "SIGKILL",
263
+ "SIGUSR1",
264
+ "SIGUSR2",
265
+ "SIGTERM"
266
+ ]
267
+ }
268
+ }
269
+ },
270
+ "TCPHealthcheck": {
271
+ "resourceType": "Fly::Machines::TCPHealthcheck",
272
+ "kind": "property",
273
+ "lexicon": "fly"
274
+ },
275
+ "TLSOptions": {
276
+ "resourceType": "Fly::Machines::TLSOptions",
277
+ "kind": "property",
278
+ "lexicon": "fly"
279
+ },
280
+ "Volume": {
281
+ "resourceType": "Fly::Machines::Volume",
282
+ "kind": "resource",
283
+ "lexicon": "fly"
284
+ }
285
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Runtime factory constructors — re-exported from core.
3
+ */
4
+ export { createResource, createProperty } from "@intentius/chant/runtime";
@@ -0,0 +1,123 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { FlyGenerator } from "./generator";
3
+
4
+ const generator = new FlyGenerator();
5
+
6
+ function makeIR(resources: any[]) {
7
+ return { resources, parameters: [] };
8
+ }
9
+
10
+ function content(ir: ReturnType<typeof makeIR>): string {
11
+ return generator.generate(ir)[0].content;
12
+ }
13
+
14
+ describe("FlyGenerator", () => {
15
+ test("generates chant TypeScript from IR", () => {
16
+ const ir = makeIR([
17
+ { logicalId: "my-app", type: "Fly::Machines::App", properties: { name: "my-app", org_slug: "personal" } },
18
+ ]);
19
+ const result = content(ir);
20
+ expect(result).toContain('import { App } from "@intentius/chant-lexicon-fly";');
21
+ expect(result).toContain("export const myApp = new App({");
22
+ expect(result).toContain('name: "my-app"');
23
+ });
24
+
25
+ test("import source is @intentius/chant-lexicon-fly", () => {
26
+ const ir = makeIR([
27
+ { logicalId: "web", type: "Fly::Machines::Machine", properties: { name: "web" } },
28
+ ]);
29
+ expect(content(ir)).toContain('from "@intentius/chant-lexicon-fly"');
30
+ });
31
+
32
+ test("App and Machine produce two exports", () => {
33
+ const ir = makeIR([
34
+ { logicalId: "my-app", type: "Fly::Machines::App", properties: { name: "my-app" } },
35
+ { logicalId: "web", type: "Fly::Machines::Machine", properties: { name: "web", region: "iad" } },
36
+ ]);
37
+ const result = content(ir);
38
+ expect(result).toContain("export const myApp = new App(");
39
+ expect(result).toContain("export const web = new Machine(");
40
+ });
41
+
42
+ test("camelCase variable names from kebab-case logical names", () => {
43
+ const ir = makeIR([
44
+ { logicalId: "my-web-app", type: "Fly::Machines::App", properties: { name: "my-web-app" } },
45
+ ]);
46
+ expect(content(ir)).toContain("export const myWebApp");
47
+ });
48
+
49
+ test("config is wrapped in new MachineConfig(...) so it typechecks", () => {
50
+ const ir = makeIR([
51
+ {
52
+ logicalId: "web",
53
+ type: "Fly::Machines::Machine",
54
+ properties: { name: "web", config: { image: "nginx:1" } },
55
+ },
56
+ ]);
57
+ const result = content(ir);
58
+ expect(result).toContain("config: new MachineConfig({");
59
+ expect(result).toContain("MachineConfig");
60
+ // MachineConfig is imported alongside the resource classes.
61
+ expect(result).toMatch(/import \{[^}]*MachineConfig[^}]*\}/);
62
+ });
63
+
64
+ test("nested declarables (guest, services, ports) get their own constructors", () => {
65
+ const ir = makeIR([
66
+ {
67
+ logicalId: "web",
68
+ type: "Fly::Machines::Machine",
69
+ properties: {
70
+ name: "web",
71
+ config: {
72
+ image: "nginx:1",
73
+ guest: { cpu_kind: "shared", cpus: 1 },
74
+ services: [
75
+ { protocol: "tcp", internal_port: 8080, ports: [{ port: 443, handlers: ["tls"] }] },
76
+ ],
77
+ },
78
+ },
79
+ },
80
+ ]);
81
+ const result = content(ir);
82
+ expect(result).toContain("guest: new MachineGuest({");
83
+ expect(result).toContain("new MachineService({");
84
+ expect(result).toContain("new MachinePort({");
85
+ // Every wrapped class appears in the import.
86
+ const importLine = result.split("\n").find((l) => l.startsWith("import"))!;
87
+ for (const cls of ["App", "Machine", "MachineConfig", "MachineGuest", "MachineService", "MachinePort"]) {
88
+ // App is not used here; only imports for classes actually emitted.
89
+ if (cls === "App") continue;
90
+ expect(importLine).toContain(cls);
91
+ }
92
+ });
93
+
94
+ test("scalar maps (metadata, env) stay plain object literals", () => {
95
+ const ir = makeIR([
96
+ {
97
+ logicalId: "web",
98
+ type: "Fly::Machines::Machine",
99
+ properties: { name: "web", config: { metadata: { "managed-by": "chant" }, env: { PORT: "8080" } } },
100
+ },
101
+ ]);
102
+ const result = content(ir);
103
+ expect(result).toContain("metadata: {");
104
+ expect(result).toContain('"managed-by": "chant"');
105
+ expect(result).toContain("env: {");
106
+ expect(result).toContain("PORT: ");
107
+ });
108
+
109
+ test("imports are sorted alphabetically", () => {
110
+ const ir = makeIR([
111
+ { logicalId: "web", type: "Fly::Machines::Machine", properties: { name: "web", config: { image: "x" } } },
112
+ { logicalId: "my-app", type: "Fly::Machines::App", properties: { name: "my-app" } },
113
+ ]);
114
+ const importLine = content(ir).split("\n").find((l) => l.startsWith("import"))!;
115
+ expect(importLine.indexOf("App")).toBeLessThan(importLine.indexOf("Machine"));
116
+ });
117
+
118
+ test("empty IR still produces a single file", () => {
119
+ const files = generator.generate(makeIR([]));
120
+ expect(files).toHaveLength(1);
121
+ expect(typeof files[0].content).toBe("string");
122
+ });
123
+ });
@@ -0,0 +1,242 @@
1
+ /**
2
+ * flaps TypeScript generator.
3
+ *
4
+ * Converts import IR from {@link FlyParser} into typed chant TypeScript:
5
+ * `import { App, Machine, ... } from "@intentius/chant-lexicon-fly";` followed by
6
+ * `export const <var> = new <Class>({ ... });`.
7
+ *
8
+ * fly's nested config types (`MachineConfig`, `MachineGuest`, `MachineService`,
9
+ * ...) are property declarables — classes with an empty instance shape — so a
10
+ * bare object literal would trip TypeScript's excess-property check against the
11
+ * declared parameter type. Wherever a field's declared type is one of those
12
+ * classes, the value is wrapped in `new <Class>({ ... })` (recursively), which is
13
+ * the same authoring surface `chant init` scaffolds and the serializer walks back
14
+ * to the identical flaps body.
15
+ */
16
+
17
+ import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
18
+ import type { TemplateIR } from "@intentius/chant/import/parser";
19
+
20
+ /**
21
+ * How a nested field is wrapped: `single` → `new Cls({...})`, `array` → each
22
+ * element wrapped, `record` → each value wrapped.
23
+ */
24
+ type Nested = { kind: "single" | "array" | "record"; cls: string };
25
+
26
+ /**
27
+ * `<OwnerClass> → { <field>: { kind, cls } }` for every field whose declared
28
+ * type is a fly property declarable (a class, an array of one, or a
29
+ * `Record<string, one>`). Mirrors the constructor parameter types in the
30
+ * generated resource classes (src/generated). Regenerate this map if the pinned
31
+ * flaps spec changes the config surface.
32
+ */
33
+ const NESTED_DECLARABLES: Record<string, Record<string, Nested>> = {
34
+ Machine: {
35
+ config: { kind: "single", cls: "MachineConfig" },
36
+ },
37
+ Volume: {
38
+ compute: { kind: "single", cls: "MachineGuest" },
39
+ },
40
+ ContainerConfig: {
41
+ depends_on: { kind: "array", cls: "ContainerDependency" },
42
+ env_from: { kind: "array", cls: "EnvFrom" },
43
+ files: { kind: "array", cls: "File" },
44
+ healthchecks: { kind: "array", cls: "ContainerHealthcheck" },
45
+ restart: { kind: "single", cls: "MachineRestart" },
46
+ secrets: { kind: "array", cls: "MachineSecret" },
47
+ stop: { kind: "single", cls: "StopConfig" },
48
+ },
49
+ ContainerHealthcheck: {
50
+ exec: { kind: "single", cls: "ExecHealthcheck" },
51
+ http: { kind: "single", cls: "HTTPHealthcheck" },
52
+ tcp: { kind: "single", cls: "TCPHealthcheck" },
53
+ },
54
+ DNSConfig: {
55
+ dns_forward_rules: { kind: "array", cls: "DnsForwardRule" },
56
+ options: { kind: "array", cls: "DnsOption" },
57
+ },
58
+ HTTPHealthcheck: {
59
+ headers: { kind: "array", cls: "MachineHTTPHeader" },
60
+ },
61
+ HTTPOptions: {
62
+ replay_cache: { kind: "array", cls: "ReplayCache" },
63
+ response: { kind: "single", cls: "HTTPResponseOptions" },
64
+ },
65
+ MachineCheck: {
66
+ headers: { kind: "array", cls: "MachineHTTPHeader" },
67
+ },
68
+ MachineConfig: {
69
+ cache_drive: { kind: "single", cls: "MachineCacheDrive" },
70
+ checks: { kind: "record", cls: "MachineCheck" },
71
+ containers: { kind: "array", cls: "ContainerConfig" },
72
+ dns: { kind: "single", cls: "DNSConfig" },
73
+ files: { kind: "array", cls: "File" },
74
+ guest: { kind: "single", cls: "MachineGuest" },
75
+ init: { kind: "single", cls: "MachineInit" },
76
+ metrics: { kind: "single", cls: "MachineMetrics" },
77
+ mounts: { kind: "array", cls: "MachineMount" },
78
+ processes: { kind: "array", cls: "MachineProcess" },
79
+ restart: { kind: "single", cls: "MachineRestart" },
80
+ rootfs: { kind: "single", cls: "MachineRootfs" },
81
+ services: { kind: "array", cls: "MachineService" },
82
+ spot: { kind: "single", cls: "MachineSpot" },
83
+ statics: { kind: "array", cls: "Static" },
84
+ stop_config: { kind: "single", cls: "StopConfig" },
85
+ },
86
+ MachinePort: {
87
+ http_options: { kind: "single", cls: "HTTPOptions" },
88
+ proxy_proto_options: { kind: "single", cls: "ProxyProtoOptions" },
89
+ tls_options: { kind: "single", cls: "TLSOptions" },
90
+ },
91
+ MachineProcess: {
92
+ env_from: { kind: "array", cls: "EnvFrom" },
93
+ secrets: { kind: "array", cls: "MachineSecret" },
94
+ },
95
+ MachineService: {
96
+ checks: { kind: "array", cls: "MachineServiceCheck" },
97
+ concurrency: { kind: "single", cls: "MachineServiceConcurrency" },
98
+ ports: { kind: "array", cls: "MachinePort" },
99
+ },
100
+ MachineServiceCheck: {
101
+ headers: { kind: "array", cls: "MachineHTTPHeader" },
102
+ },
103
+ };
104
+
105
+ export class FlyGenerator implements TypeScriptGenerator {
106
+ generate(ir: TemplateIR): GeneratedFile[] {
107
+ const imports = new Set<string>();
108
+ const bodyLines: string[] = [];
109
+
110
+ for (const resource of ir.resources) {
111
+ const parts = resource.type.split("::");
112
+ const className = parts.length >= 3 ? parts[2] : resource.type;
113
+ imports.add(className);
114
+ const varName = camelCase(resource.logicalId);
115
+ const props = formatProps(resource.properties, 0, className, imports);
116
+ bodyLines.push(`export const ${varName} = new ${className}(${props});`);
117
+ bodyLines.push("");
118
+ }
119
+
120
+ const lines: string[] = [];
121
+ if (imports.size > 0) {
122
+ lines.push(
123
+ `import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-fly";`,
124
+ );
125
+ lines.push("");
126
+ }
127
+ lines.push(...bodyLines);
128
+
129
+ return [{ path: "main.ts", content: lines.join("\n") + "\n" }];
130
+ }
131
+ }
132
+
133
+ function camelCase(str: string): string {
134
+ return str
135
+ .replace(/[-_.](.)/g, (_, c) => c.toUpperCase())
136
+ .replace(/^(.)/, (_, c) => c.toLowerCase());
137
+ }
138
+
139
+ /**
140
+ * Format an object literal for the props of `ownerClass`. Fields whose declared
141
+ * type is a nested declarable are wrapped in their constructor; everything else
142
+ * is emitted as a plain literal.
143
+ */
144
+ function formatProps(
145
+ props: Record<string, unknown>,
146
+ indent: number,
147
+ ownerClass: string,
148
+ imports: Set<string>,
149
+ ): string {
150
+ const entries = Object.entries(props);
151
+ if (entries.length === 0) return "{}";
152
+
153
+ const pad = " ".repeat(indent + 1);
154
+ const closePad = " ".repeat(indent);
155
+ const nested = NESTED_DECLARABLES[ownerClass];
156
+
157
+ const lines = entries.map(([key, value]) => {
158
+ const spec = nested?.[key];
159
+ const rendered =
160
+ spec !== undefined
161
+ ? formatNested(value, indent + 1, spec, imports)
162
+ : formatValue(value, indent + 1);
163
+ return `${pad}${key}: ${rendered},`;
164
+ });
165
+
166
+ return `{\n${lines.join("\n")}\n${closePad}}`;
167
+ }
168
+
169
+ /** Wrap a nested-declarable value in its constructor per the field's kind. */
170
+ function formatNested(
171
+ value: unknown,
172
+ indent: number,
173
+ spec: Nested,
174
+ imports: Set<string>,
175
+ ): string {
176
+ const construct = (obj: unknown, at: number): string => {
177
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
178
+ return formatValue(obj, at);
179
+ }
180
+ imports.add(spec.cls);
181
+ return `new ${spec.cls}(${formatProps(obj as Record<string, unknown>, at, spec.cls, imports)})`;
182
+ };
183
+
184
+ if (spec.kind === "single") {
185
+ return construct(value, indent);
186
+ }
187
+
188
+ if (spec.kind === "array") {
189
+ if (!Array.isArray(value) || value.length === 0) return "[]";
190
+ const pad = " ".repeat(indent + 1);
191
+ const closePad = " ".repeat(indent);
192
+ const items = value.map((v) => `${pad}${construct(v, indent + 1)},`);
193
+ return `[\n${items.join("\n")}\n${closePad}]`;
194
+ }
195
+
196
+ // record: a plain object whose values are wrapped in the constructor.
197
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
198
+ return formatValue(value, indent);
199
+ }
200
+ const record = value as Record<string, unknown>;
201
+ const recKeys = Object.entries(record);
202
+ if (recKeys.length === 0) return "{}";
203
+ const pad = " ".repeat(indent + 1);
204
+ const closePad = " ".repeat(indent);
205
+ const items = recKeys.map(([k, v]) => `${pad}${JSON.stringify(k)}: ${construct(v, indent + 1)},`);
206
+ return `{\n${items.join("\n")}\n${closePad}}`;
207
+ }
208
+
209
+ function formatValue(value: unknown, indent: number): string {
210
+ if (value === null || value === undefined) return "undefined";
211
+ if (typeof value === "string") return JSON.stringify(value);
212
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
213
+
214
+ if (Array.isArray(value)) {
215
+ if (value.length === 0) return "[]";
216
+ const pad = " ".repeat(indent + 1);
217
+ const closePad = " ".repeat(indent);
218
+ const items = value.map((v) => `${pad}${formatValue(v, indent + 1)},`);
219
+ return `[\n${items.join("\n")}\n${closePad}]`;
220
+ }
221
+
222
+ if (typeof value === "object") {
223
+ return formatPlainObject(value as Record<string, unknown>, indent);
224
+ }
225
+
226
+ return String(value);
227
+ }
228
+
229
+ /** A plain object literal (no constructor wrapping) — for scalar-valued maps like env/metadata. */
230
+ function formatPlainObject(obj: Record<string, unknown>, indent: number): string {
231
+ const entries = Object.entries(obj);
232
+ if (entries.length === 0) return "{}";
233
+ const pad = " ".repeat(indent + 1);
234
+ const closePad = " ".repeat(indent);
235
+ const lines = entries.map(([key, value]) => `${pad}${objectKey(key)}: ${formatValue(value, indent + 1)},`);
236
+ return `{\n${lines.join("\n")}\n${closePad}}`;
237
+ }
238
+
239
+ /** Emit a bare identifier key when it is a valid one, else a quoted key. */
240
+ function objectKey(key: string): string {
241
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
242
+ }