@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.
- package/.spec.swcrc +22 -0
- package/LICENSE +355 -0
- package/README.md +1 -1
- package/build/__tests__/edge-provider-schema/cli.js +8 -3
- package/build/get/__tests__/generator/import-style.test.d.ts +2 -0
- package/build/get/__tests__/generator/import-style.test.js +101 -0
- package/build/get/__tests__/generator/module-generator.test.js +12 -12
- package/build/get/constructs-maker.d.ts +5 -1
- package/build/get/constructs-maker.js +14 -6
- package/build/get/generator/emitter/struct-emitter.d.ts +2 -1
- package/build/get/generator/emitter/struct-emitter.js +14 -11
- package/build/get/generator/module-generator.js +3 -3
- package/build/get/generator/provider-generator.d.ts +9 -1
- package/build/get/generator/provider-generator.js +9 -6
- package/jest.config.js +16 -9
- package/package.json +24 -23
- package/package.sh +1 -1
- package/src/__tests__/__snapshots__/edge-provider-schema.test.ts.snap +8 -8
- package/src/__tests__/__snapshots__/provider.test.ts.snap +2951 -2951
- package/src/__tests__/edge-provider-schema/builder.ts +185 -0
- package/src/__tests__/edge-provider-schema/cli.ts +51 -0
- package/src/__tests__/edge-provider-schema/index.ts +161 -0
- package/src/__tests__/edge-provider-schema.test.ts +24 -0
- package/src/__tests__/provider.test.ts +180 -0
- package/src/get/__tests__/constructs-maker.test.ts +118 -0
- package/src/get/__tests__/generator/__snapshots__/complex-computed-types.test.ts.snap +5 -5
- package/src/get/__tests__/generator/__snapshots__/export-sharding.test.ts.snap +3310 -3310
- package/src/get/__tests__/generator/__snapshots__/module-generator.test.ts.snap +355 -355
- package/src/get/__tests__/generator/__snapshots__/nested-types.test.ts.snap +8 -8
- package/src/get/__tests__/generator/__snapshots__/provider.test.ts.snap +8 -8
- package/src/get/__tests__/generator/__snapshots__/resource-types.test.ts.snap +126 -126
- package/src/get/__tests__/generator/__snapshots__/skipped-attributes.test.ts.snap +17 -17
- package/src/get/__tests__/generator/__snapshots__/types.test.ts.snap +65 -65
- package/src/get/__tests__/generator/complex-computed-types.test.ts +28 -0
- package/src/get/__tests__/generator/deep-nested-attributes.test.ts +56 -0
- package/src/get/__tests__/generator/description-escaping.test.ts +84 -0
- package/src/get/__tests__/generator/empty-provider-resources.test.ts +26 -0
- package/src/get/__tests__/generator/export-sharding.test.ts +169 -0
- package/src/get/__tests__/generator/import-style.test.ts +129 -0
- package/src/get/__tests__/generator/module-generator.test.ts +528 -0
- package/src/get/__tests__/generator/nested-types.test.ts +54 -0
- package/src/get/__tests__/generator/provider.test.ts +51 -0
- package/src/get/__tests__/generator/resource-types.test.ts +118 -0
- package/src/get/__tests__/generator/skipped-attributes.test.ts +72 -0
- package/src/get/__tests__/generator/types.test.ts +611 -0
- package/src/get/__tests__/generator/versions-file.test.ts +72 -0
- package/src/get/__tests__/util.ts +75 -0
- package/src/get/constructs-maker.ts +822 -0
- package/src/get/generator/custom-defaults.ts +493 -0
- package/src/get/generator/emitter/attributes-emitter.ts +225 -0
- package/src/get/generator/emitter/index.ts +5 -0
- package/src/get/generator/emitter/resource-emitter.ts +226 -0
- package/src/get/generator/emitter/struct-emitter.ts +683 -0
- package/src/get/generator/loop-detection.ts +81 -0
- package/src/get/generator/models/attribute-model.ts +216 -0
- package/src/get/generator/models/attribute-type-model.ts +448 -0
- package/src/get/generator/models/index.ts +7 -0
- package/src/get/generator/models/resource-model.ts +161 -0
- package/src/get/generator/models/scope.ts +54 -0
- package/src/get/generator/models/struct.ts +116 -0
- package/src/get/generator/module-generator.ts +234 -0
- package/src/get/generator/provider-generator.ts +355 -0
- package/src/get/generator/resource-parser.ts +762 -0
- package/src/get/generator/sanitized-comments.ts +49 -0
- package/src/get/generator/skipped-attributes.ts +27 -0
- package/src/index.ts +40 -0
- 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
|
+
};
|