@cdktn/provider-generator 0.25.0-pre.4 → 0.25.0-pre.40

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 (53) 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 +68 -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/resource-model.d.ts +3 -1
  34. package/build/get/generator/models/resource-model.js +16 -10
  35. package/build/get/generator/models/supported-stored-classes.js +2 -3
  36. package/build/get/generator/provider-generator.d.ts +15 -1
  37. package/build/get/generator/provider-generator.js +19 -4
  38. package/build/get/generator/resource-parser.d.ts +3 -1
  39. package/build/get/generator/resource-parser.js +16 -7
  40. package/build/get/generator/sanitized-comments.js +2 -3
  41. package/build/get/generator/skipped-attributes.js +2 -3
  42. package/build/index.js +3 -3
  43. package/build/util.js +3 -4
  44. package/package.json +9 -9
  45. package/src/get/__tests__/constructs-maker.test.ts +125 -1
  46. package/src/get/__tests__/generator/fixtures/provider-functions-name-collision.test.fixture.json +42 -0
  47. package/src/get/__tests__/generator/provider-functions-name-collision.test.ts +84 -0
  48. package/src/get/constructs-maker.ts +65 -0
  49. package/src/get/generator/models/attribute-type-model.ts +1 -2
  50. package/src/get/generator/models/resource-model.ts +27 -9
  51. package/src/get/generator/provider-generator.ts +26 -3
  52. package/src/get/generator/resource-parser.ts +21 -3
  53. package/tsconfig.json +4 -0
@@ -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,
@@ -336,10 +339,15 @@ export class ConstructsMaker {
336
339
  const endTSTimer = logTimespan(`Generate Typescript for ${target.name}`);
337
340
  const generator = new TerraformProviderGenerator(this.code, schema, {
338
341
  importExtension: this.options.languageOptions?.importExtension,
342
+ targetVersions: this.options.targetVersions,
339
343
  });
340
344
  generator.generate(target);
341
345
 
342
346
  this.versions = { ...this.versions, ...generator.versions };
347
+ this.emittedResourceFolders = {
348
+ ...this.emittedResourceFolders,
349
+ ...generator.emittedResourceFolders,
350
+ };
343
351
  endTSTimer();
344
352
  }
345
353
 
@@ -595,6 +603,62 @@ export class ConstructsMaker {
595
603
  });
596
604
  }
597
605
 
606
+ /**
607
+ * Delete resource directories under `providers/` that this run did not emit, so each provider folder holds
608
+ * exactly the resources in the version it was generated against.
609
+ *
610
+ * Walks only providers present in `emittedResourceFolders`, keeping any provider this run skipped. Within those,
611
+ * removes every subdirectory not emitted and leaves provider-level files (`index.ts`, `lazy-index.ts`) in place.
612
+ * Deletes files, so it must run after `save()` has written the current run's output.
613
+ *
614
+ * TypeScript only: `providers/<name>/<resource>/` maps one-to-one onto the emitted index exports. Other languages
615
+ * co-locate providers and modules in the same folders, which makes a dropped resource indistinguishable from a
616
+ * hand-written construct.
617
+ */
618
+ public async removeOrphanedResourceFolders() {
619
+ if (!this.isJavascriptTarget) {
620
+ return;
621
+ }
622
+
623
+ for (const [provider, emittedFolders] of Object.entries(
624
+ this.emittedResourceFolders,
625
+ )) {
626
+ const providerFolder = path.resolve(
627
+ this.codeMakerOutdir,
628
+ "providers",
629
+ provider,
630
+ );
631
+
632
+ let entries: string[] = [];
633
+ try {
634
+ entries = await fs.readdir(providerFolder);
635
+ } catch (e) {
636
+ logger.debug(
637
+ `Could not list resource folders in '${providerFolder}': ${e}`,
638
+ );
639
+ continue;
640
+ }
641
+
642
+ const expected = new Set(emittedFolders);
643
+ for (const entry of entries) {
644
+ if (expected.has(entry)) {
645
+ continue;
646
+ }
647
+
648
+ const entryPath = path.resolve(providerFolder, entry);
649
+ if (!fs.statSync(entryPath).isDirectory()) {
650
+ // Leave index.ts / lazy-index.ts and any other provider-level files alone.
651
+ continue;
652
+ }
653
+
654
+ logger.debug(
655
+ `Removing orphaned resource folder '${entry}' from provider '${provider}': it was not emitted for the current provider version`,
656
+ );
657
+ await fs.remove(entryPath);
658
+ }
659
+ }
660
+ }
661
+
598
662
  // emits a constraints.json file with a map of the used provider fqpns and version constraints
599
663
  // this is used for caching purposes
