@cdktn/provider-generator 0.25.0-pre.3 → 0.25.0-pre.30

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 (49) hide show
  1. package/build/__tests__/edge-provider-schema/builder.js +3 -3
  2. package/build/__tests__/edge-provider-schema/cli.js +18 -8
  3. package/build/__tests__/edge-provider-schema.test.js +18 -8
  4. package/build/__tests__/provider.test.js +18 -8
  5. package/build/get/__tests__/constructs-maker.test.js +91 -8
  6. package/build/get/__tests__/generator/complex-computed-types.test.js +18 -8
  7. package/build/get/__tests__/generator/deep-nested-attributes.test.js +18 -8
  8. package/build/get/__tests__/generator/description-escaping.test.js +18 -8
  9. package/build/get/__tests__/generator/empty-provider-resources.test.js +18 -8
  10. package/build/get/__tests__/generator/ephemeral-resources.test.js +18 -8
  11. package/build/get/__tests__/generator/export-sharding.test.js +18 -8
  12. package/build/get/__tests__/generator/import-style.test.js +18 -8
  13. package/build/get/__tests__/generator/module-generator.test.js +18 -8
  14. package/build/get/__tests__/generator/nested-types.test.js +18 -8
  15. package/build/get/__tests__/generator/provider-functions-name-collision.test.d.ts +2 -0
  16. package/build/get/__tests__/generator/provider-functions-name-collision.test.js +89 -0
  17. package/build/get/__tests__/generator/provider-functions.test.js +18 -8
  18. package/build/get/__tests__/generator/provider.test.js +18 -8
  19. package/build/get/__tests__/generator/resource-types.test.js +18 -8
  20. package/build/get/__tests__/generator/skipped-attributes.test.js +18 -8
  21. package/build/get/__tests__/generator/types.test.js +18 -8
  22. package/build/get/__tests__/generator/versions-file.test.js +18 -8
  23. package/build/get/__tests__/generator/write-only.test.js +18 -8
  24. package/build/get/__tests__/target-versions.test.js +18 -8
  25. package/build/get/__tests__/util.js +20 -11
  26. package/build/get/constructs-maker.d.ts +17 -0
  27. package/build/get/constructs-maker.js +67 -10
  28. package/build/get/generator/emitter/struct-emitter.js +18 -8
  29. package/build/get/generator/loop-detection.js +2 -3
  30. package/build/get/generator/models/attribute-model.js +3 -3
  31. package/build/get/generator/models/attribute-type-model.js +3 -3
  32. package/build/get/generator/models/provider-function-model.js +3 -4
  33. package/build/get/generator/models/supported-stored-classes.js +2 -3
  34. package/build/get/generator/provider-generator.d.ts +12 -0
  35. package/build/get/generator/provider-generator.js +18 -3
  36. package/build/get/generator/resource-parser.js +10 -4
  37. package/build/get/generator/sanitized-comments.js +2 -3
  38. package/build/get/generator/skipped-attributes.js +2 -3
  39. package/build/index.js +3 -3
  40. package/build/util.js +3 -4
  41. package/package.json +9 -9
  42. package/src/get/__tests__/constructs-maker.test.ts +125 -1
  43. package/src/get/__tests__/generator/fixtures/provider-functions-name-collision.test.fixture.json +42 -0
  44. package/src/get/__tests__/generator/provider-functions-name-collision.test.ts +84 -0
  45. package/src/get/constructs-maker.ts +64 -0
  46. package/src/get/generator/models/attribute-type-model.ts +1 -2
  47. package/src/get/generator/provider-generator.ts +21 -2
  48. package/src/get/generator/resource-parser.ts +11 -2
  49. package/tsconfig.json +4 -0
