@cdktn/provider-generator 0.24.0-pre.87 → 0.24.0-pre.89

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 (66) hide show
  1. package/build/__tests__/edge-provider-schema/builder.d.ts +9 -3
  2. package/build/__tests__/edge-provider-schema/builder.js +6 -3
  3. package/build/__tests__/edge-provider-schema/index.js +119 -1
  4. package/build/__tests__/provider.test.js +4 -1
  5. package/build/get/__tests__/generator/ephemeral-resources.test.d.ts +2 -0
  6. package/build/get/__tests__/generator/ephemeral-resources.test.js +49 -0
  7. package/build/get/__tests__/generator/provider-functions.test.d.ts +2 -0
  8. package/build/get/__tests__/generator/provider-functions.test.js +176 -0
  9. package/build/get/__tests__/generator/provider.test.js +9 -1
  10. package/build/get/__tests__/generator/write-only.test.d.ts +2 -0
  11. package/build/get/__tests__/generator/write-only.test.js +65 -0
  12. package/build/get/__tests__/target-versions.test.d.ts +2 -0
  13. package/build/get/__tests__/target-versions.test.js +171 -0
  14. package/build/get/constructs-maker.d.ts +8 -1
  15. package/build/get/constructs-maker.js +13 -4
  16. package/build/get/generator/emitter/attributes-emitter.d.ts +12 -2
  17. package/build/get/generator/emitter/attributes-emitter.js +47 -5
  18. package/build/get/generator/emitter/index.d.ts +1 -0
  19. package/build/get/generator/emitter/index.js +2 -1
  20. package/build/get/generator/emitter/provider-functions-emitter.d.ts +11 -0
  21. package/build/get/generator/emitter/provider-functions-emitter.js +94 -0
  22. package/build/get/generator/emitter/resource-emitter.d.ts +5 -1
  23. package/build/get/generator/emitter/resource-emitter.js +83 -7
  24. package/build/get/generator/models/attribute-model.d.ts +2 -0
  25. package/build/get/generator/models/attribute-model.js +2 -1
  26. package/build/get/generator/models/index.d.ts +1 -0
  27. package/build/get/generator/models/index.js +2 -1
  28. package/build/get/generator/models/provider-function-model.d.ts +102 -0
  29. package/build/get/generator/models/provider-function-model.js +667 -0
  30. package/build/get/generator/models/resource-model.d.ts +3 -0
  31. package/build/get/generator/models/resource-model.js +13 -4
  32. package/build/get/generator/models/struct.d.ts +2 -0
  33. package/build/get/generator/models/struct.js +6 -2
  34. package/build/get/generator/provider-generator.d.ts +2 -0
  35. package/build/get/generator/provider-generator.js +31 -3
  36. package/build/get/generator/resource-parser.js +3 -1
  37. package/package.json +5 -5
  38. package/src/__tests__/__snapshots__/edge-provider-schema.test.ts.snap +4 -0
  39. package/src/__tests__/edge-provider-schema/builder.ts +17 -1
  40. package/src/__tests__/edge-provider-schema/index.ts +124 -1
  41. package/src/__tests__/provider.test.ts +3 -0
  42. package/src/get/__tests__/generator/__snapshots__/ephemeral-resources.test.ts.snap +524 -0
  43. package/src/get/__tests__/generator/__snapshots__/provider-functions.test.ts.snap +444 -0
  44. package/src/get/__tests__/generator/__snapshots__/write-only.test.ts.snap +262 -0
  45. package/src/get/__tests__/generator/ephemeral-resources.test.ts +46 -0
  46. package/src/get/__tests__/generator/fixtures/ephemeral-resources.test.fixture.json +160 -0
  47. package/src/get/__tests__/generator/fixtures/provider-functions-collision.test.fixture.json +31 -0
  48. package/src/get/__tests__/generator/fixtures/provider-functions-synthetic.test.fixture.json +192 -0
  49. package/src/get/__tests__/generator/fixtures/provider-functions.test.fixture.json +170 -0
  50. package/src/get/__tests__/generator/fixtures/write-only.test.fixture.json +58 -0
  51. package/src/get/__tests__/generator/provider-functions.test.ts +262 -0
  52. package/src/get/__tests__/generator/provider.test.ts +8 -0
  53. package/src/get/__tests__/generator/write-only.test.ts +71 -0
  54. package/src/get/__tests__/target-versions.test.ts +206 -0
  55. package/src/get/constructs-maker.ts +43 -3
  56. package/src/get/generator/emitter/attributes-emitter.ts +61 -6
  57. package/src/get/generator/emitter/index.ts +1 -0
  58. package/src/get/generator/emitter/provider-functions-emitter.ts +117 -0
  59. package/src/get/generator/emitter/resource-emitter.ts +110 -6
  60. package/src/get/generator/models/attribute-model.ts +3 -0
  61. package/src/get/generator/models/index.ts +1 -0
  62. package/src/get/generator/models/provider-function-model.ts +872 -0
  63. package/src/get/generator/models/resource-model.ts +25 -3
  64. package/src/get/generator/models/struct.ts +9 -1
  65. package/src/get/generator/provider-generator.ts +70 -4
  66. package/src/get/generator/resource-parser.ts +2 -0
