From 9a57c6ccc3ab8254269da7d31be151bab9464e5d Mon Sep 17 00:00:00 2001 From: Artem Kobzar Date: Fri, 29 Jul 2022 09:26:12 +0000 Subject: [PATCH] [K/JS] feat(Generated TS validation): add a new property and gradle task to validate generated by the IR compiler typescript declaration files. --- .../kotlin/gradle/Kotlin2JsGradlePluginIT.kt | 39 ++++++++- .../js-ir-validate-ts/build.gradle.kts | 16 ++++ .../js-ir-validate-ts/gradle.properties | 1 + .../js-ir-validate-ts/src/main/kotlin/main.kt | 15 ++++ .../build.gradle.kts | 28 +++++++ .../src/jsMain/main.kt | 5 ++ .../gradle/targets/js/NpmPackages.kt | 1 + .../kotlin/gradle/plugin/KotlinProperties.kt | 9 +++ .../kotlin/gradle/targets/js/NpmVersions.kt | 1 + .../js/ir/GeneratedTypeScriptValidation.kt | 16 ++++ .../kotlin/gradle/targets/js/ir/JsBinaries.kt | 11 +++ .../gradle/targets/js/ir/KotlinJsIrTarget.kt | 42 ++++++++-- .../js/typescript/TypeScriptValidationTask.kt | 79 +++++++++++++++++++ 13 files changed, 252 insertions(+), 11 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/build.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/gradle.properties create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/src/main/kotlin/main.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/build.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/src/jsMain/main.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/GeneratedTypeScriptValidation.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/typescript/TypeScriptValidationTask.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt index 032b94c478a..2ff54ddce85 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt @@ -33,9 +33,7 @@ import java.nio.file.Paths import java.util.zip.ZipFile import kotlin.io.path.* import kotlin.streams.toList -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.* @JsGradlePluginTests class Kotlin2JsIrGradlePluginIT : AbstractKotlin2JsGradlePluginIT(true) { @@ -252,6 +250,41 @@ class Kotlin2JsIrGradlePluginIT : AbstractKotlin2JsGradlePluginIT(true) { } } } + + @DisplayName("generated typescript declarations validation") + @GradleTest + fun testGeneratedTypeScriptDeclarationsValidation(gradleVersion: GradleVersion) { + project("js-ir-validate-ts", gradleVersion) { + buildGradleKts.appendText( + """ + |fun makeTypeScriptFileInvalid(mode: String) { + | val dts = projectDir.resolve("build/compileSync/main/" + mode + "Executable/kotlin/js-ir-validate-ts.d.ts") + | dts.appendText("\nlet invalidCode: unique symbol = Symbol()") + |} + | + |tasks.named("compileDevelopmentExecutableKotlinJs").configure { + | doLast { makeTypeScriptFileInvalid("development") } + |} + | + |tasks.named("compileProductionExecutableKotlinJs").configure { + | doLast { makeTypeScriptFileInvalid("production") } + |} + """.trimMargin() + ) + + buildAndFail("compileDevelopmentExecutableKotlinJs") { + assertTasksFailed(":developmentExecutableValidateGeneratedByCompilerTypeScript") + assertFileInProjectExists("build/js/packages/js-ir-validate-ts/kotlin/js-ir-validate-ts.js") + assertFileInProjectExists("build/js/packages/js-ir-validate-ts/kotlin/js-ir-validate-ts.d.ts") + } + + build("compileProductionExecutableKotlinJs") { + assertTasksExecuted(":productionExecutableValidateGeneratedByCompilerTypeScript") + assertFileInProjectExists("build/js/packages/js-ir-validate-ts/kotlin/js-ir-validate-ts.js") + assertFileInProjectExists("build/js/packages/js-ir-validate-ts/kotlin/js-ir-validate-ts.d.ts") + } + } + } } @JsGradlePluginTests diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/build.gradle.kts new file mode 100644 index 00000000000..75f7596eb3f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("js") +} + +repositories { + mavenLocal() + mavenCentral() +} + +kotlin { + js(IR) { + binaries.executable() + browser { + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/gradle.properties b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/gradle.properties new file mode 100644 index 00000000000..991733675e5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/gradle.properties @@ -0,0 +1 @@ +kotlin.js.ir.development.typescript.validation.strategy=error diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/src/main/kotlin/main.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/src/main/kotlin/main.kt new file mode 100644 index 00000000000..30c13f47715 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-ir-validate-ts/src/main/kotlin/main.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package example + +@JsExport +class Child { + val name: String = "Child" +} + +fun main() { + println("Hello there!") +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/build.gradle.kts new file mode 100644 index 00000000000..9306b2711f9 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + kotlin("multiplatform") + `maven-publish` +} + +repositories { + mavenLocal() + mavenCentral() +} + +kotlin { + js(IR) { + browser { + commonWebpackConfig { + cssSupport { + enabled = true + } + scssSupport { + enabled = true + } + } + } + } +} + +dependencies { + "jsMainImplementation"(npm("decamelize", "4.0.0", generateExternals = true)) +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/src/jsMain/main.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/src/jsMain/main.kt new file mode 100644 index 00000000000..1be2bb8692e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/js-library-ir-validate-ts/src/jsMain/main.kt @@ -0,0 +1,5 @@ +package example + +fun main() { + println("Hello there!") +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-npm-versions-codegen/src/main/kotlin/org/jetbrains/kotlin/generators/gradle/targets/js/NpmPackages.kt b/libraries/tools/kotlin-gradle-plugin-npm-versions-codegen/src/main/kotlin/org/jetbrains/kotlin/generators/gradle/targets/js/NpmPackages.kt index 3961349dc8c..3e2b58cda82 100644 --- a/libraries/tools/kotlin-gradle-plugin-npm-versions-codegen/src/main/kotlin/org/jetbrains/kotlin/generators/gradle/targets/js/NpmPackages.kt +++ b/libraries/tools/kotlin-gradle-plugin-npm-versions-codegen/src/main/kotlin/org/jetbrains/kotlin/generators/gradle/targets/js/NpmPackages.kt @@ -36,6 +36,7 @@ val npmPackages = listOf( NpmPackage("karma-coverage"), NpmPackage("karma-sourcemap-loader"), NpmPackage("format-util"), + NpmPackage("typescript"), ) data class NpmPackage( diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt index 4cbb26a20aa..48af087b1e4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService import org.jetbrains.kotlin.gradle.targets.js.dukat.ExternalsOutputFormat import org.jetbrains.kotlin.gradle.targets.js.dukat.ExternalsOutputFormat.Companion.externalsOutputFormatProperty +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinIrJsGeneratedTSValidationStrategy import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrOutputGranularity import org.jetbrains.kotlin.gradle.targets.js.webpack.WebpackMajorVersion import org.jetbrains.kotlin.gradle.targets.native.DisabledNativeTargetsReporter @@ -118,6 +119,14 @@ internal class PropertiesProvider private constructor(private val project: Proje get() = property("kotlin.js.ir.output.granularity")?.let { KotlinJsIrOutputGranularity.byArgument(it) } ?: KotlinJsIrOutputGranularity.PER_MODULE + val jsIrGeneratedTypeScriptValidationDevStrategy: KotlinIrJsGeneratedTSValidationStrategy + get() = property("kotlin.js.ir.development.typescript.validation.strategy")?.let { KotlinIrJsGeneratedTSValidationStrategy.byArgument(it) } + ?: KotlinIrJsGeneratedTSValidationStrategy.IGNORE + + val jsIrGeneratedTypeScriptValidationProdStrategy: KotlinIrJsGeneratedTSValidationStrategy + get() = property("kotlin.js.ir.production.typescript.validation.strategy")?.let { KotlinIrJsGeneratedTSValidationStrategy.byArgument(it) } + ?: KotlinIrJsGeneratedTSValidationStrategy.IGNORE + val incrementalMultiplatform: Boolean? get() = booleanProperty("kotlin.incremental.multiplatform") diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/NpmVersions.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/NpmVersions.kt index 85e26658e63..bfed381bf2a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/NpmVersions.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/NpmVersions.kt @@ -42,6 +42,7 @@ class NpmVersions : Serializable { val karmaCoverage = NpmPackageVersion("karma-coverage", "2.2.0") val karmaSourcemapLoader = NpmPackageVersion("karma-sourcemap-loader", "0.3.8") val formatUtil = NpmPackageVersion("format-util", "1.0.5") + val typeScript = NpmPackageVersion("typescript", "4.7.4") val kotlinJsTestRunner = KotlinGradleNpmPackage("test-js-runner") } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/GeneratedTypeScriptValidation.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/GeneratedTypeScriptValidation.kt new file mode 100644 index 00000000000..c97a121d62f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/GeneratedTypeScriptValidation.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.targets.js.ir + +enum class KotlinIrJsGeneratedTSValidationStrategy { + ERROR, + IGNORE; + + companion object { + fun byArgument(argument: String): KotlinIrJsGeneratedTSValidationStrategy? = + values().firstOrNull { it.name.equals(argument, ignoreCase = true) } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/JsBinaries.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/JsBinaries.kt index 12c3f5f5eae..e4a07a71026 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/JsBinaries.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/JsBinaries.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.Distribution import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsBinaryContainer.Companion.generateBinaryName import org.jetbrains.kotlin.gradle.targets.js.subtargets.DefaultDistribution +import org.jetbrains.kotlin.gradle.targets.js.typescript.TypeScriptValidationTask import org.jetbrains.kotlin.gradle.tasks.withType import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName @@ -50,6 +51,8 @@ sealed class JsIrBinary( val linkSyncTaskName: String = linkSyncTaskName() + val validateGeneratedTsTaskName: String = validateTypeScriptTaskName() + val linkSyncTask: TaskProvider get() = target.project.tasks .withType() @@ -63,6 +66,14 @@ sealed class JsIrBinary( COMPILE_SYNC ) + private fun validateTypeScriptTaskName(): String = + lowerCamelCaseName( + compilation.target.disambiguationClassifier, + compilation.name.takeIf { it != KotlinCompilation.MAIN_COMPILATION_NAME }, + name, + TypeScriptValidationTask.NAME + ) + val target: KotlinTarget get() = compilation.target diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt index aed43193822..522bb39efaa 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt @@ -23,6 +23,7 @@ import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenExec import org.jetbrains.kotlin.gradle.targets.js.dsl.* import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject +import org.jetbrains.kotlin.gradle.targets.js.typescript.TypeScriptValidationTask import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask import org.jetbrains.kotlin.gradle.tasks.registerTask import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName @@ -42,6 +43,7 @@ constructor( KotlinWasmTargetDsl, KotlinJsSubTargetContainerDsl, KotlinWasmSubTargetContainerDsl { + private val propertiesProvider = PropertiesProvider(project) override lateinit var testRuns: NamedDomainObjectContainer internal set @@ -115,6 +117,7 @@ constructor( .withType(JsIrBinary::class.java) .all { binary -> val syncTask = registerCompileSync(binary) + val tsValidationTask = registerTypeScriptCheckTask(binary) binary.linkTask.configure { it.kotlinOptions.outputFile = project.buildDir @@ -125,7 +128,12 @@ constructor( .canonicalPath it.finalizedBy(syncTask) + + if (tsValidationTask != null) { + it.finalizedBy(tsValidationTask) + } } + } } } @@ -133,16 +141,10 @@ constructor( private fun registerCompileSync(binary: JsIrBinary): TaskProvider { val compilation = binary.compilation val npmProject = compilation.npmProject - return project.registerTask( + return project.registerTask( binary.linkSyncTaskName ) { task -> - task.from( - project.layout.file( - binary.linkTask.flatMap { linkTask -> - linkTask.normalizedDestinationDirectory.map { it.asFile } - } - ) - ) + task.from(project.layout.file(binary.linkTask.normalizedDestinationDirectory)) task.from(project.tasks.named(compilation.processResourcesTaskName)) @@ -163,6 +165,30 @@ constructor( } } + private fun registerTypeScriptCheckTask(binary: JsIrBinary): TaskProvider? { + val linkTask = binary.linkTask + val compilation = binary.compilation + return if (compilation.name == KotlinCompilation.TEST_COMPILATION_NAME) { + null + } else { + project.registerTask(binary.validateGeneratedTsTaskName, listOf(compilation)) { + it.inputDir.set(project.layout.dir(linkTask.normalizedDestinationDirectory)) + it.validationStrategy.set( + when (binary.mode) { + KotlinJsBinaryMode.DEVELOPMENT -> propertiesProvider.jsIrGeneratedTypeScriptValidationDevStrategy + KotlinJsBinaryMode.PRODUCTION -> propertiesProvider.jsIrGeneratedTypeScriptValidationProdStrategy + } + ) + } + } + } + + private val TaskProvider.normalizedDestinationDirectory + get() = + flatMap { linkTask -> + linkTask.normalizedDestinationDirectory.map { it.asFile } + } + //Binaryen private val applyBinaryenHandlers = mutableListOf<(BinaryenExec.() -> Unit) -> Unit>() diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/typescript/TypeScriptValidationTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/typescript/TypeScriptValidationTask.kt new file mode 100644 index 00000000000..518b95090f0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/typescript/TypeScriptValidationTask.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.targets.js.typescript + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.* +import org.gradle.api.provider.Property +import org.gradle.api.file.DirectoryProperty +import org.jetbrains.kotlin.gradle.internal.execWithProgress +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation +import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinIrJsGeneratedTSValidationStrategy +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin +import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies +import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject +import javax.inject.Inject + +abstract class TypeScriptValidationTask +@Inject +constructor( + @Internal + @Transient + override val compilation: KotlinJsCompilation +) : DefaultTask(), RequiresNpmDependencies { + private val npmProject = compilation.npmProject + + @get:Internal + @Transient + protected val nodeJs = NodeJsRootPlugin.apply(project.rootProject) + + @get:Internal + override val nodeModulesRequired: Boolean get() = false + + @get:Internal + override val requiredNpmDependencies: Set + get() = setOf(nodeJs.versions.typeScript) + + @get:SkipWhenEmpty + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val inputDir: DirectoryProperty + + @get:Input + abstract val validationStrategy: Property + + private val generatedDts + get() = inputDir.asFileTree.matching { it.include("*.d.ts") }.files + + @TaskAction + fun run() { + val validationStrategy = validationStrategy.get() + + if (validationStrategy == KotlinIrJsGeneratedTSValidationStrategy.IGNORE) return + + val files = generatedDts.map { it.absolutePath } + + if (files.isEmpty()) return + + val result = services.execWithProgress("typescript") { + npmProject.useTool(it, "typescript/bin/tsc", listOf(), listOf("--noEmit")) + } + + if (result.exitValue == 0) return + + val message = "Oops, Kotlin/JS compiler generated invalid d.ts files." + + when (validationStrategy) { + KotlinIrJsGeneratedTSValidationStrategy.ERROR -> error(message) + KotlinIrJsGeneratedTSValidationStrategy.IGNORE -> {} + } + } + + companion object { + const val NAME: String = "validateGeneratedByCompilerTypeScript" + } +} \ No newline at end of file