@cdktn/hcl2cdk 0.24.0-pre.45 → 0.24.0-pre.48

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 (89) hide show
  1. package/LICENSE +355 -0
  2. package/README.md +1 -1
  3. package/build/__tests__/expressions.test.js +10 -19
  4. package/build/__tests__/functions.test.js +8 -18
  5. package/build/__tests__/testHelpers.js +3 -2
  6. package/build/coerceType.js +11 -21
  7. package/build/dynamic-blocks.js +3 -3
  8. package/build/expressions.js +13 -22
  9. package/build/function-bindings/functions.generated.js +2 -2
  10. package/build/generation.js +24 -34
  11. package/build/index.js +15 -25
  12. package/build/iteration.js +7 -6
  13. package/build/jsii-rosetta-workarounds.js +6 -5
  14. package/build/partialCode.js +11 -20
  15. package/build/provider.js +4 -3
  16. package/build/references.js +6 -5
  17. package/build/schema.js +8 -18
  18. package/build/terraformSchema.js +4 -4
  19. package/build/utils.js +3 -3
  20. package/build/variables.js +12 -21
  21. package/package.json +20 -17
  22. package/package.sh +1 -1
  23. package/src/__tests__/coerceType.test.ts +207 -0
  24. package/src/__tests__/expressionToTs.test.ts +1167 -0
  25. package/src/__tests__/expressions.test.ts +541 -0
  26. package/src/__tests__/findExpressionType.test.ts +112 -0
  27. package/src/__tests__/functions.test.ts +768 -0
  28. package/src/__tests__/generation.test.ts +72 -0
  29. package/src/__tests__/jsii-rosetta-workarounds.test.ts +145 -0
  30. package/src/__tests__/partialCode.test.ts +432 -0
  31. package/src/__tests__/terraformSchema.test.ts +107 -0
  32. package/src/__tests__/testHelpers.ts +11 -0
  33. package/src/coerceType.ts +261 -0
  34. package/src/dynamic-blocks.ts +61 -0
  35. package/src/expressions.ts +968 -0
  36. package/src/function-bindings/functions.generated.ts +1139 -0
  37. package/src/function-bindings/functions.ts +104 -0
  38. package/src/generation.ts +1189 -0
  39. package/src/index.ts +584 -0
  40. package/src/iteration.ts +156 -0
  41. package/src/jsii-rosetta-workarounds.ts +145 -0
  42. package/src/partialCode.ts +132 -0
  43. package/src/provider.ts +60 -0
  44. package/src/references.ts +193 -0
  45. package/src/schema.ts +74 -0
  46. package/src/terraformSchema.ts +182 -0
  47. package/src/types.ts +58 -0
  48. package/src/utils.ts +19 -0
  49. package/src/variables.ts +214 -0
  50. package/test/__snapshots__/backends.test.ts.snap +70 -0
  51. package/test/__snapshots__/externals.test.ts.snap +37 -0
  52. package/test/__snapshots__/granular-imports.test.ts.snap +180 -0
  53. package/test/__snapshots__/imports.test.ts.snap +159 -0
  54. package/test/__snapshots__/iteration.test.ts.snap +532 -0
  55. package/test/__snapshots__/jsiiLanguage.test.ts.snap +347 -0
  56. package/test/__snapshots__/locals.test.ts.snap +55 -0
  57. package/test/__snapshots__/modules.test.ts.snap +127 -0
  58. package/test/__snapshots__/outputs.test.ts.snap +77 -0
  59. package/test/__snapshots__/partialCode.test.ts.snap +120 -0
  60. package/test/__snapshots__/provider.test.ts.snap +128 -0
  61. package/test/__snapshots__/references.test.ts.snap +376 -0
  62. package/test/__snapshots__/resource-meta-properties.test.ts.snap +342 -0
  63. package/test/__snapshots__/resources.test.ts.snap +613 -0
  64. package/test/__snapshots__/tfExpressions.test.ts.snap +537 -0
  65. package/test/__snapshots__/typeCoercion.test.ts.snap +253 -0
  66. package/test/__snapshots__/variables.test.ts.snap +150 -0
  67. package/test/backends.test.ts +75 -0
  68. package/test/convertProject.test.ts +257 -0
  69. package/test/externals.test.ts +35 -0
  70. package/test/globalSetup.ts +224 -0
  71. package/test/globalTeardown.ts +11 -0
  72. package/test/granular-imports.test.ts +161 -0
  73. package/test/hcl2cdk.test.ts +88 -0
  74. package/test/helpers/convert.ts +543 -0
  75. package/test/helpers/tmp.ts +25 -0
  76. package/test/imports.test.ts +141 -0
  77. package/test/iteration.test.ts +342 -0
  78. package/test/jsiiLanguage.test.ts +73 -0
  79. package/test/locals.test.ts +47 -0
  80. package/test/modules.test.ts +143 -0
  81. package/test/outputs.test.ts +69 -0
  82. package/test/partialCode.test.ts +25 -0
  83. package/test/provider.test.ts +106 -0
  84. package/test/references.test.ts +287 -0
  85. package/test/resource-meta-properties.test.ts +288 -0
  86. package/test/resources.test.ts +551 -0
  87. package/test/tfExpressions.test.ts +300 -0
  88. package/test/typeCoercion.test.ts +154 -0
  89. package/test/variables.test.ts +96 -0
