@cdktn/provider-generator 0.24.0-pre.5 → 0.24.0-pre.50

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 (67) hide show
  1. package/.spec.swcrc +22 -0
  2. package/LICENSE +355 -0
  3. package/README.md +1 -1
  4. package/build/__tests__/edge-provider-schema/cli.js +8 -3
  5. package/build/get/__tests__/generator/import-style.test.d.ts +2 -0
  6. package/build/get/__tests__/generator/import-style.test.js +101 -0
  7. package/build/get/__tests__/generator/module-generator.test.js +12 -12
  8. package/build/get/constructs-maker.d.ts +5 -1
  9. package/build/get/constructs-maker.js +14 -6
  10. package/build/get/generator/emitter/struct-emitter.d.ts +2 -1
  11. package/build/get/generator/emitter/struct-emitter.js +14 -11
  12. package/build/get/generator/module-generator.js +3 -3
  13. package/build/get/generator/provider-generator.d.ts +9 -1
  14. package/build/get/generator/provider-generator.js +9 -6
  15. package/jest.config.js +16 -9
  16. package/package.json +24 -23
  17. package/package.sh +1 -1
  18. package/src/__tests__/__snapshots__/edge-provider-schema.test.ts.snap +8 -8
  19. package/src/__tests__/__snapshots__/provider.test.ts.snap +2951 -2951
  20. package/src/__tests__/edge-provider-schema/builder.ts +185 -0
  21. package/src/__tests__/edge-provider-schema/cli.ts +51 -0
  22. package/src/__tests__/edge-provider-schema/index.ts +161 -0
  23. package/src/__tests__/edge-provider-schema.test.ts +24 -0
  24. package/src/__tests__/provider.test.ts +180 -0
  25. package/src/get/__tests__/constructs-maker.test.ts +118 -0
  26. package/src/get/__tests__/generator/__snapshots__/complex-computed-types.test.ts.snap +5 -5
  27. package/src/get/__tests__/generator/__snapshots__/export-sharding.test.ts.snap +3310 -3310
  28. package/src/get/__tests__/generator/__snapshots__/module-generator.test.ts.snap +355 -355
  29. package/src/get/__tests__/generator/__snapshots__/nested-types.test.ts.snap +8 -8
  30. package/src/get/__tests__/generator/__snapshots__/provider.test.ts.snap +8 -8
  31. package/src/get/__tests__/generator/__snapshots__/resource-types.test.ts.snap +126 -126
  32. package/src/get/__tests__/generator/__snapshots__/skipped-attributes.test.ts.snap +17 -17
  33. package/src/get/__tests__/generator/__snapshots__/types.test.ts.snap +65 -65
  34. package/src/get/__tests__/generator/complex-computed-types.test.ts +28 -0
  35. package/src/get/__tests__/generator/deep-nested-attributes.test.ts +56 -0
  36. package/src/get/__tests__/generator/description-escaping.test.ts +84 -0
  37. package/src/get/__tests__/generator/empty-provider-resources.test.ts +26 -0
  38. package/src/get/__tests__/generator/export-sharding.test.ts +169 -0
  39. package/src/get/__tests__/generator/import-style.test.ts +129 -0
  40. package/src/get/__tests__/generator/module-generator.test.ts +528 -0
  41. package/src/get/__tests__/generator/nested-types.test.ts +54 -0
  42. package/src/get/__tests__/generator/provider.test.ts +51 -0
  43. package/src/get/__tests__/generator/resource-types.test.ts +118 -0
  44. package/src/get/__tests__/generator/skipped-attributes.test.ts +72 -0
  45. package/src/get/__tests__/generator/types.test.ts +611 -0
  46. package/src/get/__tests__/generator/versions-file.test.ts +72 -0
  47. package/src/get/__tests__/util.ts +75 -0
  48. package/src/get/constructs-maker.ts +822 -0
  49. package/src/get/generator/custom-defaults.ts +493 -0
  50. package/src/get/generator/emitter/attributes-emitter.ts +225 -0
  51. package/src/get/generator/emitter/index.ts +5 -0
  52. package/src/get/generator/emitter/resource-emitter.ts +226 -0
  53. package/src/get/generator/emitter/struct-emitter.ts +683 -0
  54. package/src/get/generator/loop-detection.ts +81 -0
  55. package/src/get/generator/models/attribute-model.ts +216 -0
  56. package/src/get/generator/models/attribute-type-model.ts +448 -0
  57. package/src/get/generator/models/index.ts +7 -0
  58. package/src/get/generator/models/resource-model.ts +161 -0
  59. package/src/get/generator/models/scope.ts +54 -0
  60. package/src/get/generator/models/struct.ts +116 -0
  61. package/src/get/generator/module-generator.ts +234 -0
  62. package/src/get/generator/provider-generator.ts +355 -0
  63. package/src/get/generator/resource-parser.ts +762 -0
  64. package/src/get/generator/sanitized-comments.ts +49 -0
  65. package/src/get/generator/skipped-attributes.ts +27 -0
  66. package/src/index.ts +40 -0
  67. package/src/util.ts +26 -0
