diff --git a/packages/@cdktf/hcl2cdk/lib/__tests__/jsii-rosetta-workarounds.test.ts b/packages/@cdktf/hcl2cdk/lib/__tests__/jsii-rosetta-workarounds.test.ts new file mode 100644 index 0000000000..56653c1d11 --- /dev/null +++ b/packages/@cdktf/hcl2cdk/lib/__tests__/jsii-rosetta-workarounds.test.ts @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc +// SPDX-License-Identifier: MPL-2.0 + +import { + replaceCsharpImports, + replaceGoImports, + replaceJavaImports, + replacePythonImports, +} from "../jsii-rosetta-workarounds"; + +describe("jsii-rosetta-workarounds", () => { + describe("replacePythonImports", () => { + it("normal imports", () => { + const code = ` +import constructs as constructs +# Provider bindings are generated by running cdktf get. +# See https://cdk.tf/provider-generation for more details. +import ...gen.providers.scaleway as scaleway +class MyConvertedCode(constructs.Construct): + def __init__(self, scope, name): + super().__init__(scope, name) + scaleway.provider.ScalewayProvider(self, "scaleway", + region="fr-par", + zone="fr-par-1" + )`; + + expect(replacePythonImports(code)).toEqual( + expect.stringContaining("import imports.scaleway as scaleway") + ); + }); + + it("granular imports", () => { + const code = `from ...gen.providers.aws.lib.rdsCluster import RdsCluster`; + + expect(replacePythonImports(code)).toEqual( + "from imports.aws.rdsCluster import RdsCluster" + ); + + const withoutLibCode = `from ...gen.providers.azurerm.resource_group import ResourceGroup`; + expect(replacePythonImports(withoutLibCode)).toEqual( + "from imports.azurerm.resource_group import ResourceGroup" + ); + }); + + it("resource imports", () => { + const code = `import ...gen.providers.aws.lib.rdsCluster as RdsCluster`; + + expect(replacePythonImports(code)).toEqual( + "import imports.aws.rdsCluster as RdsCluster" + ); + }); + + it("fixes module imports", () => { + const code = `import ...gen.modules.hello_module as HelloModule`; + + expect(replacePythonImports(code)).toEqual( + "import imports.hello_module as HelloModule" + ); + }); + }); + + describe("replaceCsharpImports", () => { + it("normal imports", () => { + const code = `using Gen.Providers.Aws.Lib.RdsCluster;`; + + expect(replaceCsharpImports(code)).toEqual("using aws.RdsCluster;"); + }); + + it("fixes module imports", () => { + const code = `using Gen.Modules.HelloModule`; + + expect(replaceCsharpImports(code)).toEqual("using HelloModule"); + + const anotherCode = `using Gen.Modules.Hello.Module`; + + expect(replaceCsharpImports(anotherCode)).toEqual("using Hello.Module"); + }); + }); + + describe("replaceJavaImports", () => { + it("normal imports", () => { + const code = `import gen.providers.scaleway.objectResource.*;`; + + expect(replaceJavaImports(code)).toEqual( + "import imports.scaleway.objectResource.*;" + ); + }); + + it("precise imports", () => { + const code = `import gen.providers.aws.lib.s3Bucket.S3Bucket;`; + + expect(replaceJavaImports(code)).toEqual( + "import imports.aws.s3Bucket.S3Bucket;" + ); + + const withoutLibImport = `import gen.providers.aws.s3Bucket.S3Bucket;`; + + expect(replaceJavaImports(withoutLibImport)).toEqual( + "import imports.aws.s3Bucket.S3Bucket;" + ); + }); + + it("fixes module imports", () => { + const code = `import gen.modules.hello.module.*;`; + + expect(replaceJavaImports(code)).toEqual( + "import imports.hello.module.*;" + ); + + const anotherCode = `import gen.modules.helloModule.*;`; + + expect(replaceJavaImports(anotherCode)).toEqual( + "import imports.helloModule.*;" + ); + }); + }); + + describe("replaceGoImports", () => { + it("normal imports", () => { + const code = `import "github.com/aws-samples/dummy/gen/providers/scaleway/objectBucket"`; + + expect(replaceGoImports(code)).toEqual( + `import "cdk.tf/go/stack/generated/scaleway/objectBucket"` + ); + }); + + it("precise imports", () => { + const code = `import "github.com/aws-samples/dummy/gen/providers/aws/lib/dbInstance"`; + + expect(replaceGoImports(code)).toEqual( + `import "cdk.tf/go/stack/generated/aws/dbInstance"` + ); + }); + + it("fixes module imports", () => { + const code = `import helloModule "github.com/aws-samples/dummy/gen/modules/hello_module"`; + + expect(replaceGoImports(code)).toEqual( + // "github.com/hashicorp/terraform-cdk/examples/go/documentation/generated/terraform-aws-modules/aws/vpc" + `import helloModule "cdk.tf/go/stack/generated/hello_module"` + ); + }); + }); +}); diff --git a/packages/@cdktf/hcl2cdk/lib/index.ts b/packages/@cdktf/hcl2cdk/lib/index.ts index ddb7ff82bb..e4370d9e0a 100644 --- a/packages/@cdktf/hcl2cdk/lib/index.ts +++ b/packages/@cdktf/hcl2cdk/lib/index.ts @@ -43,6 +43,12 @@ import { getProviderRequirements } from "./provider"; import { logger } from "./utils"; import { FQPN } from "@cdktf/provider-generator/lib/get/generator/provider-schema"; import { attributeNameToCdktfName } from "./generation"; +import { + replaceCsharpImports, + replaceGoImports, + replaceJavaImports, + replacePythonImports, +} from "./jsii-rosetta-workarounds"; export const CODE_MARKER = "// define resources here"; @@ -382,6 +388,7 @@ For a more precise conversion please use the --provider flag in convert.` // We split up the generated code so that users can have more control over what to insert where return { + // TODO: Remove imports and code because rosetta won't be able to translate them all: await gen([ ...constructImports, ...moduleImports(plan.module), @@ -405,9 +412,28 @@ For a more precise conversion please use the --provider flag in convert.` } type File = { contents: string; fileName: string }; +const translators = { + python: { + visitor: new rosetta.PythonVisitor(), + postTranslationMutation: replacePythonImports, + }, + java: { + visitor: new rosetta.JavaVisitor(), + postTranslationMutation: replaceJavaImports, + }, + csharp: { + visitor: new rosetta.CSharpVisitor(), + postTranslationMutation: replaceCsharpImports, + }, + go: { + visitor: new rosetta.GoVisitor(), + postTranslationMutation: replaceGoImports, + }, +}; -function translatorForVisitor(visitor: any) { +function translatorForLanguage(language: keyof typeof translators) { return (file: File, throwOnTranslationError: boolean) => { + const { visitor, postTranslationMutation } = translators[language]; const { translation, diagnostics } = rosetta.translateTypeScript( file, visitor, @@ -418,33 +444,23 @@ function translatorForVisitor(visitor: any) { throwOnTranslationError && diagnostics.filter((diag) => diag.isError).length > 0 ) { - logger.debug( - `Could not translate TS to ${visitor.language}:\n${file.contents}` - ); + logger.debug(`Could not translate TS to ${language}:\n${file.contents}`); throw new Error( - `Could not translate TS to ${visitor.language}: ${diagnostics + `Could not translate TS to ${language}: ${diagnostics .map((diag) => diag.formattedMessage) .join("\n")}` ); } - return translation; + return postTranslationMutation(translation); }; } -const translations = { - typescript: (file: File, _throwOnTranslationError: boolean) => file.contents, - python: translatorForVisitor(new rosetta.PythonVisitor()), - java: translatorForVisitor(new rosetta.JavaVisitor()), - csharp: translatorForVisitor(new rosetta.CSharpVisitor()), - go: translatorForVisitor(new rosetta.GoVisitor()), -}; - type ConvertOptions = { /** * The language to convert to */ - language: keyof typeof translations; + language: keyof typeof translators | "typescript"; /** * The provider schema to use for conversion */ @@ -470,7 +486,10 @@ export async function convert( }: ConvertOptions ) { const fileName = "terraform.tf"; - const translater = translations[language]; + const translater = + language === "typescript" + ? (file: File, _throwOnTranslationError: boolean) => file.contents + : translatorForLanguage(language); if (!translater) { throw new Error("Unsupported language used: " + language); diff --git a/packages/@cdktf/hcl2cdk/lib/jsii-rosetta-workarounds.ts b/packages/@cdktf/hcl2cdk/lib/jsii-rosetta-workarounds.ts new file mode 100644 index 0000000000..d9f86fee78 --- /dev/null +++ b/packages/@cdktf/hcl2cdk/lib/jsii-rosetta-workarounds.ts @@ -0,0 +1,145 @@ +// Copyright (c) HashiCorp, Inc +// SPDX-License-Identifier: MPL-2.0 + +export function replacePythonImports(code: string) { + return code + .split("\n") + .map((line) => { + // Replace from-import lines with lib + const fromImportLibRegex = + /from \.\.\.gen\.providers\.([^.]+)(?:\.lib)?\.(.*) import/; + if (fromImportLibRegex.test(line)) { + return line.replace(fromImportLibRegex, "from imports.$1.$2 import"); + } + + // Replace import lines with lib + const importLibRegex = + /import \.\.\.gen\.providers\.([^.]+)(?:\.lib)?\.(.*) as (.*)/; + if (importLibRegex.test(line)) { + return line.replace(importLibRegex, "import imports.$1.$2 as $3"); + } + + // Replace from-import lines + if (line.startsWith("from ...gen.providers.")) { + return line.replace("from ...gen.providers.", "from imports."); + } + // Replace import lines + if (line.startsWith("import ...gen.providers.")) { + return line.replace("import ...gen.providers.", "import imports."); + } + + // Replace modules + if (line.startsWith("import ...gen.modules.")) { + return line.replace("import ...gen.modules.", "import imports."); + } + + return line; + }) + .join("\n"); +} + +export function replaceJavaImports(code: string) { + return code + .split("\n") + .map((line) => { + // Replace using lines with lib and precices import + const importWithLib = + /import gen\.providers\.([^.]+)(?:\.lib)?\.([^.]+)\.(.*);/; + const matchWithLib = line.match(importWithLib); + if (matchWithLib) { + const [, provider, resource, className] = matchWithLib; + return `import imports.${provider}.${resource}.${className};`; + } + + // Replace using lines + const importWithoutLib = /import gen\.providers\.([^.]+)\.([^.]+)\.\*;/; + const matchWithoutLib = line.match(importWithoutLib); + if (matchWithoutLib) { + const [, provider, resource] = matchWithoutLib; + return `import imports.${provider}.${resource}.*;`; + } + + // Replace using lines + const importModules = /import gen\.modules\.(.+)\.\*;/; + const importModulesMatch = line.match(importModules); + if (importModulesMatch) { + const [, module] = importModulesMatch; + return `import imports.${module}.*;`; + } + + return line; + }) + .join("\n"); +} + +export function replaceCsharpImports(code: string) { + return code + .split("\n") + .map((line) => { + // Replace using lines with lib + const fromImportLibRegex = + /using Gen\.Providers\.([^.]*)(?:\.Lib)?\.(.*);/; + const match = line.match(fromImportLibRegex); + if (match) { + const [, provider, resource] = match; + return `using ${lowercaseFirstChar(provider)}.${resource};`; + } + + // Replace using lines + if (line.startsWith("using Gen.Providers.")) { + const importLine = line.replace("using Gen.Providers.", ""); + + return `using ${importLine + .substring(0, 1) + .toLocaleLowerCase()}${importLine.substring(1)}`; + } + + if (line.startsWith("using Gen.Modules.")) { + const importLine = line.replace("using Gen.Modules.", ""); + + return `using ${importLine}`; + } + + return line; + }) + .join("\n"); +} + +export function replaceGoImports(code: string) { + return code + .split("\n") + .map((line) => { + // Replace using lines with lib + const fromImportLibRegex = + /import \"github.com\/aws-samples\/dummy\/gen\/providers\/([^\/]*)(?:\/lib)?\/(.*)\"/; + const matchLib = line.match(fromImportLibRegex); + if (matchLib) { + const [, provider, resource] = matchLib; + return `import "cdk.tf/go/stack/generated/${provider}/${resource}"`; + } + + // Replace using lines + const fromImportRegex = + /import \"github.com\/aws-samples\/dummy\/gen\/providers\/([^\/]+)\/(.*)\"/; + const match = line.match(fromImportRegex); + if (match) { + const [, provider, resource] = match; + return `import "cdk.tf/go/stack/generated/${provider}/${resource}"`; + } + + const importModulesRegex = + /import (.*) \"github.com\/aws-samples\/dummy\/gen\/modules\/(.*)\"/; + const matchModules = line.match(importModulesRegex); + if (matchModules) { + const [, name, module] = matchModules; + return `import ${name} "cdk.tf/go/stack/generated/${module}"`; + } + + return line; + }) + .join("\n"); +} + +function lowercaseFirstChar(str: string) { + return str.substring(0, 1).toLocaleLowerCase() + str.substring(1); +} diff --git a/packages/@cdktf/hcl2cdk/test/__snapshots__/granular-imports.test.ts.snap b/packages/@cdktf/hcl2cdk/test/__snapshots__/granular-imports.test.ts.snap index d820865d05..29b0feb4af 100644 --- a/packages/@cdktf/hcl2cdk/test/__snapshots__/granular-imports.test.ts.snap +++ b/packages/@cdktf/hcl2cdk/test/__snapshots__/granular-imports.test.ts.snap @@ -7,8 +7,8 @@ using HashiCorp.Cdktf; * Provider bindings are generated by running \`cdktf get\`. * See https://cdk.tf/provider-generation for more details. */ -using Gen.Providers.Kubernetes.Deployment; -using Gen.Providers.Kubernetes.Provider; +using kubernetes.Deployment; +using kubernetes.Provider; class MyConvertedCode : TerraformStack { public MyConvertedCode(Construct scope, string name) : base(scope, name) @@ -76,8 +76,8 @@ import "github.com/hashicorp/terraform-cdk-go/cdktf" * Provider bindings are generated by running \`cdktf get\`. * See https://cdk.tf/provider-generation for more details. */ -import "github.com/aws-samples/dummy/gen/providers/kubernetes/deployment" -import "github.com/aws-samples/dummy/gen/providers/kubernetes/provider" +import "cdk.tf/go/stack/generated/kubernetes/deployment" +import "cdk.tf/go/stack/generated/kubernetes/provider" type myConvertedCode struct { terraformStack } @@ -154,8 +154,8 @@ import com.hashicorp.cdktf.TerraformStack; * Provider bindings are generated by running \`cdktf get\`. * See https://cdk.tf/provider-generation for more details. */ -import gen.providers.kubernetes.deployment.Deployment; -import gen.providers.kubernetes.provider.KubernetesProvider; +import imports.kubernetes.deployment.Deployment; +import imports.kubernetes.provider.KubernetesProvider; public class MyConvertedCode extends TerraformStack { public MyConvertedCode(Construct scope, String name) { super(scope, name); @@ -217,8 +217,8 @@ from cdktf import Token, TerraformStack # Provider bindings are generated by running \`cdktf get\`. # See https://cdk.tf/provider-generation for more details. # -from ...gen.providers.kubernetes.deployment import Deployment -from ...gen.providers.kubernetes.provider import KubernetesProvider +from imports.kubernetes.deployment import Deployment +from imports.kubernetes.provider import KubernetesProvider class MyConvertedCode(TerraformStack): def __init__(self, scope, name): super().__init__(scope, name) diff --git a/packages/@cdktf/hcl2cdk/test/__snapshots__/jsiiLanguage.test.ts.snap b/packages/@cdktf/hcl2cdk/test/__snapshots__/jsiiLanguage.test.ts.snap index 8042a45024..90064e7253 100644 --- a/packages/@cdktf/hcl2cdk/test/__snapshots__/jsiiLanguage.test.ts.snap +++ b/packages/@cdktf/hcl2cdk/test/__snapshots__/jsiiLanguage.test.ts.snap @@ -7,11 +7,11 @@ using HashiCorp.Cdktf; * Provider bindings are generated by running \`cdktf get\`. * See https://cdk.tf/provider-generation for more details. */ -using Gen.Providers.Aws.Kms.Key; -using Gen.Providers.Aws.Provider; -using Gen.Providers.Aws.S3.Bucket; -using Gen.Providers.Aws.S3.Bucket.Object; -using Gen.Providers.Aws.Security.Group; +using aws.Kms.Key; +using aws.Provider; +using aws.S3.Bucket; +using aws.S3.Bucket.Object; +using aws.Security.Group; class MyConvertedCode : TerraformStack { public MyConvertedCode(Construct scope, string name) : base(scope, name) @@ -73,11 +73,11 @@ import "github.com/hashicorp/terraform-cdk-go/cdktf" * Provider bindings are generated by running \`cdktf get\`. * See https://cdk.tf/provider-generation for more details. */ -import "github.com/aws-samples/dummy/gen/providers/aws/kmskey" -import "github.com/aws-samples/dummy/gen/providers/aws/provider" -import "github.com/aws-samples/dummy/gen/providers/aws/s3bucket" -import "github.com/aws-samples/dummy/gen/providers/aws/s3bucketobject" -import "github.com/aws-samples/dummy/gen/providers/aws/securitygroup" +import "cdk.tf/go/stack/generated/aws/kmskey" +import "cdk.tf/go/stack/generated/aws/provider" +import "cdk.tf/go/stack/generated/aws/s3bucket" +import "cdk.tf/go/stack/generated/aws/s3bucketobject" +import "cdk.tf/go/stack/generated/aws/securitygroup" type myConvertedCode struct { terraformStack } @@ -153,11 +153,11 @@ import com.hashicorp.cdktf.TerraformStack; * Provider bindings are generated by running \`cdktf get\`. * See https://cdk.tf/provider-generation for more details. */ -import gen.providers.aws.kms.key.KmsKey; -import gen.providers.aws.provider.AwsProvider; -import gen.providers.aws.s3.bucket.S3Bucket; -import gen.providers.aws.s3.bucket.object.S3BucketObject; -import gen.providers.aws.security.group.SecurityGroup; +import imports.aws.kms.key.KmsKey; +import imports.aws.provider.AwsProvider; +import imports.aws.s3.bucket.S3Bucket; +import imports.aws.s3.bucket.object.S3BucketObject; +import imports.aws.security.group.SecurityGroup; public class MyConvertedCode extends TerraformStack { public MyConvertedCode(Construct scope, String name) { super(scope, name); @@ -217,11 +217,11 @@ from cdktf import Token, TerraformStack # Provider bindings are generated by running \`cdktf get\`. # See https://cdk.tf/provider-generation for more details. # -from ...gen.providers.aws.kms_key import KmsKey -from ...gen.providers.aws.provider import AwsProvider -from ...gen.providers.aws.s3_bucket import S3Bucket -from ...gen.providers.aws.s3_bucket_object import S3BucketObject -from ...gen.providers.aws.security_group import SecurityGroup +from imports.aws.kms_key import KmsKey +from imports.aws.provider import AwsProvider +from imports.aws.s3_bucket import S3Bucket +from imports.aws.s3_bucket_object import S3BucketObject +from imports.aws.security_group import SecurityGroup class MyConvertedCode(TerraformStack): def __init__(self, scope, name): super().__init__(scope, name)