@cdktn/provider-generator 0.25.0-pre.61 → 0.25.0-pre.64
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/build/get/__tests__/generator/module-docs-registry.test.d.ts +2 -0
- package/build/get/__tests__/generator/module-docs-registry.test.js +91 -0
- package/build/get/__tests__/registry-class-invalidation.test.d.ts +2 -0
- package/build/get/__tests__/registry-class-invalidation.test.js +120 -0
- package/build/get/__tests__/target-versions.test.js +29 -1
- package/build/get/constructs-maker.js +19 -2
- package/build/get/generator/module-generator.d.ts +3 -2
- package/build/get/generator/module-generator.js +15 -17
- package/package.json +4 -4
- package/src/get/__tests__/generator/module-docs-registry.test.ts +89 -0
- package/src/get/__tests__/registry-class-invalidation.test.ts +123 -0
- package/src/get/__tests__/target-versions.test.ts +43 -0
- package/src/get/constructs-maker.ts +43 -7
- package/src/get/generator/module-generator.ts +20 -18
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) HashiCorp, Inc
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { CodeMaker } from "codemaker";
|
|
6
|
+
import {
|
|
7
|
+
ConstructsMakerModuleTarget,
|
|
8
|
+
Language,
|
|
9
|
+
TerraformModuleConstraint,
|
|
10
|
+
TerraformTargetVersions,
|
|
11
|
+
} from "@cdktn/commons";
|
|
12
|
+
import { ModuleGenerator } from "../../generator/module-generator";
|
|
13
|
+
import { createTmpHelper } from "../util";
|
|
14
|
+
|
|
15
|
+
const tmp = createTmpHelper();
|
|
16
|
+
|
|
17
|
+
async function docsComment(
|
|
18
|
+
source: string,
|
|
19
|
+
targetVersions?: TerraformTargetVersions,
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const workdir = tmp("module-docs-registry");
|
|
22
|
+
const target = ConstructsMakerModuleTarget.from(
|
|
23
|
+
new TerraformModuleConstraint(source),
|
|
24
|
+
Language.TYPESCRIPT,
|
|
25
|
+
) as ConstructsMakerModuleTarget;
|
|
26
|
+
target.spec = { name: target.name, inputs: [], outputs: [] };
|
|
27
|
+
|
|
28
|
+
const code = new CodeMaker();
|
|
29
|
+
new ModuleGenerator(code, [target], targetVersions);
|
|
30
|
+
await code.save(workdir);
|
|
31
|
+
|
|
32
|
+
const emitted = fs.readFileSync(path.join(workdir, target.fileName), "utf-8");
|
|
33
|
+
const line = emitted
|
|
34
|
+
.split("\n")
|
|
35
|
+
.find((l) => l.includes("Docs at") || l.includes("Source at"));
|
|
36
|
+
return (line ?? "").trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("generated module docs links", () => {
|
|
40
|
+
const module = "terraform-aws-modules/vpc/aws@3.19.0";
|
|
41
|
+
const submodule =
|
|
42
|
+
"terraform-aws-modules/vpc/aws//modules/vpc-endpoints@3.19.0";
|
|
43
|
+
|
|
44
|
+
it("uses the Terraform Registry when nothing is declared", async () => {
|
|
45
|
+
expect(await docsComment(module)).toContain(
|
|
46
|
+
"https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/3.19.0",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses the OpenTofu Registry when only OpenTofu is declared", async () => {
|
|
51
|
+
const comment = await docsComment(module, { opentofu: ">=1.6.0" });
|
|
52
|
+
expect(comment).toContain("Docs at OpenTofu Registry");
|
|
53
|
+
expect(comment).toContain(
|
|
54
|
+
"https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("links an OpenTofu submodule with the singular segment", async () => {
|
|
59
|
+
expect(await docsComment(submodule, { opentofu: ">=1.6.0" })).toContain(
|
|
60
|
+
"https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0/submodule/vpc-endpoints",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Registries serve submodule pages only for a direct child of `modules/`
|
|
65
|
+
// (checked against rendered pages); anything else links to the module.
|
|
66
|
+
it.each([
|
|
67
|
+
[
|
|
68
|
+
"an examples path",
|
|
69
|
+
"terraform-aws-modules/vpc/aws//examples/complete@3.19.0",
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"a nested modules path",
|
|
73
|
+
"terraform-aws-modules/vpc/aws//modules/x/y@3.19.0",
|
|
74
|
+
],
|
|
75
|
+
])("links %s to the module page", async (_name, source) => {
|
|
76
|
+
expect(await docsComment(source)).toContain(
|
|
77
|
+
"{@link https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/3.19.0 ",
|
|
78
|
+
);
|
|
79
|
+
expect(await docsComment(source, { opentofu: ">=1.6.0" })).toContain(
|
|
80
|
+
"{@link https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0 ",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("keeps the Terraform submodule shape unchanged", async () => {
|
|
85
|
+
expect(await docsComment(submodule)).toContain(
|
|
86
|
+
"https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/3.19.0/submodules/vpc-endpoints",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
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 {
|
|
6
|
+
Language,
|
|
7
|
+
TerraformProviderConstraint,
|
|
8
|
+
TerraformTargetVersions,
|
|
9
|
+
} from "@cdktn/commons";
|
|
10
|
+
import { createTmpHelper } from "./util";
|
|
11
|
+
|
|
12
|
+
const mockReadSchema = jest.fn();
|
|
13
|
+
jest.mock("@cdktn/provider-schema", () => ({
|
|
14
|
+
readSchema: (...args: unknown[]) => mockReadSchema(...args),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Imported after the mock so ConstructsMaker picks up the mocked readSchema.
|
|
18
|
+
import { ConstructsMaker } from "../constructs-maker";
|
|
19
|
+
|
|
20
|
+
const tmp = createTmpHelper();
|
|
21
|
+
|
|
22
|
+
// No resources, so generation stays in TypeScript emission.
|
|
23
|
+
const fakeSchema = {
|
|
24
|
+
providerSchema: {
|
|
25
|
+
format_version: "0.2" as const,
|
|
26
|
+
provider_schemas: {
|
|
27
|
+
"registry.terraform.io/hashicorp/random": {
|
|
28
|
+
resource_schemas: null,
|
|
29
|
+
data_source_schemas: null,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
provider_versions: { "registry.terraform.io/hashicorp/random": "3.2.0" },
|
|
33
|
+
cli_name: "terraform",
|
|
34
|
+
cli_version: "1.7.5",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("filterAlreadyGenerated: registry class", () => {
|
|
39
|
+
const constraint = new TerraformProviderConstraint("hashicorp/random@=3.2.0");
|
|
40
|
+
let outdir: string;
|
|
41
|
+
|
|
42
|
+
const maker = (targetVersions?: TerraformTargetVersions) =>
|
|
43
|
+
new ConstructsMaker({
|
|
44
|
+
codeMakerOutput: outdir,
|
|
45
|
+
targetLanguage: Language.TYPESCRIPT,
|
|
46
|
+
targetVersions,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** A real previous `cdktn get`, so constraints.json is what generation writes. */
|
|
50
|
+
const generateWith = (targetVersions?: TerraformTargetVersions) =>
|
|
51
|
+
maker(targetVersions).generate([constraint]);
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockReadSchema.mockReset();
|
|
55
|
+
mockReadSchema.mockResolvedValue(fakeSchema);
|
|
56
|
+
outdir = tmp("registry-class");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it.each<
|
|
60
|
+
[
|
|
61
|
+
string,
|
|
62
|
+
TerraformTargetVersions | undefined,
|
|
63
|
+
TerraformTargetVersions | undefined,
|
|
64
|
+
]
|
|
65
|
+
>([
|
|
66
|
+
["nothing changed", undefined, undefined],
|
|
67
|
+
[
|
|
68
|
+
"both products declared, the same class as nothing",
|
|
69
|
+
undefined,
|
|
70
|
+
{ terraform: ">=1.5.7", opentofu: ">=1.6.0" },
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"a range moving within the OpenTofu class",
|
|
74
|
+
{ opentofu: ">=1.6.0" },
|
|
75
|
+
{ opentofu: ">=1.8.0" },
|
|
76
|
+
],
|
|
77
|
+
])("skips regeneration: %s", async (_name, before, after) => {
|
|
78
|
+
await generateWith(before);
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
maker(after).filterAlreadyGenerated([constraint]),
|
|
82
|
+
).resolves.toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("regenerates when the project switches to OpenTofu-only", async () => {
|
|
86
|
+
await generateWith(undefined);
|
|
87
|
+
|
|
88
|
+
await expect(
|
|
89
|
+
maker({ opentofu: ">=1.6.0" }).filterAlreadyGenerated([constraint]),
|
|
90
|
+
).resolves.toEqual([constraint]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("regenerates when the project switches away from OpenTofu-only", async () => {
|
|
94
|
+
await generateWith({ opentofu: ">=1.6.0" });
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
maker({ terraform: ">=1.5.7" }).filterAlreadyGenerated([constraint]),
|
|
98
|
+
).resolves.toEqual([constraint]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// constraints.json written before targetVersions was stamped has no such field;
|
|
102
|
+
// it must read as the Terraform class, which is what it was generated for.
|
|
103
|
+
describe("constraints.json from before targetVersions was recorded", () => {
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
await generateWith({ terraform: ">=1.5.7" });
|
|
106
|
+
const file = path.join(outdir, "constraints.json");
|
|
107
|
+
const { targetVersions: _dropped, ...older } = fs.readJsonSync(file);
|
|
108
|
+
fs.writeJsonSync(file, older);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("is fresh for a Terraform project", async () => {
|
|
112
|
+
await expect(
|
|
113
|
+
maker().filterAlreadyGenerated([constraint]),
|
|
114
|
+
).resolves.toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("is stale for an OpenTofu-only project", async () => {
|
|
118
|
+
await expect(
|
|
119
|
+
maker({ opentofu: ">=1.6.0" }).filterAlreadyGenerated([constraint]),
|
|
120
|
+
).resolves.toEqual([constraint]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import * as fs from "fs-extra";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import {
|
|
6
|
+
ConstructsMakerModuleTarget,
|
|
6
7
|
Language,
|
|
7
8
|
TerraformDependencyConstraint,
|
|
9
|
+
TerraformModuleConstraint,
|
|
8
10
|
TerraformTargetVersions,
|
|
9
11
|
} from "@cdktn/commons";
|
|
10
12
|
import { createTmpHelper } from "./util";
|
|
@@ -204,3 +206,44 @@ describe("targetVersions threading through 'cdktn get'", () => {
|
|
|
204
206
|
expect(toGenerate).toEqual([]);
|
|
205
207
|
});
|
|
206
208
|
});
|
|
209
|
+
|
|
210
|
+
describe("targetVersions reaching generated module bindings", () => {
|
|
211
|
+
const moduleConstraint = new TerraformModuleConstraint(
|
|
212
|
+
"terraform-aws-modules/vpc/aws@3.19.0",
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const emittedSources = (dir: string): string[] =>
|
|
216
|
+
fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
217
|
+
const full = path.join(dir, entry.name);
|
|
218
|
+
if (entry.isDirectory()) return emittedSources(full);
|
|
219
|
+
return entry.name.endsWith(".ts") ? [fs.readFileSync(full, "utf8")] : [];
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
mockReadSchema.mockReset();
|
|
224
|
+
const target = ConstructsMakerModuleTarget.from(
|
|
225
|
+
moduleConstraint,
|
|
226
|
+
Language.TYPESCRIPT,
|
|
227
|
+
) as ConstructsMakerModuleTarget;
|
|
228
|
+
mockReadSchema.mockResolvedValue({
|
|
229
|
+
moduleSchema: {
|
|
230
|
+
[target.moduleKey]: { name: target.name, inputs: [], outputs: [] },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("links an OpenTofu-only project's modules to the OpenTofu registry", async () => {
|
|
236
|
+
const workdir = tmp("target-versions-module.test");
|
|
237
|
+
const maker = new ConstructsMaker({
|
|
238
|
+
codeMakerOutput: workdir,
|
|
239
|
+
targetLanguage: Language.TYPESCRIPT,
|
|
240
|
+
targetVersions: { opentofu: ">=1.6.0" },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await maker.generate([moduleConstraint]);
|
|
244
|
+
|
|
245
|
+
expect(emittedSources(workdir).join("\n")).toContain(
|
|
246
|
+
"https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0",
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
type LanguageOptions,
|
|
18
18
|
type TerraformTargetVersions,
|
|
19
19
|
} from "@cdktn/commons";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
DISPLAY_VERSION,
|
|
22
|
+
Language,
|
|
23
|
+
registryForTargetVersions,
|
|
24
|
+
} from "@cdktn/commons";
|
|
21
25
|
import { TerraformProviderGenerator } from "./generator/provider-generator";
|
|
22
26
|
import { ModuleGenerator } from "./generator/module-generator";
|
|
23
27
|
import { glob } from "glob";
|
|
@@ -275,11 +279,13 @@ type ConstraintFile = {
|
|
|
275
279
|
providers: Record<string, string>;
|
|
276
280
|
cdktf: string;
|
|
277
281
|
/**
|
|
278
|
-
* The project's declared targetVersions (cdktf.json)
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
282
|
+
* The project's declared targetVersions (cdktf.json). Read back by
|
|
283
|
+
* `filterAlreadyGenerated` to compare registry *classes*: which registry the
|
|
284
|
+
* generated docs links point at follows from this, so output produced for the
|
|
285
|
+
* other class is stale. The version ranges themselves do not affect the
|
|
286
|
+
* generated surface - the full surface is always generated and narrowing
|
|
287
|
+
* happens at synth - so a range moving within a class is not a reason to
|
|
288
|
+
* regenerate.
|
|
283
289
|
*/
|
|
284
290
|
targetVersions?: TerraformTargetVersions;
|
|
285
291
|
/**
|
|
@@ -407,6 +413,22 @@ export class ConstructsMaker {
|
|
|
407
413
|
return constraints;
|
|
408
414
|
}
|
|
409
415
|
|
|
416
|
+
// Generated docs links, and the prose naming the registry around them, follow
|
|
417
|
+
// the project's declared targets, so output from the other registry class is
|
|
418
|
+
// stale even when every provider version still matches.
|
|
419
|
+
const previousRegistry = registryForTargetVersions(
|
|
420
|
+
previousConstraints.targetVersions,
|
|
421
|
+
);
|
|
422
|
+
const currentRegistry = registryForTargetVersions(
|
|
423
|
+
this.options.targetVersions,
|
|
424
|
+
);
|
|
425
|
+
if (previousRegistry.hostname !== currentRegistry.hostname) {
|
|
426
|
+
logger.info(
|
|
427
|
+
`The target registry has changed from ${previousRegistry.displayName} to ${currentRegistry.displayName}, generating all constraints.`,
|
|
428
|
+
);
|
|
429
|
+
return constraints;
|
|
430
|
+
}
|
|
431
|
+
|
|
410
432
|
const constraintsToGenerate = constraints.filter((constraint) => {
|
|
411
433
|
const constraintMatches =
|
|
412
434
|
previousConstraints.providers![constraint.fqn] === constraint.version;
|
|
@@ -457,7 +479,7 @@ export class ConstructsMaker {
|
|
|
457
479
|
) {
|
|
458
480
|
const endTSTimer = logTimespan(`Generate Typescript for ${target.name}`);
|
|
459
481
|
target.spec = schema;
|
|
460
|
-
new ModuleGenerator(this.code, [target]);
|
|
482
|
+
new ModuleGenerator(this.code, [target], this.options.targetVersions);
|
|
461
483
|
endTSTimer();
|
|
462
484
|
}
|
|
463
485
|
|
|
@@ -529,7 +551,21 @@ export class ConstructsMaker {
|
|
|
529
551
|
);
|
|
530
552
|
}
|
|
531
553
|
|
|
554
|
+
// Keys are fully qualified and the host is whichever registry the fetching CLI
|
|
555
|
+
// resolved from, so a provider regenerated this run can arrive under a different
|
|
556
|
+
// host than the file already holds. Carrying the old key forward as well would
|
|
557
|
+
// leave two entries for one provider, and the stale one sorts first.
|
|
558
|
+
const regenerated = new Set(
|
|
559
|
+
Object.keys(this.versions).map((fqpn) =>
|
|
560
|
+
fqpn.split("/").slice(1).join("/"),
|
|
561
|
+
),
|
|
562
|
+
);
|
|
563
|
+
|
|
532
564
|
const versions = allowedConstraints.reduce((acc, constraint) => {
|
|
565
|
+
if (regenerated.has(constraint.fqn)) {
|
|
566
|
+
return acc;
|
|
567
|
+
}
|
|
568
|
+
|
|
533
569
|
const provider = Object.entries(previousVersions).find(([name]) =>
|
|
534
570
|
// This could be more refined, but it's good enough for now
|
|
535
571
|
name.endsWith(constraint.fqn),
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
import { CodeMaker, toCamelCase } from "codemaker";
|
|
4
4
|
import { AttributeModel } from "./models";
|
|
5
5
|
import { sanitizedComment } from "./sanitized-comments";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ConstructsMakerModuleTarget,
|
|
8
|
+
TerraformTargetVersions,
|
|
9
|
+
registryForTargetVersions,
|
|
10
|
+
} from "@cdktn/commons";
|
|
7
11
|
|
|
8
12
|
export class ModuleGenerator {
|
|
9
13
|
constructor(
|
|
10
14
|
private readonly code: CodeMaker,
|
|
11
15
|
private readonly targets: ConstructsMakerModuleTarget[],
|
|
16
|
+
private readonly targetVersions?: TerraformTargetVersions,
|
|
12
17
|
) {
|
|
13
18
|
this.code.indentation = 2;
|
|
14
19
|
|
|
@@ -76,22 +81,19 @@ export class ModuleGenerator {
|
|
|
76
81
|
// ... and more
|
|
77
82
|
// ../module and ./module (local paths)
|
|
78
83
|
const isNonRegistryModule = target.source.includes(".");
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// not submodule specified, just append the version
|
|
93
|
-
registryPath = `${target.source}/${target.version || "latest"}`;
|
|
94
|
-
}
|
|
84
|
+
// Keyed off the project's declared targets rather than a fixed registry, so
|
|
85
|
+
// the link matches the product the project builds for.
|
|
86
|
+
const registry = registryForTargetVersions(this.targetVersions);
|
|
87
|
+
// Registries only have pages for a direct child of `modules/`, e.g.
|
|
88
|
+
// terraform-aws-modules/vpc/aws//modules/vpc-endpoints; any other `//` path
|
|
89
|
+
// links to the module itself.
|
|
90
|
+
const [moduleSource, subPath] = target.source.split("//", 2);
|
|
91
|
+
const submodule = subPath?.match(/^modules\/([^/]+)$/)?.[1];
|
|
92
|
+
const docsUrl = registry.moduleDocsUrl(
|
|
93
|
+
moduleSource,
|
|
94
|
+
target.version,
|
|
95
|
+
submodule,
|
|
96
|
+
);
|
|
95
97
|
|
|
96
98
|
const comment = sanitizedComment(this.code);
|
|
97
99
|
comment.line(`Defines an ${baseName} based on a Terraform module`);
|
|
@@ -99,7 +101,7 @@ export class ModuleGenerator {
|
|
|
99
101
|
comment.line(
|
|
100
102
|
isNonRegistryModule
|
|
101
103
|
? `Source at ${target.source}`
|
|
102
|
-
: `Docs at
|
|
104
|
+
: `Docs at ${registry.displayName}: {@link ${docsUrl} ${target.source}}`,
|
|
103
105
|
);
|
|
104
106
|
comment.end();
|
|
105
107
|
this.code.openBlock(`export class ${baseName} extends TerraformModule`);
|