KT-45789 Transitive npm dependencies

This commit is contained in:
Martynas Petuška
2022-06-29 12:52:30 +01:00
committed by teamcity
parent 5a2ec4a0d5
commit 3e6253cede
6 changed files with 82 additions and 12 deletions
@@ -5,11 +5,14 @@
package org.jetbrains.kotlin.gradle
import com.google.gson.Gson
import com.google.gson.JsonObject
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
import org.jetbrains.kotlin.gradle.testbase.*
import org.junit.jupiter.api.DisplayName
import java.util.zip.ZipFile
import kotlin.io.path.reader
import kotlin.test.assertNotNull
// TODO: This suite is failing with deprecation error on Gradle <7.0 versions
@@ -34,6 +37,14 @@ class KotlinJsIrLibraryGradlePluginIT : KGPBaseTest() {
assertFileInProjectExists("build/productionLibrary/js-library.js")
assertFileInProjectExists("build/productionLibrary/package.json")
assertFileInProjectExists("build/productionLibrary/main.js")
projectPath.resolve("build/productionLibrary/package.json").reader()
.use { Gson().fromJson(it, JsonObject::class.java) }
.getAsJsonObject("dependencies")
?.entrySet()?.associate { (k, v) -> k to v.asString }
.let { dependencies ->
assertNotNull(dependencies?.get("kotlin")) { "Direct npm dependency missing in package.json" }
assertNotNull(dependencies?.get("@js-joda/core")) { "Transitive npm dependency missing in package.json" }
}
}
}
}
@@ -77,4 +88,4 @@ class KotlinJsIrLibraryGradlePluginIT : KGPBaseTest() {
}
}
}
}
}
@@ -74,12 +74,11 @@ fun KGPBaseTest.project(
localRepoDir?.let { testProject.configureLocalRepository(localRepoDir) }
if (buildJdk != null) testProject.setupNonDefaultJdk(buildJdk)
runCatching {
val result = runCatching {
testProject.test()
}.onFailure {
// A convenient place to place a breakpoint to be able to inspect project output files
throw it
}
// A convenient place to place a breakpoint to be able to inspect project output files
result.getOrThrow()
return testProject
}
@@ -24,4 +24,4 @@ kotlin {
// We need to think about it, when we will support multiple binaries
tasks.named("nodeProductionLibraryPrepare") {
mustRunAfter("productionExecutableCompileSync")
}
}
@@ -15,4 +15,12 @@ kotlin {
binaries.library()
nodejs()
}
}
sourceSets {
main {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:latest.release")
implementation(npm("kotlin", "*"))
}
}
}
}
@@ -5,6 +5,8 @@
package org.jetbrains.kotlin.gradle.targets.js.npm
import com.google.gson.Gson
import com.google.gson.JsonObject
import java.io.File
import java.io.Serializable
@@ -14,4 +16,25 @@ import java.io.Serializable
data class GradleNodeModule(val name: String, val version: String, val path: File) : Serializable {
val semver: SemVer
get() = SemVer.from(version)
}
@get:Synchronized
val dependencies: Set<NpmDependencyDeclaration> by lazy {
val pJson = path.resolve("package.json").reader().use {
Gson().fromJson(it, JsonObject::class.java)
}
val normal = pJson.getAsJsonObject("dependencies")
val peer = pJson.getAsJsonObject("peerDependencies")
val optional = pJson.getAsJsonObject("optionalDependencies")
val dev = pJson.getAsJsonObject("devDependencies")
mapOf(
NpmDependency.Scope.NORMAL to normal,
NpmDependency.Scope.PEER to peer,
NpmDependency.Scope.OPTIONAL to optional,
NpmDependency.Scope.DEV to dev
).mapValues { (_, deps) ->
deps?.entrySet()?.associate { (k, v) -> k to v.asString }
}.mapNotNull { (scope, deps) ->
deps?.map { (k, v) -> NpmDependencyDeclaration(scope, k, v, false) }
}.flatten().toSet()
}
}
@@ -446,6 +446,9 @@ internal class KotlinCompilationNpmResolver(
)
}
}.filterNotNull()
val transitiveNpmDependencies = importedExternalGradleDependencies.flatMap {
it.dependencies
}.filter { it.scope != NpmDependency.Scope.DEV }
val compositeDependencies = internalCompositeDependencies.flatMap { dependency ->
dependency.getPackages()
@@ -456,8 +459,7 @@ internal class KotlinCompilationNpmResolver(
file
)
}
}
.filterNotNull()
}.filterNotNull()
val toolsNpmDependencies = compilationResolver.rootResolver.taskRequirements
.getCompilationNpmRequirements(projectPath, compilationResolver.compilationDisambiguatedName)
@@ -473,7 +475,8 @@ internal class KotlinCompilationNpmResolver(
)
} else emptySet()
val allNpmDependencies = externalNpmDependencies + toolsNpmDependencies + dukatIfNecessary
val otherNpmDependencies = toolsNpmDependencies + dukatIfNecessary + transitiveNpmDependencies
val allNpmDependencies = disambiguateDependencies(externalNpmDependencies, otherNpmDependencies)
val packageJsonHandlers = if (compilationResolver.compilation != null) {
compilationResolver.compilation.packageJsonHandlers
} else {
@@ -511,6 +514,32 @@ internal class KotlinCompilationNpmResolver(
)
}
private fun disambiguateDependencies(
direct: Collection<NpmDependencyDeclaration>,
others: Collection<NpmDependencyDeclaration>,
): Collection<NpmDependencyDeclaration> {
val unique = others.groupBy(NpmDependencyDeclaration::name)
.filterKeys { k -> direct.none { it.name == k } }
.mapNotNull { (name, dependencies) ->
dependencies.maxByOrNull { dep ->
SemVer.from(dep.version, true)
}?.also { selected ->
if (dependencies.size > 1) {
compilationResolver.project.logger.warn(
"""
Transitive npm dependency version clash for compilation "${compilationResolver.compilation.name}"
Candidates:
${dependencies.joinToString("\n") { "\t\t" + it.name + "@" + it.version }}
Selected:
${selected.name}@${selected.version}
""".trimIndent()
)
}
}
}
return direct + unique
}
private fun CompositeDependency.getPackages(): List<File> {
val packages = includedBuildDir.resolve(projectPackagesDir.relativeTo(rootDir))
return packages
@@ -520,4 +549,4 @@ internal class KotlinCompilationNpmResolver(
?: emptyList()
}
}
}
}