@@ -0,0 +1,262 @@
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 { TerraformProviderGenerator } from "../../generator/provider-generator";
6
+ import {
7
+ assertNoFunctionsGetterCollision,
8
+ buildProviderFunctionsModel,
9
+ } from "../../generator/models/provider-function-model";
10
+ import { CodeMaker } from "codemaker";
11
+ import { FunctionSignature } from "@cdktn/commons";
12
+ import { createTmpHelper } from "../util";
13
+
14
+ const tmp = createTmpHelper();
15
+
16
+ test("generate provider functions for the time provider (real terraform 1.15.6 schema fragment)", async () => {
17
+ const code = new CodeMaker();
18
+ const workdir = tmp("provider-functions.test");
19
+ const spec = JSON.parse(
20
+ fs.readFileSync(
21
+ path.join(__dirname, "fixtures", "provider-functions.test.fixture.json"),
22
+ "utf-8",
23
+ ),
24
+ );
25
+ new TerraformProviderGenerator(code, spec).generateAll();
26
+ await code.save(workdir);
27
+
28
+ const providerFunctionsOutput = fs.readFileSync(
29
+ path.join(workdir, "providers/time/provider-functions/index.ts"),
30
+ "utf-8",
31
+ );
32
+ expect(providerFunctionsOutput).toMatchSnapshot("time-provider-functions");
33
+
34
+ const providerOutput = fs.readFileSync(
35
+ path.join(workdir, "providers/time/provider/index.ts"),
36
+ "utf-8",
37
+ );
38
+ expect(providerOutput).toMatchSnapshot("time-provider");
39
+
40
+ const providerIndex = fs.readFileSync(
41
+ path.join(workdir, "providers/time/index.ts"),
42
+ "utf-8",
43
+ );
44
+ expect(providerIndex).toMatchSnapshot("provider-index");
45
+
46
+ const providerLazyIndex = fs.readFileSync(
47
+ path.join(workdir, "providers/time/lazy-index.ts"),
48
+ "utf-8",
49
+ );
50
+ expect(providerLazyIndex).toMatchSnapshot("provider-lazy-index");
51
+ });
52
+
53
+ describe("generate provider functions covering variadic parameters, primitive/list returns, and a 'default' parameter name", () => {
54
+ let providerFunctionsOutput: string;
55
+ let providerIndex: string;
56
+ let providerLazyIndex: string;
57
+
58
+ beforeAll(async () => {
59
+ const code = new CodeMaker();
60
+ const workdir = tmp("provider-functions-synthetic.test");
61
+ const spec = JSON.parse(
62
+ fs.readFileSync(
63
+ path.join(
64
+ __dirname,
65
+ "fixtures",
66
+ "provider-functions-synthetic.test.fixture.json",
67
+ ),
68
+ "utf-8",
69
+ ),
70
+ );
71
+ new TerraformProviderGenerator(code, spec).generateAll();
72
+ await code.save(workdir);
73
+
74
+ providerFunctionsOutput = fs.readFileSync(
75
+ path.join(workdir, "providers/example/provider-functions/index.ts"),
76
+ "utf-8",
77
+ );
78
+ providerIndex = fs.readFileSync(
79
+ path.join(workdir, "providers/example/index.ts"),
80
+ "utf-8",
81
+ );
82
+ providerLazyIndex = fs.readFileSync(
83
+ path.join(workdir, "providers/example/lazy-index.ts"),
84
+ "utf-8",
85
+ );
86
+ });
87
+
88
+ test("matches the snapshot", () => {
89
+ expect(providerFunctionsOutput).toMatchSnapshot(
90
+ "example-provider-functions",
91
+ );
92
+ expect(providerIndex).toMatchSnapshot("provider-index");
93
+ expect(providerLazyIndex).toMatchSnapshot("provider-lazy-index");
94
+ });
95
+
96
+ test("nested collection docstrings use the compact recursive Array/Set notation", () => {
97
+ // list(set(string)): notation carries the structure; a plain Terraform
98
+ // list needs no prose note.
99
+ expect(providerFunctionsOutput).toContain(
100
+ "@param {Array<Set<string>>} nameGroups",
101
+ );
102
+ expect(providerFunctionsOutput).toContain("@returns {Array<Set<string>>}");
103
+ // set(list(string)): Set<...> notation plus the set-semantics prose note
104
+ // (the jsii type is still an array - the note keeps a reader from
105
+ // literally passing a JS Set).
106
+ expect(providerFunctionsOutput).toContain(
107
+ "@param {Set<Array<string>>} nameLists - Terraform set; ordering is not guaranteed and duplicate values are removed.",
108
+ );
109
+ expect(providerFunctionsOutput).toContain(
110
+ "@returns {Set<Array<string>>} Terraform set; ordering is not guaranteed and duplicate values are removed.",
111
+ );
112
+ });
113
+
114
+ // This package has no TypeScript-compiling harness
115
+ // for generated output (no fixture/test anywhere generates a snippet and
116
+ // feeds it through `tsc`/`ts.transpileModule`, and `cdktn` itself isn't a
117
+ // dependency of this package to typecheck against), so these are
118
+ // structural assertions on the emitted declaration text rather than an
119
+ // actual compile - see also the runtime-level proof in
120
+ // packages/cdktn/test/provider-functions.test.ts, which exercises the
121
+ // same union/token mechanics these signatures rely on.
122
+ describe("composition: a function's return type is directly assignable to another function's parameter of the 'same' Terraform type", () => {
123
+ test("flagsForSeed()'s set(bool) return type (cdktn.IResolvable) is one of the exact union members flagSet()'s set(bool) parameter declares", () => {
124
+ expect(providerFunctionsOutput).toContain(
125
+ "public flagsForSeed(seed: string): cdktn.IResolvable {",
126
+ );
127
+ expect(providerFunctionsOutput).toContain(
128
+ "public flagSet(flags: Array<boolean | cdktn.IResolvable> | cdktn.IResolvable): cdktn.IResolvable {",
129
+ );
130
+ });
131
+
132
+ test("defaultTags()'s map(string) return type is textually identical to tagMap()'s map(string) parameter type", () => {
133
+ expect(providerFunctionsOutput).toContain(
134
+ "public defaultTags(prefix: string): { [key: string]: string } {",
135
+ );
136
+ expect(providerFunctionsOutput).toContain(
137
+ "public tagMap(tags: { [key: string]: string }): string {",
138
+ );
139
+ });
140
+ });
141
+
142
+ test("a list(bool) parameter (not just set(bool)) also accepts a whole-collection token", () => {
143
+ expect(providerFunctionsOutput).toContain(
144
+ "public flagsOrdered(flags: Array<boolean | cdktn.IResolvable> | cdktn.IResolvable): cdktn.IResolvable {",
145
+ );
146
+ });
147
+
148
+ test("a list(object) parameter widens to any[] | cdktn.IResolvable (whole-collection token accepted)", () => {
149
+ expect(providerFunctionsOutput).toContain(
150
+ "public describeConfigs(configs: any[] | cdktn.IResolvable): string {",
151
+ );
152
+ });
153
+ });
154
+
155
+ test("buildProviderFunctionsModel throws when two function names collapse to the same generated method name", () => {
156
+ expect(() =>
157
+ buildProviderFunctionsModel("example", {
158
+ foo_bar: { return_type: "string", parameters: [] },
159
+ foo__bar: { return_type: "string", parameters: [] },
160
+ }),
161
+ ).toThrow(/foo_bar/);
162
+ expect(() =>
163
+ buildProviderFunctionsModel("example", {
164
+ foo_bar: { return_type: "string", parameters: [] },
165
+ foo__bar: { return_type: "string", parameters: [] },
166
+ }),
167
+ ).toThrow(/foo__bar/);
168
+ });
169
+
170
+ test("buildProviderFunctionsModel throws when two parameter names collapse within one function", () => {
171
+ expect(() =>
172
+ buildProviderFunctionsModel("example", {
173
+ my_function: {
174
+ return_type: "string",
175
+ parameters: [
176
+ { name: "some_value", type: "string" },
177
+ { name: "some__value", type: "string" },
178
+ ],
179
+ },
180
+ }),
181
+ ).toThrow(/some_value/);
182
+ expect(() =>
183
+ buildProviderFunctionsModel("example", {
184
+ my_function: {
185
+ return_type: "string",
186
+ parameters: [
187
+ { name: "some_value", type: "string" },
188
+ { name: "some__value", type: "string" },
189
+ ],
190
+ },
191
+ }),
192
+ ).toThrow(/some__value/);
193
+ });
194
+
195
+ // Both of these assert the model-level throw happens before any file is
196
+ // generated: buildProviderFunctionsModel is called directly, with no
197
+ // CodeMaker/generator involved, so there is no emission step to reach.
198
+ test("buildProviderFunctionsModel throws when a function name sanitizes to 'constructor'", () => {
199
+ // The nested signature literal is cast to `FunctionSignature` (rather than
200
+ // relying on contextual typing from the surrounding index signature)
201
+ // because TypeScript special-cases a property literally named
202
+ // "constructor" - contextually typing it against `Function` (from
203
+ // `Object.prototype`) instead of the index signature's `FunctionSignature`,
204
+ // and spuriously flagging `return_type: "string"` as an error. See
205
+ // https://github.com/microsoft/TypeScript/issues/40776.
206
+ const signature: FunctionSignature = {
207
+ return_type: "string",
208
+ parameters: [],
209
+ };
210
+ expect(() =>
211
+ buildProviderFunctionsModel("example", {
212
+ constructor: signature,
213
+ }),
214
+ ).toThrow(/constructor/);
215
+ expect(() =>
216
+ buildProviderFunctionsModel("example", {
217
+ constructor: signature,
218
+ }),
219
+ ).toThrow(/example/);
220
+ });
221
+
222
+ test("buildProviderFunctionsModel throws when a function name sanitizes to 'providerLocalName'", () => {
223
+ expect(() =>
224
+ buildProviderFunctionsModel("example", {
225
+ provider_local_name: { return_type: "string", parameters: [] },
226
+ }),
227
+ ).toThrow(/provider_local_name/);
228
+ expect(() =>
229
+ buildProviderFunctionsModel("example", {
230
+ provider_local_name: { return_type: "string", parameters: [] },
231
+ }),
232
+ ).toThrow(/providerLocalName/);
233
+ });
234
+
235
+ test("assertNoFunctionsGetterCollision throws when the provider's own config schema would generate a 'functions' property", () => {
236
+ expect(() =>
237
+ assertNoFunctionsGetterCollision("example", ["alias", "functions"]),
238
+ ).toThrow(/"functions"/);
239
+ });
240
+
241
+ test("assertNoFunctionsGetterCollision does not throw when there is no colliding attribute", () => {
242
+ expect(() =>
243
+ assertNoFunctionsGetterCollision("example", ["alias"]),
244
+ ).not.toThrow();
245
+ });
246
+
247
+ test("generation throws when a provider's config attribute collides with the generated 'functions' getter", async () => {
248
+ const code = new CodeMaker();
249
+ const spec = JSON.parse(
250
+ fs.readFileSync(
251
+ path.join(
252
+ __dirname,
253
+ "fixtures",
254
+ "provider-functions-collision.test.fixture.json",
255
+ ),
256
+ "utf-8",
257
+ ),
258
+ );
259
+ expect(() =>
260
+ new TerraformProviderGenerator(code, spec).generateAll(),
261
+ ).toThrow(/"functions"/);
262
+ });
@@ -25,6 +25,10 @@ test("generate provider", async () => {
25
25
  "utf-8",
26
26
  );
27
27
  expect(output).toMatchSnapshot();
28
+ // aws has no provider-defined functions in this fixture - no getter, no
29
+ // cross-directory import should be emitted.
30
+ expect(output).not.toContain("public get functions()");
31
+ expect(output).not.toContain("provider-functions");
28
32
  });
