@cdktn/provider-generator 0.24.0-pre.9 → 0.24.0-pre.90

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