@cdktn/provider-generator 0.25.0-pre.2 → 0.25.0-pre.21

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 (51) 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 +21 -9
  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 +26 -12
  26. package/build/get/constructs-maker.d.ts +17 -0
  27. package/build/get/constructs-maker.js +72 -12
  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 +11 -11
  42. package/src/__tests__/provider.test.ts +3 -1
  43. package/src/get/__tests__/constructs-maker.test.ts +125 -1
  44. package/src/get/__tests__/generator/fixtures/provider-functions-name-collision.test.fixture.json +42 -0
  45. package/src/get/__tests__/generator/provider-functions-name-collision.test.ts +84 -0
  46. package/src/get/__tests__/util.ts +6 -1
  47. package/src/get/constructs-maker.ts +71 -2
  48. package/src/get/generator/models/attribute-type-model.ts +1 -2
  49. package/src/get/generator/provider-generator.ts +21 -2
  50. package/src/get/generator/resource-parser.ts +11 -2
  51. 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.2",
3
+ "version": "0.25.0-pre.21",
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/commons": "0.25.0-pre.2",
39
- "@cdktn/provider-schema": "0.25.0-pre.2",
40
- "cdktn": "0.25.0-pre.2"
36
+ "jsii": "~6.0.0",
37
+ "jsii-pacmak": "1.140.0",
38
+ "@cdktn/commons": "0.25.0-pre.21",
39
+ "@cdktn/provider-schema": "0.25.0-pre.21",
40
+ "cdktn": "0.25.0-pre.21"
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",
@@ -50,8 +50,8 @@
50
50
  "watch": "tsc -w",
51
51
  "watch-preserve-output": "tsc -w --preserveWatchOutput",
52
52
  "test:update": "jest -u",
53
- "package": "./package.sh",
53
+ "package": "bash package.sh",
54
54
  "dist-clean": "rm -rf dist",
55
- "package:js": "./package.sh"
55
+ "package:js": "bash package.sh"
56
56
  }
57
57
  }
@@ -39,7 +39,9 @@ function directorySnapshot(root: string) {
39
39
  content = fs.readFileSync(filePath, "utf-8");
40
40
  }
41
41
 
42
- output[file] = content;
42
+ // glob yields native separators on Windows; key the snapshot by a
43
+ // "/"-separated path so it is identical on every platform.
44
+ output[file.split(path.sep).join(path.posix.sep)] = content;
43
45
  }
44
46
 
45
47
  return output;
@@ -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
+ });
@@ -36,7 +36,12 @@ export function expectModuleToMatchSnapshot(
36
36
  fixtureNames: string[],
37
37
  ) {
38
38
  test(testName, async () => {
39
- await withTempDir(`${testName.replace(/\s*/, "-")}.test`, async () => {
39
+ // Test names become a directory name, so collapse anything that is not
40
+ // filename-safe. The previous /\s*/ matched the empty string at index 0
41
+ // (no global flag either), which only prepended a "-" and left spaces --
42
+ // and characters like "*" in a test title are illegal in Windows paths.
43
+ const safeName = testName.replace(/[^a-zA-Z0-9._-]+/g, "-");
44
+ await withTempDir(`${safeName}.test`, async () => {
40
45
  const curdir = process.cwd();
41
46
  fs.mkdirSync("module");
42
47
 
@@ -38,9 +38,14 @@ async function resolvePublishableFiles(dir: string): Promise<string[]> {
38
38
  const stdout = await exec("npm", ["pack", "--dry-run", "--json"], {
39
39
  cwd: dir,
40
40
  });
41
- const packInfo = JSON.parse(stdout) as Array<{ files: { path: string }[] }>;
41
+ // npm <=11 reports an array of packed packages; npm >=12 reports an object
42
+ // keyed by package name. Accept either so the build is npm-version agnostic.
43
+ const parsed = JSON.parse(stdout) as
44
+ | Array<{ files: { path: string }[] }>
45
+ | Record<string, { files: { path: string }[] }>;
46
+ const packInfo = Array.isArray(parsed) ? parsed[0] : Object.values(parsed)[0];
42
47
  // Drop `..`-escape paths npm emits when following pnpm's node_modules symlinks.
43
- return packInfo[0].files.map((f) => f.path).filter((p) => !p.includes(".."));
48
+ return packInfo.files.map((f) => f.path).filter((p) => !p.includes(".."));
44
49
  }
45
50
 
46
51
  export interface GenerateJSIIOptions {
@@ -310,6 +315,8 @@ export class ConstructsMaker {
310
315
  private readonly codeMakerOutdir: string;
311
316
  private readonly code: CodeMaker;
312
317
  private versions: { [providerName: string]: string | undefined };
318
+ /** Resource directory names emitted per provider folder during this run */
319
+ public emittedResourceFolders: { [providerName: string]: string[] };
313
320
 
314
321
  constructor(
315
322
  private readonly options: GetOptions,
@@ -323,6 +330,7 @@ export class ConstructsMaker {
323
330
  fs.mkdirpSync(this.codeMakerOutdir);
324
331
  this.code = new CodeMaker();
325
332
  this.versions = {};
333
+ this.emittedResourceFolders = {};
326
334
  }
327
335
  private async generateTypescriptProvider(
328
336
  target: ConstructsMakerProviderTarget,
@@ -335,6 +343,10 @@ export class ConstructsMaker {
335
343
  generator.generate(target);
336
344
 
337
345
  this.versions = { ...this.versions, ...generator.versions };
346
+ this.emittedResourceFolders = {
347
+ ...this.emittedResourceFolders,
348
+ ...generator.emittedResourceFolders,
349
+ };
338
350
  endTSTimer();
339
351
  }
340
352
 
@@ -590,6 +602,62 @@ export class ConstructsMaker {
590
602
  });
591
603
  }
592
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
+
593
661
  // emits a constraints.json file with a map of the used provider fqpns and version constraints
594
662
  // this is used for caching purposes
595
663
  private emitConstraintsFile(
@@ -752,6 +820,7 @@ a NODE_OPTIONS variable, we won't override it. Hence, the provider generation mi
752
820
 
753
821
  if (this.isJavascriptTarget) {
754
822
  await this.save();
823
+ await this.removeOrphanedResourceFolders();
755
824
  }
756
825
 
757
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",