29
33
 
30
34
  test("generate provider with only block_types", async () => {
@@ -48,4 +52,8 @@ test("generate provider with only block_types", async () => {
48
52
  "utf-8",
49
53
  );
50
54
  expect(output).toMatchSnapshot();
55
+ // elasticstack has no provider-defined functions in this fixture - no
56
+ // getter, no cross-directory import should be emitted.
57
+ expect(output).not.toContain("public get functions()");
58
+ expect(output).not.toContain("provider-functions");
51
59
  });
@@ -0,0 +1,71 @@
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 { TerraformProviderGenerator } from "../../generator/provider-generator";
6
+ import { CodeMaker } from "codemaker";
7
+ import { createTmpHelper } from "../util";
8
+
9
+ const tmp = createTmpHelper();
10
+
11
+ test("generate a vault_alicloud_secret_backend resource with a write-only attribute", async () => {
12
+ const code = new CodeMaker();
13
+ const workdir = tmp("write-only.test");
14
+ const spec = JSON.parse(
15
+ fs.readFileSync(
16
+ path.join(__dirname, "fixtures", "write-only.test.fixture.json"),
17
+ "utf-8",
18
+ ),
19
+ );
20
+ new TerraformProviderGenerator(code, spec).generateAll();
21
+ await code.save(workdir);
22
+
23
+ const output = fs.readFileSync(
24
+ path.join(workdir, "providers/vault/alicloud-secret-backend/index.ts"),
25
+ "utf-8",
26
+ );
27
+ expect(output).toMatchSnapshot();
28
+
29
+ // The write-only attribute's getter is deprecated ...
30
+ expect(output).toMatch(
31
+ /@deprecated Write-only: the provider never returns this value[\s\S]*?public get secretKeyWo\(\)/,
32
+ );
33
+ // ... its setter is a plain assignment ...
34
+ expect(output).toMatch(
35
+ /public set secretKeyWo\(value: string\) \{\n\s*this\._secretKeyWo = value;\n\s*\}/,
36
+ );
37
+ // ... and usage registration happens at resolve time, from inside
38
+ // synthesizeAttributes()/synthesizeHclAttributes(), by wrapping the
39
+ // mapped value in markWriteOnlyAttribute() ...
40
+ expect(output).toMatch(
41
+ /secret_key_wo: this\.markWriteOnlyAttribute\(cdktn\.stringToTerraform\(this\._secretKeyWo\)\),/,
42
+ );
43
+ expect(output).toMatch(
44
+ /value: this\.markWriteOnlyAttribute\(cdktn\.stringToHclTerraform\(this\._secretKeyWo\)\),/,
45
+ );
46
+
47
+ // The required write-only attribute gets no reset method (required
48
+ // attributes never do) ...
49
+ expect(output).not.toMatch(/resetSecretKeyWo\b/);
50
+ // ... but the OPTIONAL write-only sibling does, exercising the reset ->
51
+ // clear -> synth-passes path end to end at the generator level.
52
+ expect(output).toMatch(
53
+ /public resetSecondarySecretKeyWo\(\) \{\n\s*this\._secondarySecretKeyWo = undefined;\n\s*\}/,
54
+ );
55
+ expect(output).toMatch(
56
+ /secondary_secret_key_wo: this\.markWriteOnlyAttribute\(cdktn\.stringToTerraform\(this\._secondarySecretKeyWo\)\),/,
57
+ );
58
+
59
+ // The non-write-only sibling attribute is untouched: its getter is
60
+ // emitted directly (no @deprecated JSDoc immediately above), its setter
61
+ // has no registration call, and its synthesis isn't wrapped.
62
+ expect(output).toMatch(
63
+ /private _secretKeyWoVersion\?: number; \n\s*public get secretKeyWoVersion\(\)/,
64
+ );
65
+ expect(output).toMatch(
66
+ /public set secretKeyWoVersion\(value: number\) \{\n\s*this\._secretKeyWoVersion = value;\n\s*\}/,
67
+ );
68
+ expect(output).toMatch(
69
+ /secret_key_wo_version: cdktn\.numberToTerraform\(this\._secretKeyWoVersion\),/,
70
+ );
71
+ });
@@ -0,0 +1,206 @@
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
+ TerraformDependencyConstraint,
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, GetOptions } from "../constructs-maker";
19
+
20
+ const tmp = createTmpHelper();
21
+
22
+ const constraint: TerraformDependencyConstraint = {
23
+ fqn: "hashicorp/random",
24
+ name: "random",
25
+ source: "hashicorp/random",
26
+ version: "3.1.3",
27
+ };
28
+
29
+ // Minimal schema - no resources/data sources - so generation stays within
30
+ // pure TypeScript emission (no jsii-pacmak invocation), mirroring the
31
+ // "empty-provider-resources" fixture used elsewhere in this package.
32
+ const fakeSchema = {
33
+ providerSchema: {
34
+ format_version: "0.2" as const,
35
+ provider_schemas: {
36
+ "registry.terraform.io/hashicorp/random": {
37
+ resource_schemas: null,
38
+ data_source_schemas: null,
39
+ },
40
+ },
41
+ provider_versions: {
42
+ "registry.terraform.io/hashicorp/random": "3.1.3",
43
+ },
44
+ cli_name: "terraform",
45
+ cli_version: "1.7.5",
46
+ },
47
+ };
48
+
49
+ describe("targetVersions threading through 'cdktn get'", () => {
50
+ beforeEach(() => {
51
+ mockReadSchema.mockReset();
52
+ mockReadSchema.mockResolvedValue(fakeSchema);
53
+ });
54
+
55
+ it("passes GetOptions.targetVersions through to readSchema", async () => {
56
+ const workdir = tmp("target-versions-thread.test");
57
+ const targetVersions: TerraformTargetVersions = { terraform: ">=1.5.7" };
58
+ const options: GetOptions = {
59
+ codeMakerOutput: workdir,
60
+ targetLanguage: Language.TYPESCRIPT,
61
+ targetVersions,
62
+ };
63
+
64
+ const maker = new ConstructsMaker(options, "/some/cache/path");
65
+ await maker.generate([constraint]);
66
+
67
+ expect(mockReadSchema).toHaveBeenCalledWith(
68
+ [constraint],
69
+ "/some/cache/path",
70
+ targetVersions,
71
+ );
72
+ });
73
+
74
+ it("omits targetVersions from the readSchema call when not configured", async () => {
75
+ const workdir = tmp("target-versions-absent.test");
76
+ const options: GetOptions = {
77
+ codeMakerOutput: workdir,
78
+ targetLanguage: Language.TYPESCRIPT,
79
+ };
80
+
81
+ const maker = new ConstructsMaker(options);
82
+ await maker.generate([constraint]);
83
+
84
+ expect(mockReadSchema).toHaveBeenCalledWith(
85
+ [constraint],
86
+ undefined,
87
+ undefined,
88
+ );
89
+ });
90
+
91
+ it("stamps constraints.json with targetVersions + cli identity, and older constraints.json files without those fields are still treated as fresh (no forced regeneration)", async () => {
92
+ const workdir = tmp("target-versions-stamp.test");
93
+ const targetVersions: TerraformTargetVersions = { terraform: ">=1.5.7" };
94
+ const options: GetOptions = {
95
+ codeMakerOutput: workdir,
96
+ targetLanguage: Language.TYPESCRIPT,
97
+ targetVersions,
98
+ };
99
+
100
+ const maker = new ConstructsMaker(options);
101
+ await maker.generate([constraint]);
102
+
103
+ const constraintsFile = JSON.parse(
104
+ await fs.readFile(path.join(workdir, "constraints.json"), "utf8"),
105
+ );
106
+ expect(constraintsFile.targetVersions).toEqual(targetVersions);
107
+ expect(constraintsFile.cli).toEqual({
108
+ name: "terraform",
109
+ version: "1.7.5",
110
+ });
111
+
112
+ // Rewrite constraints.json as it would have looked before this feature
113
+ // landed (no targetVersions/cli fields), keeping the rest identical -
114
+ // this simulates re-running `cdktn get` against an output directory
115
+ // generated by an older cdktn version.
116
+ const {
117
+ targetVersions: _tv,
118
+ cli: _cli,
119
+ ...legacyConstraints
120
+ } = constraintsFile;
121
+ await fs.writeFile(
122
+ path.join(workdir, "constraints.json"),
123
+ JSON.stringify(legacyConstraints, null, 2),
124
+ );
125
+
126
+ const maker2 = new ConstructsMaker(options);
127
+ const toGenerate = await maker2.filterAlreadyGenerated([constraint]);
128
+ expect(toGenerate).toEqual([]);
129
+ });
130
+
131
+ it("threads a partial targetVersions (single product) through unchanged and stamps it verbatim", async () => {
132
+ const workdir = tmp("target-versions-partial.test");
133
+ const targetVersions: TerraformTargetVersions = { opentofu: ">=1.11.0" };
134
+ const options: GetOptions = {
135
+ codeMakerOutput: workdir,
136
+ targetLanguage: Language.TYPESCRIPT,
137
+ targetVersions,
138
+ };
139
+
140
+ const maker = new ConstructsMaker(options);
141
+ await maker.generate([constraint]);
142
+
143
+ expect(mockReadSchema).toHaveBeenCalledWith(
144
+ [constraint],
145
+ undefined,
146
+ targetVersions,
147
+ );
148
+
149
+ // The stamp records exactly what the project declared - no terraform
150
+ // default is injected at get-time (defaulting for the undeclared
151
+ // product happens only at synth-time, in resolveTargetVersions).
152
+ const constraintsFile = JSON.parse(
153
+ await fs.readFile(path.join(workdir, "constraints.json"), "utf8"),
154
+ );
155
+ expect(constraintsFile.targetVersions).toEqual({ opentofu: ">=1.11.0" });
156
+ });
157
+
158
+ it("omits the cli stamp when the fetched schema carries no CLI version metadata", async () => {
159
+ const workdir = tmp("target-versions-no-cli.test");
160
+ const {
161
+ cli_name: _cliName,
162
+ cli_version: _cliVersion,
163
+ ...schemaWithoutCli
164
+ } = fakeSchema.providerSchema;
165
+ mockReadSchema.mockResolvedValue({ providerSchema: schemaWithoutCli });
166
+
167
+ const targetVersions: TerraformTargetVersions = { terraform: ">=1.5.7" };
168
+ const options: GetOptions = {
169
+ codeMakerOutput: workdir,
170
+ targetLanguage: Language.TYPESCRIPT,
171
+ targetVersions,
172
+ };
173
+
174
+ const maker = new ConstructsMaker(options);
175
+ await maker.generate([constraint]);
176
+
177
+ const constraintsFile = JSON.parse(
178
+ await fs.readFile(path.join(workdir, "constraints.json"), "utf8"),
179
+ );
180
+ expect(constraintsFile.cli).toBeUndefined();
181
+ expect(constraintsFile.targetVersions).toEqual(targetVersions);
182
+ });
183
+
184
+ it("does not force regeneration when targetVersions change between runs", async () => {
185
+ // The stamp is diagnostic-only: per the GetOptions.targetVersions doc,
186
+ // targetVersions never filters codegen (the full schema surface is
187
+ // always generated), so freshness only compares the providers map and
188
+ // the cdktf version.
189
+ const workdir = tmp("target-versions-change.test");
190
+ const options: GetOptions = {
191
+ codeMakerOutput: workdir,
192
+ targetLanguage: Language.TYPESCRIPT,
193
+ targetVersions: { terraform: ">=1.5.7" },
194
+ };
195
+
196
+ const maker = new ConstructsMaker(options);
197
+ await maker.generate([constraint]);
198
+
199
+ const maker2 = new ConstructsMaker({
200
+ ...options,
201
+ targetVersions: { terraform: ">=1.11.0" },
202
+ });
203
+ const toGenerate = await maker2.filterAlreadyGenerated([constraint]);
204
+ expect(toGenerate).toEqual([]);
205
+ });
206
+ });
@@ -15,6 +15,7 @@ import {
15
15
  ModuleSchema,
16
16
  Errors,
17
17
  type LanguageOptions,
18
+ type TerraformTargetVersions,
18
19
  } from "@cdktn/commons";
