[K/JS] feat(Generated TS validation): add a new property and gradle task to validate generated by the IR compiler typescript declaration files.

This commit is contained in:
Artem Kobzar
2022-07-29 09:26:12 +00:00
committed by Space
parent 20bc46e06e
commit 9a57c6ccc3
13 changed files with 252 additions and 11 deletions
@@ -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<org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrLink>("compileDevelopmentExecutableKotlinJs").configure {
| doLast { makeTypeScriptFileInvalid("development") }
|}
|
|tasks.named<org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrLink>("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
@@ -0,0 +1,16 @@
plugins {
kotlin("js")
}
repositories {
mavenLocal()
mavenCentral()
}
kotlin {
js(IR) {
binaries.executable()
browser {
}
}
}
@@ -0,0 +1 @@
kotlin.js.ir.development.typescript.validation.strategy=error
@@ -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!")
}
@@ -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))
}
@@ -0,0 +1,5 @@
package example
fun main() {
println("Hello there!")
}
@@ -36,6 +36,7 @@ val npmPackages = listOf(
NpmPackage("karma-coverage"),
NpmPackage("karma-sourcemap-loader"),
NpmPackage("format-util"),
NpmPackage("typescript"),
)
data class NpmPackage(
@@ -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")
@@ -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")
}
@@ -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) }
}
}
@@ -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<Copy>
get() = target.project.tasks
.withType<Copy>()
@@ -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
@@ -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<KotlinJsReportAggregatingTestRun>
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<Copy> {
val compilation = binary.compilation
val npmProject = compilation.npmProject
return project.registerTask(
return project.registerTask<Copy>(
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<TypeScriptValidationTask>? {
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<KotlinJsIrLink>.normalizedDestinationDirectory
get() =
flatMap { linkTask ->
linkTask.normalizedDestinationDirectory.map { it.asFile }
}
//Binaryen
private val applyBinaryenHandlers = mutableListOf<(BinaryenExec.() -> Unit) -> Unit>()
@@ -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<RequiredKotlinJsDependency>
get() = setOf(nodeJs.versions.typeScript)
@get:SkipWhenEmpty
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputDir: DirectoryProperty
@get:Input
abstract val validationStrategy: Property<KotlinIrJsGeneratedTSValidationStrategy>
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"
}
}