package/build/util.js CHANGED
@@ -2,7 +2,8 @@
2
2
  // Copyright (c) HashiCorp, Inc
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.uppercaseFirst = exports.downcaseFirst = void 0;
5
+ exports.downcaseFirst = downcaseFirst;
6
+ exports.uppercaseFirst = uppercaseFirst;
6
7
  /**
7
8
  * Downcase the first character in a string.
8
9
  *
@@ -14,7 +15,6 @@ function downcaseFirst(str) {
14
15
  }
15
16
  return `${str[0].toLocaleLowerCase()}${str.slice(1)}`;
16
17
  }
17
- exports.downcaseFirst = downcaseFirst;
18
18
  /**
19
19
  * Uppercase the first character in a string.
20
20
  *
@@ -26,5 +26,4 @@ function uppercaseFirst(str) {
26
26
  }
27
27
  return `${str[0].toLocaleUpperCase()}${str.slice(1)}`;
28
28
  }
29
- exports.uppercaseFirst = uppercaseFirst;
30
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSwrQkFBK0I7QUFDL0IsbUNBQW1DOzs7QUFFbkM7Ozs7R0FJRztBQUNILFNBQWdCLGFBQWEsQ0FBQyxHQUFXO0lBQ3ZDLElBQUksR0FBRyxLQUFLLEVBQUUsRUFBRSxDQUFDO1FBQ2YsT0FBTyxHQUFHLENBQUM7SUFDYixDQUFDO0lBQ0QsT0FBTyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxpQkFBaUIsRUFBRSxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztBQUN4RCxDQUFDO0FBTEQsc0NBS0M7QUFFRDs7OztHQUlHO0FBQ0gsU0FBZ0IsY0FBYyxDQUFDLEdBQVc7SUFDeEMsSUFBSSxHQUFHLEtBQUssRUFBRSxFQUFFLENBQUM7UUFDZixPQUFPLEdBQUcsQ0FBQztJQUNiLENBQUM7SUFDRCxPQUFPLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLGlCQUFpQixFQUFFLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0FBQ3hELENBQUM7QUFMRCx3Q0FLQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAoYykgSGFzaGlDb3JwLCBJbmNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBNUEwtMi4wXG5cbi8qKlxuICogRG93bmNhc2UgdGhlIGZpcnN0IGNoYXJhY3RlciBpbiBhIHN0cmluZy5cbiAqXG4gKiBAcGFyYW0gc3RyIHRoZSBzdHJpbmcgdG8gYmUgcHJvY2Vzc2VkLlxuICovXG5leHBvcnQgZnVuY3Rpb24gZG93bmNhc2VGaXJzdChzdHI6IHN0cmluZyk6IHN0cmluZyB7XG4gIGlmIChzdHIgPT09IFwiXCIpIHtcbiAgICByZXR1cm4gc3RyO1xuICB9XG4gIHJldHVybiBgJHtzdHJbMF0udG9Mb2NhbGVMb3dlckNhc2UoKX0ke3N0ci5zbGljZSgxKX1gO1xufVxuXG4vKipcbiAqIFVwcGVyY2FzZSB0aGUgZmlyc3QgY2hhcmFjdGVyIGluIGEgc3RyaW5nLlxuICpcbiAqIEBwYXJhbSBzdHIgdGhlIHN0cmluZyB0byBiZSBwcm9jZXNzZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB1cHBlcmNhc2VGaXJzdChzdHI6IHN0cmluZyk6IHN0cmluZyB7XG4gIGlmIChzdHIgPT09IFwiXCIpIHtcbiAgICByZXR1cm4gc3RyO1xuICB9XG4gIHJldHVybiBgJHtzdHJbMF0udG9Mb2NhbGVVcHBlckNhc2UoKX0ke3N0ci5zbGljZSgxKX1gO1xufVxuIl19
29
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSwrQkFBK0I7QUFDL0IsbUNBQW1DOztBQU9uQyxzQ0FLQztBQU9ELHdDQUtDO0FBdEJEOzs7O0dBSUc7QUFDSCxTQUFnQixhQUFhLENBQUMsR0FBVztJQUN2QyxJQUFJLEdBQUcsS0FBSyxFQUFFLEVBQUUsQ0FBQztRQUNmLE9BQU8sR0FBRyxDQUFDO0lBQ2IsQ0FBQztJQUNELE9BQU8sR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsaUJBQWlCLEVBQUUsR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7QUFDeEQsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxTQUFnQixjQUFjLENBQUMsR0FBVztJQUN4QyxJQUFJLEdBQUcsS0FBSyxFQUFFLEVBQUUsQ0FBQztRQUNmLE9BQU8sR0FBRyxDQUFDO0lBQ2IsQ0FBQztJQUNELE9BQU8sR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsaUJBQWlCLEVBQUUsR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7QUFDeEQsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAoYykgSGFzaGlDb3JwLCBJbmNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBNUEwtMi4wXG5cbi8qKlxuICogRG93bmNhc2UgdGhlIGZpcnN0IGNoYXJhY3RlciBpbiBhIHN0cmluZy5cbiAqXG4gKiBAcGFyYW0gc3RyIHRoZSBzdHJpbmcgdG8gYmUgcHJvY2Vzc2VkLlxuICovXG5leHBvcnQgZnVuY3Rpb24gZG93bmNhc2VGaXJzdChzdHI6IHN0cmluZyk6IHN0cmluZyB7XG4gIGlmIChzdHIgPT09IFwiXCIpIHtcbiAgICByZXR1cm4gc3RyO1xuICB9XG4gIHJldHVybiBgJHtzdHJbMF0udG9Mb2NhbGVMb3dlckNhc2UoKX0ke3N0ci5zbGljZSgxKX1gO1xufVxuXG4vKipcbiAqIFVwcGVyY2FzZSB0aGUgZmlyc3QgY2hhcmFjdGVyIGluIGEgc3RyaW5nLlxuICpcbiAqIEBwYXJhbSBzdHIgdGhlIHN0cmluZyB0byBiZSBwcm9jZXNzZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB1cHBlcmNhc2VGaXJzdChzdHI6IHN0cmluZyk6IHN0cmluZyB7XG4gIGlmIChzdHIgPT09IFwiXCIpIHtcbiAgICByZXR1cm4gc3RyO1xuICB9XG4gIHJldHVybiBgJHtzdHJbMF0udG9Mb2NhbGVVcHBlckNhc2UoKX0ke3N0ci5zbGljZSgxKX1gO1xufVxuIl19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdktn/provider-generator",
3
- "version": "0.25.0-pre.3",
3
+ "version": "0.25.0-pre.30",
4
4
  "description": "Exposes API to generate Terraform CDK provider bindings",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -29,20 +29,20 @@
29
29
  ],
30
30
  "license": "MPL-2.0",
31
31
  "dependencies": {
32
- "codemaker": "1.128.0",
33
- "constructs": "10.6.0",
32
+ "codemaker": "1.140.0",
33
+ "constructs": "10.8.1",
34
34
  "fs-extra": "11.3.6",
35
35
  "glob": "13.0.6",
36
- "jsii": "~5.9.0",
37
- "jsii-pacmak": "1.128.0",
38
- "@cdktn/provider-schema": "0.25.0-pre.3",
39
- "cdktn": "0.25.0-pre.3",
40
- "@cdktn/commons": "0.25.0-pre.3"
36
+ "jsii": "~6.0.0",
37
+ "jsii-pacmak": "1.140.0",
38
+ "@cdktn/commons": "0.25.0-pre.30",
39
+ "@cdktn/provider-schema": "0.25.0-pre.30",
40
+ "cdktn": "0.25.0-pre.30"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/fs-extra": "11.0.4",
44
44
  "@types/node": "22.20.1",
45
- "typescript": "^5.0.4"
45
+ "typescript": "~6.0.0"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc",
@@ -8,7 +8,7 @@ import {
8
8
  ConstructsMakerModuleTarget,
9
9
  ConstructsMakerProviderTarget,
10
10
  } from "@cdktn/commons";
11
- import { determineGoModuleName } from "../constructs-maker";
11
+ import { ConstructsMaker, determineGoModuleName } from "../constructs-maker";
12
12
 
13
13
  describe("constructsMaker", () => {
14
14
  describe("determineGoModuleName", () => {
@@ -115,4 +115,128 @@ describe("constructsMaker", () => {
115
115
  expect(target.srcMakName).toEqual("security_group");
116
116
  });
117
117
  });
118
+
119
+ describe("removeOrphanedResourceFolders", () => {
120
+ let tmpDir: string;
121
+
122
+ const providerDir = (provider: string) =>
123
+ path.join(tmpDir, "providers", provider);
124
+
125
+ /**
126
+ * Lay out a provider folder containing the given resource directories plus
127
+ * the provider-level index files a real generation run writes.
128
+ */
129
+ const seedProvider = async (provider: string, resources: string[]) => {
130
+ for (const resource of resources) {
131
+ await fs.mkdirs(path.join(providerDir(provider), resource));
132
+ await fs.writeFile(
133
+ path.join(providerDir(provider), resource, "index.ts"),
134
+ "// generated\n",
135
+ );
136
+ }
137
+ await fs.writeFile(
138
+ path.join(providerDir(provider), "index.ts"),
139
+ "// generated by cdktn get\n",
140
+ );
141
+ await fs.writeFile(
142
+ path.join(providerDir(provider), "lazy-index.ts"),
143
+ "// generated by cdktn get\n",
144
+ );
145
+ };
146
+
147
+ const makeMaker = (targetLanguage: Language) =>
148
+ new ConstructsMaker({
149
+ codeMakerOutput: tmpDir,
150
+ targetLanguage,
151
+ });
152
+
153
+ beforeEach(async () => {
154
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "cdktf.orphans"));
155
+ });
156
+
157
+ afterEach(async () => {
158
+ await fs.remove(tmpDir);
159
+ });
160
+
161
+ it("removes resource folders the current run did not emit", async () => {
162
+ // 'teams' and 'data-lake-pipeline' exist only in the previously generated
163
+ // provider version; the new schema no longer produces them.
164
+ await seedProvider("mongodbatlas", [
165
+ "cluster",
166
+ "teams",
167
+ "data-lake-pipeline",
168
+ ]);
169
+
170
+ const maker = makeMaker(Language.TYPESCRIPT);
171
+ maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
172
+ await maker.removeOrphanedResourceFolders();
173
+
174
+ expect(
175
+ await fs.pathExists(path.join(providerDir("mongodbatlas"), "cluster")),
176
+ ).toBe(true);
177
+ expect(
178
+ await fs.pathExists(path.join(providerDir("mongodbatlas"), "teams")),
179
+ ).toBe(false);
180
+ expect(
181
+ await fs.pathExists(
182
+ path.join(providerDir("mongodbatlas"), "data-lake-pipeline"),
183
+ ),
184
+ ).toBe(false);
185
+ });
186
+
187
+ it("keeps provider-level files", async () => {
188
+ await seedProvider("mongodbatlas", ["cluster", "teams"]);
189
+
190
+ const maker = makeMaker(Language.TYPESCRIPT);
191
+ maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
192
+ await maker.removeOrphanedResourceFolders();
193
+
194
+ expect(
195
+ await fs.pathExists(path.join(providerDir("mongodbatlas"), "index.ts")),
196
+ ).toBe(true);
197
+ expect(
198
+ await fs.pathExists(
199
+ path.join(providerDir("mongodbatlas"), "lazy-index.ts"),
200
+ ),
201
+ ).toBe(true);
202
+ });
203
+
204
+ it("leaves providers that were not regenerated untouched", async () => {
205
+ // filterAlreadyGenerated can skip a provider entirely, in which case it
206
+ // has no entry in emittedResourceFolders and must not be pruned.
207
+ await seedProvider("mongodbatlas", ["cluster"]);
208
+ await seedProvider("onepassword", ["item"]);
209
+
210
+ const maker = makeMaker(Language.TYPESCRIPT);
211
+ maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
212
+ await maker.removeOrphanedResourceFolders();
213
+
214
+ expect(
215
+ await fs.pathExists(path.join(providerDir("onepassword"), "item")),
216
+ ).toBe(true);
217
+ });
218
+
219
+ it("is a no-op for non-TypeScript targets", async () => {
220
+ // Other languages co-locate providers and modules, so a dropped resource
221
+ // is indistinguishable from a hand-written construct.
222
+ await seedProvider("mongodbatlas", ["cluster", "teams"]);
223
+
224
+ const maker = makeMaker(Language.PYTHON);
225
+ maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
226
+ await maker.removeOrphanedResourceFolders();
227
+
228
+ expect(
229
+ await fs.pathExists(path.join(providerDir("mongodbatlas"), "teams")),
230
+ ).toBe(true);
231
+ });
232
+
233
+ it("does not throw when the provider folder is missing", async () => {
234
+ const maker = makeMaker(Language.TYPESCRIPT);
235
+ maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
236
+
237
+ await expect(
238
+ maker.removeOrphanedResourceFolders(),
239
+ ).resolves.not.toThrow();
240
+ });
241
+ });
118
242
  });