19
20
  import { DISPLAY_VERSION, Language } from "@cdktn/commons";
20
21
  import { TerraformProviderGenerator } from "./generator/provider-generator";
@@ -265,7 +266,23 @@ export async function generateJsiiLanguage(
265
266
  });
266
267
  }
267
268
 
268
- type ConstraintFile = { providers: Record<string, string>; cdktf: string };
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
+ };
269
286
 
270
287
  export interface GetOptions {
271
288
  readonly targetLanguage: Language;
@@ -280,6 +297,13 @@ export interface GetOptions {
280
297
  * Language-specific code generation options.
281
298
  */
282
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;
283
307
  }
284
308
 
285
309
  export class ConstructsMaker {
@@ -570,6 +594,7 @@ export class ConstructsMaker {
570
594
  // this is used for caching purposes
571
595
  private emitConstraintsFile(
572
596
  allowedConstraints: TerraformDependencyConstraint[],
597
+ providerSchema?: ProviderSchema,
573
598
  ) {
574
599
  const filePath = "constraints.json";
575
600
 
@@ -586,6 +611,17 @@ export class ConstructsMaker {
586
611
  ),
587
612
  };
588
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
+
589
625
  this.code.openFile(filePath);
590
626
  this.code.line(JSON.stringify(content, null, 2));
591
627
  this.code.closeFile(filePath);
@@ -686,7 +722,11 @@ a NODE_OPTIONS variable, we won't override it. Hence, the provider generation mi
686
722
  }
687
723
 
688
724
  public async getSchemas(targets: TerraformDependencyConstraint[]) {
689
- return await readSchema(targets, this.schemaCachePath);
725
+ return await readSchema(
726
+ targets,
727
+ this.schemaCachePath,
728
+ this.options.targetVersions,
729
+ );
690
730
  }
691
731
 
692
732
  public async generate(
@@ -708,7 +748,7 @@ a NODE_OPTIONS variable, we won't override it. Hence, the provider generation mi
708
748
  endGenerateTimer();
709
749
 
710
750
  this.updateVersionsFile(allConstraints);
711
- this.emitConstraintsFile(allConstraints);
751
+ this.emitConstraintsFile(allConstraints, schemas.providerSchema);
712
752
 
713
753
  if (this.isJavascriptTarget) {
714
754
  await this.save();