@cdktn/hcl2cdk 0.24.0-pre.43 → 0.24.0-pre.47

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
package/src/index.ts ADDED
@@ -0,0 +1,584 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import { parse } from "@cdktn/hcl2json";
4
+ import {
5
+ isRegistryModule,
6
+ TerraformProviderGenerator,
7
+ CodeMaker,
8
+ } from "@cdktn/provider-generator";
9
+
10
+ import * as t from "@babel/types";
11
+ import prettier from "prettier";
12
+ import * as path from "path";
13
+ import * as glob from "glob";
14
+ import * as fs from "fs";
15
+ import { DirectedGraph } from "graphology";
16
+ import * as rosetta from "jsii-rosetta";
17
+ import * as z from "zod";
18
+
19
+ import { schema } from "./schema";
20
+ import { findUsedReferences } from "./references";
21
+ import {
22
+ backendToExpression,
23
+ gen,
24
+ local,
25
+ moduleImports,
26
+ modules,
27
+ output,
28
+ provider,
29
+ resource,
30
+ variable,
31
+ wrapCodeInConstructor,
32
+ addImportForCodeContainer,
33
+ buildImports,
34
+ generateConfigType,
35
+ imports,
36
+ } from "./generation";
37
+ import { TerraformResourceBlock, ProgramScope } from "./types";
38
+ import {
39
+ forEachProvider,
40
+ forEachGlobal,
41
+ forEachNamespaced,
42
+ resourceStats,
43
+ } from "./iteration";
44
+ import { getProviderRequirements } from "./provider";
45
+ import { logger } from "./utils";
46
+ import { FQPN } from "@cdktn/provider-schema";
47
+ import { attributeNameToCdktfName } from "./generation";
48
+ import {
49
+ replaceCsharpImports,
50
+ replaceGoImports,
51
+ replaceJavaImports,
52
+ replacePythonImports,
53
+ } from "./jsii-rosetta-workarounds";
54
+ import { ProviderSchema } from "@cdktn/commons";
55
+ import { forEachImport } from "./iteration";
56
+
57
+ export const CODE_MARKER = "// define resources here";
58
+
59
+ export async function getParsedHcl(hcl: string) {
60
+ logger.debug(`Parsing HCL: ${hcl}`);
61
+ // Get the JSON representation of the HCL
62
+ let json: Record<string, unknown>;
63
+ try {
64
+ json = await parse("terraform.tf", hcl);
65
+ } catch (err) {
66
+ logger.error(`Failed to parse HCL: ${err}`);
67
+ throw new Error(
68
+ `Error: Could not parse HCL, this means either that the HCL passed is invalid or that you found a bug. If the HCL seems valid, please file a bug under https://github.com/open-constructs/cdk-terrain/issues/new?assignees=&labels=bug%2C+new%2C+feature%2Fconvert&template=bug-report.yml&title=`,
69
+ );
70
+ }
71
+
72
+ // Ensure the JSON representation matches the expected structure
73
+ let plan: z.infer<typeof schema>;
74
+ try {
75
+ plan = schema.parse(json);
76
+ } catch (err) {
77
+ throw new Error(`Error: HCL-JSON does not conform to schema. This is not expected, please file a bug under https://github.com/open-constructs/cdk-terrain/issues/new?assignees=&labels=bug%2C+new%2C+feature%2Fconvert&template=bug-report.yml&title=
78
+ Please include this information:
79
+ ${JSON.stringify((err as z.ZodError).errors)}`);
80
+ }
81
+
82
+ return plan;
83
+ }
84
+
85
+ export async function parseProviderRequirements(hcl: string) {
86
+ logger.debug("Parsing provider requirements");
87
+ const plan = await getParsedHcl(hcl);
88
+ return getProviderRequirements(plan);
89
+ }
90
+
91
+ export async function convertToTypescript(
92
+ hcl: string,
93
+ providerSchema: ProviderSchema,
94
+ codeContainer: string,
95
+ ) {
96
+ logger.debug("Converting to typescript");
97
+ const plan = await getParsedHcl(hcl);
98
+
99
+ // Each key in the scope needs to be unique, therefore we save them in a set
100
+ // Each variable needs to be unique as well, we save them in a record so we can identify if two variables are the same
101
+ const scope: ProgramScope = {
102
+ providerSchema,
103
+ providerGenerator: Object.keys(
104
+ providerSchema.provider_schemas || {},
105
+ ).reduce((carry, fqpn) => {
106
+ const providerGenerator = new TerraformProviderGenerator(
107
+ new CodeMaker(),
108
+ providerSchema,
109
+ );
110
+ providerGenerator.buildResourceModels(fqpn as FQPN); // can't use that type on the keys yet, since we are not on TS >=4.4 yet :sadcat:
111
+ return { ...carry, [fqpn]: providerGenerator };
112
+ }, {}),
113
+ constructs: new Set<string>(),
114
+ variables: {},
115
+ hasTokenBasedTypeCoercion: false,
116
+ nodeIds: [],
117
+ importables: [],
118
+ topLevelConfig: {},
119
+ };
120
+
121
+ const graph = new DirectedGraph<{
122
+ code: (
123
+ g: DirectedGraph<any>,
124
+ ) => Promise<Array<t.Statement | t.VariableDeclaration>>;
125
+ }>();
126
+
127
+ // Get all items in the JSON as a map of id to function that generates the AST
128
+ // We will use this to construct the nodes for a dependency graph
129
+ // We need to use a function here because the same node has different representation based on if it's referenced by another one
130
+ const nodeMap: Record<
131
+ string,
132
+ {
133
+ code: (
134
+ g: typeof graph,
135
+ ) => Promise<Array<t.Statement | t.VariableDeclaration>>;
136
+ value: unknown;
137
+ }
138
+ > = {
139
+ ...forEachProvider(scope, plan.provider, provider),
140
+ ...forEachGlobal(scope, "var", plan.variable, variable),
141
+ // locals are a special case
142
+ ...forEachGlobal(
143
+ scope,
144
+ "local",
145
+ Array.isArray(plan.locals)
146
+ ? plan.locals.reduce((carry, locals) => ({ ...carry, ...locals }), {})
147
+ : {},
148
+ local,
149
+ ),
150
+ ...forEachGlobal(scope, "out", plan.output, output),
151
+ ...forEachGlobal(scope, "module", plan.module, modules),
152
+ ...forEachImport(scope, "import", plan.import, imports),
153
+ ...forEachNamespaced(scope, plan.resource, resource),
154
+ ...forEachNamespaced(scope, plan.data, resource, "data"),
155
+ };
156
+
157
+ // Add all nodes to the dependency graph so we can detect if an edge is added for an unknown link
158
+ Object.entries(nodeMap).forEach(([key, value]) => {
159
+ logger.debug(`Adding node '${key}' to graph`);
160
+ graph.addNode(key, value);
161
+ });
162
+
163
+ // Finding references becomes easier of the to be referenced ids are already known
164
+ const nodeIds = Object.keys(nodeMap);
165
+ scope.nodeIds = nodeIds;
166
+ async function addEdges(id: string, value: TerraformResourceBlock) {
167
+ (await findUsedReferences(nodeIds, value)).forEach((ref) => {
168
+ if (
169
+ !graph.hasDirectedEdge(ref.referencee.id, id) &&
170
+ graph.hasNode(ref.referencee.id) // in case the referencee is a dynamic variable
171
+ ) {
172
+ if (!graph.hasNode(id)) {
173
+ throw new Error(
174
+ `The dependency graph is expected to link from ${
175
+ ref.referencee.id
176
+ } to ${id} but ${id} does not exist.
177
+ These nodes exist: ${graph.nodes().join("\n")}`,
178
+ );
179
+ }
180
+
181
+ // The graph should have no self-references
182
+ if (id === ref.referencee.id) {
183
+ logger.debug(`Skipping self-reference for ${id}`);
184
+ return;
185
+ }
186
+
187
+ logger.debug(`Adding edge from ${ref.referencee.id} to ${id}`);
188
+ graph.addDirectedEdge(ref.referencee.id, id, { ref });
189
+ }
190
+ });
191
+ }
192
+
193
+ // We recursively inspect each resource value to find references to other values
194
+ // We add these to a dependency graph so that the programming code has the right order
195
+ async function addGlobalEdges(
196
+ _scope: ProgramScope,
197
+ _key: string,
198
+ id: string,
199
+ value: TerraformResourceBlock,
200
+ ) {
201
+ await addEdges(id, value);
202
+ }
203
+ async function addProviderEdges(
204
+ _scope: ProgramScope,
205
+ _key: string,
206
+ id: string,
207
+ value: TerraformResourceBlock,
208
+ ) {
209
+ await addEdges(id, value);
210
+ }
211
+ async function addNamespacedEdges(
212
+ _scope: ProgramScope,
213
+ _type: string,
214
+ _key: string,
215
+ id: string,
216
+ value: TerraformResourceBlock,
217
+ ) {
218
+ await addEdges(id, value);
219
+ }
220
+
221
+ await Promise.all(
222
+ Object.values({
223
+ ...forEachProvider(scope, plan.provider, addProviderEdges),
224
+ ...forEachGlobal(scope, "var", plan.variable, addGlobalEdges),
225
+ // locals are a special case
226
+ ...forEachGlobal(
227
+ scope,
228
+ "local",
229
+ Array.isArray(plan.locals)
230
+ ? plan.locals.reduce((carry, locals) => ({ ...carry, ...locals }), {})
231
+ : {},
232
+ addGlobalEdges,
233
+ ),
234
+ ...forEachGlobal(scope, "out", plan.output, addGlobalEdges),
235
+ ...forEachGlobal(scope, "module", plan.module, addGlobalEdges),
236
+ ...forEachNamespaced(scope, plan.resource, addNamespacedEdges),
237
+ ...forEachNamespaced(scope, plan.data, addNamespacedEdges, "data"),
238
+ }).map(({ code: addEdgesToGraph }) => addEdgesToGraph(graph)),
239
+ );
240
+
241
+ logger.debug(`Graph: ${JSON.stringify(graph, null, 2)}`);
242
+ logger.debug(`Starting to assemble the typescript code`);
243
+ // We traverse the dependency graph to get the unordered JSON nodes into an ordered array
244
+ // where no node is referenced before it's defined
245
+ // As we check that the nodes on both ends of an edge exist we can be sure
246
+ // that no infinite loop exists, there can be no stray dependency on a node
247
+ const expressions: t.Statement[] = [];
248
+ let nodesToVisit = [...nodeIds];
249
+ // This ensures we detect cycles and don't end up in an endless loop
250
+ let nodesVisitedThisIteration = 0;
251
+ do {
252
+ nodesVisitedThisIteration = 0;
253
+
254
+ // Find next nodes to visit
255
+ const nodeExpressionGenerators = graph.mapNodes((nodeId, { code }) => {
256
+ if (!nodesToVisit.includes(nodeId)) {
257
+ return undefined;
258
+ }
259
+
260
+ const unresolvedDependencies = graph
261
+ .inNeighbors(nodeId)
262
+ .filter((item) => nodesToVisit.includes(item));
263
+
264
+ if (unresolvedDependencies.length === 0) {
265
+ nodesToVisit = nodesToVisit.filter((id) => nodeId !== id);
266
+ nodesVisitedThisIteration = nodesVisitedThisIteration + 1;
267
+
268
+ logger.debug(`Visiting node ${nodeId}`);
269
+ return code;
270
+ }
271
+ return undefined;
272
+ });
273
+
274
+ // Generate the code for the nodes
275
+ for (const code of nodeExpressionGenerators) {
276
+ if (code) {
277
+ expressions.push(...(await code(graph)));
278
+ }
279
+ }
280
+
281
+ logger.debug(
282
+ `${nodesToVisit.length} unvisited nodes: ${nodesToVisit.join(", ")}`,
283
+ );
284
+ } while (nodesToVisit.length > 0 && nodesVisitedThisIteration != 0);
285
+
286
+ if (nodesToVisit.length > 0) {
287
+ throw new Error(
288
+ `There are ${
289
+ nodesToVisit.length
290
+ } terraform elements that could not be visited.
291
+ This is likely due to a cycle in the dependency graph.
292
+ These nodes are: ${nodesToVisit.join(", ")}`,
293
+ );
294
+ }
295
+
296
+ logger.debug(
297
+ `${nodesToVisit.length} unvisited nodes: ${nodesToVisit.join(", ")}`,
298
+ );
299
+
300
+ const backendExpressions = (
301
+ await Promise.all(
302
+ plan.terraform?.map((terraform) =>
303
+ backendToExpression(scope, terraform.backend),
304
+ ) || [Promise.resolve([])],
305
+ )
306
+ ).reduce((carry, item) => [...carry, ...item], []);
307
+
308
+ logger.debug(
309
+ `Using these backend expressions: ${JSON.stringify(
310
+ backendExpressions,
311
+ null,
312
+ 2,
313
+ )}`,
314
+ );
315
+
316
+ // We collect all module sources
317
+ const moduleRequirements = [
318
+ ...new Set(
319
+ Object.values(plan.module || {}).reduce(
320
+ (carry, moduleBlock) => [
321
+ ...carry,
322
+ ...moduleBlock.reduce(
323
+ (arr, { source, version }) => [
324
+ ...arr,
325
+ version ? `${source}@${version}` : source,
326
+ ],
327
+ [] as string[],
328
+ ),
329
+ ],
330
+ [] as string[],
331
+ ) || [],
332
+ ),
333
+ ];
334
+
335
+ logger.debug(
336
+ `Found these modules: ${JSON.stringify(moduleRequirements, null, 2)}`,
337
+ );
338
+
339
+ if (Object.keys(plan.variable || {}).length > 0 && expressions.length > 0) {
340
+ expressions[0] = t.addComment(
341
+ expressions[0],
342
+ "leading",
343
+ `Terraform Variables are not always the best fit for getting inputs in the context of Terraform CDK.
344
+ You can read more about this at https://cdktn.io/docs/concepts/variables-and-outputs#input-variables`,
345
+ );
346
+ }
347
+
348
+ const providerRequirements = getProviderRequirements(plan);
349
+ logger.debug(
350
+ `Found these provider requirements: ${JSON.stringify(
351
+ providerRequirements,
352
+ null,
353
+ 2,
354
+ )}`,
355
+ );
356
+
357
+ // We add a comment if there are providers with missing schema information
358
+ const providersLackingSchema = Object.keys(providerRequirements).filter(
359
+ (providerName) =>
360
+ providerName !== "terraform" &&
361
+ !Object.keys(providerSchema.provider_schemas || {}).some((schemaName) =>
362
+ schemaName.endsWith(providerName),
363
+ ),
364
+ );
365
+ logger.debug(
366
+ `${
367
+ providersLackingSchema.length
368
+ } providers lack schema information: ${providersLackingSchema.join(", ")}`,
369
+ );
370
+
371
+ if (providersLackingSchema.length > 0) {
372
+ expressions[0] = t.addComment(
373
+ expressions[0],
374
+ "leading",
375
+ `The following providers are missing schema information and might need manual adjustments to synthesize correctly: ${providersLackingSchema.join(
376
+ ", ",
377
+ )}.
378
+ For a more precise conversion please use the --provider flag in convert.`,
379
+ );
380
+ }
381
+
382
+ // Always add constructs
383
+ scope.importables.push({
384
+ constructName: "Construct",
385
+ provider: "constructs",
386
+ });
387
+
388
+ if (scope.hasTokenBasedTypeCoercion) {
389
+ scope.importables.push({
390
+ constructName: "Token",
391
+ provider: "cdktn",
392
+ });
393
+ }
394
+
395
+ // Add specific import for codeContainer
396
+ addImportForCodeContainer(scope, codeContainer);
397
+ const constructImports = buildImports(scope.importables);
398
+
399
+ const code = [...(backendExpressions || []), ...expressions];
400
+ const configTypeName =
401
+ Object.keys(scope.topLevelConfig).length > 0 ? "MyConfig" : undefined;
402
+
403
+ const classConfig = configTypeName
404
+ ? [generateConfigType(configTypeName, scope.topLevelConfig)]
405
+ : [];
406
+
407
+ // We split up the generated code so that users can have more control over what to insert where
408
+ return {
409
+ // TODO: Remove imports and code because rosetta won't be able to translate them
410
+ all: await gen([
411
+ ...constructImports,
412
+ ...moduleImports(plan.module),
413
+ ...classConfig,
414
+ wrapCodeInConstructor(
415
+ codeContainer,
416
+ code,
417
+ "MyConvertedCode",
418
+ configTypeName,
419
+ ),
420
+ ]),
421
+ imports: await gen([...constructImports, ...moduleImports(plan.module)]),
422
+ code: await gen(code),
423
+ providers: Object.entries(providerRequirements).map(([source, version]) =>
424
+ version === "*" ? source : `${source}@${version}`,
425
+ ),
426
+ modules: moduleRequirements,
427
+ // We track some usage data to make it easier to understand what is used
428
+ stats: {
429
+ numberOfModules: moduleRequirements.length,
430
+ numberOfProviders: Object.keys(providerRequirements).length,
431
+ resources: resourceStats(plan.resource || {}),
432
+ data: resourceStats(plan.data || {}),
433
+ convertedLines: hcl.split("\n").length,
434
+ },
435
+ };
436
+ }
437
+
438
+ type File = { contents: string; fileName: string };
439
+ const translators = {
440
+ python: {
441
+ visitor: () => new rosetta.PythonVisitor(),
442
+ postTranslationMutation: replacePythonImports,
443
+ },
444
+ java: {
445
+ visitor: () => new rosetta.JavaVisitor(),
446
+ postTranslationMutation: replaceJavaImports,
447
+ },
448
+ csharp: {
449
+ visitor: () => new rosetta.CSharpVisitor(),
450
+ postTranslationMutation: replaceCsharpImports,
451
+ },
452
+ go: {
453
+ visitor: () => new rosetta.GoVisitor(),
454
+ postTranslationMutation: replaceGoImports,
455
+ },
456
+ };
457
+
458
+ function translatorForLanguage(language: keyof typeof translators) {
459
+ return (file: File, throwOnTranslationError: boolean) => {
460
+ const { visitor, postTranslationMutation } = translators[language];
461
+ const { translation, diagnostics } = rosetta.translateTypeScript(
462
+ file,
463
+ visitor(),
464
+ throwOnTranslationError ? { includeCompilerDiagnostics: true } : {},
465
+ );
466
+
467
+ if (
468
+ throwOnTranslationError &&
469
+ diagnostics.filter((diag) => diag.isError).length > 0
470
+ ) {
471
+ logger.debug(`Could not translate TS to ${language}:\n${file.contents}`);
472
+ throw new Error(
473
+ `Could not translate TS to ${language}: ${diagnostics
474
+ .map((diag) => diag.formattedMessage)
475
+ .join("\n")}`,
476
+ );
477
+ }
478
+
479
+ return postTranslationMutation(translation);
480
+ };
481
+ }
482
+
483
+ type ConvertOptions = {
484
+ /**
485
+ * The language to convert to
486
+ */
487
+ language: keyof typeof translators | "typescript";
488
+ /**
489
+ * The provider schema to use for conversion
490
+ */
491
+ providerSchema: ProviderSchema;
492
+ /**
493
+ * The base class to extend from. Defaults to `constructs.Construct`
494
+ */
495
+ codeContainer?: string;
496
+ /**
497
+ * Whether to throw an error if the translation fails
498
+ * Defaults to false
499
+ */
500
+ throwOnTranslationError?: boolean;
501
+ };
502
+
503
+ export async function convert(
504
+ hcl: string,
505
+ {
506
+ language,
507
+ providerSchema,
508
+ throwOnTranslationError = false,
509
+ codeContainer = "cdktn.TerraformStack",
510
+ }: ConvertOptions,
511
+ ) {
512
+ const fileName = "terraform.tf";
513
+ const translater =
514
+ language === "typescript"
515
+ ? (file: File, _throwOnTranslationError: boolean) => file.contents
516
+ : translatorForLanguage(language);
517
+
518
+ if (!translater) {
519
+ throw new Error("Unsupported language used: " + language);
520
+ }
521
+
522
+ const tsCode = await convertToTypescript(hcl, providerSchema, codeContainer);
523
+
524
+ return {
525
+ ...tsCode,
526
+ all: translater(
527
+ { fileName, contents: tsCode.all },
528
+ throwOnTranslationError,
529
+ ),
530
+ imports: translater({ fileName, contents: tsCode.imports }, false),
531
+ code: translater({ fileName, contents: tsCode.code }, false),
532
+ stats: { ...tsCode.stats, language },
533
+ };
534
+ }
535
+
536
+ export function getTerraformConfigFromDir(importPath: string) {
537
+ const absPath = path.resolve(importPath);
538
+ const fileContents = glob
539
+ .sync("./*.tf", { cwd: absPath })
540
+ .map((p) => fs.readFileSync(path.resolve(absPath, p), "utf8"));
541
+
542
+ return fileContents.join("\n");
543
+ }
544
+
545
+ type CdktfJson = Record<string, unknown> & {
546
+ terraformProviders: any[];
547
+ terraformModules: any[];
548
+ };
549
+ export async function convertProject(
550
+ combinedHcl: string,
551
+ { language, providerSchema }: ConvertOptions,
552
+ ) {
553
+ if (language !== "typescript") {
554
+ throw new Error("Unsupported language used: " + language);
555
+ }
556
+
557
+ const {
558
+ imports,
559
+ code,
560
+ providers,
561
+ modules: tfModules,
562
+ stats,
563
+ } = await convert(combinedHcl, {
564
+ language,
565
+ providerSchema,
566
+ });
567
+
568
+ return {
569
+ code: (inputMainFile: string) => {
570
+ const importMainFile = [imports, inputMainFile].join("\n");
571
+ const outputMainFile = importMainFile.replace(CODE_MARKER, code);
572
+ return prettier.format(outputMainFile, { parser: "babel" });
573
+ },
574
+ cdktfJson: (inputCdktfJson: CdktfJson) => {
575
+ const cdktfJson = { ...inputCdktfJson };
576
+ cdktfJson.terraformProviders = providers;
577
+ cdktfJson.terraformModules = tfModules;
578
+ return cdktfJson;
579
+ },
580
+ stats,
581
+ };
582
+ }
583
+
584
+ export { isRegistryModule, attributeNameToCdktfName };