Gradle, js: support generating kotlin external declarations from .d.ts

#KT-31703 Fixed
This commit is contained in:
Sergey Rostov
2019-07-07 22:26:46 +03:00
parent 2fb4d23f1e
commit 3c606c27a1
16 changed files with 426 additions and 5 deletions
@@ -13,6 +13,8 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
* Package versions used by tasks
*/
class NpmVersions {
val dukat = NpmPackageVersion("dukat", "0.0.10")
val webpack = NpmPackageVersion("webpack", "4.29.6")
val webpackCli = NpmPackageVersion("webpack-cli", "3.3.0")
val webpackBundleAnalyzer = NpmPackageVersion("webpack-bundle-analyzer", "3.3.2")
@@ -0,0 +1,45 @@
/*
* Copyright 2010-2019 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.dukat
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
import java.io.File
class DtsResolver(val npmProject: NpmProject) {
private val typeModules = npmProject.modules.copy(
packageJsonEntries = listOf("types"),
indexFileSuffixes = listOf(".d.ts")
)
fun getAllDts(externalNpmDependencies: Collection<NpmDependency>): List<Dts> = externalNpmDependencies
.filter { it.scope != NpmDependency.Scope.DEV }
.flatMap { it.getDependenciesRecursively() }
.mapNotNullTo(mutableSetOf()) { typeModules.resolve(it.key)?.let { file -> Dts(file, it) } }
.sortedBy { it.inputKey }
class Dts(val file: File, val npmDependency: NpmDependency) {
val inputKey: String
get() = npmDependency.key + "@" + npmDependency.resolvedVersion + "#" + npmDependency.integrity
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Dts
if (file != other.file) return false
return true
}
override fun hashCode(): Int {
return file.hashCode()
}
override fun toString(): String = inputKey
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2019 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.dukat
import org.jetbrains.kotlin.gradle.plugin.mpp.disambiguateName
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.plugins.CompilationResolverPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinRootNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinCompilationNpmResolver
import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask
internal class DukatCompilationResolverPlugin(
private val resolver: KotlinCompilationNpmResolver
) : CompilationResolverPlugin {
val project get() = resolver.project
val nodeJs get() = resolver.nodeJs
val npmProject get() = resolver.npmProject
val compilation get() = npmProject.compilation
val taskName = npmProject.compilation.disambiguateName("generateExternals")
init {
compilation.defaultSourceSet.kotlin.srcDir(npmProject.externalsDir)
val task = project.createOrRegisterTask<PackageJsonDukatTask>(taskName) {
it.compilation = compilation
it.group = NodeJsRootPlugin.TASKS_GROUP_NAME
it.description = "Generate Kotlin/JS external declarations for .d.ts files in ${compilation}"
it.dependsOn(nodeJs.npmInstallTask, npmProject.packageJsonTask)
}
compilation.compileKotlinTask.dependsOn(task.getTaskOrProvider())
}
override fun hookDependencies(
internalDependencies: MutableSet<KotlinCompilationNpmResolver>,
externalGradleDependencies: MutableSet<KotlinCompilationNpmResolver.ExternalGradleDependency>,
externalNpmDependencies: MutableSet<NpmDependency>
) {
if (nodeJs.experimental.discoverTypes) {
// todo: discoverTypes
}
}
fun executeDukatIfNeeded(
packageJsonIsUpdated: Boolean,
resolution: KotlinRootNpmResolution
) {
val externalNpmDependencies = resolution[project][compilation].externalNpmDependencies
PackageJsonDukatExecutor(
nodeJs,
DtsResolver(npmProject).getAllDts(externalNpmDependencies),
npmProject,
packageJsonIsUpdated,
operation = compilation.name + " > " + PackageJsonDukatExecutor.OPERATION,
compareInputs = true
).execute()
}
companion object {
const val VERSION = "2"
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2010-2019 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.dukat
import org.jetbrains.kotlin.gradle.internal.execWithProgress
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import java.io.File
class DukatExecutor(
val compilation: KotlinJsCompilation,
val dTsFiles: Collection<File>,
val destDir: File,
val qualifiedPackageName: String? = null,
val jsInteropJvmEngine: String? = null,
val operation: String = "Generating Kotlin/JS external declarations"
) {
fun execute() {
compilation.target.project.execWithProgress(operation) { exec ->
val args = mutableListOf<String>()
val qualifiedPackageName = qualifiedPackageName
if (qualifiedPackageName != null) {
args.add("-p")
args.add(qualifiedPackageName)
}
args.add("-d")
args.add(destDir.absolutePath)
val jsInteropJvmEngine = jsInteropJvmEngine
if (jsInteropJvmEngine != null) {
args.add("-js")
args.add(jsInteropJvmEngine)
}
args.addAll(dTsFiles.map { it.absolutePath })
compilation.npmProject.useTool(exec, ".bin/dukat", *args.toTypedArray())
}
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2019 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.dukat
import org.jetbrains.kotlin.gradle.targets.js.npm.plugins.RootResolverPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinRootNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinCompilationNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver
internal class DukatRootResolverPlugin(val resolver: KotlinRootNpmResolver) : RootResolverPlugin {
val compilations = mutableListOf<DukatCompilationResolverPlugin>()
override fun createCompilationResolverPlugins(resolver: KotlinCompilationNpmResolver): List<DukatCompilationResolverPlugin> {
val plugin = DukatCompilationResolverPlugin(resolver)
compilations.add(plugin)
return listOf(plugin)
}
override fun close(resolution: KotlinRootNpmResolution) {
println("tests")
if (resolver.forceFullResolve) {
// inside idea import
compilations.forEach {
it.executeDukatIfNeeded(true, resolution)
}
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2010-2019 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.dukat
import org.gradle.api.internal.AbstractTask
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies
import java.io.File
abstract class AbstractDukatTask : AbstractTask(), RequiresNpmDependencies {
private val nodeJs get() = NodeJsRootPlugin.apply(project.rootProject)
override lateinit var compilation: KotlinJsCompilation
override val nodeModulesRequired: Boolean
get() = true
override val requiredNpmDependencies: Collection<RequiredKotlinJsDependency>
get() = listOf(nodeJs.versions.dukat)
/**
* Package name for the generated file (by default filename.d.ts renamed to filename.d.kt)
*/
@Input
@Optional
var qualifiedPackageName: String? = null
/**
* Collection of d.ts files
*/
abstract val dTsFiles: List<File>
/**
* Destination directory for files with converted declarations
*/
@get:OutputDirectory
abstract val destDir: File
val operation: String = "Generating Kotlin/JS external declarations"
@TaskAction
open fun run() {
nodeJs.npmResolutionManager.checkRequiredDependencies(this)
DukatExecutor(
compilation,
dTsFiles,
destDir,
qualifiedPackageName,
null,
operation
).execute()
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010-2019 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.dukat
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinRootNpmResolution
class PackageJsonDukatExecutor(
val nodeJs: NodeJsRootExtension,
val typeDefinitions: List<DtsResolver.Dts>,
val npmProject: NpmProject,
val packageJsonIsUpdated: Boolean,
val operation: String = OPERATION,
val compareInputs: Boolean = true
) {
companion object {
const val OPERATION = "Generating Kotlin/JS external declarations"
}
val versionFile = npmProject.externalsDirRoot.resolve("version.txt")
val version = DukatCompilationResolverPlugin.VERSION + ", " + nodeJs.versions.dukat.version
val prevVersion = if (versionFile.exists()) versionFile.readText() else null
val inputsFile = npmProject.externalsDirRoot.resolve("inputs.txt")
val shouldSkip: Boolean
get() = inputsFile.isFile && prevVersion == version && !packageJsonIsUpdated
fun execute() {
// delete file to run visit on error even without package.json updates
versionFile.delete()
npmProject.externalsDirRoot.mkdirs()
val inputs = typeDefinitions.joinToString("\n") { it.inputKey }
if (!compareInputs || !inputsFile.isFile || inputsFile.readText() != inputs) {
// delete file to run visit on error even without package.json updates
inputsFile.delete()
npmProject.externalsDir.deleteRecursively()
DukatExecutor(
npmProject.compilation,
typeDefinitions.map { it.file },
npmProject.externalsDir,
operation = operation
).execute()
inputsFile.writeText(inputs)
}
versionFile.writeText(version)
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2019 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.dukat
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import java.io.File
open class PackageJsonDukatTask : AbstractDukatTask() {
private val nodeJs get() = NodeJsRootPlugin.apply(project.rootProject)
val dts by lazy {
val resolvedCompilation = nodeJs.npmResolutionManager.requireInstalled()[project][compilation]
val dtsResolver = DtsResolver(resolvedCompilation.npmProject)
dtsResolver.getAllDts(resolvedCompilation.externalNpmDependencies)
}
@get:Internal
override val dTsFiles: List<File>
get() = dts.map { it.file }
@get:Input
val inputs
get() = dts.map { it.inputKey }
@get:OutputDirectory
override val destDir: File
get() = compilation.npmProject.externalsDir
private val executer by lazy {
PackageJsonDukatExecutor(nodeJs, dts, compilation.npmProject, true, compareInputs = false)
}
override fun run() {
executer.execute()
}
}
@@ -36,6 +36,13 @@ open class NodeJsRootExtension(val rootProject: Project) {
var packageManager: NpmApi = Yarn
class Experimental {
var generateKotlinExternals: Boolean = false
var discoverTypes: Boolean = false
}
val experimental = Experimental()
val nodeJsSetupTask: NodeJsSetupTask
get() = rootProject.tasks.getByName(NodeJsSetupTask.NAME) as NodeJsSetupTask
@@ -131,6 +131,7 @@ class KotlinNpmResolutionManager(val nodeJsSettings: NodeJsRootExtension) {
val forceUpToDate = upToDate && !forceFullResolve
state1.resolver.close(forceUpToDate).also {
this.state = ResolutionState.Installed(it)
state1.resolver.closePlugins(it)
}
}
}
@@ -55,7 +55,13 @@ open class NpmProject(val compilation: KotlinJsCompilation) {
val main: String
get() = "kotlin/$name.js"
private val modules = object : NpmProjectModules(dir, nodeModulesDir) {
val externalsDirRoot: File
get() = project.buildDir.resolve("externals").resolve(name)
val externalsDir: File
get() = externalsDirRoot.resolve("src")
internal val modules = object : NpmProjectModules(dir, nodeModulesDir) {
override val parent get() = rootNodeModules
}
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2019 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.npm.plugins
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinCompilationNpmResolver
internal interface CompilationResolverPlugin {
fun hookDependencies(
internalDependencies: MutableSet<KotlinCompilationNpmResolver>,
externalGradleDependencies: MutableSet<KotlinCompilationNpmResolver.ExternalGradleDependency>,
externalNpmDependencies: MutableSet<NpmDependency>
)
}
@@ -0,0 +1,14 @@
/*
* Copyright 2010-2019 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.npm.plugins
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinRootNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinCompilationNpmResolver
internal interface RootResolverPlugin {
fun createCompilationResolverPlugins(resolver: KotlinCompilationNpmResolver): List<CompilationResolverPlugin>
fun close(resolution: KotlinRootNpmResolution)
}
@@ -16,6 +16,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
import org.jetbrains.kotlin.gradle.plugin.usesPlatformOf
import org.jetbrains.kotlin.gradle.targets.js.dukat.DukatCompilationResolverPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson
import org.jetbrains.kotlin.gradle.targets.js.npm.fixSemver
@@ -23,6 +24,7 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinCompilationNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinPackageJsonTask
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency.Scope.*
import org.jetbrains.kotlin.gradle.targets.js.npm.plugins.CompilationResolverPlugin
import java.io.File
import java.io.Serializable
@@ -39,6 +41,9 @@ internal class KotlinCompilationNpmResolver(
val target get() = compilation.target
val project get() = target.project
val packageJsonTaskHolder = KotlinPackageJsonTask.create(compilation)
val plugins: List<CompilationResolverPlugin> = projectResolver.resolver.plugins.flatMap {
it.createCompilationResolverPlugins(this)
}
override fun toString(): String = "KotlinCompilationNpmResolver(${npmProject.name})"
@@ -151,6 +156,14 @@ internal class KotlinCompilationNpmResolver(
val main = compilation.target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) as KotlinJsCompilation
internalDependencies.add(projectResolver[main])
}
plugins.forEach {
it.hookDependencies(
internalDependencies,
externalGradleDependencies,
externalNpmDependencies
)
}
}
private fun visitDependency(dependency: ResolvedDependency) {
@@ -6,10 +6,8 @@
package org.jetbrains.kotlin.gradle.targets.js.npm.resolver
import org.gradle.api.Project
import org.gradle.api.Task
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.dsl.kotlinExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
@@ -17,7 +15,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinProjectNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinPackageJsonTask
/**
* See [KotlinNpmResolutionManager] for details about resolution process.
@@ -8,9 +8,11 @@ package org.jetbrains.kotlin.gradle.targets.js.npm.resolver
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.dukat.DukatRootResolverPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.npm.*
import org.jetbrains.kotlin.gradle.targets.js.npm.GradleNodeModulesCache
import org.jetbrains.kotlin.gradle.targets.js.npm.plugins.RootResolverPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinCompilationNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinRootNpmResolution
@@ -24,6 +26,12 @@ internal class KotlinRootNpmResolver internal constructor(
val rootProject: Project
get() = nodeJs.rootProject
val plugins = mutableListOf<RootResolverPlugin>().also {
if (nodeJs.experimental.generateKotlinExternals) {
it.add(DukatRootResolverPlugin(this))
}
}
private var closed: Boolean = false
val gradleNodeModules = GradleNodeModulesCache(nodeJs)
@@ -92,7 +100,15 @@ internal class KotlinRootNpmResolver internal constructor(
upToDateChecks.forEach { it.commit() }
return KotlinRootNpmResolution(rootProject, projectResolutions)
val resolution = KotlinRootNpmResolution(rootProject, projectResolutions)
return resolution
}
fun closePlugins(resolution: KotlinRootNpmResolution) {
plugins.forEach {
it.close(resolution)
}
}
private fun removeOutdatedPackages(nodeJs: NodeJsRootExtension, allNpmPackages: List<KotlinCompilationNpmResolution>) {