From 7f7aa9a921a021d9f3501697101111ff188c6ac6 Mon Sep 17 00:00:00 2001 From: Artem Kobzar Date: Thu, 28 Oct 2021 17:33:41 +0000 Subject: [PATCH] feat(Inner Classes): generate typescript type definitions and JS for Inner Classes. --- .../ir/backend/js/export/ExportModel.kt | 11 ++- .../backend/js/export/ExportModelGenerator.kt | 1 + .../js/export/ExportModelToJsStatements.kt | 74 +++++++++++++++++-- .../js/export/ExportModelToTsDeclarations.kt | 70 +++++++++++++++++- .../backend/js/lower/JsInnerClassesSupport.kt | 1 + js/js.tests/build.gradle.kts | 21 +++++- .../IrJsTypeScriptExportES6TestGenerated.java | 18 +++++ .../IrJsTypeScriptExportTestGenerated.java | 18 +++++ .../classes/inner-class.d.ts | 30 ++++++++ .../typescript-export/classes/inner-class.kt | 25 +++++++ .../classes/inner-class__main.js | 24 ++++++ .../classes/inner-class__main.ts | 30 ++++++++ .../typescript-export/classes/tsconfig.json | 4 + 13 files changed, 315 insertions(+), 12 deletions(-) create mode 100644 js/js.translator/testData/typescript-export/classes/inner-class.d.ts create mode 100644 js/js.translator/testData/typescript-export/classes/inner-class.kt create mode 100644 js/js.translator/testData/typescript-export/classes/inner-class__main.js create mode 100644 js/js.translator/testData/typescript-export/classes/inner-class__main.ts create mode 100644 js/js.translator/testData/typescript-export/classes/tsconfig.json diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt index 9bcdcaa5ce5..efe2b7ddf37 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt @@ -23,7 +23,7 @@ class ExportedNamespace( val declarations: List ) : ExportedDeclaration() -class ExportedFunction( +data class ExportedFunction( val name: String, val returnType: ExportedType, val parameters: List, @@ -35,11 +35,16 @@ class ExportedFunction( val ir: IrSimpleFunction ) : ExportedDeclaration() -class ExportedConstructor( +data class ExportedConstructor( val parameters: List, val isProtected: Boolean ) : ExportedDeclaration() +data class ExportedConstructSignature( + val parameters: List, + val returnType: ExportedType, +) : ExportedDeclaration() + class ExportedProperty( val name: String, val type: ExportedType, @@ -56,7 +61,7 @@ class ExportedProperty( // TODO: Cover all cases with frontend and disable error declarations class ErrorDeclaration(val message: String) : ExportedDeclaration() -class ExportedClass( +data class ExportedClass( val name: String, val isInterface: Boolean = false, val isAbstract: Boolean = false, diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt index 60f5d85867b..b250451f10f 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt @@ -187,6 +187,7 @@ class ExportModelGenerator( is IrField -> { assert( candidate.origin == IrDeclarationOrigin.FIELD_FOR_OBJECT_INSTANCE + || candidate.origin == IrDeclarationOrigin.FIELD_FOR_OUTER_THIS || candidate.correspondingPropertySymbol != null ) { "Unexpected field without property ${candidate.fqNameWhenAvailable}" diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt index 1ce255df5e2..e4aea9ef9da 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt @@ -8,8 +8,13 @@ package org.jetbrains.kotlin.ir.backend.js.export import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsAstUtils import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.defineProperty import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.jsAssignment +import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.prototypeOf import org.jetbrains.kotlin.ir.backend.js.utils.IrNamer +import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope +import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName +import org.jetbrains.kotlin.ir.util.companionObject import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.util.collectionUtils.filterIsInstanceAnd class ExportModelToJsStatements( private val namer: IrNamer, @@ -69,6 +74,7 @@ class ExportModelToJsStatements( } is ExportedConstructor -> emptyList() + is ExportedConstructSignature -> emptyList() is ExportedProperty -> { require(namespace != null) { "Only namespaced properties are allowed" } @@ -87,25 +93,81 @@ class ExportModelToJsStatements( if (namespace == null) { JsExport(name, alias = JsName(declaration.name, false)) } else { - jsAssignment( - newNameSpace, - JsNameRef(name) - ).makeStmt() + jsAssignment(newNameSpace, JsNameRef(name)).makeStmt() } // These are only used when exporting secondary constructors annotated with @JsName - val staticFunctions = declaration.members.filter { it is ExportedFunction && it.isStatic } + val staticFunctions = declaration.members + .filter { it is ExportedFunction && it.isStatic } + .takeIf { !declaration.ir.isInner }.orEmpty() // Nested objects are exported as static properties val staticProperties = declaration.members.mapNotNull { (it as? ExportedProperty)?.takeIf { it.isStatic } } + val innerClassesAssignments = declaration.nestedClasses + .filter { it.ir.isInner } + .map { it.generateInnerClassAssignment(declaration) } + val staticsExport = (staticFunctions + staticProperties + declaration.nestedClasses) .flatMap { generateDeclarationExport(it, newNameSpace) } - listOf(klassExport) + staticsExport + listOf(klassExport) + staticsExport + innerClassesAssignments } } } + + private fun ExportedClass.generateInnerClassAssignment(outerClass: ExportedClass): JsStatement { + val innerClassRef = namer.getNameForStaticDeclaration(ir).makeRef() + val outerClassRef = namer.getNameForStaticDeclaration(outerClass.ir).makeRef() + val companionObject = ir.companionObject() + val secondaryConstructors = members.filterIsInstanceAnd { it.isStatic } + val bindConstructor = JsName("__bind_constructor_", false) + + val blockStatements = mutableListOf( + JsVars(JsVars.JsVar(bindConstructor, innerClassRef.bindToThis())) + ) + + if (companionObject != null) { + val companionName = companionObject.getJsNameOrKotlinName().identifier + blockStatements.add( + jsAssignment( + JsNameRef(companionName, bindConstructor.makeRef()), + JsNameRef(companionName, innerClassRef), + ).makeStmt() + ) + } + + secondaryConstructors.forEach { + val currentFunRef = namer.getNameForStaticDeclaration(it.ir).makeRef() + val assignment = jsAssignment( + JsNameRef(it.name, bindConstructor.makeRef()), + currentFunRef.bindToThis() + ).makeStmt() + + blockStatements.add(assignment) + } + + blockStatements.add(JsReturn(bindConstructor.makeRef())) + + return defineProperty( + prototypeOf(outerClassRef), + name, + JsFunction( + emptyScope, + JsBlock(*blockStatements.toTypedArray()), + "inner class '$name' getter" + ), + null + ).makeStmt() + } + + private fun JsNameRef.bindToThis(): JsInvocation { + return JsInvocation( + JsNameRef("bind", this), + JsNullLiteral(), + JsThisRef() + ) + } } diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt index 832f6afcdec..8a8153bcd7d 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt @@ -5,7 +5,10 @@ package org.jetbrains.kotlin.ir.backend.js.export +import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName import org.jetbrains.kotlin.ir.backend.js.utils.sanitizeName +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.util.parentAsClass import org.jetbrains.kotlin.serialization.js.ModuleKind // TODO: Support module kinds other than plain @@ -73,12 +76,18 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin "${prefix}$visibility$keyword$name$renderedTypeParameters($renderedParameters): $renderedReturnType;" } + is ExportedConstructor -> { val visibility = if (isProtected) "protected " else "" val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) } "${visibility}constructor($renderedParameters);" } + is ExportedConstructSignature -> { + val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) } + "new($renderedParameters): ${returnType.toTypeScript(indent)};" + } + is ExportedProperty -> { val visibility = if (isProtected) "protected " else "" val keyword = when { @@ -98,7 +107,18 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin " $superInterfacesKeyword " + superInterfaces.joinToString(", ") { it.toTypeScript(indent) } } else "" - val membersString = members.joinToString("") { it.toTypeScript("$indent ") + "\n" } + val members = members.map { + if (!ir.isInner || it !is ExportedFunction || !it.isStatic) { + it + } else { + // Remove $outer argument from secondary constructors of inner classes + it.copy(parameters = it.parameters.drop(1)) + } + } + + val (innerClasses, nonInnerClasses) = nestedClasses.partition { it.ir.isInner } + val innerClassesProperties = innerClasses.map { it.toReadonlyProperty() } + val membersString = (members + innerClassesProperties).joinToString("") { it.toTypeScript("$indent ") + "\n" } // If there are no exported constructors, add a private constructor to disable default one val privateCtorString = @@ -117,12 +137,60 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin val bodyString = privateCtorString + membersString + indent + val nestedClasses = nonInnerClasses + innerClasses.map { it.withProtectedConstructors() } val klassExport = "$prefix$modifiers$keyword $name$renderedTypeParameters$superClassClause$superInterfacesClause {\n$bodyString}" val staticsExport = if (nestedClasses.isNotEmpty()) "\n" + ExportedNamespace(name, nestedClasses).toTypeScript(indent, prefix) else "" klassExport + staticsExport } } +fun IrClass.asNestedClassAccess(): String { + val name = getJsNameOrKotlinName().identifier + if (parent !is IrClass) return name + return "${parentAsClass.asNestedClassAccess()}.$name" +} + +fun ExportedClass.withProtectedConstructors(): ExportedClass { + return copy(members = members.map { + if (it !is ExportedConstructor || it.isProtected) { + it + } else { + it.copy(isProtected = true) + } + }) +} + +fun ExportedClass.toReadonlyProperty(): ExportedProperty { + val innerClassReference = ir.asNestedClassAccess() + val allPublicConstructors = members.asSequence() + .filterIsInstance() + .filterNot { it.isProtected } + .map { + ExportedConstructSignature( + parameters = it.parameters.drop(1), + returnType = ExportedType.TypeParameter(innerClassReference), + ) + } + .toList() + + val type = ExportedType.IntersectionType( + ExportedType.InlineInterfaceType(allPublicConstructors), + ExportedType.TypeOf(innerClassReference) + ) + + return ExportedProperty( + name = name, + type = type, + mutable = false, + isMember = true, + isStatic = false, + isAbstract = false, + isProtected = false, + irGetter = null, + irSetter = null + ) +} + fun ExportedParameter.toTypeScript(indent: String): String = "$name: ${type.toTypeScript(indent)}" diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsInnerClassesSupport.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsInnerClassesSupport.kt index 5ee4ebe7e5c..bfb0d95b1f4 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsInnerClassesSupport.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsInnerClassesSupport.kt @@ -75,6 +75,7 @@ class JsInnerClassesSupport(mapping: JsMapping, private val irFactory: IrFactory returnType = oldConstructor.returnType }.also { it.parent = oldConstructor.parent + it.annotations = oldConstructor.annotations } newConstructor.copyTypeParametersFrom(oldConstructor) diff --git a/js/js.tests/build.gradle.kts b/js/js.tests/build.gradle.kts index 001148a3caa..d7728295973 100644 --- a/js/js.tests/build.gradle.kts +++ b/js/js.tests/build.gradle.kts @@ -170,6 +170,20 @@ val unzipV8 by task { into(unpackedDir) } +val testDataDir = project(":js:js.translator").projectDir.resolve("testData") + +val installTsDependencies by task { + workingDir.set(testDataDir) + args.set(listOf("install")) +} + +val generateTypeScriptTests by task { + dependsOn(installTsDependencies) + + workingDir.set(testDataDir) + args.set(listOf("run", "generateTypeScriptTests")) +} + fun Test.setupV8() { dependsOn(unzipV8) val v8Path = unzipV8.get().destinationDir @@ -190,6 +204,11 @@ fun Test.setUpJsBoxTests(jsEnabled: Boolean, jsIrEnabled: Boolean) { inputs.files(rootDir.resolve("js/js.engines/src/org/jetbrains/kotlin/js/engine/repl.js")) dependsOn(":dist") + + if (!project.hasProperty("teamcity")) { + dependsOn(generateTypeScriptTests) + } + if (jsEnabled) { dependsOn(testJsRuntime) inputs.files(testJsRuntime) @@ -241,8 +260,6 @@ fun Test.setUpBoxTests() { } } -val testDataDir = project(":js:js.translator").projectDir.resolve("testData") - projectTest(parallel = true, jUnitMode = JUnitMode.Mix) { setUpJsBoxTests(jsEnabled = true, jsIrEnabled = true) systemProperty("kotlin.js.ir.pir", "false") diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsTypeScriptExportES6TestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsTypeScriptExportES6TestGenerated.java index 94fdf66a942..57379b2599d 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsTypeScriptExportES6TestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsTypeScriptExportES6TestGenerated.java @@ -30,6 +30,24 @@ public class IrJsTypeScriptExportES6TestGenerated extends AbstractIrJsTypeScript KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/typescript-export"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS_IR_ES6, true); } + @TestMetadata("js/js.translator/testData/typescript-export/classes") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Classes extends AbstractIrJsTypeScriptExportES6Test { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.JS_IR_ES6, testDataFilePath); + } + + public void testAllFilesPresentInClasses() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/typescript-export/classes"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS_IR_ES6, true); + } + + @TestMetadata("inner-class.kt") + public void testInner_class() throws Exception { + runTest("js/js.translator/testData/typescript-export/classes/inner-class.kt"); + } + } + @TestMetadata("js/js.translator/testData/typescript-export/constructors") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsTypeScriptExportTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsTypeScriptExportTestGenerated.java index 5726ca3d2ae..31d8629c971 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsTypeScriptExportTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsTypeScriptExportTestGenerated.java @@ -30,6 +30,24 @@ public class IrJsTypeScriptExportTestGenerated extends AbstractIrJsTypeScriptExp KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/typescript-export"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS_IR, true); } + @TestMetadata("js/js.translator/testData/typescript-export/classes") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Classes extends AbstractIrJsTypeScriptExportTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.JS_IR, testDataFilePath); + } + + public void testAllFilesPresentInClasses() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/typescript-export/classes"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS_IR, true); + } + + @TestMetadata("inner-class.kt") + public void testInner_class() throws Exception { + runTest("js/js.translator/testData/typescript-export/classes/inner-class.kt"); + } + } + @TestMetadata("js/js.translator/testData/typescript-export/constructors") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/js/js.translator/testData/typescript-export/classes/inner-class.d.ts b/js/js.translator/testData/typescript-export/classes/inner-class.d.ts new file mode 100644 index 00000000000..39f5d2b14df --- /dev/null +++ b/js/js.translator/testData/typescript-export/classes/inner-class.d.ts @@ -0,0 +1,30 @@ +declare namespace JS_TESTS { + type Nullable = T | null | undefined + namespace foo { + class TestInner { + constructor(a: string); + readonly a: string; + readonly Inner: { + new(a: string): TestInner.Inner; + } & typeof TestInner.Inner; + } + namespace TestInner { + class Inner { + protected constructor($outer: foo.TestInner, a: string); + readonly a: string; + readonly concat: string; + static fromNumber(a: number): foo.TestInner.Inner; + readonly SecondLayerInner: { + new(a: string): TestInner.Inner.SecondLayerInner; + } & typeof TestInner.Inner.SecondLayerInner; + } + namespace Inner { + class SecondLayerInner { + protected constructor($outer: foo.TestInner.Inner, a: string); + readonly a: string; + readonly concat: string; + } + } + } + } +} \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/classes/inner-class.kt b/js/js.translator/testData/typescript-export/classes/inner-class.kt new file mode 100644 index 00000000000..5f66bfd51bd --- /dev/null +++ b/js/js.translator/testData/typescript-export/classes/inner-class.kt @@ -0,0 +1,25 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// RUN_PLAIN_BOX_FUNCTION +// SKIP_MINIFICATION +// SKIP_NODE_JS +// IGNORE_BACKEND: JS +// INFER_MAIN_MODULE +// MODULE: JS_TESTS +// FILE: inner-class.kt + +package foo + +@JsExport +class TestInner(val a: String) { + inner class Inner(val a: String) { + val concat: String = this@TestInner.a + this.a + + @JsName("fromNumber") + constructor(a: Int): this(a.toString()) + + @JsName("SecondLayerInner") + inner class InnerInner(val a: String) { + val concat: String = this@TestInner.a + this@Inner.a + this.a + } + } +} \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/classes/inner-class__main.js b/js/js.translator/testData/typescript-export/classes/inner-class__main.js new file mode 100644 index 00000000000..6269ae6cb0e --- /dev/null +++ b/js/js.translator/testData/typescript-export/classes/inner-class__main.js @@ -0,0 +1,24 @@ +"use strict"; +var TestInner = JS_TESTS.foo.TestInner; +function assert(condition) { + if (!condition) { + throw "Assertion failed"; + } +} +function box() { + var outer = new TestInner("Hello "); + var inner = new outer.Inner("World"); + var innerFromNumber = outer.Inner.fromNumber(1654); + var secondInner = new inner.SecondLayerInner("!"); + assert(outer instanceof TestInner); + assert(inner instanceof TestInner.Inner); + assert(innerFromNumber instanceof TestInner.Inner); + assert(secondInner instanceof TestInner.Inner.SecondLayerInner); + assert(inner.a == "World"); + assert(inner.concat == "Hello World"); + assert(innerFromNumber.a == "1654"); + assert(innerFromNumber.concat == "Hello 1654"); + assert(secondInner.a == "!"); + assert(secondInner.concat == "Hello World!"); + return "OK"; +} diff --git a/js/js.translator/testData/typescript-export/classes/inner-class__main.ts b/js/js.translator/testData/typescript-export/classes/inner-class__main.ts new file mode 100644 index 00000000000..b26adc9955b --- /dev/null +++ b/js/js.translator/testData/typescript-export/classes/inner-class__main.ts @@ -0,0 +1,30 @@ +import TestInner = JS_TESTS.foo.TestInner; + +function assert(condition: boolean) { + if (!condition) { + throw "Assertion failed"; + } +} + +function box(): string { + const outer = new TestInner("Hello ") + const inner = new outer.Inner("World") + const innerFromNumber = outer.Inner.fromNumber(1654) + const secondInner = new inner.SecondLayerInner("!") + + assert(outer instanceof TestInner) + assert(inner instanceof TestInner.Inner) + assert(innerFromNumber instanceof TestInner.Inner) + assert(secondInner instanceof TestInner.Inner.SecondLayerInner) + + assert(inner.a == "World") + assert(inner.concat == "Hello World") + + assert(innerFromNumber.a == "1654") + assert(innerFromNumber.concat == "Hello 1654") + + assert(secondInner.a == "!") + assert(secondInner.concat == "Hello World!") + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/classes/tsconfig.json b/js/js.translator/testData/typescript-export/classes/tsconfig.json new file mode 100644 index 00000000000..17612c56c9d --- /dev/null +++ b/js/js.translator/testData/typescript-export/classes/tsconfig.json @@ -0,0 +1,4 @@ +{ + "include": [ "./*" ], + "extends": "../common.tsconfig.json" +} \ No newline at end of file