@@ -0,0 +1,822 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import * as fs from "fs-extra";
4
+ import * as path from "path";
5
+ import { CodeMaker } from "codemaker";
6
+ import { exec, mkdtemp } from "@cdktn/commons";
7
+ import {
8
+ TerraformDependencyConstraint,
9
+ logger,
10
+ logTimespan,
11
+ ConstructsMakerProviderTarget,
12
+ ConstructsMakerModuleTarget,
13
+ ConstructsMakerTarget,
14
+ ProviderSchema,
15
+ ModuleSchema,
16
+ Errors,
17
+ type LanguageOptions,
18
+ } from "@cdktn/commons";
19
+ import { DISPLAY_VERSION, Language } from "@cdktn/commons";
20
+ import { TerraformProviderGenerator } from "./generator/provider-generator";
21
+ import { ModuleGenerator } from "./generator/module-generator";
22
+ import { glob } from "glob";
23
+ import { readSchema } from "@cdktn/provider-schema";
24
+
25
+ const pacmakModule = require.resolve("jsii-pacmak/bin/jsii-pacmak");
26
+ const jsiiModule = require.resolve("jsii/bin/jsii");
27
+
28
+ export interface GenerateJSIIOptions {
29
+ entrypoint: string;
30
+ deps: string[];
31
+ moduleKey: string;
32
+ exports?: Record<string, ExportDefinition | string>;
33
+ jsii?: JsiiOutputOptions;
34
+ python?: PythonOutputOptions;
35
+ java?: JavaOutputOptions;
36
+ csharp?: CSharpOutputOptions;
37
+ golang?: GoLangOutputOptions;
38
+ }
39
+
40
+ export interface JsiiOutputOptions {
41
+ path: string;
42
+ }
43
+
44
+ export interface PythonOutputOptions {
45
+ outdir: string;
46
+ moduleName: string;
47
+ }
48
+
49
+ export interface JavaOutputOptions {
50
+ outdir: string;
51
+ package: string;
52
+ }
53
+
54
+ export interface CSharpOutputOptions {
55
+ outdir: string;
56
+ namespace: string;
57
+ }
58
+
59
+ export interface GoLangOutputOptions {
60
+ outdir: string;
61
+ moduleName: string;
62
+ packageName: string;
63
+ }
64
+
65
+ /**
66
+ * See https://nodejs.org/api/packages.html#conditional-exports for more information
67
+ */
68
+ export interface ExportDefinition {
69
+ node?: string;
70
+ import?: string;
71
+ require?: string;
72
+ default?: string;
73
+ }
74
+
75
+ export async function generateJsiiLanguage(
76
+ code: CodeMaker,
77
+ opts: GenerateJSIIOptions,
78
+ outputPath: string,
79
+ disallowedFileGlobs: string[] = [],
80
+ ) {
81
+ await mkdtemp(async (staging) => {
82
+ // this is not typescript, so we generate in a staging directory and
83
+ // use jsii-srcmak to compile and extract the language-specific source
84
+ // into our project.
85
+ await code.save(staging);
86
+
87
+ // as the above generated the Typescript code for all providers and modules,
88
+ // we need to filter out the ones we don't need so they don't end up in the JSII bundle over and over again.
89
+ const filesToDelete = disallowedFileGlobs.flatMap((pattern) =>
90
+ glob.sync(pattern, { cwd: staging }),
91
+ );
92
+ await Promise.all(
93
+ filesToDelete.map((file) => fs.remove(path.join(staging, file))),
94
+ );
95
+
96
+ // Compile with JSII
97
+ const jsiiArgs = ["--silence-warnings", "reserved-word"];
98
+ const jsiiEntrypoint = opts.entrypoint;
99
+ const basepath = path.join(
100
+ path.dirname(jsiiEntrypoint),
101
+ path.basename(jsiiEntrypoint, ".ts"),
102
+ );
103
+
104
+ const moduleKey = opts.moduleKey.replace(/\./g, "").replace(/\//g, "");
105
+ const moduleDirs = opts.deps;
106
+ const targets: Record<string, any> = {};
107
+ const deps: Record<string, string> = {};
108
+ for (const dir of moduleDirs) {
109
+ // read module metadata
110
+ const metadata = await fs.readJson(path.join(dir, "package.json"));
111
+ const moduleName: string = metadata.name;
112
+ const moduleVersion: string = metadata.version;
113
+
114
+ const targetdir = path.join(
115
+ path.join(staging, "node_modules"),
116
+ moduleName,
117
+ );
118
+ await fs.mkdirp(path.dirname(targetdir));
119
+ await fs.copy(dir, targetdir, { dereference: true });
120
+
121
+ // add to "deps" and "peer deps"
122
+ if (!moduleName.startsWith("@types/")) {
123
+ deps[moduleName] = moduleVersion;
124
+ }
125
+ }
126
+ const pkg = {
127
+ name: moduleKey,
128
+ version: "0.0.0",
129
+ author: "generated@generated.com",
130
+ main: `${basepath}.js`,
131
+ types: `${basepath}.d.ts`,
132
+ license: "UNLICENSED",
133
+ repository: { url: "http://generated", type: "git" },
134
+ jsii: {
135
+ outdir: "dist",
136
+ targets: targets,
137
+ },
138
+ dependencies: deps,
139
+ peerDependencies: deps,
140
+ };
141
+
142
+ if (opts.exports) {
143
+ (pkg as Record<string, any>).exports = opts.exports;
144
+ }
145
+ if (opts.python) {
146
+ targets.python = {
147
+ distName: "generated",
148
+ module: opts.python.moduleName,
149
+ };
150
+ }
151
+
152
+ if (opts.java) {
153
+ targets.java = {
154
+ package: opts.java.package,
155
+ maven: {
156
+ groupId: "generated",
157
+ artifactId: "generated",
158
+ },
159
+ };
160
+ }
161
+
162
+ if (opts.csharp) {
163
+ targets.dotnet = {
164
+ namespace: opts.csharp.namespace,
165
+ packageId: opts.csharp.namespace,
166
+ };
167
+ }
168
+
169
+ if (opts.golang) {
170
+ targets.go = {
171
+ moduleName: opts.golang.moduleName,
172
+ packageName: opts.golang.packageName,
173
+ };
174
+ }
175
+
176
+ await fs.writeFile(
177
+ path.join(staging, "package.json"),
178
+ JSON.stringify(pkg, undefined, 2),
179
+ );
180
+
181
+ const endJsiiTimer = logTimespan("jsii");
182
+ await exec(jsiiModule, jsiiArgs, {
183
+ cwd: staging,
184
+ });
185
+ endJsiiTimer();
186
+
187
+ // extract .jsii if requested
188
+ if (opts.jsii) {
189
+ await fs.copy(path.join(staging, ".jsii"), opts.jsii.path);
190
+ }
191
+
192
+ // run pacmak to generate code
193
+ const endJsiiPacmakTimer = logTimespan("jsii-pacmak");
194
+ await exec(pacmakModule, ["--code-only"], { cwd: staging });
195
+ endJsiiPacmakTimer();
196
+
197
+ if (opts.python) {
198
+ const reldir = opts.python.moduleName.replace(/\./g, "/"); // jsii replaces "." with "/"
199
+ const source = path.resolve(
200
+ path.join(staging, "dist/python/src", reldir),
201
+ );
202
+ const target = path.join(opts.python.outdir, reldir);
203
+ await fs.move(source, target, { overwrite: true });
204
+ }
205
+
206
+ if (opts.java) {
207
+ const source = path.resolve(path.join(staging, "dist/java/src/"));
208
+ const target = path.join(opts.java.outdir, "src/");
209
+ // Pre-create shared package directories.
210
+ const packageDir = opts.java.package.replace(/\./g, "/");
211
+ await fs.mkdirp(path.join(target, "main/java", packageDir));
212
+ await fs.mkdirp(path.join(target, "main/resources", packageDir));
213
+ await fs.copy(source, target, { recursive: true, overwrite: false });
214
+ }
215
+
216
+ if (opts.csharp) {
217
+ const reldir = opts.csharp.namespace;
218
+ const source = path.resolve(path.join(staging, "dist/dotnet/", reldir));
219
+ const target = path.join(opts.csharp.outdir, reldir);
220
+ await fs.move(source, target, { overwrite: true });
221
+ }
222
+
223
+ if (opts.golang) {
224
+ const reldir = opts.golang.packageName;
225
+ const source = path.resolve(path.join(staging, "dist/go/", reldir));
226
+ const target = path.join(opts.golang.outdir, reldir);
227
+ await fs.move(source, target, { overwrite: true });
228
+ // remove go.mod as this would make it a submodule
229
+ await fs.remove(path.join(target, "go.mod"));
230
+ }
231
+
232
+ ["versions.json", "constraints.json"].forEach((file) => {
233
+ try {
234
+ fs.copySync(
235
+ path.resolve(staging, file),
236
+ path.resolve(outputPath, file),
237
+ );
238
+ } catch (e) {
239
+ logger.debug(`Failed to copy ${file}: ${e}`);
240
+ }
241
+ });
242
+ });
243
+ }
244
+
245
+ type ConstraintFile = { providers: Record<string, string>; cdktf: string };
246
+
247
+ export interface GetOptions {
248
+ readonly targetLanguage: Language;
249
+ readonly codeMakerOutput: string;
250
+ readonly jsiiParallelism?: number;
251
+ /**
252
+ * Path to copy the output .jsii file.
253
+ * @default - jsii file is not emitted
254
+ */
255
+ readonly outputJsii?: string;
256
+ /**
257
+ * Language-specific code generation options.
258
+ */
259
+ readonly languageOptions?: LanguageOptions;
260
+ }
261
+
262
+ export class ConstructsMaker {
263
+ private readonly codeMakerOutdir: string;
264
+ private readonly code: CodeMaker;
265
+ private versions: { [providerName: string]: string | undefined };
266
+
267
+ constructor(
268
+ private readonly options: GetOptions,
269
+ private readonly schemaCachePath?: string,
270
+ private readonly reportTelemetry: (payload: {
271
+ targetLanguage: string;
272
+ trackingPayload: Record<string, any>;
273
+ }) => Promise<void> = async () => {},
274
+ ) {
275
+ this.codeMakerOutdir = path.resolve(this.options.codeMakerOutput);
276
+ fs.mkdirpSync(this.codeMakerOutdir);
277
+ this.code = new CodeMaker();
278
+ this.versions = {};
279
+ }
280
+ private async generateTypescriptProvider(
281
+ target: ConstructsMakerProviderTarget,
282
+ schema: ProviderSchema,
283
+ ) {
284
+ const endTSTimer = logTimespan(`Generate Typescript for ${target.name}`);
285
+ const generator = new TerraformProviderGenerator(this.code, schema, {
286
+ importExtension: this.options.languageOptions?.importExtension,
287
+ });
288
+ generator.generate(target);
289
+
290
+ this.versions = { ...this.versions, ...generator.versions };
291
+ endTSTimer();
292
+ }
293
+
294
+ public async filterAlreadyGenerated(
295
+ constraints: TerraformDependencyConstraint[],
296
+ ) {
297
+ let constraintsFile = "{}";
298
+ try {
299
+ constraintsFile = await fs.readFile(
300
+ path.join(this.codeMakerOutdir, "constraints.json"),
301
+ "utf8",
302
+ );
303
+ } catch (e) {
304
+ logger.debug(
305
+ `Could not find constraints.json file while filtering: ${e}. This means no providers were generated, so all constraints need to be generated.`,
306
+ );
307
+ return constraints;
308
+ }
309
+ logger.debug(`Found constraints.json file: ${constraintsFile}`);
310
+
311
+ let previousConstraints: Partial<ConstraintFile> = {};
312
+ try {
313
+ previousConstraints = JSON.parse(constraintsFile);
314
+ } catch (e) {
315
+ logger.info(
316
+ `Could not parse constraints.json file while filtering: ${e}. Generating all constraints.`,
317
+ );
318
+ return constraints;
319
+ }
320
+
321
+ logger.debug(
322
+ `Found previous constraints: ${JSON.stringify(
323
+ previousConstraints,
324
+ null,
325
+ 2,
326
+ )}`,
327
+ );
328
+
329
+ if (
330
+ !previousConstraints.providers ||
331
+ typeof previousConstraints.providers !== "object"
332
+ ) {
333
+ logger.info(
334
+ `Could not find providers in constraints.json file, generating all constraints. The constraints file was ${JSON.stringify(
335
+ previousConstraints,
336
+ null,
337
+ 2,
338
+ )}`,
339
+ );
340
+ return constraints;
341
+ }
342
+
343
+ if (previousConstraints.cdktf !== DISPLAY_VERSION) {
344
+ logger.info(
345
+ `The CDKTN version has changed, generating all constraints. The previous version was ${previousConstraints.cdktf}, the current version is ${DISPLAY_VERSION}`,
346
+ );
347
+ return constraints;
348
+ }
349
+
350
+ const constraintsToGenerate = constraints.filter((constraint) => {
351
+ const constraintMatches =
352
+ previousConstraints.providers![constraint.fqn] === constraint.version;
353
+ let providerFolderExists = false;
354
+
355
+ switch (this.options.targetLanguage) {
356
+ case Language.TYPESCRIPT:
357
+ providerFolderExists = fs.existsSync(
358
+ path.join(this.codeMakerOutdir, "providers", constraint.name),
359
+ );
360
+ break;
361
+ case Language.PYTHON:
362
+ case Language.JAVA:
363
+ case Language.CSHARP:
364
+ providerFolderExists = fs.existsSync(
365
+ path.join(this.codeMakerOutdir, constraint.name),
366
+ );
367
+ break;
368
+ case Language.GO:
369
+ providerFolderExists = fs.existsSync(
370
+ path.join(
371
+ this.codeMakerOutdir,
372
+ constraint.namespace || "hashicorp",
373
+ constraint.name,
374
+ ),
375
+ );
376
+ break;
377
+ }
378
+
379
+ const providerExists = constraintMatches && providerFolderExists;
380
+ return !providerExists;
381
+ });
382
+
383
+ logger.debug(
384
+ `Constraints to generate: ${JSON.stringify(
385
+ constraintsToGenerate,
386
+ null,
387
+ 2,
388
+ )}`,
389
+ );
390
+
391
+ return constraintsToGenerate;
392
+ }
393
+
394
+ private async generateTypescriptModule(
395
+ target: ConstructsMakerModuleTarget,
396
+ schema: ModuleSchema,
397
+ ) {
398
+ const endTSTimer = logTimespan(`Generate Typescript for ${target.name}`);
399
+ target.spec = schema;
400
+ new ModuleGenerator(this.code, [target]);
401
+ endTSTimer();
402
+ }
403
+
404
+ private async generateTypescript(
405
+ target: ConstructsMakerTarget,
406
+ schemas: Awaited<ReturnType<typeof readSchema>>,
407
+ ) {
408
+ if (target.isModule) {
409
+ const schema = schemas.moduleSchema?.[target.moduleKey];
410
+ if (!schema) {
411
+ throw Errors.Internal(
412
+ `Could not generate schema for module ${target.moduleKey}`,
413
+ );
414
+ }
415
+
416
+ await this.generateTypescriptModule(
417
+ target as ConstructsMakerModuleTarget,
418
+ schema,
419
+ );
420
+ } else if (target.isProvider) {
421
+ if (!schemas.providerSchema) {
422
+ throw Errors.Internal(`Could not generate schema for providers`);
423
+ }
424
+
425
+ await this.generateTypescriptProvider(
426
+ target as ConstructsMakerProviderTarget,
427
+ schemas.providerSchema,
428
+ );
429
+ } else {
430
+ throw new Error(
431
+ `Unknown target type used to generate bindings: ${target.name}`,
432
+ );
433
+ }
434
+ }
435
+
436
+ // emits a versions.json file with a map of the used version for each provider fqpn
437
+ private updateVersionsFile(
438
+ allowedConstraints: TerraformDependencyConstraint[],
439
+ ) {
440
+ logger.debug(
441
+ `Updating versions file with generated versions ${JSON.stringify(
442
+ this.versions,
443
+ null,
444
+ 2,
445
+ )} with allowed constraints ${JSON.stringify(
446
+ allowedConstraints,
447
+ null,
448
+ 2,
449
+ )}`,
450
+ );
451
+ const filePath = "versions.json";
452
+ let previousVersions: Record<string, string> = {};
453
+ try {
454
+ previousVersions = JSON.parse(
455
+ fs.readFileSync(path.resolve(this.codeMakerOutdir, filePath), "utf8"),
456
+ );
457
+
458
+ logger.debug(
459
+ `Read existing versions file: ${JSON.stringify(
460
+ previousVersions,
461
+ null,
462
+ 2,
463
+ )}`,
464
+ );
465
+ } catch (e) {
466
+ // ignore
467
+ logger.debug(
468
+ `Could not read versions file, this is expected if there are no pre-existing local providers: ${e}`,
469
+ );
470
+ }
471
+
472
+ const versions = allowedConstraints.reduce((acc, constraint) => {
473
+ const provider = Object.entries(previousVersions).find(([name]) =>
474
+ // This could be more refined, but it's good enough for now
475
+ name.endsWith(constraint.fqn),
476
+ );
477
+
478
+ if (provider) {
479
+ const [name, version] = provider;
480
+ return { ...acc, [name]: version };
481
+ }
482
+
483
+ return acc;
484
+ }, {});
485
+
486
+ logger.debug(
487
+ `Writing versions file (${filePath}): ${JSON.stringify(
488
+ versions,
489
+ null,
490
+ 2,
491
+ )}`,
492
+ );
493
+
494
+ this.code.openFile(filePath);
495
+ this.code.line(JSON.stringify({ ...versions, ...this.versions }, null, 2));
496
+ this.code.closeFile(filePath);
497
+ return filePath;
498
+ }
499
+
500
+ public async removeFoldersThatShouldNotExist(
501
+ constraintsThatShouldExist: TerraformDependencyConstraint[],
502
+ ) {
503
+ logger.debug(
504
+ `Removing providers except for ${JSON.stringify(
505
+ constraintsThatShouldExist,
506
+ null,
507
+ 2,
508
+ )}`,
509
+ );
510
+
511
+ // All languages besides TS keep their providers in the same folders as modules
512
+ // this makes it impossible for us to distinguish a no longer required provider
513
+ // from a manually written construct or a module
514
+ if (!this.isJavascriptTarget) {
515
+ return;
516
+ }
517
+
518
+ let filesInProviders: string[] = [];
519
+ const providersFolder = path.resolve(this.codeMakerOutdir, "providers");
520
+ try {
521
+ filesInProviders = await fs.readdir(providersFolder);
522
+ } catch (e) {
523
+ logger.debug(
524
+ `Error listing files in providers folder '${providersFolder}': ${e}`,
525
+ );
526
+ }
527
+
528
+ const folders = filesInProviders.filter((file) =>
529
+ fs
530
+ .statSync(path.resolve(this.codeMakerOutdir, "providers", file))
531
+ .isDirectory(),
532
+ );
533
+
534
+ return folders.forEach((folder) => {
535
+ const shouldExist = constraintsThatShouldExist.some(
536
+ (constraint) => constraint.name === folder,
537
+ );
538
+
539
+ if (!shouldExist) {
540
+ logger.debug(`Removing folder ${folder} from providers`);
541
+ fs.removeSync(path.resolve(this.codeMakerOutdir, "providers", folder));
542
+ }
543
+ });
544
+ }
545
+
546
+ // emits a constraints.json file with a map of the used provider fqpns and version constraints
547
+ // this is used for caching purposes
548
+ private emitConstraintsFile(
549
+ allowedConstraints: TerraformDependencyConstraint[],
550
+ ) {
551
+ const filePath = "constraints.json";
552
+
553
+ const content: ConstraintFile = {
554
+ cdktf: DISPLAY_VERSION,
555
+ providers: allowedConstraints
556
+ .sort((a, b) => a.fqn.localeCompare(b.fqn))
557
+ .reduce(
558
+ (carry, item) => ({
559
+ ...carry,
560
+ [item.fqn]: item.version,
561
+ }),
562
+ {},
563
+ ),
564
+ };
565
+
566
+ this.code.openFile(filePath);
567
+ this.code.line(JSON.stringify(content, null, 2));
568
+ this.code.closeFile(filePath);
569
+ return filePath;
570
+ }
571
+
572
+ private async generateJsiiLanguage(target: ConstructsMakerTarget) {
573
+ // Module dependencies we compile against. Static require.resolve calls so
574
+ // knip / pnpm strict isolation can see the deps.
575
+ const deps = [
576
+ path.dirname(require.resolve("@types/node/package.json")),
577
+ path.dirname(require.resolve("constructs/package.json")),
578
+ path.dirname(require.resolve("cdktn/package.json")),
579
+ ];
580
+ const opts: GenerateJSIIOptions = {
581
+ entrypoint: target.fileName,
582
+ deps,
583
+ moduleKey: target.moduleKey,
584
+ exports: target.isProvider // Modules are small enough that we don't need this optimization
585
+ ? {
586
+ ".": {
587
+ import: `./providers/${target.name}/index.js`,
588
+ require: `./providers/${target.name}/lazy-index.js`,
589
+ },
590
+ }
591
+ : undefined,
592
+ };
593
+
594
+ // used for testing.
595
+ if (this.options.outputJsii) {
596
+ opts.jsii = { path: this.options.outputJsii };
597
+ }
598
+
599
+ if (this.isPythonTarget) {
600
+ opts.python = {
601
+ outdir: this.codeMakerOutdir,
602
+ moduleName: target.srcMakName,
603
+ };
604
+ }
605
+
606
+ if (this.isJavaTarget) {
607
+ if (
608
+ this.options.codeMakerOutput.includes("/") ||
609
+ this.options.codeMakerOutput.includes("\\")
610
+ ) {
611
+ throw Errors.Usage(
612
+ `When using Java the "codeMakerOutput" option in the cdktf.json must be the organization identifier for your project (e.g. com.my-company), not a path. The generated Java code will be placed in a subdirectory of the given directory. If you are migrating from a < 0.19 version of cdktf you want to change the codemakerOutput to "imports".`,
613
+ );
614
+ }
615
+
616
+ opts.java = {
617
+ outdir: ".", // generated java files aren't packaged, so just include directly in app
618
+ package: `${this.options.codeMakerOutput}.${target.srcMakName}`,
619
+ };
620
+ }
621
+
622
+ if (this.isCsharpTarget) {
623
+ opts.csharp = {
624
+ outdir: this.codeMakerOutdir,
625
+ namespace: target.srcMakName,
626
+ };
627
+ }
628
+
629
+ if (this.isGoTarget) {
630
+ // TODO: check if needed for modules somehow
631
+ // const targetType = target.isProvider ? 'provider' : 'module';
632
+
633
+ // jsii-srcmac will produce a folder inside this dir named after "packageName"
634
+ // so this results in e.g. .gen/hashicorp/random
635
+ const outdir = path.join(this.codeMakerOutdir, target.namespace ?? "");
636
+
637
+ opts.golang = {
638
+ outdir,
639
+ moduleName: await determineGoModuleName(outdir), // e.g. `github.com/org/userproject/.gen/hashicorp`
640
+ packageName: target.srcMakName, // package will be named e.g. random for hashicorp/random
641
+ };
642
+ }
643
+
644
+ if (
645
+ process.env.NODE_OPTIONS &&
646
+ !process.env.NODE_OPTIONS.includes(`--max-old-space-size`)
647
+ ) {
648
+ logger.warn(`found NODE_OPTIONS environment variable without a setting for --max-old-space-size.
649
+ The provider generation needs a substantial amount of memory (~13GB) for some providers and languages.
650
+ So cdktn-cli sets it to NODE_OPTIONS="--max-old-space-size=16384" by default. As your environment already contains
651
+ a NODE_OPTIONS variable, we won't override it. Hence, the provider generation might fail with an out of memory error.`);
652
+ } else {
653
+ // increase memory to allow generating large providers (i.e. aws or azurerm for Go)
654
+ // srcmak is going to spawn a childprocess (for jsii-pacmak) which is going to be affected by this env var
655
+ process.env.NODE_OPTIONS = "--max-old-space-size=16384";
656
+ }
657
+
658
+ const jsiiTimer = logTimespan("JSII");
659
+ await generateJsiiLanguage(this.code, opts, this.codeMakerOutdir, [
660
+ target.isModule ? "providers/**" : "modules/**",
661
+ ]);
662
+ jsiiTimer();
663
+ }
664
+
665
+ public async getSchemas(targets: TerraformDependencyConstraint[]) {
666
+ return await readSchema(targets, this.schemaCachePath);
667
+ }
668
+
669
+ public async generate(
670
+ allConstraints: TerraformDependencyConstraint[],
671
+ constraintsToGenerate = allConstraints,
672
+ ) {
673
+ const targets = constraintsToGenerate.map((constraint) =>
674
+ ConstructsMakerTarget.from(constraint, this.options.targetLanguage),
675
+ );
676
+
677
+ const endSchemaTimer = logTimespan("Gathering schema");
678
+ const schemas = await this.getSchemas(constraintsToGenerate);
679
+ endSchemaTimer();
680
+
681
+ const endGenerateTimer = logTimespan("Generate TS");
682
+ await Promise.all(
683
+ targets.map((target) => this.generateTypescript(target, schemas)),
684
+ );
685
+ endGenerateTimer();
686
+
687
+ this.updateVersionsFile(allConstraints);
688
+ this.emitConstraintsFile(allConstraints);
689
+
690
+ if (this.isJavascriptTarget) {
691
+ await this.save();
692
+ }
693
+
694
+ if (!this.isJavascriptTarget || this.options.outputJsii) {
695
+ const numberOfWorkers = Math.max(
696
+ 1,
697
+ this.options.jsiiParallelism === -1
698
+ ? targets.length
699
+ : this.options.jsiiParallelism || 1,
700
+ );
701
+
702
+ const work = [...targets];
703
+ const workers = new Array(numberOfWorkers).fill(async () => {
704
+ let target: ConstructsMakerTarget | undefined;
705
+ while ((target = work.pop())) {
706
+ const endJsiiTarget = logTimespan(
707
+ `Generating JSII bindings for ${target.name}`,
708
+ );
709
+ await this.generateJsiiLanguage(target);
710
+ endJsiiTarget();
711
+ }
712
+ });
713
+
714
+ await Promise.all(workers.map((fn) => fn()));
715
+ }
716
+
717
+ for (const target of targets) {
718
+ await this.reportTelemetry({
719
+ trackingPayload: target.trackingPayload,
720
+ targetLanguage: target.targetLanguage,
721
+ });
722
+ }
723
+
724
+ if (this.isPythonTarget) {
725
+ const endPythonTimer = logTimespan("Python post-processing");
726
+ // Remove from . import ... statements from root level __init__.py
727
+ // This removes root-level imports of namespaces, but saves 25s synth time for the aws provider alone
728
+ const allInitPyPaths = glob
729
+ .sync("**/__init__.py", {
730
+ cwd: this.codeMakerOutdir,
731
+ })
732
+ // sort by depth, so we start with the shallowest files
733
+ .sort((a, b) => a.split("/").length - b.split("/").length);
734
+
735
+ const visitedDirectories: string[] = [];
736
+ for (const initPyPath of allInitPyPaths) {
737
+ const directoryPath = path.dirname(initPyPath);
738
+ if (visitedDirectories.some((dir) => directoryPath.startsWith(dir))) {
739
+ // we already processed this directory
740
+ continue;
741
+ }
742
+ visitedDirectories.push(directoryPath);
743
+
744
+ const absoluteInitPyPath = path.join(this.codeMakerOutdir, initPyPath);
745
+ const initPy = await fs.readFile(absoluteInitPyPath, "utf8");
746
+ const initPyWithoutImports = initPy.replace(/from \. import .*\n/g, "");
747
+ await fs.writeFile(absoluteInitPyPath, initPyWithoutImports);
748
+ }
749
+
750
+ endPythonTimer();
751
+ }
752
+ }
753
+
754
+ private async save(outdir = this.codeMakerOutdir) {
755
+ await this.code.save(outdir);
756
+ }
757
+
758
+ private get isJavascriptTarget() {
759
+ return this.options.targetLanguage === Language.TYPESCRIPT;
760
+ }
761
+
762
+ private get isPythonTarget() {
763
+ return this.options.targetLanguage === Language.PYTHON;
764
+ }
765
+
766
+ private get isJavaTarget() {
767
+ return this.options.targetLanguage === Language.JAVA;
768
+ }
769
+
770
+ private get isCsharpTarget() {
771
+ return this.options.targetLanguage === Language.CSHARP;
772
+ }
773
+
774
+ private get isGoTarget() {
775
+ return this.options.targetLanguage === Language.GO;
776
+ }
777
+ }
778
+
779
+ /**
780
+ * searches for the closest `go.mod` file and returns the nested go module name for `dir`
781
+ * e.g. (/dir/.gen/) => cdk.tf/stack/.gen if the parent dir of .gen has a go.mod for "module cdk.tf/stack"
782
+ *
783
+ * @param dir the directory to start the search from (searches upwards)
784
+ * @returns the package name for `dir`
785
+ * @throws an Error if no go.mod was found
786
+ */
787
+ export const determineGoModuleName = async (dir: string): Promise<string> => {
788
+ let previousDir;
789
+ let currentDir = path.resolve(dir);
790
+
791
+ do {
792
+ let files: string[] = [];
793
+ try {
794
+ files = await fs.readdir(currentDir);
795
+ } catch (e: any) {
796
+ // directory might not exist yet, but we still walk upwards from there, so ignore 'ENOENT'
797
+ if (e.code !== "ENOENT") {
798
+ throw e;
799
+ }
800
+ }
801
+ if (files.includes("go.mod")) {
802
+ const file = path.resolve(currentDir, "go.mod");
803
+ const gomod = await fs.readFile(file);
804
+ const match = /^module\s*(\S*)\s*$/m.exec(gomod.toString());
805
+ if (match && match[1]) {
806
+ const childdir = path.relative(currentDir, dir).replace(/\\/g, "/"); // replace '\' with '/' for windows paths
807
+ return childdir.length > 0 ? `${match[1]}/${childdir}` : match[1];
808
+ }
809
+ throw new Error(
810
+ `Could not determine the root Go module name. Found ${file} but failed to regex match the module name directive`,
811
+ );
812
+ }
813
+ // go up one directory. As dirname('/') will return '/' we cancel the loop
814
+ // as soon as the dir does not change anymore.
815
+ previousDir = currentDir;
816
+ currentDir = path.dirname(currentDir);
817
+ } while (currentDir !== previousDir);
818
+
819
+ throw new Error(
820
+ `Could not determine the root Go module name. No go.mod found in ${dir} and any parent directories`,
821
+ );
822
+ };