@@ -0,0 +1,1189 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import generate from "@babel/generator";
4
+ import template from "@babel/template";
5
+ import * as t from "@babel/types";
6
+ import { DirectedGraph } from "graphology";
7
+ import prettier from "prettier";
8
+
9
+ import {
10
+ TerraformResourceBlock,
11
+ ProgramScope,
12
+ ResourceScope,
13
+ ImportableConstruct,
14
+ AttributePath,
15
+ } from "./types";
16
+ import { camelCase, logger, pascalCase, uniqueId } from "./utils";
17
+ import {
18
+ Resource,
19
+ TerraformConfig,
20
+ Module,
21
+ Provider,
22
+ Variable,
23
+ Output,
24
+ Import,
25
+ } from "./schema";
26
+ import { convertTerraformExpressionToTs, expressionAst } from "./expressions";
27
+ import { Reference } from "./types";
28
+ import { findUsedReferences } from "./references";
29
+ import {
30
+ TerraformModuleConstraint,
31
+ escapeAttributeName,
32
+ } from "@cdktn/provider-generator";
33
+ import {
34
+ getTypeAtPath,
35
+ isMapAttribute,
36
+ getDesiredType,
37
+ } from "./terraformSchema";
38
+ import { Errors, AttributeType, BlockType, Schema } from "@cdktn/commons";
39
+ import { TFExpressionSyntaxTree as tex } from "@cdktn/hcl2json";
40
+ import { extractDynamicBlocks, isNestedDynamicBlock } from "./dynamic-blocks";
41
+ import {
42
+ constructAst,
43
+ referenceToVariableName,
44
+ variableName,
45
+ } from "./variables";
46
+ import { snakeCase } from "cdktn/lib/util";
47
+ import { fillWithConfigAccessors } from "./partialCode";
48
+
49
+ function getReference(graph: DirectedGraph, id: string) {
50
+ logger.debug(`Finding reference for ${id}`);
51
+ const neighbors = graph.outNeighbors(id);
52
+
53
+ if (neighbors.length > 0) {
54
+ logger.debug(`Found neighbors ${neighbors} for ${id}`);
55
+ const edge = graph.directedEdge(id, neighbors[0]);
56
+
57
+ if (edge) {
58
+ logger.debug(`Found first edge ${edge} for ${id}`);
59
+ logger.debug(
60
+ `Returning reference ${graph.getEdgeAttribute(edge, "ref")}`,
61
+ );
62
+ return graph.getEdgeAttribute(edge, "ref") as Reference;
63
+ } else {
64
+ logger.debug(`Found no edge for ${id}`);
65
+ return undefined;
66
+ }
67
+ } else {
68
+ return undefined;
69
+ }
70
+ }
71
+
72
+ export function attributeNameToCdktfName(name: string) {
73
+ return escapeAttributeName(camelCase(name));
74
+ }
75
+
76
+ export const valueToTs = async (
77
+ scope: ResourceScope,
78
+ item: TerraformResourceBlock,
79
+ path: string,
80
+ isModule = false,
81
+ ): Promise<t.Expression> => {
82
+ switch (typeof item) {
83
+ case "string":
84
+ if (
85
+ (await findUsedReferences(scope.nodeIds, item)).some((ref) =>
86
+ path.startsWith(ref.referencee.id),
87
+ )
88
+ ) {
89
+ return t.stringLiteral(item);
90
+ }
91
+
92
+ return await convertTerraformExpressionToTs(scope, `"${item}"`, () =>
93
+ getDesiredType(scope, path),
94
+ );
95
+
96
+ case "boolean":
97
+ return await convertTerraformExpressionToTs(scope, `${item}`, () =>
98
+ getDesiredType(scope, path),
99
+ );
100
+ case "number":
101
+ return await convertTerraformExpressionToTs(scope, `${item}`, () =>
102
+ getDesiredType(scope, path),
103
+ );
104
+ case "object": {
105
+ if (item === undefined || item === null) {
106
+ return t.nullLiteral();
107
+ }
108
+
109
+ // For iterators and dynamic blocks we put the correct TS expression in the config ahead of time
110
+ if (t.isNode(item) && t.isExpression(item)) {
111
+ return item;
112
+ }
113
+
114
+ const attributeType = getTypeAtPath(scope.providerSchema, path);
115
+
116
+ function shouldRemoveArrayBasedOnType(
117
+ attributeType: Schema | AttributeType | BlockType | null,
118
+ ): boolean {
119
+ if (!attributeType) {
120
+ return false; // The default assumption is we need the array
121
+ }
122
+
123
+ // maps and object don't need to be wrapped in an array
124
+ if (
125
+ Array.isArray(attributeType) &&
126
+ (attributeType[0] === "map" || attributeType[0] === "object")
127
+ ) {
128
+ return true;
129
+ }
130
+
131
+ // If it's a block type with max_items = 1 we don't need to wrap it in an array
132
+ if (
133
+ typeof attributeType === "object" &&
134
+ "max_items" in attributeType &&
135
+ attributeType.max_items === 1
136
+ ) {
137
+ return true;
138
+ }
139
+
140
+ return false;
141
+ }
142
+
143
+ const unwrappedItem =
144
+ Array.isArray(item) &&
145
+ (shouldRemoveArrayBasedOnType(attributeType) ||
146
+ path.endsWith("lifecycle") ||
147
+ path.endsWith("connection"))
148
+ ? item[0]
149
+ : item;
150
+
151
+ if (Array.isArray(unwrappedItem)) {
152
+ return t.arrayExpression(
153
+ await Promise.all(
154
+ unwrappedItem.map((i) => valueToTs(scope, i, `${path}.[]`)),
155
+ ),
156
+ );
157
+ }
158
+
159
+ return t.objectExpression(
160
+ (
161
+ await Promise.all(
162
+ Object.entries(unwrappedItem).map(async ([key, value]) => {
163
+ if (value === undefined) {
164
+ return undefined;
165
+ }
166
+
167
+ if (key === "dynamic") {
168
+ const { for_each: _for_each, ...others } = value as any;
169
+ const dynamicRef = Object.keys(others)[0];
170
+ return t.objectProperty(
171
+ t.identifier(
172
+ scope.withinOverrideExpression
173
+ ? dynamicRef
174
+ : escapeAttributeName(camelCase(dynamicRef)),
175
+ ),
176
+ t.arrayExpression(),
177
+ );
178
+ }
179
+
180
+ const itemPath = `${path}.${key}`;
181
+ const itemAttributeType = getTypeAtPath(
182
+ scope.providerSchema,
183
+ itemPath,
184
+ );
185
+
186
+ const typeMetadata = getTypeAtPath(
187
+ scope.providerSchema,
188
+ itemPath,
189
+ );
190
+
191
+ const isSingleItemBlock =
192
+ typeMetadata &&
193
+ typeof typeMetadata === "object" &&
194
+ Object.prototype.hasOwnProperty.call(typeMetadata, "max_items")
195
+ ? (typeMetadata as any).max_items === 1
196
+ : false;
197
+
198
+ const shouldBeArray =
199
+ typeof value === "object" &&
200
+ !Array.isArray(value) &&
201
+ !(t.isNode(value) && t.isExpression(value)) &&
202
+ !isSingleItemBlock &&
203
+ // Map type attributes must not be wrapped in arrays
204
+ !isMapAttribute(itemAttributeType) &&
205
+ key !== "tags" &&
206
+ key !== "forEach" &&
207
+ key !== "lifecycle";
208
+
209
+ const keepKeyName: boolean =
210
+ !isModule &&
211
+ key !== "depends_on" &&
212
+ !path.includes("lifecycle") &&
213
+ (key === "for_each" ||
214
+ !typeMetadata ||
215
+ isMapAttribute(attributeType)) &&
216
+ !(path.startsWith("var.") && path.includes("validation"));
217
+
218
+ return t.objectProperty(
219
+ t.stringLiteral(
220
+ keepKeyName ? key : attributeNameToCdktfName(key),
221
+ ),
222
+ shouldBeArray
223
+ ? t.arrayExpression([await valueToTs(scope, value, itemPath)])
224
+ : await valueToTs(scope, value, itemPath),
225
+ );
226
+ }),
227
+ )
228
+ ).filter((expr) => expr !== undefined) as t.ObjectProperty[],
229
+ );
230
+ }
231
+ }
232
+ throw new Error("Unsupported type " + item);
233
+ };
234
+
235
+ export async function backendToExpression(
236
+ scope: ProgramScope,
237
+ tf: TerraformConfig["backend"],
238
+ ): Promise<t.Statement[]> {
239
+ return (
240
+ await Promise.all(
241
+ Object.entries(tf || {}).map(async ([type, [config]]) => {
242
+ const backendIdentifier = pascalCase(`${type}Backend`);
243
+ scope.importables.push({
244
+ constructName: backendIdentifier,
245
+ provider: "cdktn",
246
+ });
247
+ return t.expressionStatement(
248
+ t.newExpression(t.identifier(backendIdentifier), [
249
+ t.thisExpression(),
250
+ t.objectExpression(
251
+ (
252
+ await Promise.all(
253
+ Object.entries(config).map(async ([property, value]) =>
254
+ t.objectProperty(
255
+ t.identifier(camelCase(property)),
256
+ await valueToTs(
257
+ scope,
258
+ value,
259
+ "path-for-backends-can-be-ignored",
260
+ ),
261
+ ),
262
+ ),
263
+ )
264
+ ).reduce(
265
+ (carry, item) => [...carry, item],
266
+ [] as t.ObjectProperty[],
267
+ ),
268
+ ),
269
+ ]),
270
+ );
271
+ }),
272
+ )
273
+ ).reduce((carry, item) => [...carry, item], [] as t.Statement[]);
274
+ }
275
+
276
+ function addOverrideExpression(
277
+ variable: string,
278
+ path: string,
279
+ value: t.Expression,
280
+ explanatoryComment?: string,
281
+ ) {
282
+ const ast = t.expressionStatement(
283
+ t.callExpression(
284
+ t.memberExpression(t.identifier(variable), t.identifier("addOverride")),
285
+ [t.stringLiteral(path), value],
286
+ ),
287
+ );
288
+
289
+ if (explanatoryComment) {
290
+ t.addComment(ast, "leading", explanatoryComment);
291
+ }
292
+
293
+ return ast;
294
+ }
295
+
296
+ function addOverrideLogicalIdExpression(variable: string, logicalId: string) {
297
+ const ast = t.expressionStatement(
298
+ t.callExpression(
299
+ t.memberExpression(
300
+ t.identifier(variable),
301
+ t.identifier("overrideLogicalId"),
302
+ ),
303
+ [t.stringLiteral(logicalId)],
304
+ ),
305
+ );
306
+
307
+ t.addComment(
308
+ ast,
309
+ "leading",
310
+ "This allows the Terraform resource name to match the original name. You can remove the call if you don't need them to match.",
311
+ );
312
+
313
+ return ast;
314
+ }
315
+
316
+ function getRemoteStateType(item: Resource) {
317
+ const backendRecord = item.find((val) => val.backend);
318
+ if (backendRecord) {
319
+ const backend = backendRecord.backend;
320
+ switch (backend) {
321
+ case "remote":
322
+ return "";
323
+ case "etcdv3":
324
+ return "_etcd_v3";
325
+ default:
326
+ return `_${backend}`;
327
+ }
328
+ } else {
329
+ return "";
330
+ }
331
+ }
332
+
333
+ function resourceType(provider: string, name: string[], item: Resource) {
334
+ switch (provider) {
335
+ case "data.terraform":
336
+ return `cdktn.data_terraform_${name.join("_")}${getRemoteStateType(
337
+ item,
338
+ )}`;
339
+ case "null":
340
+ return `NullProvider.${name.join("_")}`;
341
+ default:
342
+ return `${provider}.${name.join("_")}`;
343
+ }
344
+ }
345
+
346
+ function mapConfigPerResourceType(resource: string, item: Resource[0]) {
347
+ // Backends have a slightly different API
348
+ if (resource.startsWith("cdktn.data_terraform_")) {
349
+ return item.config;
350
+ }
351
+ return item;
352
+ }
353
+
354
+ const loopComment = `In most cases loops should be handled in the programming language context and
355
+ not inside of the Terraform context. If you are looping over something external, e.g. a variable or a file input
356
+ you should consider using a for loop. If you are looping over something only known to Terraform, e.g. a result of a data source
357
+ you need to keep this like it is.`;
358
+ export async function resource(
359
+ scope: ProgramScope,
360
+ type: string,
361
+ key: string,
362
+ id: string,
363
+ item: Resource,
364
+ graph: DirectedGraph,
365
+ ): Promise<t.Statement[]> {
366
+ const [provider, ...name] = type.split("_");
367
+ const resource = resourceType(provider, name, item);
368
+
369
+ if (!provider) {
370
+ throw new Error(`Could not parse resource type '${type}'`);
371
+ }
372
+ let expressions: t.Statement[] = [];
373
+ const varName = variableName(scope, resource, key);
374
+ const { for_each, count, provisioner, ...config } = item[0];
375
+ const mappedConfig = mapConfigPerResourceType(resource, config);
376
+
377
+ let forEachIteratorName: string | undefined;
378
+ if (for_each) {
379
+ forEachIteratorName = variableName(
380
+ scope,
381
+ resource,
382
+ `${key}_for_each_iterator`,
383
+ );
384
+ const referenceAst = await convertTerraformExpressionToTs(
385
+ scope,
386
+ `"${for_each}"`,
387
+ () => ["list", "dynamic"],
388
+ );
389
+
390
+ scope.importables.push({
391
+ provider: "cdktn",
392
+ constructName: "TerraformIterator",
393
+ });
394
+
395
+ const iterator = t.variableDeclaration("const", [
396
+ t.variableDeclarator(
397
+ t.identifier(forEachIteratorName),
398
+ t.callExpression(
399
+ t.memberExpression(
400
+ t.identifier("TerraformIterator"),
401
+ t.identifier("fromList"),
402
+ ),
403
+
404
+ [referenceAst],
405
+ ),
406
+ ),
407
+ ]);
408
+ t.addComment(iterator, "leading", loopComment);
409
+ expressions.push(iterator);
410
+
411
+ mappedConfig.forEach = t.identifier(forEachIteratorName);
412
+ }
413
+
414
+ let countIteratorName: string | undefined;
415
+ if (count) {
416
+ countIteratorName = variableName(scope, resource, `${key}_count`);
417
+ const referenceAst = await convertTerraformExpressionToTs(
418
+ scope,
419
+ `"${count}"`,
420
+ () => "number",
421
+ );
422
+
423
+ scope.importables.push({
424
+ provider: "cdktn",
425
+ constructName: "TerraformCount",
426
+ });
427
+
428
+ const iterator = t.variableDeclaration("const", [
429
+ t.variableDeclarator(
430
+ t.identifier(countIteratorName),
431
+ t.callExpression(
432
+ t.memberExpression(
433
+ t.identifier("TerraformCount"),
434
+ t.identifier("of"),
435
+ ),
436
+ [referenceAst],
437
+ ),
438
+ ),
439
+ ]);
440
+ t.addComment(iterator, "leading", loopComment);
441
+ mappedConfig.count = t.identifier(countIteratorName);
442
+ expressions.push(iterator);
443
+ }
444
+
445
+ const dynBlocks = extractDynamicBlocks(mappedConfig);
446
+ const nestedDynamicBlocks = dynBlocks.filter((block) =>
447
+ isNestedDynamicBlock(dynBlocks, block),
448
+ );
449
+ const dynamicBlocksUsingOverrides = dynBlocks.filter(
450
+ (block) =>
451
+ // nested blocks need overrides
452
+ nestedDynamicBlocks.includes(block) ||
453
+ // blocks that contain nested blocks need them as well
454
+ nestedDynamicBlocks.some((nestedBlock) =>
455
+ nestedBlock.path.startsWith(block.path),
456
+ ),
457
+ );
458
+ // all others can be handled by the CDKTN runtime
459
+ const dynamicBlocksUsingRuntime = dynBlocks.filter(
460
+ (block) => !dynamicBlocksUsingOverrides.includes(block),
461
+ );
462
+
463
+ for (const [i, block] of dynamicBlocksUsingRuntime.entries()) {
464
+ const dynamicBlockIteratorName = variableName(
465
+ scope,
466
+ resource,
467
+ `${key}_dynamic_iterator_${i}`,
468
+ );
469
+
470
+ const referenceAst = await convertTerraformExpressionToTs(
471
+ scope,
472
+ `"${block.for_each}"`,
473
+ () => ["list", "dynamic"],
474
+ );
475
+
476
+ scope.importables.push({
477
+ provider: "cdktn",
478
+ constructName: "TerraformIterator",
479
+ });
480
+
481
+ const iterator = t.variableDeclaration("const", [
482
+ t.variableDeclarator(
483
+ t.identifier(dynamicBlockIteratorName),
484
+ t.callExpression(
485
+ t.memberExpression(
486
+ t.identifier("TerraformIterator"),
487
+ t.identifier("fromList"),
488
+ ),
489
+ [referenceAst],
490
+ ),
491
+ ),
492
+ ]);
493
+ t.addComment(iterator, "leading", loopComment);
494
+ expressions.push(iterator);
495
+ const dynamicCallExpression = t.callExpression(
496
+ t.memberExpression(
497
+ t.identifier(dynamicBlockIteratorName),
498
+ t.identifier("dynamic"),
499
+ ),
500
+ [
501
+ await valueToTs(
502
+ {
503
+ ...scope,
504
+ scopedVariables: {
505
+ [block.scopedVar]: dynamicBlockIteratorName,
506
+ },
507
+ },
508
+ fillWithConfigAccessors(
509
+ scope,
510
+ Array.isArray(block.content) ? block.content[0] : block.content,
511
+ block.path.replace(block.scopedVar, ""),
512
+ ),
513
+ block.path.replace(block.scopedVar, ""),
514
+ false,
515
+ ),
516
+ ],
517
+ );
518
+
519
+ const parts = block.path
520
+ .replace(`dynamic.${block.scopedVar}`, "")
521
+ .split(".")
522
+ .filter((p) => p.length > 0);
523
+
524
+ const parent = parts.reduce((acc, part) => {
525
+ if (Array.isArray(acc) && !Number.isNaN(parseInt(part, 10))) {
526
+ return acc[parseInt(part, 10)];
527
+ } else {
528
+ return acc[part];
529
+ }
530
+ }, mappedConfig);
531
+ parent[block.scopedVar] = dynamicCallExpression;
532
+ delete parent.dynamic;
533
+ }
534
+
535
+ const overrideReference = dynamicBlocksUsingOverrides.length
536
+ ? {
537
+ start: 0,
538
+ end: 0,
539
+ referencee: {
540
+ id: `${type}.${key}`,
541
+ full: `${type}.${key}`,
542
+ },
543
+ }
544
+ : undefined;
545
+
546
+ if (provisioner) {
547
+ mappedConfig.provisioners = await Promise.all(
548
+ Object.entries(provisioner).flatMap(([type, p]: [string, any]) =>
549
+ p.map((pp: Record<string, any>) =>
550
+ valueToTs(
551
+ scope,
552
+ { type, ...pp },
553
+ "path-for-provisioners-can-be-ignored",
554
+ ),
555
+ ),
556
+ ),
557
+ );
558
+ }
559
+
560
+ const importGraphId = `import.${resource.replace(".", "_")}.${key}`;
561
+ const importDefinition: Import | undefined = graph.hasNode(importGraphId)
562
+ ? graph.getNodeAttribute(importGraphId, "value")
563
+ : undefined;
564
+
565
+ expressions = expressions.concat(
566
+ await asExpression(
567
+ { ...scope, forEachIteratorName, countIteratorName },
568
+ resource,
569
+ key,
570
+ mappedConfig,
571
+ false,
572
+ false,
573
+ getReference(graph, id) || overrideReference,
574
+ importDefinition,
575
+ ),
576
+ );
577
+
578
+ // Check for dynamic blocks
579
+ expressions = expressions.concat(
580
+ await Promise.all(
581
+ dynamicBlocksUsingOverrides.map(async ({ path, for_each, content }) => {
582
+ // We need to let the expression conversion know all available
583
+ // dynamic block names, so we don't replace them. The "dynamic-block"
584
+ // scoped variable indicates to the expression conversion to use the
585
+ // key name instead of an iterator
586
+ const scopedVariablesInPath = Object.fromEntries(
587
+ path
588
+ .substring(1) // The path starts with a dot that results in an empty split
589
+ .split(".")
590
+ .filter(
591
+ (p) => !["dynamic", "content"].includes(p) && isNaN(parseInt(p)),
592
+ )
593
+ .map((p) => [p, "dynamic-block"]),
594
+ );
595
+
596
+ return addOverrideExpression(
597
+ varName,
598
+ path.substring(1), // The path starts with a dot that we don't want
599
+ await valueToTs(
600
+ {
601
+ ...scope,
602
+ withinOverrideExpression: true,
603
+ scopedVariables: scopedVariablesInPath,
604
+ },
605
+ {
606
+ for_each,
607
+ content,
608
+ },
609
+ "path-for-dynamic-blocks-can-be-ignored",
610
+ ),
611
+ loopComment,
612
+ );
613
+ }),
614
+ ),
615
+ );
616
+
617
+ return expressions;
618
+ }
619
+
620
+ async function asExpression(
621
+ scope: ResourceScope,
622
+ type: string,
623
+ name: string,
624
+ config: TerraformResourceBlock,
625
+ isModuleImport: boolean,
626
+ isProvider: boolean,
627
+ reference?: Reference,
628
+ imported?: Import,
629
+ ) {
630
+ const { providers, ...otherOptions } = config as any;
631
+
632
+ const constructId = uniqueId(scope.constructs, name);
633
+ const overrideId = !isProvider && constructId !== name;
634
+
635
+ const completeObject = fillWithConfigAccessors(scope, otherOptions, type);
636
+
637
+ const expression = t.newExpression(
638
+ constructAst(scope, type, isModuleImport),
639
+ [
640
+ t.thisExpression(),
641
+ t.stringLiteral(constructId),
642
+
643
+ await valueToTs(
644
+ scope,
645
+ {
646
+ ...completeObject,
647
+ providers:
648
+ providers && Object.keys(providers).length
649
+ ? Object.entries(providers).map(([key, value]) => ({
650
+ moduleAlias: key,
651
+ provider: value,
652
+ }))
653
+ : undefined,
654
+ },
655
+ `${type}`,
656
+ isModuleImport,
657
+ ),
658
+ ],
659
+ );
660
+
661
+ const statements = [];
662
+ const varName = reference
663
+ ? referenceToVariableName(scope, reference)
664
+ : variableName(scope, type, name);
665
+
666
+ if (reference || overrideId || imported) {
667
+ statements.push(
668
+ t.variableDeclaration("const", [
669
+ t.variableDeclarator(t.identifier(varName), expression),
670
+ ]),
671
+ );
672
+ } else {
673
+ statements.push(t.expressionStatement(expression));
674
+ }
675
+
676
+ if (overrideId) {
677
+ statements.push(addOverrideLogicalIdExpression(varName, name));
678
+ }
679
+
680
+ if (imported) {
681
+ // Adds myVar.importFrom("my-arn")
682
+ const importExpression = t.expressionStatement(
683
+ t.callExpression(
684
+ t.memberExpression(t.identifier(varName), t.identifier("importFrom")),
685
+ [t.stringLiteral(imported.id)],
686
+ ),
687
+ );
688
+
689
+ if (imported.provider) {
690
+ t.addComment(
691
+ importExpression,
692
+ "leading",
693
+ `This import was configured with a provider. CDKTN does support this, but the cdktn convert command does not yet. Please add the provider reference manually. See https://cdktn.io/docs/concepts/resources#importing-resources for more information.`,
694
+ );
695
+ }
696
+
697
+ statements.push(importExpression);
698
+ }
699
+
700
+ return statements;
701
+ }
702
+
703
+ export async function output(
704
+ scope: ProgramScope,
705
+ key: string,
706
+ _id: string,
707
+ item: Output,
708
+ _graph: DirectedGraph,
709
+ ) {
710
+ const [{ value, description, sensitive }] = item;
711
+
712
+ return asExpression(
713
+ scope,
714
+ "cdktn.TerraformOutput",
715
+ key,
716
+ {
717
+ value,
718
+ description,
719
+ sensitive,
720
+ },
721
+ false,
722
+ false,
723
+ undefined,
724
+ undefined,
725
+ );
726
+ }
727
+
728
+ export async function variableTypeToAst(
729
+ scope: ProgramScope,
730
+ type: string,
731
+ ): Promise<t.Expression> {
732
+ const addVariableTypeToImports = () =>
733
+ scope.importables.push({
734
+ constructName: "VariableType",
735
+ provider: "cdktn",
736
+ });
737
+
738
+ function parsedTypeToAst(type: tex.ExpressionType): t.Expression {
739
+ if (tex.isScopeTraversalExpression(type)) {
740
+ addVariableTypeToImports();
741
+ switch (type.meta.value) {
742
+ case "string":
743
+ return t.identifier("VariableType.STRING");
744
+ case "number":
745
+ return t.identifier("VariableType.NUMBER");
746
+ case "bool":
747
+ return t.identifier("VariableType.BOOL");
748
+ case "any":
749
+ default:
750
+ return t.identifier("VariableType.ANY");
751
+ }
752
+ }
753
+
754
+ if (tex.isFunctionCallExpression(type)) {
755
+ addVariableTypeToImports();
756
+ switch (type.meta.name) {
757
+ case "list":
758
+ case "set":
759
+ case "map":
760
+ case "tuple":
761
+ case "object":
762
+ return t.callExpression(
763
+ t.identifier(`VariableType.${type.meta.name}`),
764
+ type.children.map((child) => parsedTypeToAst(child)),
765
+ );
766
+ }
767
+ }
768
+
769
+ if (tex.isObjectExpression(type)) {
770
+ return t.objectExpression(
771
+ Object.entries(type.meta.items).map(([key, value]) =>
772
+ t.objectProperty(
773
+ t.stringLiteral(key),
774
+ // This does not deal with complex types nested within objects
775
+ // If such a type is found it will result in an Any type
776
+ // e.g. { foo: list(string) } will result in { foo: any }
777
+ parsedTypeToAst({
778
+ type: "scopeTraversal",
779
+ meta: { value },
780
+ } as any),
781
+ ),
782
+ ),
783
+ );
784
+ }
785
+
786
+ addVariableTypeToImports();
787
+ return t.identifier("VariableType.ANY");
788
+ }
789
+
790
+ return parsedTypeToAst(await expressionAst(type));
791
+ }
792
+
793
+ export async function variable(
794
+ scope: ProgramScope,
795
+ key: string,
796
+ id: string,
797
+ item: Variable,
798
+ graph: DirectedGraph,
799
+ ) {
800
+ const [{ type, ...props }] = item;
801
+
802
+ if (!getReference(graph, id)) {
803
+ return [];
804
+ }
805
+
806
+ return asExpression(
807
+ scope,
808
+ id,
809
+ key,
810
+ { ...props, type: type ? await variableTypeToAst(scope, type) : undefined },
811
+ false,
812
+ false,
813
+ getReference(graph, id),
814
+ undefined,
815
+ );
816
+ }
817
+
818
+ export async function local(
819
+ scope: ProgramScope,
820
+ key: string,
821
+ id: string,
822
+ item: TerraformResourceBlock,
823
+ graph: DirectedGraph,
824
+ ): Promise<t.VariableDeclaration[]> {
825
+ logger.debug(`Initializing local resource ${key} with id ${id}`);
826
+ if (!getReference(graph, id)) {
827
+ logger.debug(`No reference found for ${key}`);
828
+ return [];
829
+ }
830
+ return [
831
+ t.variableDeclaration("const", [
832
+ t.variableDeclarator(
833
+ t.identifier(variableName(scope, "local", key)),
834
+ await valueToTs(scope, item, "path-for-local-blocks-can-be-ignored"),
835
+ ),
836
+ ]),
837
+ ];
838
+ }
839
+
840
+ export async function modules(
841
+ scope: ProgramScope,
842
+ key: string,
843
+ id: string,
844
+ item: Module,
845
+ graph: DirectedGraph,
846
+ ) {
847
+ const [{ source, version: _version, ...props }] = item;
848
+
849
+ const moduleConstraint = new TerraformModuleConstraint(source);
850
+
851
+ return asExpression(
852
+ scope,
853
+ moduleConstraint.className,
854
+ key,
855
+ props,
856
+ true,
857
+ false,
858
+ getReference(graph, id),
859
+ undefined,
860
+ );
861
+ }
862
+
863
+ export async function imports(
864
+ scope: ProgramScope,
865
+ _id: string,
866
+ item: Import,
867
+ graph: DirectedGraph,
868
+ ) {
869
+ // Move from ${aws_instance.example} to aws_instance.example
870
+ const target =
871
+ item.to.startsWith("${") && item.to.endsWith("}")
872
+ ? item.to.substring(2, item.to.length - 1)
873
+ : item.to;
874
+
875
+ // Check if the import goes into a module
876
+ if (target.startsWith("module.")) {
877
+ return [
878
+ t.addComment(
879
+ t.emptyStatement(),
880
+ "leading",
881
+ `CDKTN does not support imports into modules yet, please remove the import block importing ${item.id} into ${target} from your configuration`,
882
+ ),
883
+ ];
884
+ }
885
+
886
+ // We now know that the import goes into a resource, e.g. aws_instance.example
887
+ const [resourceTypeIdentifier, resourceName] = target.split(".");
888
+ if (resourceName.includes("[")) {
889
+ return [
890
+ t.addComment(
891
+ t.emptyStatement(),
892
+ "leading",
893
+ `CDKTN does not support imports into resources with count or for_each yet, please remove the import block importing ${item.id} into ${target} from your configuration`,
894
+ ),
895
+ ];
896
+ }
897
+
898
+ // Check if we have a existing resource config with the given name
899
+ if (graph.hasNode(target)) {
900
+ // We will handle this case in the resource function
901
+ // so we can skip over it
902
+ return [];
903
+ }
904
+
905
+ const [provider, ...resourceTypeNameParts] =
906
+ resourceTypeIdentifier.split("_");
907
+
908
+ const constructId = uniqueId(scope.constructs, camelCase(resourceName));
909
+ const constructClass = constructAst(
910
+ scope,
911
+ `${provider}.${resourceTypeNameParts.join("_")}`,
912
+ false,
913
+ );
914
+ return [
915
+ t.expressionStatement(
916
+ t.callExpression(
917
+ t.memberExpression(
918
+ constructClass,
919
+ t.identifier("generateConfigForImport"),
920
+ ),
921
+ [
922
+ t.thisExpression(),
923
+ t.stringLiteral(constructId),
924
+ t.stringLiteral(item.id),
925
+ ],
926
+ ),
927
+ ),
928
+ ];
929
+ }
930
+
931
+ export async function provider(
932
+ scope: ProgramScope,
933
+ key: string,
934
+ id: string,
935
+ item: Provider[0],
936
+ graph: DirectedGraph,
937
+ ) {
938
+ const { version: _version, ...props } = item;
939
+
940
+ const importKey = key === "null" ? "NullProvider" : key;
941
+
942
+ return asExpression(
943
+ scope,
944
+ `${importKey}.${pascalCase(key)}Provider`,
945
+ key,
946
+ props,
947
+ false,
948
+ true,
949
+ getReference(graph, id),
950
+ undefined,
951
+ );
952
+ }
953
+
954
+ export const cdktfImport = template(
955
+ `import * as cdktn from "cdktn"`,
956
+ )() as t.Statement;
957
+
958
+ export const constructsImport = template(
959
+ `import * as constructs from "constructs"`,
960
+ )() as t.Statement;
961
+
962
+ export const providerImports = (providers: string[]) =>
963
+ providers.map((providerName) => {
964
+ const parts = providerName.split("/");
965
+ const name = parts.length > 1 ? parts[1] : parts[0];
966
+ const importName = name === "null" ? "NullProvider" : name;
967
+ return template(
968
+ `import * as ${importName} from "./.gen/providers/${name.replace(
969
+ "./",
970
+ "",
971
+ )}"`,
972
+ )() as t.Statement;
973
+ });
974
+
975
+ export const moduleImports = (modules: Record<string, Module> | undefined) => {
976
+ const uniqueModules = new Set<string>();
977
+ Object.values(modules || {}).map(([module]) =>
978
+ uniqueModules.add(module.source),
979
+ );
980
+
981
+ const imports: t.Statement[] = [];
982
+ uniqueModules.forEach((m) => {
983
+ const moduleConstraint = new TerraformModuleConstraint(m);
984
+ imports.push(
985
+ template.ast(
986
+ `import * as ${moduleConstraint.className} from "./.gen/modules/${moduleConstraint.fileName}"`,
987
+ ) as t.Statement,
988
+ );
989
+ });
990
+ return imports;
991
+ };
992
+
993
+ export async function gen(statements: t.Statement[]) {
994
+ logger.debug(`Generating code for ${JSON.stringify(statements, null, 2)}`);
995
+ const code = prettier.format(generate(t.program(statements) as any).code, {
996
+ parser: "babel",
997
+ });
998
+
999
+ logger.debug(`Generated code:\n${code}`);
1000
+
1001
+ return code;
1002
+ }
1003
+
1004
+ export function addImportForCodeContainer(
1005
+ scope: ProgramScope,
1006
+ codeContainer: string,
1007
+ ) {
1008
+ switch (codeContainer) {
1009
+ case "constructs.Construct":
1010
+ scope.importables.push({
1011
+ provider: "constructs",
1012
+ constructName: "Construct",
1013
+ });
1014
+ break;
1015
+
1016
+ case "cdktn.TerraformStack":
1017
+ scope.importables.push({
1018
+ provider: "cdktn",
1019
+ constructName: "TerraformStack",
1020
+ });
1021
+ break;
1022
+
1023
+ case "cdktf.TerraformStack":
1024
+ scope.importables.push({
1025
+ provider: "cdktf",
1026
+ constructName: "TerraformStack",
1027
+ });
1028
+ break;
1029
+ default:
1030
+ throw Errors.Internal("Unsupported code container: " + codeContainer);
1031
+ }
1032
+ }
1033
+
1034
+ export function wrapCodeInConstructor(
1035
+ codeContainer: string,
1036
+ code: t.Statement[],
1037
+ className: string,
1038
+ configTypeName?: string,
1039
+ ): t.Statement {
1040
+ let baseContainerClass: t.Identifier;
1041
+ switch (codeContainer) {
1042
+ case "constructs.Construct":
1043
+ baseContainerClass = t.identifier("Construct");
1044
+ break;
1045
+
1046
+ case "cdktn.TerraformStack":
1047
+ baseContainerClass = t.identifier("TerraformStack");
1048
+ break;
1049
+ default:
1050
+ throw Errors.Internal("Unsupported code container: " + codeContainer);
1051
+ }
1052
+ if (configTypeName) {
1053
+ return template.statement(
1054
+ `
1055
+ class %%className%% extends %%base%% {
1056
+ constructor(scope: Construct, name: string, config: ${configTypeName}) {
1057
+ super(scope, name);
1058
+ %%code%%
1059
+ }
1060
+ }
1061
+ `,
1062
+ { syntacticPlaceholders: true, plugins: ["typescript"] },
1063
+ )({
1064
+ code,
1065
+ base: baseContainerClass,
1066
+ className: t.identifier(className),
1067
+ }) as t.Statement;
1068
+ }
1069
+ return template.statement(
1070
+ `
1071
+ class %%className%% extends %%base%% {
1072
+ constructor(scope: Construct, name: string) {
1073
+ super(scope, name);
1074
+ %%code%%
1075
+ }
1076
+ }
1077
+ `,
1078
+ { syntacticPlaceholders: true, plugins: ["typescript"] },
1079
+ )({
1080
+ code,
1081
+ base: baseContainerClass,
1082
+ className: t.identifier(className),
1083
+ }) as t.Statement;
1084
+ }
1085
+
1086
+ export const providerConstructImports = (importable: ImportableConstruct[]) => {
1087
+ let provider = importable[0].provider;
1088
+ let namespace = importable[0].namespace;
1089
+ const names = importable.map((i) => i.constructName);
1090
+
1091
+ if (provider === "cdktn" || provider === "constructs") {
1092
+ return template(
1093
+ `import { ${names.join(", ")} } from "${provider}"`,
1094
+ )() as t.Statement;
1095
+ }
1096
+
1097
+ if (namespace) {
1098
+ namespace = snakeCase(namespace).replace(/_/g, "-");
1099
+ }
1100
+
1101
+ // Special cases to undo provider names that we override
1102
+ if (provider === "NullProvider") {
1103
+ provider = "null";
1104
+ }
1105
+
1106
+ return template(
1107
+ `import { ${names.join(
1108
+ ", ",
1109
+ )} } from "./.gen/providers/${provider}/${namespace}"`,
1110
+ )() as t.Statement;
1111
+ };
1112
+
1113
+ export function buildImports(importables: ImportableConstruct[]) {
1114
+ const groupedImportables = importables.reduce(
1115
+ (acc, importable) => {
1116
+ const ns = importable.namespace || "";
1117
+ // Doing some hacky ordering of the imports to make them look a bit nicer
1118
+ const prefix =
1119
+ importable.provider === "constructs"
1120
+ ? "1"
1121
+ : importable.provider === "cdktn"
1122
+ ? "2"
1123
+ : importable.provider === "cdktf"
1124
+ ? "3"
1125
+ : "4";
1126
+ const groupName = `${prefix}.${importable.provider}.${ns}`;
1127
+ const fullName = `${importable.provider}.${ns}.${importable.constructName}`;
1128
+
1129
+ if (acc[groupName]) {
1130
+ const existsAlready = acc[groupName].some(
1131
+ (importable) =>
1132
+ `${importable.provider}.${ns}.${importable.constructName}` ===
1133
+ fullName,
1134
+ );
1135
+ if (existsAlready) {
1136
+ return acc;
1137
+ }
1138
+ acc[groupName].push(importable);
1139
+ acc[groupName].sort();
1140
+ } else {
1141
+ acc[groupName] = [importable];
1142
+ }
1143
+
1144
+ return acc;
1145
+ },
1146
+ {} as Record<string, ImportableConstruct[]>,
1147
+ );
1148
+
1149
+ let commentAdded = false;
1150
+ const constructImports = Object.keys(groupedImportables)
1151
+ .sort()
1152
+ .map((groupName) => {
1153
+ const importStatement = providerConstructImports(
1154
+ groupedImportables[groupName],
1155
+ );
1156
+
1157
+ if (groupName.startsWith("4.") && !commentAdded) {
1158
+ commentAdded = true;
1159
+ t.addComment(
1160
+ importStatement,
1161
+ "leading",
1162
+ `\n* Provider bindings are generated by running \`cdktn get\`.
1163
+ * See https://cdktn.io/docs/concepts/providers#import-providers for more details.\n`,
1164
+ );
1165
+ }
1166
+ return importStatement;
1167
+ });
1168
+
1169
+ return constructImports;
1170
+ }
1171
+
1172
+ export function generateConfigType(
1173
+ name: string,
1174
+ config: Record<string, AttributePath>,
1175
+ ): t.Statement {
1176
+ return t.tsInterfaceDeclaration(
1177
+ t.identifier(name),
1178
+ undefined,
1179
+ undefined,
1180
+ t.tsInterfaceBody(
1181
+ Object.entries(config).map(([key, _value]) =>
1182
+ t.tsPropertySignature(
1183
+ t.identifier(key),
1184
+ t.tSTypeAnnotation(t.tsAnyKeyword()), // TODO: Try to make this better than any
1185
+ ),
1186
+ ),
1187
+ ),
1188
+ );
1189
+ }