600
664
  private emitConstraintsFile(
@@ -757,6 +821,7 @@ a NODE_OPTIONS variable, we won't override it. Hence, the provider generation mi
757
821
 
758
822
  if (this.isJavascriptTarget) {
759
823
  await this.save();
824
+ await this.removeOrphanedResourceFolders();
760
825
  }
761
826
 
762
827
  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 {
@@ -5,7 +5,11 @@ import path from "path";
5
5
  import { FQPN, parseFQPN, ProviderName } from "@cdktn/provider-schema";
6
6
  import { AttributeModel } from "./attribute-model";
7
7
  import { Struct, ConfigStruct } from "./struct";
8
- import { Schema } from "@cdktn/commons";
8
+ import {
9
+ Schema,
10
+ TerraformTargetVersions,
11
+ registryForTargetVersions,
12
+ } from "@cdktn/commons";
9
13
  import { ProviderFunctionsModel } from "./provider-function-model";
10
14
 
11
15
  // Limit is 1200 to prevent stack size error.
@@ -25,6 +29,7 @@ interface ResourceModelOptions {
25
29
  schema: Schema;
26
30
  terraformSchemaType: string;
27
31
  configStructName: string;
32
+ targetVersions?: TerraformTargetVersions;
28
33
  }
29
34
 
30
35
  type DefinitionPath = string;
@@ -35,6 +40,7 @@ export class ResourceModel {
35
40
  public baseName: string;
36
41
  public provider: ProviderName;
37
42
  public fqpn: FQPN;
43
+ private readonly targetVersions?: TerraformTargetVersions;
38
44
  public providerVersionConstraint?: string;
39
45
  public providerVersion?: string;
40
46
  public terraformProviderSource?: string;
@@ -68,6 +74,7 @@ export class ResourceModel {
68
74
  this.attributes = options.attributes;
69
75
  this.schema = options.schema;
70
76
  this.fqpn = options.fqpn;
77
+ this.targetVersions = options.targetVersions;
71
78
  this.provider = parseFQPN(options.fqpn).name;
72
79
  this.terraformProviderName = this.provider;
73
80
  this.fileName = options.fileName;
@@ -109,15 +116,26 @@ export class ResourceModel {
109
116
  }
110
117
 
111
118
  public get linkToDocs(): string {
112
- const { hostname, namespace, name } = parseFQPN(this.fqpn);
119
+ // Keyed off the project's declared targets, not the FQPN hostname: that
120
+ // hostname reflects whichever CLI fetched the schema, which would make the
121
+ // generated link differ between a terraform and a tofu `cdktn get`.
122
+ const registry = registryForTargetVersions(this.targetVersions);
123
+ const { namespace, name } = parseFQPN(this.fqpn);
113
124
  const version = this.providerVersion || "latest";
114
- const base = `https://${hostname}/providers/${namespace}/${name}/${version}/docs`;
115
- if (this.isProvider) return base;
116
- if (this.isDataSource)
117
- return `${base}/data-sources/${this.terraformDocName}`;
118
- if (this.isEphemeralResource)
119
- return `${base}/ephemeral-resources/${this.terraformDocName}`;
120
- return `${base}/resources/${this.terraformDocName}`;
125
+ const kind = this.isProvider
126
+ ? "provider"
127
+ : this.isDataSource
128
+ ? "data-source"
129
+ : this.isEphemeralResource
130
+ ? "ephemeral-resource"
131
+ : "resource";
132
+ return registry.docsUrl(
133
+ namespace,
134
+ name,
135
+ version,
136
+ kind,
137
+ this.terraformDocName,
138
+ );
121
139
  }
122
140
 
123
141
  public get isProvider(): boolean {
@@ -8,6 +8,7 @@ import {
8
8
  logger,
9
9
  ProviderSchema,
10
10
  TerraformProviderConstraint,
11
+ TerraformTargetVersions,
11
12
  } from "@cdktn/commons";
12
13
  import { FQPN, parseFQPN, ProviderName } from "@cdktn/provider-schema";
13
14
  import {
@@ -29,6 +30,8 @@ export interface TerraformProviderGeneratorOptions {
29
30
  * Defaults to "" (CommonJS).
30
31
  */
31
32
  readonly importExtension?: string;
33
+ /** Project's declared targetVersions; selects the docs registry. */
34
+ readonly targetVersions?: TerraformTargetVersions;
32
35
  }
33
36
 
34
37
  interface ProviderData {
@@ -107,12 +110,17 @@ export interface ProviderConstraints {
107
110
  }
108
111
 
109
112
  export class TerraformProviderGenerator {
110
- private resourceParser = new ResourceParser();
113
+ private resourceParser: ResourceParser;
111
114
  private resourceEmitter: ResourceEmitter;
112
115
  private structEmitter: StructEmitter;
113
116
  private providerFunctionsEmitter: ProviderFunctionsEmitter;
114
117
  private readonly importExtension: string;
115
118
  public versions: { [fqpn: string]: string | undefined } = {};
119
+ /**
120
+ * Resource directory names emitted per provider folder Used to prune directories
121
+ * left over from a previous provider version.
122
+ */
123
+ public emittedResourceFolders: { [provider: string]: string[] } = {};
116
124
 
117
125
  constructor(
118
126
  private readonly code: CodeMaker,
@@ -121,6 +129,7 @@ export class TerraformProviderGenerator {
121
129
  ) {
122
130
  this.code.indentation = 2;
123
131
  this.importExtension = options.importExtension ?? "";
132
+ this.resourceParser = new ResourceParser(options.targetVersions);
124
133
  this.resourceEmitter = new ResourceEmitter(this.code, this.importExtension);
125
134
  this.structEmitter = new StructEmitter(this.code, this.importExtension);
126
135
  this.providerFunctionsEmitter = new ProviderFunctionsEmitter(this.code);
@@ -298,6 +307,20 @@ export class TerraformProviderGenerator {
298
307
 
299
308
  this.emitIndexFile(name, files);
300
309
  this.emitLazyIndexFile(name, files);
310
+
311
+ // Record exactly what this run emitted, derived the same way the index
312
+ // files derive their exports, so pruning can never disagree with them.
313
+ this.emittedResourceFolders[name] = files.map((file) =>
314
+ this.resourceFolderName(name, file),
315
+ );
316
+ }
317
+
318
+ /**
319
+ * Map an emitted file path to the resource directory name it lives in, e.g.
320
+ * `providers/mongodbatlas/teams/index.ts` -> `teams`.
321
+ */
322
+ private resourceFolderName(provider: ProviderName, file: string): string {
323
+ return file.replace(`providers/${provider}/`, "").replace("/index.ts", "");
301
324
  }
302
325
 
303
326
  private emitProviderFunctions(
@@ -333,7 +356,7 @@ export class TerraformProviderGenerator {
333
356
  this.code.openFile(filePath);
334
357
  this.code.line("// generated by cdktn get");
335
358
  for (const file of files) {
336
- const dirName = file.replace(`${folder}/`, "").replace("/index.ts", "");
359
+ const dirName = this.resourceFolderName(provider, file);
337
360
  this.code.line(
338
361
  `export * as ${toCamelCase(dirName)} from './${dirName}/index${this.importExtension}';`,
339
362
  );
@@ -348,7 +371,7 @@ export class TerraformProviderGenerator {
348
371
  this.code.openFile(filePath);
349
372
  this.code.line("// generated by cdktn get");
350
373
  for (const file of files) {
351
- const dirName = file.replace(`${folder}/`, "").replace("/index.ts", "");
374
+ const dirName = this.resourceFolderName(provider, file);
352
375
  this.code.line(
353
376
  `Object.defineProperty(exports, '${toCamelCase(
354
377
  dirName,
@@ -11,6 +11,7 @@ import {
11
11
  isAttributeNestedType,
12
12
  isNestedTypeAttribute,
13
13
  Schema,
14
+ TerraformTargetVersions,
14
15
  } from "@cdktn/commons";
15
16
  import { ProviderName, FQPN, parseFQPN } from "@cdktn/provider-schema";
16
17
  import {
@@ -82,8 +83,17 @@ export function sanitizeClassOrNamespaceName(
82
83
  isProvider = false,
83
84
  ) {
84
85
  const resourceIsNamedProvider = !isProvider && baseName === "provider";
85
-
86
- if (isReservedClassOrNamespaceName(baseName) || resourceIsNamedProvider) {
86
+ // A resource named `<provider>_provider_functions` maps to the same
87
+ // providers/<provider>/provider-functions/ directory that the provider's
88
+ // functions submodule is emitted to, and would silently overwrite it.
89
+ const resourceIsNamedProviderFunctions =
90
+ !isProvider && baseName === "provider_functions";
91
+
92
+ if (
93
+ isReservedClassOrNamespaceName(baseName) ||
94
+ resourceIsNamedProvider ||
95
+ resourceIsNamedProviderFunctions
96
+ ) {
87
97
  return `${baseName}_resource`;
88
98
  } else {
89
99
  return baseName;
@@ -114,6 +124,7 @@ class Parser {
114
124
  constructor(
115
125
  private classNames: string[],
116
126
  private usedBaseNames: Map<string, string>,
127
+ private targetVersions?: TerraformTargetVersions,
117
128
  ) {}
118
129
 
119
130
  private uniqueClassName(className: string): string {
@@ -285,6 +296,7 @@ class Parser {
285
296
  terraformSchemaType,
286
297
  structs: this.structs,
287
298
  configStructName,
299
+ targetVersions: this.targetVersions,
288
300
  });
289
301
 
290
302
  return resourceModel;
@@ -718,6 +730,8 @@ export class ResourceParser {
718
730
  private uniqueBaseNames: Map<string, string> = new Map();
719
731
  private resources: Record<string, ResourceModel> = {};
720
732
 
733
+ constructor(private readonly targetVersions?: TerraformTargetVersions) {}
734
+
721
735
  public parse(
722
736
  fqpn: FQPN,
723
737
  type: string,
@@ -729,7 +743,11 @@ export class ResourceParser {
729
743
  return this.resources[type];
730
744
  }
731
745
 
732
- const parser = new Parser(this.uniqueClassnames, this.uniqueBaseNames);
746
+ const parser = new Parser(
747
+ this.uniqueClassnames,
748
+ this.uniqueBaseNames,
749
+ this.targetVersions,
750
+ );
733
751
  const resource = parser.resourceFrom(
734
752
  fqpn,
735
753
  type,
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",