@@ -0,0 +1,42 @@
1
+ {
2
+ "provider_schemas": {
3
+ "registry.terraform.io/hashicorp/example": {
4
+ "resource_schemas": {
5
+ "example_provider_functions": {
6
+ "version": 0,
7
+ "block": {
8
+ "attributes": {
9
+ "id": {
10
+ "type": "string",
11
+ "description_kind": "plain",
12
+ "computed": true
13
+ },
14
+ "name": {
15
+ "type": "string",
16
+ "description_kind": "plain",
17
+ "optional": true
18
+ }
19
+ }
20
+ }
21
+ }
22
+ },
23
+ "functions": {
24
+ "sum": {
25
+ "description": "Adds up two numbers.",
26
+ "summary": "Sum numbers",
27
+ "return_type": "number",
28
+ "parameters": [
29
+ {
30
+ "name": "a",
31
+ "type": "number"
32
+ },
33
+ {
34
+ "name": "b",
35
+ "type": "number"
36
+ }
37
+ ]
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,84 @@
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 { TerraformProviderGenerator } from "../../generator/provider-generator";
7
+ import { sanitizeClassOrNamespaceName } from "../../generator/resource-parser";
8
+ import { createTmpHelper } from "../util";
9
+
10
+ const tmp = createTmpHelper();
11
+
12
+ // A provider declaring provider-defined functions emits them to
13
+ // providers/<provider>/provider-functions/index.ts. A resource named
14
+ // <provider>_provider_functions maps to that same directory, so without a
15
+ // guard the resource overwrites the functions wrapper and the provider index
16
+ // exports the name twice.
17
+ describe("resource named like the provider functions submodule", () => {
18
+ let workdir: string;
19
+
20
+ beforeAll(async () => {
21
+ const code = new CodeMaker();
22
+ workdir = tmp("provider-functions-name-collision.test");
23
+ const spec = JSON.parse(
24
+ fs.readFileSync(
25
+ path.join(
26
+ __dirname,
27
+ "fixtures",
28
+ "provider-functions-name-collision.test.fixture.json",
29
+ ),
30
+ "utf-8",
31
+ ),
32
+ );
33
+ new TerraformProviderGenerator(code, spec).generateAll();
34
+ await code.save(workdir);
35
+ });
36
+
37
+ const read = (relPath: string) =>
38
+ fs.readFileSync(path.join(workdir, relPath), "utf-8");
39
+
40
+ test("the functions submodule holds the functions wrapper, not the resource", () => {
41
+ const output = read("providers/example/provider-functions/index.ts");
42
+ expect(output).toContain("class ExampleProviderFunctions");
43
+ expect(output).not.toContain("cdktn.TerraformResource");
44
+ });
45
+
46
+ test("the resource is emitted to its own directory", () => {
47
+ const output = read(
48
+ "providers/example/provider-functions-resource/index.ts",
49
+ );
50
+ expect(output).toContain("class ProviderFunctionsResource");
51
+ expect(output).toContain("cdktn.TerraformResource");
52
+ });
53
+
54
+ test("the provider index exports both, without duplicate names", () => {
55
+ const index = read("providers/example/index.ts");
56
+ expect(index).toContain(
57
+ "export * as providerFunctions from './provider-functions/index';",
58
+ );
59
+ expect(index).toContain(
60
+ "export * as providerFunctionsResource from './provider-functions-resource/index';",
61
+ );
62
+
63
+ const exportedNames = [...index.matchAll(/export \* as (\w+) from/g)].map(
64
+ (m) => m[1],
65
+ );
66
+ expect(new Set(exportedNames).size).toBe(exportedNames.length);
67
+ });
68
+ });
69
+
70
+ describe("sanitizeClassOrNamespaceName", () => {
71
+ test("suffixes a resource named provider_functions", () => {
72
+ expect(sanitizeClassOrNamespaceName("provider_functions")).toBe(
73
+ "provider_functions_resource",
74
+ );
75
+ });
76
+
77
+ test("still suffixes a resource named provider", () => {
78
+ expect(sanitizeClassOrNamespaceName("provider")).toBe("provider_resource");
79
+ });
80
+
81
+ test("leaves an unrelated resource name alone", () => {
82
+ expect(sanitizeClassOrNamespaceName("function_app")).toBe("function_app");
83
+ });
84
+ });
@@ -315,6 +315,8 @@ export class ConstructsMaker {
315
315
  private readonly codeMakerOutdir: string;
316
316
  private readonly code: CodeMaker;
317
317
  private versions: { [providerName: string]: string | undefined };
318
+ /** Resource directory names emitted per provider folder during this run */
319
+ public emittedResourceFolders: { [providerName: string]: string[] };
318
320
 
319
321
  constructor(
320
322
  private readonly options: GetOptions,
@@ -328,6 +330,7 @@ export class ConstructsMaker {
328
330
  fs.mkdirpSync(this.codeMakerOutdir);
329
331
  this.code = new CodeMaker();
330
332
  this.versions = {};
333
+ this.emittedResourceFolders = {};
331
334
  }
332
335
  private async generateTypescriptProvider(
333
336
  target: ConstructsMakerProviderTarget,
@@ -340,6 +343,10 @@ export class ConstructsMaker {
340
343
  generator.generate(target);
341
344
 
342
345
  this.versions = { ...this.versions, ...generator.versions };
346
+ this.emittedResourceFolders = {
347
+ ...this.emittedResourceFolders,
348
+ ...generator.emittedResourceFolders,
349
+ };
343
350
  endTSTimer();
344
351
  }
345
352
 
@@ -595,6 +602,62 @@ export class ConstructsMaker {
595
602
  });
596
603
  }
597
604
 
605
+ /**
606
+ * Delete resource directories under `providers/` that this run did not emit, so each provider folder holds
607
+ * exactly the resources in the version it was generated against.
608
+ *
609
+ * Walks only providers present in `emittedResourceFolders`, keeping any provider this run skipped. Within those,
610
+ * removes every subdirectory not emitted and leaves provider-level files (`index.ts`, `lazy-index.ts`) in place.
611
+ * Deletes files, so it must run after `save()` has written the current run's output.
612
+ *
613
+ * TypeScript only: `providers/<name>/<resource>/` maps one-to-one onto the emitted index exports. Other languages
614
+ * co-locate providers and modules in the same folders, which makes a dropped resource indistinguishable from a
615
+ * hand-written construct.
616
+ */
617
+ public async removeOrphanedResourceFolders() {
618
+ if (!this.isJavascriptTarget) {
619
+ return;
620
+ }
621
+
622
+ for (const [provider, emittedFolders] of Object.entries(
623
+ this.emittedResourceFolders,
624
+ )) {
625
+ const providerFolder = path.resolve(
626
+ this.codeMakerOutdir,
627
+ "providers",
628
+ provider,
629
+ );
630
+
631
+ let entries: string[] = [];
632
+ try {
633
+ entries = await fs.readdir(providerFolder);
634
+ } catch (e) {
635
+ logger.debug(
636
+ `Could not list resource folders in '${providerFolder}': ${e}`,
637
+ );
638
+ continue;
639
+ }
640
+
641
+ const expected = new Set(emittedFolders);
642
+ for (const entry of entries) {
643
+ if (expected.has(entry)) {
644
+ continue;
645
+ }
646
+
647
+ const entryPath = path.resolve(providerFolder, entry);
648
+ if (!fs.statSync(entryPath).isDirectory()) {
649
+ // Leave index.ts / lazy-index.ts and any other provider-level files alone.
650
+ continue;
651
+ }
652
+
653
+ logger.debug(
654
+ `Removing orphaned resource folder '${entry}' from provider '${provider}': it was not emitted for the current provider version`,
655
+ );
656
+ await fs.remove(entryPath);
657
+ }
658
+ }
659
+ }
660
+
598
661
  // emits a constraints.json file with a map of the used provider fqpns and version constraints
599
662
  // this is used for caching purposes
600
663
  private emitConstraintsFile(
@@ -757,6 +820,7 @@ a NODE_OPTIONS variable, we won't override it. Hence, the provider generation mi
757
820
 
758
821
  if (this.isJavascriptTarget) {
759
822
  await this.save();
823
+ await this.removeOrphanedResourceFolders();
760
824
  }
761
825
 
762
826
  if (!this.isJavascriptTarget || this.options.outputJsii) {
@@ -215,8 +215,7 @@ export interface CollectionAttributeTypeModel extends AttributeTypeModel {
215
215
  // into the nearest typed "Any"-wrapper. undefined only defensively - see
216
216
  // resolveStoredClassName in supported-stored-classes.ts.
217
217
  readonly resolvedStoredClass:
218
- | { name: string; collapsed: boolean }
219
- | undefined;
218
+ { name: string; collapsed: boolean } | undefined;
220
219
  }
221
220
 
222
221
  export class ListAttributeTypeModel implements CollectionAttributeTypeModel {
@@ -113,6 +113,11 @@ export class TerraformProviderGenerator {
113
113
  private providerFunctionsEmitter: ProviderFunctionsEmitter;
114
114
  private readonly importExtension: string;
115
115
  public versions: { [fqpn: string]: string | undefined } = {};
116
+ /**
117
+ * Resource directory names emitted per provider folder Used to prune directories
118
+ * left over from a previous provider version.
119
+ */
120
+ public emittedResourceFolders: { [provider: string]: string[] } = {};
116
121
 
117
122
  constructor(
118
123
  private readonly code: CodeMaker,
@@ -298,6 +303,20 @@ export class TerraformProviderGenerator {
298
303
 
299
304
  this.emitIndexFile(name, files);
300
305
  this.emitLazyIndexFile(name, files);
306
+
307
+ // Record exactly what this run emitted, derived the same way the index
308
+ // files derive their exports, so pruning can never disagree with them.
309
+ this.emittedResourceFolders[name] = files.map((file) =>
310
+ this.resourceFolderName(name, file),
311
+ );
312
+ }
313
+
314
+ /**
315
+ * Map an emitted file path to the resource directory name it lives in, e.g.
316
+ * `providers/mongodbatlas/teams/index.ts` -> `teams`.
317
+ */
318
+ private resourceFolderName(provider: ProviderName, file: string): string {
319
+ return file.replace(`providers/${provider}/`, "").replace("/index.ts", "");
301
320
  }
302
321
 
303
322
  private emitProviderFunctions(
@@ -333,7 +352,7 @@ export class TerraformProviderGenerator {
333
352
  this.code.openFile(filePath);
334
353
  this.code.line("// generated by cdktn get");
335
354
  for (const file of files) {
336
- const dirName = file.replace(`${folder}/`, "").replace("/index.ts", "");
355
+ const dirName = this.resourceFolderName(provider, file);
337
356
  this.code.line(
338
357
  `export * as ${toCamelCase(dirName)} from './${dirName}/index${this.importExtension}';`,
339
358
  );
@@ -348,7 +367,7 @@ export class TerraformProviderGenerator {
348
367
  this.code.openFile(filePath);
349
368
  this.code.line("// generated by cdktn get");
350
369
  for (const file of files) {
351
- const dirName = file.replace(`${folder}/`, "").replace("/index.ts", "");
370
+ const dirName = this.resourceFolderName(provider, file);
352
371
  this.code.line(
353
372
  `Object.defineProperty(exports, '${toCamelCase(
354
373
  dirName,
@@ -82,8 +82,17 @@ export function sanitizeClassOrNamespaceName(
82
82
  isProvider = false,
83
83
  ) {
84
84
  const resourceIsNamedProvider = !isProvider && baseName === "provider";
85
-
86
- if (isReservedClassOrNamespaceName(baseName) || resourceIsNamedProvider) {
85
+ // A resource named `<provider>_provider_functions` maps to the same
86
+ // providers/<provider>/provider-functions/ directory that the provider's
87
+ // functions submodule is emitted to, and would silently overwrite it.
88
+ const resourceIsNamedProviderFunctions =
89
+ !isProvider && baseName === "provider_functions";
90
+
91
+ if (
92
+ isReservedClassOrNamespaceName(baseName) ||
93
+ resourceIsNamedProvider ||
94
+ resourceIsNamedProviderFunctions
95
+ ) {
87
96
  return `${baseName}_resource`;
88
97
  } else {
89
98
  return baseName;
package/tsconfig.json CHANGED
@@ -24,6 +24,10 @@
24
24
  "stripInternal": true,
25
25
  "skipLibCheck": true,
26
26
  "target": "ES2018",
27
+ "types": [
28
+ "node",
29
+ "jest"
30
+ ],
27
31
  "incremental": true,
28
32
  "esModuleInterop": true,
29
33
  "rootDir": "./src",